Build Session
YESreplaces $3.33/mosaves $39.96/yrback to the verdict
A local focus timer whose hard problems are measuring elapsed time correctly when the machine sleeps, never counting an abandoned session as completed, and being honest about what app blocking a normal application can and cannot do.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Builds the interface and runs the tests.
Get it nodejs.org, or nvm install 22. open ↗
Verify
node --version prints v22 or higher - installfree
Why Tauri 2 needs it, and the tray, the global shortcut and the natively scheduled notification all live there.
Get it rustup.rs, then the platform build dependencies Tauri lists. open ↗
Verify
cargo --version prints a version and a dev build shows a tray icon - have readyfree
Why The suspend behaviour is the central correctness problem here and it cannot be tested in a unit test, only by closing a lid and opening it again.
Get it Any laptop, or a desktop you can suspend and wake on demand.
Verify
A session started before a suspend produces a suspends row after waking - have readyfree
Why What blocking can honestly mean differs completely by operating system, and writing this feature without reading that first produces either a placebo or a request for a permission you do not need.
Get it Read the operating system vendor's own developer documentation on the relevant mechanism and permission.
Verify
You can state in one sentence what your platform allows and what it does not - decidefree
Why Whether a session slept through counts as focus is a judgement that changes every number in the history, and it must be made before the history exists.
Get it Write one paragraph covering suspend, forward clock change and backward clock change.
Verify
The paragraph is in the README and each case is named in a test
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- sessions(id, intent, planned_seconds, started_at_utc, ended_at_utc, outcome, tag) · outcome is completed, stopped, abandoned or interrupted - intervals(session_id, ordinal, kind, started_at_utc, ended_at_utc, monotonic_start_ms, monotonic_end_ms) · kind is focus, pause or suspend - suspends(session_id, detected_at_utc, wall_gap_s, monotonic_gap_s, resolution) · when the two clocks disagree, this row records by how much - breaks(session_id, ordinal, planned_seconds, started_at_utc, ended_at_utc, skipped) - blocklist(id, pattern, kind, enabled) · kind names the mechanism, because what is possible differs per platform - days(date, focus_seconds, completed_sessions, abandoned_sessions) · derived, rebuildable from intervals - There is no remaining-seconds column anywhere. Remaining is computed from the stored instants on every tick, which is why a crash costs nothing.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATA_DIR | required | ~/.local/share/focus | The SQLite file with sessions, intervals and suspends. |
DEFAULT_SESSION_MINUTES | required | 25 | The planned duration for a new session. Stored per session, so changing it does not rewrite history. |
DEFAULT_BREAK_MINUTES | required | 5 | Breaks are recorded like sessions and can be skipped. |
SUSPEND_THRESHOLD_SECONDS | required | 60 | A clock divergence larger than this is treated as a suspend and resolved by the documented rule. |
GLOBAL_SHORTCUT | required | CmdOrCtrl+Shift+F | Starts and stops without the window. Conflicts are the commonest setup failure. |
BLOCKING_MODE | required | nudge | Whatever your platform genuinely supports. The interface label is generated from this value. |
The build, in order
Time that survives a closed lid
A session's end is derived from recorded instants, and a suspend is detected and recorded rather than silently absorbed.
The countdown is a rendering, not a state.
Files
src-tauri/src/session.rssrc/timer/remaining.tsTheir disagreement is the only way to see a suspend.
Files
src-tauri/src/clocks.rstests/suspend-detection.test.tsDoes a slept twenty minutes count as focus? Decide once, in the README.
Files
docs/time-policy.md
done when · tick each as it passeswatch out- Counting down with a monotonic timer and assuming it behaves the same everywhere is wrong in a way you will only see on someone else's machine: Rust's own standard library documentation says it is not specified whether system suspends count as elapsed time, that the behaviour varies across platforms and Rust versions, and that Instant uses CLOCK_UPTIME_RAW on Darwin, CLOCK_MONOTONIC on other Unix systems and QueryPerformanceCounter on Windows (checked against the Rust documentation). Record both clocks, detect the divergence, and decide explicitly what a twenty minute sleep does to a twenty five minute session.
Start, pause, stop, from the keyboard
The session state machine is append-only and every transition has a key.
The total is a query; the intervals are the truth.
Files
src/timer/intervals.tsIncluding the ones you think nobody uses.
Files
src/shortcuts.tstests/keyboard-complete.test.tsSo the timer does not need a window to be useful.
Files
src-tauri/src/tray.rs
done when · tick each as it passeswatch out- Implementing pause by subtracting from a duration field loses the history and double-counts after a crash, because the app has no way to know whether the subtraction already happened. Append a pause interval and a resume interval, and derive focus time by summing focus intervals only.
A chime that arrives on time
The end of a session is announced when it happens, whatever the window was doing.
The web view tick is decoration.
Files
src-tauri/src/notify.rsTwo moments where a stale display becomes a wrong decision.
Files
src/timer/sync.tsSleeping through the end is the normal case, not the edge case.
Files
src/timer/sync.tstests/late-resume.test.ts
done when · tick each as it passeswatch out- Driving the timer from a timer callback in the web view means the operating system throttles it when the window is not in front, so the chime for a twenty five minute session arrives several minutes late and only when you happen to click back, which is precisely the moment the timer was supposed to save you from. Schedule against the stored end instant natively and treat the interface tick as display only.
History that does not flatter you
Statistics count what happened, and an abandoned session is not a completed one.
Four outcomes, and only one of them is completed.
Files
src/sessions/outcome.tstests/abandoned.test.tsAnd assert the rebuild matches, so the derived table cannot drift.
Files
src/stats/rebuild.tsThe number that is actually useful is the honest one.
Files
src/views/history.tsx
done when · tick each as it passeswatch out- Counting any session that reached its end instant as completed rewards walking away from the machine, and within a fortnight the statistics say you focused for six hours on a day you did not, which makes the entire history worthless for the one decision it exists to support. Mark a session abandoned when the application was not running at its end, and show abandoned time separately.
Blocking, described honestly, and everything exportableproduct builder
Whatever blocking the platform actually allows is what is offered, named accurately, and the data leaves in an open format.
Read the platform's own guidance before writing a line of this.
Files
src-tauri/src/blocking.rsdocs/blocking-per-platform.mdNudge or block, in the label, not in a footnote.
Files
src/views/blocking.tsxTotals are opinions; intervals are the data.
Files
src/export.ts
done when · tick each as it passeswatch out- Shipping a feature called blocking that edits a system file or closes a window, when any determined user reopens it in seconds and a permission prompt was needed to do even that, trades a real privilege for a placebo and teaches the user their tool lies to them. Find out what your platform's documented mechanism actually is, implement that, and name the feature after what it does.
That is the whole plan for Session. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Cross-device sync.
- Per-application activity tracking.
- Calendar integration.
- Enforcement-grade blocking via privileged helpers.
- cross-device sync
- deep operating-system edge-case coverage
- polished shortcuts and accessibility
- vendor-maintained updates
- Tagging sessions by project, which is one column and one filter on data you already store
- A weekly review view built entirely from intervals, with no new storage
- Optional import of an existing timer's history, mapped onto outcomes and intervals
Need the files? The project pack on the verdict page hands your agent the whole brief · more productivity utilities.