@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.
Files changed (102) hide show
  1. package/dist/ansi.d.ts +2 -0
  2. package/dist/ansi.d.ts.map +1 -1
  3. package/dist/ansi.js +23 -13
  4. package/dist/ansi.js.map +1 -1
  5. package/dist/events.d.ts.map +1 -1
  6. package/dist/events.js +10 -2
  7. package/dist/events.js.map +1 -1
  8. package/dist/frame-style.d.ts +7 -0
  9. package/dist/frame-style.d.ts.map +1 -0
  10. package/dist/frame-style.js +27 -0
  11. package/dist/frame-style.js.map +1 -0
  12. package/dist/keymap.d.ts.map +1 -1
  13. package/dist/keymap.js +4 -2
  14. package/dist/keymap.js.map +1 -1
  15. package/dist/layout.d.ts +5 -1
  16. package/dist/layout.d.ts.map +1 -1
  17. package/dist/layout.js +55 -24
  18. package/dist/layout.js.map +1 -1
  19. package/dist/mouse.d.ts +6 -0
  20. package/dist/mouse.d.ts.map +1 -1
  21. package/dist/mouse.js +38 -17
  22. package/dist/mouse.js.map +1 -1
  23. package/dist/primitives.d.ts.map +1 -1
  24. package/dist/primitives.js +8 -1
  25. package/dist/primitives.js.map +1 -1
  26. package/dist/render.d.ts.map +1 -1
  27. package/dist/render.js +266 -70
  28. package/dist/render.js.map +1 -1
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +13 -5
  31. package/dist/runtime.js.map +1 -1
  32. package/dist/session.d.ts.map +1 -1
  33. package/dist/session.js +325 -83
  34. package/dist/session.js.map +1 -1
  35. package/dist/text.d.ts +7 -0
  36. package/dist/text.d.ts.map +1 -1
  37. package/dist/text.js +114 -0
  38. package/dist/text.js.map +1 -1
  39. package/dist/theme.d.ts.map +1 -1
  40. package/dist/theme.js +3 -0
  41. package/dist/theme.js.map +1 -1
  42. package/dist/tree.d.ts.map +1 -1
  43. package/dist/tree.js +18 -4
  44. package/dist/tree.js.map +1 -1
  45. package/dist/types.d.ts +41 -4
  46. package/dist/types.d.ts.map +1 -1
  47. package/docs/api-reference.md +18 -8
  48. package/docs/cookbook.md +1 -1
  49. package/docs/interaction-model.md +10 -8
  50. package/docs/primitive-gallery.md +9 -5
  51. package/examples/basic.tsx +22 -0
  52. package/examples/cli.tsx +55 -0
  53. package/examples/demo.tsx +98 -0
  54. package/examples/docs/background-fill.tsx +107 -0
  55. package/examples/docs/component-composition.tsx +140 -0
  56. package/examples/docs/cursor.tsx +121 -0
  57. package/examples/docs/employees-list.tsx +138 -0
  58. package/examples/docs/hello.tsx +98 -0
  59. package/examples/docs/interactive-note.tsx +111 -0
  60. package/examples/docs/module-api-dashboard.tsx +307 -0
  61. package/examples/docs/module-flux-store.tsx +181 -0
  62. package/examples/docs/module-form-workflow.tsx +339 -0
  63. package/examples/docs/module-forms.tsx +218 -0
  64. package/examples/docs/module-money.tsx +175 -0
  65. package/examples/docs/module-native-store.tsx +188 -0
  66. package/examples/docs/module-pulses.tsx +142 -0
  67. package/examples/docs/module-query.tsx +209 -0
  68. package/examples/docs/module-request.tsx +194 -0
  69. package/examples/docs/module-state-workbench.tsx +283 -0
  70. package/examples/docs/module-tasks.tsx +223 -0
  71. package/examples/docs/module-translate.tsx +194 -0
  72. package/examples/docs/module-utils.tsx +168 -0
  73. package/examples/docs/module-valyrian-core.tsx +159 -0
  74. package/examples/docs/pizza-builder.tsx +463 -0
  75. package/examples/docs/primitive-activity-console.tsx +113 -0
  76. package/examples/docs/primitive-command-panel.tsx +186 -0
  77. package/examples/docs/primitive-data-explorer.tsx +155 -0
  78. package/examples/docs/primitive-input-workbench.tsx +128 -0
  79. package/examples/docs/primitive-layout-shell.tsx +115 -0
  80. package/examples/docs/responsive-split.tsx +186 -0
  81. package/examples/docs/style-system.tsx +209 -0
  82. package/examples/docs/theme-colors.tsx +225 -0
  83. package/examples/docs/virtualized-list-workbench.tsx +232 -0
  84. package/examples/opencode-dogfood-app.tsx +215 -0
  85. package/examples/opencode-dogfood-lifecycle.tsx +194 -0
  86. package/examples/opencode-dogfood.tsx +11 -0
  87. package/llms-full.txt +38 -22
  88. package/package.json +3 -2
  89. package/src/ansi.ts +23 -13
  90. package/src/events.ts +6 -2
  91. package/src/frame-style.ts +36 -0
  92. package/src/keymap.ts +4 -2
  93. package/src/layout.ts +59 -25
  94. package/src/mouse.ts +41 -16
  95. package/src/primitives.ts +8 -1
  96. package/src/render.ts +286 -71
  97. package/src/runtime.ts +13 -5
  98. package/src/session.ts +343 -79
  99. package/src/text.ts +148 -0
  100. package/src/theme.ts +3 -0
  101. package/src/tree.ts +19 -4
  102. package/src/types.ts +48 -3
