Automatic context detection for Taskwarrior

Translations: br
Publication date: Aug 24, 2020
Tags: taskwarrior, gtd, python

One of the main ideas of GTD is to have a context associated with each task, so that it is very easy to see which tasks can be done in your current context. I organize my tasks with Taskwarrior, so to make it work with contexts, when adding a new task I need to assign a context to it and also update the current context whenever it changes. There are different types of context, but the easiest to automate are the spatial ones, that is, which tasks I can do where I'm at right now.

Assigning a task to a context is as simple as adding a tag to it. I have three different places I'm normally at, so I either add a @rep, @sp or @uni tag depending on where the task can be done.

To update the current context though I'd need to manually tell Taskwarrior where I'm at right now every time I go to any other place. For example, I'd need to type task context uni every time I went to the university. This is not only very boring but also error-prone: more than once it took me a couple seconds to understand why some tasks were missing.

As any other little problem in life, this can be solved with a little scripting. So that's why I wrote a python script to automatically detect and set the current context for Taskwarrior.

Python script

The idea is very simple: I'm almost always connected to the WiFi, since it connects automatically, and each location has specific WiFi network names, so I just need to get the current WiFi SSID and set the corresponding context.

That is what the following python script does (aside from sending a notification with notify-send):

import subprocess


contexts = {
    'rep': ["rep wifi 1", "rep wifi 2", "rep wifi 3"],
    'sp':  ["sp wifi 1", "sp wifi 2"],
    'uni': ["eduroam"]
}


def get_context(wifi):
    for context in contexts:
        if wifi in contexts[context]:
            return context
    return None


def get_current_context():
    wifi_cmd = subprocess.run(["iwgetid", "-r"], text=True, capture_output=True)
    wifi = wifi_cmd.stdout.split('\n')[0]
    return get_context(wifi)


def set_current_context():
    context = get_current_context()
    if context:
        subprocess.run(["notify-send", "Taskwarrior context",
                       f"Setting context to <b>{context}</b>"])
        subprocess.run(["task", "context", context])
    else:
        subprocess.run(["notify-send", "-u", "critical", "Taskwarrior context",
                       "Failure to detect context"])
        subprocess.run(["task", "context", "none"])

Note: The WiFi names for the rep and sp contexts were redacted to avoid exposure.

Systemd service

Since when I go from one place to the other I always suspend, hibernate or shutdown my notebook, that script only needs to be run after resuming or turning the computer on, which is exactly what the following systemd service is for:

[Unit]
Description=Set taskwarrior context
Wants=network-online.target NetworkManager-wait-online.service
After=network-online.target NetworkManager-wait-online.service hibernate.target suspend.target

[Service]
User=%I
Type=oneshot
Environment=PATH=/usr/bin:/home/nfraprado/ark/code/.path/
Environment=DISPLAY=:0
Environment=XAUTHORITY=%h/.Xauthority
Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
ExecStartPre=/usr/bin/sleep 30
ExecStart=/home/nfraprado/ark/code/.path/taskwarrior-update_context
ExecStartPost=

[Install]
WantedBy=multi-user.target
WantedBy=hibernate.target
WantedBy=suspend.target

Details about this service file:

  • The network-online and wait-online services supposedly make it wait for NetworkManager to connect to a network before executing, but from my tests that wasn't enough, so I ended up adding a 30 second delay as can be seen in ExecStartPre.
  • The hibernate and suspend targets make it run after the computer resumes or turns on.
  • The DISPLAY, XAUTHORITY and DBUS_SESSION_BUS_ADDRESS environment variables allow the notification to appear.
  • taskwarrior-update_context basically calls set_current_context() from the python script.

And that's it! With those two pieces, after I move between two locations and open my notebook the Taskwarrior context is automatically updated, showing me only the tasks relevant to the place I'm at.

Extra: Previous script

As a side note, before that python script, I made a bash script. Even though running a command in bash is way cleaner than python's subprocess.run(), I really despise bash's syntax. I also find it awful to need to define a array_contains function (which I copied from some StackOverflow answer). Anyway, here's the bash script if you're curious:

#!/bin/bash
wifi="$(iwgetid -r)"

declare -a rep=("rep wifi 1" "rep wifi 2" "rep wifi 3")
declare -a sp=("sp wifi 1" "sp wifi 2")
declare -a uni=("eduroam")

array_contains () {
    local array="$1[@]"
    local seeking=$2
    local in=1
    for element in "${!array}"; do
        if [[ $element == $seeking ]]; then
            in=0
            break
        fi
    done
    return $in
}

array_contains rep "$wifi" && context=rep
array_contains sp "$wifi" && context=sp
array_contains uni "$wifi" && context=uni

if [ -z "$context" ];then
    notify-send -u critical "Taskwarrior context" "Failure to detect context"
    task context none >/dev/null 2>&1
else
    notify-send "Taskwarrior context" "Setting context to <b>$context</b>"
    task context $context >/dev/null 2>&1
fi