Time Tracking

Originally I started time tracking with Clockify as part of learning Japanese to ensure immersion related goals were being met/exceeded. I migrated my data to Toggl, which I later abandoned due to time tracking beginning to feel like a chore.

I looked into some alternatives and started tracking time again. Since January I’ve used timewarrior to track the time I allocate to productive activity. This includes most everything excluding necessities (food, hygeine, sleep, etc.) and leisure activity (english media consumption and gaming).

Scripting

Being that timewarrior is a command line program, the flexibility provided via Bash or Python scripting alone makes Timewarrior worth it over alternatives.

I’ve written several scripts to streamline automatic tagging, starting, and stopping of new intervals. These scripts have resolved my complaints as they make interval management less of a chore by putting the controls just a key away.

Blockscript

The first script I wrote a few months ago is a simple status bar block. It just reformats an open interval’s tags and elapsed time to a single line:

TAGS=$(timew | head -n 1)
TIME=$(timew | tail -n 1)
if [ ! "$TAGS" = "There is no active time tracking." ]; then
    echo "[$(echo $TAGS | cut --complement -c -9) \
    $(echo $TIME | awk '{ print $2} ' | cut --complement -c 5-)]"
fi

I use this script in tandem with dwmblocks where it checks automatically every 60 seconds if an interval is running. Alternatively the block can be forcefully refreshed when dwmblocks receives an RTMIN+4 signal (this signal value is configured within dwmblocks config.h file). This signalling is incredibly useful for the following scripts.

Generic Keybind

I originally wrote this one in bash, but rewrote it in Python. This script has some non-python dependencies: dmenu and timewarrior. The entry object might be a little overkill for this usecase but for future timewarrior related projects/extensions, it should prove very useful.

I don’t consider this one done, I intend on revisiting it in the future to make adding new template intervals not require messing with the source file.

#!/bin/python

import datetime
import os
import subprocess
import json

class entry():
    annotation: str
    taglist: list[str]
    start: datetime.datetime
    end: datetime.datetime

    def __init__(self, line: str):
        line = line.replace("\n", "")
        delimited = line.split(' # ')
        try:
            self.taglist = delimited[1].split(' ')
        except Exception:
            self.taglist = []
        try:
            self.annotation = delimited[2]
        except Exception:
            self.annotation = ""
        date_interval = delimited[0].split(' ')
        try:
            self.start = datetime.datetime.fromisoformat(date_interval[1])
            self.end = datetime.datetime.fromisoformat(date_interval[3])
        except IndexError:
            return

    def __str__(self):
        return "{0} -- {1}".format(self.annotation, " ".join(self.taglist))

    def duration(self):
        delta: datetime.timedelta
        try:
            delta = self.end - self.start
        except Exception:
            now = datetime.datetime.now(datetime.timezone.utc)
            delta = now - self.start
        return delta

def dmenu(options=[''], prompt="", vertical=1):
    options = "\n".join(options)
    dmenu_string = "dmenu "
    if len(prompt) != 0:
        dmenu_string += "-p '" + prompt + "' "
    if vertical != 1:
        dmenu_string += "-l " + str(vertical) + " "
    return subprocess.getoutput("printf '{0}' | {1}".format(options, dmenu_string))

def start_timew(annotation: str, taglist: list[str]):
    os.system("timew start " + " ".join(taglist))
    os.system("timew annotate '" + annotation + "'")

def main():
    config = json.loads(open("./config.json").read())
    now = datetime.datetime.now(datetime.timezone.utc)
    path = config["data_dir"] + str(now.year) + "-" + "{:02d}".format(now.month)+ ".data"

    entries = open(path, "r").readlines()
    entries.reverse()
    last = entry(entries[0])

    # exit code 0 means open interval
    # if interval started more than 3 minutes ago save; otherwise cancel
    if os.system("timew") == 0:
        if last.duration().seconds/60 > config["threshold"]:
            os.system("timew stop")
        else:
            os.system("timew cancel")
        os.system("pkill -RTMIN+" + str(config["signal"]) + " dwmblocks")
        return
    
    options = [str(last), "continue", "leetcode", "htb", "anime", "anki"]
    sel = dmenu(options=options, prompt="What doing?")
    if sel == "":
        return
    elif sel == str(last):
        os.system("timew continue @1")
    elif sel == "continue":
        entry_list = []
        added_hashes = []
        options_list = []
        for index in range(len(entries)):
            temp = entry(entries[index])
            if hash(str(temp)) not in added_hashes and len(added_hashes) < config["continue_limit"]:
                entry_list.append(temp)
                options_list.append(str(temp))
                added_hashes.append(hash(str(temp)))
        e = entry_list[options_list.index(dmenu(options_list,vertical=20))]
        start_timew(e.annotation, e.taglist)
    else:
        taglist = [sel]
        if sel == "anime":
            taglist.append("jp")
            taglist.append("immersion")
        elif sel == "anki":
            taglist.append("jp")
        elif sel == "leetcode":
            taglist.append("dev")
        annotation = dmenu(prompt="Annotation")
        start_timew(annotation, taglist)
    os.system("pkill -RTMIN+" + str(config["signal"]) + " dwmblocks")
    return 0

if __name__ == '__main__':
    main()

Immersion Keybind

For Japanese immersion I have a directory of symlinks to the content I’m currently immersing in. This one simply prompts a directory with dmenu, begins playback with mpv, tags the interval, and annotates it with the directory’s name. The script remains alive until the mpv session is closed by the user, where the interval is closed and the associated status bar block is refreshed to reflect this change.

#!/bin/sh
DIR="/home/jstn/anime/"
SELECTION=$(ls -1 $DIR | dmenu -l 20)
if [ -z "$SELECTION" ]; then
    exit
else
    mpv --save-position-on-quit --ignore-path-in-watch-later-config --playlist="$DIR$SELECTION" & 
    timew start anime immersion jp
    timew annotate "$SELECTION"
    pkill -RTMIN+4 dwmblocks
    wait $(pidof mpv)
    timew stop
    pkill -RTMIN+4 dwmblocks
fi