Add article view

* avandu.el: Add Tiny Tiny RSS url to Commentary.

  (avandu-article-title):
  (avandu-article-author): New faces.

  (avandu-html2text-command): New user option.

  (avandu-article-button-map): Wrap a lambda around the call to
  `avandu-mark-article-read'.

  (avu-prop): New macro.

  (avandu--get-session-id):
  (avandu--get-status-id):
  (avandu--send-command):
  (avandu-logged-in-p):
  (avandu-new-articles-count):
  (avandu-tt-rss-api-level):
  (avandu-tt-rss-version):
  (avandu-overview): Use `avu-prop'.

  (avandu--insert-article-title): Show the article screen when
  activating an article button instead of showing its URL.

  (avandu-categories):
  (avandu-feeds):
  (avandu-headlines): No more need to call `cdr' and `assq' on the
  result of `avandu--send-command'.

  (avandu-get-article): New function.

  (avandu-mark-article-read): Just send a command to the server, don't
  do anything with the UI.

  (avandu-ui-mark-article-read): New function.  Split off from
  `avandu-mark-article-read'.

  (avandu-article-mode): New major mode.

  (avandu-view-article): New function.
This commit is contained in:
Tom Willemsen 2012-08-22 12:44:50 +02:00
parent 93461d87cd
commit 6d056c6a63

156
avandu.el
View file

