Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Time tracking hook script for Taskwarrior that outputs ledger timelog formatted data.
#!/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))
@linuxcaffe

This comment has been minimized.

Copy link

linuxcaffe commented Feb 24, 2015

timelog files don't (seem to) support much metadata, other than "project", but I think project (if exist) and description can fruitfully be combined as "project.subproj:Description with spaces"

@linuxcaffe

This comment has been minimized.

Copy link

linuxcaffe commented Feb 24, 2015

optimal task timelog?

i 2015/02/23 15:34:21 project:subproj  description
o 2015/02/23 15:35:46  ; :tag1:tag2:tag3: uuid: 874865465765764576547655

i 2015/02/23 16:55:29 no project  description
o 2015/02/23 16:56:27  ; uuid: 8763876837638763

notes:

  • "project.subproject" becomes "project:subproject"
  • double-space before description
  • "no project" required placeholder
  • double-space before ";"
  • tags enclosed with ":"
@wbsch

This comment has been minimized.

Copy link
Owner Author

wbsch commented Mar 5, 2015

Note: Format described by David is now implemented, and a variation of https://bug.tasktools.org/browse/TW-1562 implemented. In other news, I never knew I was capable of writing Python code this ugly :)

@7uXi

This comment has been minimized.

Copy link

7uXi commented Jul 16, 2015

had problems with german umlauts (utf-8) on debian wheezy (python 2.7.3) added:
entry = entry.encode('utf-8')
after line 111

I am not a python programmer ;-). Seems to work.

Thanks for the hook.

@wbsch

This comment has been minimized.

Copy link
Owner Author

wbsch commented Jan 12, 2016

Thank you for your comment 7uXi. Apparently github doesn't send notifications for comments on gists, or I would have adjusted this sooner.

@daraul

This comment has been minimized.

Copy link

daraul commented Aug 27, 2019

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.