[HOME] | [ABOUT ME] | [BLOG] | [CONTACT] |
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).
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.
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.
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()
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