Elfeed Tips and Tricks

This past weekend I had some questions from next-user-here (NUH) on my original Elfeed post about changing some of Elfeed’s behavior. NUH is an Elisp novice so accomplishing some of the requested modifications wasn’t obvious. A novice is mostly limited to setting variables, not defining advice or using hooks. I’ve also been using Elfeed daily for about three months now as my sole web feed reader and along the way I’ve developed some best practices. In addition to responding to some of NIH’s questions here, I’d like to share some tips and tricks.

Custom Entry Launchers

Currently you can press “b” to launch one or more entries in your browser. You can use “y” to copy an single entry to the clipboard. What if you want to make another action.

In my configuration I have a fancy binding that sends the entry URLs in the selected region to youtube-dl for downloading the videos. It’s too large to share as a snippet so here’s a small example of something similar using a program called xcowsay.

(defun xcowsay (message)
  (call-process "xcowsay" nil nil nil message))

(defun elfeed-xcowsay ()
  (interactive)
  (let ((entry (elfeed-search-selected :single)))
    (xcowsay (elfeed-entry-title entry))))

(define-key elfeed-search-mode-map "x" #'elfeed-xcowsay)

Now when I hit “x” over an entry in Elfeed I’m greeted by a cow announcing the title.

Entry Listing Customization

The search buffer you see when starting Elfeed, where entries are listed, can be customized a few different ways. First, this buffer does grow dynamically. After re-sizing the window/frame horizontally you just have to refresh the view by pressing g (an Emacs convention). How it fills out depends on the settings of these variables,

They control how wide the different columns should be as the window size changes. An important caveat to this is that the cache stored in elfeed-search-cache must be cleared before the changes will be reflected in the display. This cache exists because building the display, assembling all the special faces, is actually quite CPU-intensive. It was an optimization I established early on.

(clrhash elfeed-search-cache)

If you set these variables in your start-up configuration you don’t need to worry about clearing the cache because it will already be empty. It’s only a concern when playing with the settings.

Date Display

Another question was about adding time to the entry listing. Elfeed only displays the entry’s date. Dates are formatted by the function elfeed-search-format-date. This can be redefined to display dates differently.

(defun elfeed-search-format-date (date)
  (format-time-string "%Y-%m-%d %H:%M" (seconds-to-time date)))

It’s given epoch seconds as a float and it returns a string to display as a date.

Faces and Colors

All of the faces used in the display are declared for customization, so these can be changed to whatever you like.

Say you suffered a head injury and decided you want your Elfeed dates to be bold, purple, and underlined,

(custom-set-faces
 '(elfeed-search-date-face
   ((t :foreground "#f0f"
       :weight extra-bold
       :underline t))))

Database Manipulation

Feeds and entries in the database can be manipulated to become whatever you want them to be. Because Elfeed is regularly modifying the database, the trick is to perform the manipulation at just the right time.

Feed Title Changes

Say you want to change a feed title because you don’t like the title supplied by the feed. For example, the title to my blog’s feed is “null program” but instead you think it should be “Seriously Handsome Programmer” (head injury, remember?). The function elfeed-db-get-feed can be used to fetch a feed’s data structure from the database, given it’s exact URL as listed in your elfeed-feeds.

(let ((feed (elfeed-db-get-feed "https://nullprogram.com/feed/")))
  (setf (elfeed-feed-title feed) "Seriously Handsome Programmer"))

Hold it, that didn’t work. First, that display cache is getting in the way again. Feed titles change very infrequently so they’re cached aggressively. More importantly, next time you update your feeds Elfeed will re-synchronize the feed title with the official title. It’s going to fight against your intervention.

The solution is to do it with a little bit of advice just before the title is displayed. Advise the function elfeed-search-update with some “before” advice.

(defadvice elfeed-search-update (before nullprogram activate)
  (let ((feed (elfeed-db-get-feed "https://nullprogram.com/feed/")))
    (setf (elfeed-feed-title feed) "Seriously Handsome Programmer")))

Entry Tweaking

Automatic entry modification should happen immediately upon discovery so that it looks like the entry arrived that way. This is done through the elfeed-new-entry-hook. Generally this would be used for applying custom tags. These examples are from the documentation:

