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:
parent
93461d87cd
commit
6d056c6a63
1 changed files with 134 additions and 58 deletions
192
avandu.el
192
avandu.el
|
@ -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,18 +440,18 @@ 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)
|
,@(when limit `((limit . ,limit)))
|
||||||
,@(when limit `((limit . ,limit)))
|
,@(when skip `((skip . ,skip)))
|
||||||
,@(when skip `((skip . ,skip)))
|
,@(when is-cat `((is_cat . ,is-cat)))
|
||||||
,@(when is-cat `((is_cat . ,is-cat)))
|
,@(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
|
||||||
,@(when include-attachments `((include_attachments . ,include-attachments)))
|
. ,include-attachments)))
|
||||||
,@(when since-id `((since_id . ,since-id)))))))))
|
,@(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)
|
||||||
(button-put button 'face 'avandu-overview-read-article)
|
"Try to change the state of BUTTON to a read article button, if
|
||||||
(message "Marked article as read: %s" (button-label button)))
|
BUTTON is nil, try to use a button at `point'."
|
||||||
(avandu-next-article))
|
(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 ()
|
(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
|
||||||
|
|
Loading…
Reference in a new issue