Files
caffeinate-extension/README.md
T
adrianbonpin 5ca37fbc79 Make orphaning structurally impossible: the process is the state
The extension used to record the spawned pid in LocalStorage, which meant
two pieces of state had to stay in sync: the record and the process. When
the record was lost (extension reinstalled, storage cleared, a crash
between spawn and save) the process kept holding a sleep assertion that
nothing could ever release — a machine that silently never sleeps. That
happened for real during development.

Fix it by removing the thing that can be lost rather than guarding it:
the process now carries its own identity, so the OS is the single source
of truth.

Each session is tagged in its own command line:

  vicinae-caffeinate:v1:<startedAt>:<flags>   e.g. ...:1789895271765:id

- macOS: caffeinate has no free-form field, so the marker is installed as
  argv[0] via the shell's `exec -a`. exec replaces the shell in place, so
  the pid Node receives is the caffeinate pid. (Verified that both `ps -o
  command=` and `ps -o comm=` surface the custom argv[0].)
- Linux: the marker rides in systemd-inhibit's --why next to a
  human-readable reason.

status is now a `ps` scan for the marker; stop signals every match. This
deletes lib/state.ts and every LocalStorage call — the extension is
stateless, which also retires the Vicinae null-vs-undefined storage quirk
as a hazard for this codebase.

Also:
- Signal the process group (-pid) rather than a single pid. Sessions are
  spawned detached so the child is a group leader, and on Linux
  systemd-inhibit forks a `sleep` that a parent-only signal can leave
  behind.
- start() clears existing sessions first, so assertions cannot stack.
- If the spawn never registers a marker, roll back instead of leaving a
  half-started session.
- Drop the now-meaningless "stale" concept; add `extra` so an unexpected
  duplicate surfaces in the status view instead of being invisible.
- Add describeFlags() to render flag letters for humans.

Verified by deleting the installed extension while a session was active,
restarting Vicinae, then reinstalling: the fresh install saw the leftover
as ACTIVE and stopped it rather than starting a second and orphaning the
first. A user-owned `caffeinate -u -dmi` was untouched throughout.

vici lint, tsc, eslint, 17/17 vitest, vici build all green; live toggle
on/off round-trips cleanly.
2026-09-20 17:08:19 +08:00

4.9 KiB

Caffeinate

Keep your computer awake from Vicinae. One toggle, no terminal.

Caffeinate icon

Why this exists

The Raycast coffee extension does not work on Vicinae. Its index command is a menu-bar command, which Vicinae cannot run at all, and its status command crashes because it gates on LocalStorage.getItem(key) === undefined — Vicinae resolves a missing key to null instead of undefined.

This is a native @vicinae/api extension, so none of that applies.

Commands

Command Mode What it does
Toggle Caffeination no-view Flips sleep prevention on or off, whichever is appropriate. The fast path.
Caffeination Status view Whether sleep is being blocked, since when, and the exact process holding it. Start/stop from here.

How it works

Sleep prevention is an OS-level assertion, so the extension delegates to the platform tool and then gets out of the way:

Platform Mechanism
macOS /usr/bin/caffeinate -i [-d] [-s]
Linux systemd-inhibit --what=idle:sleep --why=<reason> sleep infinity

Both run until signalled — there is no timer, so nothing needs to be refreshed or babysat.

The process is spawned detached and unref'd. That is required: Vicinae unloads a no-view command as soon as its function returns, so a child process would otherwise be torn down with it.

The process is the state

Nothing is persisted. The running process carries its own identity in its command line:

vicinae-caffeinate:v1:<startedAt>:<flags>        e.g. vicinae-caffeinate:v1:1789895271765:id
  • macOS has no free-form field on caffeinate, so the marker is installed as argv[0] via the shell's exec -a. exec replaces the shell in place, so the pid Node receives is the caffeinate pid.
  • Linux passes it in systemd-inhibit's --why, alongside a human-readable reason.

Status is therefore a ps scan for that marker, and stopping signals every match.

Why this design

The obvious approach — remember the pid in LocalStorage — has a failure mode that is worse than it sounds. Two pieces of state have to stay in sync: the record, and the actual process. If the record is ever lost (extension reinstalled, storage cleared, a crash between spawning and saving) the process keeps holding a sleep assertion that nothing will ever release. A laptop that silently never sleeps, with no way to find the culprit.

Making the process self-describing removes the class of bug rather than guarding against it: there is no record to lose, so the extension cannot lose track of a session. Any status or toggle reconciles against the truth, and start always clears existing sessions first so assertions cannot stack.

Verified by deleting the installed extension while a session was active, restarting Vicinae, then reinstalling: the fresh install saw the leftover as active and stopped it, instead of starting a second one and orphaning the first.

Signals go to the process group (kill -TERM -<pid>). Sessions are spawned detached, so the child is a group leader; on Linux that matters because systemd-inhibit forks a sleep, and signalling only the parent can leave the child behind.

A caffeinate you started yourself is never touched — only processes carrying the marker are ever signalled. If a session ever does need to be cleared by hand:

pkill -f vicinae-caffeinate

Preferences

Preference Default Notes
Also Prevent Display Sleep off macOS only. systemd-inhibit cannot hold the display awake — that is the compositor's job, and the UI says so rather than pretending.
Also Prevent System Sleep off On macOS this assertion only takes effect while on AC power.

Development

npm install
npm run dev        # vici develop — Vicinae must be running

Other scripts: npm run lint (manifest), npm run lint:code (ESLint), npm run typecheck, npm test.

To install a production build into Vicinae:

npm run build      # writes to ~/.local/share/vicinae/extensions/caffeinate

No top bar indicator

Vicinae has no menu-bar command support. This is not a Raycast-compat gap — a native mode: "menu-bar" command is accepted by the manifest and builds cleanly, then is silently dropped at registration, with no warning. @vicinae/api exports a MenuBarExtra component, but there is no host to render it into.

For a real top bar indicator you need a standalone macOS menu-bar app; it cannot be part of a Vicinae extension.

Not implemented

  • Windows. detectPlatform throws with a clear message instead of failing obscurely. powercfg would be the path.
  • Timed windows (caffeinate -t), while-an-app-is-running (caffeinate -w <pid>), and scheduled windows. Deliberately omitted — on/off covers the common case.

License

MIT