A lightweight, automatic time-tracking desktop app for Windows 11. It runs silently in the system tray, watches the active window title and the keywords you type, and automatically attributes each block of active time to a client and task. It pauses on idle and exports CSV timesheets for any period.
- Active-window polling (every 2s, configurable) reads the foreground window title and process name via the Windows API.
- Keystroke matching uses a small in-memory rolling buffer of recently typed characters, matched against your client keywords. Typed keywords only count while fresh (default 120s) so a client name typed once can't misattribute hours of later activity. Raw keystrokes are never written to disk or the database.
- Everything is logged — time that matches no client is stored as
Unassigned(visible in reports, reassignable from the timeline), so no work time silently disappears. - Live checkpointing — the current interval is written to the database the moment it starts and its end time is saved every 15s, so totals are live and a crash loses at most one checkpoint period.
- Task buckets — within each client, time is grouped by task: either named task rules you define (keywords per task) or an automatic label derived from the window title (browser suffixes and tab counters stripped, file paths reduced to the folder name).
- Manual pin — tray menu → Pin client (or the Today tab) forces all activity to one client, for calls and meetings no window title will catch. The pin clears automatically when you go idle.
- Idle detection uses
GetLastInputInfo; afteridle_timeout_secwith no input, tracking pauses and no time is accrued. - Single instance — a named mutex prevents a second copy from launching and double-counting.
Attribution priority: manual pin > window-title match > fresh typed keyword >
Unassigned. No OCR / screen capture is used (idle CPU ≈ 0%, RAM < 50 MB).
| Tab | What it does |
|---|---|
| Today | Live status (client, task, session length, today's total), pin-to-client selector, colour-coded timeline of the day. Select rows → Reassign selected to → Apply to retroactively correct attribution. |
| Reports | Period picker (today / this week / last week / this month / last month / custom dates), client → task breakdown with hours and %, include/exclude Unassigned, CSV export. Every task row has a checkbox — tick several (clicking a client header ticks all its tasks), pick a target in Assign checked to (a client, Unassigned, or Delete these entries to remove junk permanently, with confirmation) and hit Apply. Right-click any row for the same options on a single row. Ideal for clearing the Unassigned bucket. |
| Clients & Rules | Add/delete clients, edit window and typed keywords, define named tasks per client. Saves apply immediately — no restart. |
| Settings | Idle timeout, poll interval, keyword freshness, checkpoint interval, window-title privacy toggle, Unassigned logging toggle, autostart at Windows login, pause/resume. |
cd time-tracker
py -m venv venv # skip if venv exists
venv\Scripts\activate
pip install -r requirements.txt
python -m src.main # or run.bat for a silent start| Key | Meaning | Default |
|---|---|---|
poll_interval_sec |
How often the active window is checked | 2 |
idle_timeout_sec |
Idle time before tracking pauses | 180 |
typed_keyword_ttl_sec |
How long a typed keyword stays valid | 120 |
checkpoint_interval_sec |
Seconds between live DB saves of the open interval | 15 |
keyword_buffer_len |
Max chars held in the in-memory typed buffer | 200 |
store_window_title |
false = log process name only (extra privacy) |
true |
track_unassigned |
Log unmatched time as Unassigned |
true |
clients |
Per-client window_keywords, typed_keywords, and optional tasks ({"Task Name": {"keywords": [...]}}) |
seeded |
All of it is editable in the GUI; matching is case-insensitive substring, and the longest matching keyword wins when several clients match.
Data lives next to the source (config.json, timetracker.db); a built
.exe uses %LOCALAPPDATA%\TimeTracker instead. v1 databases are migrated
in place automatically (new task/process columns).
- Summary CSV — Client, Task, Hours, Percent for the chosen period, with client subtotal rows.
- Detail CSV — every merged interval: client, task, window title, process, start, end, duration, source.
Weeks start on Monday. Sessions crossing a period boundary are clipped correctly.
venv\Scripts\python -m pytest tests/ -qvenv\Scripts\activate
pip install pyinstaller
pyinstaller --noconsole --onefile --name TimeTracker ^
--add-data "config.json;." ^
--add-data "assets;assets" ^
--icon "assets\timetracker.ico" ^
--hidden-import win32timezone ^
src\main.pyThe executable lands in dist\TimeTracker.exe; on first launch it creates its
config and database under %LOCALAPPDATA%\TimeTracker.
Both are toggles on the Settings tab:
- "Start TimeTracker automatically at Windows login" — creates
TimeTracker.lnkinshell:startup; untick to remove. - "Show TimeTracker icon on the desktop" — creates a desktop shortcut
(with the app icon from
assets/timetracker.ico).
Double-clicking the desktop icon while TimeTracker is already running simply opens the dashboard of the running instance — it never starts a duplicate.
Note: Task Manager shows two pythonw.exe processes for one running
TimeTracker. That's normal — the venv's pythonw.exe is a tiny launcher shim
that starts the real interpreter as a child and waits.
This app records the foreground window title and process name plus
timestamps — stored locally in SQLite, never transmitted anywhere. Typed text
is used only transiently in memory to match client keywords and is never
persisted. Set store_window_title to false to log only application names.