Omapomodoro
A tomato-timer chip on the Omarchy bar. Click it and a KeyboardPanel peeks out — the same chrome as Network, Display, and Omascreentime — with the live cycle, today's sessions, the week, and streaks.
The overlay does not own the clock. A small Rust tracker,
omapomodoro-track, runs the phase machine, writes history, and
publishes a capped snapshot the bar chip and panel watch. Sleep cannot
complete a focus. A nap consumes the break. Lock pauses focus and leaves
the break running.
Plugin id: postman.omapomodoro · License: MIT · Kinds:
service + bar-widget
Sessions are unlabelled. There is no cloud, no accounts, and no website blocker. It does not track which window you focused — that is Omascreentime.
What you get
A chip on the bar. Idle, it shows today's completed count. Running,
paused, or armed, it shows remaining MM:SS. Click to open the panel.
Middle-click starts, pauses, or resumes without opening anything.
Four tabs:
- Time — phase ring, start / pause / skip / reset, and a Cycle card for focus / short / long / every. Click the card header to show or hide the steppers. Duration changes apply to the next phase.
- Day — today's sessions: completed, abandoned, and the live one.
- Week — last 7 local days, weekends called out, daily goal hit or miss.
- Streak — daily goal, current / best streak,
keepDaysheatmap.




