-
-
Save wbsch/d977b0ac29aa1dfa4437 to your computer and use it in GitHub Desktop.
| #!/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)) |
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 ":"
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 :)
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.
Thank you for your comment 7uXi. Apparently github doesn't send notifications for comments on gists, or I would have adjusted this sooner.
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 bytesI'm on python3:
daraul/.task: python --version [INSERT] Python 3.7.4Not 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.
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"