todo.txt and fzf
This post is about offering an interactive interface to todo.txt on the command line. Skip the first three paragraphs if you don’t give a crap about the history.
todo.txt is pretty close to perfect software. It’s plain text, meaning that hundreds of well-understood, fast, and venerable plain text manipulation tools work on it; it’s simple, requiring no more than a text editor to use; it’s well-documented; and the rules are easily remembered. It’s been around long enough that dozens of tools have been written to work with todo.txt files – GUIs for people who want check boxes and drop-down menus, command-line utilities, mobile apps, web apps. The core command-line client, todo.txt-cli, has its first git commit in 2009 by Gina Trapani who, I think(?) was the person who came up with the format. It’s simply a well-thought-out bit of kit, and I’m humbled whenever I think about the genius that created it.
Software being software, there have been a few ncurses implementations over the years, one of which I really liked but which started to suffer from bitrot and a couple of times corrupted my main todo.txt file. Consequently, I’d just been using the command line script for the past few months, but recently I started playing around with fzf as an action, and I’m really liking where it’s at.
The command-line client has a sort of plug-in system where it looks for a shell script in a directory if it’s passed any command it doesn’t recognize. The trivial plug-in is a two-line shell script:
#!/bin/bash
todo.sh ls | head -n -2 | fzf
All this does is pipe the output of todo.sh to fzf, stripping off the header for convenience. But fzf can do so much more! In particular, you can bind keys to actions in fzf, and while the resulting command is undeniably ugly and nearly impossible to read, the result is fantastic. Here’s the end result:
#!/bin/bash
case $1 in
"usage")
echo "todo.sh fzf, select and press ^x to make a task as complete"
;;
*)
todo.sh ls | head -n -2 | fzf --bind 'alt-x:execute(echo {} | cut -f1 -d" " | xargs todo.sh do)+reload(todo.sh ls | head -n -2),alt-r:reload(todo.sh ls | head -n -2),alt-s:execute(todo.sh sort)+reload(todo.sh ls | head -n -2),alt-n:execute(N=""; vared N; todo.sh add "$N")+reload(todo.sh ls | head -n -2),alt-e:execute(L=`echo {} | cut -d" " -f1`; E=`echo {} | cut -d" " -f2-`; vared E; sed -i "${TODO_FILE}" -e "${L}c\\${E}")+reload(todo.sh ls | head -n -2)'
;;
esac
As you can see, the fzf command got a little crazy. It binds four keystrokes (search for alt-\w
), and while I’m not going to explain everything that’s going on – the fzf manpage is fine for that – I will explain what the bindings do:
- alt-x: complete a task.
echo {} | cut -f1 -d" " | xargs todo.sh do
fzf binds {} to the selected line, so we’re echoing the line and grabbing the first word, which is the line number; we then pass that totodo.sh do
which completes that task. - alt-r: reload. This simply re-runs fzf:
todo.sh ls | head -n -2
- alt-s: sorts the list. Actually, it calls an action, but this is a common action for people to define. Mine is a script that merely does:
<$TODO_FILE sed -e "s/(\(\w\)) \([0-9:]\+\).* due:\([0-9-]\+\).*/\3 \2 \1 &/" -e t -e "s/^/9999-00-00 00:00 Z /" | LC_ALL=C sort -k1 | sed -e "s/^[0-9-]\+ [0-9:]\+ [A-Z]\+ //" | sponge $TODO_FILE
Yeah, that’s pretty ugly, too, but you can find other examples online. Mine sorts by due date first, but priority second, and then writes the output back to the todo.txt file using themoreutils
package’ssponge
command. - alt-n: add a new task. Here’s where it starts getting interesting, I think; this depends on zsh, so if zsh isn’t your shell, this won’t work for you.
N=""; vared N; todo.sh add "$N"
vared
is zsh’s version of bash’sread
, only it is a full readline editor (unlikeread
). We initialize an empty variable, usevared
to edit it, and then call todo.sh to add it. - alt-e: edit the selected item. This is gnarly mostly because of the escaping. It also depends on your shell being zsh. For the sake of readability, I’m going to separate the lines.
# Get the line number from the input L=`echo {} | cut -d" " -f1`; # Get everything but the line number E=`echo {} | cut -d" " -f2-`; # Edit the line vared E; # Replace the line, by line number, with the edit sed -i "${TODO_FILE}" -e "${L}c\\${E}"
For me, this is 99% of what I do with todo.txt: add, view, and complete, with a couple of utility functions. fzf makes a nice interface to viewing and filtering the list, and the bindings provide a fast and simple way to make minor changes. There are some failure cases – not everything is quoted out, and I’m sure I could create a task that breaks the edit command – but it’s highly unlikely I’m going to run into those. YMMV.
It demonstrates the power of uncomplex designs and simple formats, and the capabilities you gain by having an entire generation of text processing tools at the ready.
Update 2022-10-03 #
My current config adds alt-a
to run archive
, because why not?
...,alt-a:execute(todo.sh archive)+reload(todo.sh ls | head -n -2)'
Also, I actually added another action, noh
, that just does todo.sh ls | head -n -2
and allows me to reduce all of the reloads to todo.sh noh
; it makes things a little cleaner, but isn’t necessary.