@@ -0,0 +1,223 @@
1
+ import { Task } from "valyrian.js/tasks";
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 TaskDemoState {
15
+ status: string;
16
+ result: string;
17
+ error: string;
18
+ currentJob: string;
19
+ seenStates: string[];
20
+ running: boolean;
21
+ }
22
+
23
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
24
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
25
+
26
+ function shouldRunSnapshot() {
27
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
28
+ }
29
+
30
+ function waitForControlledAsync(signal: AbortSignal) {
31
+ return new Promise<void>((resolve) => {
32
+ const timer = setTimeout(resolve, 20);
33
+ signal.addEventListener(
34
+ "abort",
35
+ () => {
36
+ clearTimeout(timer);
37
+ resolve();
38
+ },
39
+ { once: true }
40
+ );
41
+ });
42
+ }
43
+
44
+ function syncTaskState(state: TaskDemoState, task: Task<string, string>) {
45
+ const snapshot = task.state;
46
+ state.status = snapshot.status;
47
+ state.result = snapshot.result ? `Result: ${snapshot.result}` : "Result: none";
48
+ state.error = snapshot.error instanceof Error ? `Error: ${snapshot.error.message}` : "Error: none";
49
+ }
50
+
51
+ function visibleTaskStates(states: string[]) {
52
+ const compact: string[] = [];
53
+ for (const status of states) {
54
+ if ((status === "cancelled" || status === "error") && compact[compact.length - 1] === "running") {
55
+ compact.pop();
56
+ }
57
+ if (compact[compact.length - 1] !== status) {
58
+ compact.push(status);
59
+ }
60
+ }
61
+ return compact.join(" -> ");
62
+ }
63
+
64
+ export function App({ state }: { state: TaskDemoState }) {
65
+ return (
66
+ <Screen title="Background Maintenance Runner">
67
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Background Maintenance Runner</Text>
68
+ <Pane style={PANEL_STYLE}>
69
+ <Text>Maintenance Runner: runs local cleanup work through a Valyrian Task.</Text>
70
+ <Text>{`Task status: ${state.status}`}</Text>
71
+ <Text>{`Maintenance job: ${state.currentJob}`}</Text>
72
+ <Text>{state.result}</Text>
73
+ <Text>{state.error}</Text>
74
+ <Text>{`Seen states: ${visibleTaskStates(state.seenStates)}`}</Text>
75
+ </Pane>
76
+ <Text style={FOOTER_STYLE}>R: run cleanup C: cancel scan X: controlled fail Z: reset Ctrl+C: quit</Text>
77
+ </Screen>
78
+ );
79
+ }
80
+
81
+ export function createModuleTasksDemo(options: TerminalMountOptions = {}): ModuleDemo {
82
+ const state: TaskDemoState = {
83
+ status: "idle",
84
+ result: "Result: none",
85
+ error: "Error: none",
86
+ currentJob: "none",
87
+ seenStates: ["idle"],
88
+ running: true
89
+ };
90
+ const task = new Task<string, string>(async (label, { signal }) => {
91
+ if (label === "fail") {
92
+ throw new Error("controlled task failure");
93
+ }
94
+ if (label === "scan") {
95
+ await waitForControlledAsync(signal);
96
+ if (signal.aborted) return "Cancelled stale scan";
97
+ }
98
+ return `Completed ${label}`;
99
+ }, { strategy: "restartable" });
100
+ const rememberState = () => {
101
+ const status = task.state.status;
102
+ if (state.seenStates[state.seenStates.length - 1] !== status) {
103
+ state.seenStates.push(status);
104
+ }
105
+ syncTaskState(state, task);
106
+ };
107
+ const offTaskState = task.on("state", () => {
108
+ rememberState();
109
+ session?.update();
110
+ });
111
+ let session: TerminalSession;
112
+
113
+ function quit() {
114
+ state.running = false;
115
+ offTaskState();
116
+ task.reset();
117
+ session.destroy();
118
+ }
119
+
120
+ async function runTask(label: string, job: string) {
121
+ state.currentJob = job;
122
+ try {
123
+ rememberState();
124
+ await task.run(label);
125
+ } catch {
126
+ // The task state snapshot carries the controlled error message.
127
+ }
128
+ syncTaskState(state, task);
129
+ session.update();
130
+ }
131
+
132
+ session = mountTerminal(<App state={state} />, {
133
+ ...options,
134
+ cols: options.cols ?? 92,
135
+ rows: options.rows ?? 16,
136
+ keymap: {
137
+ ...options.keymap,
138
+ bindings: [
139
+ ...(options.keymap?.bindings || []),
140
+ { key: "r", command: { id: "task.run" }, scope: "global" },
141
+ { key: "R", command: { id: "task.run" }, scope: "global" },
142
+ { key: "c", command: { id: "task.cancel" }, scope: "global" },
143
+ { key: "C", command: { id: "task.cancel" }, scope: "global" },
144
+ { key: "x", command: { id: "task.error" }, scope: "global" },
145
+ { key: "X", command: { id: "task.error" }, scope: "global" },
146
+ { key: "z", command: { id: "task.reset" }, scope: "global" },
147
+ { key: "Z", command: { id: "task.reset" }, scope: "global" },
148
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
149
+ ],
150
+ onCommand(command, context) {
151
+ if (command.id === "task.run") {
152
+ void runTask("cleanup", "nightly cleanup");
153
+ return true;
154
+ }
155
+ if (command.id === "task.cancel") {
156
+ state.currentJob = "stale scan";
157
+ void task.run("scan");
158
+ rememberState();
159
+ task.cancel();
160
+ rememberState();
161
+ session.update();
162
+ return true;
163
+ }
164
+ if (command.id === "task.error") {
165
+ void runTask("fail", "permission audit");
166
+ return true;
167
+ }
168
+ if (command.id === "task.reset") {
169
+ state.currentJob = "none";
170
+ task.reset();
171
+ rememberState();
172
+ session.update();
173
+ return true;
174
+ }
175
+ if (command.id === "quit") {
176
+ quit();
177
+ return true;
178
+ }
179
+ return options.keymap?.onCommand?.(command, context);
180
+ }
181
+ }
182
+ });
183
+
184
+ return {
185
+ session,
186
+ dispatchKey(key: string) {
187
+ return session.dispatchKey(key);
188
+ },
189
+ output() {
190
+ return session.output();
191
+ },
192
+ ansiOutput() {
193
+ return session.ansiOutput();
194
+ },
195
+ isRunning() {
196
+ return state.running;
197
+ },
198
+ destroy() {
199
+ state.running = false;
200
+ offTaskState();
201
+ task.reset();
202
+ session.destroy();
203
+ }
204
+ };
205
+ }
206
+
207
+ if (import.meta.main) {
208
+ if (shouldRunSnapshot()) {
209
+ const demo = createModuleTasksDemo({ runtime: "headless", cols: 92, rows: 16 });
210
+ demo.dispatchKey("R");
211
+ await new Promise((resolve) => setTimeout(resolve, 0));
212
+ demo.dispatchKey("C");
213
+ await new Promise((resolve) => setTimeout(resolve, 0));
214
+ demo.dispatchKey("X");
215
+ await new Promise((resolve) => setTimeout(resolve, 0));
216
+ demo.dispatchKey("Z");
217
+ process.stdout.write(demo.output());
218
+ process.stdout.write("\n");
219
+ demo.destroy();
220
+ } else {
221
+ createModuleTasksDemo();
222
+ }
223
+ }
@@ -0,0 +1,194 @@
1
+ import { getLang, setLang, setLog, setStoreStrategy, setTranslations, t } from "valyrian.js/translate";
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 Ticket = { id: string; customer: string; status: "open" | "waiting" | "closed" };
15
+ type SupportState = { selected: number; language: "en" | "es"; filter: "all" | "open" };
16
+
17
+ const TICKETS: Ticket[] = [
18
+ { id: "T-104", customer: "Northwind", status: "open" },
19
+ { id: "T-118", customer: "Contoso", status: "waiting" },
20
+ { id: "T-130", customer: "Tailspin", status: "closed" }
21
+ ];
22
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
23
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
24
+ let storedLanguage = "en";
25
+
26
+ function configureTranslations() {
27
+ setLog(false);
28
+ setStoreStrategy({
29
+ get: () => storedLanguage,
30
+ set: (language) => {
31
+ storedLanguage = language;
32
+ }
33
+ });
34
+ setTranslations(
35
+ {
36
+ title: "Bilingual Support Console",
37
+ purpose: "Support Console",
38
+ language: "Language",
39
+ filter: "Filter",
40
+ selected: "Selected ticket",
41
+ all: "all tickets",
42
+ openOnly: "open only",
43
+ status: { open: "open", waiting: "waiting", closed: "closed" },
44
+ help: "L language J/K select ticket F filter status Ctrl+C: quit"
45
+ },
46
+ {
47
+ es: {
48
+ title: "Bilingual Support Console",
49
+ purpose: "Consola de soporte",
50
+ language: "Idioma",
51
+ filter: "Filtro",
52
+ selected: "Ticket elegido",
53
+ all: "todos los tickets",
54
+ openOnly: "solo abiertos",
55
+ status: { open: "abierto", waiting: "en espera", closed: "cerrado" },
56
+ help: "L idioma J/K elegir ticket F filtrar estado Ctrl+C: quit"
57
+ }
58
+ }
59
+ );
60
+ }
61
+
62
+ function shouldRunSnapshot() {
63
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
64
+ }
65
+
66
+ function visibleTickets(filter: SupportState["filter"]) {
67
+ return filter === "open" ? TICKETS.filter((ticket) => ticket.status === "open") : TICKETS;
68
+ }
69
+
70
+ export function App({ state }: { state: SupportState }) {
71
+ setLang(state.language);
72
+ const tickets = visibleTickets(state.filter);
73
+ const selected = tickets[state.selected] || tickets[0];
74
+
75
+ return (
76
+ <Screen title="Bilingual Support Console">
77
+ <Text style={{ color: "#ffffff", background: "#020617" }}>{t("title")}</Text>
78
+ <Pane style={PANEL_STYLE}>
79
+ <Text>{t("purpose")}</Text>
80
+ <Text>{`${t("language")}: ${state.language === "en" ? "English" : "Español"}`}</Text>
81
+ <Text>{`${t("filter")}: ${state.filter === "open" ? t("openOnly") : t("all")}`}</Text>
82
+ {tickets.map((ticket, index) => (
83
+ <Text>{`${index === state.selected ? ">" : " "} ${ticket.id} ${ticket.customer} — ${t(`status.${ticket.status}`)}`}</Text>
84
+ ))}
85
+ <Text>{`${t("selected")}: ${selected.id} ${selected.customer}`}</Text>
86
+ </Pane>
87
+ <Text style={FOOTER_STYLE}>{t("help")}</Text>
88
+ </Screen>
89
+ );
90
+ }
91
+
92
+ export function createModuleTranslateDemo(options: TerminalMountOptions = {}): ModuleDemo {
93
+ const previousLanguage = getLang();
94
+ configureTranslations();
95
+ const state: SupportState = { selected: 0, language: "en", filter: "all" };
96
+ setLang(state.language);
97
+ let running = true;
98
+ let session: TerminalSession;
99
+
100
+ function quit() {
101
+ running = false;
102
+ setLang(previousLanguage);
103
+ session.destroy();
104
+ }
105
+
106
+ function clampSelection() {
107
+ const count = visibleTickets(state.filter).length;
108
+ state.selected = Math.min(state.selected, Math.max(0, count - 1));
109
+ }
110
+
111
+ function move(delta: number) {
112
+ const count = visibleTickets(state.filter).length;
113
+ state.selected = (state.selected + delta + count) % count;
114
+ }
115
+
116
+ session = mountTerminal(<App state={state} />, {
117
+ ...options,
118
+ cols: options.cols ?? 92,
119
+ rows: options.rows ?? 18,
120
+ keymap: {
121
+ ...options.keymap,
122
+ bindings: [
123
+ ...(options.keymap?.bindings || []),
124
+ { key: "l", command: { id: "support.language" }, scope: "global" },
125
+ { key: "L", command: { id: "support.language" }, scope: "global" },
126
+ { key: "j", command: { id: "support.next" }, scope: "global" },
127
+ { key: "J", command: { id: "support.next" }, scope: "global" },
128
+ { key: "k", command: { id: "support.previous" }, scope: "global" },
129
+ { key: "K", command: { id: "support.previous" }, scope: "global" },
130
+ { key: "f", command: { id: "support.filter" }, scope: "global" },
131
+ { key: "F", command: { id: "support.filter" }, scope: "global" },
132
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
133
+ ],
134
+ onCommand(command, context) {
135
+ if (command.id === "support.language") {
136
+ state.language = state.language === "en" ? "es" : "en";
137
+ setLang(state.language);
138
+ session.update();
139
+ return true;
140
+ }
141
+ if (command.id === "support.next") {
142
+ move(1);
143
+ return true;
144
+ }
145
+ if (command.id === "support.previous") {
146
+ move(-1);
147
+ return true;
148
+ }
149
+ if (command.id === "support.filter") {
150
+ state.filter = state.filter === "all" ? "open" : "all";
151
+ clampSelection();
152
+ return true;
153
+ }
154
+ if (command.id === "quit") {
155
+ quit();
156
+ return true;
157
+ }
158
+ return options.keymap?.onCommand?.(command, context);
159
+ }
160
+ }
161
+ });
162
+
163
+ return {
164
+ session,
165
+ dispatchKey(key: string) {
166
+ return session.dispatchKey(key);
167
+ },
168
+ output() {
169
+ return session.output();
170
+ },
171
+ ansiOutput() {
172
+ return session.ansiOutput();
173
+ },
174
+ isRunning() {
175
+ return running;
176
+ },
177
+ destroy() {
178
+ running = false;
179
+ setLang(previousLanguage);
180
+ session.destroy();
181
+ }
182
+ };
183
+ }
184
+
185
+ if (import.meta.main) {
186
+ if (shouldRunSnapshot()) {
187
+ const demo = createModuleTranslateDemo({ runtime: "headless", cols: 92, rows: 18 });
188
+ process.stdout.write(demo.output());
189
+ process.stdout.write("\n");
190
+ demo.destroy();
191
+ } else {
192
+ createModuleTranslateDemo();
193
+ }
194
+ }
@@ -0,0 +1,168 @@
1
+ import { get, pick, set } from "valyrian.js/utils";
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 AccountPayload = {
15
+ account: { name: string; plan: string; region?: string; owner: { name: string; email: string } };
16
+ usage: { seats: number; projects: number; lastLogin: string };
17
+ billing: { status: string; renewal: string };
18
+ };
19
+ type InspectorState = { selected: number; filter: "all" | "account"; record: AccountPayload };
20
+ type FieldRow = { group: string; label: string; value: string };
21
+ type AccountSummaryKey = "name" | "plan" | "region";
22
+
23
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
24
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
25
+
26
+ function shouldRunSnapshot() {
27
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
28
+ }
29
+
30
+ function createRecord(): AccountPayload {
31
+ const record: AccountPayload = {
32
+ account: { name: "Northwind", plan: "Team", owner: { name: "Ada Lovelace", email: "ada@example.test" } },
33
+ usage: { seats: 12, projects: 4, lastLogin: "2026-05-29" },
34
+ billing: { status: "current", renewal: "2026-06-15" }
35
+ };
36
+ set(record, "account.region", "MX");
37
+ return record;
38
+ }
39
+
40
+ function buildRows(record: AccountPayload, filter: InspectorState["filter"]): FieldRow[] {
41
+ const safeAccount = pick<AccountPayload["account"], AccountSummaryKey>(record.account, ["name", "plan", "region"]);
42
+ const rows = [
43
+ { group: "account", label: "Account", value: String(get(safeAccount, "name", "unknown")) },
44
+ { group: "account", label: "Plan", value: String(get(safeAccount, "plan", "none")) },
45
+ { group: "account", label: "Region", value: String(get(safeAccount, "region", "unknown")) },
46
+ { group: "owner", label: "Owner", value: String(get(record, "account.owner.name", "unknown")) },
47
+ { group: "usage", label: "Seats", value: String(get(record, "usage.seats", 0)) },
48
+ { group: "usage", label: "Projects", value: String(get(record, "usage.projects", 0)) },
49
+ { group: "billing", label: "Billing", value: String(get(record, "billing.status", "unknown")) }
50
+ ];
51
+ return filter === "account" ? rows.filter((row) => row.group === "account") : rows;
52
+ }
53
+
54
+ export function App({ state }: { state: InspectorState }) {
55
+ const rows = buildRows(state.record, state.filter);
56
+ const selected = rows[state.selected] || rows[0];
57
+
58
+ return (
59
+ <Screen title="Account Data Inspector">
60
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Account Data Inspector</Text>
61
+ <Pane style={PANEL_STYLE}>
62
+ <Text>Account Data Inspector</Text>
63
+ <Text>{`Filter: ${state.filter === "account" ? "account fields" : "all groups"}`}</Text>
64
+ {rows.map((row, index) => (
65
+ <Text>{`${index === state.selected ? ">" : " "} [${row.group}] ${row.label}: ${row.value}`}</Text>
66
+ ))}
67
+ <Text>{`Selected field: ${selected.label}`}</Text>
68
+ </Pane>
69
+ <Text style={FOOTER_STYLE}>J/K select field F filter group R reset Ctrl+C: quit</Text>
70
+ </Screen>
71
+ );
72
+ }
73
+
74
+ export function createModuleUtilsDemo(options: TerminalMountOptions = {}): ModuleDemo {
75
+ const state: InspectorState = { selected: 0, filter: "all", record: createRecord() };
76
+ let running = true;
77
+ let session: TerminalSession;
78
+
79
+ function quit() {
80
+ running = false;
81
+ session.destroy();
82
+ }
83
+
84
+ function clampSelection() {
85
+ const count = buildRows(state.record, state.filter).length;
86
+ state.selected = Math.min(state.selected, Math.max(0, count - 1));
87
+ }
88
+
89
+ session = mountTerminal(<App state={state} />, {
90
+ ...options,
91
+ cols: options.cols ?? 92,
92
+ rows: options.rows ?? 20,
93
+ keymap: {
94
+ ...options.keymap,
95
+ bindings: [
96
+ ...(options.keymap?.bindings || []),
97
+ { key: "j", command: { id: "inspector.next" }, scope: "global" },
98
+ { key: "J", command: { id: "inspector.next" }, scope: "global" },
99
+ { key: "k", command: { id: "inspector.previous" }, scope: "global" },
100
+ { key: "K", command: { id: "inspector.previous" }, scope: "global" },
101
+ { key: "f", command: { id: "inspector.filter" }, scope: "global" },
102
+ { key: "F", command: { id: "inspector.filter" }, scope: "global" },
103
+ { key: "r", command: { id: "inspector.reset" }, scope: "global" },
104
+ { key: "R", command: { id: "inspector.reset" }, scope: "global" },
105
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
106
+ ],
107
+ onCommand(command, context) {
108
+ if (command.id === "inspector.next") {
109
+ const count = buildRows(state.record, state.filter).length;
110
+ state.selected = (state.selected + 1) % count;
111
+ return true;
112
+ }
113
+ if (command.id === "inspector.previous") {
114
+ const count = buildRows(state.record, state.filter).length;
115
+ state.selected = (state.selected - 1 + count) % count;
116
+ return true;
117
+ }
118
+ if (command.id === "inspector.filter") {
119
+ state.filter = state.filter === "all" ? "account" : "all";
120
+ clampSelection();
121
+ return true;
122
+ }
123
+ if (command.id === "inspector.reset") {
124
+ state.selected = 0;
125
+ state.filter = "all";
126
+ state.record = createRecord();
127
+ return true;
128
+ }
129
+ if (command.id === "quit") {
130
+ quit();
131
+ return true;
132
+ }
133
+ return options.keymap?.onCommand?.(command, context);
134
+ }
135
+ }
136
+ });
137
+
138
+ return {
139
+ session,
140
+ dispatchKey(key: string) {
141
+ return session.dispatchKey(key);
142
+ },
143
+ output() {
144
+ return session.output();
145
+ },
146
+ ansiOutput() {
147
+ return session.ansiOutput();
148
+ },
149
+ isRunning() {
150
+ return running;
151
+ },
152
+ destroy() {
153
+ running = false;
154
+ session.destroy();
155
+ }
156
+ };
157
+ }
158
+
159
+ if (import.meta.main) {
160
+ if (shouldRunSnapshot()) {
161
+ const demo = createModuleUtilsDemo({ runtime: "headless", cols: 92, rows: 20 });
162
+ process.stdout.write(demo.output());
163
+ process.stdout.write("\n");
164
+ demo.destroy();
165
+ } else {
166
+ createModuleUtilsDemo();
167
+ }
168
+ }