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.
98 lines
4.9 KiB
Markdown
98 lines
4.9 KiB
Markdown
# Caffeinate
|
|
|
|
> Keep your computer awake from [Vicinae](https://vicinae.com). One toggle, no terminal.
|
|
|
|

|
|
|
|
## Why this exists
|
|
|
|
The Raycast [`coffee`](https://www.raycast.com/mooxl/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:
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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
|