Terminal-first time tracking, reporting, and invoicing for solo developers.
Track time where you already live — the terminal. Log work in natural language, run timers, roll everything up into reports, and send client-ready PDF invoices, all without leaving your shell.
$ ttd log "today 8am to 5pm" -p api-rewrite --note "auth endpoints"
✓ Logged 9:00 on Tue Jun 9 (8:00am–5:00pm)
$ ttd invoice create --client acme-corp --month 2026-06 --pdf
✓ Created invoice 2026-001
✓ Wrote ~/Documents/invoices/2026-001-acme-corp.pdf
Full documentation: syn54x.github.io/ttd — guides, the TUI tour, time-expression cookbook, and the complete CLI and configuration reference.
uv tool install ttd-ledger # or: pipx install ttd-ledger
ttd --install-completion # shell completion- CLI — every feature is a command. Scriptable, pipeable.
-iforms — add-ito any mutating command for an interactive form; any flags you already passed pre-fill it.- TUI — run bare
ttdfor the full-screen app: live timer, activity heatmap, day-by-day timesheet with as-you-type time parsing, reports, and invoice management. Keys:1–5screens,sstart/stop,lquick-log.
Try it with demo data: ttd db seed-demo && ttd (add --reset to wipe and reseed)
Natural language, retrospective or live:
ttd log "today 8am to 5pm" # interval
ttd log "yesterday 9-11:30" # am/pm inferred from your workday window
ttd log "monday 1pm for 3 hours" # most recent monday
ttd log "2h this morning" # duration-only entry
ttd log "6/3 10am-1pm" # explicit date
ttd start api-rewrite # live timer
ttd stop --at 5pm
ttd statusMultiple entries per project per day are normal; reports and invoices roll them up into hours per day. Ambiguous times ("6 to 8") are rejected with the candidate readings instead of silently guessing.
ttd client add "Acme Corp" --rate 150 --email billing@acme.test
ttd project add "API Rewrite" --client acme-corp # inherits $150/h
ttd project add "Mobile App" --client acme-corp --rate 175Rates resolve project → client → [business].default_hourly_rate.
ttd report day # today, entry by entry
ttd report week --by project # sparklines + billable value
ttd report month 2026-05 --client acme-corp
ttd report range --from 2026-01-01 --to 2026-03-31 --by clientttd invoice create --client acme-corp --month 2026-05 --pdf --md
ttd invoice list
ttd invoice mark 2026-001 sent # then: paid, or voidBillable, uninvoiced entries roll up to one line per project per day, rounded
per your [billing] policy. Invoiced entries lock; voiding an invoice releases
them (numbers are never reused).
CSV, JSON, XLSX, and Apple Numbers — both directions:
ttd export hours.xlsx # format inferred from extension
ttd export backup.json # JSON carries client/project metadata
ttd import hours.csv --dry-run # preview: new/update/skip/errors
ttd import hours.csv --on-conflict update --create-missingImports match by entry id, then by content; invoiced entries are never touched.
Layered TOML, nearest-wins (like ruff): CLI flags → TTD_* env →
.ttd.toml (walks up from cwd) → ~/.config/ttd/config.toml → defaults.
# ~/.config/ttd/config.toml
[user]
name = "Taylor"
email = "taylor@example.com"
[business]
currency = "USD"
default_hourly_rate = 150
[billing]
rounding = "nearest" # nearest | up | none
increment_minutes = 15 # applied per project-day
[invoice]
number_format = "{year}-{seq:03d}"
payment_terms_days = 30
output_dir = "~/Documents/invoices"
[parsing]
workday_start = 7 # am/pm inference window
workday_end = 19Pin a repo to a project so bare ttd start / ttd log "2h" just work:
cd ~/code/acme-api
ttd config set defaults.client acme-corp --local
ttd config set defaults.project api-rewrite --localttd config list --origin shows where every value came from.
uv sync
just test # pytest
just lint # ruff + ty
just tui # textual dev modeStack: Python 3.13, Ferro-ORM over SQLite, Cyclopts, Rich, Textual, questionary, fpdf2, openpyxl, numbers-parser.