Contributing
The fastest path from git clone to a passing PR: install the prerequisites below, run npm run tauri dev to confirm the app builds, then read Architecture internals for the module map and the 1Hz run-loop walkthrough. When you're ready to make a change, the patterns lower on this page — adding a Tauri command, adding a setting, adding a suppression guard — are the seams the codebase is built around.
Layout
src-tauri/— the Rust backend (Tauri 2 + Tokio).src/— the React 19 + TypeScript renderer.scripts/— small dev/build helpers (the a11y audit lives here).
The docs site you're reading is under docs/ and ships as a VitePress build deployed to GitHub Pages.
Prerequisites
- Rust stable (whatever
rust-toolchainresolves to — currently no pinned version, just stable). - Node LTS (20.x or newer).
- macOS / Linux / Windows all build. Linux needs the Tauri system deps (
libwebkit2gtk-4.1-dev,libappindicator3-dev,librsvg2-dev,patchelf,libxss-dev).
Running the dev app
npm install # first time
npm run tauri dev # starts Vite + cargo, opens the appThe Tauri dev server hot-reloads both the React UI and (via cargo's watcher) the Rust code. Quitting the app stops the dev server.
Tests
# Rust (244 tests at the time of writing)
cargo test --manifest-path src-tauri/Cargo.toml --lib
# Frontend unit tests (vitest, 101 tests)
npm test
# Accessibility audit — full Vite build + Puppeteer + axe-core,
# every tab × light & dark scheme (14 audits total)
npm run audit:a11yLints and formatting
CI enforces all four. Run them locally before pushing:
cargo fmt --manifest-path src-tauri/Cargo.toml --check
cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings
RUSTDOCFLAGS='-D warnings' cargo doc --manifest-path src-tauri/Cargo.toml --no-deps
npx tsc --noEmitThe cargo doc step rejects broken intra-doc links — a renamed module that's still referenced from a [link] somewhere will fail the build, even when nothing else has drifted.
Branch / PR workflow
- Branch off
mainfor every change. PRs land via Squash & merge. - Feature branches use prefixes:
feat/...,fix/...,refactor/...,docs/...,chore/.... - Test plans in PR descriptions are not required — list verification steps in the body instead (what you ran, what you changed, what's new).
- CI runs on PR open and on every push to a PR branch: frontend job (tsc + vitest + a11y) and a Rust matrix (macOS / Ubuntu / Windows; fmt + clippy + test + doc).
Adding a Tauri command
Every IPC entry-point is a #[tauri::command] somewhere under src-tauri/src/scheduler/commands/. The pattern:
- Add the function to the right submodule (
settings.rs,breaks.rs,profiles.rs, etc.). - Add a
///doc comment — at minimum: what it does, what events it emits, what errors it returns. This is what surfaces in the generated Rust API reference, and CI fails the build if intra-doc links break. - Register it in
lib.rsinside thetauri::generate_handler![...]macro. Tauri's capability layer authorises commands per-name; a function tagged#[tauri::command]that's not in the macro is silently unreachable from the renderer. - If the renderer needs to call it, add a wrapper in the relevant
views/settings/hooks/use-*.tsso the call stays out of components. The hooks layer is where shape drift gets caught — theinvokeboundary itself is untyped (see #13).
See the IPC contract for the existing surface.
Adding a setting
- Add the field to the Rust
Settingsstruct insrc-tauri/src/scheduler/settings.rs, with a sensible default in theDefaultimpl. The default is what older installs get when theirsettings.jsonlacks the field — the#[serde(default)]and#[serde(alias = ...)]pattern in the same file is how settings migrate forward without a schema version bump. - Add the matching field to the TS
SchedulerSettingstype insrc/views/settings/types.ts. The two types are mirrored by hand — no parity test yet (#13) — so skipping this step turns into a renderer runtime crash, not a compile error. - Render a control on the relevant tab under
src/views/settings/tabs/. - If the scheduler should react to the field at runtime, wire it into
run_loop.rsand add a test. Settings that only affect rendering (overlay theme, hints, etc.) don't need this step — the run-loop only reads the fields it cares about each tick.
Adding a break suppression / guard
The 1Hz loop in src-tauri/src/scheduler/run_loop.rs consults each guard in order. To add one:
- Add the detection module (e.g.
dnd.rs,camera.rs,video.rs) or extend an existing one. Per-OS branches live inside the module; the public surface is a single boolean check the run-loop can call cheaply on every tick. - Add the
GuardReasonvariant insrc-tauri/src/stats.rs. This is what shows up in the Insights tab's "Breaks suppressed by" breakdown — without a variant, the suppression is invisible to the user even though the break didn't fire. - Hook the check into
run_loopbefore the fire-decision; if active, reset the per-kind timers, log vialog_suppressions, andcontinue. Order matters — earlier guards in the chain win when several would trigger on the same tick, so place the new guard wherever the precedence ought to fall. - Add a setting that gates it (see "Adding a setting" above) and surface it on the Quiet tab.
Filing issues
Use the GitHub issue tracker. Include the diagnostics report from Settings → About → Copy diagnostics report — it's a redacted snapshot of your settings, session stats, and the last 50 KB of logs.