Compare commits

...

26 commits

Author SHA1 Message Date
76b4b93838
Merge pull request #13 from Thaodan/display_buffer
elfeed-summary: Comply with display-buffer-alist and action
2024-09-29 23:43:24 +03:00
Björn Bidar
c82d0dff9b elfeed-summary: Comply with display-buffer-alist and action
By using display-buffer we can comply with display-buffer-alist
or display-buffer-base-action/pop-frames.
2024-09-26 22:31:12 +03:00
7e308adaa3 elfeed-summary: respect skip-sync-tag in sync at point 2023-12-31 17:56:58 +03:00
5ca7a36eca elfeed-summary: log update time 2023-12-31 17:56:47 +03:00
92b5d026e0 elfeed-summary: add update feed at point 2023-12-31 03:20:32 +03:00
efa30b88d7 elfeed-summary: add Published-At 2023-12-26 02:10:02 +03:00
129b893da1 ci: try to fix melpazoid 2023-08-23 12:39:09 +03:00
6bad19a7d4 feat: skip certain tags in sync 2023-08-23 12:25:25 +03:00
96ffc25862 fix: widget-button face shadowing elfeed faces 2023-08-13 12:42:06 +03:00
d3caffb44a ci: fix ci 2023-06-18 16:21:35 +03:00
063f60a9a7
Merge pull request #11 from Thaodan/git_ignore_autoload
Ignore generated autoloads file
2023-03-11 12:09:19 +03:00
Björn Bidar
7a25708b8a Ignore generated autoloads file
Signed-off-by: Björn Bidar <bjorn.bidar@thaodan.de>
2023-03-11 00:05:14 +02:00
ccbaf85d9e
Merge pull request #10 from SqrtMinusOne/auto-tags
Automatic tree generation
2022-12-10 16:49:25 +03:00
b583c2f3ea docs: update README 2022-12-10 16:46:06 +03:00
49dd44be53 fix: workaround for magit-section-toggle 2022-12-10 14:07:35 +03:00
38099f3dd3 feat: add tag-groups 2022-12-10 13:41:26 +03:00
d51ca5be1d fix: byte-compile 2022-12-10 12:42:01 +03:00
7193b2f269 feat: auto-tags seems to be done 2022-12-10 12:34:50 +03:00
d302c10be3 feat: auto-generating tree works 2022-12-10 01:02:42 +03:00
ebf851fa2d feat: some progress on auto-tags 2022-12-09 23:35:18 +03:00
121a3df868 ci: Python 3.6 -> Python 3.9 2022-12-09 20:22:35 +03:00
fa917339d2 fix: customize interface for `elfeed-summary-settings' 2022-12-09 20:17:39 +03:00
125e0f059d feat: improve search handling 2022-11-17 11:41:12 +03:00
5006ef6432
Merge pull request #5 from fe11x/master
fix: function void: time-convert for emacs 26.5
2022-09-06 11:22:40 +03:00
zhanghui
1930de3924 fix: function void: time-convert 2022-09-06 09:41:29 +08:00
012f6fee58 fix: use rx-to-string for opening sections 2022-07-02 12:06:23 +03:00
4 changed files with 664 additions and 53 deletions

View file

@ -10,15 +10,16 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.6
uses: actions/setup-python@v1
with: { python-version: 3.6 }
- uses: actions/checkout@v3
- name: Set up Python 3.10
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install
run: |
python -m pip install --upgrade pip
sudo apt-get install emacs && emacs --version
git clone https://github.com/riscy/melpazoid.git ~/melpazoid
git clone https://github.com/SqrtMinusOne/melpazoid.git ~/melpazoid
pip install ~/melpazoid
- name: Run
env:

5
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Autoloads
*autoloads*
# Compiled
*.elc
@ -8,4 +11,4 @@
*~
# Undo-tree save-files
*.~undo-tree
*.~undo-tree

View file

@ -24,6 +24,8 @@ The tree consists of:
- searches;
- groups, that can include other groups, feeds, and searches.
Groups can also be generated automatically.
Available keybindings in the summary mode:
| Keybinding | Command | Description |
@ -49,6 +51,10 @@ This is a list of these possible items:
Query extracts a subset of elfeed feeds based on the given criteria. Each found feed will be represented as a line.
- Search =(search . <search-params>)=
Elfeed search, as defined by =elfeed-search-set-filter=.
- Tags tree =(auto-tags . <auto-tags-params>)=
A tree generated automatically from the available tags.
- Tag groups =(tag-groups . <tag-group-params>)=
Insert one tag as one group.
- a few special forms
=<group-params>= is an alist with the following keys:
@ -89,6 +95,21 @@ Query examples:
- =:title= (mandatory) title.
- =:tags= - list of tags to get the face of the entry.
=<auto-tags-params>= is an alist with the following keys:
- =:max-level= - maximum level of the tree (default 2)
- =:source= - which feeds to use to build the tree.
Can be =:misc= (default) or =(query . <query-params>)=.
- =:original-order= - do not try to build a more concise tree by
putting the most frequent tags closer to the root of the tree.
- =:faces= - list of faces for groups.
=<tag-group-params>= is an alist with the following keys:
- =:source= - which feeds to use to build the tree.
Can be =:misc= (default) or =(query . <query-params>)=.
- =:repeat-feeds= - allow feeds to repeat. Otherwise, each feed is
assigned to group with the least amount of members.
- =:face= - face for groups.
Available special forms:
- =:misc= - print out feeds, not found by any query above.
@ -150,11 +171,106 @@ Here is an excerpt from my configuration that was used to produce this screensho
(:title . "Ungrouped")
(:elements :misc))))))
#+end_src
** Automatic generation of groups
*** =auto-tags=
As described in the [[*Tree configuration][tree configuration]] section, there are two ways to avoid defining all the relevant groups manually, =auto-tags= and =tag-groups=. Both use tags that are defined in =elfeed-feeds=.
=auto-tags= tries to build the most concise tree from these tags. E.g. if we have feeds:
#+begin_example
feed1 tag1 tag2
feed2 tag1 tag2
feed3 tag1 tag3
feed4 tag1 tag3
#+end_example
It will create the following tree:
- tag1
- tag2
- feed1
- feed2
- tag3
- feed3
- feed4
The tree is truncated by =:max-level=, which is 2 by default.
If tags don't form this kind of hierarchy in =elfeed-feeds=, the algorithm will still try to build the most "optimal" tree, where the most frequent tags are on the top.
To avoid that you can set =(:original-order . t)=, in which case each feed will be placed at the path =tag1 tag2 ... tagN feed=, where the order of tags is the same as in =elfeed-feeds=. By the way, this allows reproducing the hierarchy of [[https://github.com/remyhonig/elfeed-org][elfeed-org]], e.g. this structure:
#+begin_example
,* tag1 :tag1:
,** feed1
,** feed2 :tag2:
,** feed3 :tag2:
,* tag3 :tag3:
,** feed4 :tag2:
,** feed5 :tag2:
,** feed6 :tag2:
#+end_example
Will be converted to this:
- tag1
- feed1
- tag2
- feed2
- feed3
- tag3
- tag2
- feed4
- feed5
- feed6
Whereas without =:original-order= the structure will be:
- tag1
- feed1
- tag2
- tag1
- feed2
- feed3
- tag3
- feed4
- feed5
- feed6
*** =tag-groups=
The second option is =tag-groups=, which creates a group for each tag.
By default, each feed is assigned to its less frequent tag. This can be turned off by setting =(:repeat-feeds . t)=.
E.g., the elfeed-org setup from the section above will be converted to this structure:
- tag1
- feed1
- feed2
- feed3
- tag3
- feed4
- feed5
- feed6
And with =:repeat-feeds=:
- tag1
- feed1
- feed2
- feed3
- tag2
- feed2
- feed3
- feed4
- feed5
- feed6
- tag3
- feed4
- feed5
- feed6
*** Common options
Both =auto-tags= and =tag-groups= allow setting the =:search= parameter.
The default value is =(:search . :misc)=, i.e. use feeds that weren't found by other queries.
Passing =(:search . (query . <query-params>))= is another option.
** Faces
Faces for groups by default use the =elfeed-summary-group-faces= variable, which serves as a list of faces for each level of the tree. Individual group faces can be overridden with the =:face= attribute.
Group faces by default use the =elfeed-summary-group-faces= variable, which serves as a list of faces for each level of the tree. Individual group faces can be overridden with the =:face= attribute.
Faces for feeds by default reuse [[https://github.com/skeeto/elfeed#custom-tag-faces][the existing elfeed mechanism]]. The tags for feeds are taken from the =elfeed-feeds= variable; if a feed has at least one unread entry, the unread tag is added to the list. This can be overridden by setting the =elfeed-summary-feed-face-fn= variable.
Feed faces by default reuse [[https://github.com/skeeto/elfeed#custom-tag-faces][the existing elfeed mechanism]]. The tags for feeds are taken from the =elfeed-feeds= variable; if a feed has at least one unread entry, the unread tag is added to the list. This can be overridden by setting the =elfeed-summary-feed-face-fn= variable.
Searches are mostly the same as feeds, but tags for the search are taken from the =:tags= attribute. This also can be overridden with =elfeed-summary-search-face-fn= variable.
** Opening =elfeed-search= in other window
@ -166,10 +282,23 @@ If you set:
Then =RET= and =M-RET= in the =elfeed-summary= buffer will open the search buffer in other window.
=elfeed-summary-width= regulates the width of the remaining summary window in this case. It is useful because the data in the search buffer is generally wider than in the summary buffer. The variable can also be set to =nil= to disable this behavior.
** Skipping feeds
[[https://tt-rss.org/][tt-rss]] has a feature to disable updating a particular feed but keep it in the feed list. I also want that for elfeed.
To use that, set =elfeed-summary-skip-sync-tag= to some value:
#+begin_src emacs-lisp
(setq elfeed-summary-skip-sync-tag 'skip)
#+end_src
And tag the feeds you want to skip with this tag. Then, running =M-x elfeed-summary-update= will skip them. This won't affect =M-x elfeed-update= unless you:
#+begin_src emacs-lisp
(advice-add #'elfeed-update :override #'elfeed-summary-update)
#+end_src
Also watch out if you use [[https://github.com/remyhonig/elfeed-org][elfeed-org]] and want to use the =ignore= tag, because this package omits feeds with this tag altogether (configurable by =rmh-elfeed-org-ignore-tag=).
** Other options
Also take a look at =M-x customize-group elfeed-summary= for the rest of available options.
Setting a unique URL for every feed resolves the problem.
* Ideas and alternatives
The default interface of elfeed is just a list of all entries. Naturally, it gets hard to navigate when there are a lot of sources with varying frequencies of posts.

View file

@ -1,12 +1,13 @@
;;; elfeed-summary.el --- Feed summary interface for elfeed -*- lexical-binding: t -*-
;; Copyright (C) 2022 Korytov Pavel
;; Copyright (C) 2022, 2023 Korytov Pavel
;; Author: Korytov Pavel <thexcloud@gmail.com>
;; Maintainer: Korytov Pavel <thexcloud@gmail.com>
;; Version: 0.1.0
;; Version: 0.1.1
;; Package-Requires: ((emacs "27.1") (magit-section "3.3.0") (elfeed "3.4.1"))
;; Homepage: https://github.com/SqrtMinusOne/elfeed-summary.el
;; Published-At: 2022-03-26
;; This file is NOT part of GNU Emacs.
@ -25,8 +26,8 @@
;;; Commentary:
;; The package provides a tree-based feed summary interface for
;; elfeed. The tree can include individual feeds, searches, and
;; groups. It mainly serves as an easier "jumping point" for elfeed,
;; elfeed. The tree can include individual feeds, searches, and
;; groups. It mainly serves as an easier "jumping point" for elfeed,
;; so searching a subset of the elfeed database is one action away.
;;
;; `elfeed-summary' pops up the summary buffer. The buffer shows
@ -122,7 +123,47 @@
(string :tag "Filter title"))
(cons :tag "Tags"
(const :tag "Tags" :tags)
(repeat symbol)))))
(repeat symbol))
(cons (const :tag "Add the default filter string" :add-default)
(boolean :tag "Add the default filter string")))))
(cons :tag "Generate a tree from tags"
(const auto-tags)
(repeat :tag "Tree generation parameters"
(choice
(cons :tag "Source"
(const :tag "Source" :source)
(choice
(cons :tag "Query"
(const query)
elfeed-summary-query)
(const :tag "Misc feeds" :misc)))
(cons :tag "Maximum tree level"
(const :tag "Maximum tree level" :max-level)
(number :tag "Value"))
(cons :tag "Original tag order"
(const :tag "Original tag order" :original-order)
(boolean :tag "Original tag order"))
(cons :tag "Faces"
(const :tag "Faces" :faces)
(repeat
(face :tag "Face"))))))
(cons :tag "Insert each tag as group"
(const tag-groups)
(repeat :tag "Tag group parameters"
(choice
(cons :tag "Source"
(const :tag "Source" :source)
(choice
(cons :tag "Query"
(const query)
elfeed-summary-query)
(const :tag "Misc feeds" :misc)))
(cons :tag "Allow feeds to repeat"
(const :tag "Allow feeds to repeat" :repeat-feeds)
(boolean :tag "Allow feeds to repeat"))
(cons :tag "Face"
(const :tag "Face" :faces)
(face :tag "Face")))))
(const :tag "Misc feeds" :misc))))
(defgroup elfeed-summary ()
@ -149,11 +190,15 @@ This is a list of these possible items:
Each found feed will be represented as a line.
- Search `(search . <search-params>)'
Elfeed search, as defined by `elfeed-search-set-filter'.
- Tags tree `(auto-tags . <auto-tags-params>)'
A tree generated automatically from the available tags.
- Tag groups `(tag-groups . <tag-group-params>)'
Insert one tag as one group.
- a few special forms
`<group-params>' is an alist with the following keys:
- `:title' (mandatory)
- `:elements' (mandatory) - elements of the group. The structure is
- `:elements' (mandatory) - elements of the group. The structure is
the same as in the root definition.
- `:face' - group face. The default face is `elfeed-summary-group-face'.
- `:hide' - if non-nil, the group is collapsed by default.
@ -172,8 +217,6 @@ This is a list of these possible items:
- `(or <q-1> <q-2> ... <q-n>)' or `(<q-1> <q-2> ... <q-n>)'
Match if any of the conditions 1, 2, ..., n match.
- `(not <query>)'
Feed tags for the query are determined by the `elfeed-feeds'
variable.
Query examples:
@ -190,6 +233,23 @@ Query examples:
`elfeed-search-set-filter'
- `:title' (mandatory) title.
- `:tags' - list of tags to get the face of the entry.
- `:add-default' - if t, prepend the filter with
`elfeed-summary-default-filter'.
`<auto-tags-params>' is an alist with the following keys:
- `:max-level' - maximum level of the tree (default 2)
- `:source' - which feeds to use to build the tree.
Can be `:misc' (default) or `(query . <query-params>)'.
- `:original-order' - do not try to build a more concise tree by
putting the most frequent tags closer to the root of the tree.
- `:faces' - list of faces for groups.
`<tag-group-params>' is an alist with the following keys:
- `:source' - which feeds to use to build the tree.
Can be `:misc' (default) or `(query . <query-params>)'.
- `:repeat-feeds' - allow feeds to repeat. Otherwise, each feed is
assigned to group with the least amount of members.
- `:face' - face for groups.
Available special forms:
- `:misc' - print out feeds, not found by any query above.
@ -229,6 +289,17 @@ Probably should be one of `elfeed-initial-tags'."
:group 'elfeed-summary
:type 'symbol)
(defcustom elfeed-summary-skip-sync-tag nil
"Do not sync feeds with this tag.
Feeds are tagged in `elfeed-feeds'. Watch out if you're using
elfeed-org, because `rmh-elfeed-org-ignore-tag' is set to \"ignore\"
by default, which seems to remove the feed from `elfeed-feeds'
altogether. This options keeps the feed there, just makes
`elfeed-summary-update' to skip in sync."
:group 'elfeed-summary
:type 'symbol)
(defcustom elfeed-summary-feed-face-fn #'elfeed-summary--feed-face-fn
"Function to get the face of the feed entry.
@ -266,6 +337,18 @@ ordering."
:group 'elfeed-summary
:type 'function)
(defcustom elfeed-summary-auto-tags-group-title-fn
#'elfeed-summary--auto-tags-group-title
"Function to get the title of an autogenerated group.
Accepts the only parameter, which is a tree node created by
`elfeed-summary--arrange-sequences-in-tree'.
See `elfeed-summary--auto-tags-group-title' for the default
implementation."
:group 'elfeed-summary
:type 'function)
(defcustom elfeed-summary-refresh-on-each-update nil
"Whether to refresh the elfeed summary buffer after each update.
@ -315,6 +398,14 @@ Useful only if `elfeed-summary-other-window' is set to t."
"Face for the number of entries of an unread feed or search."
:group 'elfeed-summary)
(defface elfeed-summary-button-face
'((t (:inherit bold)))
"Face for buttons in the elfeed summary buffer.
This has to be distinct from the built-it `widget-button' face because
some themes override the foreground there, which shadows faces in the
button text."
:group 'elfeed-summary)
;;; Logic
(cl-defun elfeed-summary--match-tag (query &key tags title url author title-meta)
@ -479,11 +570,17 @@ SEARCH is a `<search-params>' form as described in
`elfeed-summary-settings'.
Implented the same way as `elfeed-search--update-list'."
(let* ((filter (elfeed-search-parse-filter (alist-get :filter search)))
(let* ((filter-str
(concat
(when (alist-get :add-default search)
elfeed-summary-default-filter)
(alist-get :filter search)))
(filter (elfeed-search-parse-filter filter-str))
(head (list nil))
(tail head)
(unread 0)
(total 0))
(total 0)
unread-ids)
(if elfeed-search-compile-filter
;; Force lexical bindings regardless of the current
;; buffer-local value. Lexical scope uses the faster
@ -496,20 +593,279 @@ Implented the same way as `elfeed-search--update-list'."
tail (cdr tail)
total (1+ total))
(when (member elfeed-summary-unread-tag (elfeed-entry-tags entry))
(setq unread (1+ unread))))))
(setq unread (1+ unread))
(push (elfeed-entry-id entry) unread-ids) ))))
(with-elfeed-db-visit (entry feed)
(when (elfeed-search-filter filter entry feed total)
(setf (cdr tail) (list entry)
tail (cdr tail)
total (1+ total))
(when (member elfeed-summary-unread-tag (elfeed-entry-tags entry))
(setq unread (1+ unread))))))
(setq unread (1+ unread))
(push (elfeed-entry-id entry) unread-ids)))))
`(search . ((params . ,(cdr search))
(faces . ,(funcall elfeed-summary-search-face-fn
(cdr search) unread total))
(unread . ,unread)
(unread-ids . ,unread-ids)
(total . ,total)))))
(defun elfeed-summary--get-tags-ordered ()
"Return the list of elfeed tags, properly ordered.
The tags are ordered (1) by their most frequent position in
`elfeed-feeds' and (2) alphabetically."
(let* ((tags-order
;; list of (tag . ((<position-1> . <freq-1>) (<position-2> . <freq-2>) ...))
(cl-loop with tags-order = '()
for feed in elfeed-feeds
do (cl-loop for tag in (cdr feed)
for i from 0
unless (alist-get tag tags-order)
do (push (list tag) tags-order)
do (cl-incf (alist-get
i (alist-get tag tags-order) 0)))
finally return tags-order))
;; list of (tag . <most-frequent-position>)
(tags-most-freq-order
(cl-loop for (tag . order) in tags-order collect
(cons
tag
(car
(cl-reduce
(lambda (acc value)
(if (> (cdr value) (cdr acc))
value
acc))
order
:initial-value '(-1 . -1)))))))
(mapcar
#'car
(seq-sort
(lambda (datum1 datum2)
(if (not (= (cdr datum1) (cdr datum2)))
(< (cdr datum1) (cdr datum2))
(string-lessp (symbol-name (car datum1))
(symbol-name (car datum2)))))
tags-most-freq-order))))
(defun elfeed-summary--build-tree-auto-tags-reorder-tags (feeds)
"Reorder tags in FEEDS.
FEEDS is a list of (<feed> . <tags>), where <feed> is an instance of
`elfeed-feed' and <tags> is a list of tag symbols."
(let* ((all-tags (elfeed-summary--get-tags-ordered))
(tag-priority (make-hash-table)))
(cl-loop for tag in all-tags
for i from 0
do (puthash tag i tag-priority))
(cl-loop for (feed . tags) in feeds
collect
(cons feed
(seq-sort-by (lambda (tag) (gethash tag tag-priority))
#'> tags)))))
(defun elfeed-summary--compare-sequences (sequence1 sequence2)
"Compare SEQUENCE1 and SEQUENCE2.
Both are lists of symbols."
(cond
((null sequence1) t)
((null sequence2) nil)
(t (let ((item1 (symbol-name (car sequence1)))
(item2 (symbol-name (car sequence2))))
(if (string-equal item1 item2)
(elfeed-summary--compare-sequences (cdr sequence1)
(cdr sequence2))
(string-lessp item1 item2))))))
(defun elfeed-summary--arrange-sequences-in-tree (sequences)
"Arrange SEQUENCES in a tree structure.
Each element of SEQUENCES is a list of symbols.
The resulting structure is an alist of tree nodes with the following keys:
- `value' - the current node symbol
- `children' - child nodes
- `sequences' - sequences at this node
The root of the tree has the value of nil."
(let ((ordered-sequences
(seq-reverse
(seq-sort #'elfeed-summary--compare-sequences sequences)))
(tree `(,nil . ((value . ,nil) (children . ,nil) (sequences . ,nil))))
current-tree-pos
(processed-sequences (make-hash-table :test #'equal)))
(dolist (sequence ordered-sequences)
(unless (gethash sequence processed-sequences)
(setq current-tree-pos tree)
(dolist (value sequence)
(if-let ((value-in-tree (alist-get value (alist-get 'children current-tree-pos))))
(setq current-tree-pos value-in-tree)
(setq current-tree-pos
(setf
(alist-get value (alist-get 'children current-tree-pos))
`((value . ,value) (children . ,nil) (sequences . ,nil))))))
(push sequence (alist-get 'sequences current-tree-pos))
(puthash sequence t processed-sequences)))
tree))
(defun elfeed-summary--auto-tags-group-title (child-tree)
"Default function to get the name of an auto-tags group.
CHILD-TREE is a structure as defined in
`elfeed-summary--arrange-sequences-in-tree', with tag lists as
sequences."
(symbol-name (alist-get 'value child-tree)))
(defun elfeed-summary--build-tree-auto-tags-recursive
(param tree feeds-by-tag-sequence unread-count total-count &optional level)
"Recursively create the auto-tags tree.
PARAM is an `<auto-tags-params>' form as described in
`elfeed-summary-settings'. TREE is the result of applying
`elfeed-summary--arrange-sequences-in-tree' onto the list of tags of
all feeds.
FEEDS-BY-TAG-SEQUENCE is a hashmap with lists of tags as keys and
instances of `elfeed-feed' as values. This is used to figure out
feeds in a particular TREE node.
UNREAD-COUNT and TOTAL-COUNT are hashmaps with feed ids as keys and
corresponding numbers of entries as values.
LEVEL is the current level of recursion, which is 0 by default."
(unless level
(setq level 0))
(let ((max-level (or (alist-get :max-level (cdr param)) 2))
(face (when-let (faces (alist-get :faces (cdr param)))
(nth (% level (length faces)) faces))))
(append
;; Just append all the feeds at the current level
(cl-loop for sequence in (alist-get 'sequences tree) append
(cl-loop for feed in (seq-sort
#'elfeed-summary--feed-sort-fn
(gethash sequence feeds-by-tag-sequence))
collect (elfeed-summary--build-tree-feed
feed unread-count total-count)))
;; Go deeper if we can
(when (< level max-level)
(cl-loop
for datum in (alist-get 'children tree)
for child-tree = (cdr datum) collect
`(group . ((params . ((:title
. ,(funcall elfeed-summary-auto-tags-group-title-fn
child-tree))))
(face . ,face)
(children . ,(elfeed-summary--build-tree-auto-tags-recursive
param child-tree feeds-by-tag-sequence
unread-count total-count (1+ level)))))))
;; If we can't go deeper, this will just append all the feeds to
;; the current level anyway
(when (>= level max-level)
(cl-loop for datum in (alist-get 'children tree) append
(elfeed-summary--build-tree-auto-tags-recursive
param (cdr datum) feeds-by-tag-sequence
unread-count total-count (1+ level)))))))
(defun elfeed-summary--build-tree-get-feeds (param misc-feeds)
"Get feeds for PARAM.
PARAM is an alist with the optional `:source' key. The value can be
either `(query . <query-params>)' or `:misc' (default).
MISC-FEEDS is the list of feeds used for `:misc'.
The result is a list of items like (`<feed>' tag1 tag2 ...), where
`<feed>' is an instance of `elfeed-feed'."
(let* ((source (alist-get :source (cdr param))))
(mapcar
(lambda (feed)
(cons feed (alist-get (elfeed-feed-id feed) elfeed-feeds
nil nil #'equal)))
(cond ((or (eq source :misc) (null source))
misc-feeds)
((and (listp source) (eq (car source) 'query))
(elfeed-summary--get-feeds (cdr source)))
(t (error "Invalid source: %s" source))))))
(defun elfeed-summary--build-tree-auto-tags (param unread-count total-count misc-feeds)
"Create the auto-tags tree.
PARAM is a cons cell `(auto-tags . <auto-tags-params>)', where
`<auto-tags-params>' is described in `elfeed-summary-settings'.
UNREAD-COUNT and TOTAL-COUNT are hashmaps with feed ids as keys and
corresponding numbers of entries as values.
MISC-FEEDS is a list of feeds that were not used in PARAMS."
(let ((feeds (elfeed-summary--build-tree-get-feeds param misc-feeds))
(reorder-tags (not (alist-get :original-order (cdr param)))))
(when reorder-tags
(setq feeds (elfeed-summary--build-tree-auto-tags-reorder-tags feeds)))
(let ((tree (elfeed-summary--arrange-sequences-in-tree
(mapcar #'cdr feeds)))
(feeds-by-tag-sequence (make-hash-table :test #'equal)))
(cl-loop
for (feed . sequence) in feeds
do (puthash sequence (cons feed (gethash sequence feeds-by-tag-sequence))
feeds-by-tag-sequence))
(elfeed-summary--build-tree-auto-tags-recursive
param tree feeds-by-tag-sequence unread-count total-count))))
(defun elfeed-summary--build-tree-tag-groups (param unread-count total-count misc-feeds)
"Create the tag-groups tree.
PARAM is a cell of `(tag-groups . <tag-group-params>)', with the
`<tag-group-params>' form as defined in `elfeed-summary-settings'.
UNREAD-COUNT and TOTAL-COUNT are hashmaps with feed ids as keys and
corresponding numbers of entries as values.
MISC-FEEDS is a list of feeds that were not used in PARAMS."
(let ((feeds (elfeed-summary--build-tree-get-feeds param misc-feeds))
(repeat-feeds (alist-get :repeat-feeds (cdr param)))
(face (alist-get :face (cdr param)))
(groups (make-hash-table)))
(if (not repeat-feeds)
(let ((tag-freqs (make-hash-table)))
(cl-loop for feed in feeds do
(cl-loop for tag in (cdr feed) do
(puthash
tag (1+ (gethash tag tag-freqs 0))
tag-freqs)))
(cl-loop for feed in feeds
for min-freq-tag = (cl-reduce
(lambda (acc tag)
(let ((freq (gethash tag tag-freqs)))
(if (or (null (cdr acc)) (< freq (cdr acc)))
(cons tag freq)
acc)))
(cdr feed)
:initial-value '(nil . nil))
when min-freq-tag do
(puthash (car min-freq-tag)
(cons (car feed) (gethash min-freq-tag groups))
groups)))
(cl-loop for feed in feeds do
(cl-loop for tag in (cdr feed) do
(puthash tag (cons (car feed) (gethash tag groups)) groups))))
(let ((groups-list (seq-sort-by
(lambda (f) (symbol-name (car f)))
#'string-lessp
(cl-loop for tag being the hash-keys of groups
using (hash-values feeds)
collect (cons tag feeds)))))
(cl-loop for (tag . feeds) in groups-list
collect `(group
. ((params . ((:title . ,(symbol-name tag))))
(face . ,face)
(children . ,(mapcar
(lambda (feed) (elfeed-summary--build-tree-feed
feed unread-count total-count))
(seq-sort
#'elfeed-summary--feed-sort-fn feeds)))))))))
(defun elfeed-summary--build-tree (params unread-count total-count misc-feeds)
"Recursively create the summary details tree.
@ -534,8 +890,15 @@ The resulting form is described in `elfeed-summary--get-data'."
append (cl-loop for feed in (elfeed-summary--get-feeds (cdr param))
collect (elfeed-summary--build-tree-feed
feed unread-count total-count))
else if (and (listp param) (eq (car param) 'auto-tags))
append (elfeed-summary--build-tree-auto-tags
param unread-count total-count misc-feeds)
else if (and (listp param) (eq (car param) 'tag-groups))
append (elfeed-summary--build-tree-tag-groups
param unread-count total-count misc-feeds)
else if (eq param :misc)
append (cl-loop for feed in misc-feeds
append (cl-loop for feed in (seq-sort #'elfeed-summary--feed-sort-fn
misc-feeds)
collect (elfeed-summary--build-tree-feed
feed unread-count total-count))
else do (error "Can't parse: %s" (prin1-to-string param))))
@ -549,7 +912,13 @@ PARAMS is a form as described in `elfeed-summary-settings'."
append (elfeed-summary--extract-feeds
(cdr (assoc :elements (cdr param))))
else if (and (listp param) (eq (car param) 'query))
append (elfeed-summary--get-feeds (cdr param))))
append (elfeed-summary--get-feeds (cdr param))
else if (and (listp param)
(or (eq (car param) 'auto-tags) (eq (car param) 'tag-groups))
(eq (car-safe (alist-get :source (cdr param)))
'query))
append (elfeed-summary--get-feeds
(cdr (alist-get :source (cdr param))))))
(defun elfeed-summary--ensure ()
"Ensure that elfeed database is loaded and feeds are set up."
@ -589,6 +958,7 @@ The return value is a list of alists of the following elements:
`elfeed-summary-settings'.
- `faces' - list of faces for the search entry.
- `unread' - number of unread entries in the search results.
- `unread-ids' - ids of unread entries for marking them as read.
- `total' - total number of entries in the search results."
(let* ((feeds (elfeed-summary--extract-feeds
elfeed-summary-settings))
@ -608,7 +978,7 @@ The return value is a list of alists of the following elements:
(puthash (elfeed-feed-id feed)
(1+ (or (gethash (elfeed-feed-id feed) unread-count) 0))
unread-count))
(when (> (- (time-convert nil 'integer)
(when (> (- (float-time)
elfeed-summary-look-back)
(elfeed-entry-date entry))
(elfeed-db-return)))
@ -634,6 +1004,20 @@ The return value is a list of alists of the following elements:
(defvar elfeed-summary--search-mark-read nil
"If t, mark the feed as read instead of switching to it.")
(defun elfeed-summary--magit-section-toggle-workaround (section)
"`magit-section-toggle' with a workaround for invisible lines.
SECTION is an instance of `magit-section'.
No idea what I'm doing wrong, but this seems to help."
(interactive (list (save-excursion
(let ((lines (count-lines (point-min) (point-max))))
(while (and (invisible-p (point))
(< (line-number-at-pos) lines))
(forward-line 1)))
(magit-current-section))))
(magit-section-toggle section))
(defvar elfeed-summary-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map magit-section-mode-map)
@ -642,13 +1026,16 @@ The return value is a list of alists of the following elements:
(define-key map (kbd "q") #'elfeed-summary-quit-window)
(define-key map (kbd "r") #'elfeed-summary--refresh)
(define-key map (kbd "R") #'elfeed-summary-update)
(define-key map (kbd "p") #'elfeed-summary-update-at-point)
(define-key map (kbd "u") #'elfeed-summary-toggle-only-unread)
(define-key map (kbd "U") #'elfeed-summary--action-mark-read)
(define-key map (kbd "<tab>") #'elfeed-summary--magit-section-toggle-workaround)
(when (fboundp #'evil-define-key*)
(evil-define-key* 'normal map
(kbd "<tab>") #'magit-section-toggle
(kbd "<tab>") #'elfeed-summary--magit-section-toggle-workaround
"r" #'elfeed-summary--refresh
"R" #'elfeed-summary-update
"p" #'elfeed-summary-update-at-point
"u" #'elfeed-summary-toggle-only-unread
(kbd "RET") #'elfeed-summary--action
"M-RET" #'elfeed-summary--action-show-read
@ -671,10 +1058,10 @@ The return value is a list of alists of the following elements:
If there's a widget at the point, pass the press event to the widget.
That should result in the call to
`elfeed-summary--search-feed-notify'. Otherwise, if there's a group
`elfeed-summary--feed-notify'. Otherwise, if there's a group
section, run the corresponding action for the group.
The behavior of both `elfeed-summary--search-feed-notify' and
The behavior of both `elfeed-summary--feed-notify' and
`elfeed-summary--open-section' is modified by lexically scoped
variables `elfeed-summary--search-show-read' and
`elfeed-summary--search-mark-read'.
@ -730,8 +1117,8 @@ If `elfeed-summary-other-window' is t, open elfeed in other window."
(enlarge-window (- elfeed-summary-width
(window-width))
t))))
(switch-to-buffer (elfeed-search-buffer)))
(unless (eq major-mode 'elfeed-search-mode)
(display-buffer (elfeed-search-buffer)))
(with-current-buffer (elfeed-search-buffer)
(elfeed-search-mode)))
(defun elfeed-summary--goto-feed (feed show-read)
@ -747,13 +1134,12 @@ items."
show-read)
(format "+%s " elfeed-summary-unread-tag))
"="
(rx-to-string (elfeed-feed-id feed) t)
)))
(rx-to-string (elfeed-feed-id feed) t))))
(defun elfeed-summary--search-feed-notify (widget &rest _)
"A function to run in `:notify' in a feed widget button.
(defun elfeed-summary--feed-notify (widget &rest _)
"The function to run in `:notify' in a feed widget button.
WIDGET is an instance of the pressed widget."
WIDGET is the instance of the pressed widget."
(cond
(elfeed-summary--search-mark-read
(elfeed-summary--mark-read (list (widget-get widget :feed))))
@ -790,10 +1176,7 @@ SECTION is an instance of `elfeed-summary-group-section'."
(format "+%s " elfeed-summary-unread-tag))
(mapconcat
(lambda (feed)
(format "=%s" (replace-regexp-in-string
(rx "?" (* not-newline) eos)
""
(elfeed-feed-id feed))))
(format "=%s" (rx-to-string (elfeed-feed-id feed) t)))
feeds
" ")))))))
@ -820,12 +1203,49 @@ descent."
'elfeed-summary-count-face))
(propertize title 'face (alist-get 'faces data)))))
(widget-create 'push-button
:notify #'elfeed-summary--search-feed-notify
:notify #'elfeed-summary--feed-notify
:feed feed
:only-read (= 0 (alist-get 'unread data))
:button-face 'elfeed-summary-button-face
text)
(insert "\n")))
(defun elfeed-summary--mark-read-ids (ids)
"Mark elfeed entries with IDS as read."
(when (or (not elfeed-summary-confirm-mark-read)
(y-or-n-p "Mark all entries in feed as read? "))
(let ((ids-hash (make-hash-table)))
(dolist (id ids)
(puthash id t ids-hash))
(with-elfeed-db-visit (entry feed)
;; XXX to shut up the byte compiler
(ignore feed)
(when (and
(gethash (elfeed-entry-id entry) ids-hash nil)
(member elfeed-summary-unread-tag (elfeed-entry-tags entry)))
(setf (elfeed-entry-tags entry)
(seq-filter (lambda (tag) (not (eq elfeed-summary-unread-tag tag)))
(elfeed-entry-tags entry))))))
(elfeed-summary--refresh)))
(defun elfeed-summary--search-notify (widget &rest _)
"The function to run in `:notify' in a search widget button.
WIDGET is the instance of the pressed widget."
(cond
(elfeed-summary--search-mark-read
(elfeed-summary--mark-read-ids
(widget-get widget :unread-ids)))
(t (elfeed-summary--open-elfeed)
(elfeed-search-set-filter
(concat
(when (widget-get widget :add-default)
elfeed-summary-default-filter)
(widget-get widget :filter)
(unless (or elfeed-summary--search-show-read
(widget-get widget :only-read))
(format " +%s " elfeed-summary-unread-tag)))))))
(defun elfeed-summary--render-search (data _level)
"Render a search item for the elfeed summary buffer.
@ -850,11 +1270,11 @@ descent."
'face
(alist-get 'faces data)))))
(widget-create 'push-button
:notify (lambda (widget &rest _)
(elfeed-summary--open-elfeed)
(elfeed-search-set-filter
(widget-get widget :filter)))
:notify #'elfeed-summary--search-notify
:filter (alist-get :filter search-data)
:only-read (= 0 (alist-get 'unread data))
:add-default (alist-get :add-default search-data)
:unread-ids (alist-get 'unread-ids data)
text)
(widget-insert "\n")))
@ -1055,11 +1475,32 @@ summary buffer."
(with-current-buffer buffer
(elfeed-summary--refresh))))
(defun elfeed-summary-update ()
"Update all the feeds in `elfeed-feeds' and the summary buffer."
(interactive)
(elfeed-log 'info "Elfeed update: %s"
(format-time-string "%B %e %Y %H:%M:%S %Z"))
(defun elfeed-summary--feed-list ()
"Return a flat list version of `elfeed-feeds'.
This is a modification of `elfeed-feed-list' that takes
`elfeed-summary-skip-sync-tag' in account. The return value is a list
of string."
;; Validate elfeed-feeds and fail early rather than asynchronously later.
(dolist (feed elfeed-feeds)
(unless (cl-typecase feed
(list (and (stringp (car feed))
(cl-every #'symbolp (cdr feed))))
(string t))
;; Chris, package-lint doesn't like your code :P
(error "`elfeed-feeds' malformed, bad entry: %S" feed)))
(cl-loop for feed in elfeed-feeds
when (and (listp feed)
(not (memq elfeed-summary-skip-sync-tag
(cdr feed))))
collect (car feed)
else if (not (listp feed)) collect feed))
(defvar elfeed-summary--update-start-time nil
"Time when the current elfeed update started.")
(defun elfeed-summary--update (feeds)
"Update elfeed FEEDS."
;; XXX Here's a remarkably dirty solution. This command is meant to
;; refresh the elfeed-summary buffer after all the feeds have been
;; updated. But elfeed doesn't seem to provide anything to hook
@ -1076,8 +1517,10 @@ summary buffer."
(not (or (and (listp hook) (eq (car hook) 'closure))
(byte-code-function-p hook))))
elfeed-update-hooks))
(setq elfeed-summary--update-start-time (time-convert nil 'integer))
(let* ((elfeed--inhibit-update-init-hooks t)
(remaining-feeds (elfeed-feed-list))
(remaining-feeds (seq-copy feeds))
(feed-count (length remaining-feeds))
(elfeed-update-closure
(lambda (url)
(message (if (> (elfeed-queue-count-total) 0)
@ -1085,7 +1528,12 @@ summary buffer."
(in-process (elfeed-queue-count-active)))
(format "%d jobs pending, %d active..."
(- total in-process) in-process))
"Elfeed update completed"))
(format "Elfeed update completed: %s feeds in %s"
feed-count
(format-seconds
"%M, %S" (time-subtract
(time-convert nil 'integer)
elfeed-summary--update-start-time)))))
(setq remaining-feeds
(seq-remove
(lambda (url-1)
@ -1101,10 +1549,40 @@ summary buffer."
elfeed-summary-refresh-on-each-update)
(elfeed-summary--refresh-if-exists)))))
(add-hook 'elfeed-update-hooks elfeed-update-closure)
(mapc #'elfeed-update-feed (elfeed--shuffle (elfeed-feed-list)))
(mapc #'elfeed-update-feed (elfeed--shuffle feeds))
(run-hooks 'elfeed-update-init-hooks)
(elfeed-db-save)))
(defun elfeed-summary-update ()
"Update all feeds in `elfeed-feeds' and the summary buffer."
(interactive)
(elfeed-log 'info "Elfeed update: %s"
(format-time-string "%B %e %Y %T %Z"))
(elfeed-summary--update (elfeed-summary--feed-list)))
(defun elfeed-summary-update-at-point ()
"Update `elfeed-summary' feeds at point."
(interactive)
(let ((feeds (or
(when-let (feed (widget-get (get-char-property (point) 'button) :feed))
(list (elfeed-feed-url feed)))
(when-let (section (magit-current-section))
(when (slot-boundp section 'group)
(mapcar #'elfeed-feed-url
(elfeed-summary--group-extract-feeds
(oref section group)))))))
(ignore-feeds
(cl-loop for feed in elfeed-feeds
when (and (listp feed) (memq elfeed-summary-skip-sync-tag
(cdr feed)))
collect (car feed))))
(unless feeds
(user-error "No feeds at point"))
(setq feeds (seq-difference feeds ignore-feeds))
(unless feeds
(user-error "All feeds at point are ignored"))
(elfeed-summary--update feeds)))
(defvar elfeed-summary--setup nil
"Whether elfeed summary was set up.")
@ -1154,7 +1632,7 @@ options."
(with-current-buffer buffer
(elfeed-summary--render
(elfeed-summary--get-data)))
(switch-to-buffer buffer)
(display-buffer buffer)
(goto-char (point-min))))
(provide 'elfeed-summary)