My sfeed setup
Introduction §
Since I just finished putting the final touches to my feed reader setup, I thought I'd write about it because I think sfeed is an underrated software gem and because some of the fiddling I did may prove useful for others.
But first, a tangentially related logbook of my feed reader journey (from distant memory):
- newsbeuter: for a young me that was starting his UNIX journey, still easily impressed by good rice from desktop threads invariably full of TUI blinkenlights, this was the one to get. It worked fine but died.
- newsboat: dead newsbeuter's fork, I only remember that I was already using Gentoo and getting into the suckless philosophy at that point so when asciidoctor (thus Ruby) and Rust became required to build, I kinda started looking elsewhere.
- rawdog: think I used this for a short time, it did the job fine but was killed by Python 2's EOL and full aggregation was suboptimal for my growing number of feeds.
- sfeed: my last stop and the subject of this post. It did everything I wanted, had CLI, TUI and HTML outputs and was the usual suckless codebase: simple C with very few dependencies.
Sfeed praise §
I wasn't enough of a computing wizard at the time to realize why it felt so perfect but now I do. It's exactly the same deal as gron: instead of being a kitchen sink contraption without clean parser/UI separation or inventing its own crappy DSL, this is just an adapter between Web formats and the UNIX world.
So, gron transforms from JSON to a simple line-oriented format and back. Similarly, sfeed is a collection of tools to convert RSS/Atom to a TSV format then convert/visualize it; for UNIX newfriends, formats like TSV are what classic UNIX tools (such as awk, cut, sort, paste and join) eat, sleep and breathe.
In addition to that, sfeed had the brilliant idea of making its config file a sh script sourced for specific variables and functions. I have no doubt these days that declarative config formats are a strong liability when it comes to flexibility and power (in exchange for security and luser-friendliness), as demonstrated by any program with an "rc" file (startx, bash/zsh, emacs, bspwm).
Personal additions §
sfeed_curses
patch
As said before, a big advantage of suckless software is that it's very easy to read and modify,
which is why I have two little patches (stored as one here) in my /etc/portage/patches/
for the TUI visualizer:
- hunk 1-2: at least for Atom, the
<id>
element is the only one specified as unique, so use that instead of<link>
to mark an article as read (was causing issues with the Gentoo package feeds). - hunk 3-4: a simple convenience keybind to jump to the next (or previous) feed with unread
items and open it; easier than
t + ]/[
orJ/K + Return
.
sfeed_view
Prompted by me wanting sfeed_curses
to display its feed list using sfeedrc's
definition order. This is where having a sh config file is useful, as I simply have to source
said config and override the feed()
function to get what I want, no tortuous and
brittle parsing needed!
rc=$1 sfeedpath=$(sfeedpath=$HOME/.sfeed/feeds; . "$rc"; echo "${sfeedpath%/}") sfeeddir=${sfeedpath%/*} feed_names=$(. "$rc"; feed() { echo "$1";}; feeds)
sfeed_aggregate
This one is pretty recent and in fact the reason why I thought sharing my setup wouldn't be a bad idea.
The problem is simple: I wanted to aggregate specific feeds under a single name (e.g. "Various blogs") but to my dismay, this pretty basic use case isn't natively handled by sfeed =(. So I rolled up my sleeves and cobbled something in a jiffy; which is when I became conscious of how glad I was that sfeed is simply a flexible toolset around TSV.
In the end, I just had to reserve a special feed name syntax, write a tiny sfeedrc (sh) function:
# $1 aggregate feed name # $2..$# constituent feed URLs aggregate_feed() { local feed=$1 shift local url=; for url do feed "!$feed!$url" "$url" done }
and a script to merge those marked feeds into a single one for sfeed_curses
to consume:
#!/bin/sh set -eu # cf moreutils sponge() { cat >"$1".sponge mv -f -- "$1".sponge "$1" } rc=$1 sfeedpath=$(sfeedpath=$HOME/.sfeed/feeds; . "$rc"; echo "${sfeedpath%/}") find "$sfeedpath" -type f -name '!*' | sed -E 's#.*/!([^!]+)!.*#\1#' | sort -u | while IFS= read -r name do feed=$sfeedpath/$name ! [ -e "$feed" ] && touch "$feed" # cf merge() and order() from sfeed_update sort -t ' ' -u -k6,6 -k2,2 -k3,3 "$feed" "$sfeedpath/!$name!"* | sort -t ' ' -k1rn,1 | sponge "$feed" done