How the app is built, from the pure timer logic to the device it runs on.
The Car Thing is an 800×480 landscape touchscreen with a rotary encoder (the wheel), a press-in knob, four buttons along the top, a mic button and a back button. Chromium on the device delivers the encoder as horizontal wheel events; one detent is a delta of 120.
| Input | Timer | Picker / History |
|---|---|---|
| Turn wheel | Idle: set the duration. Running: add / remove whole minutes | Move through designs / scroll days |
| Press knob | Start, pause or resume | Apply design / back to timer |
| Hold knob (700 ms) | Stop the active phase, or skip a waiting break | — |
| Buttons 1 / 2 / 3 | Presets 15 / 25 / 45 min | — |
| Button 4 | Toggle history | Toggle history |
| Mic button | Open the design picker | Close |
| Back button | — | Close |
A set is four focus sessions. After each focus session comes a short break; after the fourth, a long break, then the set starts over.
All timer logic is a pure module (timer.ts) — no React, no device
calls. State has a mode (idle / running /
paused) and a phase (focus / short /
long). Times are stored as epoch timestamps, so a throttled or
backgrounded page catches up correctly instead of drifting.
// one detent of the wheel; press / hold handlers
export function press(s, now) {
switch (s.mode) {
case 'idle': return { ...s, mode: 'running', endsAt: now + phaseMs(s), startedAt: now };
case 'running': return { ...s, mode: 'paused', remaining: s.endsAt - now, pausedAt: now };
case 'paused': return { ...s, mode: 'running', endsAt: now + s.remaining,
pausedTotal: s.pausedTotal + (now - s.pausedAt) };
}
}
When a running phase reaches its end, complete() chains focus into its
break from the exact end instant and logs the finished session; a finished break
returns to idle.
The Car Thing has no battery-backed real-time clock. A systemd guard saves the time every few minutes and restores it at boot, so after a power-off the clock resumes behind by however long the device was off — and nothing on the device can tell how long that was. The app judges whether the clock is believable, strongest evidence first:
Differences under two minutes are treated as ordinary drift. The status resolves to
ok, unverified (rebooted since last confirmed), or
behind by a known amount, which designs surface as an unobtrusive warning
rather than silently logging against a wrong time.
Timer logic and presentation are kept apart, so a design never touches the clock.
timer.ts — the pure state machine: press,
turn, hold, complete, and history encoding.model.ts — derives a view model from that state: countdown string,
progress, the upcoming schedule, streak and today's totals, all pre-computed.src/themes/<id>/ — each design renders only that view model,
plus a matching history screen. Adding one is a folder and a single registry entry.App.tsx — wires device input (wheel, knob, buttons) to the state
machine, runs the 250 ms tick, and handles persistence.State is persisted through the bridgething daemon's per-app store, with
localStorage as a fallback for the moment before the socket settles and
for desktop development where there is no daemon. Timer writes are debounced
(~400 ms); history is rewritten only when it changes.
| Key | Holds |
|---|---|
pomodoro.v1 | timer state |
pomodoro.history.v1 | session history, compact array form |
pomodoro.theme.v1 | selected design |
pomodoro.clock-trust.v1 | boot id the clock was last confirmed on |
History is stored in a tight wire form — [startSec, endSec, focusedSec, done]
per session — and capped at 3000 entries (roughly a year at eight sessions a day).
The app talks to the on-device daemon through @bridgething/client over a
local WebSocket (ws://<host>/). Surfaces it uses:
| Surface | Used for |
|---|---|
store | per-app key/value persistence (the four keys above) |
time | the phone's wall clock, to check the device clock |
system | boot diagnostics (the boot id) for clock trust |
capabilities | discovering the device's notification earcons |
audio | playing an earcon when a session completes |
A public/manifest.json declares the app id, name, icon and permissions.
npm install
npm run dev # vite, size the window to 800x480
npm run build # tsc --noEmit, then vite build -> dist/
DEVICE=<adb-serial> npm run deploy
Deploy pushes the built dist/ into
/var/bridgething/webapps/<app-id>/ over adb and restarts
the bridgething service. On desktop, keyboard stands in for the hardware: Enter/Space is
the knob, 1–4 the top buttons, M the design picker, Escape back, and a trackpad's
horizontal scroll is the wheel.