Detecção de contexto automática para o Taskwarrior

Traduções: en
24/08/2020

Uma das principais ideias do GTD é a associação de contexto à cada tarefa para que seja muito fácil ver quais tarefas podem ser feitas no contexto atual. Eu organizo minhas tarefas com o Taskwarrior, então para utilizar contextos com ele, quando eu adiciono uma nova tarefa eu preciso vinculá-la a um contexto e também atualizar o contexto sempre que ele mudar. Há diferentes tipos de contexto, mas os mais fáceis de automatizar são os espaciais, ou seja, quais tarefas eu posso fazer onde eu estou nesse momento.

Para vincular uma tarefa a um contexto basta adicionar um rótulo a ela. Como eu normalmente estou em um de três lugares, eu adiciono o rótulo @rep, @sp ou @uni dependendo de onde a tarefa pode ser feita.

Já para atualizar o contexto atual eu precisaria indicar manualmente para o Taskwarrior onde eu estou no momento toda vez que eu for para um outro lugar. Por exemplo, eu precisaria digitar task context uni toda vez que eu fosse para a universidade. Isso além de ser bem chato é uma fonte de erros: já aconteceu mais de uma vez de eu levar alguns segundos até entender porque algumas tarefas estavam faltando.

Mas como todo pequeno problema na vida, isso pode ser solucionado com um pequeno script. E é por isso que eu escrevi um script em python para detectar e configurar automaticamente o contexto atual do Taskwarrior.

Script em python

A ideia é bem simples: eu quase sempre estou conectado a uma rede sem fio, já que essa conexão é automática, e cada local tem um nome específico para a rede, então eu só preciso obter o SSID da rede atual e configurar o contexto correspondente.

É isso que o seguinte programa em python faz (além de mandar uma notificação usando 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"])

Obs: Os nomes das redes dos contextos rep e sp foram censurados para evitar exposição.

Serviço do systemd

Já que quando eu vou de um lugar para o outro eu sempre suspendo, hiberno ou desligo meu computador, esse programa só precisa executar depois de resumir ou ligar o computador, e é exatamente para isso que serve o seguinte serviço do systemd:

[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

Detalhes sobre esse arquivo de serviço:

  • Os serviços network-online e wait-online supostamente fazem com que ele espere o NetworkManager conectar a uma rede antes de executar, mas pelos meus testes isso não foi o suficiente e portanto eu acabei adicionando 30 segundos de atraso como pode ser visto em ExecStartPre.
  • Os alvos hibernate e suspend fazem com que ele rode depois do computador resumir ou ligar.
  • As variáveis de ambiente DISPLAY, XAUTHORITY e DBUS_SESSION_BUS_ADDRESS permitem que a notificação apareça.
  • taskwarrior-update_context basicamente chama set_current_context() do script em python.

E é isso! Com esses dois componentes, depois que eu vou de um lugar para o outro e abro meu computador, o contexto do Taskwarrior é automaticamente atualizado, me mostrando só as tarefas relevantes para o lugar que estou atualmente.

Extra: Script anterior

Uma pequena tangente: antes daquele script em python, eu tinha feito um em bash. Apesar de executar comandos no bash ser mais limpo do que o subprocess.run() do python, eu acho a sintaxe do bash horrível. Eu também acho ridículo precisar definir uma função array_contains (que eu copiei de alguma resposta do StackOverflow). Enfim, esse era o script em bash caso esteja com curiosidade:

#!/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