Last active
November 10, 2021 21:17
-
-
Save wbsch/d977b0ac29aa1dfa4437 to your computer and use it in GitHub Desktop.
Time tracking hook script for Taskwarrior that outputs ledger timelog formatted data.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# | |
# Writes task start/stop times to a timelog formatted file. | |
# You might need to adjust LEDGERFILE, or set the TIMELOG environment variable. | |
# | |
# Example reports, after using start/stop on a task: | |
# ledger -f /path/to/timelog.ledger print | |
# ledger -f /path/to/timelog.ledger register | |
# | |
# Projects, tags, and UUIDs are fully supported and queryable from ledger. | |
# | |
# | |
# 2015-03-05 wbsch | |
# - Now with "I forgot to start/stop this task!" convenience features: | |
# "task $id start $x" | |
# "task $id stop $x" | |
# "task $id done $x" | |
# Where $x is the time in minutes you want the entry in your timelog | |
# file to be backdated. Note that this is not properly displayed in | |
# Taskwarrior itself, but only in your timelog file. | |
# | |
# Note: This will only work on Taskwarrior 2.4.2+ due to a bug in | |
# earlier versions. The basic time tracking functionality will | |
# work on 2.4.1+. | |
# | |
# | |
# May the Holy Python forgive me for this mess. | |
# | |
import calendar | |
import json | |
import os | |
import re | |
import sys | |
from datetime import datetime | |
from datetime import timedelta | |
LEDGERFILE = "%s/.task/hooks/timetrack.ledger" % os.getenv('HOME') | |
if 'TIMELOG' in os.environ: | |
LEDGERFILE = os.environ['TIMELOG'] | |
def adjust_date(d, adjust_by): | |
if not isinstance(d, datetime): | |
d = tw_to_dt(d) | |
d -= timedelta(minutes=int(adjust_by)) | |
return d | |
def tw_to_dt(s): | |
""" Taskwarrior JSON date ---> datetime object. """ | |
return datetime.strptime(s, "%Y%m%dT%H%M%SZ") | |
def dt_to_tw(d): | |
""" datetime object ---> Taskwarrior JSON date. """ | |
return d.strftime("%Y%m%dT%H%M%SZ") | |
old = json.loads(sys.stdin.readline()) | |
new = json.loads(sys.stdin.readline()) | |
annotation_added = ('annotations' in new and not 'annotations' in old) \ | |
or \ | |
('annotations' in new and 'annotations' in old and \ | |
len(new['annotations']) > len(old['annotations'])) | |
# task started | |
if ('start' in new and not 'start' in old) and annotation_added: | |
new['annotations'].sort(key=lambda anno: anno['entry']) | |
m = re.match('^[0-9]+$', new['annotations'][-1]['description']) | |
if m: | |
new['start'] = dt_to_tw(adjust_date(new['start'], int(m.group(0)))) | |
new['annotations'] = new['annotations'][:-1] | |
if not new['annotations']: | |
del new['annotations'] | |
print("Timelog: Started task %s minutes ago." % m.group(0)) | |
if tw_to_dt(new['start']) < tw_to_dt(new['entry']): | |
new['entry'] = new['start'] | |
# task stopped | |
if 'start' in old and not 'start' in new: | |
started_utc = tw_to_dt(old['start']) | |
started_ts = calendar.timegm(started_utc.timetuple()) | |
started = datetime.fromtimestamp(started_ts) | |
stopped = datetime.now() | |
if annotation_added: | |
new['annotations'].sort(key=lambda anno: anno['entry']) | |
m = re.match('^[0-9]+$', new['annotations'][-1]['description']) | |
if m: | |
new['annotations'] = new['annotations'][:-1] | |
if not new['annotations']: | |
del new['annotations'] | |
stopped = adjust_date(stopped, m.group(0)) | |
if stopped < started: | |
print("ERROR: Stop date -%s minutes would be before the start date!" % m.group(0)) | |
sys.exit(1) | |
print("Timelog: Stopped task %s minutes ago." % m.group(0)) | |
entry = "i " + started.strftime("%Y/%m/%d %H:%M:%S") | |
entry += " " | |
entry += new['project'].replace('.', ':') if 'project' in new else "no project" | |
entry += " " + new['description'] + "\n" | |
entry += "o " + stopped.strftime("%Y/%m/%d %H:%M:%S") | |
entry += " ;" | |
entry += " :" + ":".join(new['tags']) + ":" if 'tags' in new else "" | |
entry += " uuid: " + new['uuid'] | |
entry += "\n\n" | |
with open(LEDGERFILE, "a") as ledger: | |
ledger.write(entry.encode("utf-8")) | |
print(json.dumps(new)) |
I'm getting this error:
Traceback (most recent call last):
File "/home/daraul/.task/hooks/on-modify.timetrack.py", line 112, in <module>
ledger.write(entry.encode("utf-8"))
TypeError: write() argument must be str, not bytes
I'm on python3:
daraul/.task: python --version [INSERT]
Python 3.7.4
Not sure if that matters.
I'm getting this error:
Traceback (most recent call last): File "/home/daraul/.task/hooks/on-modify.timetrack.py", line 112, in <module> ledger.write(entry.encode("utf-8")) TypeError: write() argument must be str, not bytes
I'm on python3:
daraul/.task: python --version [INSERT] Python 3.7.4
Not sure if that matters.
I had to change that line to:
with open(LEDGERFILE, "ab") as ledger:
ledger.write(entry.encode("utf-8"))
notice the additional b
in the open command. Tells it to open as a binary.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for your comment 7uXi. Apparently github doesn't send notifications for comments on gists, or I would have adjusted this sooner.