@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.
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +12 -14
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -0
- 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/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +53 -23
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +8 -1
- package/dist/mouse.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +87 -48
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -0
- 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/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +6 -3
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +5 -5
- package/docs/primitive-gallery.md +4 -4
- 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 +16 -13
- package/package.json +3 -2
- package/src/ansi.ts +12 -14
- package/src/events.ts +2 -0
- package/src/frame-style.ts +36 -0
- package/src/layout.ts +57 -24
- package/src/mouse.ts +10 -1
- package/src/render.ts +92 -48
- package/src/session.ts +2 -0
- package/src/text.ts +148 -0
- package/src/types.ts +3 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createPulse } from "valyrian.js/pulses";
|
|
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 ShiftPulses = {
|
|
15
|
+
open: () => number;
|
|
16
|
+
resolved: () => number;
|
|
17
|
+
priority: () => boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const INITIAL_OPEN = 4;
|
|
21
|
+
const INITIAL_RESOLVED = 1;
|
|
22
|
+
const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
|
|
23
|
+
const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
|
|
24
|
+
|
|
25
|
+
function shouldRunSnapshot() {
|
|
26
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function App({ pulses }: { pulses: ShiftPulses }) {
|
|
30
|
+
const open = pulses.open();
|
|
31
|
+
const resolved = pulses.resolved();
|
|
32
|
+
const priority = pulses.priority();
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Screen title="Live Shift/Ticket Counter">
|
|
36
|
+
<Text style={{ color: "#ffffff", background: "#020617" }}>Live Shift/Ticket Counter</Text>
|
|
37
|
+
<Pane style={PANEL_STYLE}>
|
|
38
|
+
<Text>Shift/Ticket Counter</Text>
|
|
39
|
+
<Text>{`Open tickets: ${open}`}</Text>
|
|
40
|
+
<Text>{`Resolved this shift: ${resolved}`}</Text>
|
|
41
|
+
<Text>{`Priority lane: ${priority ? "active" : "normal"}`}</Text>
|
|
42
|
+
<Text>{`Workload: ${open > 5 || priority ? "watch closely" : "steady"}`}</Text>
|
|
43
|
+
</Pane>
|
|
44
|
+
<Text style={FOOTER_STYLE}>A add ticket D resolve ticket P priority lane R reset Ctrl+C: quit</Text>
|
|
45
|
+
</Screen>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createModulePulsesDemo(options: TerminalMountOptions = {}): ModuleDemo {
|
|
50
|
+
const [openTickets, setOpenTickets] = createPulse(INITIAL_OPEN);
|
|
51
|
+
const [resolvedTickets, setResolvedTickets] = createPulse(INITIAL_RESOLVED);
|
|
52
|
+
const [priorityActive, setPriorityActive] = createPulse(false);
|
|
53
|
+
let running = true;
|
|
54
|
+
let session: TerminalSession;
|
|
55
|
+
|
|
56
|
+
function quit() {
|
|
57
|
+
running = false;
|
|
58
|
+
session.destroy();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function reset() {
|
|
62
|
+
setOpenTickets(INITIAL_OPEN);
|
|
63
|
+
setResolvedTickets(INITIAL_RESOLVED);
|
|
64
|
+
setPriorityActive(false);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
session = mountTerminal(<App pulses={{ open: openTickets, resolved: resolvedTickets, priority: priorityActive }} />, {
|
|
68
|
+
...options,
|
|
69
|
+
cols: options.cols ?? 92,
|
|
70
|
+
rows: options.rows ?? 16,
|
|
71
|
+
keymap: {
|
|
72
|
+
...options.keymap,
|
|
73
|
+
bindings: [
|
|
74
|
+
...(options.keymap?.bindings || []),
|
|
75
|
+
{ key: "a", command: { id: "tickets.add" }, scope: "global" },
|
|
76
|
+
{ key: "A", command: { id: "tickets.add" }, scope: "global" },
|
|
77
|
+
{ key: "d", command: { id: "tickets.resolve" }, scope: "global" },
|
|
78
|
+
{ key: "D", command: { id: "tickets.resolve" }, scope: "global" },
|
|
79
|
+
{ key: "p", command: { id: "tickets.priority" }, scope: "global" },
|
|
80
|
+
{ key: "P", command: { id: "tickets.priority" }, scope: "global" },
|
|
81
|
+
{ key: "r", command: { id: "tickets.reset" }, scope: "global" },
|
|
82
|
+
{ key: "R", command: { id: "tickets.reset" }, scope: "global" },
|
|
83
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
84
|
+
],
|
|
85
|
+
onCommand(command, context) {
|
|
86
|
+
if (command.id === "tickets.add") {
|
|
87
|
+
setOpenTickets((value) => value + 1);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
if (command.id === "tickets.resolve") {
|
|
91
|
+
setOpenTickets((value) => Math.max(0, value - 1));
|
|
92
|
+
setResolvedTickets((value) => value + 1);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
if (command.id === "tickets.priority") {
|
|
96
|
+
setPriorityActive((value) => !value);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
if (command.id === "tickets.reset") {
|
|
100
|
+
reset();
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (command.id === "quit") {
|
|
104
|
+
quit();
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return options.keymap?.onCommand?.(command, context);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
session,
|
|
114
|
+
dispatchKey(key: string) {
|
|
115
|
+
return session.dispatchKey(key);
|
|
116
|
+
},
|
|
117
|
+
output() {
|
|
118
|
+
return session.output();
|
|
119
|
+
},
|
|
120
|
+
ansiOutput() {
|
|
121
|
+
return session.ansiOutput();
|
|
122
|
+
},
|
|
123
|
+
isRunning() {
|
|
124
|
+
return running;
|
|
125
|
+
},
|
|
126
|
+
destroy() {
|
|
127
|
+
running = false;
|
|
128
|
+
session.destroy();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (import.meta.main) {
|
|
134
|
+
if (shouldRunSnapshot()) {
|
|
135
|
+
const demo = createModulePulsesDemo({ runtime: "headless", cols: 92, rows: 16 });
|
|
136
|
+
process.stdout.write(demo.output());
|
|
137
|
+
process.stdout.write("\n");
|
|
138
|
+
demo.destroy();
|
|
139
|
+
} else {
|
|
140
|
+
createModulePulsesDemo();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { QueryClient } from "valyrian.js/query";
|
|
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
|
+
interface InventoryItem {
|
|
15
|
+
sku: string;
|
|
16
|
+
name: string;
|
|
17
|
+
category: string;
|
|
18
|
+
stock: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface InventoryResult {
|
|
22
|
+
readLabel: string;
|
|
23
|
+
items: InventoryItem[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface QueryState {
|
|
27
|
+
value: string;
|
|
28
|
+
status: string;
|
|
29
|
+
invalidated: boolean;
|
|
30
|
+
fetchCount: number;
|
|
31
|
+
selectedIndex: number;
|
|
32
|
+
items: InventoryItem[];
|
|
33
|
+
running: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
|
|
37
|
+
const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
|
|
38
|
+
|
|
39
|
+
const INVENTORY: InventoryItem[] = [
|
|
40
|
+
{ sku: "OPS-100", name: "Battery pack", category: "Operations", stock: 12 },
|
|
41
|
+
{ sku: "NET-210", name: "Switch kit", category: "Network", stock: 4 },
|
|
42
|
+
{ sku: "SEC-404", name: "Seal tags", category: "Security", stock: 30 }
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function shouldRunSnapshot() {
|
|
46
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function selectedItem(state: QueryState) {
|
|
50
|
+
return state.items[state.selectedIndex] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function App({ state }: { state: QueryState }) {
|
|
54
|
+
const item = selectedItem(state);
|
|
55
|
+
return (
|
|
56
|
+
<Screen title="Cached Inventory Browser">
|
|
57
|
+
<Text style={{ color: "#ffffff", background: "#020617" }}>Cached Inventory Browser</Text>
|
|
58
|
+
<Pane style={PANEL_STYLE}>
|
|
59
|
+
<Text>Inventory Browser: fetches local stock data through a Valyrian query cache.</Text>
|
|
60
|
+
<Text>{`Query status: ${state.status}`}</Text>
|
|
61
|
+
<Text>{`Cached value: ${state.value}`}</Text>
|
|
62
|
+
<Text>{`Invalidated: ${state.invalidated ? "yes" : "no"}`}</Text>
|
|
63
|
+
<Text>{`Fetcher calls: ${state.fetchCount}`}</Text>
|
|
64
|
+
<Text>{`Selected item: ${item ? `${item.name} (${item.sku})` : "none"}`}</Text>
|
|
65
|
+
<Text>{`Category: ${item ? item.category : "none"}`}</Text>
|
|
66
|
+
<Text>{`Stock on hand: ${item ? item.stock : 0}`}</Text>
|
|
67
|
+
</Pane>
|
|
68
|
+
<Text style={FOOTER_STYLE}>F: fetch inventory I: invalidate cache J/K: select item Ctrl+C: quit</Text>
|
|
69
|
+
</Screen>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createModuleQueryDemo(options: TerminalMountOptions = {}): ModuleDemo {
|
|
74
|
+
const state: QueryState = {
|
|
75
|
+
value: "none",
|
|
76
|
+
status: "idle",
|
|
77
|
+
invalidated: false,
|
|
78
|
+
fetchCount: 0,
|
|
79
|
+
selectedIndex: 0,
|
|
80
|
+
items: [],
|
|
81
|
+
running: true
|
|
82
|
+
};
|
|
83
|
+
const client = new QueryClient({ staleTime: 30000, cacheTime: 1000 });
|
|
84
|
+
const query = client.query({
|
|
85
|
+
key: ["docs", "inventory"],
|
|
86
|
+
fetcher: (): InventoryResult => {
|
|
87
|
+
state.fetchCount += 1;
|
|
88
|
+
return {
|
|
89
|
+
readLabel: `terminal read #${state.fetchCount}`,
|
|
90
|
+
items: INVENTORY.map((item) => ({ ...item }))
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
let session: TerminalSession;
|
|
95
|
+
|
|
96
|
+
async function fetchInventory() {
|
|
97
|
+
state.status = "loading";
|
|
98
|
+
session.update();
|
|
99
|
+
const data = await query.fetch();
|
|
100
|
+
state.value = data?.readLabel || "none";
|
|
101
|
+
state.items = data?.items || [];
|
|
102
|
+
state.selectedIndex = Math.min(state.selectedIndex, Math.max(state.items.length - 1, 0));
|
|
103
|
+
state.status = query.state.status;
|
|
104
|
+
session.update();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function invalidateInventory() {
|
|
108
|
+
query.invalidate();
|
|
109
|
+
state.status = query.state.status;
|
|
110
|
+
state.invalidated = true;
|
|
111
|
+
session.update();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function moveSelection(delta: number) {
|
|
115
|
+
if (state.items.length === 0) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
state.selectedIndex = (state.selectedIndex + delta + state.items.length) % state.items.length;
|
|
119
|
+
session.update();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function quit() {
|
|
123
|
+
state.running = false;
|
|
124
|
+
client.clear();
|
|
125
|
+
session.destroy();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
session = mountTerminal(<App state={state} />, {
|
|
129
|
+
...options,
|
|
130
|
+
cols: options.cols ?? 92,
|
|
131
|
+
rows: options.rows ?? 16,
|
|
132
|
+
keymap: {
|
|
133
|
+
...options.keymap,
|
|
134
|
+
bindings: [
|
|
135
|
+
...(options.keymap?.bindings || []),
|
|
136
|
+
{ key: "f", command: { id: "query.fetch" }, scope: "global" },
|
|
137
|
+
{ key: "F", command: { id: "query.fetch" }, scope: "global" },
|
|
138
|
+
{ key: "i", command: { id: "query.invalidate" }, scope: "global" },
|
|
139
|
+
{ key: "I", command: { id: "query.invalidate" }, scope: "global" },
|
|
140
|
+
{ key: "j", command: { id: "inventory.next" }, scope: "global" },
|
|
141
|
+
{ key: "J", command: { id: "inventory.next" }, scope: "global" },
|
|
142
|
+
{ key: "k", command: { id: "inventory.previous" }, scope: "global" },
|
|
143
|
+
{ key: "K", command: { id: "inventory.previous" }, scope: "global" },
|
|
144
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
145
|
+
],
|
|
146
|
+
onCommand(command, context) {
|
|
147
|
+
if (command.id === "query.fetch") {
|
|
148
|
+
void fetchInventory();
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
if (command.id === "query.invalidate") {
|
|
152
|
+
invalidateInventory();
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (command.id === "inventory.next") {
|
|
156
|
+
moveSelection(1);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
if (command.id === "inventory.previous") {
|
|
160
|
+
moveSelection(-1);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
if (command.id === "quit") {
|
|
164
|
+
quit();
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
return options.keymap?.onCommand?.(command, context);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
session,
|
|
174
|
+
dispatchKey(key: string) {
|
|
175
|
+
return session.dispatchKey(key);
|
|
176
|
+
},
|
|
177
|
+
output() {
|
|
178
|
+
return session.output();
|
|
179
|
+
},
|
|
180
|
+
ansiOutput() {
|
|
181
|
+
return session.ansiOutput();
|
|
182
|
+
},
|
|
183
|
+
isRunning() {
|
|
184
|
+
return state.running;
|
|
185
|
+
},
|
|
186
|
+
destroy() {
|
|
187
|
+
state.running = false;
|
|
188
|
+
client.clear();
|
|
189
|
+
session.destroy();
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (import.meta.main) {
|
|
195
|
+
if (shouldRunSnapshot()) {
|
|
196
|
+
const demo = createModuleQueryDemo({ runtime: "headless", cols: 92, rows: 16 });
|
|
197
|
+
demo.dispatchKey("F");
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
199
|
+
demo.dispatchKey("J");
|
|
200
|
+
demo.dispatchKey("I");
|
|
201
|
+
demo.dispatchKey("F");
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
203
|
+
process.stdout.write(demo.output());
|
|
204
|
+
process.stdout.write("\n");
|
|
205
|
+
demo.destroy();
|
|
206
|
+
} else {
|
|
207
|
+
createModuleQueryDemo();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { request } from "valyrian.js/request";
|
|
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
|
+
interface RequestState {
|
|
15
|
+
status: "idle" | "loading" | "success" | "error";
|
|
16
|
+
message: string;
|
|
17
|
+
fetchedPath: string;
|
|
18
|
+
lastError: string;
|
|
19
|
+
failNext: boolean;
|
|
20
|
+
history: Array<"success" | "error">;
|
|
21
|
+
running: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
|
|
25
|
+
const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
|
|
26
|
+
|
|
27
|
+
function shouldRunSnapshot() {
|
|
28
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createState(): RequestState {
|
|
32
|
+
return {
|
|
33
|
+
status: "idle",
|
|
34
|
+
message: "Ready to check the service status",
|
|
35
|
+
fetchedPath: "none",
|
|
36
|
+
lastError: "none",
|
|
37
|
+
failNext: false,
|
|
38
|
+
history: [],
|
|
39
|
+
running: true
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function historySummary(history: RequestState["history"]) {
|
|
44
|
+
return history.length === 0 ? "none" : history.join(" -> ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const api = request.new("http://terminal.local", { allowedMethods: ["get"] });
|
|
48
|
+
|
|
49
|
+
async function withFakeFetch<T>(shouldFail: boolean, run: () => Promise<T>) {
|
|
50
|
+
const originalFetch = globalThis.fetch;
|
|
51
|
+
globalThis.fetch = ((input: RequestInfo | URL) => {
|
|
52
|
+
const url = new URL(String(input));
|
|
53
|
+
if (shouldFail) {
|
|
54
|
+
return Promise.resolve(
|
|
55
|
+
new Response(JSON.stringify({ message: "controlled local failure" }), { status: 503 })
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return Promise.resolve(
|
|
59
|
+
new Response(JSON.stringify({ status: "ok", path: `${url.pathname}${url.search}` }), { status: 200 })
|
|
60
|
+
);
|
|
61
|
+
}) as typeof fetch;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
return await run();
|
|
65
|
+
} finally {
|
|
66
|
+
globalThis.fetch = originalFetch;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function App({ state }: { state: RequestState }) {
|
|
71
|
+
return (
|
|
72
|
+
<Screen title="Service Status Client">
|
|
73
|
+
<Text style={{ color: "#ffffff", background: "#020617" }}>Service Status Client</Text>
|
|
74
|
+
<Pane style={PANEL_STYLE}>
|
|
75
|
+
<Text>Status Client: checks a local service endpoint without using the network.</Text>
|
|
76
|
+
<Text>{`Request status: ${state.status}`}</Text>
|
|
77
|
+
<Text>{state.message}</Text>
|
|
78
|
+
<Text>{`Fetched: ${state.fetchedPath}`}</Text>
|
|
79
|
+
<Text>{`Last error: ${state.lastError}`}</Text>
|
|
80
|
+
<Text>{`Next request: ${state.failNext ? "controlled error" : "success"}`}</Text>
|
|
81
|
+
<Text>{`History: ${historySummary(state.history)}`}</Text>
|
|
82
|
+
</Pane>
|
|
83
|
+
<Text style={FOOTER_STYLE}>R: refresh status E: force next error Ctrl+C: quit</Text>
|
|
84
|
+
</Screen>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createModuleRequestDemo(options: TerminalMountOptions = {}): ModuleDemo {
|
|
89
|
+
const state = createState();
|
|
90
|
+
let session: TerminalSession;
|
|
91
|
+
|
|
92
|
+
async function loadStatus() {
|
|
93
|
+
if (state.status === "loading") {
|
|
94
|
+
state.message = "Refresh already in progress";
|
|
95
|
+
session.update();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
state.status = "loading";
|
|
100
|
+
state.message = "Loading local service status";
|
|
101
|
+
session.update();
|
|
102
|
+
try {
|
|
103
|
+
const shouldFail = state.failNext;
|
|
104
|
+
state.failNext = false;
|
|
105
|
+
const body = (await withFakeFetch(shouldFail, () => api.get("/status", { page: 1 }))) as { status: string; path: string };
|
|
106
|
+
state.status = "success";
|
|
107
|
+
state.history.push("success");
|
|
108
|
+
state.message = state.lastError === "none" ? `HTTP status: ${body.status}` : `Retry result: ${body.status}`;
|
|
109
|
+
state.fetchedPath = body.path;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
state.status = "error";
|
|
112
|
+
state.history.push("error");
|
|
113
|
+
state.message = error instanceof Error ? error.message : "Request failed";
|
|
114
|
+
state.lastError = state.message;
|
|
115
|
+
state.fetchedPath = "/status?page=1";
|
|
116
|
+
}
|
|
117
|
+
session.update();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function quit() {
|
|
121
|
+
state.running = false;
|
|
122
|
+
session.destroy();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
session = mountTerminal(<App state={state} />, {
|
|
126
|
+
...options,
|
|
127
|
+
cols: options.cols ?? 92,
|
|
128
|
+
rows: options.rows ?? 16,
|
|
129
|
+
keymap: {
|
|
130
|
+
...options.keymap,
|
|
131
|
+
bindings: [
|
|
132
|
+
...(options.keymap?.bindings || []),
|
|
133
|
+
{ key: "r", command: { id: "request.load" }, scope: "global" },
|
|
134
|
+
{ key: "R", command: { id: "request.load" }, scope: "global" },
|
|
135
|
+
{ key: "e", command: { id: "request.failNext" }, scope: "global" },
|
|
136
|
+
{ key: "E", command: { id: "request.failNext" }, scope: "global" },
|
|
137
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
138
|
+
],
|
|
139
|
+
onCommand(command, context) {
|
|
140
|
+
if (command.id === "request.load") {
|
|
141
|
+
void loadStatus();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
if (command.id === "request.failNext") {
|
|
145
|
+
state.failNext = true;
|
|
146
|
+
state.message = "Next refresh will use the controlled error path";
|
|
147
|
+
session.update();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (command.id === "quit") {
|
|
151
|
+
quit();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return options.keymap?.onCommand?.(command, context);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
session,
|
|
161
|
+
dispatchKey(key: string) {
|
|
162
|
+
return session.dispatchKey(key);
|
|
163
|
+
},
|
|
164
|
+
output() {
|
|
165
|
+
return session.output();
|
|
166
|
+
},
|
|
167
|
+
ansiOutput() {
|
|
168
|
+
return session.ansiOutput();
|
|
169
|
+
},
|
|
170
|
+
isRunning() {
|
|
171
|
+
return state.running;
|
|
172
|
+
},
|
|
173
|
+
destroy() {
|
|
174
|
+
state.running = false;
|
|
175
|
+
session.destroy();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (import.meta.main) {
|
|
181
|
+
if (shouldRunSnapshot()) {
|
|
182
|
+
const demo = createModuleRequestDemo({ runtime: "headless", cols: 92, rows: 16 });
|
|
183
|
+
demo.dispatchKey("E");
|
|
184
|
+
demo.dispatchKey("R");
|
|
185
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
186
|
+
demo.dispatchKey("R");
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
188
|
+
process.stdout.write(demo.output());
|
|
189
|
+
process.stdout.write("\n");
|
|
190
|
+
demo.destroy();
|
|
191
|
+
} else {
|
|
192
|
+
createModuleRequestDemo();
|
|
193
|
+
}
|
|
194
|
+
}
|