A deep-work timer for macOS that actually clears the distractions.
Cadence is a Pomodoro-style focus timer that lives in your macOS menu bar. When a focus session starts it blocks distracting websites (by writing to /etc/hosts) and quits the apps you've configured. When the break starts, sites unblock and apps relaunch. Sessions are visualized as calendar-event "time blocks" with explicit start / end clock times — you always know when you'll be free.
It's designed for one person, one machine, with no cloud — your data lives in a local SQLite database under ~/Library/Application Support/Cadence/.
- Menu-bar native — no Dock icon, no main window. Click the tray glyph for a Loom-style popover with the timer, stats, and settings inline.
- Time Block UI instead of the cliché ring. Each session renders as a calendar event, so you see both time remaining and clock time the session ends.
- Auto-blocking: focus starts →
/etc/hostsmutated to drop the sites in your list to127.0.0.1, plus configured apps are quit. Break starts → reverse, with optional minimized re-launch. - Color-coded state in the tray and across the UI — green while focusing, blue on break.
- Background soundscapes during focus — 16 procedural sounds with ongoing variation: binaural beats (Alpha/Beta/Gamma), three noise colors, thunderstorm, rain, ocean, mountain stream, forest, wind, night crickets, crackling fire, coffee shop, wind chimes. Random thunder rolls, wandering bird chirps, drifting filter sweeps — nothing ever loops the same way twice. Plus a file picker (lo-fi / jazz / your own MP3s) and YouTube link support.
- Optional picture-in-picture floating window with six anchor positions, draggable, always on top.
- Stats view: today / week / streak / all-time focused minutes, with a 30-day bar chart and per-day breakdown.
- State-aware notifications with the brand mark colored by session kind.
Heads up: this is alpha-quality software that hasn't been notarized or distributed through the App Store yet. You'll need to build from source.
- macOS 12 (Monterey) or newer
- Node ≥ 18 (we recommend 20 LTS or 22)
- Homebrew for the optional privileged helper
git clone https://github.com/<your-user>/cadence.git
cd cadence
npm install
npm run icons # generate icon.icns + tray PNGs from the SVG sources
npm run dev # or `npm run build:mac` for a packaged .dmg in dist/The first time a focus session starts, macOS will prompt for your password — that's the AppleScript admin shim writing /etc/hosts. The same prompt fires when the session ends (to remove the block). If you want to skip those prompts permanently, see Silent website blocking below.
electron/services/timer.ts owns the single source of truth. The renderer (React) subscribes via IPC events for ticks and state changes; the tray title pulls from the same event stream. State transitions are:
idle ──start(focus)──▶ running(focus) ──complete──▶ running(break) ──complete──▶ idle/running(focus)
▲ │
│ └──pause──▶ paused ──resume──▲
└──────────────────stop──────────────┘
When a focus session naturally completes, the break always auto-starts (this is non-negotiable — a focus session that doesn't transition into a break isn't doing its job). Whether the next focus also auto-starts when the break ends is a user setting ("Auto-start next focus" in Settings → General).
The hosts mutation is delimited by sentinels so it's idempotent and reversible:
# >>> CADENCE_BLOCK_START >>>
127.0.0.1 twitter.com
127.0.0.1 www.twitter.com
::1 twitter.com
::1 www.twitter.com
# <<< CADENCE_BLOCK_END <<<
Whatever was in /etc/hosts before the focus session is preserved untouched. The block is removed when:
- The focus session ends or is stopped
- The app is quit (
before-quitrunsensureUnblocked()) - The user clicks Uninstall on the privileged helper
A small Python HTTP server bound to 127.0.0.1:80 serves a clean "this site is blocked, you're focusing" page for any blocked-host request — so the user sees an intentional landing rather than a generic browser "this site can't be reached" error. HTTPS sites still show a connection error because serving HTTPS would require trusted self-signed certs per domain (a much bigger project).
osascript ... with administrator privileges doesn't cache credentials across subprocess invocations, so by default each completed focus session triggers two password prompts (one on start, one on end). To skip them, Settings → General → Silent website blocking → Install:
- Copies
hosts-writer.shto/usr/local/bin/cadence-hosts-writer(root-owned, mode 755). - Writes
/etc/sudoers.d/cadencegranting your userNOPASSWDfor that exact binary. Validated withvisudo -cbefore activation. - Future focus sessions call
sudo -n /usr/local/bin/cadence-hosts-writer ...directly — no prompt.
This is a persistent privilege escalation — narrowly scoped to one root-owned script that only edits its own sentinel block in /etc/hosts and sanitizes hostnames — but real. Opt in deliberately. Click Uninstall in the same panel to remove the binary and sudoers rule cleanly.
Three backends, all wired through a single AudioController (src/lib/audio.ts):
- Procedural — Web Audio API. Binaural beats are two detuned sine oscillators routed into stereo channels. Noise is filled with Paul Kellet's economy pink/brown filters. Rain is band-passed pink noise; ocean is brown noise low-passed at 600 Hz with a 0.1 Hz LFO. Fire / forest / cafe layer continuous noise sources with stochastic short-lived event schedulers for crackles, bird chirps, and cup clinks. Recursive
setTimeoutchains drive the events; cleanup walks the timers on stop. - File-based — user-picked MP3 / WAV / FLAC / M4A streamed through a custom
cadence-media://Electron protocol so we don't have to disablewebSecurity. - YouTube — hidden 1×1
BrowserWindowloads the youtube-nocookie embed withloop=1&playlist=<id>. Three layers of loop guarantee: iframe param,<video>.loop = trueinjected via JS, plus anendedlistener that re-seeks and replays.
┌──────────────────────────────────────────────────────────────────┐
│ MAIN PROCESS (Node.js) │
│ │
│ TimerService ──tick──┐ │
│ │ │ │
│ ▼ │ │
│ BlocklistService ──▶ /etc/hosts (sudo helper or AppleScript) │
│ AppController ──▶ osascript (quit/launch apps) │
│ NotificationSvc ──▶ macOS Notifications + audio + brand icon │
│ YouTubePlayer ──▶ hidden BrowserWindow │
│ SQLite (better-sqlite3) │
│ │ │
│ TrayService ◀─────────┤ state-colored "C" + countdown title │
│ WindowMgr ◀──────────┘ popover + floating PiP │
│ │
│ ▲ │
│ │ IPC (contextBridge, no nodeIntegration) │
│ ▼ │
│ RENDERER (React 18 + Tailwind, Linear-inspired) │
│ ├── Popover window — Timer / Stats / Settings │
│ └── Floating PiP — slim landscape pill │
└──────────────────────────────────────────────────────────────────┘
| Layer | Stack |
|---|---|
| Shell | Electron 32 + electron-vite |
| UI | React 18, TypeScript, Tailwind, Zustand |
| Persistence | better-sqlite3 (WAL) |
| System control | osascript (AppleScript) + optional sudoers'd hosts-writer.sh |
| Audio | Web Audio API for procedural sounds; custom cadence-media:// protocol for files |
| Packaging | electron-builder (dmg + zip targets) |
cadence/
├── electron/ # main process
│ ├── main.ts # app lifecycle + event wiring
│ ├── ipc.ts # IPC handler registration
│ ├── preload.ts # contextBridge
│ ├── windows.ts # popover + floating window mgmt
│ ├── tray.ts # menu-bar icon + title + click handler
│ ├── services/ # timer, blocklist, apps, audio, db, settings
│ └── helpers/ # hosts-writer.sh, install.sh, uninstall.sh
├── shared/types.ts # types + IPC channel constants
├── src/ # React renderer
│ ├── App.tsx, main.tsx, floating.tsx
│ ├── routes/ # Timer, Stats, Settings
│ ├── components/ # TimeBlock, UpNext, MusicControl, PopoverHeader, MoreMenu, …
│ └── lib/ # ipc, store, audio, cycle, format
├── build/
│ ├── icons/ # source SVGs + the build pipeline
│ ├── brand/ # canonical logos (light/focus/break) + generated PNGs
│ ├── entitlements.mac.plist
│ └── icon.icns # generated
└── resources/tray/ # template tray icons (generated)
Everything lives in Settings (open the popover, click ⋯ → Settings):
- General — pre-warning style, auto-start next focus, tray countdown, floating PiP toggle, position picker, and the privileged-helper install
- Focus — focus length, pre-warning seconds, block-sites / close-apps toggles, background sound + volume
- Break — short and long break lengths, long-break cadence, reopen-apps toggle (with "open minimized")
- Sites — the website blocklist (auto-seeded with common social / news domains, fully editable)
- Apps — the app blocklist with a "currently running" suggestion strip
npm run build:macOutputs dist/Cadence-<version>.dmg and dist/Cadence-<version>-mac.zip. For real distribution you'll need to:
- Have an Apple Developer ID certificate installed in your Keychain. Set
CSC_LINKto the path of a.p12export andCSC_KEY_PASSWORDto its passphrase. - For notarization, set
APPLE_ID,APPLE_APP_SPECIFIC_PASSWORD, andAPPLE_TEAM_ID—electron-builderpicks them up automatically.
Without signing, macOS Gatekeeper will block first launch. Right-click the app → Open to bypass for personal builds.
Things on the wishlist, roughly ordered:
- Signed
SMAppServiceprivileged helper (no sudoers entry needed) - Browser-extension companion for HTTPS / DoH bypass
- macOS Focus Mode integration (drive macOS focus filters when starting a Cadence session)
- iCloud / Supabase sync across devices
- Apple Watch glance
- Calendar integration ("block this meeting in your timer view")
Issues and pull requests welcome. Before you open a non-trivial PR:
- Open an issue describing the change — it's faster to align on direction than to rewrite a PR.
npm run lint(TypeScript check) should pass.- If you change anything in
build/icons/orbuild/brand/, runnpm run iconsand commit the regenerated assets.
Bug reports: include macOS version, Node version (node --version), and steps to reproduce. Logs are in ~/Library/Logs/Cadence/ and stdout from npm run dev.
MIT. See LICENSE at the monorepo root.
- Lot — typeface used for the brand mark, by Svetoslav Simov / Fontfabric (2009). Free for personal and commercial use.
- Electron, Vite, React, Tailwind — the platform.
- better-sqlite3 — synchronous, embedded persistence.
- Zustand — renderer state.
- @resvg/resvg-js — pure-WASM SVG rasterizer powering the icon build pipeline.
- opentype.js — extracted the Lot "C" outline into the canonical mark.
Inspired by the visual aesthetic of Linear, the menu-bar pattern of Loom, and the calendar-event metaphor every focus app should have been using all along.