Install
omarchy plugin add clones the plugin. It does not compile the
tracker. You need mise (or any Rust toolchain)
once, after clone.
omarchy plugin add https://github.com/kennetpostigo/omapomodoro.git --enable
cd ~/.config/omarchy/plugins/postman.omapomodoro
mise install
./scripts/build.sh
omarchy-shell shell rescanPlugins
If the chip did not land on the right of the bar:
omarchy plugin enable postman.omapomodoro --section right
A clone without ./scripts/build.sh shows Tracker not built in the
overlay. Run the build, then omarchy-shell shell rescanPlugins.
Optional Super-menu entry (opt-in; writes only trigger.omapomodoro
into your menu extension):
./scripts/install-menu.sh
Use
Click the chip, or:
omarchy-shell shell toggle postman.omapomodoro
Keys are the real PanelKeyCatcher signals — Space is not a text
key. While a cycle stepper is focused, Space is blocked so it cannot
start or pause.
| Key | Action |
|---|---|
| Space / Enter | Start, pause, or resume. On Streak, cycle the daily pomodoro goal 4 → 6 → 8 → 12 → 0 |
x |
Reset to idle |
s |
Skip the current or armed phase |
1 / 2 / 3 / 4 |
Time / Day / Week / Streak |
arrows / h j k l |
Day: move in the session list. Other tabs: scroll. On a focused stepper: change the value |
r |
Refresh the view |
g / G |
Scroll to top / bottom |
| Esc | Close. On a focused stepper: leave the stepper |
| Tab | Switch to the next bar panel |
| Right-click the chip | Icon-only mode |
| Middle-click the chip | Start / pause / resume |
On Time, click Cycle to show or hide the duration steppers.
Collapsed, the card shows 25 · 5 · 15 × 4. Open, it edits focus /
short / long / every. Changes apply to the next phase. You can
also set them from the command line:
omarchy bar set postman.omapomodoro focusMin 50
omarchy bar set postman.omapomodoro shortBreakMin 10
omarchy bar set postman.omapomodoro longBreakMin 20
omarchy bar set postman.omapomodoro longBreakEvery 3
| Key | Default | Meaning |
|---|---|---|
focusMin |
25 |
Focus length in minutes |
shortBreakMin |
5 |
Short break in minutes |
longBreakMin |
15 |
Long break in minutes |
longBreakEvery |
4 |
Long break every N completed focuses |
autoStartNext |
false |
Auto-start the next armed phase |
dailyGoalPomos |
8 |
Daily completed-focus goal (0 = off). On Streak, Space cycles 4 → 6 → 8 → 12 → 0 |
dailyGoalMin |
0 |
Daily focus-minutes goal (0 = off) |
keepDays |
31 |
History window (heatmap length) |
iconOnly |
false |
Hide the duration / count on the bar |
showCycle |
true |
Show the Cycle steppers on Time. Click the card header, or: omarchy bar set postman.omapomodoro showCycle false |
notifications |
true |
Announce phase end (OSD + desktop notification) |
sound |
true |
Play a sound on phase end |
omarchy bar move postman.omapomodoro --section right
Architecture
This section is for anyone who wants to change, port, or harden
Omapomodoro. Coding agents should also read AGENTS.md.
The on-the-wire contract is protocol.md.
Why two processes
omarchy-shell is one long-lived Quickshell process. A QML Timer as
source of truth would stall the desktop and lie across sleep, lock,
midnight, and shell restart.
Omapomodoro therefore splits in two:
service/Service.qml keep the tracker daemon alive
bar/BarWidget.qml KeyboardPanel peek + bar chip
│
▼
overlay/Overlay.qml tabs, FileView, one-shot Process children
│
│ Unix socket + atomic snapshot.json
▼
target/release/omapomodoro-track
daemon | ensure | status | view | proto | stop
start | pause | resume | skip | reset | config | goal
flowchart TB
subgraph shell["omarchy-shell"]
SVC["service/Service.qml<br/>ensure every 60s"]
BAR["bar/BarWidget.qml<br/>chip + KeyboardPanel"]
OVL["overlay/Overlay.qml<br/>FileView + one-shot Process"]
BAR --> OVL
end
SNAP["snapshot.json"]
SOCK["Unix socket"]
D["omapomodoro-track daemon"]
SVC -->|"ensure"| D
BAR -->|"watch"| SNAP
OVL -->|"watch + view"| SNAP
OVL -->|"start / pause / skip / reset / config / goal"| SOCK
SOCK --> D
D -->|"atomic write"| SNAP
The daemon samples three clocks (REALTIME, BOOTTIME, MONOTONIC),
runs the phase machine, and atomically writes snapshot.json. The bar
chip and the overlay watch that file. Opening the panel also asks
view once for a fresh read. There is no 1-second process poll. The
overlay interpolates remaining from the last snapshot — it never calls
Date.now().
Clock rules
| Does | Does not |
|---|---|
| Focus remaining is monotonic elapsed | Sleep / lid-close completing a focus |
| Break remaining is boottime elapsed | A nap leaving a 5-minute break intact |
LockedHint pauses focus |
IdleHint / screensaver-without-lock pausing |
| Break keeps counting while locked | Skip of a focus incrementing cycleIndex |
| Complete = focus remaining reached 0 | Skip, reset, or crash counting as a pomodoro |
| Credit the session to the local day it started | Midnight mutating an open day's totals |
Crash: a quick bounce (no boottime hole) may resume running. Anything
else reloads the envelope paused (focus) or applies the break-consume
rule. stop / SIGINT / SIGTERM flush the open phase — they do
not abandon it.
Config changes apply to the next phase. A running 25:00 focus is not rewritten to 30:00 mid-flight.
Tracker (src/)
| Module | Job |
|---|---|
clock.rs |
REALTIME / BOOTTIME / MONOTONIC, classify suspend vs NTP, local day keys |
timer.rs |
Phase machine, remaining, armed, lock pause, midnight |
session.rs |
loginctl show-session for IdleHint / LockedHint |
store.rs |
Atomic history.json / session.json / snapshot.json / config.json |
stats.rs |
Today sessions, week strip, heatmap, insights |
streaks.rs |
Current/best streak; today-in-play (yesterday can still count) |
goals.rs |
goals.json daily pomos / minutes |
config.rs |
Durations and flags; omitted patch keys do not change |
daemon.rs |
poll loop, control socket, commit, snapshot |
paths.rs |
State dir (0700), flock, unique tmp names |
protocol.rs |
NDJSON constructors |
main.rs |
CLI |
Crash loss is bounded by the periodic commit. A signal flushes the
envelope and does not leave a torn JSON file. Snapshots are
rename-replaced without fsync; history and session are fsync'd.
State:
~/.local/state/omapomodoro/ # or $XDG_STATE_HOME/omapomodoro
history.json # { v, days: { YYYY-MM-DD: { focusCompleted, …, sessions } } }
snapshot.json # last view event (bar + overlay watch this)
session.json # open-phase envelope (survives crash / stop)
config.json # last-seen durations / flags
goals.json # dailyPomos + dailyMin
daemon.lock / daemon.pid
$XDG_RUNTIME_DIR/omapomodoro/omapomodoro.sock
Overlay data flow
- Service starts (and every 60s
ensures) the daemon. - Bar chip
FileViewssnapshot.json. No periodicProcess. - Open →
startSession: reload snapshot,ensure(bar/overlay pass schema flags; omitted keys stay), oneview. - Control actions are one-shot
Processchildren (start/pause/skip/reset/goal). Duration edits gooverlay.applyDuration→BarWidget.setDuration→updateEntryInline+onSettingsChangedensure. Do not writeconfig.jsonfrom QML. - Header (logo, title, tabs) stays outside the Flickable so it stays pinned while the body scrolls.
- Remaining on the ring is last snapshot minus tick count, floored to
remainingMs - 1000. NeverDate.now(). 1234switch tabs. Space isonActivateRequested.xisonDeleteRequested. Arrows /hjklareonMoveRequested. Cycle steppers setpickerOpensoPanelKeyCatcherisblockedwhile they have focus.
ListModel must not use a role named color — Quickshell treats that
as reserved. Use fill.
Protocol
One JSON object per stdout line. v must be 1. Unknown type →
ignore. stderr is human logs only; the overlay must not parse it. The
daemon's first stderr line is a JSON hello.
label is today's completed count, never the streak. Session IdleHint
is sessionIdle — do not ship a field named idle. Omitted config /
goal flags never reset last-seen values. Drop a view with
sessions.length > 80 or heatmap.length > 400.
Full schema: protocol.md.
Limits
- The tracker does not watch windows or
/proc. It does not keylog. - History is unlabelled sessions on this machine — treat the state dir as private.
- Plugins run unsandboxed inside
omarchy-shell. Review the source before enabling anything. omarchy plugin addnever runs install hooks. A clone without./scripts/build.shshows a missing-tracker error.
Repository map
manifest.json plugin id, service + bar-widget schema
service/Service.qml supervise omapomodoro-track
bar/BarWidget.qml chip + KeyboardPanel host
bar/Model.js chip text / tooltip / parse
overlay/Overlay.qml session, tabs, FileView, keyboard
overlay/OverlayModel.js parse, merge, palette, actions
overlay/Hero.qml pinned title + Time/Day/Week/Streak
overlay/TimerPanel.qml ring + start/pause/skip/reset + cycle
overlay/DurationStepper.qml focus / short / long / every row
overlay/RingCanvas.qml phase ring
overlay/TodayPanel.qml today's sessions
overlay/WeekPanel.qml 7-day strip + rows
overlay/StreaksPanel.qml goals + streak + heatmap
overlay/SessionList.qml session rows
overlay/EmptyState.qml first-run / missing binary / error
overlay/StatusBar.qml running / paused / offline
overlay/Format.js MM:SS, durations, words
src/*.rs tracker daemon
tests/ unit + CLI
protocol.md NDJSON contract
scripts/build.sh mise exec cargo build --release
scripts/test.sh cargo test + plugin validate
scripts/dev-install.sh symlink into ~/.config/omarchy/plugins
scripts/install-menu.sh optional trigger.omapomodoro
scripts/publish.sh GitHub repo + marketplace URL
Develop
mise install
./scripts/test.sh
./scripts/dev-install.sh
./scripts/dev-watch.sh
After QML edits on a symlink install, force a reload with
omarchy-shell shell rescanPlugins — inotify does not follow the plugin
symlink.
Tracker:
./target/release/omapomodoro-track proto
./target/release/omapomodoro-track ensure
./target/release/omapomodoro-track view
./target/release/omapomodoro-track status
./target/release/omapomodoro-track start
./target/release/omapomodoro-track pause
./target/release/omapomodoro-track skip
./target/release/omapomodoro-track reset
./target/release/omapomodoro-track stop
./scripts/test.sh is the gate: cargo fmt --check, clippy, unit +
CLI tests, omarchy plugin validate, and a JSON parse of
manifest.json.
Manual (not in CI): close the lid mid-focus and wake paused; lock
mid-focus (pauses) vs mid-break (keeps falling); omarchy restart shell mid-focus (chip still ticking); cross local midnight with a
running focus (complete counts on the start day).
Good first changes: overlay chrome, cycle-card layout, heatmap density, insight copy. Harder: clock classification, midnight credit, snapshot cadence. Do not add window tracking, tasks/tags, or cloud sync unless that is the request.
Remove
omarchy plugin disable postman.omapomodoro
./target/release/omapomodoro-track stop || true
omarchy plugin remove postman.omapomodoro --yes
rm -rf ~/.local/state/omapomodoro
rm -rf "${XDG_RUNTIME_DIR:-/run/user/$UID}/omapomodoro"
Disable does not stop a running daemon — omapomodoro-track stop
does.
If you added the optional menu trigger, delete the
trigger.omapomodoro block from
~/.config/omarchy/extensions/omarchy-menu.jsonc.
A local symlink install (from ./scripts/dev-install.sh) is not a git
checkout:
rm -f ~/.config/omarchy/plugins/postman.omapomodoro
License
MIT. See LICENSE.