@ -23,8 +23,9 @@
;;; Commentary: ;;; Commentary:
;; Avandu is an emacs mode that connects to a Tiny Tiny RSS instance ;; Avandu is an emacs mode that connects to a Tiny Tiny RSS
;; and allows you to read the feeds it has gathered locally. ;; (http://tt-rss.org) instance and allows you to read the feeds it
;; has gathered.
;; The simplest way to install it is to use package.el: ;; The simplest way to install it is to use package.el:
@ -105,6 +106,21 @@
"Face for unread article titles in avandu overview." "Face for unread article titles in avandu overview."
:group 'avandu) :group 'avandu)
(defface avandu-article-title
'((((class color)
(background dark))
(:foreground "orange3" :weight bold :family "sans"))
(((class color)
(background light))
(:foreground "red4" :weight bold :family "sans")))
"Face for titles in avandu article view."
:group 'avandu)
(defface avandu-article-author
'((t (:inherit shadow :slant italic :height 0.9)))
"Face for the author's name in avandu article view."
:group 'avandu)
;; User options ;; User options
(defcustom avandu-tt-rss-api-url nil (defcustom avandu-tt-rss-api-url nil
"URL of your Tiny Tiny RSS instance. For example: "URL of your Tiny Tiny RSS instance. For example:
@ -117,6 +133,11 @@
:group 'avandu :group 'avandu
:type 'string) :type 'string)
(defcustom avandu-html2text-command nil
"Shell command to call to change HTML to plain text."
:group 'avandu
:type 'string)
;; Variables ;; Variables
(defvar avandu--session-id nil (defvar avandu--session-id nil
"*internal* Session id for avandu.") "*internal* Session id for avandu.")
@ -125,7 +146,12 @@
(let ((map (make-sparse-keymap))) (let ((map (make-sparse-keymap)))
(set-keymap-parent map button-map) (set-keymap-parent map button-map)
(define-key map "o" 'avandu-browse-article) (define-key map "o" 'avandu-browse-article)
(define-key map "r" 'avandu-mark-article-read) (define-key map "r" #'(lambda ()
(interactive)
(let ((button (button-at (point))))
(avandu-mark-article-read
(button-get button 'article-id))
(avandu-ui-mark-article-read button))))
map) map)
"Keymap for articles in `avandu-overview-mode'.") "Keymap for articles in `avandu-overview-mode'.")
@ -182,6 +208,10 @@
`(or ,var (setq ,var (,(if passwdp 'read-passwd 'read-string) `(or ,var (setq ,var (,(if passwdp 'read-passwd 'read-string)
,prompt)))) ,prompt))))
(defmacro avu-prop (element property)
"Get PROPERTY from ELEMENT."
`(cdr (assq (quote ,property) ,element)))
;; Internal ;; Internal
(defun avandu--check-login () (defun avandu--check-login ()
"Check to see if we're (still) logged in, try to login "Check to see if we're (still) logged in, try to login
@ -249,11 +279,11 @@ with `auth-source-search' and then by asking the user."
(defun avandu--get-session-id (results) (defun avandu--get-session-id (results)
"Get the session id from RESULTS." "Get the session id from RESULTS."
(cdr (assq 'session_id (assq 'content results)))) (avu-prop (assq 'content results) session_id))
(defun avandu--get-status-id (results) (defun avandu--get-status-id (results)
"Get the status id from RESULTS." "Get the status id from RESULTS."
(cdr (assq 'status results))) (avu-prop results status))
(defun avandu--insert-article-excerpt (excerpt) (defun avandu--insert-article-excerpt (excerpt)
"Insert the excerpt of an article." "Insert the excerpt of an article."
@ -280,7 +310,8 @@ the article-id and link properties, respectively."
'link link 'link link
'keymap avandu-article-button-map 'keymap avandu-article-button-map
'action #'(lambda (button) 'action #'(lambda (button)
(message "%s" (button-get button 'link)))) (avandu-view-article (button-get button 'article-id))))
(fill-region pos (point)) (fill-region pos (point))
(insert-char ?\n 1))) (insert-char ?\n 1)))
@ -337,15 +368,14 @@ returned json."
(search-forward "\n\n") (search-forward "\n\n")
(setq result (json-read))) (setq result (json-read)))
(kill-buffer buffer) (kill-buffer buffer)
result)) (avu-prop result content)))
(defun avandu-categories (&optional unread) (defun avandu-categories (&optional unread)
"Get the created categories. If UNREAD is non-nil only get "Get the created categories. If UNREAD is non-nil only get
categories with feeds with unread articles in them." categories with feeds with unread articles in them."
(cdr (assq 'content
(avandu--send-command (avandu--send-command
`((op . "getCategories") `((op . "getCategories")
,@(when unread `((unread_only . ,unread)))))))) ,@(when unread `((unread_only . ,unread))))))
(defun avandu-feeds (&optional category unread limit offset) (defun avandu-feeds (&optional category unread limit offset)
"Get the subscribed feeds. If CATEGORY has been specified show "Get the subscribed feeds. If CATEGORY has been specified show
@ -359,13 +389,12 @@ There are a number of special category IDs:
-2 -- Labels -2 -- Labels
-3 -- All feeds, excluding virtual feeds (e.g. Labels and such) -3 -- All feeds, excluding virtual feeds (e.g. Labels and such)
-4 -- All feeds, including virtual feeds" -4 -- All feeds, including virtual feeds"
(cdr (assq 'content
(avandu--send-command (avandu--send-command
`((op . "getFeeds") `((op . "getFeeds")
,@(when category `((cat_id . ,category))) ,@(when category `((cat_id . ,category)))
,@(when unread `((unread_only . ,unread))) ,@(when unread `((unread_only . ,unread)))
,@(when limit `((limit . ,limit))) ,@(when limit `((limit . ,limit)))
,@(when offset `((offset . ,offset)))))))) ,@(when offset `((offset . ,offset))))))
(defun avandu-headlines (feed-id &rest plist) (defun avandu-headlines (feed-id &rest plist)
"Get a list of headlines from Tiny Tiny RSS from the feed "Get a list of headlines from Tiny Tiny RSS from the feed
@ -411,7 +440,6 @@ There are some special feed IDs:
(view-mode (plist-get plist :view-mode)) (view-mode (plist-get plist :view-mode))
(include-attachments (plist-get plist :include-attachments)) (include-attachments (plist-get plist :include-attachments))
(since-id (plist-get plist :since-id))) (since-id (plist-get plist :since-id)))
(cdr (assq 'content
(avandu--send-command (avandu--send-command
`((op . "getHeadlines") `((op . "getHeadlines")
(feed_id . ,feed-id) (feed_id . ,feed-id)
@ -421,8 +449,9 @@ There are some special feed IDs:
,@(when show-excerpt `((show_excerpt . ,show-excerpt))) ,@(when show-excerpt `((show_excerpt . ,show-excerpt)))
,@(when show-content `((show_content . ,show-content))) ,@(when show-content `((show_content . ,show-content)))
,@(when view-mode `((view_mode . ,view-mode))) ,@(when view-mode `((view_mode . ,view-mode)))
,@(when include-attachments `((include_attachments . ,include-attachments))) ,@(when include-attachments `((include_attachments
,@(when since-id `((since_id . ,since-id))))))))) . ,include-attachments)))
,@(when since-id `((since_id . ,since-id)))))))
(defun avandu-update-article (article-ids mode field &optional data) (defun avandu-update-article (article-ids mode field &optional data)
"Update the status of FIELD to MODE for the articles identified "Update the status of FIELD to MODE for the articles identified
@ -449,6 +478,13 @@ When updating FIELD 3 DATA functions as the note's contents."
(field . ,field) (field . ,field)
,@(when data `((data . ,data)))))) ,@(when data `((data . ,data))))))
(defun avandu-get-article (article-ids)
"Get one or more articles from Tiny Tiny RSS with ARTICLE-IDS,
if you're using version 1.5.0 or higher this can also be a
comma-separated list of ids."
(avandu--send-command `((op . "getArticle")
(article_id . ,article-ids))))
;; Commands ;; Commands
(defun avandu-browse-article () (defun avandu-browse-article ()
"Browse the current button's article url." "Browse the current button's article url."
@ -456,7 +492,8 @@ When updating FIELD 3 DATA functions as the note's contents."
(let ((button (button-at (point))) (let ((button (button-at (point)))
(message-truncate-lines t)) (message-truncate-lines t))
(browse-url (button-get button 'link)) (browse-url (button-get button 'link))
(avandu-mark-article-read button) (avandu-mark-article-read (button-get button 'article-id))
(avandu-ui-mark-article-read button)
(message "Opened: %s" (button-label button)))) (message "Opened: %s" (button-label button))))
(defun avandu-feed-catchup () (defun avandu-feed-catchup ()
@ -475,7 +512,7 @@ When updating FIELD 3 DATA functions as the note's contents."
"Send a request to tt-rss to see if we're (still) logged "Send a request to tt-rss to see if we're (still) logged
in. This function returns t if we are, or nil if we're not." in. This function returns t if we are, or nil if we're not."
(let* ((response (avandu--send-command '((op . "isLoggedIn")))) (let* ((response (avandu--send-command '((op . "isLoggedIn"))))
(result (cdr (assq 'status (assq 'content response))))) (result (avu-prop response status)))
(if (eq result :json-false) (if (eq result :json-false)
nil nil
result))) result)))
@ -505,7 +542,7 @@ otherwise."
(avandu--send-command '((op . "logout"))) (avandu--send-command '((op . "logout")))
(avandu--clear-data)) (avandu--clear-data))
(defun avandu-mark-article-read (&optional button) (defun avandu-mark-article-read (id)
"Send a request to tt-rss to mark an article as read. "Send a request to tt-rss to mark an article as read.
BUTTON, if given, should be a button widget, as created by BUTTON, if given, should be a button widget, as created by
@ -513,13 +550,18 @@ BUTTON, if given, should be a button widget, as created by
nil, it will be assumed that `point' is currently within the nil, it will be assumed that `point' is currently within the
bounds of a button." bounds of a button."
(interactive) (interactive)
(let* ((button (or button (button-at (point)))) (let* ((message-truncate-lines t))
(id (button-get button 'article-id)) (avandu-update-article id 0 2)))
(message-truncate-lines t))
(avandu-update-article id 0 2) (defun avandu-ui-mark-article-read (&optional button)
"Try to change the state of BUTTON to a read article button, if
BUTTON is nil, try to use a button at `point'."
(let ((button (or button (button-at (point)))))
(if button
(progn
(button-put button 'face 'avandu-overview-read-article) (button-put button 'face 'avandu-overview-read-article)
(message "Marked article as read: %s" (button-label button)))
(avandu-next-article)) (avandu-next-article))
(error "No button found."))))
(defun avandu-new-articles-count () (defun avandu-new-articles-count ()
"Send a request to tt-rss for the total number of unread "Send a request to tt-rss for the total number of unread
@ -527,7 +569,7 @@ feeds."
(interactive) (interactive)
(avandu--check-login) (avandu--check-login)
(let* ((result (avandu--send-command '((op . "getUnread")))) (let* ((result (avandu--send-command '((op . "getUnread"))))
(count (cdr (assq 'unread (assq 'content result))))) (count (avu-prop result unread)))
(when (called-interactively-p 'any) (when (called-interactively-p 'any)
(message "There are %s unread articles" count)) (message "There are %s unread articles" count))
@ -557,10 +599,8 @@ feeds."
(defun avandu-tt-rss-api-level () (defun avandu-tt-rss-api-level ()
"Get the API level of your Tiny Tiny RSS instance." "Get the API level of your Tiny Tiny RSS instance."
(interactive) (interactive)
(let ((level (cdr (assq 'level (let ((level (avu-prop (avandu--send-command '((op . "getApiLevel")))
(assq 'content level)))
(avandu--send-command
'((op . "getApiLevel"))))))))
(when (called-interactively-p 'any) (when (called-interactively-p 'any)
(message "API Level: %d" level)) (message "API Level: %d" level))
@ -569,10 +609,8 @@ feeds."
(defun avandu-tt-rss-version () (defun avandu-tt-rss-version ()
"Get the version of your Tiny Tiny RSS instance." "Get the version of your Tiny Tiny RSS instance."
(interactive) (interactive)
(let ((version (cdr (assq 'version (let ((version (avu-prop (avandu--send-command '((op . "getVersion")))
(assq 'content version)))
(avandu--send-command
'((op . "getVersion"))))))))
(when (called-interactively-p 'any) (when (called-interactively-p 'any)
(message "Tiny Tiny RSS Version: %s" version)) (message "Tiny Tiny RSS Version: %s" version))
@ -595,6 +633,15 @@ doesn't sort the list, so you'll have to set that up in tt-rss.
avandu-overview-mode-name avandu-overview-mode-name
(avandu-new-articles-count)))) (avandu-new-articles-count))))
(define-derived-mode avandu-article-mode special-mode
"Avandu:Article"
"Major mode for the avandu article screen.
This screen shows the contents of an article.
\\{avandu-overview-map}
\\<avandu-overview-map>")
;;;###autoload ;;;###autoload
(defun avandu-overview () (defun avandu-overview ()
"Request the headlines of unread articles and list them grouped "Request the headlines of unread articles and list them grouped
@ -610,22 +657,51 @@ by feed."
(goto-char (point-min)) (goto-char (point-min))
(mapc #'(lambda (elt) (mapc #'(lambda (elt)
(unless (equal feed-id (assq 'feed_id elt)) (unless (equal feed-id (assq 'feed_id elt))
(avandu--insert-feed-title (avandu--insert-feed-title (avu-prop elt feed_id)
(cdr (assq 'feed_id elt)) (avu-prop elt feed_title)))
(cdr (assq 'feed_title elt))))
(setq feed-id (assq 'feed_id elt)) (setq feed-id (assq 'feed_id elt))
(avandu--insert-article-title (avandu--insert-article-title (avu-prop elt id)
(cdr (assq 'id elt)) (avu-prop elt link)
(cdr (assq 'link elt)) (avu-prop elt title))
(cdr (assq 'title elt))) (avandu--insert-article-excerpt (avu-prop elt excerpt)))
(avandu--insert-article-excerpt
(cdr (assq 'excerpt elt))))
result) result)
(setq buffer-read-only t) (setq buffer-read-only t)
(goto-char (point-min)) (goto-char (point-min))
(avandu-overview-mode)) (avandu-overview-mode))
(switch-to-buffer buffer))) (switch-to-buffer buffer)))
(defun avandu-view-article (id)
"Show a single article in a new buffer."
(interactive "nArticle id: ")
(let* ((data (avandu-get-article id))
(buffer (get-buffer-create "*avandu-article*"))
(inhibit-read-only t))
(with-current-buffer buffer
(erase-buffer)
(mapc #'(lambda (item)
(insert
(propertize (avu-prop item title)
'face 'avandu-article-title))
(newline)
(insert
(propertize (concat "by: " (avu-prop item author))
'face 'avandu-article-author))
(newline)(newline)
(let ((pos (point)))
(insert (avu-prop item content))
(when avandu-html2text-command
(shell-command-on-region
pos (point) avandu-html2text-command buffer t)))
(newline)(newline))
data)
(setq buffer-read-only t)
(goto-char (point-min))
(avandu-article-mode))
(avandu-mark-article-read id)
(avandu-ui-mark-article-read)
(switch-to-buffer buffer)))
(provide 'avandu) (provide 'avandu)
;;; avandu.el ends here ;;; avandu.el ends here