A focus timer you drive from a terminal.
Type timer 50m break 10m loop 3, press enter, and get back to work. It is a Pomodoro timer, but the interface is a command line rather than a settings screen, the timer state lives in Rust rather than in the browser, and the window behind it drifts with the season — snow in winter, blossom in spring, falling leaves in autumn — over an ambient loop.
No account, no telemetry, nothing leaves the machine.










Every timer app puts duration behind a settings panel: open it, find the field, clear it, type, close it. That is four interactions to change one number, and you do it constantly, because 25/5 is a default rather than a fit — some work wants fifty minutes and some wants fifteen.
A command line is one interaction. s starts, p pauses, r stops; those three and timer cover most days, with aliases for the handful used constantly and a history that survives a restart.
$ Zetta Focus — Console
Tab completes · ↑↓ history
Zetta Focus Console v1.0.0
Type "help" for available commands.
Tip: Use Tab for autocomplete, ↑↓ for history.
The real grammar, parsed here. Try break 10m timer 50m loop 3 — the order does not matter. The engine itself is Rust, and lives in the app.
Durations, profiles, sound, strict mode and the engine’s own state are all reachable by typing. There is no settings screen you have to go and find.
The timer is a state machine in Rust and the interface renders what it reports, keeping no clock of its own — so there is nothing to drift out of step.
The statistics panel keeps a row per day and draws the last seven as bars, scaled against your own best day rather than a target the app invented. Ninety days are kept.
Start a session that cannot be paused or stopped. Force-closing the app marks it failed.
Four presets plus as many of your own as you want — duration, season, motion, sound and volume, per profile.
Three synthesised alarm tones, generated in the app so nothing is downloaded and they sound the same everywhere, over four seamless ambient loops embedded in the binary.
Every one of these is recorded in the repository itself. They are here because a choice explained is worth more than a feature listed.
AppState holds the timer, the active profile, sound and strict mode. Every command — typed into the terminal or clicked in the UI — goes through execute_command and comes back as a state event the interface renders. The React side owns no timer state of its own, which is what removes the entire class of bugs where two clocks disagree. The tray icon is drawn from that same state.
Every ambient track loops forever. MP3 carries encoder delay and padding that decode as silence at both ends, so each pass through the loop clicks audibly. Vorbis is gapless.
The source recordings ran up to ten and a half minutes at 256 kbps. Since they loop, everything past the loop point is inaudible — and those four files were 42 MB of a 44 MB bundle. Cutting them to the loop point is most of the difference between a 44 MB download and a 5 MB one.
sysinfo’s System::new_all() snapshots every process, disk and network interface on the machine. Building one every five seconds to read two numbers is both wasteful and wrong: CPU usage is a delta between refreshes, so a freshly built System always reports zero.
Each particle is the profile’s own emoji in a div, moved by a CSS transform. A glyph is rasterised into a shared cache once and reused by every particle, where an SVG would be a path subtree each and a blurred gradient a texture each — so the cheapest thing to draw is also the one that ties the panel to the active profile. No video, no sprite sheets, no WebGL.
Four PNGs would be four more files to keep in step with the app mark, and a progress arc would need a file per position. It is rasterised from the bundled icon at 32×32, quantised to twenty-four arc positions, and handed to the OS only when it would actually change — so a fifty-minute session repaints the tray two dozen times rather than three thousand.
The repository opens at a single commit. It was roughly eighty commits as a private project, and that history carried 42 MB of the original uncompressed audio — which every clone would have paid for forever to fetch files the app no longer uses — alongside planning documents for a paid version that no longer exists. Squashing it was the right call; leaving it unexplained would not have been.
src-tauri/src/
engine.rs EngineState: app state, sound, system probe
state.rs AppState, preference load and save
commands/ timer.rs (engine + command processing), profile.rs, parser.rs
sound.rs Vorbis playback via rodio
storage.rs preferences.json
tray.rs the tray icon, drawn from the same state
src/
components/ UI, including the ambience in ambient-panel/
hooks/app/ the bridge to Rust — one state subscription, no local clockThe bridge is one state subscription. Nothing on the React side keeps a clock.
The three duration commands compose into one line in any order, which is the whole argument for typing at it. A duration on its own is a one-off; add loop 3 and you are asking for a cycle instead. Durations accept 25m, 90s, or a bare number read as minutes.
Tab completes, ↑ and ↓ walk the history, and the history is kept next to your preferences on disk rather than in the window — so it survives a restart. Typing help opens the full list inside the app, over the console rather than instead of it.
The two marked global work from any application; the rest need the window focused. The tray menu carries start, stop, pause, resume, settings and quit.
One file, on your machine.
It holds your profiles, theme, volume, statistics, preferences and the console’s command history. There is no account, no sync, no analytics and no network call — the app does not open a socket. Delete it to reset completely.
Needs Rust, Node 20+ and Bun. On Linux you also need the Tauri system dependencies — webkit2gtk, and libasound2-dev for audio.
bun install
bun run tauri build # installers land in src-tauri/target/release/bundle
bun run tauri dev # or run it in developmentEach platform builds only its own artifacts; the release workflow in the repository does all of them at once.
v1.0.010 August 2026
5 downloads of this release. Checksums and release notes are on the releases page.
The macOS builds are unsigned, so Gatekeeper will call the app damaged on first run. It is not — the repository has the one command that clears it, and the reason the Intel build is missing.