@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,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
+ }
@@ -0,0 +1,339 @@
1
+ import { FormStore } from "valyrian.js/forms";
2
+ import { Money, formatMoney, parseMoneyInput } from "valyrian.js/money";
3
+ import { createNativeStore, StorageType } from "valyrian.js/native-store";
4
+ import { setLang, setLog, setStoreStrategy, setTranslations, t } from "valyrian.js/translate";
5
+ import { get, set } from "valyrian.js/utils";
6
+ import { Input, Pane, Screen, Split, Text, mountTerminal } from "@valyrianjs/terminal";
7
+ import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
8
+
9
+ type Language = "en" | "es";
10
+
11
+ type BillingForm = {
12
+ name: string;
13
+ seats: number;
14
+ budgetCents: number;
15
+ };
16
+
17
+ interface SavedBilling {
18
+ name: string;
19
+ seats: number;
20
+ budgetCents: number;
21
+ workspace: string;
22
+ }
23
+
24
+ interface FormWorkflowState {
25
+ language: Language;
26
+ savedWorkspace: string;
27
+ savedBilling: SavedBilling | null;
28
+ saveMessage: string;
29
+ resetMessage: string;
30
+ running: boolean;
31
+ }
32
+
33
+ interface ModuleFormWorkflowDemo {
34
+ session: TerminalSession;
35
+ dispatchKey(key: string): string;
36
+ output(): string;
37
+ ansiOutput(): string;
38
+ isRunning(): boolean;
39
+ destroy(): void;
40
+ }
41
+
42
+ type FormWorkflowOptions = TerminalMountOptions & {
43
+ storeKey?: string;
44
+ };
45
+
46
+ const DEFAULT_STORE_KEY = "valyrian-terminal-docs.billing-settings-wizard";
47
+ const DEFAULT_WORKSPACE = "Northwind CLI";
48
+
49
+ const SHELL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
50
+ const FORM_STYLE = { color: "#ffffff", background: "#0f3b3e", padding: { left: 1, right: 1 } };
51
+ const SUMMARY_STYLE = { color: "#ffffff", background: "#3b1f4f", padding: { left: 1, right: 1 } };
52
+ const FOOTER_STYLE = { color: "#e5e7eb", background: "#1f2937" };
53
+ const MUTED_STYLE = { color: "#cbd5e1" };
54
+ const ERROR_STYLE = { color: "#fecaca" };
55
+
56
+ function shouldRunSnapshot() {
57
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
58
+ }
59
+
60
+
61
+ function configureTranslations() {
62
+ setLog(false);
63
+ let storedLanguage = "en";
64
+ setStoreStrategy({
65
+ get: () => storedLanguage,
66
+ set: (language) => {
67
+ storedLanguage = language;
68
+ }
69
+ });
70
+ setTranslations(
71
+ {
72
+ heading: "Billing/Settings Wizard",
73
+ language: "Language: English",
74
+ workspace: "Workspace",
75
+ customer: "Customer",
76
+ seats: "Seats",
77
+ budget: "Budget"
78
+ },
79
+ {
80
+ es: {
81
+ heading: "Asistente de cobro/configuración",
82
+ language: "Idioma: español",
83
+ workspace: "Espacio",
84
+ customer: "Cliente",
85
+ seats: "Asientos",
86
+ budget: "Presupuesto"
87
+ }
88
+ }
89
+ );
90
+ }
91
+
92
+ function parseSeats(value: unknown) {
93
+ const seats = Number(String(value).trim());
94
+ return Number.isFinite(seats) ? seats : 0;
95
+ }
96
+
97
+ function parseBudgetCents(value: unknown) {
98
+ try {
99
+ return parseMoneyInput(String(value), { decimalPlaces: 2 }).toCents();
100
+ } catch {
101
+ return -1;
102
+ }
103
+ }
104
+
105
+ function createBillingForm(onSaved: (billing: SavedBilling) => void) {
106
+ return new FormStore<BillingForm>({
107
+ state: { name: "Ada", seats: 12, budgetCents: 129950 },
108
+ schema: {
109
+ type: "object",
110
+ properties: {
111
+ name: { type: "string", minLength: 1 },
112
+ seats: { type: "number", minimum: 1 },
113
+ budgetCents: { type: "number", minimum: 100 }
114
+ },
115
+ required: ["name", "seats", "budgetCents"]
116
+ },
117
+ clean: {
118
+ name: (value) => String(value).trim(),
119
+ seats: parseSeats,
120
+ budgetCents: parseBudgetCents
121
+ },
122
+ format: {
123
+ seats: (value) => String(value),
124
+ budgetCents: (value) => (Number(value) / 100).toFixed(2)
125
+ },
126
+ onSubmit(values) {
127
+ onSaved({
128
+ name: values.name,
129
+ seats: values.seats,
130
+ budgetCents: values.budgetCents,
131
+ workspace: DEFAULT_WORKSPACE
132
+ });
133
+ }
134
+ });
135
+ }
136
+
137
+ function formatBudget(cents: number, language: Language) {
138
+ const money = Money.fromCents(Number.isFinite(cents) ? cents : 0);
139
+ return formatMoney(money, { currency: "USD", locale: language === "es" ? "es-MX" : "en-US" });
140
+ }
141
+
142
+ function fieldError(form: FormStore<BillingForm>, field: keyof BillingForm) {
143
+ return String(form.validationErrors[field] || "not checked");
144
+ }
145
+
146
+ function savedSummary(saved: SavedBilling | null, language: Language) {
147
+ if (!saved) return "none";
148
+ return `${saved.name} / ${saved.seats} seats / ${formatBudget(saved.budgetCents, language)}`;
149
+ }
150
+
151
+ export function App({ form, state, onSubmit }: { form: FormStore<BillingForm>; state: FormWorkflowState; onSubmit: () => void }) {
152
+ setLang(state.language);
153
+ const data = { billing: { name: form.state.name, seats: form.state.seats } };
154
+ set(data, "billing.workspace", DEFAULT_WORKSPACE);
155
+ set(data, "billing.total", formatBudget(form.state.budgetCents, state.language));
156
+ const workspace = String(get(data, "billing.workspace", DEFAULT_WORKSPACE));
157
+ const displayBudget = String(get(data, "billing.total", formatBudget(0, state.language)));
158
+
159
+ return (
160
+ <Screen title="Billing/Settings Wizard">
161
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Valyrian.js modules in terminal apps: form workflow</Text>
162
+ <Split direction="row" gap={1} sizes={["1fr", "1fr"]}>
163
+ <Pane style={FORM_STYLE}>
164
+ <Text>{t("heading")}</Text>
165
+ <Text>{t("language")}</Text>
166
+ <Text>{`${t("workspace")}: ${workspace}`}</Text>
167
+ <Text>{t("customer")}</Text>
168
+ <Input id="billing-name" value={String(form.state.name)} placeholder="Customer name" onchange={(event) => form.setField("name", event.value)} onsubmit={onSubmit} />
169
+ <Text>{`Name: ${form.state.name || "missing"}`}</Text>
170
+ <Text style={ERROR_STYLE}>{`Name error: ${fieldError(form, "name")}`}</Text>
171
+ <Text>{t("seats")}</Text>
172
+ <Input id="billing-seats" value={String(form.formatValue("seats", form.state.seats))} placeholder="Seat count" onchange={(event) => form.setField("seats", event.value)} onsubmit={onSubmit} />
173
+ <Text>{`Seats: ${form.state.seats}`}</Text>
174
+ <Text style={ERROR_STYLE}>{`Seats error: ${fieldError(form, "seats")}`}</Text>
175
+ <Text>{t("budget")}</Text>
176
+ <Input id="billing-budget" value={String(form.formatValue("budgetCents", form.state.budgetCents))} placeholder="Monthly budget" onchange={(event) => form.setField("budgetCents", event.value)} onsubmit={onSubmit} />
177
+ <Text>{`Budget: ${displayBudget}`}</Text>
178
+ <Text style={ERROR_STYLE}>{`Budget error: ${fieldError(form, "budgetCents")}`}</Text>
179
+ </Pane>
180
+ <Pane style={SUMMARY_STYLE}>
181
+ <Text>Cleaned billing summary</Text>
182
+ <Text>{`Workspace: ${workspace}`}</Text>
183
+ <Text>{`Saved workspace: ${state.savedWorkspace || "none"}`}</Text>
184
+ <Text>{`Saved billing: ${savedSummary(state.savedBilling, state.language)}`}</Text>
185
+ <Text>{`Form dirty: ${form.isDirty}`}</Text>
186
+ <Text>{`Form success: ${form.success}`}</Text>
187
+ <Text>{`Save: ${state.saveMessage}`}</Text>
188
+ <Text>{`Reset: ${state.resetMessage}`}</Text>
189
+ <Text style={MUTED_STYLE}>Forms clean input, native-store persists workspace, translate changes labels, money formats totals, and utils shape summary data.</Text>
190
+ </Pane>
191
+ </Split>
192
+ <Pane style={SHELL_STYLE}>
193
+ <Text>N: fill name Tab: next field Shift+Tab: previous field L: language S/Enter: save R: reset form X: clear saved Ctrl+C: quit</Text>
194
+ <Text style={FOOTER_STYLE}>See the Valyrian.js documentation for forms, native-store, translate, money, and utils.</Text>
195
+ </Pane>
196
+ </Screen>
197
+ );
198
+ }
199
+
200
+ export function createModuleFormWorkflowDemo(options: FormWorkflowOptions = {}): ModuleFormWorkflowDemo {
201
+ configureTranslations();
202
+ const settings = createNativeStore<{ workspace: string }>(options.storeKey ?? DEFAULT_STORE_KEY, {}, StorageType.Session, true);
203
+ const state: FormWorkflowState = {
204
+ language: "en",
205
+ savedWorkspace: String(settings.get("workspace") || ""),
206
+ savedBilling: null,
207
+ saveMessage: "not saved",
208
+ resetMessage: "not reset",
209
+ running: true
210
+ };
211
+ const form = createBillingForm((billing) => {
212
+ state.savedBilling = billing;
213
+ state.savedWorkspace = billing.workspace;
214
+ settings.set("workspace", billing.workspace);
215
+ state.saveMessage = `saved ${billing.name} for ${billing.seats} seats`;
216
+ });
217
+ let session: TerminalSession;
218
+
219
+ function quit() {
220
+ state.running = false;
221
+ settings.cleanup();
222
+ session.destroy();
223
+ }
224
+
225
+ function resetForm() {
226
+ form.reset();
227
+ state.savedBilling = null;
228
+ state.savedWorkspace = "";
229
+ state.saveMessage = "not saved";
230
+ state.resetMessage = "form reset";
231
+ settings.clear();
232
+ session.update();
233
+ }
234
+
235
+ async function submitForm() {
236
+ const ok = await form.submit();
237
+ if (!ok) {
238
+ state.saveMessage = "blocked by validation";
239
+ }
240
+ session.update();
241
+ }
242
+
243
+ session = mountTerminal(<App form={form} state={state} onSubmit={submitForm} />, {
244
+ ...options,
245
+ cols: options.cols ?? 100,
246
+ rows: options.rows ?? 24,
247
+ keymap: {
248
+ ...options.keymap,
249
+ bindings: [
250
+ ...(options.keymap?.bindings || []),
251
+ { key: "n", command: { id: "form.name" }, scope: "global" },
252
+ { key: "N", command: { id: "form.name" }, scope: "global" },
253
+ { key: "l", command: { id: "form.language" }, scope: "global" },
254
+ { key: "L", command: { id: "form.language" }, scope: "global" },
255
+ { key: "s", command: { id: "form.save" }, scope: "global" },
256
+ { key: "S", command: { id: "form.save" }, scope: "global" },
257
+ { key: "r", command: { id: "form.reset" }, scope: "global" },
258
+ { key: "R", command: { id: "form.reset" }, scope: "global" },
259
+ { key: "x", command: { id: "form.clear" }, scope: "global" },
260
+ { key: "X", command: { id: "form.clear" }, scope: "global" },
261
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
262
+ ],
263
+ onCommand(command, context) {
264
+ if (command.id === "form.name") {
265
+ form.setField("name", "Ada Lovelace");
266
+ session.update();
267
+ return true;
268
+ }
269
+ if (command.id === "form.language") {
270
+ state.language = state.language === "en" ? "es" : "en";
271
+ setLang(state.language);
272
+ session.update();
273
+ return true;
274
+ }
275
+ if (command.id === "form.save") {
276
+ void submitForm();
277
+ return true;
278
+ }
279
+ if (command.id === "form.reset") {
280
+ resetForm();
281
+ return true;
282
+ }
283
+ if (command.id === "form.clear") {
284
+ state.savedWorkspace = "";
285
+ state.savedBilling = null;
286
+ state.saveMessage = "not saved";
287
+ settings.clear();
288
+ session.update();
289
+ return true;
290
+ }
291
+ if (command.id === "quit") {
292
+ quit();
293
+ return true;
294
+ }
295
+ return options.keymap?.onCommand?.(command, context);
296
+ }
297
+ }
298
+ });
299
+
300
+ session.focus("billing-name");
301
+
302
+ return {
303
+ session,
304
+ dispatchKey(key: string) {
305
+ return session.dispatchKey(key);
306
+ },
307
+ output() {
308
+ return session.output();
309
+ },
310
+ ansiOutput() {
311
+ return session.ansiOutput();
312
+ },
313
+ isRunning() {
314
+ return state.running;
315
+ },
316
+ destroy() {
317
+ state.running = false;
318
+ settings.cleanup();
319
+ session.destroy();
320
+ }
321
+ };
322
+ }
323
+
324
+ if (import.meta.main) {
325
+ if (shouldRunSnapshot()) {
326
+ const demo = createModuleFormWorkflowDemo({ runtime: "headless", cols: 100, rows: 24 });
327
+ demo.dispatchKey("S");
328
+ await new Promise((resolve) => setTimeout(resolve, 0));
329
+ demo.dispatchKey("N");
330
+ demo.dispatchKey("L");
331
+ demo.dispatchKey("S");
332
+ await new Promise((resolve) => setTimeout(resolve, 0));
333
+ process.stdout.write(demo.output());
334
+ process.stdout.write("\n");
335
+ demo.destroy();
336
+ } else {
337
+ createModuleFormWorkflowDemo();
338
+ }
339
+ }