Reduce to a pure on/off extension
Durations were the only thing keeping a lot of machinery alive, so drop them and let the dead code go with them. Removed: - The "Caffeinate for a Duration" command. - lib/duration.ts entirely (parseDuration / formatDuration / formatRemaining / normalizeDuration / DURATION_PRESETS) plus its test file. Every export was reachable only from duration paths. - The 30s setInterval re-render in the status view. It existed only to keep a "27m left" countdown honest, and with no timed sessions there is nothing to count down. - The duration-preset actions in the status view. - describeOptions(), which only the duration form used to explain itself. - durationSeconds / expiresAt from the options, state and command builder, along with the `-t <seconds>` and `sleep <n>` branches. Both platforms now hold the assertion until signalled, which is what "on" means here. - The unused "restarted" member of ToggleResult. The status view now reports "since <time>" instead of a countdown, and keeps a Refresh action. Also fixes a type error the narrower helper surfaced: the status view's run() helper was typed () => Promise<void> while startCaffeination returns Promise<CaffeinateState> and stopCaffeination returns Promise<boolean>. 407 deletions against 57 insertions. Verified: vici lint, tsc --noEmit, eslint, 8/8 vitest, vici build, and live in Vicinae — toggle on spawns `caffeinate -i`, toggle off stops it, and a pre-existing user-owned `caffeinate -u -dmi` is left untouched throughout.
This commit is contained in:
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<Form
|
||||
navigationTitle="Caffeinate for a Duration"
|
||||
actions={
|
||||
<ActionPanel>
|
||||
<Action.SubmitForm title="Start Caffeinating" icon={Icon.MugSteam} onSubmit={handleSubmit} />
|
||||
</ActionPanel>
|
||||
}
|
||||
>
|
||||
<Form.TextField
|
||||
id="duration"
|
||||
title="Duration"
|
||||
placeholder="30"
|
||||
defaultValue="30"
|
||||
info="A bare number means minutes. Also accepts 45m, 2h, 1h30m, 90s."
|
||||
onChange={setDuration}
|
||||
/>
|
||||
<Form.Description title="Will run" text={resolved} />
|
||||
<Form.Separator />
|
||||
{behaviour.map((line) => (
|
||||
<Form.Description key={line} text={line} />
|
||||
))}
|
||||
<Form.Description text="Tip: open the extension preferences to also hold off display or system sleep." />
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -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<CaffeinatePreferences>();
|
||||
return {
|
||||
preventDisplaySleep: Boolean(prefs.preventDisplaySleep),
|
||||
preventSystemSleep: Boolean(prefs.preventSystemSleep),
|
||||
durationSeconds,
|
||||
reason,
|
||||
};
|
||||
}
|
||||
@@ -115,8 +114,6 @@ export async function startCaffeination(options: CaffeinateOptions): Promise<Caf
|
||||
const state: CaffeinateState = {
|
||||
pid: child.pid,
|
||||
startedAt: Date.now(),
|
||||
expiresAt: options.durationSeconds && options.durationSeconds > 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<CaffeinationStatus> {
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
const UNIT_SECONDS: Record<string, number> = { 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);
|
||||
}
|
||||
+4
-30
@@ -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;
|
||||
}
|
||||
|
||||
+2
-6
@@ -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";
|
||||
|
||||
+17
-74
@@ -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<CaffeinationStatus | null>(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<string>, successTitle: (message: string) => string) => {
|
||||
async (action: () => Promise<unknown>, 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 (
|
||||
<List isLoading={isLoading} navigationTitle="Caffeination">
|
||||
<List.Section title={active ? "Active" : "Inactive"}>
|
||||
<List.Item
|
||||
icon={active ? { source: Icon.MugSteam, tintColor: Color.Orange } : { source: Icon.Mug, tintColor: Color.SecondaryText }}
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
accessories={accessories}
|
||||
icon={
|
||||
active
|
||||
? { source: Icon.MugSteam, tintColor: Color.Green }
|
||||
: { source: Icon.Mug, tintColor: Color.SecondaryText }
|
||||
}
|
||||
title={active ? "Keeping this computer awake" : "Sleep is not being prevented"}
|
||||
subtitle={active && state ? `Since ${new Date(state.startedAt).toLocaleTimeString()}` : undefined}
|
||||
accessories={active ? [{ tag: { value: "on", color: Color.Green } }] : []}
|
||||
detail={<List.Item.Detail markdown={buildDetail(active, state)} />}
|
||||
actions={
|
||||
<ActionPanel>
|
||||
{active ? (
|
||||
<Action title="Stop Caffeinating" icon={Icon.StopFilled} onAction={stop} />
|
||||
) : (
|
||||
<Action title="Keep Awake Until Stopped" icon={Icon.MugSteam} onAction={startIndefinitely} />
|
||||
<Action title="Keep Awake" icon={Icon.MugSteam} onAction={start} />
|
||||
)}
|
||||
<ActionPanel.Section title="Keep awake for…">
|
||||
{DURATION_PRESETS.map((preset) => (
|
||||
<Action
|
||||
key={preset.title}
|
||||
title={preset.title}
|
||||
icon={Icon.Clock}
|
||||
onAction={() => startFor(preset.seconds)}
|
||||
/>
|
||||
))}
|
||||
</ActionPanel.Section>
|
||||
<ActionPanel.Section>
|
||||
<Action title="Refresh" icon={Icon.ArrowClockwise} onAction={refresh} />
|
||||
<Action title="Open Extension Preferences" icon={Icon.Cog} onAction={openExtensionPreferences} />
|
||||
@@ -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} |`,
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user