diff --git a/elfeed-summary.el b/elfeed-summary.el index a23b631..666c0cd 100644 --- a/elfeed-summary.el +++ b/elfeed-summary.el @@ -37,6 +37,7 @@ :offset 4 :tag "Extract subset of elfeed feed list" :type '(choice (symbol :tag "One tag") + (const :tag "All" :all) (cons :tag "Match title" (const :tag "Title" title) (choice (string :tag "String") @@ -52,6 +53,9 @@ (cons :tag "AND" (const :tag "AND" and) (repeat elfeed-summary-query)) + (cons :tag "NOT" + (const :tag "NOT" not) + elfeed-summary-query) (repeat :tag "OR (Implicit)" elfeed-summary-query) (cons :tag "OR" (const :tag "OR" or) @@ -63,39 +67,106 @@ :tag "Group" :type '(repeat (choice - (list :tag "Group" - (cons - (const :tag "Title" :title) - (string :tag "Title")) - (cons - (const :tag "Sort function" :sort-fn) - (choice - function - (const :tag "None" nil))) - (cons - (const :tag "Elements" :elements) - elfeed-summary-group)) + (cons :tag "Group" + (const group) + (list :tag "Group params" + (cons + (const :tag "Title" :title) + (string :tag "Title")) + (cons + (const :tag "Sort function" :sort-fn) + (choice + function + (const :tag "None" nil))) + (cons + (const :tag "Elements" :elements) + elfeed-summary-group))) elfeed-summary-query))) (defgroup elfeed-summary () "Feed summary inteface for elfeed." :group 'elfeed) -(defcustom elfeed-summary-settings '(((:title . "All feeds") - (:sort-fn . string-lessp) - (:elements nil))) - "Elfeed summary buffer settings." +(defcustom elfeed-summary-settings '((group (:title . "All feeds") + (:sort-fn . string-lessp) + (:elements :all))) + "Elfeed summary buffer settings. + +This is a list of these possible items: +- group +- query +- a few special forms + +Groups are used to group queries under collapsible sections. + +A group is a cons cell like (group . ), where params are an +alist with the following attributes: +- `:title' (mandatory) +- `:elements' (mandatory) - also a list of groups and queries +- `:sort-fn' - function used to sort titles of feeds, found by queries + in `:elements'. E.g. `string-greaterp' for alphabetical order. + +Query is a form that extract a subset of elfeed feeds based on +some criteria. In the summary buffer, each feed found by the +query will be represented as a line. + +Query can be: +- A symbol of a tag. + A feed will be matched if it has that tag. +- `:all'. Will match anything. +- `(title . \"string\")' or `(title .
)' + Match feed title with `string-match-p'. makes sense if you + want to pass something like `rx'. +- `(author . \"string\")' or `(author . )' +- `(url . \"string\")' or `(url . )' +- `(and ... )' + Match if all the conditions 1, 2, ..., n match. +- `(or ... )' or `( ... )' + Match if any of the conditions 1, 2, ..., n match. +- `(not )' + +Feed tags are taken from `elfeed-feeds'. + +Query examples: +- `(emacs lisp)' + Return all feeds that have either \"emacs\" or \"lisp\" tags. +- `(and emacs lisp)' + Return all feeds that have both \"emacs\" and \"lisp\" tags. +- `(and (title . \"Emacs\") (not planets)) + Return all feeds that have \"Emacs\" in their title and don't have + the \"planets\" tag. + +Available special forms: +- `:misc' - print out feeds, not found by any query above. +- `:unread' - a special feed of all unread entries." :group 'elfeed-summary :type 'elfeed-summary-group) +(defcustom elfeed-summary-look-back (* 60 60 24 180) + "TODO" + :group 'elfeed-summary + :type 'integer) + +(defcustom elfeed-summary-unread-tag 'unread + "Unread tag" + :group 'elfeed-summary + :type 'symbol) + +(defface elfeed-summary-group-face + '((t (:inherit magit-section-heading))) + "Default face for the elfeed-summary group." + :group 'elfeed-summary) + (cl-defun elfeed-summary--match-tag (query &key tags title url author title-meta) "Check if attributes of elfeed feed match QUERY. -QUERY is a form as described in TODO. +QUERY is a form as described in `elfeed-summary-settings'. TAGS is a list of tags from `elfeed-feeds', TITLE, URL, AUTHOR and TITLE-META are attributes of the `elfeed-db-feed'." (cond + ;; `:all' + ((equal query :all) t) ;; symbol ((symbolp query) (member query tags)) ;; (title . "Title") @@ -143,6 +214,16 @@ and TITLE-META are attributes of the `elfeed-db-feed'." :url url :author author)) (cdr query))) + ;; (not ) + ((eq (car query) 'not) + (not + (elfeed-summary--match-tag + (cdr query) + :tags tags + :title title + :title-meta title-meta + :url url + :author author))) ;; (or ... ) ;; ( ... ) (t (seq-some @@ -161,7 +242,7 @@ and TITLE-META are attributes of the `elfeed-db-feed'." (defun elfeed-summary--get-feeds (query) "Get elfeed feeds that match QUERY. -QUERY is described in TODO." +QUERY is described in `elfeed-summary-settings'." (cl-loop for feed in elfeed-feeds for url = (car feed) for tags = (cdr feed) @@ -175,5 +256,77 @@ QUERY is described in TODO." :author (plist-get (car (elfeed-feed-author feed)) :name)) collect feed)) +(defun elfeed-summary--extract-feeds (params) + (cl-loop for param in params + if (and (listp param) (eq (car param) 'group)) + append (elfeed-summary--extract-feeds + (cdr (assoc :elements (cdr param)))) + else append (elfeed-summary--get-feeds param))) + +(defun elfeed-summary--build-tree-feed (feed unread-count total-count) + (let* ((unread (or (gethash (elfeed-feed-id feed) unread-count) 0)) + (tags (alist-get (elfeed-feed-id feed) elfeed-feeds + nil nil #'equal)) + (all-tags (if (< 0 unread) + (cons elfeed-summary-unread-tag tags) + tags))) + `((feed . ,feed) + (unread . ,unread) + (total . ,(or (gethash (elfeed-feed-id feed) total-count) 0)) + (faces . ,(elfeed-search--faces all-tags)) + (tags . ,all-tags)))) + +(defun elfeed-summary--build-tree-unread (unread-count) + (let ((unread (apply #'+ (maphash (lambda (k v) v) unread-count)))) + `((feed . ,(elfeed-feed--create + :id unread + :title "Unread")) + (unread . ,unread) + (total . ,unread)))) + +(defun elfeed-summary--build-tree (params unread-count total-count misc-feeds) + (cl-loop for param in params + if (and (listp param) (eq (car param) 'group)) + collect `(,param + (children . ,(elfeed-summary--build-tree + (cdr (assoc :elements (cdr param))) + unread-count total-count misc-feeds))) + else if (eq param :misc) + append (cl-loop for feed in misc-feeds + collect (elfeed-summary--build-tree-feed + feed unread-count total-count)) + else if (eq param :unread) + collect (elfeed-summary--build-tree-unread unread-count) + else + append (cl-loop for feed in (elfeed-summary--get-feeds param) + collect (elfeed-summary--build-tree-feed + feed unread-count total-count)))) + +(defun elfeed-summary--get-data () + (let* ((feeds (elfeed-summary--extract-feeds + elfeed-summary-settings)) + (all-feeds (mapcar #'car elfeed-feeds)) + (misc-feeds + (thread-last feeds + (mapcar #'elfeed-feed-id) + (seq-difference all-feeds) + (mapcar #'elfeed-db-get-feed))) + (unread-count (make-hash-table :test #'equal)) + (total-count (make-hash-table :test #'equal))) + (with-elfeed-db-visit (entry feed) + (puthash (elfeed-feed-id feed) + (1+ (or (gethash (elfeed-feed-id feed) total-count) 0)) + total-count) + (when (member elfeed-summary-unread-tag (elfeed-entry-tags entry)) + (puthash (elfeed-feed-id feed) + (1+ (or (gethash (elfeed-feed-id feed) unread-count) 0)) + unread-count)) + (when (> (- (time-convert nil 'integer) + elfeed-summary-look-back) + (elfeed-entry-date entry)) + (elfeed-db-return))) + (elfeed-summary--build-tree elfeed-summary-settings + unread-count total-count misc-feeds))) + (provide 'elfeed-summary) ;;; elfeed-summary.el ends here