A personal time-tracking tray app for Linux, built around a single plain-text worklog file. The tray icon shows whether a task is running; a click opens a text editor over this month's worklog so you log work by typing a line. The file is the whole database -- editable by hand, in any editor, while the app runs.
Requires Python 3 with PyGObject (gi, GTK 3), pystray, and Pillow (PIL).
There is no build step or packaging.
./timer.py # launch the tray app
Worklog files live in ~/worklog/ (override with the WORKLOG_DIR environment
variable), one file per month, named YYYY-MM.txt. The directory and files are
created on demand.
./timer.py --task <token> # segments logged for one task token
./timer.py --project [prefix] # list project prefixes, or segments for one
./timer.py day|week|month|year|YYYY-MM-DD [prefix] # time report
./stat.py [period] [prefix] # same report, standalone
./analyze_worklog.py <file> # report for a single worklog file
- Left click opens the editor on this month's file, scrolled to the bottom
with a fresh
HH:MMline seeded under today's header and the cursor waiting on it. Type(token) what you startedto begin a task, or plain text to jot a note. Close the window (orCtrl+Enter, orEsc) to save; an untouched seeded line is discarded. - A strip above the text draws today's task spans on an hourly timeline, one colour per task, the running task outlined.
[ ]/[x]checkboxes in the text toggle on click.- Right click allows to stop the running task, and shows today's per-task totals.
- The icon is a red disc while a task runs. If a task has two-letter prefix (like pr-sometask), that prefix shows over the red disc. Full task name shows in the tooltip.
UTF-8 text, read line by line. Three line kinds carry meaning; everything else is a free note kept for display but ignored for time accounting.
# 2026-03-02 optional day comment a day header; lines below belong to it
9:00 (ap-321) fixing the importer open task ap-321 at 9:00
9:20 found the off-by-one a timestamped note on the open task
[x] making arbitrary notes with some [ ] clickable boxes
9:45 (bp-7) switch projects close ap-321, open bp-7
10:30 (*) coffee stop: close the task, start an idle gap
- A time entry with a
(token)is a boundary: it closes the open segment and opens a new one. Without a token it is a note on the open segment and does not split time. The token*closes the segment into an idle gap that is never counted. - A task token's project is everything before the first
-(ap-321->ap). If that prefix is two letters, it is also shown on the tray icon. There is no separate project or issue field -- the token is the whole identity. - Times are always minute-resolution.
- A timed line in the future is a reminder: the app reopens the editor on that line when its moment arrives.
To fix or delete anything, edit the file -- the app has no delete command and picks up external edits within about a second.
Layered by responsibility, one file per concern:
worklog.py-- the (only) format engine: parsing and aggregation.parse(text)returns aJournalofSegments plus the open segment and a list of parseproblems.store.py-- the only module that touches disk: file location, month rollover, atomic append and whole-file replace, and an mtime change token.timer.py-- the trayApp(pystray icon, callable menu, 1s tick) and theEditorWindow(GTK text view plus the timeline), and the CLI.stat.py,analyze_worklog.py-- report front-ends over the same parser: across all months, and over one file.
Run the self-tests with python3 worklog.py and python3 store.py.
- The worklog text file is the single source of truth. Apart from the UI, the state of the time tracker is reperesented in the current text file.
- The whole tray interaction is one editor window over the month file, seeding a line to type into. This interaction draws user focus to the worklog, and is preferred over automated operations.
- The only remaining automation is a quick "end task" action to be used when there's no time write text notes.
- The project code is derived from the token rather than stored, to prevent any references in data storage.
- Minute resolution is deliberate, as a minimum that deserves user attention.
- "Running now" is an open segment dated today; an open segment on an earlier day is not a running task but a surfaced parse problem.
- Parsing never throws, and does not attempt to overwrite the file. A malformed or mid-edit file yields the last good state plus a list of problems.
- File watching: mtime polling or
Gio.FileMonitor. Polling is immune to rename-and-replace saves and reuses the clock tick;FileMonitorwould give sub-second response but needs care around inode-replacing saves. - A task left open across midnight or a month boundary: surfaced as an unclosed-earlier-day problem, or handled automatically by splitting at the boundary. Surfacing keeps the parser simple and matches the "close the day before leaving" workflow; auto-splitting would remove a documented sharp edge at the cost of engine complexity.