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 ls | head -n -2 | fzf

All this does is pipe the output of 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:


case $1 in
            echo " fzf, select and press ^x to make a task as complete"
   ls | head -n -2 | fzf --bind 'alt-x:execute(echo {} | cut -f1 -d" " | xargs do)+reload( ls | head -n -2),alt-r:reload( ls | head -n -2),alt-s:execute( sort)+reload( ls | head -n -2),alt-n:execute(N=""; vared N; add "$N")+reload( 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( ls | head -n -2)'

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 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 to do which completes that task.
  • alt-r: reload. This simply re-runs fzf: 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 the moreutils package’s sponge 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; add "$N" vared is zsh’s version of bash’s read, only it is a full readline editor (unlike read). We initialize an empty variable, use vared to edit it, and then call 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( archive)+reload( ls | head -n -2)'

Also, I actually added another action, noh, that just does ls | head -n -2 and allows me to reduce all of the reloads to noh; it makes things a little cleaner, but isn’t necessary.