A time tracker that only counts the time you were actually working.
Most trackers log the hours an app was open. That is not the same thing as the hours you worked. work-work runs the clock only while a chosen application is focused and you are producing input. Stop touching the keyboard and mouse and the clock stops, turns red, and tells you to get back to work.
It is a Linux/Wayland reimplementation of a small Windows utility that did exactly this, built for people who bill by the hour or just want an honest number at the end of the day.
- What it looks like
- How it decides you are working
- Setup — step by step
- Commands
- Guides
- Roadmap
- Contributing
- License
On the bar. The module sets a CSS class per state, so the bar reacts on its own. This is the styling from step 5c, pasted as-is:
In the terminal.
The numbers in these screenshots come from a seeded demo database, not from a real week — see docs/screenshots. The rendering itself is genuine output from the real binary.
Two signals, combined:
| Signal | Source | Tells us |
|---|---|---|
| Focus | compositor IPC | which window has keyboard focus |
| Activity | ext-idle-notify-v1 |
whether you have produced input recently |
focused app is in your config and not idle → the clock runs.
The interesting consequence is what work-work cannot see. ext-idle-notify-v1
is a standard Wayland protocol that reports only the transition between "active"
and "idle" — never keycodes, never coordinates, never window contents. So:
- no keylogging, and no ability to keylog
- no root, no setuid, no membership in the
inputgroup - no reading
/dev/input - nothing leaves your machine; history lives in a local SQLite file
That is a property of the protocol, not a promise in a README.
Idle time is never credited. With idle_timeout = 15, stopping work at
14:00:00 means the compositor tells us at 14:00:15, and the session is recorded
as ending at 14:00:00. You are never paid for the seconds you had already
stopped.
You need a Wayland compositor that implements ext-idle-notify-v1. Hyprland,
sway, KDE Plasma and other wlroots compositors all do. Right now only Hyprland
is supported for the focus half; see the roadmap.
You need Rust. On Arch: sudo pacman -S rust.
git clone https://github.com/britto64/work-work
cd work-work
cargo build --release
install -Dm755 target/release/workwork ~/.local/bin/workworkCheck it worked:
~/.local/bin/workwork --versionMake sure
~/.local/binis on your PATH so you can just typeworkwork. Ifworkwork --versionsays "command not found", add this to~/.zshrcor~/.bashrcand open a new terminal:export PATH="$HOME/.local/bin:$PATH"
workwork initThat writes ~/.config/workwork/config.toml. Open it in your editor and make it
look like this — this whole block is copy-paste ready, just change the apps
to the ones you use:
# ~/.config/workwork/config.toml
# Seconds of no input before the clock stops. 15 is a good starting point:
# long enough to think, short enough to be honest.
idle_timeout = 15
# Track every app under its own name, even ones with no [[project]] rule.
# Leave this false once you have written your rules.
track_all = false
# Your daily target, in minutes. 360 = 6 hours. Drives the Waybar percentage.
daily_goal_minutes = 360
# What the tooltip says when you stop working with a tracked app focused.
idle_message = "get back to work"
# ---------------------------------------------------------------------------
# PROJECTS — each one is a bucket of time.
#
# A window belongs to a project if:
# its app id matches something in `apps` <- a whole APPLICATION
# OR its title matches something in `titles` <- a specific WINDOW
#
# Matching ignores upper/lower case, and `*` means "anything can go here".
# The FIRST project that matches wins, so put specific rules ABOVE general ones.
# ---------------------------------------------------------------------------
[[project]]
name = "3D"
icon = ""
apps = ["blender"]
[[project]]
name = "Design"
icon = ""
apps = ["krita", "gimp", "org.inkscape.Inkscape"]
[[project]]
name = "Code"
icon = ""
# NOTE: most Wayland apps use a long reverse-DNS name. Ghostty is
# "com.mitchellh.ghostty", NOT "ghostty". Run `workwork apps` to see the
# real names instead of guessing.
apps = ["com.mitchellh.ghostty", "code", "dev.zed.Zed"]
# ---------------------------------------------------------------------------
# Tracking one WINDOW instead of a whole app.
#
# This splits a single application across projects. "Reference" is listed
# ABOVE "Browsing", so ArtStation in Firefox counts as work while the rest of
# Firefox does not. Delete the # to switch these on.
# ---------------------------------------------------------------------------
# [[project]]
# name = "Reference"
# icon = ""
# titles = ["*ArtStation*", "*Pinterest*", "*Sketchfab*"]
# [[project]]
# name = "Browsing"
# icon = ""
# apps = ["firefox"]Anything with no rule is simply not tracked — the clock stops when you switch to it. That is the point: Discord and Spotify should not be billable.
Restart the daemon after every config change, or it keeps using the old one.
This is the step people get wrong. An app's id is usually not its name.
Start the tracker in a terminal and leave it running:
workwork daemonUse your computer normally for a bit, then in another terminal:
workwork appsThe left column is what goes in apps = [...]. A — in the middle column means
that app matches no project and is not being tracked.
Quicker one-off check for whatever is focused right now:
hyprctl activewindow | grep classOn Hyprland. Which snippet you want depends on which config format you use.
Check what you have — ls ~/.config/hypr/ — and use the matching one.
hyprland.lua (the Lua config, newer Hyprland)
Put it with your other startup programs, inside the hyprland.start handler:
hl.on("hyprland.start", function()
hl.exec_cmd("waybar")
hl.exec_cmd("hypridle")
-- Full path: Hyprland's PATH does not include ~/.local/bin.
hl.exec_cmd("$HOME/.local/bin/workwork daemon")
end)If you already have such a block, add only the workwork line to it rather
than writing a second handler.
hyprland.conf (the original format)
exec-once = $HOME/.local/bin/workwork daemonUse the full path in either format. Hyprland's
PATHusually does not include~/.local/bin, so a bareworkwork daemonsilently does nothing — and nothing tells you why.
With systemd (only if your session actually activates
graphical-session.target — uwsm does, a plain Hyprland session usually does
not):
mkdir -p ~/.config/systemd/user
cp contrib/systemd/workwork.service ~/.config/systemd/user/
systemctl --user enable --now workwork.servicework-work coexists with hypridle and other idle daemons. ext-idle-notify-v1
allows any number of listeners, and work-work never inhibits or locks anything.
The short version — paste this into ~/.config/waybar/config.jsonc inside the
outermost { }, and add "custom/workwork" to one of your module lists:
"modules-right": ["custom/workwork", "battery", "network", "clock"],It will work, but it will be unstyled — the colours and the "get back to work" block are all CSS. docs/waybar.md has the stylesheet to paste, an explanation of every state, and how to change which number the clock shows.
| Command | Does |
|---|---|
workwork |
current state, session clock, today's total |
workwork daemon |
run the tracker in the foreground |
workwork waybar |
stream JSON for a Waybar custom module |
workwork today |
today's time by project |
workwork report --days N |
heatmap, project split, average, best day, streak |
workwork report --week |
the last seven days |
workwork apps |
every app id seen, and which project it matches |
workwork project create NAME |
add a project (--apps, --titles, --icon) |
workwork project list |
every project, whether it is tracking, its all-time total |
workwork project remove NAME |
delete a project, keeping its recorded time |
workwork project enable / disable |
park a project without deleting it |
workwork project pick |
the arrow-key picker |
workwork session start [NAME] |
bank the current session and start a fresh clock |
workwork session stop |
close the running session |
workwork session list |
recent sessions and the work in each |
workwork reload |
make a running daemon re-read the config |
workwork pause / resume / toggle |
stop and start the clock by hand |
workwork init |
write a starter config (--force to overwrite) |
workwork status --json emits the raw snapshot, for scripting against.
The README covers installing it and getting a clock on your bar. The rest lives
in docs/:
| Guide | What is in it |
|---|---|
| Waybar | The stylesheet, every CSS state, and choosing what the clock counts — including the totals that never reset |
| Sessions | Banking the current clock and starting a fresh one, naming sessions, and the midnight setting |
| Managing projects | Creating and deleting projects from the command line, and the arrow-key picker |
| Troubleshooting | The clock never starts, the bar is empty, and other things that go wrong |
| Architecture | Where files live on disk, and how the daemon is put together |
- Floating always-on-top overlay window (GTK4 + libadwaita + layer-shell), as a separate binary so the daemon stays GTK-free. Follows the system light/dark and accent colour automatically, themeable with GTK CSS.
- sway, river and Wayfire, via
wlr-foreign-toplevel-management-v1— the seam isfocus::Backend - X11 fallback
-
workwork export --csvfor invoicing - AUR package (a working
PKGBUILDis already incontrib/)
cargo test # no compositor required
cargo clippy
cargo fmtAdding a compositor means adding a variant to focus::Backend and a task that
pushes Focus values into a channel. Everything downstream is shared.
Commits follow Conventional Commits:
feat(waybar): add a progress ring for the daily goal
fix(config): match titles containing commas
docs: explain the app id gotcha
refactor(engine): extract the day rollover
test(report): cover DST transitions
Use ! and a BREAKING CHANGE: footer for anything that changes existing
behaviour or CLI flags.
MIT — see LICENSE. Do what you like with it.







