diff --git a/README.md b/README.md index 69c4af6..99b2d73 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Caffeinate -> Keep your computer awake from [Vicinae](https://vicinae.com). Start, stop, and time-limit sleep prevention without touching a terminal. +> Keep your computer awake from [Vicinae](https://vicinae.com). One toggle, no terminal. ![Caffeinate icon](./assets/caffeinate.png) @@ -14,9 +14,8 @@ This is a native `@vicinae/api` extension, so none of that applies. | 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. | +| **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 command holding it. Start/stop from here. | ## How it works @@ -24,16 +23,18 @@ Sleep prevention is an OS-level assertion, so the extension delegates to the pla | Platform | Mechanism | | --- | --- | -| macOS | `/usr/bin/caffeinate -i [-d] [-s] [-t ]` | -| Linux | `systemd-inhibit --what=idle:sleep --why= sleep ` | +| macOS | `/usr/bin/caffeinate -i [-d] [-s]` | +| Linux | `systemd-inhibit --what=idle:sleep --why= sleep 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 ` self-terminate when the window elapses. +Both run until signalled — there is no timer, so nothing needs to be refreshed or babysat. -State is recorded in `LocalStorage` as `{ pid, startedAt, expiresAt, command, args, processName, … }`. +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. + +State is recorded in `LocalStorage` as `{ pid, startedAt, 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. +Status is self-healing: if the recorded process is gone (killed elsewhere, or a reboot), the stale state is cleared on the next read. ## Preferences @@ -42,12 +43,6 @@ Status is self-healing: if the recorded process is gone (timed out, or killed el | 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 @@ -63,11 +58,16 @@ 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` support would be the path. -- **While-an-app-is-running / scheduled windows.** `caffeinate -w ` covers the former on macOS; neither is wired up. -- **A persistent menu bar indicator** — Vicinae has no menu bar support. Use **Caffeination Status** instead. +- **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 `), and **scheduled** windows. Deliberately omitted — on/off covers the common case. ## License diff --git a/package.json b/package.json index cc09fe1..08955a8 100644 --- a/package.json +++ b/package.json @@ -21,12 +21,6 @@ "title": "Caffeination Status", "description": "See whether sleep is currently being prevented, and for how much longer. Start or stop it from here.", "mode": "view" - }, - { - "name": "caffeinate-for", - "title": "Caffeinate for a Duration", - "description": "Keep your computer awake for a set amount of time, then let it sleep again automatically.", - "mode": "view" } ], "preferences": [ @@ -74,4 +68,4 @@ "test": "vitest run", "test:watch": "vitest" } -} +} \ No newline at end of file diff --git a/src/__tests__/duration.test.ts b/src/__tests__/duration.test.ts deleted file mode 100644 index a780315..0000000 --- a/src/__tests__/duration.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, it } from "vitest"; - -import { formatDuration, formatRemaining, normalizeDuration, parseDuration } from "../lib/duration"; - -describe("parseDuration", () => { - it("treats a bare number as minutes", () => { - expect(parseDuration("30")).toBe(1800); - expect(parseDuration("1")).toBe(60); - expect(parseDuration("1.5")).toBe(90); - }); - - it("parses single units", () => { - expect(parseDuration("90s")).toBe(90); - expect(parseDuration("45m")).toBe(2700); - expect(parseDuration("2h")).toBe(7200); - expect(parseDuration("1d")).toBe(86400); - }); - - it("parses combined units", () => { - expect(parseDuration("1h30m")).toBe(5400); - expect(parseDuration("1d2h")).toBe(93600); - expect(parseDuration("1h30m10s")).toBe(5410); - }); - - it("accepts friendly words and spacing", () => { - expect(parseDuration("1 hour")).toBe(3600); - expect(parseDuration("30 minutes")).toBe(1800); - expect(parseDuration("2 hrs")).toBe(7200); - expect(parseDuration(" 45 m ")).toBe(2700); - expect(parseDuration("90 seconds")).toBe(90); - }); - - it("is case insensitive", () => { - expect(parseDuration("2H")).toBe(7200); - expect(parseDuration("30M")).toBe(1800); - }); - - it("rejects invalid or non-positive input", () => { - expect(parseDuration("")).toBeNull(); - expect(parseDuration(" ")).toBeNull(); - expect(parseDuration("abc")).toBeNull(); - expect(parseDuration("0")).toBeNull(); - expect(parseDuration("0m")).toBeNull(); - expect(parseDuration("-5")).toBeNull(); - expect(parseDuration("h")).toBeNull(); - expect(parseDuration("m30")).toBeNull(); - }); - - it("rejects partially consumed input rather than misreading it", () => { - expect(parseDuration("1h30")).toBeNull(); - expect(parseDuration("30m garbage")).toBeNull(); - expect(parseDuration("1h 30x")).toBeNull(); - }); -}); - -describe("normalizeDuration", () => { - it("expands words and strips whitespace", () => { - expect(normalizeDuration("1 Hour 30 Minutes")).toBe("1h30m"); - expect(normalizeDuration(" 45 mins ")).toBe("45m"); - expect(normalizeDuration("1 day")).toBe("1d"); - }); -}); - -describe("formatDuration", () => { - it("formats the largest two units", () => { - expect(formatDuration(45)).toBe("45s"); - expect(formatDuration(90)).toBe("1m 30s"); - expect(formatDuration(1800)).toBe("30m"); - expect(formatDuration(3600)).toBe("1h"); - expect(formatDuration(5400)).toBe("1h 30m"); - expect(formatDuration(93600)).toBe("1d 2h"); - }); - - it("handles zero and negatives", () => { - expect(formatDuration(0)).toBe("0s"); - expect(formatDuration(-10)).toBe("0s"); - }); -}); - -describe("formatRemaining", () => { - it("rounds down to whole units and never goes negative", () => { - const now = 1_000_000; - expect(formatRemaining(now + 90_000, now)).toBe("1m 30s"); - expect(formatRemaining(now + 3_600_000, now)).toBe("1h"); - expect(formatRemaining(now - 5_000, now)).toBe("0s"); - }); -}); diff --git a/src/__tests__/platform.test.ts b/src/__tests__/platform.test.ts index 892c408..b262906 100644 --- a/src/__tests__/platform.test.ts +++ b/src/__tests__/platform.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "vitest"; -import { MACOS_CAFFEINATE_PATH, UnsupportedPlatformError, buildCommand, describeOptions, detectPlatform } from "../lib/platform"; +import { + MACOS_CAFFEINATE_PATH, + UnsupportedPlatformError, + buildCommand, + detectPlatform, +} from "../lib/platform"; const base = { preventDisplaySleep: false, preventSystemSleep: false, reason: "test" }; @@ -26,6 +31,8 @@ describe("buildCommand on macOS", () => { }); it("adds display and system assertions when requested", () => { + expect(buildCommand("darwin", { ...base, preventDisplaySleep: true }).args).toEqual(["-i", "-d"]); + expect(buildCommand("darwin", { ...base, preventSystemSleep: true }).args).toEqual(["-i", "-s"]); expect(buildCommand("darwin", { ...base, preventDisplaySleep: true, preventSystemSleep: true }).args).toEqual([ "-i", "-d", @@ -33,23 +40,14 @@ describe("buildCommand on macOS", () => { ]); }); - it("adds a self-expiring timeout for durations", () => { - expect(buildCommand("darwin", { ...base, durationSeconds: 1800 }).args).toEqual(["-i", "-t", "1800"]); - }); - - it("floors fractional durations", () => { - expect(buildCommand("darwin", { ...base, durationSeconds: 90.7 }).args).toEqual(["-i", "-t", "90"]); - }); - - it("omits the timeout for indefinite or non-positive durations", () => { - expect(buildCommand("darwin", { ...base, durationSeconds: 0 }).args).toEqual(["-i"]); - expect(buildCommand("darwin", { ...base, durationSeconds: -60 }).args).toEqual(["-i"]); + it("never adds a timeout, so the assertion lasts until stopped", () => { expect(buildCommand("darwin", base).args).not.toContain("-t"); + expect(buildCommand("darwin", { ...base, preventDisplaySleep: true }).args).not.toContain("-t"); }); }); describe("buildCommand on Linux", () => { - it("uses systemd-inhibit with an infinite sleep by default", () => { + it("uses systemd-inhibit with an infinite sleep", () => { expect(buildCommand("linux", base)).toEqual({ command: "systemd-inhibit", args: ["--what=idle:sleep", "--why=test", "sleep", "infinity"], @@ -57,30 +55,12 @@ describe("buildCommand on Linux", () => { }); }); - it("hands the duration to sleep so it self-terminates", () => { - expect(buildCommand("linux", { ...base, durationSeconds: 600 }).args).toEqual([ - "--what=idle:sleep", - "--why=test", - "sleep", - "600", - ]); - }); - it("does not claim to control the display, which systemd-inhibit cannot do", () => { const { args } = buildCommand("linux", { ...base, preventDisplaySleep: true }); expect(args).toEqual(["--what=idle:sleep", "--why=test", "sleep", "infinity"]); }); -}); -describe("describeOptions", () => { - it("only mentions the display when it can actually be held", () => { - const macLines = describeOptions("darwin", { ...base, preventDisplaySleep: true }); - const linuxLines = describeOptions("linux", { ...base, preventDisplaySleep: true }); - expect(macLines.join(" ")).toMatch(/Display stays on/); - expect(linuxLines.join(" ")).toMatch(/cannot be held/); - }); - - it("always describes the baseline behaviour", () => { - expect(describeOptions("darwin", base)[0]).toMatch(/idle sleep blocked/); + it("passes the reason through so it shows up in systemd-inhibit --list", () => { + expect(buildCommand("linux", { ...base, reason: "Plex" }).args).toContain("--why=Plex"); }); }); diff --git a/src/caffeinate-for.tsx b/src/caffeinate-for.tsx deleted file mode 100644 index 238ec4b..0000000 --- a/src/caffeinate-for.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Action, ActionPanel, Form, Icon, Toast, popToRoot, showHUD, showToast } from "@vicinae/api"; -import { useState } from "react"; - -import { optionsFromPreferences, startCaffeination } from "./lib/caffeinate"; -import { formatDuration, parseDuration } from "./lib/duration"; -import { describeOptions, detectPlatform } from "./lib/platform"; - -export default function CaffeinateFor() { - const [duration, setDuration] = useState("30"); - - const parsed = parseDuration(duration); - const resolved = parsed === null ? "Enter a duration, e.g. 30, 45m, 2h or 1h30m" : `Staying awake for ${formatDuration(parsed)}`; - - let behaviour: string[]; - try { - behaviour = describeOptions(detectPlatform(), optionsFromPreferences(parsed ?? undefined)); - } catch (error) { - behaviour = [error instanceof Error ? error.message : String(error)]; - } - - async function handleSubmit(values: Form.Values) { - const seconds = parseDuration(String(values.duration ?? "")); - if (seconds === null) { - await showToast({ - style: Toast.Style.Failure, - title: "Not a valid duration", - message: "Try a number of minutes, or values like 45m, 2h or 1h30m.", - }); - return; - } - - try { - await startCaffeination(optionsFromPreferences(seconds, `Caffeinate for ${formatDuration(seconds)}`)); - await showHUD(`☕ Staying awake for ${formatDuration(seconds)}`); - await popToRoot(); - } catch (error) { - await showToast({ - style: Toast.Style.Failure, - title: "Could not start caffeinating", - message: error instanceof Error ? error.message : String(error), - }); - } - } - - return ( -
- - - } - > - - - - {behaviour.map((line) => ( - - ))} - - - ); -} diff --git a/src/lib/caffeinate.ts b/src/lib/caffeinate.ts index 2107007..fd0283f 100644 --- a/src/lib/caffeinate.ts +++ b/src/lib/caffeinate.ts @@ -18,12 +18,11 @@ export interface CaffeinatePreferences { preventSystemSleep?: boolean; } -export function optionsFromPreferences(durationSeconds?: number, reason: string = DEFAULT_REASON): CaffeinateOptions { +export function optionsFromPreferences(reason: string = DEFAULT_REASON): CaffeinateOptions { const prefs = getPreferenceValues(); return { preventDisplaySleep: Boolean(prefs.preventDisplaySleep), preventSystemSleep: Boolean(prefs.preventSystemSleep), - durationSeconds, reason, }; } @@ -115,8 +114,6 @@ export async function startCaffeination(options: CaffeinateOptions): Promise 0 ? Date.now() + options.durationSeconds * 1000 : null, - durationSeconds: options.durationSeconds && options.durationSeconds > 0 ? Math.floor(options.durationSeconds) : null, command, args: spec.args, processName: spec.processName, @@ -156,7 +153,7 @@ export async function getStatus(): Promise { if (!state) return { active: false, state: null, stale: false }; if (!(await isProcessRunning(state))) { - // The process timed out or was killed outside the extension. Self-heal. + // The process was killed outside the extension, or the machine rebooted. Self-heal. await clearState(); return { active: false, state, stale: true }; } diff --git a/src/lib/duration.ts b/src/lib/duration.ts deleted file mode 100644 index 8444c9e..0000000 --- a/src/lib/duration.ts +++ /dev/null @@ -1,77 +0,0 @@ -const UNIT_SECONDS: Record = { s: 1, m: 60, h: 3600, d: 86400 }; - -export const DURATION_PRESETS: { title: string; seconds: number }[] = [ - { title: "15 minutes", seconds: 15 * 60 }, - { title: "30 minutes", seconds: 30 * 60 }, - { title: "45 minutes", seconds: 45 * 60 }, - { title: "1 hour", seconds: 3600 }, - { title: "2 hours", seconds: 2 * 3600 }, - { title: "4 hours", seconds: 4 * 3600 }, -]; - -/** Lowercase, expand friendly words, and drop all whitespace. */ -export function normalizeDuration(input: string): string { - return input - .trim() - .toLowerCase() - .replace(/\b(?:minutes?|mins?)\b/g, "m") - .replace(/\b(?:hours?|hrs?)\b/g, "h") - .replace(/\b(?:seconds?|secs?)\b/g, "s") - .replace(/\b(?:days?)\b/g, "d") - .replace(/\s+/g, ""); -} - -/** - * Parse a human duration into seconds. - * - * Accepts a bare number (interpreted as minutes), or unit-suffixed values such - * as `45m`, `2h`, `90s`, `1d`, and combinations like `1h30m`. The whole string - * must be consumed, so `1h30` is rejected rather than silently misread. - * - * @returns seconds, or null when the input is not a valid positive duration. - */ -export function parseDuration(input: string): number | null { - const text = normalizeDuration(input); - if (!text) return null; - - if (/^\d+(?:\.\d+)?$/.test(text)) { - const minutes = Number(text); - return minutes > 0 ? Math.round(minutes * 60) : null; - } - - const part = /(\d+(?:\.\d+)?)([dhms])/g; - let total = 0; - let consumed = ""; - let match: RegExpExecArray | null; - - while ((match = part.exec(text)) !== null) { - total += Number(match[1]) * UNIT_SECONDS[match[2]]; - consumed += match[0]; - } - - if (!consumed || consumed !== text) return null; - return total > 0 ? Math.round(total) : null; -} - -/** "1h 30m", "45m", "30s" — largest two units only. */ -export function formatDuration(seconds: number): string { - if (seconds <= 0) return "0s"; - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); - - const parts: string[] = []; - if (days) parts.push(`${days}d`); - if (hours) parts.push(`${hours}h`); - if (minutes) parts.push(`${minutes}m`); - if (!days && !hours && secs) parts.push(`${secs}s`); - if (!parts.length) parts.push(`${secs}s`); - - return parts.slice(0, 2).join(" "); -} - -export function formatRemaining(expiresAt: number, now: number): string { - const remaining = Math.max(0, Math.round((expiresAt - now) / 1000)); - return formatDuration(remaining); -} diff --git a/src/lib/platform.ts b/src/lib/platform.ts index c64153c..67d5faf 100644 --- a/src/lib/platform.ts +++ b/src/lib/platform.ts @@ -19,46 +19,20 @@ export function detectPlatform(platform: string = process.platform): SupportedPl /** * Build the command that holds the sleep assertion. * - * The returned process is expected to run for the whole caffeination window: - * `caffeinate -t N` (macOS) and `sleep N` (Linux) both self-terminate once the - * duration elapses, so nothing has to babysit them. + * Both variants run until they are signalled, which is what "on" means here: + * there is no duration, so nothing has to be timed or refreshed. */ export function buildCommand(platform: SupportedPlatform, options: CaffeinateOptions): CaffeinateCommand { - const duration = - options.durationSeconds !== undefined && options.durationSeconds > 0 - ? Math.floor(options.durationSeconds) - : null; - if (platform === "darwin") { - // -i prevents idle sleep. Without -t the assertion is held until the process dies. + // -i prevents idle sleep; the assertion is held for as long as this process lives. const args = ["-i"]; if (options.preventDisplaySleep) args.push("-d"); if (options.preventSystemSleep) args.push("-s"); - if (duration !== null) args.push("-t", String(duration)); return { command: MACOS_CAFFEINATE_PATH, args, processName: "caffeinate" }; } // systemd-inhibit has no equivalent for keeping the display on, so // preventDisplaySleep is intentionally not mapped here. - const args = ["--what=idle:sleep", `--why=${options.reason}`, "sleep", duration !== null ? String(duration) : "infinity"]; + const args = ["--what=idle:sleep", `--why=${options.reason}`, "sleep", "infinity"]; return { command: "systemd-inhibit", args, processName: "systemd-inhibit" }; } - -export function describeOptions(platform: SupportedPlatform, options: CaffeinateOptions): string[] { - const lines: string[] = ["System stays awake (idle sleep blocked)."]; - if (options.preventDisplaySleep) { - lines.push( - platform === "darwin" - ? "Display stays on." - : "Display sleep is controlled by your compositor and cannot be held by systemd-inhibit.", - ); - } - if (options.preventSystemSleep) { - lines.push( - platform === "darwin" - ? "Full system sleep is blocked (macOS only honours this on AC power)." - : "Full system sleep is blocked.", - ); - } - return lines; -} diff --git a/src/lib/types.ts b/src/lib/types.ts index a991038..f4895f0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,8 +3,6 @@ export type SupportedPlatform = "darwin" | "linux"; export interface CaffeinateOptions { preventDisplaySleep: boolean; preventSystemSleep: boolean; - /** Omitted or <= 0 means "until stopped". */ - durationSeconds?: number; reason: string; } @@ -21,8 +19,6 @@ export interface CaffeinateCommand { export interface CaffeinateState { pid: number; startedAt: number; - expiresAt: number | null; - durationSeconds: number | null; command: string; args: string[]; processName: string; @@ -33,8 +29,8 @@ export interface CaffeinateState { export interface CaffeinationStatus { active: boolean; state: CaffeinateState | null; - /** State was recorded but the process is gone (timed out, or killed elsewhere). */ + /** State was recorded but the process is gone (killed elsewhere, or a reboot). */ stale: boolean; } -export type ToggleResult = "started" | "stopped" | "restarted"; +export type ToggleResult = "started" | "stopped"; diff --git a/src/status.tsx b/src/status.tsx index 71499cc..fc85ea8 100644 --- a/src/status.tsx +++ b/src/status.tsx @@ -2,22 +2,16 @@ import { Action, ActionPanel, Color, Icon, List, Toast, openExtensionPreferences import { useCallback, useEffect, useState } from "react"; import { getStatus, optionsFromPreferences, startCaffeination, stopCaffeination } from "./lib/caffeinate"; -import { DURATION_PRESETS, formatDuration, formatRemaining } from "./lib/duration"; import { CaffeinationStatus } from "./lib/types"; -/** Re-render cadence so the "time remaining" readout stays roughly honest. */ -const TICK_MS = 30_000; - export default function CaffeinationStatusView() { const [status, setStatus] = useState(null); const [isLoading, setIsLoading] = useState(true); - const [now, setNow] = useState(() => Date.now()); const refresh = useCallback(async () => { setIsLoading(true); try { setStatus(await getStatus()); - setNow(Date.now()); } catch (error) { await showToast({ style: Toast.Style.Failure, @@ -33,17 +27,11 @@ export default function CaffeinationStatusView() { void refresh(); }, [refresh]); - useEffect(() => { - if (!status?.active) return; - const timer = setInterval(() => setNow(Date.now()), TICK_MS); - return () => clearInterval(timer); - }, [status?.active]); - const run = useCallback( - async (action: () => Promise, successTitle: (message: string) => string) => { + async (action: () => Promise, successTitle: string) => { try { - const message = await action(); - await showToast({ style: Toast.Style.Success, title: successTitle(message) }); + await action(); + await showToast({ style: Toast.Style.Success, title: successTitle }); await refresh(); } catch (error) { await showToast({ @@ -56,74 +44,32 @@ export default function CaffeinationStatusView() { [refresh], ); - const startFor = (seconds: number) => - run( - async () => { - await startCaffeination(optionsFromPreferences(seconds, `Caffeinate for ${formatDuration(seconds)}`)); - return formatDuration(seconds); - }, - (label) => `Staying awake for ${label}`, - ); - - const startIndefinitely = () => - run( - async () => { - await startCaffeination(optionsFromPreferences(undefined, "Caffeinate until stopped")); - return ""; - }, - () => "Staying awake until stopped", - ); - - const stop = () => - run( - async () => { - await stopCaffeination(); - return ""; - }, - () => "Sleep is allowed again", - ); + const start = () => run(() => startCaffeination(optionsFromPreferences()), "Staying awake until stopped"); + const stop = () => run(() => stopCaffeination(), "Sleep is allowed again"); const state = status?.state ?? null; const active = status?.active ?? false; - const title = active ? "Keeping this computer awake" : "Sleep is not being prevented"; - - const subtitle = (() => { - if (!active || !state) return undefined; - if (state.expiresAt === null) return "Until stopped"; - return `${formatRemaining(state.expiresAt, now)} left`; - })(); - - const accessories = active - ? [{ tag: { value: state?.expiresAt === null ? "indefinite" : "timed", color: state?.expiresAt === null ? Color.Green : Color.Orange } }] - : []; - return ( } actions={ {active ? ( ) : ( - + )} - - {DURATION_PRESETS.map((preset) => ( - startFor(preset.seconds)} - /> - ))} - @@ -138,22 +84,19 @@ export default function CaffeinationStatusView() { function buildDetail(active: boolean, state: CaffeinationStatus["state"]): string { if (!active || !state) { - return ["# Sleep is not being prevented", "", "Run **Toggle Caffeination**, or start a window from the actions panel."].join( + return ["# Sleep is not being prevented", "", "Use the action panel to start keeping this computer awake."].join( "\n", ); } - const started = new Date(state.startedAt).toLocaleString(); - return [ "# Keeping awake", "", - `Holding a sleep assertion via \`${state.processName}\`.`, + `Holding a sleep assertion via \`${state.processName}\`. It stays in place until stopped.`, "", "| | |", "|---|---|", - `| Started | ${started} |`, - `| Ends | ${state.expiresAt === null ? "When stopped" : new Date(state.expiresAt).toLocaleString()} |`, + `| Started | ${new Date(state.startedAt).toLocaleString()} |`, `| PID | ${state.pid} |`, `| Platform | ${state.platform} |`, "",