Copyright (c) 2025 Robert August Vincent II pillarsdotnet@gmail.com Co-author: Cursor-AI and GitHub Copilot.
CLI for tracking work start/stop and reporting time by activity and by day of week.
In the 90's, I had a boss who required me to turn in a detailed weekly timesheet listing exactly how much time I spent on each task, assigned or unassigned. As a borderline austistic, the idea of fudging or guessing at such a report was deeply troubling. So I self-assigned a task to write quick-and-dirty program that pops up every five minutes and asks what I've been doing. I called it "bugme".
My present position has similar reporting requirements, so I have recreated that old program with improvements. I took the opportunity to simultaneously scratch two itches: AI and the Rust Programming Language. So I used an AI agent almost exclusively to write the program code, both in its original form as a set of Korn Shell scripts, and in its current form as a Rust program.
One of these days, when I find the time, I'll read through the code and try to figure out how it works. For now I'm just glad that it does.
-
Timesheet data file:
~/Documents/timesheet.log(editDEFAULT_TIMESHEETinsrc/main.rsand rebuild to change) -
macOS: no extra dependencies (reminder dialogs use built-in AppleScript/AppKit).
-
Linux (KDE/Ubuntu/etc.): the reminder prompt uses, in order of preference:
- A single-click chooser built with Python 3 + PyQt (
python3pluspython3-pyqt6orpython3-pyqt5). This is the preferred experience: each entry acts on a single click with no OK/Cancel buttons (Qt, native on Wayland). Install e.g.sudo apt install python3-pyqt6. - A fallback list dialog via
kdialog(KDE/Plasma) orzenity(GNOME/other) when PyQt is unavailable — a select-then-OK list. Install whichever matches your desktop, e.g.sudo apt install kdialogorsudo apt install zenity.
notify-send(fromlibnotify-bin) is used for the "reminders stopped" notification, andsystemd --userforts autostart. With no chooser available at all, reminders fall back to recording a STOP at each interval andts startdefaults to misc/unspecified instead of prompting. - A single-click chooser built with Python 3 + PyQt (
The log file contains one entry per line:
START|unix_epoch|activitySTOP|unix_epoch
Start/stop pairs are matched in LIFO order (each STOP pairs with the most recent START). The report uses these pairs to compute duration and attribute time to activity and day of week.
The ts command takes a required subcommand as its first argument. Full documentation: ts help or ts manpage.
Subcommands (alphabetical):
| Subcommand | Description |
|---|---|
alias |
Interactively replace activity text in START entries from the current week. Matches the search text literally first; if nothing matches and the search text is a valid regex, falls back to regex search-and-replace. |
autostart |
Register ts start on login and ts stop on logout/shutdown (macOS: LaunchAgents + logout hook; Linux: systemd user units + a system-level logout hook). Optional first argument: interval (e.g. 5s, 3m) to set reminder interval and start the daemon in this session. Without interval: starts the daemon if needed and shows the current reminder interval. Use ts autostart uninstall to remove. |
edit |
Open the timesheet log ($HOME/Documents/timesheet.log) in your editor, taken from $EDITOR (then $VISUAL, else vi). |
help |
Show the manual page in a pager (groff -man -Tascii | less). |
install |
Copy the binary (and on macOS the embedded icon as ts-icon.svg) to a directory on PATH. Optional: ts install [install_dir] [repo_path]. Works without the source repo on macOS (icon is embedded). |
interval |
Set or show the reminder daemon interval (e.g. 3, 3m, 100s, 1h30m). With an argument, sets the interval and restarts the daemon. |
list |
Plaintext report: % time per activity, hours per day of week; optional file/extension, date, or negative rotated-log index (e.g. ts list 2/19, ts list 260220, ts list -1) to select a log. If work in progress, shows current task and duration. |
manpage |
Output the Unix manual page in groff format to stdout. |
rebuild |
Build from source and install into the directory of the running binary. Optional directory argument; see ts help. |
uninstall |
Stop the reminder daemon, remove autostart hooks, optionally remove timesheet log files, then remove ts-icon.svg and the ts binary from the install directory. |
rename |
Same as alias. |
reminder |
Alias for interval. |
restart |
Alias for interval (with no argument, reports current interval and restarts the daemon). |
rotate |
Rename timesheet.log to timesheet.YYMMDD using the earliest entry's date; if last entry is START, appends a STOP no later than one reminder interval after that entry first. If a file for that date already exists, appends to it. |
start |
Record work start now. With no activity: shows the reminder dialog to pick/enter an activity (macOS, or Linux with kdialog/zenity installed); otherwise defaults to misc/unspecified. Starts the reminder daemon if not already running. |
started |
Record a work start at a past time. Args: ts started <start_time> [activity...]. Time formats: e.g. YYYY-MM-DD HH:MM, HH:MM, or GNU date -d style. |
stop |
Record work stop at now or at an optional stop time. If the last entry is already STOP and no time is given, nothing happens; if a time is given, the last STOP is amended. If the last entry is START, appends the new STOP. When a stop is recorded, stops the reminder daemon and shows a dialog that reminders have been stopped (skipped during logout/shutdown). |
stopped |
Alias for stop. |
tail |
Latest ten log entries with timestamps in local time; START lines show duration. Consecutive STARTs with the same activity are collapsed, then last 10 shown. Optional file/extension or date match to select a log. |
timeoff |
Show the stop-work time for an 8 h/day average. Requires only a START entry (work in progress); no completed session on the current day is required. If the log is empty or the last entry is STOP, appends a START first. |
ts startstarts the reminder daemon if it is not already running. With no activity,ts startshows the reminder chooser immediately to pick/enter an activity (macOS via AppleScript/AppKit; Linux via the PyQt single-click chooser, falling back tokdialog/zenity). While this foreground chooser is open no daemon runs, so it cannot pop a second window; a fresh daemon starts once you pick. The daemon prompts “What are you working on?” at the configured interval.- Chooser (Linux, PyQt): a single click acts immediately — Stop Work records a STOP and stops reminders; an activity records a START for it and closes the window; Enter new activity… opens an input box where a non-empty entry (press Enter) records that activity and closes everything, while a blank entry returns you to the list.
ts stop(when it records a stop) stops the reminder daemon and shows a dialog that reminders have been stopped (skipped during logout/shutdown).ts intervalorts restart [duration]sets or shows the interval and restarts the daemon.- Reminder behavior: If the reminder times out without a mouse click, a STOP is recorded at the time the reminder appeared, capped to no more than one reminder interval after the latest log entry, and the existing reminder window is brought to the front (no new prompt is launched). If the reminder or “Enter new activity” dialog is dismissed without choosing (e.g. closed, Escape), it re-shows immediately. The “Enter new activity” text dialog has no timeout. At logout/shutdown the open session is stopped: on macOS the daemon records STOP when launchd sends it SIGTERM (same one-interval cap); on Linux the systemd session unit’s
ExecStoprunsts stopinstead, and the daemon stays silent on SIGTERM (systemd may signal it during ordinary teardown, so a STOP there would be spurious). - Automatic STOP cap: Whenever a STOP is added automatically (a missed shutdown reconciled at the next
ts start/ts autostart, closing the previous session before a new START, a reminder timeout, orts rotate), its timestamp is capped to no more than one reminder interval after the latest log entry — the interval is how often you’re prompted (default 5 minutes; seets interval). So forgetting to stop never records work all night: the session ends at most one interval after your last logged activity. ts autostart [interval](macOS/Linux) registersts startat login andts stopat logout/shutdown. An optional interval (e.g.5s,3m) sets the reminder interval and starts the daemon in this session so the reminder appears soon. Without interval: starts the daemon if needed and shows the current reminder interval. Startup skips a new START if the last log entry is a STOP less than 60 seconds old, and if startup finds a non-STOP event more than 5 minutes old it backfills a STOP one reminder interval after that event before recording the new START. It also installs a logout hook as a second guarantee that STOP is recorded at logout/shutdown: on macOS viacom.apple.loginwindow LogoutHook, on Linux via a system-level systemd unit (ts-logout-<uid>.service) whoseExecStoprunsts stopbeforeshutdown.target. Installing the hook needs administrator access, sots autostartprints thesudocommand and offers to run it; if you decline, run the printed command yourself. Once the hook is present, later runs skip it.ts autostart uninstalloffers to remove it (also viasudo).
From the repository directory:
cargo build --release && ./target/release/ts installTo install into a specific directory (e.g. ~/bin): ts install ~/bin. Or copy manually:
cp target/release/ts ~/bin/ts
chmod +x ~/bin/tsThe binary uses $HOME/Documents/timesheet.log by default.
Build with Rust installed:
cargo build --releaseThe binary is produced at target/release/ts (or target/debug/ts for cargo build). See INSTALL.md for full instructions.
To set up the full toolchain (Rust components, git hooks) and run the checks, see CONTRIBUTING.md.
The CI lint workflow checks commit messages with commitlint (Conventional Commits). Use a leading type and optional scope, e.g. feat(macos): add dock icon or fix: record STOP on shutdown. See .commitlintrc.yaml and Conventional Commits.
Rustdoc-compatible comments are in the Rust source. Generate and open the docs with:
cargo doc --no-deps --openOutput is under target/doc/ts/.
For command-line usage, run ts help or ts manpage.