2024-07-14
Tags: emacs
I decided to start using an RSS client, primarily because I kept
coming back to these same few blogs with really nice content and
wanted to stay updated, but also because the signal-to-noise ratio
on e.g. Reddit is horrendous and mindlessly doomscrolling is not a
good use of my time.
I've gone through a real Emacs ricing stint lately, and as the
lines of elisp in my config continue to accrue, so too does the
gravitational pull of my favorite text-based operating system
gradually increase --- email, IRC, and now RSS have fallen through
the event horizon.
I decided to pick up Elfeed and I wasn't disappointed; you can tell
that Chris Wellons, despite having turned to the dark side [1], has
put a lot of thought into the UX and extensibility of it.
== [1m[4mElfeed Hacks[22m[24m
Here are a few lines from my config that I think might be of value
to other people:
[1m[37m([0m[37muse-package[0m[1m[37m [0m[1m[36melfeed[0m[1m[37m[0m
[1m[37m [0m[37m:config[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mdefun[0m[1m[37m [0m[1m[36melfeed-clean-trash[0m[1m[37m [0m[1m[37m([0m[33m&optional[0m[1m[37m [0m[1m[36mnow[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37minteractive[0m[1m[37m [0m[33m"P"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mwith-elfeed-db-visit[0m[1m[37m [0m[1m[37m([0m[1m[36mentry[0m[1m[37m [0m[1m[36m_[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mwhen[0m[1m[37m [0m[1m[37m([0m[1m[36melfeed-tagged-p[0m[1m[37m [0m[33m'trash[0m[1m[37m [0m[1m[36mentry[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37msetf[0m[1m[37m [0m[1m[37m([0m[1m[36melfeed-entry-content[0m[1m[37m [0m[1m[36mentry[0m[1m[37m)[0m[1m[37m [0m[1m[37mnil[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mif[0m[1m[37m [0m[1m[36mnow[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36melfeed-db-gc[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mrun-with-idle-timer[0m[1m[37m [0m[1m[36m15[0m[1m[37m [0m[1m[37mnil[0m[1m[37m [0m[37m#'[0m[1m[36melfeed-db-gc[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mdefface[0m[1m[37m [0m[1m[36melfeed-search-trashed-title-face[0m[1m[37m[0m
[1m[37m [0m[1m[36m'[0m[1m[37m(((([0m[1m[36mclass[0m[1m[37m [0m[1m[36mcolor[0m[1m[37m))[0m[1m[37m [0m[1m[37m([0m[37m:foreground[0m[1m[37m [0m[33m"red"[0m[1m[37m [0m[37m:weight[0m[1m[37m [0m[1m[36mextra-bold[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[33m"Face used in search mode for trashed entries."[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mpush[0m[1m[37m [0m[1m[36m'[0m[1m[37m([0m[1m[36mtrash[0m[1m[37m [0m[1m[36melfeed-search-trashed-title-face[0m[1m[37m)[0m[1m[37m [0m[1m[36melfeed-search-face-alist[0m[1m[37m)[0m[1m[37m[0m
So Elfeed doesn't really have a concept of "deleting" entries, but
you can delete the content of an entry if you're worried about disk
space; given that I subscribe to news aggregators, this is a
legitimate concern.
[1m[37m [0m[1m[37m([0m[37msetq-default[0m[1m[37m [0m[1m[36melfeed-search-filter[0m[1m[37m [0m[33m"-trash @6-months-ago +unread"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37msetq[0m[1m[37m [0m[1m[36mfysh/elfeed-filters[0m[1m[37m [0m[1m[36m'[0m[1m[37m(([0m[33m"Blogs"[0m[1m[37m [0m[1m[36m.[0m[1m[37m [0m[33m"-trash +blog"[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[33m"Comics"[0m[1m[37m [0m[1m[36m.[0m[1m[37m [0m[33m"-trash +webcomic"[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[33m"News"[0m[1m[37m [0m[1m[36m.[0m[1m[37m [0m[33m"-trash @1-week-ago +aggregator +unread"[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mdefun[0m[1m[37m [0m[1m[36mfysh/elfeed-pick-filter[0m[1m[37m [0m[1m[37m([0m[1m[36mfilter[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37minteractive[0m[1m[37m [0m[1m[37m([0m[37mlist[0m[1m[37m [0m[1m[37m([0m[1m[36malist-get[0m[1m[37m [0m[1m[37m([0m[37mcompleting-read[0m[1m[37m [0m[33m"Filter: "[0m[1m[37m [0m[1m[37m([0m[37mmapcar[0m[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m([0m[1m[36ms[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[37mcar[0m[1m[37m [0m[1m[36ms[0m[1m[37m))[0m[1m[37m [0m[1m[36mfysh/elfeed-filters[0m[1m[37m))[0m[1m[37m [0m[1m[36mfysh/elfeed-filters[0m[1m[37m [0m[1m[37mnil[0m[1m[37m [0m[1m[37mnil[0m[1m[37m [0m[33m'equal[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37msetq[0m[1m[37m [0m[1m[36melfeed-search-filter[0m[1m[37m [0m[1m[36mfilter[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36melfeed-search-update--force[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mgoto-char[0m[1m[37m [0m[1m[37m([0m[37mpoint-min[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[37m:general-config[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[33m'normal[0m[1m[37m [0m[33m'elfeed-search-mode-map[0m[1m[37m [0m[33m"S"[0m[1m[37m [0m[37m#'[0m[1m[36melfeed-search-live-filter[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[33m'normal[0m[1m[37m [0m[33m'elfeed-search-mode-map[0m[1m[37m [0m[33m"s"[0m[1m[37m [0m[37m#'[0m[1m[36mfysh/elfeed-pick-filter[0m[1m[37m)[0m[1m[37m[0m
I really like the interactive search feature, but having a few
defaults for my most common tags is quite nice.
[1m[37m [0m[37m:config[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mdefine-advice[0m[1m[37m [0m[1m[36melfeed-search-show-entry[0m[1m[37m [0m[1m[37m([0m[37m:override[0m[1m[37m [0m[1m[37m([0m[1m[36mentry[0m[1m[37m [0m[33m&optional[0m[1m[37m [0m[1m[36marg[0m[1m[37m)[0m[1m[37m [0m[1m[36mquick-browse[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37minteractive[0m[1m[37m [0m[1m[37m([0m[37mlist[0m[1m[37m [0m[1m[37m([0m[1m[36melfeed-search-selected[0m[1m[37m [0m[37m:ignore-region[0m[1m[37m)[0m[1m[37m [0m[1m[36mcurrent-prefix-arg[0m[1m[37m))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mrequire[0m[1m[37m [0m[33m'elfeed-show[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mwhen[0m[1m[37m [0m[1m[37m([0m[1m[36melfeed-entry-p[0m[1m[37m [0m[1m[36mentry[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36melfeed-untag[0m[1m[37m [0m[1m[36mentry[0m[1m[37m [0m[33m'unread[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36melfeed-search-update-entry[0m[1m[37m [0m[1m[36mentry[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37munless[0m[1m[37m [0m[1m[36melfeed-search-remain-on-entry[0m[1m[37m [0m[1m[37m([0m[37mforward-line[0m[1m[37m))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mpcase[0m[1m[37m [0m[1m[36marg[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36m'[0m[1m[37m([0m[1m[36m4[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[37mfuncall[0m[1m[37m [0m[1m[36mbrowse-url-browser-function[0m[1m[37m [0m[1m[37m([0m[1m[36melfeed-entry-link[0m[1m[37m [0m[1m[36mentry[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36m'[0m[1m[37m([0m[1m[36m16[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[37mfuncall[0m[1m[37m [0m[1m[36mbrowse-url-secondary-browser-function[0m[1m[37m [0m[1m[37m([0m[1m[36melfeed-entry-link[0m[1m[37m [0m[1m[36mentry[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36m_[0m[1m[37m [0m[1m[37m([0m[1m[36melfeed-show-entry[0m[1m[37m [0m[1m[36mentry[0m[1m[37m))))))[0m[1m[37m[0m
Although I usually prefer using shr to view entries, entries from
news aggregators just contain a link to the post and a link to the
comments section; for these I can [40m`C-u RET`[49m to open the
article directly in my browser (I'm using the surprisingly nice
[40m`xwidget-webkit`[49m).
Speaking of news aggregators, it's rather annoying to have the same
blog post appear multiple times from Hacker News and Lobsters; to
address that, I patched Elfeed to generate the unique id for an
entry based on its link, causing "hash collisions" between
identical articles.
I haven't upstreamed this because it's certainly an antipattern
(Elfeed fetches feeds asynchronously, so there's a race condition
between which feed you fetch a link from first), but I tend to
filter posts using tags, not feeds, so I like it better this way:
if you're interested, you can get the patch here [2].
Finally, there's an annoying issue where cached images are
displayed at their original resolution, usually exceeding the
dimensions of the window I'm viewing the post in.
I originally thought this was a bug in shr, then realized it was a
bug in Elfeed [3], but when fixing Elfeed I realized there
[3mwas[23m a related SVG bug in Emacs [4].
If you're impatient you can [40m[35m`(setq shr-ignore-cache
t)`[39m[49m as a band-aid fix.
== [1m[4mBonus Round: Browser Config[22m[24m
[1m[37m([0m[37muse-package[0m[1m[37m [0m[1m[36memacs[0m[1m[37m[0m
[1m[37m [0m[37m:requires[0m[1m[37m [0m[1m[36mxwidget-internal[0m[1m[37m[0m
[1m[37m [0m[37m:init[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mdefun[0m[1m[37m [0m[1m[36mfysh/search[0m[1m[37m [0m[1m[37m([0m[1m[36mstart[0m[1m[37m [0m[1m[36mend[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[33m"Search selected string using a search engine."[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37minteractive[0m[1m[37m [0m[33m"r"[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mlet*[0m[1m[37m [0m[1m[37m(([0m[1m[36mq[0m[1m[37m [0m[1m[37m([0m[37mbuffer-substring-no-properties[0m[1m[37m [0m[1m[36mstart[0m[1m[37m [0m[1m[36mend[0m[1m[37m))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mquery[0m[1m[37m [0m[1m[37m([0m[37mif[0m[1m[37m [0m[1m[37m([0m[1m[36muse-region-p[0m[1m[37m)[0m[1m[37m [0m[1m[36mq[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mread-string[0m[1m[37m [0m[1m[37m([0m[1m[36mformat-prompt[0m[1m[37m [0m[33m"Search"[0m[1m[37m [0m[1m[37m([0m[1m[36mcurrent-word[0m[1m[37m))[0m[1m[37m [0m[1m[37mnil[0m[1m[37m [0m[1m[37mnil[0m[1m[37m [0m[1m[37m([0m[1m[36mcurrent-word[0m[1m[37m)))))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mbrowse-url[0m[1m[37m [0m[1m[37m([0m[37mconcat[0m[1m[37m [0m[1m[36mfysh/search-prefix[0m[1m[37m [0m[1m[37m([0m[1m[36murl-hexify-string[0m[1m[37m [0m[1m[36mquery[0m[1m[37m)))))[0m[1m[37m[0m
[1m[37m [0m[37m:general[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mleader-def[0m[1m[37m [0m[33m"S"[0m[1m[37m [0m[37m#'[0m[1m[36mfysh/search[0m[1m[37m)[0m[1m[37m[0m
There's gotta be a builtin keybinding for this...
[1m[37m [0m[37m:hook[0m[1m[37m [0m[1m[37m([0m[1m[36mevil-collection-setup[0m[1m[37m [0m[1m[36m.[0m[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m([0m[33m&rest[0m[1m[37m [0m[1m[36m_[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[1m[36mgeneral-def[0m[1m[37m [0m[33m'normal[0m[1m[37m [0m[33m'xwidget-webkit-mode-map[0m[1m[37m [0m[33m"J"[0m[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m()[0m[1m[37m [0m[1m[37m([0m[37minteractive[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[1m[36mxwidget-webkit-scroll-up[0m[1m[37m [0m[1m[37m([0m[37m/[0m[1m[37m [0m[1m[37m([0m[37mframe-pixel-height[0m[1m[37m)[0m[1m[37m [0m[1m[36m2[0m[1m[37m))))))[0m[1m[37m[0m
I rarely need to scroll horizontally, and apparently no one else
does either, because it was broken [5] for quite a while ( ̄ω ̄;)
[1m[37m [0m[37m:config[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mdefun[0m[1m[37m [0m[1m[36mfysh/eww-browse-url-ephemeral[0m[1m[37m [0m[1m[37m([0m[1m[36murl[0m[1m[37m [0m[33m&rest[0m[1m[37m [0m[1m[36margs[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36meww-browse-url[0m[1m[37m [0m[1m[36murl[0m[1m[37m [0m[1m[36margs[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[36mrun-with-timer[0m[1m[37m [0m[1m[36m5[0m[1m[37m [0m[1m[37mnil[0m[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m()[0m[1m[37m [0m[1m[37m([0m[1m[36mkill-matching-buffers[0m[1m[37m [0m[33m"eww-"[0m[1m[37m [0m[1m[37mnil[0m[1m[37m [0m[1m[37mt[0m[1m[37m))))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37msetq[0m[1m[37m [0m[1m[36mbrowse-url-handlers[0m[1m[37m [0m[1m[36m'[0m[1m[37m([0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[33m"journals\\.aps\\.org"[0m[1m[37m [0m[1m[36m.[0m[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m([0m[1m[36murl[0m[1m[37m [0m[33m&rest[0m[1m[37m [0m[1m[36margs[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mlet*[0m[1m[37m [0m[1m[37m(([0m[1m[36mpdf[0m[1m[37m [0m[1m[37m([0m[1m[36mstring-replace[0m[1m[37m [0m[33m"/abstract/"[0m[1m[37m [0m[33m"/pdf/"[0m[1m[37m [0m[1m[36murl[0m[1m[37m)))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mif[0m[1m[37m [0m[1m[37m([0m[37mstring-equal[0m[1m[37m [0m[1m[36murl[0m[1m[37m [0m[1m[36mpdf[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[1m[36mbrowse-url-default-browser[0m[1m[37m [0m[1m[36murl[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[1m[36meww-browse-url[0m[1m[37m [0m[1m[37m([0m[37mconcat[0m[1m[37m [0m[1m[36mfysh/browse-url-proxy-prefix[0m[1m[37m [0m[1m[36mpdf[0m[1m[37m))))))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[33m"arxiv\\.org"[0m[1m[37m [0m[1m[36m.[0m[1m[37m [0m[1m[37m([0m[37mlambda[0m[1m[37m [0m[1m[37m([0m[1m[36murl[0m[1m[37m [0m[33m&rest[0m[1m[37m [0m[1m[36margs[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[37mcond[0m[1m[37m [0m[1m[37m(([0m[1m[36mstring-match-p[0m[1m[37m [0m[33m"/pdf/"[0m[1m[37m [0m[1m[36murl[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[1m[36mfysh/eww-browse-url-ephemeral[0m[1m[37m [0m[1m[36murl[0m[1m[37m [0m[1m[36margs[0m[1m[37m))[0m[1m[37m[0m
[1m[37m [0m[1m[37m(([0m[1m[36mstring-match-p[0m[1m[37m [0m[33m"/abs/"[0m[1m[37m [0m[1m[36murl[0m[1m[37m)[0m[1m[37m [0m[1m[37m([0m[1m[36mfysh/eww-browse-url-ephemeral[0m[1m[37m [0m[1m[37m([0m[1m[36mstring-replace[0m[1m[37m [0m[33m"/abs/"[0m[1m[37m [0m[33m"/pdf/"[0m[1m[37m [0m[1m[36murl[0m[1m[37m)[0m[1m[37m [0m[1m[36margs[0m[1m[37m))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[1m[37mt[0m[1m[37m [0m[1m[37m([0m[1m[36mbrowse-url-default-browser[0m[1m[37m [0m[1m[36murl[0m[1m[37m [0m[1m[36margs[0m[1m[37m)))))[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[33m"pdf$"[0m[1m[37m [0m[1m[36m.[0m[1m[37m [0m[1m[36mfysh/eww-browse-url-ephemeral[0m[1m[37m)[0m[1m[37m[0m
[1m[37m [0m[1m[37m([0m[33m"."[0m[1m[37m [0m[1m[36m.[0m[1m[37m [0m[1m[36mxwidget-webkit-browse-url[0m[1m[37m)))[0m[1m[37m[0m
One really nice feature of eww is that PDFs and other non-text
files will automatically open in their respective Emacs major mode;
to further facilitate that, I mimicked Zotero's connectors by
redirecting arXiv abstracts to their PDFs.
[40m[35m`fysh/browse-url-proxy-prefix`[39m[49m was meant to
view PDFs from paywalled journals through my university's proxy;
eww can't auth because there's no JS engine, so I considered
reusing cookies from my primary browser, but that sounds like a lot
more work than just Gib Lenning the papers instead 😉
[1m[37m [0m[1m[37m([0m[37mdefine-advice[0m[1m[37m [0m[1m[36mxwidget-kill-buffer-query-function[0m[1m[37m [0m[1m[37m([0m[37m:override[0m[1m[37m [0m[1m[37m()[0m[1m[37m [0m[1m[36malways-kill[0m[1m[37m)[0m[1m[37m [0m[1m[37mt[0m[1m[37m))[0m[1m[37m[0m
I don't like being asked to kill buffers; [40m[35m`(defalias
'yes-or-no-p 'y-or-n-p)`[39m[49m usually does the trick, but take
this as my parting advice for the reader.
== [1m[4mFeeds I Read[22m[24m
=== [1m[4mScott Aaronson [6] cs, quantum[22m[24m
=== [1m[4mAndrew Kelley [7] zig, systems[22m[24m
=== [1m[4mXe Iaso [8] nix[22m[24m
=== [1m[4mArtemis [9] nix[22m[24m
=== [1m[4mJade Lovelace [10] nix[22m[24m
=== [1m[4mIan Henry [11] nix[22m[24m
=== [1m[4mJakob Kreuze [12] emacs, security[22m[24m
=== [1m[4mJon Sangster [13] nix, emacs[22m[24m
=== [1m[4mChris Wellons [14] emacs, systems[22m[24m
=== [1m[4mEvan Ovadia [15] vale, systems[22m[24m
=== [1m[4mKarthinks [16] emacs[22m[24m
=== [1m[4mXKCD [17] webcomic[22m[24m
=== [1m[4mLeftover Salad [18] webcomic[22m[24m
References:
(HTM) [1] turned to the dark side
(TXT) [2] here
(HTM) [3] bug in Elfeed
(HTM) [4] bug in Emacs
(HTM) [5] broken
(HTM) [6] Scott Aaronson
(HTM) [7] Andrew Kelley
(HTM) [8] Xe Iaso
(HTM) [9] Artemis
(HTM) [10] Jade Lovelace
(HTM) [11] Ian Henry
(HTM) [12] Jakob Kreuze
(HTM) [13] Jon Sangster
(HTM) [14] Chris Wellons
(HTM) [15] Evan Ovadia
(HTM) [16] Karthinks
(HTM) [17] XKCD
(HTM) [18] Leftover Salad
>=================================================================<
(DIR) Blog
(DIR) Writeups
(DIR) jp
copyright 2026 George Huebner
(HTM) email