A minimalist, hackable countdown timer that lives in your macOS status bar. Click a preset and get back to work, and the time stays one glance away. Made for developers and makers who want a Pomodoro-style timer within reach and have no use for bloat.
- ⚡️ One click to start: Pick a Focus or Rest preset in the popover; click it again to pause or resume
- ⏱ The time where you look: A pill in the menu bar counts down, and dims when paused
- 🎚 Presets you can reshape in seconds: Drag a circle up or down to adjust it, double-click or just type to set an exact value, add up to eight per column
- 🕒 Minutes and seconds: Write your own list, e.g.
25, 5m, 30s - 🟣 A ring that shows what's left: The running preset drains clockwise, once per second
- ⌨️ Keyboard-first when you want it:
Tabthrough the circles,Spaceto start,Enteror a digit to edit,↑/↓to nudge,Deleteto remove - 🔔 Native notification & sound: Get told when time is up; the sound is optional
- 🎨 Distraction-free: No Dock icon, no windows, no nags
- 🌗 Follows your Mac: Dark and light, switching with the system
- 💾 Remembers everything: Presets and options persist between launches
Download. Get the latest build from Releases, unzip it and drag CountdownTimerBar.app to Applications. The app is not notarized yet, so macOS blocks the first launch. Open System Settings, go to Privacy & Security and click Open Anyway next to CountdownTimerBar. You can also run xattr -dr com.apple.quarantine /Applications/CountdownTimerBar.app once.
Build it yourself. You need macOS 15 or later and mise:
git clone https://github.com/Bazai/CountdownTimerBar.git
cd CountdownTimerBar
mise install
make build # creates releases/CountdownTimerBar.app
open releases/CountdownTimerBar.appWhen the first timer finishes, macOS asks whether to allow notifications. If you said no, enable them in System Settings, under Notifications.
- Click the pill in your status bar to open the popover.
- Click a preset to start it. Click the running one to pause it and click again to resume. The stop button ends the countdown.
- Drag a circle up or down to adjust it. Double-click it, or focus it and type digits, to enter an exact value.
mandsswitch between minutes and seconds. - Hover a circle to show the × that removes it. The + button adds a new preset.
- Open the gear icon for the options:
- Edit the Focus and Rest lists as text (
10,30s,5m), which stays in sync with the circles. - Turn the sound on or off.
- Open About, or quit with
⌘Q.
- Edit the Focus and Rest lists as text (
- While a preset runs, you cannot edit or remove it, so you cannot lose a countdown by accident.
- It lives in the menu bar and has no Dock icon.
- The whole interface is one popover.
- Presets are plain text that you can write by hand.
- The source is open under the MIT license.
MIT. Do what you want, but don't blame me if you miss a meeting.
PRs are welcome. If you have an idea or find a bug, open an issue or a pull request. The rest of this file is for people and AI agents who work on the code.
mise install # Rust 1.98.1, pinned in mise.toml
cargo run # run from the working tree
make check # fmt check, clippy (-D warnings) and all tests; run before every commitcargo run works without a bundle. Notifications need the bundled app (make build), because macOS requires a bundle identifier for them.
| Command | What it does |
|---|---|
make build |
Builds the release bundle releases/CountdownTimerBar.app (unsigned unless signing variables are set) |
make dist |
Runs make build, then writes releases/CountdownTimerBar-v<version>.zip for the GitHub release |
make license |
Sets the LICENSE years to <first year>-<current year>; the About card shows the same text |
make agents |
Links .agents/skills into .claude/skills and creates CLAUDE.md (@AGENTS.md) if it is missing |
make run |
Builds, stops a running copy and opens the new one |
make check |
Runs fmt-check, lint and test |
make bump |
Bumps Cargo.toml and Cargo.lock to the next patch version and commits (BUMP=minor, BUMP=major or BUMP=1.2.3; DRY_RUN=1 previews) |
make release |
Checks that the Cargo.toml version is ready to release (clean tree, current LICENSE years, no tag yet, make check); it publishes nothing |
make fmt / make lint / make test |
Run the parts of check |
UPDATE_GOLDEN=1 cargo test --test visual |
Rewrites the golden images after an intended visual change |
packaging/build-app.sh signs and notarizes the bundle when APPLE_SIGNING_IDENTITY, APPLE_TEAM_ID and APPLE_NOTARY_KEYCHAIN_PROFILE are all set. The last one names credentials stored with xcrun notarytool store-credentials. Never put Apple ID passwords or API keys in scripts or arguments.
The project is Rust 2021 on GPUI through gpui-kit. The crate is a library plus a one-line binary.
| Path | Responsibility |
|---|---|
src/domain |
Pure model: the countdown state machine over an injectable Clock, durations, presets (PresetList owns slots, ids and the limit of eight) and settings over a KeyValueStore. No UI, no platform code |
src/theme |
Palettes (dark is the reference, light is derived from it), metrics, and their application to gpui-component |
src/gui |
state.rs (AppState behind ports), panel.rs (the popover window), popover/ (views), gesture.rs, inline_edit.rs, timer_circle.rs |
src/macos |
Everything that touches AppKit: status item, notifications, user defaults, appearance observer, activation policy |
src/testing.rs |
Test harness (test-support feature): the real popover over a manual clock, an in-memory store and spies |
tests/ |
ui_flows (interactions), visual and golden/ (screenshots), and tests of the public domain API |
docs/adr |
Architecture decision records |
Know these design rules before you change anything:
AppStatereaches the platform only throughStatusDisplay,Notifier,KeyValueStoreandClock. A new side effect gets a port, not a direct call.- Where it is cheap, illegal states cannot be built:
NonZeroU32durations, oneInteractionenum for press and edit,PresetListfor ids and limits. - Colours and sizes come from
cx.palette()andtheme::metrics. Views never write a hex value.
cargo test runs three layers:
- Unit and integration tests for
domain,themeand gestures, plus doc tests on the public API. tests/ui_flows.rsdrives the real popover on GPUI's headless test platform with real hit testing and key bindings: clicks, double clicks, drags, keys and the settings input. Time comes from aManualClock, so countdowns are exact. Elements are found by id, so give each new interactive element an id and.test_support().tests/visual.rsrenders every state in both themes on GPUI's headless Metal renderer and compares the result withtests/golden/*.png(channel tolerance 2, at most 0.05 % of pixels). It runs withharness = falseon the main thread and is skipped outside macOS. On a mismatch,target/visual/holds the actual image and a diff.
Rules for tests:
- One behaviour per test, named for the behaviour, with no narrating comments.
- A UI change comes with a flow test, or with a changed golden image that you looked at before committing.
- Tests never touch real user defaults or the real menu bar. Use
MemoryStoreand the spies fromtesting. docs/adr/0006-ui-testing-on-headless-gpui.mdhas the reasoning.
The two images at the top of this README (.github/hero-*.png) are composites. They combine a headless render of the running popover with a generated wallpaper and menu bar, and a pill drawn to match. Refresh them when the look changes.
- Everything in the repository is in English: code, comments, commit messages and docs. The maintainer may talk to agents in Russian, see below.
- Write comments only where the code cannot speak for itself: platform or dependency quirks,
SAFETYnotes, public API contracts and ADR references. Do not narrate, do not record history, and do not reference documents that may disappear. - Lints are enforced.
clippy::allis an error,pedanticis a warning, andmake lintpasses-D warnings. Silence a lint locally with#[expect(clippy::x, reason = "...")]and never globally. Do not useunwrap,expectorpanicoutside tests and the test harness. Stderr goes throughdiag::report. Seedocs/adr/0005-lint-policy-and-module-layout.md. - Commits follow the Sentry convention (
feat(scope): Subject, imperative mood, at most 70 characters, a body that explains why). Thecommitskill in.claude/skillswrites them. - A decision that changes behaviour or structure gets an ADR in
docs/adr.
- Read
AGENTS.mdfirst. Talk to the maintainer in Russian and write code, comments, commits and docs in English. Do not spawngpt-6-astra, and never escalate the subagent model after failures. - Skills live in
.agents/skills. Runmake agentsto link them into.claude/skills(generated, not committed) and to createCLAUDE.md, which importsAGENTS.md. Userust-best-practicesfor Rust work,commitfor commits andunslopfor prose. - Run
make checkbefore you say a task is done. For visual work, also look at the changed golden images, not only at the test result. - Do not commit or push unless asked. Keep experiments and throwaway code out of the tree and out of commits.
- Do not add a dependency without a reason that survives review. The project keeps its dependency list short.
| ADR | Decision |
|---|---|
| 0001 | No drag-out removal. Remove with × or Delete |
| 0002 | Enter or a digit opens the inline editor from the keyboard |
| 0003 | The running circle shows a radial progress ring |
| 0004 | A typed theme layer, light derived from dark, follows the system |
| 0005 | Lint policy, lib and bin split, module layout |
| 0006 | UI tests on GPUI's headless platform and renderer |
- Known warning. Builds print a future-incompatibility note about
block v0.1.6. It comes from the dependency chain ofgpui-kit, cannot be fixed here and does not affect the build. - Dependencies. The project consumes GPUI through
gpui-kit0.7. Aftercargo update, runmake check. Golden images can shift with a new GPUI, macOS or font version, so review the diffs and then regenerate them withUPDATE_GOLDEN=1. - Copyright. The notice in
LICENSEis the only place the years are written.make licenseupdates them, the About card reads the same line (countdown_timer_bar::copyright()), andmake releaserefuses a release whose LICENSE years are not current. - CI.
.github/workflows/ci.ymlruns on every push tomainand every pull request, on amacos-15runner. One job runs formatting, clippy, and the unit, interaction and doc tests. A second job runs the golden snapshots, and on a failure it uploads the actual and diff images as thevisual-diffartifact. A second workflow,release.yml, publishes releases (see Releasing). - Versioning.
Cargo.tomlis the only place a version is written. The About card shows it (countdown_timer_bar::VERSION), andbuild-app.shstamps it into the bundle'sInfo.plist. - Releasing. Run
make bump, which movesCargo.tomlandCargo.lockto the next patch version and commits. Runmake releaseto check the commit, then push tomain. When CI passes,.github/workflows/release.ymlreads the version. Ifv<version>has no tag yet, it checks the LICENSE years, runsmake dist, and creates the tag and the GitHub release withCountdownTimerBar-v<version>.zip, its sha256 and generated notes. A version that already has a tag is skipped. The archive is unsigned. Signing needs a Developer ID certificate and notarization secrets that the workflow does not use yet. - Storage. Settings live in the
baz.CountdownTimerBaruser-defaults domain underfocusTimers,restTimersandsoundOn. These are the same keys the earlier Swift version used, so its settings carry over. Do not rename them. - Platform minimum. macOS 15 (
LSMinimumSystemVersion).