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

192
avandu.el
View file

@ -23,8 +23,9 @@
;;; Commentary:
;; Avandu is an emacs mode that connects to a Tiny Tiny RSS instance
;; and allows you to read the feeds it has gathered locally.
;; Avandu is an emacs mode that connects to a Tiny Tiny RSS
;; (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:
@ -105,6 +106,21 @@
"Face for unread article titles in avandu overview."
: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
(defcustom avandu-tt-rss-api-url nil
"URL of your Tiny Tiny RSS instance. For example:
@ -117,6 +133,11 @@
:group 'avandu
:type 'string)
(defcustom avandu-html2text-command nil
"Shell command to call to change HTML to plain text."
:group 'avandu
:type 'string)
;; Variables
(defvar avandu--session-id nil
"*internal* Session id for avandu.")
@ -125,7 +146,12 @@
(let ((map (make-sparse-keymap)))
(set-keymap-parent map button-map)
(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)
"Keymap for articles in `avandu-overview-mode'.")
@ -182,6 +208,10 @@
`(or ,var (setq ,var (,(if passwdp 'read-passwd 'read-string)
,prompt))))
(defmacro avu-prop (element property)
"Get PROPERTY from ELEMENT."
`(cdr (assq (quote ,property) ,element)))
;; Internal
(defun avandu--check-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)
"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)
"Get the status id from RESULTS."
(cdr (assq 'status results)))
(avu-prop results status))
(defun avandu--insert-article-excerpt (excerpt)
"Insert the excerpt of an article."
@ -280,7 +310,8 @@ the article-id and link properties, respectively."
'link link
'keymap avandu-article-button-map
'action #'(lambda (button)
(message "%s" (button-get button 'link))))
(avandu-view-article (button-get button 'article-id))))
(fill-region pos (point))
(insert-char ?\n 1)))
@ -337,15 +368,14 @@ returned json."
(search-forward "\n\n")
(setq result (json-read)))
(kill-buffer buffer)
result))
(avu-prop result content)))
(defun avandu-categories (&optional unread)
"Get the created categories. If UNREAD is non-nil only get
categories with feeds with unread articles in them."
(cdr (assq 'content
(avandu--send-command
`((op . "getCategories")
,@(when unread `((unread_only . ,unread))))))))
(avandu--send-command
`((op . "getCategories")
,@(when unread `((unread_only . ,unread))))))
(defun avandu-feeds (&optional category unread limit offset)
"Get the subscribed feeds. If CATEGORY has been specified show
@ -359,13 +389,12 @@ There are a number of special category IDs:
-2 -- Labels
-3 -- All feeds, excluding virtual feeds (e.g. Labels and such)
-4 -- All feeds, including virtual feeds"
(cdr (assq 'content
(avandu--send-command
`((op . "getFeeds")
,@(when category `((cat_id . ,category)))
,@(when unread `((unread_only . ,unread)))
,@(when limit `((limit . ,limit)))
,@(when offset `((offset . ,offset))))))))
(avandu--send-command
`((op . "getFeeds")
,@(when category `((cat_id . ,category)))
,@(when unread `((unread_only . ,unread)))
,@(when limit `((limit . ,limit)))
,@(when offset `((offset . ,offset))))))
(defun avandu-headlines (feed-id &rest plist)
"Get a list of headlines from Tiny Tiny RSS from the feed
@ -411,18 +440,18 @@ There are some special feed IDs:
(view-mode (plist-get plist :view-mode))
(include-attachments (plist-get plist :include-attachments))
(since-id (plist-get plist :since-id)))
(cdr (assq 'content
(avandu--send-command
`((op . "getHeadlines")
(feed_id . ,feed-id)
,@(when limit `((limit . ,limit)))
,@(when skip `((skip . ,skip)))
,@(when is-cat `((is_cat . ,is-cat)))
,@(when show-excerpt `((show_excerpt . ,show-excerpt)))
,@(when show-content `((show_content . ,show-content)))
,@(when view-mode `((view_mode . ,view-mode)))
,@(when include-attachments `((include_attachments . ,include-attachments)))
,@(when since-id `((since_id . ,since-id)))))))))
(avandu--send-command
`((op . "getHeadlines")
(feed_id . ,feed-id)
,@(when limit `((limit . ,limit)))
,@(when skip `((skip . ,skip)))
,@(when is-cat `((is_cat . ,is-cat)))
,@(when show-excerpt `((show_excerpt . ,show-excerpt)))
,@(when show-content `((show_content . ,show-content)))
,@(when view-mode `((view_mode . ,view-mode)))
,@(when include-attachments `((include_attachments
. ,include-attachments)))
,@(when since-id `((since_id . ,since-id)))))))
(defun avandu-update-article (article-ids mode field &optional data)
"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)
,@(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
(defun avandu-browse-article ()
"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)))
(message-truncate-lines t))
(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))))
(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
in. This function returns t if we are, or nil if we're not."
(let* ((response (avandu--send-command '((op . "isLoggedIn"))))
(result (cdr (assq 'status (assq 'content response)))))
(result (avu-prop response status)))
(if (eq result :json-false)
nil
result)))
@ -505,7 +542,7 @@ otherwise."
(avandu--send-command '((op . "logout")))
(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.
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
bounds of a button."
(interactive)
(let* ((button (or button (button-at (point))))
(id (button-get button 'article-id))
(message-truncate-lines t))
(avandu-update-article id 0 2)
(button-put button 'face 'avandu-overview-read-article)
(message "Marked article as read: %s" (button-label button)))
(avandu-next-article))
(let* ((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)
(avandu-next-article))
(error "No button found."))))
(defun avandu-new-articles-count ()
"Send a request to tt-rss for the total number of unread
@ -527,7 +569,7 @@ feeds."
(interactive)
(avandu--check-login)
(let* ((result (avandu--send-command '((op . "getUnread"))))
(count (cdr (assq 'unread (assq 'content result)))))
(count (avu-prop result unread)))
(when (called-interactively-p 'any)
(message "There are %s unread articles" count))
@ -557,10 +599,8 @@ feeds."
(defun avandu-tt-rss-api-level ()
"Get the API level of your Tiny Tiny RSS instance."
(interactive)
(let ((level (cdr (assq 'level
(assq 'content
(avandu--send-command
'((op . "getApiLevel"))))))))
(let ((level (avu-prop (avandu--send-command '((op . "getApiLevel")))
level)))
(when (called-interactively-p 'any)
(message "API Level: %d" level))
@ -569,10 +609,8 @@ feeds."
(defun avandu-tt-rss-version ()
"Get the version of your Tiny Tiny RSS instance."
(interactive)
(let ((version (cdr (assq 'version
(assq 'content
(avandu--send-command
'((op . "getVersion"))))))))
(let ((version (avu-prop (avandu--send-command '((op . "getVersion")))
version)))
(when (called-interactively-p 'any)
(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-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
(defun avandu-overview ()
"Request the headlines of unread articles and list them grouped
@ -610,22 +657,51 @@ by feed."
(goto-char (point-min))
(mapc #'(lambda (elt)
(unless (equal feed-id (assq 'feed_id elt))
(avandu--insert-feed-title
(cdr (assq 'feed_id elt))
(cdr (assq 'feed_title elt))))
(avandu--insert-feed-title (avu-prop elt feed_id)
(avu-prop elt feed_title)))
(setq feed-id (assq 'feed_id elt))
(avandu--insert-article-title
(cdr (assq 'id elt))
(cdr (assq 'link elt))
(cdr (assq 'title elt)))
(avandu--insert-article-excerpt
(cdr (assq 'excerpt elt))))
(avandu--insert-article-title (avu-prop elt id)
(avu-prop elt link)
(avu-prop elt title))
(avandu--insert-article-excerpt (avu-prop elt excerpt)))
result)
(setq buffer-read-only t)
(goto-char (point-min))
(avandu-overview-mode))
(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)
;;; avandu.el ends here