@rizal72/oc-coffee 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +20 -5
- package/tui.js +232 -0
- package/tui.tsx +0 -215
package/package.json
CHANGED
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rizal72/oc-coffee",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Keeps the Mac awake while the agent works: cup badge in the footer, /coffee command",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/rizal72/opencode-plugins.git"
|
|
9
|
+
},
|
|
6
10
|
"license": "MIT",
|
|
7
|
-
"files": [
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
"files": [
|
|
12
|
+
"*.ts",
|
|
13
|
+
"tui.js",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"opencode",
|
|
18
|
+
"opencode-plugin",
|
|
19
|
+
"macos",
|
|
20
|
+
"caffeinate"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
10
25
|
"exports": {
|
|
11
26
|
".": "./index.ts",
|
|
12
|
-
"./tui": "./tui.
|
|
27
|
+
"./tui": "./tui.js"
|
|
13
28
|
}
|
|
14
29
|
}
|
package/tui.js
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { createComponent as _$createComponent } from "@opentui/solid";
|
|
2
|
+
import { effect as _$effect } from "@opentui/solid";
|
|
3
|
+
import { insert as _$insert } from "@opentui/solid";
|
|
4
|
+
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
5
|
+
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
6
|
+
import { setProp as _$setProp } from "@opentui/solid";
|
|
7
|
+
import { createElement as _$createElement } from "@opentui/solid";
|
|
8
|
+
// oc-coffee — TUI half.
|
|
9
|
+
//
|
|
10
|
+
// Keeps the Mac awake while the agent works, the port of pi's `pi-coffee`.
|
|
11
|
+
//
|
|
12
|
+
// off | idle | display | system -> caffeinate (), (-i), (-i -d), (-i -d -s -u)
|
|
13
|
+
//
|
|
14
|
+
// Four cumulative levels. The assertion is held only while a run is active — engaged on
|
|
15
|
+
// `session.execution.started`, released on `.succeeded` / `.failed` / `.interrupted` — so the
|
|
16
|
+
// Mac sleeps normally between turns. `-w <pid>` binds the child to this process, so it dies
|
|
17
|
+
// with the TUI however the TUI exits, SIGKILL included, and a closed window can never leave
|
|
18
|
+
// the machine awake for ever.
|
|
19
|
+
//
|
|
20
|
+
// The assertion lives here and not in the server because the server cannot see a run: the
|
|
21
|
+
// public event stream carries only catalogue events (`model.updated`, `provider.updated`, …)
|
|
22
|
+
// and there is no "session is executing" state to read. Measured 2026-10-16, including a
|
|
23
|
+
// clean headless run. The TUI is the only half that knows.
|
|
24
|
+
//
|
|
25
|
+
// The badge shows the level whenever it is not `off` — the setting, not the moment — and only
|
|
26
|
+
// the cup lights up (green) while the assertion is up, so the state is one mark and the level
|
|
27
|
+
// stays legible. `/coffee` opens the menu, `/coffee <level>` sets one, `/coffee status` reports.
|
|
28
|
+
// The level is persisted with the TUI's own durable storage, which replaces pi's
|
|
29
|
+
// `pi-coffee.json`; there is no file of ours.
|
|
30
|
+
//
|
|
31
|
+
// macOS only: elsewhere the plugin stays loaded and simply never engages.
|
|
32
|
+
import { Plugin } from "@opencode/plugin/tui";
|
|
33
|
+
import { spawn } from "node:child_process";
|
|
34
|
+
import { createRoot, createSignal } from "solid-js";
|
|
35
|
+
|
|
36
|
+
/** md-coffee_outline (U+F06CA), the glyph pi's own badge uses. */
|
|
37
|
+
const CUP_GLYPH = "\u{f06ca}";
|
|
38
|
+
const LEVELS = ["off", "idle", "display", "system"];
|
|
39
|
+
const LEVEL_FLAGS = {
|
|
40
|
+
off: [],
|
|
41
|
+
idle: ["-i"],
|
|
42
|
+
display: ["-i", "-d"],
|
|
43
|
+
system: ["-i", "-d", "-s", "-u"]
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The four menu rows, the wording pi uses. */
|
|
47
|
+
const LEVEL_MENU = {
|
|
48
|
+
off: "Off — Do not prevent any sleep",
|
|
49
|
+
idle: "Prevent Idle Sleep — Keep the system awake while the agent works (caffeinate -i)",
|
|
50
|
+
display: "Prevent Display Sleep — Also keep the display from idle-sleeping (caffeinate -i -d)",
|
|
51
|
+
system: "Prevent System Sleep — Also block all system sleep on AC and declare the user active (caffeinate -i -d -s -u)"
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Fallbacks, used only when the theme defines none of the candidates. */
|
|
55
|
+
const FALLBACK_GREEN = "#3fb950";
|
|
56
|
+
const FALLBACK_DIM = "#8b949e";
|
|
57
|
+
function isLevel(value) {
|
|
58
|
+
return typeof value === "string" && LEVELS.includes(value);
|
|
59
|
+
}
|
|
60
|
+
export default Plugin.define({
|
|
61
|
+
id: "oc-coffee.tui",
|
|
62
|
+
setup(context) {
|
|
63
|
+
const supported = process.platform === "darwin";
|
|
64
|
+
|
|
65
|
+
// Durable: survives a restart and is shared by every TUI. A JSON object is what the
|
|
66
|
+
// storage takes, so the level rides in one.
|
|
67
|
+
const [stored, updateStored] = context.storage.store("coffee", {
|
|
68
|
+
initial: {
|
|
69
|
+
level: "display"
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
const level = () => isLevel(stored.level) ? stored.level : "display";
|
|
73
|
+
const themeColour = (...paths) => {
|
|
74
|
+
for (const path of paths) {
|
|
75
|
+
let node = context.theme;
|
|
76
|
+
for (const key of path) node = node?.[key];
|
|
77
|
+
if (node !== undefined && node !== null) return node;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
};
|
|
81
|
+
const GREEN = themeColour(["syntax", "string"], ["diff", "text", "added"], ["text", "feedback", "success", "base"]) ?? FALLBACK_GREEN;
|
|
82
|
+
const DIM = themeColour(["text", "muted"], ["text", "faint"]) ?? FALLBACK_DIM;
|
|
83
|
+
let child = null;
|
|
84
|
+
/** Runs in flight: several sessions can execute at once, so it is a count, not a flag. */
|
|
85
|
+
let running = 0;
|
|
86
|
+
/** Reactive: the badge must repaint the moment the assertion goes up or down. A plain
|
|
87
|
+
* variable read in the render would be read once and never again. */
|
|
88
|
+
const [active, setActive] = createRoot(() => createSignal(false));
|
|
89
|
+
function engage() {
|
|
90
|
+
if (!supported || level() === "off" || child) return;
|
|
91
|
+
const spawned = spawn("caffeinate", [...LEVEL_FLAGS[level()], "-w", String(process.pid)], {
|
|
92
|
+
stdio: "ignore"
|
|
93
|
+
});
|
|
94
|
+
child = spawned;
|
|
95
|
+
setActive(true);
|
|
96
|
+
spawned.on("error", () => {
|
|
97
|
+
if (child !== spawned) return;
|
|
98
|
+
child = null;
|
|
99
|
+
setActive(false);
|
|
100
|
+
});
|
|
101
|
+
spawned.on("exit", () => {
|
|
102
|
+
if (child !== spawned) return;
|
|
103
|
+
child = null;
|
|
104
|
+
setActive(false);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function release() {
|
|
108
|
+
const held = child;
|
|
109
|
+
child = null;
|
|
110
|
+
setActive(false);
|
|
111
|
+
held?.kill("SIGTERM");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The level can only change from the command, where a failure is worth a word. */
|
|
115
|
+
async function setLevel(next) {
|
|
116
|
+
const wasUp = child !== null;
|
|
117
|
+
if (wasUp) release();
|
|
118
|
+
await updateStored(draft => {
|
|
119
|
+
draft.level = next;
|
|
120
|
+
});
|
|
121
|
+
if (wasUp || running > 0) engage();
|
|
122
|
+
}
|
|
123
|
+
const describe = () => {
|
|
124
|
+
const flags = LEVEL_FLAGS[level()];
|
|
125
|
+
const labels = flags.length === 0 ? "Sleep prevention: off" : `Sleep prevention: ${level()} (caffeinate ${flags.join(" ")})`;
|
|
126
|
+
return `${labels} — assertion ${child ? "up" : "idle"}`;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// One listener per boundary. `running` counts sessions executing right now, so the
|
|
130
|
+
// assertion is up while it is positive and the level is not `off`.
|
|
131
|
+
const stopStarted = context.data.on("session.execution.started", () => {
|
|
132
|
+
running += 1;
|
|
133
|
+
if (running === 1) engage();
|
|
134
|
+
});
|
|
135
|
+
const stopSucceeded = context.data.on("session.execution.succeeded", () => {
|
|
136
|
+
running = Math.max(0, running - 1);
|
|
137
|
+
if (running === 0) release();
|
|
138
|
+
});
|
|
139
|
+
const stopFailed = context.data.on("session.execution.failed", () => {
|
|
140
|
+
running = Math.max(0, running - 1);
|
|
141
|
+
if (running === 0) release();
|
|
142
|
+
});
|
|
143
|
+
const stopInterrupted = context.data.on("session.execution.interrupted", () => {
|
|
144
|
+
running = Math.max(0, running - 1);
|
|
145
|
+
if (running === 0) release();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/** Always visible, `off` included: the badge is the setting, and seeing `off` is how you
|
|
149
|
+
* know the plugin is loaded and deliberately doing nothing. (pi hides it when off; the
|
|
150
|
+
* owner asked for the name instead.) */
|
|
151
|
+
const Badge = () => (() => {
|
|
152
|
+
var _el$ = _$createElement("box"),
|
|
153
|
+
_el$2 = _$createElement("text"),
|
|
154
|
+
_el$4 = _$createElement("text"),
|
|
155
|
+
_el$5 = _$createTextNode(` `);
|
|
156
|
+
_$insertNode(_el$, _el$2);
|
|
157
|
+
_$insertNode(_el$, _el$4);
|
|
158
|
+
_$setProp(_el$, "flexDirection", "row");
|
|
159
|
+
_$setProp(_el$, "flexShrink", 0);
|
|
160
|
+
_$insertNode(_el$2, _$createTextNode(``));
|
|
161
|
+
_$setProp(_el$2, "wrapMode", "none");
|
|
162
|
+
_$insertNode(_el$4, _el$5);
|
|
163
|
+
_$setProp(_el$4, "fg", DIM);
|
|
164
|
+
_$setProp(_el$4, "wrapMode", "none");
|
|
165
|
+
_$insert(_el$4, level, null);
|
|
166
|
+
_$effect(_$p => _$setProp(_el$2, "fg", active() ? GREEN : DIM, _$p));
|
|
167
|
+
return _el$;
|
|
168
|
+
})();
|
|
169
|
+
context.ui.slot({
|
|
170
|
+
append: "prompt.footer.status",
|
|
171
|
+
render: () => _$createComponent(Badge, {})
|
|
172
|
+
});
|
|
173
|
+
context.keymap.layer(() => ({
|
|
174
|
+
mode: "global",
|
|
175
|
+
commands: [{
|
|
176
|
+
id: "oc-coffee.menu",
|
|
177
|
+
title: "Coffee: sleep prevention",
|
|
178
|
+
group: "Coffee",
|
|
179
|
+
palette: true,
|
|
180
|
+
slash: {
|
|
181
|
+
name: "coffee",
|
|
182
|
+
arguments: true
|
|
183
|
+
},
|
|
184
|
+
run: async input => {
|
|
185
|
+
const argument = (input ?? "").trim().split(/\s+/).filter(Boolean)[0];
|
|
186
|
+
if (argument === "status") {
|
|
187
|
+
await context.ui.dialog.alert({
|
|
188
|
+
title: `${CUP_GLYPH} Coffee`,
|
|
189
|
+
message: describe()
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (isLevel(argument)) {
|
|
194
|
+
await setLevel(argument);
|
|
195
|
+
context.ui.toast.show({
|
|
196
|
+
message: describe()
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (argument !== undefined) {
|
|
201
|
+
context.ui.toast.show({
|
|
202
|
+
message: `usage: /coffee [${LEVELS.join(" | ")} | status]`,
|
|
203
|
+
variant: "warning"
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const picked = await context.ui.dialog.select({
|
|
208
|
+
title: `${CUP_GLYPH} Sleep prevention`,
|
|
209
|
+
current: level(),
|
|
210
|
+
options: LEVELS.map(candidate => ({
|
|
211
|
+
title: candidate === level() ? `${LEVEL_MENU[candidate]}` : LEVEL_MENU[candidate],
|
|
212
|
+
value: candidate
|
|
213
|
+
}))
|
|
214
|
+
});
|
|
215
|
+
if (picked) {
|
|
216
|
+
await setLevel(String(picked));
|
|
217
|
+
context.ui.toast.show({
|
|
218
|
+
message: describe()
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}]
|
|
223
|
+
}));
|
|
224
|
+
return () => {
|
|
225
|
+
stopStarted?.();
|
|
226
|
+
stopSucceeded?.();
|
|
227
|
+
stopFailed?.();
|
|
228
|
+
stopInterrupted?.();
|
|
229
|
+
release();
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
});
|
package/tui.tsx
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
// oc-coffee — TUI half.
|
|
2
|
-
//
|
|
3
|
-
// Keeps the Mac awake while the agent works, the port of pi's `pi-coffee`.
|
|
4
|
-
//
|
|
5
|
-
// off | idle | display | system -> caffeinate (), (-i), (-i -d), (-i -d -s -u)
|
|
6
|
-
//
|
|
7
|
-
// Four cumulative levels. The assertion is held only while a run is active — engaged on
|
|
8
|
-
// `session.execution.started`, released on `.succeeded` / `.failed` / `.interrupted` — so the
|
|
9
|
-
// Mac sleeps normally between turns. `-w <pid>` binds the child to this process, so it dies
|
|
10
|
-
// with the TUI however the TUI exits, SIGKILL included, and a closed window can never leave
|
|
11
|
-
// the machine awake for ever.
|
|
12
|
-
//
|
|
13
|
-
// The assertion lives here and not in the server because the server cannot see a run: the
|
|
14
|
-
// public event stream carries only catalogue events (`model.updated`, `provider.updated`, …)
|
|
15
|
-
// and there is no "session is executing" state to read. Measured 2026-10-16, including a
|
|
16
|
-
// clean headless run. The TUI is the only half that knows.
|
|
17
|
-
//
|
|
18
|
-
// The badge shows the level whenever it is not `off` — the setting, not the moment — and only
|
|
19
|
-
// the cup lights up (green) while the assertion is up, so the state is one mark and the level
|
|
20
|
-
// stays legible. `/coffee` opens the menu, `/coffee <level>` sets one, `/coffee status` reports.
|
|
21
|
-
// The level is persisted with the TUI's own durable storage, which replaces pi's
|
|
22
|
-
// `pi-coffee.json`; there is no file of ours.
|
|
23
|
-
//
|
|
24
|
-
// macOS only: elsewhere the plugin stays loaded and simply never engages.
|
|
25
|
-
import { Plugin } from "@opencode/plugin/tui"
|
|
26
|
-
import { spawn, type ChildProcess } from "node:child_process"
|
|
27
|
-
import { createRoot, createSignal } from "solid-js"
|
|
28
|
-
|
|
29
|
-
/** md-coffee_outline (U+F06CA), the glyph pi's own badge uses. */
|
|
30
|
-
const CUP_GLYPH = "\u{f06ca}"
|
|
31
|
-
|
|
32
|
-
const LEVELS = ["off", "idle", "display", "system"] as const
|
|
33
|
-
type Level = (typeof LEVELS)[number]
|
|
34
|
-
|
|
35
|
-
const LEVEL_FLAGS: Record<Level, string[]> = {
|
|
36
|
-
off: [],
|
|
37
|
-
idle: ["-i"],
|
|
38
|
-
display: ["-i", "-d"],
|
|
39
|
-
system: ["-i", "-d", "-s", "-u"],
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** The four menu rows, the wording pi uses. */
|
|
43
|
-
const LEVEL_MENU: Record<Level, string> = {
|
|
44
|
-
off: "Off — Do not prevent any sleep",
|
|
45
|
-
idle: "Prevent Idle Sleep — Keep the system awake while the agent works (caffeinate -i)",
|
|
46
|
-
display: "Prevent Display Sleep — Also keep the display from idle-sleeping (caffeinate -i -d)",
|
|
47
|
-
system: "Prevent System Sleep — Also block all system sleep on AC and declare the user active (caffeinate -i -d -s -u)",
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Fallbacks, used only when the theme defines none of the candidates. */
|
|
51
|
-
const FALLBACK_GREEN = "#3fb950"
|
|
52
|
-
const FALLBACK_DIM = "#8b949e"
|
|
53
|
-
|
|
54
|
-
function isLevel(value: unknown): value is Level {
|
|
55
|
-
return typeof value === "string" && (LEVELS as readonly string[]).includes(value)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export default Plugin.define({
|
|
59
|
-
id: "oc-coffee.tui",
|
|
60
|
-
setup(context: any) {
|
|
61
|
-
const supported = process.platform === "darwin"
|
|
62
|
-
|
|
63
|
-
// Durable: survives a restart and is shared by every TUI. A JSON object is what the
|
|
64
|
-
// storage takes, so the level rides in one.
|
|
65
|
-
const [stored, updateStored] = context.storage.store("coffee", { initial: { level: "display" as Level } })
|
|
66
|
-
const level = (): Level => (isLevel(stored.level) ? stored.level : "display")
|
|
67
|
-
|
|
68
|
-
const themeColour = (...paths: string[][]): any => {
|
|
69
|
-
for (const path of paths) {
|
|
70
|
-
let node: any = context.theme
|
|
71
|
-
for (const key of path) node = node?.[key]
|
|
72
|
-
if (node !== undefined && node !== null) return node
|
|
73
|
-
}
|
|
74
|
-
return undefined
|
|
75
|
-
}
|
|
76
|
-
const GREEN =
|
|
77
|
-
themeColour(["syntax", "string"], ["diff", "text", "added"], ["text", "feedback", "success", "base"]) ??
|
|
78
|
-
FALLBACK_GREEN
|
|
79
|
-
const DIM = themeColour(["text", "muted"], ["text", "faint"]) ?? FALLBACK_DIM
|
|
80
|
-
|
|
81
|
-
let child: ChildProcess | null = null
|
|
82
|
-
/** Runs in flight: several sessions can execute at once, so it is a count, not a flag. */
|
|
83
|
-
let running = 0
|
|
84
|
-
/** Reactive: the badge must repaint the moment the assertion goes up or down. A plain
|
|
85
|
-
* variable read in the render would be read once and never again. */
|
|
86
|
-
const [active, setActive] = createRoot(() => createSignal(false))
|
|
87
|
-
|
|
88
|
-
function engage(): void {
|
|
89
|
-
if (!supported || level() === "off" || child) return
|
|
90
|
-
const spawned = spawn("caffeinate", [...LEVEL_FLAGS[level()], "-w", String(process.pid)], {
|
|
91
|
-
stdio: "ignore",
|
|
92
|
-
})
|
|
93
|
-
child = spawned
|
|
94
|
-
setActive(true)
|
|
95
|
-
spawned.on("error", () => {
|
|
96
|
-
if (child !== spawned) return
|
|
97
|
-
child = null
|
|
98
|
-
setActive(false)
|
|
99
|
-
})
|
|
100
|
-
spawned.on("exit", () => {
|
|
101
|
-
if (child !== spawned) return
|
|
102
|
-
child = null
|
|
103
|
-
setActive(false)
|
|
104
|
-
})
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function release(): void {
|
|
108
|
-
const held = child
|
|
109
|
-
child = null
|
|
110
|
-
setActive(false)
|
|
111
|
-
held?.kill("SIGTERM")
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** The level can only change from the command, where a failure is worth a word. */
|
|
115
|
-
async function setLevel(next: Level): Promise<void> {
|
|
116
|
-
const wasUp = child !== null
|
|
117
|
-
if (wasUp) release()
|
|
118
|
-
await updateStored((draft: any) => {
|
|
119
|
-
draft.level = next
|
|
120
|
-
})
|
|
121
|
-
if (wasUp || running > 0) engage()
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const describe = (): string => {
|
|
125
|
-
const flags = LEVEL_FLAGS[level()]
|
|
126
|
-
const labels = flags.length === 0 ? "Sleep prevention: off" : `Sleep prevention: ${level()} (caffeinate ${flags.join(" ")})`
|
|
127
|
-
return `${labels} — assertion ${child ? "up" : "idle"}`
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// One listener per boundary. `running` counts sessions executing right now, so the
|
|
131
|
-
// assertion is up while it is positive and the level is not `off`.
|
|
132
|
-
const stopStarted = context.data.on("session.execution.started", () => {
|
|
133
|
-
running += 1
|
|
134
|
-
if (running === 1) engage()
|
|
135
|
-
})
|
|
136
|
-
const stopSucceeded = context.data.on("session.execution.succeeded", () => {
|
|
137
|
-
running = Math.max(0, running - 1)
|
|
138
|
-
if (running === 0) release()
|
|
139
|
-
})
|
|
140
|
-
const stopFailed = context.data.on("session.execution.failed", () => {
|
|
141
|
-
running = Math.max(0, running - 1)
|
|
142
|
-
if (running === 0) release()
|
|
143
|
-
})
|
|
144
|
-
const stopInterrupted = context.data.on("session.execution.interrupted", () => {
|
|
145
|
-
running = Math.max(0, running - 1)
|
|
146
|
-
if (running === 0) release()
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
/** Always visible, `off` included: the badge is the setting, and seeing `off` is how you
|
|
150
|
-
* know the plugin is loaded and deliberately doing nothing. (pi hides it when off; the
|
|
151
|
-
* owner asked for the name instead.) */
|
|
152
|
-
const Badge = () => (
|
|
153
|
-
<box flexDirection="row" flexShrink={0}>
|
|
154
|
-
<text fg={active() ? GREEN : DIM} wrapMode="none">{CUP_GLYPH}</text>
|
|
155
|
-
<text fg={DIM} wrapMode="none"> {level()}</text>
|
|
156
|
-
</box>
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
context.ui.slot({ append: "prompt.footer.status", render: () => <Badge /> })
|
|
160
|
-
|
|
161
|
-
context.keymap.layer(() => ({
|
|
162
|
-
mode: "global",
|
|
163
|
-
commands: [
|
|
164
|
-
{
|
|
165
|
-
id: "oc-coffee.menu",
|
|
166
|
-
title: "Coffee: sleep prevention",
|
|
167
|
-
group: "Coffee",
|
|
168
|
-
palette: true,
|
|
169
|
-
slash: { name: "coffee", arguments: true },
|
|
170
|
-
run: async (input?: string) => {
|
|
171
|
-
const argument = (input ?? "").trim().split(/\s+/).filter(Boolean)[0]
|
|
172
|
-
|
|
173
|
-
if (argument === "status") {
|
|
174
|
-
await context.ui.dialog.alert({ title: `${CUP_GLYPH} Coffee`, message: describe() })
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
if (isLevel(argument)) {
|
|
178
|
-
await setLevel(argument)
|
|
179
|
-
context.ui.toast.show({ message: describe() })
|
|
180
|
-
return
|
|
181
|
-
}
|
|
182
|
-
if (argument !== undefined) {
|
|
183
|
-
context.ui.toast.show({
|
|
184
|
-
message: `usage: /coffee [${LEVELS.join(" | ")} | status]`,
|
|
185
|
-
variant: "warning",
|
|
186
|
-
})
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const picked = await context.ui.dialog.select({
|
|
191
|
-
title: `${CUP_GLYPH} Sleep prevention`,
|
|
192
|
-
current: level(),
|
|
193
|
-
options: LEVELS.map((candidate) => ({
|
|
194
|
-
title: candidate === level() ? `${LEVEL_MENU[candidate]}` : LEVEL_MENU[candidate],
|
|
195
|
-
value: candidate,
|
|
196
|
-
})),
|
|
197
|
-
})
|
|
198
|
-
if (picked) {
|
|
199
|
-
await setLevel(String(picked) as Level)
|
|
200
|
-
context.ui.toast.show({ message: describe() })
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
],
|
|
205
|
-
}))
|
|
206
|
-
|
|
207
|
-
return () => {
|
|
208
|
-
stopStarted?.()
|
|
209
|
-
stopSucceeded?.()
|
|
210
|
-
stopFailed?.()
|
|
211
|
-
stopInterrupted?.()
|
|
212
|
-
release()
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
})
|