welcome todo my nightmare

I have always been one to make todo lists. My process has changed a lot over the years, from when I used actual paper to using only digital lists. My current setup is a bit hacky but works for me, so I thought I would share it.

On my PC I started using a simple script (thanks to z3bra) to add tasks to a file, by default ~/.todo. There is nothing special about this todo file, it is simple plain text. I am aware of the todo.txt method but after trying it for a while I felt like it was not working for me. I have tried kanban boards after using similar methods in work, but I didn’t find it worked very well for my own personal tasks.

My personal note and wiki tool of choice is vimwiki so I tried keeping my todo list in that. This worked well for a while but I couldn’t find a quick and comfortable workflow with it.

Other notable attempts include Taskwarrior and calcurse.

At the moment I am happy with the simple todo script. I spend a lot of time in tmux, so I have added a couple of keybindings to my config. I have one keybind to display the todo list in a tmux popup and another to start a command prompt with a prefix so I can write out a task, close the single quotes, and hit enter. If you’re interested the relevant config is

# toggle todo list popup
unbind t
bind t display-popup -w 75 -h 13 ~/bin/todo

# add task to todo list
unbind T
bind T command-prompt -I "run-shell '~/bin/todo "

Adjust the width and height of the popup to your preference, and change the path to your script if required.

This workflow was working quite well when I was at my PC, but what happens when I am not? On my phone I run termux so I can easily log in to my PC from anywhere (with a VPN) and add an entry to my todo list, but sometimes this process is a bit slow, or I may not have a network connection at all.

I have Markor installed for taking notes so I thought I could sync my todo file to my phone to modify in Markor. I didn’t want to install a tool such as syncthing on both my PC and phone for a single file so I started using rsync in a script periodically run by cron. I set my crontab to push the todo file from my PC to my phone at the end of the workday, then pull from my phone before work each workday.

This worked well as long as I only edited the file on my PC during working hours and on my phone outside of working hours, which is usually the case. I knew it would bite me in the ass at some point though so I started looking at a way to sync them properly. I came across incron, which is like cron but is triggered by filesystem events instead of at specified times. This looked like a good start, so I installed it on my PC and configured incrontab to push the todo file to my phone whenever it is modified. I immediately hit a bug which caused incron to run once and then not run again.

Disappointed by this I decided to hack together something similar myself using inotifywait from the inotify-tools package. This tool is really easy to use, and is available in termux. I set a script on both my PC and my phone to watch the todo file and rsync it to the opposite device if it changes.

#!/bin/sh

LOCAL_TODO="~/.todo"
REMOTE_TODO="/path/to/markor/todo.txt"
REMOTE_HOST="pyratephone"

exec inotifywait -e close_wait -m $LOCAL_TODO | while read TODOFILE ; do
    rsync $LOCAL_TODO $REMOTE_HOST:$REMOTE_TODO
done

I daemonised this on my PC and created a service on termux using termux-services. With the package installed creating a service is straight forward; create a service directory and run script

mkdir -p $PREFIX/var/service/todod/log
ln -sf $PREFIX/share/termux-services/svlogger $PREFIX/var/service/todod/log/run
cat >> $PREFIX/var/service/todod/run << EOF
#!/data/data/com.termux/files/usr/bin/sh

LOCAL_TODO="/path/to/markor/todo.txt"
REMOTE_TODO=".todo"
REMOTE_HOST="pyratepc"

exec 2>&1
exec inotifywait -e close_write -m $LOCAL_TODO | while read TODOFILE
do
    rsync -e "ssh -i /path/to/sshkey" $LOCAL_TODO $REMOTE_HOST:$REMOTE_TODO
done
EOF
chmod +x $PREFIX/var/service/todod/run
sv start todod

I immediately hit another issue, the DELETE_SELF file event. When you pass the -d flag to the todo script to delete a line it uses the command

sed -i "${1}d" $TODOFILE

Unfortunately this command causes the file to be replaced with a new file, which generates the DELETE_SELF event. This means inotifywait sees the original file it was monitoring as deleted and can’t monitor the file anymore. It doesn’t look at the filename therefore does not recognise that the new todo file is “the same”. To overcome this I switched the use of sed with ed. The delete function in the todo script now looks like this

delete() {
    test -n "$1" || exit 1
    ed $TODO << EOF >/dev/null
${1}d
w
q
EOF
}

Using ed means the file is opened, the line deleted, and the file closed causing a CLOSE_WAIT event. You can find my version of the todo script on my git server.

The same issue occurs with rsync, the file is replaced with a new file causing a DELETE_SELF event. The quickest way I thought to fix this was to restart the daemon on the opposite device after the rsync. My script now looks like this

#!/bin/sh

LOCAL_TODO="~/.todo"
REMOTE_TODO="/path/to/markor/todo.txt"
REMOTE_HOST="pyratephone"
DAEMON_RESTART="SVDIR=/data/data/com.termux/files/usr/var/service sv restart todod"

exec inotifywait -e close_wait -m $LOCAL_TODO | while read TODOFILE ; do
    rsync $LOCAL_TODO $REMOTE_HOST:$REMOTE_TODO
    ssh $REMOTE_HOST "${DAEMON_RESTART}"
done

The run script on my phone has a different $DAEMON_RESTART variable to restart the script on my PC, and specifies the IdentityFile like I did with the rsync command.

So now I have a sync of sorts, and the workflow on my PC works well with the tmux keybindings. I expect at some point I will need to consider what happens when I make a change to the file and there is no network connection to the other device but that is a task for another day, it’s on the todo list.