Skip to content

Instantly share code, notes, and snippets.

@wbsch
Last active November 10, 2021 21:17
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save wbsch/d977b0ac29aa1dfa4437 to your computer and use it in GitHub Desktop.
Save wbsch/d977b0ac29aa1dfa4437 to your computer and use it in GitHub Desktop.
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
Copy link

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
Copy link

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
Copy link
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
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
Copy link
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
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.

@insanerwayner
Copy link

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