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 --userfortimesheet autostart. With no chooser available at all, reminders fall back to closing the open session with a STOP at the next interval (one STOP, not one per interval) andtimesheet startdefaults to misc/unspecified instead of prompting.timesheet installalso adds a per-user application-menu entry (~/.local/share/applications/timesheet.desktop) named "Timesheet" that runstimesheet start, so starting work is a point-and-click action from the Kubuntu/KDE launcher (or GNOME Activities, or any XDG menu) — the analog of the Windows Start Menu shortcut. The icon is installed into the hicolor theme as~/.local/share/icons/hicolor/scalable/apps/timesheet.svgand referenced by name, so it survives the binary moving.timesheet uninstallremoves both. - A single-click chooser built with Python 3 + PyQt (
-
Windows: no extra dependencies to build or run; PowerShell (built in) drives the reminder chooser and shortcuts. The log lives at
%USERPROFILE%\Documents\timesheet.log.timesheet startwith no activity shows a full-screen chooser (PowerShell/WinForms) to pick or enter an activity, same as macOS/Linux. The reminder daemon runs in the background and re-prompts at the configured interval;timesheet stop/timesheet intervalstop it (and any open chooser window) the same way as elsewhere.timesheet autostartregisters a Startup-folder shortcut to runtimesheet startat login (the per-user, no-admin-required analog of a macOS LaunchAgent or Linux systemd user unit); STOP at logoff/shutdown is recorded by the daemon's console control handler. Unlike macOS (LogoutHook) and Linux (systemdExecStopplus a system-level logout-hook unit), there is no second, more-guaranteed mechanism available without admin rights, so this one path is best-effort — Windows does not guarantee it waits for the handler to finish the way launchd/systemd do.timesheet editfalls back tonotepad(instead ofvi) when$EDITOR/$VISUALare unset.timesheet helphas nogroff/lessto page through, so it renders the man page as plain text throughmoreinstead.timesheet installinstalls the binary astimesheet.exe(see Install) and creates a per-user Start Menu shortcut ("Start Timesheet") that runstimesheet.exe start, so starting work is also a point-and-click action. -
timesheet pdfandtimesheet email: PDF filling and message building are compiled in, so there is nothing to install fortimesheet pdf.timesheet emailhands the finished message to asendmail(8)-compatible binary —/usr/sbin/sendmailon Linux and macOS,sendmail.exefound onPATHon Windows, unlesssendmail:names another — so delivery needs a local MTA (Postfix, ssmtp, msmtp) or a wrapper script, configured there rather than here. See Supplyingsendmail.exeon Windows.
The log file contains one entry per line. The timestamp is the first field, in strict ISO 8601 (RFC 3339) with microsecond precision and a local UTC offset:
ISO8601_timestamp|START|activityISO8601_timestamp|STOP
For example:
2026-08-03T08:00:00.000000-04:00|START|ST:Welcome session
2026-08-03T09:00:00.000000-04:00|STOP
The wall-clock time in the recorded offset is read back as local time without converting through UTC, so a log stays readable after a timezone change.
Start/stop pairs are matched in LIFO order (each STOP pairs with the most recent START). A START also closes any session still open before it, so consecutive STARTs each contribute their own interval. The report uses these pairs to compute duration and attribute time to activity and day of week.
Earlier versions wrote the kind first, as START|ISO8601_timestamp|activity and STOP|ISO8601_timestamp. timesheet migrate converts every timesheet.* file in the log directory to the current field order; lines already in it are left alone.
Optional settings live in ~/.config/timesheet.yml (or $XDG_CONFIG_HOME/timesheet.yml; $TS_CONFIG overrides both, and a timesheet.yaml sibling is used if no .yml exists). The file does not exist by default, and every setting except the pdf/email template has a default, so no configuration is needed to track time.
Only a small YAML subset is understood: key: value pairs, # comments, optional quotes, indented nesting, and sequences (either - item lines or [a, b]). Unknown keys are ignored, and a value that can't be understood prints a warning on stderr and falls back to the default. Quote a value whose leading or trailing spaces matter, such as separator: "; ".
timesheet rotates timesheet.log to timesheet.YYMMDD at the start of each week, so each rotated file holds exactly one work week. By default the week begins Sunday at 00:00 local time. If your employer's week runs Monday through Sunday — rotating at midnight between Sunday night and Monday morning — say so:
# ~/.config/timesheet.yml
rotate:
day: monday
time: "00:00"day— weekday name or three-letter abbreviation, any case (monday,Mon,SUNDAY). Default:sunday.time—HH:MM,HH:MM:SS, a bare hour, or a 12-hour time with a meridiem (5pm,5:30 PM), in local time. Default:00:00.
A scalar shorthand works too: rotate: monday, or rotate: "fri 17:00" for a week that turns over Friday at 5 pm.
The rotation boundary is checked by timesheet start, timesheet stop, timesheet started, timesheet timeoff and the reminder daemon: if the log's last entry falls before the most recent boundary, the log is rotated before the new entry is recorded. timesheet rotate run by hand always rotates, whatever the boundary. The same boundary defines "this week" for timesheet alias, and the week that timesheet pdf and timesheet email report.
Each of these may be written at the top level, or under prefixes: → PREFIX so that it applies only when that prefix is in use. A per-prefix value beats the top-level one, and a command-line option beats both — which is how one log can serve several jobs, each tagging its activities (ST:Setup Jira) and keeping its own name, template, addresses and field map.
| Setting | Meaning |
|---|---|
name |
Full name as it should appear on the timesheet. Required. |
prefix |
Default for --prefix. When absent and exactly one prefix is listed under prefixes:, that one is used. |
template |
Default for --template: path to the form-fillable PDF. No built-in default. |
output |
Default for --output. When absent, timesheet pdf writes to stdout. |
activity, separator, zero |
Defaults for --activity, --separator and --zero. |
to, cc |
Default recipients; each is either one address or a sequence of them. |
from, reply |
Default sender and Reply-To addresses. |
subject, body |
Message templates, taking the same placeholders as output plus {total_hours}. |
min_font_size, max_font_size |
Shrink-to-fit range in points (default 5 and 10). |
fields |
Maps each timesheet slot to a form-field name. Defaults suit the stock form; anything listed here replaces only the slots it names. |
sendmail |
Path to the sendmail(8)-compatible binary the message is piped to. Default /usr/sbin/sendmail, or sendmail.exe resolved against PATH on Windows. A leading ~/ is expanded. |
The slots that fields maps are contractor_name, week_start_month/_day/_year, week_end_month/_day/_year, <weekday>_hours and <weekday>_activities for each of the seven weekdays, and total_hours. The field names of a different form can be listed with mutool show form.pdf form | grep Name:.
# ~/.config/timesheet.yml
name: "Jane Contractor"
from: "jane@example.com"
prefixes:
ST:
template: "~/Documents/timesheet-fillable.pdf"
output: "timesheet_Jane_{week_start}-{week_end}.pdf"
separator: "; "
zero: ""
reply: "jane@employer.example"
to: "timesheets@employer.example"timesheet email does not speak SMTP itself. It runs <binary> -i -f <from> <recipient>..., writes the message to its standard input, and repeats whatever the binary prints to standard error — as a warning when it succeeds, as the error when it does not. Relay, credentials and queueing therefore live in the MTA's own configuration, not here.
sendmail is scoped like every other setting, so one job can be sent by a wrapper that mails through a different account while the rest use the system binary:
prefixes:
ST:
sendmail: "~/.local/bin/sendmail-employer"Anything that reads a message on stdin and takes sendmail's arguments will do. A wrapper that sends through an account other than the one in from will usually rewrite the From header, so set reply to the address you actually read.
Windows ships no MTA and has no conventional path for one, so the default there is the bare name sendmail.exe, resolved against PATH. msmtp supplies a working one: it is an SMTP client with a sendmail-compatible interface, so it takes -f and the recipients as operands, accepts and ignores -i, and returns sendmail's exit codes — which is exactly what timesheet email drives. Its own TLS and its own account configuration (msmtprc) replace the relay settings this program used to carry. Two ways to get it:
- Portable build — unzip
msmtp_windows_portable_<version>.zipfrom SourceForge; it is a 64-bit build with no installer. - MSYS2 —
pacman -S mingw-w64-x86_64-msmtp, which putsmsmtp.exein the mingw64bindirectory.
Then either copy msmtp.exe to sendmail.exe somewhere on PATH, or skip the renaming and point the setting at it:
sendmail: "C:/msys64/mingw64/bin/msmtp.exe"Configure the relay and credentials in msmtp's own msmtprc (~/.msmtprc; see its manual for where it looks on Windows), not here.
XAMPP's bundled "fake sendmail" (sendmail.exe by Byron Jones, and the sendmail-tls1-2 fork) is the other binary of that name in circulation. It is documented as emulating sendmail's -t option — recipients taken from the headers — whereas timesheet email passes the envelope as operands, so check that it actually delivers to everyone before relying on it.
A .cmd wrapper also works if you would rather delegate: sendmail: can name a batch file that calls wsl.exe -e /usr/sbin/sendmail "$@" or a PowerShell script.
timesheet pdf aggregates one week and fills a form-fillable PDF with it; timesheet email does the same and mails the result as an attachment.
timesheet pdf > timesheet.pdf # the week just worked, to stdout
timesheet pdf -o ~/Documents # into a directory, using the configured file name
timesheet pdf -1 # the most recently rotated week
timesheet pdf 260727 # the week containing 2026-07-27
timesheet email # fill and send in one stepThe optional week argument takes the same forms as timesheet list: a log file path, log for the current log, a negative rotated-log index (-1 is the most recently rotated), or a date (YYYYMMDD, YYMMDD, M/D). With no argument, the week in progress is reported on its final day and the most recently completed week on any other day — so a run late on the last day of the week, or at any time in the days after it, both report the week just worked.
Hours are credited to the day each session started on, exactly as timesheet list accounts for them, and the printed total is the sum of the day figures as rounded, so the column adds up on paper. Every log is read, so a week that straddles a rotation still reports in full.
--prefix ST reports only activities beginning ST: and strips that tag, so an entry logged as ST:Setup Jira is reported as Setup Jira. Entries without the tag belong to another job and are left out entirely — their hours as well as their descriptions. An empty prefix (-p "") reports every entry unchanged, while still reading the settings of the prefix the configuration would otherwise have selected — so the template and addresses need not be restated.
timesheet list takes -p/--prefix too, filtering and stripping the same way, so timesheet list -p ST previews on screen the hours timesheet pdf -p ST will report. It reads no configuration, so the option is the only thing that filters: with no --prefix, timesheet list reports every activity as it always has, and an empty -p "" does the same.
--output accepts {date}, {week_start}, {week_end}, {name} and {prefix}, an existing directory (which receives the configured file name), or - for stdout. Writing a PDF to a terminal is refused.
Text is shrunk to fit its cell and, in the activity columns, wrapped; a description that cannot fit even at min_font_size warns on stderr and is clipped. Appearance streams are generated rather than left to the viewer, so the filled text shows up in every reader, when printed, and to text extractors.
If a send fails, the finished PDF is kept on disk rather than discarded, so the message can be retried without rebuilding a week that may have moved on.
The timesheet command takes a required subcommand as its first argument. Full documentation: timesheet help or timesheet 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 timesheet start on login and timesheet stop on logout/shutdown (macOS: LaunchAgents + logout hook; Linux: systemd user units + a system-level logout hook; Windows: a Startup-folder shortcut, with STOP at logoff/shutdown handled by the daemon's best-effort console control handler — no admin-required second guarantee exists there). 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 timesheet autostart uninstall to remove. |
edit |
Open the timesheet log ($HOME/Documents/timesheet.log) in your editor, taken from $EDITOR (then $VISUAL) — but only when run interactively (a real tty on both stdin and stdout). Otherwise (e.g. launched from a GUI menu entry, where those variables usually aren't set anyway) it opens via the OS default instead: the program associated with .txt files on Windows, open on macOS, xdg-open on Linux. vi is the last-resort fallback for an interactive terminal with neither variable set. |
email |
Fill the timesheet PDF as pdf does and mail it as an attachment. Takes every pdf option, except that -t means --to here (the template is --template or -T), plus -c/--cc, -f/--from and -r/--reply. See Filling and sending the timesheet. |
help |
Show the manual page in a pager (groff -man -Tascii | less; on Windows, rendered as plain text and paged through more). |
install |
Copy the binary (and on macOS the embedded icon as ts-icon.svg) to a directory on PATH. Optional: timesheet install [install_dir] [repo_path]. Works without the source repo on macOS (icon is embedded). On Windows, installs as timesheet.exe (not ts.exe — see Install) and also creates a "Start Timesheet" Start Menu shortcut that runs timesheet.exe start. On Linux, creates a "Timesheet" application-menu entry that runs timesheet start. If the reminder daemon is running from the target binary (locking it open), stops it without logging a STOP, copies the new binary into place, then restarts the daemon from it — the open session carries through the reinstall untouched. |
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. timesheet list 2/19, timesheet list 260220, timesheet list -1) to select a log. If work in progress, shows current task and duration. -p/--prefix PREFIX reports only one job's activities, as for pdf. |
manpage |
Output the Unix manual page in groff format to stdout. |
pdf |
Fill a form-fillable PDF template with one week of the timesheet and write it to a file or to stdout. Optional file/extension, date, or negative rotated-log index selects the week, exactly as for list. Options: -p/--prefix, -o/--output, -t/--template, -a/--activity, -s/--separator, -z/--zero. See Filling and sending the timesheet. |
prefix |
Prepend <prefix>: to this week's activities matching a pattern. timesheet prefix foo bar is equivalent to timesheet alias bar foo:bar, prompting per match just like alias. |
rebuild |
Build from source and install into the directory of the running binary. Optional directory argument; see timesheet help. |
uninstall |
Stop the reminder daemon, remove autostart hooks, optionally remove timesheet log files, then remove ts-icon.svg and the timesheet 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. Happens automatically at the start of each week — see Configuration. |
start |
Record work start now. With no activity: shows the reminder dialog to pick/enter an activity (macOS; Windows; Linux with kdialog/zenity installed); otherwise defaults to misc/unspecified. If a session is already open, a STOP is added only when that START is more than one reminder interval old (capped to one interval after it, leaving the time you were away unbilled) — otherwise the new START closes the previous session on its own, since pairs match in LIFO order. Starts the reminder daemon if not already running. |
started |
Record a work start at a past time. Args: timesheet started <start_time> [activity...]. See Time formats. |
stop |
Record work stop at now or at an optional stop time. If the last entry is already STOP and no time is given, the log is left alone; if a time is given, the last STOP is amended. If the last entry is START, appends the new STOP. Always stops the reminder daemon and closes any prompt it has on screen, even when the log needed no change, 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. |
timesheet started <start_time> and timesheet stop [stop_time] accept the same forms. A time with no date
means today; a date with no time means midnight that day. Quote any argument containing a space.
| Form | Examples |
|---|---|
| ISO 8601 | 2026-08-06T07:00:00-04:00 |
| Date and time | "2026-08-06 07:00", "08/06/2026 7:00 PM", "8/6 7am" |
| 24-hour time | 07:00, 07:00:30, 7 |
| 12-hour time | 7am, 7 AM, 7pm, 7:30pm, "12:15:30 p.m." |
| Date only | 2026-08-06, 08/06/2026, 8/6 |
A bare hour is 24-hour (19 is 7 pm), so a bare 12 is noon while 12am is midnight. The
meridiem is case-insensitive and may be written am/pm, a.m./p.m., or a/p. MM/DD
without a year means the current year.
timesheet startstarts the reminder daemon if it is not already running. With no activity,timesheet 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; Windows via PowerShell/WinForms). 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): the window covers the full screen and stays on top, with the choices in a centered panel. 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.
timesheet stopalways stops the reminder daemon and shows a dialog that reminders have been stopped (skipped during logout/shutdown). This happens even when the log itself needs no new entry — after an unanswered reminder, which records its own STOP,timesheet stopwould otherwise have nothing to write and would leave the daemon running to prompt again one interval later. A prompt that is still on screen is closed along with the daemon: on macOS/Linux the daemon runs in its own process group, so it is signalled as a group; on Windows the daemon and any chooser window it spawned are members of the same Job Object, terminated together the same way. A daemon thattimesheet stopcould not signal (a stray that no longer owns the PID file) notices the file is gone within half a second and exits instead of prompting again, closing its own prompt if one is up.timesheet intervalortimesheet restart [duration]sets or shows the interval and restarts the daemon.- Reminder behavior: If a reminder goes unanswered for one reminder interval, a STOP is recorded at the time the reminder appeared — not when the interval expired. That timestamp is used exactly, without the one-interval cap: the reminder appears one interval after your previous entry, so it already marks the last moment you were known to be working. The reminder is then left on screen rather than dismissed (macOS also brings it back to the front). When you get back to your desk and pick an activity, that START is recorded at your return time, so the time you were away sits between the STOP and the new START — unbilled — and your return is logged accurately. No second STOP is added while work is already stopped, so leaving the screen unattended records a single STOP rather than one per interval. The reminder window covers the full screen and stays on top (macOS, Linux, and Windows), so a mouse action in progress when it appears cannot accidentally hide it. 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 (capped to one interval after the latest entry); on Linux the systemd session unit’s
ExecStoprunstimesheet stopinstead, and the daemon stays silent on SIGTERM (systemd may signal it during ordinary teardown, so a STOP there would be spurious); on Windows the daemon records STOP from a console control handler onCTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT, best-effort since Windows does not guarantee it waits for the handler to finish. - Automatic STOP cap: Whenever a STOP is added automatically (a missed shutdown reconciled at the next
timesheet start/timesheet autostart, closing the previous session before a new START, ortimesheet 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; seetimesheet interval). So forgetting to stop never records work all night: the session ends at most one interval after your last logged activity. An unanswered reminder is the one exception, and needs no cap: its STOP is stamped at the moment the reminder appeared, which is already one interval after the previous entry. timesheet autostart [interval]registerstimesheet startat login andtimesheet stopat logout/shutdown. On Linux the login unit (ts-autostart-start.service) is wanted by and ordered aftergraphical-session.target, so the chooser waits for the desktop session instead of racing it — a unit wanted bydefault.targetstarts seconds before the compositor exists, and the chooser then has no display to open on. 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. On macOS/Linux 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) whoseExecStoprunstimesheet stopbeforeshutdown.target. Installing the hook needs administrator access, sotimesheet autostartprints thesudocommand and offers to run it; if you decline, run the printed command yourself. Once the hook is present, later runs skip it. On Windows, login registration is a Startup-folder shortcut (no admin needed) and there is no second guarantee available without admin rights, so the daemon's console control handler is the only STOP-at-logoff mechanism there.timesheet autostart uninstalloffers to remove it (viasudoon macOS/Linux).
From the repository directory:
cargo build --release && ./target/release/timesheet installTo install into a specific directory (e.g. ~/bin): timesheet install ~/bin. Or copy manually:
cp target/release/timesheet ~/bin/timesheet
chmod +x ~/bin/timesheetThe binary uses $HOME/Documents/timesheet.log by default (%USERPROFILE%\Documents\timesheet.log on Windows, where timesheet install writes timesheet.exe — not ts.exe, since that name collides too easily with unrelated preinstalled tools — and no chmod step is needed).
If you invoke timesheet.exe from a WSL shell (e.g. a shell function that calls the interop path directly, since the per-user Windows install location isn't on WSL's PATH), WSL does not forward arbitrary environment variables across the interop boundary — only names listed in $WSLENV cross over. Without it, timesheet.exe never sees $EDITOR/$VISUAL, so timesheet edit falls back to the Windows .txt handler even when run from an interactive terminal. Add this to your shell profile:
export WSLENV="EDITOR/w:VISUAL/w${WSLENV:+:$WSLENV}"The /w flag forwards the variable WSL-to-Windows only.
Build with Rust installed:
cargo build --releaseThe binary is produced at target/release/timesheet (or target/debug/timesheet for cargo build). See INSTALL.md for full instructions.
To build a Windows executable from within Windows Subsystem for Linux (WSL), you need the MinGW compiler toolchain and the Rust Windows target:
-
Install MinGW and dependencies:
On Ubuntu/Debian-based WSL:
sudo apt install mingw-w64 pkg-config
On other distributions, install the equivalent MinGW package (e.g.,
mingw-w64on Fedora/RHEL). -
Install the Windows Rust target (if not already installed):
rustup target add x86_64-pc-windows-gnu
-
Build for Windows:
cargo build --release --target x86_64-pc-windows-gnu
The Windows executable will be produced at
target/x86_64-pc-windows-gnu/release/deps/timesheet-*.exe(where*is a hash). -
Access from Windows — the executable is accessible from Windows at a path like:
\\wsl$\Ubuntu\home\<username>\src\github\pillarsdotnet\timesheet\target\x86_64-pc-windows-gnu\release\deps\timesheet-<hash>.exeOr copy it to a more convenient location:
copy \\wsl$\Ubuntu\home\<username>\...\timesheet-<hash>.exe timesheet.exe
-
Install on Windows — run the executable's
installsubcommand from PowerShell to self-install:From WSL, using PowerShell Core:
pwsh -Command "& './target/x86_64-pc-windows-gnu/release/deps/timesheet-<hash>.exe' install"Or from Windows PowerShell after copying the binary:
Copy-Item '\\wsl$\Ubuntu\home\<username>\...\timesheet-<hash>.exe' 'timesheet.exe' .\timesheet.exe install
This installs the binary to
%LOCALAPPDATA%\Programs\timesheet\timesheet.exeand creates a "Start Timesheet" shortcut in the Windows Start Menu.
This cross-compilation approach avoids the need to install a separate Rust/build environment on Windows itself — the toolchain remains in WSL while producing a native Windows binary.
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/timesheet/.
For command-line usage, run timesheet help or timesheet manpage.