`caffeinate -i` blocks system idle sleep but lets the display blank, so the screen still slept and the extension looked broken. The "Also Prevent Display Sleep" preference that fixes this defaulted to off. Flip it to on, and guard it with a manifest test that reads package.json and asserts the shipped defaults map to `-i -d`. Verified on macOS: `caffeinate -i` yields PreventUserIdleDisplaySleep 0, `caffeinate -i -d` yields 1.
98 lines
5.1 KiB
Markdown
98 lines
5.1 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]` (defaults to `-i -d`) |
|
|
| 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 |
|
|
| --- | --- | --- |
|
|
| Prevent Display Sleep | **on** | macOS only. On by default: `caffeinate -i` alone blocks *system* idle sleep, so the screen still blanks and the extension looks broken. Turn it off to save battery. `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
|