@valyrianjs/terminal 0.2.0 → 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.
- package/dist/ansi.d.ts +2 -0
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +23 -13
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +10 -2
- package/dist/events.js.map +1 -1
- package/dist/frame-style.d.ts +7 -0
- package/dist/frame-style.d.ts.map +1 -0
- package/dist/frame-style.js +27 -0
- package/dist/frame-style.js.map +1 -0
- package/dist/keymap.d.ts.map +1 -1
- package/dist/keymap.js +4 -2
- package/dist/keymap.js.map +1 -1
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +55 -24
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts +6 -0
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +38 -17
- package/dist/mouse.js.map +1 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +8 -1
- package/dist/primitives.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +266 -70
- package/dist/render.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +13 -5
- package/dist/runtime.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +325 -83
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts +7 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +114 -0
- package/dist/text.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +3 -0
- package/dist/theme.js.map +1 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +18 -4
- package/dist/tree.js.map +1 -1
- package/dist/types.d.ts +41 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +18 -8
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +10 -8
- package/docs/primitive-gallery.md +9 -5
- package/examples/basic.tsx +22 -0
- package/examples/cli.tsx +55 -0
- package/examples/demo.tsx +98 -0
- package/examples/docs/background-fill.tsx +107 -0
- package/examples/docs/component-composition.tsx +140 -0
- package/examples/docs/cursor.tsx +121 -0
- package/examples/docs/employees-list.tsx +138 -0
- package/examples/docs/hello.tsx +98 -0
- package/examples/docs/interactive-note.tsx +111 -0
- package/examples/docs/module-api-dashboard.tsx +307 -0
- package/examples/docs/module-flux-store.tsx +181 -0
- package/examples/docs/module-form-workflow.tsx +339 -0
- package/examples/docs/module-forms.tsx +218 -0
- package/examples/docs/module-money.tsx +175 -0
- package/examples/docs/module-native-store.tsx +188 -0
- package/examples/docs/module-pulses.tsx +142 -0
- package/examples/docs/module-query.tsx +209 -0
- package/examples/docs/module-request.tsx +194 -0
- package/examples/docs/module-state-workbench.tsx +283 -0
- package/examples/docs/module-tasks.tsx +223 -0
- package/examples/docs/module-translate.tsx +194 -0
- package/examples/docs/module-utils.tsx +168 -0
- package/examples/docs/module-valyrian-core.tsx +159 -0
- package/examples/docs/pizza-builder.tsx +463 -0
- package/examples/docs/primitive-activity-console.tsx +113 -0
- package/examples/docs/primitive-command-panel.tsx +186 -0
- package/examples/docs/primitive-data-explorer.tsx +155 -0
- package/examples/docs/primitive-input-workbench.tsx +128 -0
- package/examples/docs/primitive-layout-shell.tsx +115 -0
- package/examples/docs/responsive-split.tsx +186 -0
- package/examples/docs/style-system.tsx +209 -0
- package/examples/docs/theme-colors.tsx +225 -0
- package/examples/docs/virtualized-list-workbench.tsx +232 -0
- package/examples/opencode-dogfood-app.tsx +215 -0
- package/examples/opencode-dogfood-lifecycle.tsx +194 -0
- package/examples/opencode-dogfood.tsx +11 -0
- package/llms-full.txt +38 -22
- package/package.json +3 -2
- package/src/ansi.ts +23 -13
- package/src/events.ts +6 -2
- package/src/frame-style.ts +36 -0
- package/src/keymap.ts +4 -2
- package/src/layout.ts +59 -25
- package/src/mouse.ts +41 -16
- package/src/primitives.ts +8 -1
- package/src/render.ts +286 -71
- package/src/runtime.ts +13 -5
- package/src/session.ts +343 -79
- package/src/text.ts +148 -0
- package/src/theme.ts +3 -0
- package/src/tree.ts +19 -4
- package/src/types.ts +48 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { FluxStore } from "valyrian.js/flux-store";
|
|
2
|
+
import { Pane, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
|
|
3
|
+
import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
|
|
4
|
+
|
|
5
|
+
interface ModuleDemo {
|
|
6
|
+
session: TerminalSession;
|
|
7
|
+
dispatchKey(key: string): string;
|
|
8
|
+
output(): string;
|
|
9
|
+
ansiOutput(): string;
|
|
10
|
+
isRunning(): boolean;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type JobStatus = "queued" | "running" | "done" | "blocked";
|
|
15
|
+
type Job = { name: string; status: JobStatus };
|
|
16
|
+
type QueueState = { selected: number; jobs: Job[] };
|
|
17
|
+
|
|
18
|
+
type QueueGetters = { doneCount: number; activeJob: string };
|
|
19
|
+
type QueueStore = FluxStore & { state: QueueState; getters: QueueGetters };
|
|
20
|
+
|
|
21
|
+
const INITIAL_JOBS: Job[] = [
|
|
22
|
+
{ name: "Sync warehouse feed", status: "queued" },
|
|
23
|
+
{ name: "Package release", status: "queued" },
|
|
24
|
+
{ name: "Review failed payouts", status: "blocked" }
|
|
25
|
+
];
|
|
26
|
+
const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
|
|
27
|
+
const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
|
|
28
|
+
|
|
29
|
+
function shouldRunSnapshot() {
|
|
30
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createQueueStore(): QueueStore {
|
|
34
|
+
return new FluxStore({
|
|
35
|
+
state: { selected: 0, jobs: INITIAL_JOBS.map((job) => ({ ...job })) },
|
|
36
|
+
mutations: {
|
|
37
|
+
SELECT_NEXT(state) {
|
|
38
|
+
const queueState = state as QueueState;
|
|
39
|
+
queueState.selected = (queueState.selected + 1) % queueState.jobs.length;
|
|
40
|
+
},
|
|
41
|
+
SELECT_PREVIOUS(state) {
|
|
42
|
+
const queueState = state as QueueState;
|
|
43
|
+
queueState.selected = (queueState.selected - 1 + queueState.jobs.length) % queueState.jobs.length;
|
|
44
|
+
},
|
|
45
|
+
SET_STATUS(state, status: JobStatus) {
|
|
46
|
+
const queueState = state as QueueState;
|
|
47
|
+
queueState.jobs = queueState.jobs.map((job, index) => (index === queueState.selected ? { ...job, status } : job));
|
|
48
|
+
},
|
|
49
|
+
RESET(state) {
|
|
50
|
+
const queueState = state as QueueState;
|
|
51
|
+
queueState.selected = 0;
|
|
52
|
+
queueState.jobs = INITIAL_JOBS.map((job) => ({ ...job }));
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
getters: {
|
|
56
|
+
doneCount(state) {
|
|
57
|
+
const queueState = state as QueueState;
|
|
58
|
+
return queueState.jobs.filter((job) => job.status === "done").length;
|
|
59
|
+
},
|
|
60
|
+
activeJob(state) {
|
|
61
|
+
const queueState = state as QueueState;
|
|
62
|
+
return queueState.jobs[queueState.selected].name;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}) as QueueStore;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function App({ store }: { store: QueueStore }) {
|
|
69
|
+
return (
|
|
70
|
+
<Screen title="Operations Queue Manager">
|
|
71
|
+
<Text style={{ color: "#ffffff", background: "#020617" }}>Operations Queue Manager</Text>
|
|
72
|
+
<Pane style={PANEL_STYLE}>
|
|
73
|
+
<Text>Queue Manager</Text>
|
|
74
|
+
{store.state.jobs.map((job, index) => (
|
|
75
|
+
<Text>{`${index === store.state.selected ? ">" : " "} ${job.name} — ${job.status}`}</Text>
|
|
76
|
+
))}
|
|
77
|
+
<Text>{`Selected: ${store.getters.activeJob}`}</Text>
|
|
78
|
+
<Text>{`Done jobs: ${store.getters.doneCount}`}</Text>
|
|
79
|
+
</Pane>
|
|
80
|
+
<Text style={FOOTER_STYLE}>J/K select S start D mark done B block R reset Ctrl+C: quit</Text>
|
|
81
|
+
</Screen>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createModuleFluxStoreDemo(options: TerminalMountOptions = {}): ModuleDemo {
|
|
86
|
+
const store = createQueueStore();
|
|
87
|
+
let running = true;
|
|
88
|
+
let session: TerminalSession;
|
|
89
|
+
|
|
90
|
+
function quit() {
|
|
91
|
+
running = false;
|
|
92
|
+
session.destroy();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
session = mountTerminal(<App store={store} />, {
|
|
96
|
+
...options,
|
|
97
|
+
cols: options.cols ?? 92,
|
|
98
|
+
rows: options.rows ?? 18,
|
|
99
|
+
keymap: {
|
|
100
|
+
...options.keymap,
|
|
101
|
+
bindings: [
|
|
102
|
+
...(options.keymap?.bindings || []),
|
|
103
|
+
{ key: "j", command: { id: "queue.next" }, scope: "global" },
|
|
104
|
+
{ key: "J", command: { id: "queue.next" }, scope: "global" },
|
|
105
|
+
{ key: "k", command: { id: "queue.previous" }, scope: "global" },
|
|
106
|
+
{ key: "K", command: { id: "queue.previous" }, scope: "global" },
|
|
107
|
+
{ key: "s", command: { id: "queue.start" }, scope: "global" },
|
|
108
|
+
{ key: "S", command: { id: "queue.start" }, scope: "global" },
|
|
109
|
+
{ key: "d", command: { id: "queue.done" }, scope: "global" },
|
|
110
|
+
{ key: "D", command: { id: "queue.done" }, scope: "global" },
|
|
111
|
+
{ key: "b", command: { id: "queue.block" }, scope: "global" },
|
|
112
|
+
{ key: "B", command: { id: "queue.block" }, scope: "global" },
|
|
113
|
+
{ key: "r", command: { id: "queue.reset" }, scope: "global" },
|
|
114
|
+
{ key: "R", command: { id: "queue.reset" }, scope: "global" },
|
|
115
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
116
|
+
],
|
|
117
|
+
onCommand(command, context) {
|
|
118
|
+
if (command.id === "queue.next") {
|
|
119
|
+
store.commit("SELECT_NEXT");
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (command.id === "queue.previous") {
|
|
123
|
+
store.commit("SELECT_PREVIOUS");
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (command.id === "queue.start") {
|
|
127
|
+
store.commit("SET_STATUS", "running");
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if (command.id === "queue.done") {
|
|
131
|
+
store.commit("SET_STATUS", "done");
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (command.id === "queue.block") {
|
|
135
|
+
store.commit("SET_STATUS", "blocked");
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (command.id === "queue.reset") {
|
|
139
|
+
store.commit("RESET");
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
if (command.id === "quit") {
|
|
143
|
+
quit();
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return options.keymap?.onCommand?.(command, context);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
session,
|
|
153
|
+
dispatchKey(key: string) {
|
|
154
|
+
return session.dispatchKey(key);
|
|
155
|
+
},
|
|
156
|
+
output() {
|
|
157
|
+
return session.output();
|
|
158
|
+
},
|
|
159
|
+
ansiOutput() {
|
|
160
|
+
return session.ansiOutput();
|
|
161
|
+
},
|
|
162
|
+
isRunning() {
|
|
163
|
+
return running;
|
|
164
|
+
},
|
|
165
|
+
destroy() {
|
|
166
|
+
running = false;
|
|
167
|
+
session.destroy();
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (import.meta.main) {
|
|
173
|
+
if (shouldRunSnapshot()) {
|
|
174
|
+
const demo = createModuleFluxStoreDemo({ runtime: "headless", cols: 92, rows: 18 });
|
|
175
|
+
process.stdout.write(demo.output());
|
|
176
|
+
process.stdout.write("\n");
|
|
177
|
+
demo.destroy();
|
|
178
|
+
} else {
|
|
179
|
+
createModuleFluxStoreDemo();
|
|
180
|
+
}
|
|
181
|
+
}
|