Lazy Elfeed

Elfeed, the feed reader for Emacs, is built on several clever ideas. The basic mode of interaction in Elfeed is through search, specifying what kinds of feeds you want to peruse at the moment. This removes the distinction between an inbox and a search result, which works surprisingly well with the expressive search syntax. The second is integration: search results can be stored as regular bookmarks, and jumping to the bookmark from anywhere in Emacs will run the corresponding query and present you with fresh results. The third is organizing by tags instead of folders. Web services like Gmail use some of these concepts (and probably pioneered them too), but the difference between web-based readers and Emacs in speed, customizability, general hackability and use is night and day.

Elfeed’s default interface has all the right pieces, but I found it needed just a little tweaking to fit what I expect from a feed reader.

Display the feed list and current entry in a split pane setup 1:

Elfeed has two kinds of views: The search view which shows feed entries matching a search query, and the show view to read a specific entry. By default only one of these is active at a time, but a more useful split-pane setup is a few tweaks away:

Stay in the search view and preview entries.

There are only two pieces to this. The first is to specify the function we want Elfeed to use to display the buffer with the current entry. In this case write our own elfeed-display-buffer to set the height.

(setq elfeed-show-entry-switch #'elfeed-display-buffer)

(defun elfeed-display-buffer (buf &optional act)
  (pop-to-buffer buf)
  (set-window-text-height (get-buffer-window) (round (* 0.7 (frame-height)))))

A different but less self-contained way would be to set it to use the built in pop-to-buffer but add an entry to display-buffer-alist as an instruction on what to do with elfeed entry buffers.

The second is to set up a keybinding to advance to the next or previous feed in the search window, with a little elbow grease. 2

;; -*- lexical-binding: t -*-
  (defun elfeed-search-show-entry-pre (&optional lines)
  "Returns a function to scroll forward or back in the Elfeed
  search results, displaying entries without switching to them."
      (lambda (times)
        (interactive "p")
        (forward-line (* times (or lines 0)))
        (call-interactively #'elfeed-search-show-entry)
        (select-window (previous-window))
        (unless elfeed-search-remain-on-entry (forward-line -1))))

  (define-key elfeed-search-mode-map (kbd "n") (elfeed-search-show-entry-pre +1))
  (define-key elfeed-search-mode-map (kbd "p") (elfeed-search-show-entry-pre -1))
  (define-key elfeed-search-mode-map (kbd "M-RET") (elfeed-search-show-entry-pre))

Alternatively, you might prefer to stay in the “show” window and just go through each entry by content instead of header. You can use the space bar to scroll the view or move to the next entry as appropriate.

Go forward (scroll or next entry) with SPC, back with S-SPC

Google Reader had one button navigation, and it is a level of laziness I have aspired to since.

(defun elfeed-scroll-up-command (&optional arg)
  "Scroll up or go to next feed item in Elfeed"
  (interactive "^P")
  (let ((scroll-error-top-bottom nil))
    (condition-case-unless-debug nil
        (scroll-up-command arg)
      (error (elfeed-show-next)))))

(defun elfeed-scroll-down-command (&optional arg)
  "Scroll up or go to next feed item in Elfeed"
  (interactive "^P")
  (let ((scroll-error-top-bottom nil))
    (condition-case-unless-debug nil
        (scroll-down-command arg)
      (error (elfeed-show-prev)))))

(define-key elfeed-show-mode-map (kbd "SPC") 'elfeed-scroll-up-command)
(define-key elfeed-show-mode-map (kbd "S-SPC") 'elfeed-scroll-down-command)

Easier tagging

By default you can tag an entry from the search results by typing “+” followed by the tag name. This is too much work for a central organizational feature, so a little helper sorts things out.

(defun elfeed-tag-selection-as (mytag)
    "Returns a function that tags an elfeed entry or selection as
    (lambda ()
      "Toggle a tag on an Elfeed search selection"
      (elfeed-search-toggle-all mytag)))

Tag with a single letter keybind:

(define-key elfeed-search-mode-map "l" (elfeed-tag-selection-as 'readlater))
(define-key elfeed-search-mode-map "d" (elfeed-tag-selection-as 'junk))

Open feeds in Emacs

Elfeed will open links to feeds in your default browser with “b”. But for text heavy posts or to look up something specific I often want to open them in place in Emacs:

Quick-preview links in eww.

Instead of writing a function to do this from scratch, we set eww (Emacs’ built in web browser) as the default in a short-lived let binding and let the usual browse-url functions handle the rest:

(defun elfeed-show-eww-open (&optional use-generic-p)
  "open with eww"
  (interactive "P")
  (let ((browse-url-browser-function #'eww-browse-url))
    (elfeed-show-visit use-generic-p)))

(defun elfeed-search-eww-open (&optional use-generic-p)
  "open with eww"
  (interactive "P")
  (let ((browse-url-browser-function #'eww-browse-url))
    (elfeed-search-browse-url use-generic-p)))

(define-key elfeed-show-mode-map (kbd "B") 'efleed-show-eww-open)
(define-key elfeed-search-mode-map (kbd "B") 'efleed-search-eww-open)

We can take this idea further. The following snippet will ensure that Emacs (and thus Elfeed) defaults to opening Youtube links in mpv (or the video player of your choice), cutting the browser out of the loop entirely. 3

(setq browse-url-browser-function
      '(("https:\\/\\/www\\.youtu\\.*be." . browse-url-mpv)
        ("." . browse-url-generic)))

(defun browse-url-mpv (url &optional single)
  (start-process "mpv" nil "mpv" (shell-quote-argument url)))

  1. There is Elfeed Goodies, a package on github that provides a split pane setup to Elfeed. But it’s rather large for what it does, has a dependency I don’t need (popwin.el) for a single function and provides much else that I have no use for. ↩︎

  2. A previous version of this write-up used macros instead of closures for the helpers. This simpler solution was suggested by /u/nv-elisp on Reddit↩︎

  3. mpv requires Youtube-dl to play youtube videos. ↩︎