Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

work-work

License: MIT Built with Rust Wayland

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.

workwork report --days 90: a heatmap of the last 90 days, time split by project, and average, best day and streak totals


Table of contents


What it looks like

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:

State What it means
Waybar module with a green clock .working A tracked app is focused and you are producing input. The clock is running.
Waybar module as a filled salmon block with black text .idle Tracked app still focused, but you stopped. The clock is stopped and the block pulses until you start again.
Waybar module tinted translucent red .untracked You are on something that belongs to no project. A calm warning, deliberately not animated.
Waybar module dimmed and italic .paused You paused the clock by hand with workwork pause.
Waybar module dimmed showing dashes .off The daemon is not running.

In the terminal.

workwork today: today's time split across four projects with a total

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.


How it decides you are working

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 input group
  • 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.


Setup — step by step

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.

1. Install

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/workwork

Check it worked:

~/.local/bin/workwork --version

Make sure ~/.local/bin is on your PATH so you can just type workwork. If workwork --version says "command not found", add this to ~/.zshrc or ~/.bashrc and open a new terminal:

export PATH="$HOME/.local/bin:$PATH"

2. Create your config

workwork init

That 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.

3. Find out what your apps are called

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 daemon

Use your computer normally for a bit, then in another terminal:

workwork apps

workwork apps: a list of application ids, the project each one matches, and how many times it was focused

The 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 class

4. Start it automatically

On 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 daemon

Use the full path in either format. Hyprland's PATH usually does not include ~/.local/bin, so a bare workwork daemon silently 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.service

work-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.

5. Add it to Waybar

The short version — paste this into ~/.config/waybar/config.jsonc inside the outermost { }, and add "custom/workwork" to one of your module lists:

  "custom/workwork": {
    "exec": "$HOME/.local/bin/workwork waybar",
    "return-type": "json",
    "format": "{}",
    "tooltip": true,
    "on-click": "$HOME/.local/bin/workwork toggle",
    "on-click-right": "ghostty --title=workwork -e $HOME/.local/bin/workwork project pick",
    "restart-interval": 5
  }
  "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.


Commands

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.


Guides

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

Roadmap

  • 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 is focus::Backend
  • X11 fallback
  • workwork export --csv for invoicing
  • AUR package (a working PKGBUILD is already in contrib/)

Contributing

cargo test          # no compositor required
cargo clippy
cargo fmt

Adding 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.


License

MIT — see LICENSE. Do what you like with it.

About

Time tracking that only counts when you're actually working.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages