@valyrianjs/terminal 0.2.1 → 0.2.2

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.
Files changed (80) hide show
  1. package/dist/ansi.d.ts.map +1 -1
  2. package/dist/ansi.js +12 -14
  3. package/dist/ansi.js.map +1 -1
  4. package/dist/events.d.ts.map +1 -1
  5. package/dist/events.js +4 -0
  6. package/dist/events.js.map +1 -1
  7. package/dist/frame-style.d.ts +7 -0
  8. package/dist/frame-style.d.ts.map +1 -0
  9. package/dist/frame-style.js +27 -0
  10. package/dist/frame-style.js.map +1 -0
  11. package/dist/layout.d.ts +5 -1
  12. package/dist/layout.d.ts.map +1 -1
  13. package/dist/layout.js +53 -23
  14. package/dist/layout.js.map +1 -1
  15. package/dist/mouse.d.ts.map +1 -1
  16. package/dist/mouse.js +8 -1
  17. package/dist/mouse.js.map +1 -1
  18. package/dist/render.d.ts.map +1 -1
  19. package/dist/render.js +87 -48
  20. package/dist/render.js.map +1 -1
  21. package/dist/session.d.ts.map +1 -1
  22. package/dist/session.js +2 -0
  23. package/dist/session.js.map +1 -1
  24. package/dist/text.d.ts +7 -0
  25. package/dist/text.d.ts.map +1 -1
  26. package/dist/text.js +114 -0
  27. package/dist/text.js.map +1 -1
  28. package/dist/types.d.ts +3 -0
  29. package/dist/types.d.ts.map +1 -1
  30. package/docs/api-reference.md +6 -3
  31. package/docs/cookbook.md +1 -1
  32. package/docs/interaction-model.md +5 -5
  33. package/docs/primitive-gallery.md +4 -4
  34. package/examples/basic.tsx +22 -0
  35. package/examples/cli.tsx +55 -0
  36. package/examples/demo.tsx +98 -0
  37. package/examples/docs/background-fill.tsx +107 -0
  38. package/examples/docs/component-composition.tsx +140 -0
  39. package/examples/docs/cursor.tsx +121 -0
  40. package/examples/docs/employees-list.tsx +138 -0
  41. package/examples/docs/hello.tsx +98 -0
  42. package/examples/docs/interactive-note.tsx +111 -0
  43. package/examples/docs/module-api-dashboard.tsx +307 -0
  44. package/examples/docs/module-flux-store.tsx +181 -0
  45. package/examples/docs/module-form-workflow.tsx +339 -0
  46. package/examples/docs/module-forms.tsx +218 -0
  47. package/examples/docs/module-money.tsx +175 -0
  48. package/examples/docs/module-native-store.tsx +188 -0
  49. package/examples/docs/module-pulses.tsx +142 -0
  50. package/examples/docs/module-query.tsx +209 -0
  51. package/examples/docs/module-request.tsx +194 -0
  52. package/examples/docs/module-state-workbench.tsx +283 -0
  53. package/examples/docs/module-tasks.tsx +223 -0
  54. package/examples/docs/module-translate.tsx +194 -0
  55. package/examples/docs/module-utils.tsx +168 -0
  56. package/examples/docs/module-valyrian-core.tsx +159 -0
  57. package/examples/docs/pizza-builder.tsx +463 -0
  58. package/examples/docs/primitive-activity-console.tsx +113 -0
  59. package/examples/docs/primitive-command-panel.tsx +186 -0
  60. package/examples/docs/primitive-data-explorer.tsx +155 -0
  61. package/examples/docs/primitive-input-workbench.tsx +128 -0
  62. package/examples/docs/primitive-layout-shell.tsx +115 -0
  63. package/examples/docs/responsive-split.tsx +186 -0
  64. package/examples/docs/style-system.tsx +209 -0
  65. package/examples/docs/theme-colors.tsx +225 -0
  66. package/examples/docs/virtualized-list-workbench.tsx +232 -0
  67. package/examples/opencode-dogfood-app.tsx +215 -0
  68. package/examples/opencode-dogfood-lifecycle.tsx +194 -0
  69. package/examples/opencode-dogfood.tsx +11 -0
  70. package/llms-full.txt +16 -13
  71. package/package.json +3 -2
  72. package/src/ansi.ts +12 -14
  73. package/src/events.ts +2 -0
  74. package/src/frame-style.ts +36 -0
  75. package/src/layout.ts +57 -24
  76. package/src/mouse.ts +10 -1
  77. package/src/render.ts +92 -48
  78. package/src/session.ts +2 -0
  79. package/src/text.ts +148 -0
  80. package/src/types.ts +3 -0
@@ -0,0 +1,138 @@
1
+ import { Screen, Text, mountTerminal } from "@valyrianjs/terminal";
2
+ import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
3
+
4
+ type Employee = {
5
+ name: string;
6
+ role: string;
7
+ };
8
+
9
+ interface EmployeesListState {
10
+ selectedIndex: number;
11
+ running: boolean;
12
+ }
13
+
14
+ export interface EmployeesListDemo {
15
+ session: TerminalSession;
16
+ dispatchKey(key: string): string;
17
+ output(): string;
18
+ ansiOutput(): string;
19
+ isRunning(): boolean;
20
+ destroy(): void;
21
+ }
22
+
23
+ const employees: Employee[] = [
24
+ { name: "Ana", role: "Design" },
25
+ { name: "Luis", role: "Support" },
26
+ { name: "Maya", role: "Engineering" }
27
+ ];
28
+
29
+ function createInitialState(): EmployeesListState {
30
+ return { selectedIndex: 0, running: true };
31
+ }
32
+
33
+ function shouldRunSnapshot() {
34
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
35
+ }
36
+
37
+ function EmployeeRow({ employee, selected }: { employee: Employee; selected: boolean }) {
38
+ return <Text>{`${selected ? ">" : " "} ${employee.name} — ${employee.role}`}</Text>;
39
+ }
40
+
41
+ function Employees({ items, selectedIndex }: { items: Employee[]; selectedIndex: number }) {
42
+ return (
43
+ <>
44
+ {items.map((employee, index) => (
45
+ <EmployeeRow employee={employee} selected={index === selectedIndex} />
46
+ ))}
47
+ </>
48
+ );
49
+ }
50
+
51
+ export function App({ state }: { state: EmployeesListState }) {
52
+ return (
53
+ <Screen title="Team">
54
+ <Employees items={employees} selectedIndex={state.selectedIndex} />
55
+ <Text>J/K or arrows: choose Ctrl+C: quit</Text>
56
+ </Screen>
57
+ );
58
+ }
59
+
60
+ export function createEmployeesListDemo(options: TerminalMountOptions = {}): EmployeesListDemo {
61
+ const state = createInitialState();
62
+ let session: TerminalSession;
63
+
64
+ function quit() {
65
+ state.running = false;
66
+ session.destroy();
67
+ }
68
+
69
+ function move(delta: number) {
70
+ const nextIndex = state.selectedIndex + delta;
71
+ state.selectedIndex = Math.max(0, Math.min(employees.length - 1, nextIndex));
72
+ }
73
+
74
+ session = mountTerminal(<App state={state} />, {
75
+ ...options,
76
+ keymap: {
77
+ ...options.keymap,
78
+ bindings: [
79
+ ...(options.keymap?.bindings || []),
80
+ { key: "j", command: { id: "employee.next" }, scope: "global" },
81
+ { key: "J", command: { id: "employee.next" }, scope: "global" },
82
+ { key: "DOWN", command: { id: "employee.next" }, scope: "global" },
83
+ { key: "ARROWDOWN", command: { id: "employee.next" }, scope: "global" },
84
+ { key: "k", command: { id: "employee.prev" }, scope: "global" },
85
+ { key: "K", command: { id: "employee.prev" }, scope: "global" },
86
+ { key: "UP", command: { id: "employee.prev" }, scope: "global" },
87
+ { key: "ARROWUP", command: { id: "employee.prev" }, scope: "global" },
88
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
89
+ ],
90
+ onCommand(command, context) {
91
+ if (command.id === "employee.next") {
92
+ move(1);
93
+ return true;
94
+ }
95
+ if (command.id === "employee.prev") {
96
+ move(-1);
97
+ return true;
98
+ }
99
+ if (command.id === "quit") {
100
+ quit();
101
+ return true;
102
+ }
103
+ return options.keymap?.onCommand?.(command, context);
104
+ }
105
+ }
106
+ });
107
+
108
+ return {
109
+ session,
110
+ dispatchKey(key: string) {
111
+ return session.dispatchKey(key);
112
+ },
113
+ output() {
114
+ return session.output();
115
+ },
116
+ ansiOutput() {
117
+ return session.ansiOutput();
118
+ },
119
+ isRunning() {
120
+ return state.running;
121
+ },
122
+ destroy() {
123
+ state.running = false;
124
+ session.destroy();
125
+ }
126
+ };
127
+ }
128
+
129
+ if (import.meta.main) {
130
+ if (shouldRunSnapshot()) {
131
+ const demo = createEmployeesListDemo({ runtime: "headless", cols: 44, rows: 8 });
132
+ process.stdout.write(demo.output());
133
+ process.stdout.write("\n");
134
+ demo.destroy();
135
+ } else {
136
+ createEmployeesListDemo();
137
+ }
138
+ }
@@ -0,0 +1,98 @@
1
+ import { Screen, Text, mountTerminal } from "@valyrianjs/terminal";
2
+ import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
3
+
4
+ interface HelloState {
5
+ expanded: boolean;
6
+ running: boolean;
7
+ }
8
+
9
+ export interface HelloDemo {
10
+ session: TerminalSession;
11
+ dispatchKey(key: string): string;
12
+ output(): string;
13
+ ansiOutput(): string;
14
+ isRunning(): boolean;
15
+ destroy(): void;
16
+ }
17
+
18
+ function createInitialState(): HelloState {
19
+ return { expanded: false, running: true };
20
+ }
21
+
22
+ function shouldRunSnapshot() {
23
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
24
+ }
25
+
26
+ export function App({ state }: { state: HelloState }) {
27
+ return (
28
+ <Screen title="Hello terminal">
29
+ <Text>Hello from Valyrian Terminal</Text>
30
+ <Text>{state.expanded ? "Build terminal apps with plain components." : "Press Enter to see one detail."}</Text>
31
+ <Text>Enter: details Ctrl+C: quit</Text>
32
+ </Screen>
33
+ );
34
+ }
35
+
36
+ export function createHelloDemo(options: TerminalMountOptions = {}): HelloDemo {
37
+ const state = createInitialState();
38
+ let session: TerminalSession;
39
+
40
+ function quit() {
41
+ state.running = false;
42
+ session.destroy();
43
+ }
44
+
45
+ session = mountTerminal(<App state={state} />, {
46
+ ...options,
47
+ keymap: {
48
+ ...options.keymap,
49
+ bindings: [
50
+ ...(options.keymap?.bindings || []),
51
+ { key: "ENTER", command: { id: "details.toggle" }, scope: "global" },
52
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
53
+ ],
54
+ onCommand(command, context) {
55
+ if (command.id === "details.toggle") {
56
+ state.expanded = !state.expanded;
57
+ return true;
58
+ }
59
+ if (command.id === "quit") {
60
+ quit();
61
+ return true;
62
+ }
63
+ return options.keymap?.onCommand?.(command, context);
64
+ }
65
+ }
66
+ });
67
+
68
+ return {
69
+ session,
70
+ dispatchKey(key: string) {
71
+ return session.dispatchKey(key);
72
+ },
73
+ output() {
74
+ return session.output();
75
+ },
76
+ ansiOutput() {
77
+ return session.ansiOutput();
78
+ },
79
+ isRunning() {
80
+ return state.running;
81
+ },
82
+ destroy() {
83
+ state.running = false;
84
+ session.destroy();
85
+ }
86
+ };
87
+ }
88
+
89
+ if (import.meta.main) {
90
+ if (shouldRunSnapshot()) {
91
+ const demo = createHelloDemo({ runtime: "headless", cols: 42, rows: 6 });
92
+ process.stdout.write(demo.output());
93
+ process.stdout.write("\n");
94
+ demo.destroy();
95
+ } else {
96
+ createHelloDemo();
97
+ }
98
+ }
@@ -0,0 +1,111 @@
1
+ import { Input, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
2
+ import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
3
+
4
+ interface InteractiveNoteState {
5
+ value: string;
6
+ saved: string;
7
+ running: boolean;
8
+ }
9
+
10
+ export interface InteractiveNoteDemo {
11
+ session: TerminalSession;
12
+ dispatchKey(key: string): string;
13
+ output(): string;
14
+ ansiOutput(): string;
15
+ isRunning(): boolean;
16
+ destroy(): void;
17
+ }
18
+
19
+ function createInitialState(): InteractiveNoteState {
20
+ return { value: "", saved: "", running: true };
21
+ }
22
+
23
+ function shouldRunSnapshot() {
24
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
25
+ }
26
+
27
+ export function App({ state }: { state: InteractiveNoteState }) {
28
+ const draftStatus = state.value.length === 0 ? "Draft: empty" : `Draft: ${state.value.length} chars`;
29
+
30
+ return (
31
+ <Screen title="Notes">
32
+ <Text>Notes</Text>
33
+ <Text>{draftStatus}</Text>
34
+ <Input
35
+ id="note"
36
+ value={state.value}
37
+ placeholder="Write a note"
38
+ onchange={(event) => {
39
+ state.value = event.value;
40
+ }}
41
+ onsubmit={(event) => {
42
+ state.saved = event.value;
43
+ state.value = "";
44
+ }}
45
+ />
46
+ <Text>{state.saved ? `Saved: ${state.saved}` : "Last saved: none"}</Text>
47
+ <Text>Enter save | Ctrl+C: quit</Text>
48
+ </Screen>
49
+ );
50
+ }
51
+
52
+ export function createInteractiveNoteDemo(options: TerminalMountOptions = {}): InteractiveNoteDemo {
53
+ const state = createInitialState();
54
+ let session: TerminalSession;
55
+
56
+ function quit() {
57
+ state.running = false;
58
+ session.destroy();
59
+ }
60
+
61
+ session = mountTerminal(<App state={state} />, {
62
+ ...options,
63
+ keymap: {
64
+ ...options.keymap,
65
+ bindings: [
66
+ ...(options.keymap?.bindings || []),
67
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
68
+ ],
69
+ onCommand(command, context) {
70
+ if (command.id === "quit") {
71
+ quit();
72
+ return true;
73
+ }
74
+ return options.keymap?.onCommand?.(command, context);
75
+ }
76
+ }
77
+ });
78
+
79
+ session.focus("note");
80
+
81
+ return {
82
+ session,
83
+ dispatchKey(key: string) {
84
+ return session.dispatchKey(key);
85
+ },
86
+ output() {
87
+ return session.output();
88
+ },
89
+ ansiOutput() {
90
+ return session.ansiOutput();
91
+ },
92
+ isRunning() {
93
+ return state.running;
94
+ },
95
+ destroy() {
96
+ state.running = false;
97
+ session.destroy();
98
+ }
99
+ };
100
+ }
101
+
102
+ if (import.meta.main) {
103
+ if (shouldRunSnapshot()) {
104
+ const demo = createInteractiveNoteDemo({ runtime: "headless", cols: 48, rows: 8 });
105
+ process.stdout.write(demo.output());
106
+ process.stdout.write("\n");
107
+ demo.destroy();
108
+ } else {
109
+ createInteractiveNoteDemo();
110
+ }
111
+ }
@@ -0,0 +1,307 @@
1
+ import { QueryClient } from "valyrian.js/query";
2
+ import { request } from "valyrian.js/request";
3
+ import { Task } from "valyrian.js/tasks";
4
+ import { Pane, Screen, Split, Text, mountTerminal } from "@valyrianjs/terminal";
5
+ import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
6
+
7
+ interface ServiceStatus {
8
+ service: string;
9
+ queue: number;
10
+ version: string;
11
+ }
12
+
13
+ interface ApiDashboardState {
14
+ errorMode: boolean;
15
+ service: string;
16
+ queryStatus: string;
17
+ apiMessage: string;
18
+ invalidated: boolean;
19
+ taskStatus: string;
20
+ taskResult: string;
21
+ taskError: string;
22
+ taskJob: string;
23
+ running: boolean;
24
+ }
25
+
26
+ interface ModuleApiDashboardDemo {
27
+ session: TerminalSession;
28
+ dispatchKey(key: string): string;
29
+ output(): string;
30
+ ansiOutput(): string;
31
+ isRunning(): boolean;
32
+ destroy(): void;
33
+ }
34
+
35
+ const PANEL_STYLE = { color: "#f8fafc", background: "#0f172a", padding: { left: 1, right: 1 } };
36
+ const API_STYLE = { color: "#ffffff", background: "#164e63", padding: { left: 1, right: 1 } };
37
+ const TASK_STYLE = { color: "#ffffff", background: "#312e81", padding: { left: 1, right: 1 } };
38
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
39
+ const ERROR_STYLE = { color: "#ffffff", background: "#7f1d1d" };
40
+ const SUCCESS_STYLE = { color: "#ffffff", background: "#14532d" };
41
+
42
+ function shouldRunSnapshot() {
43
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
44
+ }
45
+
46
+ function createState(): ApiDashboardState {
47
+ return {
48
+ errorMode: false,
49
+ service: "idle",
50
+ queryStatus: "idle",
51
+ apiMessage: "No request yet",
52
+ invalidated: false,
53
+ taskStatus: "idle",
54
+ taskResult: "Result: none",
55
+ taskError: "Error: none",
56
+ taskJob: "none",
57
+ running: true
58
+ };
59
+ }
60
+
61
+ async function withFakeFetch<T>(state: ApiDashboardState, run: () => Promise<T>) {
62
+ const originalFetch = globalThis.fetch;
63
+ globalThis.fetch = ((input: RequestInfo | URL) => {
64
+ const url = String(input);
65
+ if (state.errorMode || url.endsWith("/failure")) {
66
+ return Promise.resolve(new Response(JSON.stringify({ message: "controlled request failure" }), { status: 503 }));
67
+ }
68
+ return Promise.resolve(new Response(JSON.stringify({ service: "healthy", queue: 3, version: "local-1" }), { status: 200 }));
69
+ }) as typeof fetch;
70
+
71
+ try {
72
+ return await run();
73
+ } finally {
74
+ globalThis.fetch = originalFetch;
75
+ }
76
+ }
77
+
78
+ function waitForTask(signal: AbortSignal) {
79
+ return new Promise<void>((resolve) => {
80
+ const timer = setTimeout(resolve, 20);
81
+ signal.addEventListener(
82
+ "abort",
83
+ () => {
84
+ clearTimeout(timer);
85
+ resolve();
86
+ },
87
+ { once: true }
88
+ );
89
+ });
90
+ }
91
+
92
+ function syncTaskState(state: ApiDashboardState, task: Task<string, string>) {
93
+ const snapshot = task.state;
94
+ state.taskStatus = snapshot.status;
95
+ state.taskResult = snapshot.result ? `Result: ${snapshot.result}` : "Result: none";
96
+ state.taskError = snapshot.error instanceof Error ? `Error: ${snapshot.error.message}` : "Error: none";
97
+ }
98
+
99
+ export function App({ state }: { state: ApiDashboardState }) {
100
+ return (
101
+ <Screen title="API Operations Dashboard">
102
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Valyrian.js modules in terminal apps: API dashboard</Text>
103
+ <Split direction="row" gap={1} sizes={["1fr", "1fr"]}>
104
+ <Pane style={API_STYLE}>
105
+ <Text>API Operations Dashboard</Text>
106
+ <Text>{`Mode: ${state.errorMode ? "forced error" : "success"}`}</Text>
107
+ <Text state={state.queryStatus === "error" ? "error" : "success"} styles={{ error: ERROR_STYLE, success: SUCCESS_STYLE }}>{state.apiMessage}</Text>
108
+ <Text>{`Query: ${state.queryStatus}`}</Text>
109
+ <Text>{`Invalidated: ${state.invalidated ? "yes" : "no"}`}</Text>
110
+ <Text>{`Service: ${state.service}`}</Text>
111
+ </Pane>
112
+ <Pane style={TASK_STYLE}>
113
+ <Text>Maintenance task</Text>
114
+ <Text>{`Task: ${state.taskStatus}`}</Text>
115
+ <Text>{`Job: ${state.taskJob}`}</Text>
116
+ <Text>{state.taskResult}</Text>
117
+ <Text>{state.taskError}</Text>
118
+ <Text>Request and query use a local fake fetch, so this example never calls the network.</Text>
119
+ </Pane>
120
+ </Split>
121
+ <Pane style={PANEL_STYLE}>
122
+ <Text>R: refresh E: force error I: invalidate T: start task C: cancel Ctrl+C: quit</Text>
123
+ <Text style={FOOTER_STYLE}>See the Valyrian.js documentation for request, query, and tasks.</Text>
124
+ </Pane>
125
+ </Screen>
126
+ );
127
+ }
128
+
129
+ export function createModuleApiDashboardDemo(options: TerminalMountOptions = {}): ModuleApiDashboardDemo {
130
+ const state = createState();
131
+ const client = new QueryClient({ staleTime: 0, cacheTime: 1000 });
132
+ const api = request.new("http://terminal.local", { allowedMethods: ["get"] });
133
+ const serviceQuery = client.query({
134
+ key: ["terminal", "service-status"],
135
+ fetcher: () => api.get("/health")
136
+ });
137
+ const maintenanceTask = new Task<string, string>(async (label, { signal }) => {
138
+ if (label === "scan") {
139
+ await waitForTask(signal);
140
+ if (signal.aborted) return "Cancelled stale scan";
141
+ }
142
+ if (label === "fail") throw new Error("controlled task failure");
143
+ return `Completed ${label}`;
144
+ }, { strategy: "restartable" });
145
+ let session: TerminalSession;
146
+ const offTaskState = maintenanceTask.on("state", () => {
147
+ syncTaskState(state, maintenanceTask);
148
+ session?.update();
149
+ });
150
+
151
+ function quit() {
152
+ state.running = false;
153
+ offTaskState();
154
+ client.clear();
155
+ maintenanceTask.reset();
156
+ session.destroy();
157
+ }
158
+
159
+ async function refresh() {
160
+ if (state.queryStatus === "loading") {
161
+ state.apiMessage = "Refresh already in progress";
162
+ session.update();
163
+ return;
164
+ }
165
+
166
+ state.queryStatus = "loading";
167
+ state.apiMessage = state.errorMode ? "Loading forced error" : "Loading service status";
168
+ session.update();
169
+ try {
170
+ const data = (await withFakeFetch(state, () => serviceQuery.fetch())) as ServiceStatus;
171
+ state.service = data.service;
172
+ state.queryStatus = serviceQuery.state.status;
173
+ state.apiMessage = `Service: ${data.service} Queue: ${data.queue} Version: ${data.version}`;
174
+ state.invalidated = false;
175
+ } catch {
176
+ state.service = "unavailable";
177
+ state.queryStatus = serviceQuery.state.status;
178
+ state.apiMessage = "API error: controlled request failure";
179
+ }
180
+ session.update();
181
+ }
182
+
183
+ function invalidate() {
184
+ serviceQuery.invalidate();
185
+ state.queryStatus = serviceQuery.state.status;
186
+ state.invalidated = true;
187
+ state.apiMessage = "Cache invalidated; refresh will fetch again";
188
+ session.update();
189
+ }
190
+
191
+ async function runMaintenance(label: string, job: string) {
192
+ state.taskJob = job;
193
+ syncTaskState(state, maintenanceTask);
194
+ session.update();
195
+ try {
196
+ await maintenanceTask.run(label);
197
+ } catch {
198
+ // Task state stores the controlled error for the UI.
199
+ }
200
+ syncTaskState(state, maintenanceTask);
201
+ session.update();
202
+ }
203
+
204
+ function cancelMaintenance() {
205
+ state.taskJob = "stale scan";
206
+ void maintenanceTask.run("scan");
207
+ syncTaskState(state, maintenanceTask);
208
+ maintenanceTask.cancel();
209
+ syncTaskState(state, maintenanceTask);
210
+ session.update();
211
+ }
212
+
213
+ session = mountTerminal(<App state={state} />, {
214
+ ...options,
215
+ cols: options.cols ?? 96,
216
+ rows: options.rows ?? 22,
217
+ keymap: {
218
+ ...options.keymap,
219
+ bindings: [
220
+ ...(options.keymap?.bindings || []),
221
+ { key: "r", command: { id: "api.refresh" }, scope: "global" },
222
+ { key: "R", command: { id: "api.refresh" }, scope: "global" },
223
+ { key: "e", command: { id: "api.error" }, scope: "global" },
224
+ { key: "E", command: { id: "api.error" }, scope: "global" },
225
+ { key: "i", command: { id: "api.invalidate" }, scope: "global" },
226
+ { key: "I", command: { id: "api.invalidate" }, scope: "global" },
227
+ { key: "t", command: { id: "task.run" }, scope: "global" },
228
+ { key: "T", command: { id: "task.run" }, scope: "global" },
229
+ { key: "c", command: { id: "task.cancel" }, scope: "global" },
230
+ { key: "C", command: { id: "task.cancel" }, scope: "global" },
231
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
232
+ ],
233
+ onCommand(command, context) {
234
+ if (command.id === "api.refresh") {
235
+ void refresh();
236
+ return true;
237
+ }
238
+ if (command.id === "api.error") {
239
+ state.errorMode = true;
240
+ state.apiMessage = "Next refresh will use controlled error";
241
+ session.update();
242
+ return true;
243
+ }
244
+ if (command.id === "api.invalidate") {
245
+ invalidate();
246
+ return true;
247
+ }
248
+ if (command.id === "task.run") {
249
+ void runMaintenance("maintenance", "maintenance");
250
+ return true;
251
+ }
252
+ if (command.id === "task.cancel") {
253
+ cancelMaintenance();
254
+ return true;
255
+ }
256
+ if (command.id === "quit") {
257
+ quit();
258
+ return true;
259
+ }
260
+ return options.keymap?.onCommand?.(command, context);
261
+ }
262
+ }
263
+ });
264
+
265
+ return {
266
+ session,
267
+ dispatchKey(key: string) {
268
+ return session.dispatchKey(key);
269
+ },
270
+ output() {
271
+ return session.output();
272
+ },
273
+ ansiOutput() {
274
+ return session.ansiOutput();
275
+ },
276
+ isRunning() {
277
+ return state.running;
278
+ },
279
+ destroy() {
280
+ state.running = false;
281
+ offTaskState();
282
+ client.clear();
283
+ maintenanceTask.reset();
284
+ session.destroy();
285
+ }
286
+ };
287
+ }
288
+
289
+ if (import.meta.main) {
290
+ if (shouldRunSnapshot()) {
291
+ const demo = createModuleApiDashboardDemo({ runtime: "headless", cols: 96, rows: 22 });
292
+ demo.dispatchKey("R");
293
+ await new Promise((resolve) => setTimeout(resolve, 0));
294
+ demo.dispatchKey("I");
295
+ demo.dispatchKey("E");
296
+ demo.dispatchKey("R");
297
+ await new Promise((resolve) => setTimeout(resolve, 0));
298
+ demo.dispatchKey("T");
299
+ await new Promise((resolve) => setTimeout(resolve, 0));
300
+ demo.dispatchKey("C");
301
+ process.stdout.write(demo.output());
302
+ process.stdout.write("\n");
303
+ demo.destroy();
304
+ } else {
305
+ createModuleApiDashboardDemo();
306
+ }
307
+ }