Files
caffeinate-extension/README.md
T
adrianbonpin 65005b22a6 Add native Vicinae caffeinate extension
Keeps the computer awake from Vicinae. Written against @vicinae/api
directly because the Raycast `coffee` extension cannot work here:

- Its `index` command is `menu-bar` mode, which Vicinae does not support.
- Its `status` command crashes on `Cannot read properties of null
  (reading 'IsManuallyDecafed')` because it gates on
  `LocalStorage.getItem(key) === undefined`. Vicinae resolves a missing
  key to `null`, not `undefined` (verified with a runtime probe).

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

- macOS:  caffeinate -i [-d] [-s] [-t <seconds>]
- Linux:  systemd-inhibit --what=idle:sleep --why=<reason> sleep <n|infinity>

The process is spawned detached + unref'd, because Vicinae unloads a
no-view command as soon as its function returns. Timed sessions need no
babysitter: `caffeinate -t` and `sleep <n>` self-terminate.

Commands: toggle (no-view), status (view), caffeinate-for (view).

Two deliberate safety choices:
- Stopping signals the recorded PID rather than running `killall
  caffeinate`, so a caffeinate the user started themselves survives.
  Verified against a real concurrent `caffeinate -u -dmi` process.
- The PID's name is re-checked with `ps -o comm=` before signalling, since
  PIDs get recycled and a stale entry could otherwise kill something
  unrelated.

Also: display-sleep prevention is macOS-only, and the UI says so on Linux
rather than pretending systemd-inhibit can hold the display awake.

Verified: vici lint, tsc --noEmit, eslint, 23/23 vitest, vici build, and
live in Vicinae — toggle starts `caffeinate -i` which survives the
command unloading, toggle again stops only ours, both view commands load
with clean stdout/stderr.
2026-09-20 16:16:57 +08:00

75 lines
3.5 KiB
Markdown

# Caffeinate
> Keep your computer awake from [Vicinae](https://vicinae.com). Start, stop, and time-limit sleep prevention without touching a terminal.
![Caffeinate icon](./assets/caffeinate.png)
## 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 | Starts or stops sleep prevention, whichever is appropriate. |
| **Caffeination Status** | view | Live readout of whether sleep is being blocked, for how much longer, and the exact command holding it. Start/stop from the actions panel. |
| **Caffeinate for a Duration** | view | Keeps the computer awake for a set time, then lets it sleep again automatically. |
## 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] [-t <seconds>]` |
| Linux | `systemd-inhibit --what=idle:sleep --why=<reason> sleep <seconds\|infinity>` |
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. Timed sessions need no babysitter — `caffeinate -t` and `sleep <seconds>` self-terminate when the window elapses.
State is recorded in `LocalStorage` as `{ pid, startedAt, expiresAt, command, args, processName, … }`.
Stopping signals the **recorded PID**, not `killall caffeinate`, so a `caffeinate` you started yourself in a terminal is left alone. Before signalling, the PID's process name is re-checked via `ps -o comm=` — PIDs get recycled, and without that check a stale entry could kill an unrelated process.
Status is self-healing: if the recorded process is gone (timed out, or killed elsewhere), the stale state is cleared on the next read.
## 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. |
## Duration formats
A bare number means **minutes**. Otherwise: `45m`, `2h`, `90s`, `1d`, `1h30m`, or friendly words like `1 hour` / `30 minutes`.
The whole string must parse — `1h30` is rejected rather than silently read as `1h`.
## 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
```
## Not implemented
- **Windows.** `detectPlatform` throws with a clear message instead of failing obscurely. `powercfg` support would be the path.
- **While-an-app-is-running / scheduled windows.** `caffeinate -w <pid>` covers the former on macOS; neither is wired up.
- **A persistent menu bar indicator** — Vicinae has no menu bar support. Use **Caffeination Status** instead.
## License
MIT