;; Mark all YouTube entries
(add-hook 'elfeed-new-entry-hook
          (elfeed-make-tagger :feed-url "youtube\\.com"
                              :add '(video youtube)))

;; Entries older than 2 weeks are marked as read
(add-hook 'elfeed-new-entry-hook
          (elfeed-make-tagger :before "2 weeks ago"
                              :remove 'unread))

;; Building subset feeds
(add-hook 'elfeed-new-entry-hook
          (elfeed-make-tagger :feed-url "example\\.com"
                              :entry-title '(not "something interesting")
                              :add 'junk
                              :remove 'unread))

Due to a feature I recently ported from my personal configuration, this tagger helper function is less necessary. You can put lists in your elfeed-feeds list to supply automatic tags.

(setq elfeed-feeds
      '(("https://nullprogram.com/feed/" blog emacs)
        "http://www.50ply.com/atom.xml"  ; no autotagging
        ("http://nedroid.com/feed/" webcomic)))

Content Tweaking

Going beyond tagging you could change the content of the feed. Say you want to make feeds 100 times better.

(defun hundred-times-better (entry)
  (let* ((original (elfeed-deref (elfeed-entry-content entry)))
         (replace (replace-regexp-in-string "keyboard" "leopard" original)))
    (setf (elfeed-entry-content entry) (elfeed-ref replace))))

(add-hook 'elfeed-new-entry-hook #'hundred-times-better)

The same trick could be used to remove advertising, change the date, change the title, etc. The elfeed-deref and elfeed-ref parts are needed to fetch and store content in the content database. Only a reference is stored on the structure. You can actually use these functions at any time outside of Elfeed, but they’ll eventually get garbage collected if Elfeed doesn’t know about them.

(setf ref (elfeed-ref "Hello, World"))
;; => [cl-struct-elfeed-ref "907d14fb3af2b0d4f18c2d46abe8aedce17367bd"]

(elfeed-deref ref)
;; => "Hello, World"

Deletion

A question that’s been asked few times is if entries can be deleted. To start off, the answer to that question is “no.” There is no function provided to remove entries from the database. If you want to remove entries you’re probably taking the wrong approach.

The main problem with removal is that Elfeed needs to keep track of what it’s seen before. If an entry is removed and then rediscovered, it will reappear as unread. There are better ways to “remove” entries, such as tagging them specially.

On a moderately-powerful computer Elfeed can easily handle at least several tens of thousands of database entries. If “too many entries” ever becomes a performance problem I’d rather solve it by making the database faster than by removing information from the database. It’s already very date-oriented so that older entries are infrequently touched.

If storage is a concern, you shouldn’t get too worked up about that. As of this post I have about 6,000 entries in my database and the index file is only 3.5 MB. The content database after garbage collection, which is the data/ directory under ~/.elfeed/, with these 6k entries is 17MB. When I run M-x elfeed-db-compact, currently an experimental feature, it drops down to 1.8MB. That’s less than 1 kB per entry. It’s also less than my personal Liferea database of roughly the same amount of content (~15MB) before I wrote Elfeed.

If even this storage is still too much you can always blow away your data/ content database directory. This is safe to do even while Emacs is running. You’ll still see all of the entries listed in the search buffer but won’t be able to read them within Emacs until after the next database update (when it re-fetches the most recent entry content).

You can also clear out the content database from within Elisp by visiting every entry and clearing its content field.

(with-elfeed-db-visit (entry _)
  (setf (elfeed-entry-content entry) nil))

(elfeed-db-gc)  ;; garbage collect everything

The same sort of expression can be used to run over all known entries to perform other changes. If there was a delete function you might use it here to remove entries older than a certain date, then hope they’re not rediscovered.

If you never want to store entry content (you never read entries within Emacs), you can use a hook to always drop it on the floor as it arrives,

(add-hook 'elfeed-new-entry-hook
          (lambda (entry) (setf (elfeed-entry-content entry) nil)))

Questions?

If you have any questions or suggestions about how to make Elfeed do what you want it to do, feel free to ask. Some things may actually require that I make changes to Elfeed to support it, though I hope I’ve anticipated your particular need well enough to avoid that.

Have a comment on this article? Start a discussion in my public inbox by sending an email to ~skeeto/public-inbox@lists.sr.ht [mailing list etiquette] , or see existing discussions.

null program

Chris Wellons

wellons@nullprogram.com (PGP)
~skeeto/public-inbox@lists.sr.ht (view)