▍ humdrum codex / ticktock v0.0.2
license AGPL-3.0
4.1 KB raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/usr/bin/env python3
"""tockr — tock entries for a date range, WITH notes and tags.

`tock export` covers ranges but drops notes; `tock report --date` keeps notes
but is single-day. This loops report per day and merges, so downstream tools
(weekly-summary, monthly-invoice, the review TUI) get complete data.

Usage:
  tockr <from> <to> [--project NAME] [--by-day|--by-task|--by-week]
  tockr week            # current ISO week (Mon-Sun)
  tockr month           # current calendar month
  tockr lastweek | lastmonth

Default output: JSON array of entries. Aggregations print a text summary.
"""
import datetime as dt
import json
import subprocess
import sys


def report_day(day, project=None):
    cmd = ["tock", "report", "--date", day.isoformat(), "--json"]
    if project:
        cmd += ["-p", project]
    r = subprocess.run(cmd, capture_output=True, text=True)
    if r.returncode != 0 or not r.stdout.strip():
        return []
    try:
        return json.loads(r.stdout)
    except ValueError:
        return []


def dur_secs(s):
    p = [int(x) for x in (s or "0").split(":")]
    while len(p) < 3:
        p.insert(0, 0)
    return p[-3] * 3600 + p[-2] * 60 + p[-1]


def daterange(a, b):
    d = a
    while d <= b:
        yield d
        d += dt.timedelta(days=1)


def resolve(args):
    """Map keyword shortcuts (week/month/...) to (from, to) dates."""
    today = dt.date.today()
    kw = args[0] if args else ""
    if kw == "week":
        s = today - dt.timedelta(days=today.weekday())
        return s, s + dt.timedelta(days=6), args[1:]
    if kw == "lastweek":
        s = today - dt.timedelta(days=today.weekday() + 7)
        return s, s + dt.timedelta(days=6), args[1:]
    if kw == "month":
        s = today.replace(day=1)
        nm = (s.replace(day=28) + dt.timedelta(days=4)).replace(day=1)
        return s, nm - dt.timedelta(days=1), args[1:]
    if kw == "lastmonth":
        first = today.replace(day=1)
        end = first - dt.timedelta(days=1)
        return end.replace(day=1), end, args[1:]
    return dt.date.fromisoformat(args[0]), dt.date.fromisoformat(args[1]), args[2:]


def fmt(secs):
    h, m = secs // 3600, (secs % 3600) // 60
    return f"{h}h {m}m"


def main():
    argv = sys.argv[1:]
    if not argv:
        sys.exit(__doc__)
    project = None
    if "--project" in argv:
        i = argv.index("--project")
        project = argv[i + 1]
        argv = argv[:i] + argv[i + 2:]
    mode = next((a for a in argv if a.startswith("--by-")), None)
    argv = [a for a in argv if not a.startswith("--by-")]

    start, end, _ = resolve(argv)
    entries = []
    for day in daterange(start, end):
        entries += report_day(day, project)

    if mode == "--by-day":
        agg = {}
        for e in entries:
            agg.setdefault(e["start_time"][:10], 0)
            agg[e["start_time"][:10]] += dur_secs(e.get("duration"))
        print(f"{start} → {end}")
        for day in sorted(agg):
            print(f"  {day}  {fmt(agg[day]):>8}  ({agg[day]/3600:.2f}h)")
        print(f"  TOTAL    {fmt(sum(agg.values())):>8}  ({sum(agg.values())/3600:.2f}h)")
    elif mode == "--by-task":
        agg = {}
        for e in entries:
            t = (e.get("tags") or [e.get("project", "-")])[0]
            agg[t] = agg.get(t, 0) + dur_secs(e.get("duration"))
        print(f"{start} → {end}")
        for t, s in sorted(agg.items(), key=lambda x: -x[1]):
            print(f"  {t:<16} {fmt(s):>8}  ({s/3600:.2f}h)")
        print(f"  TOTAL            {fmt(sum(agg.values())):>8}  ({sum(agg.values())/3600:.2f}h)")
    elif mode == "--by-week":
        agg = {}
        for e in entries:
            d = dt.date.fromisoformat(e["start_time"][:10])
            wk = d - dt.timedelta(days=d.weekday())
            agg[wk] = agg.get(wk, 0) + dur_secs(e.get("duration"))
        print(f"{start} → {end}")
        for wk in sorted(agg):
            print(f"  week of {wk}  {fmt(agg[wk]):>8}  ({agg[wk]/3600:.2f}h)")
        print(f"  TOTAL              {fmt(sum(agg.values())):>8}  ({sum(agg.values())/3600:.2f}h)")
    else:
        print(json.dumps(entries, indent=2))


if __name__ == "__main__":
    main()