Skip to content

oitozero/cadence

Repository files navigation

Cadence

Cadence

A deep-work timer for macOS that actually clears the distractions.

Platform: macOS License: MIT Status: alpha


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

Features

  • 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/hosts mutated to drop the sites in your list to 127.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.

Installation

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.

Requirements

  • macOS 12 (Monterey) or newer
  • Node ≥ 18 (we recommend 20 LTS or 22)
  • Homebrew for the optional privileged helper

Build from source

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.

How it works

The timer state machine

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

Website blocking

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-quit runs ensureUnblocked())
  • 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).

Silent website blocking (optional)

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.sh to /usr/local/bin/cadence-hosts-writer (root-owned, mode 755).
  • Writes /etc/sudoers.d/cadence granting your user NOPASSWD for that exact binary. Validated with visudo -c before 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.

Sound playback

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 setTimeout chains 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 disable webSecurity.
  • YouTube — hidden 1×1 BrowserWindow loads the youtube-nocookie embed with loop=1&playlist=<id>. Three layers of loop guarantee: iframe param, <video>.loop = true injected via JS, plus an ended listener that re-seeks and replays.

Architecture

┌──────────────────────────────────────────────────────────────────┐
│  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)

Project layout

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)

Configuration

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

Building for distribution

npm run build:mac

Outputs dist/Cadence-<version>.dmg and dist/Cadence-<version>-mac.zip. For real distribution you'll need to:

  1. Have an Apple Developer ID certificate installed in your Keychain. Set CSC_LINK to the path of a .p12 export and CSC_KEY_PASSWORD to its passphrase.
  2. For notarization, set APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, and APPLE_TEAM_IDelectron-builder picks them up automatically.

Without signing, macOS Gatekeeper will block first launch. Right-click the app → Open to bypass for personal builds.

Roadmap

Things on the wishlist, roughly ordered:

  • Signed SMAppService privileged 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")

Contributing

Issues and pull requests welcome. Before you open a non-trivial PR:

  1. Open an issue describing the change — it's faster to align on direction than to rewrite a PR.
  2. npm run lint (TypeScript check) should pass.
  3. If you change anything in build/icons/ or build/brand/, run npm run icons and 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.

License

MIT. See LICENSE at the monorepo root.

Credits

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

About

A deep-work timer for macOS that actually clears the distractions.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors