@valyrianjs/terminal 0.2.1 → 0.2.3

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 (89) hide show
  1. package/dist/ansi.d.ts.map +1 -1
  2. package/dist/ansi.js +177 -17
  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-internal.d.ts +10 -0
  19. package/dist/render-internal.d.ts.map +1 -0
  20. package/dist/render-internal.js +1295 -0
  21. package/dist/render-internal.js.map +1 -0
  22. package/dist/render.d.ts.map +1 -1
  23. package/dist/render.js +13 -1205
  24. package/dist/render.js.map +1 -1
  25. package/dist/session.d.ts.map +1 -1
  26. package/dist/session.js +78 -4
  27. package/dist/session.js.map +1 -1
  28. package/dist/text.d.ts +7 -0
  29. package/dist/text.d.ts.map +1 -1
  30. package/dist/text.js +125 -0
  31. package/dist/text.js.map +1 -1
  32. package/dist/theme.d.ts.map +1 -1
  33. package/dist/theme.js +18 -2
  34. package/dist/theme.js.map +1 -1
  35. package/dist/types.d.ts +3 -2
  36. package/dist/types.d.ts.map +1 -1
  37. package/docs/api-reference.md +6 -3
  38. package/docs/cookbook.md +1 -1
  39. package/docs/interaction-model.md +5 -5
  40. package/docs/primitive-gallery.md +4 -4
  41. package/examples/basic.tsx +22 -0
  42. package/examples/cli.tsx +55 -0
  43. package/examples/demo.tsx +98 -0
  44. package/examples/docs/background-fill.tsx +107 -0
  45. package/examples/docs/component-composition.tsx +140 -0
  46. package/examples/docs/cursor.tsx +121 -0
  47. package/examples/docs/employees-list.tsx +138 -0
  48. package/examples/docs/hello.tsx +98 -0
  49. package/examples/docs/interactive-note.tsx +111 -0
  50. package/examples/docs/module-api-dashboard.tsx +307 -0
  51. package/examples/docs/module-flux-store.tsx +181 -0
  52. package/examples/docs/module-form-workflow.tsx +339 -0
  53. package/examples/docs/module-forms.tsx +218 -0
  54. package/examples/docs/module-money.tsx +175 -0
  55. package/examples/docs/module-native-store.tsx +188 -0
  56. package/examples/docs/module-pulses.tsx +142 -0
  57. package/examples/docs/module-query.tsx +209 -0
  58. package/examples/docs/module-request.tsx +194 -0
  59. package/examples/docs/module-state-workbench.tsx +283 -0
  60. package/examples/docs/module-tasks.tsx +223 -0
  61. package/examples/docs/module-translate.tsx +194 -0
  62. package/examples/docs/module-utils.tsx +168 -0
  63. package/examples/docs/module-valyrian-core.tsx +159 -0
  64. package/examples/docs/pizza-builder.tsx +463 -0
  65. package/examples/docs/primitive-activity-console.tsx +113 -0
  66. package/examples/docs/primitive-command-panel.tsx +186 -0
  67. package/examples/docs/primitive-data-explorer.tsx +155 -0
  68. package/examples/docs/primitive-input-workbench.tsx +128 -0
  69. package/examples/docs/primitive-layout-shell.tsx +115 -0
  70. package/examples/docs/responsive-split.tsx +186 -0
  71. package/examples/docs/style-system.tsx +209 -0
  72. package/examples/docs/theme-colors.tsx +225 -0
  73. package/examples/docs/virtualized-list-workbench.tsx +232 -0
  74. package/examples/opencode-dogfood-app.tsx +215 -0
  75. package/examples/opencode-dogfood-lifecycle.tsx +194 -0
  76. package/examples/opencode-dogfood.tsx +11 -0
  77. package/llms-full.txt +16 -13
  78. package/package.json +3 -2
  79. package/src/ansi.ts +207 -17
  80. package/src/events.ts +2 -0
  81. package/src/frame-style.ts +36 -0
  82. package/src/layout.ts +57 -24
  83. package/src/mouse.ts +10 -1
  84. package/src/render-internal.ts +1441 -0
  85. package/src/render.ts +14 -1324
  86. package/src/session.ts +99 -12
  87. package/src/text.ts +160 -0
  88. package/src/theme.ts +22 -2
  89. package/src/types.ts +3 -2
@@ -0,0 +1,159 @@
1
+ import { v } from "valyrian.js";
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 Command = {
15
+ id: string;
16
+ name: string;
17
+ result: string;
18
+ };
19
+
20
+ type CommandPaletteState = {
21
+ selected: number;
22
+ result: string;
23
+ history: string[];
24
+ };
25
+
26
+ const COMMANDS: Command[] = [
27
+ { id: "open-incidents", name: "Open incident board", result: "Incident board opened" },
28
+ { id: "assign-oncall", name: "Assign on-call engineer", result: "On-call engineer assigned" },
29
+ { id: "publish-summary", name: "Publish shift summary", result: "Shift summary published" }
30
+ ];
31
+
32
+ const INITIAL_RESULT = "Choose a command";
33
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
34
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
35
+
36
+ function shouldRunSnapshot() {
37
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
38
+ }
39
+
40
+ function clampSelection(index: number) {
41
+ return (index + COMMANDS.length) % COMMANDS.length;
42
+ }
43
+
44
+ const manualNode = v(Text, null, "Manual vnode: quick actions are composed with TSX rows");
45
+
46
+ export function App({ state }: { state: CommandPaletteState }) {
47
+ return (
48
+ <Screen title="Terminal Command Palette">
49
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Terminal Command Palette</Text>
50
+ <Pane style={PANEL_STYLE}>
51
+ <Text>Command Palette</Text>
52
+ {manualNode}
53
+ {COMMANDS.map((command, index) => (
54
+ <Text>{`${index === state.selected ? ">" : " "} ${command.name}`}</Text>
55
+ ))}
56
+ <Text>{`Result: ${state.result}`}</Text>
57
+ <Text>{`History: ${state.history.length ? state.history.join(" -> ") : "none"}`}</Text>
58
+ </Pane>
59
+ <Text style={FOOTER_STYLE}>J/K move Enter run command R reset Ctrl+C: quit</Text>
60
+ </Screen>
61
+ );
62
+ }
63
+
64
+ export function createModuleValyrianCoreDemo(options: TerminalMountOptions = {}): ModuleDemo {
65
+ const state: CommandPaletteState = { selected: 0, result: INITIAL_RESULT, history: [] };
66
+ let running = true;
67
+ let session: TerminalSession;
68
+
69
+ function quit() {
70
+ running = false;
71
+ session.destroy();
72
+ }
73
+
74
+ function runSelectedCommand() {
75
+ const command = COMMANDS[state.selected];
76
+ state.result = command.result;
77
+ state.history = [...state.history.slice(-2), command.name];
78
+ }
79
+
80
+ function reset() {
81
+ state.selected = 0;
82
+ state.result = INITIAL_RESULT;
83
+ state.history = [];
84
+ }
85
+
86
+ session = mountTerminal(<App state={state} />, {
87
+ ...options,
88
+ cols: options.cols ?? 92,
89
+ rows: options.rows ?? 18,
90
+ keymap: {
91
+ ...options.keymap,
92
+ bindings: [
93
+ ...(options.keymap?.bindings || []),
94
+ { key: "j", command: { id: "palette.next" }, scope: "global" },
95
+ { key: "J", command: { id: "palette.next" }, scope: "global" },
96
+ { key: "k", command: { id: "palette.previous" }, scope: "global" },
97
+ { key: "K", command: { id: "palette.previous" }, scope: "global" },
98
+ { key: "ENTER", command: { id: "palette.run" }, scope: "global" },
99
+ { key: "r", command: { id: "palette.reset" }, scope: "global" },
100
+ { key: "R", command: { id: "palette.reset" }, scope: "global" },
101
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
102
+ ],
103
+ onCommand(command, context) {
104
+ if (command.id === "palette.next") {
105
+ state.selected = clampSelection(state.selected + 1);
106
+ return true;
107
+ }
108
+ if (command.id === "palette.previous") {
109
+ state.selected = clampSelection(state.selected - 1);
110
+ return true;
111
+ }
112
+ if (command.id === "palette.run") {
113
+ runSelectedCommand();
114
+ return true;
115
+ }
116
+ if (command.id === "palette.reset") {
117
+ reset();
118
+ return true;
119
+ }
120
+ if (command.id === "quit") {
121
+ quit();
122
+ return true;
123
+ }
124
+ return options.keymap?.onCommand?.(command, context);
125
+ }
126
+ }
127
+ });
128
+
129
+ return {
130
+ session,
131
+ dispatchKey(key: string) {
132
+ return session.dispatchKey(key);
133
+ },
134
+ output() {
135
+ return session.output();
136
+ },
137
+ ansiOutput() {
138
+ return session.ansiOutput();
139
+ },
140
+ isRunning() {
141
+ return running;
142
+ },
143
+ destroy() {
144
+ running = false;
145
+ session.destroy();
146
+ }
147
+ };
148
+ }
149
+
150
+ if (import.meta.main) {
151
+ if (shouldRunSnapshot()) {
152
+ const demo = createModuleValyrianCoreDemo({ runtime: "headless", cols: 92, rows: 18 });
153
+ process.stdout.write(demo.output());
154
+ process.stdout.write("\n");
155
+ demo.destroy();
156
+ } else {
157
+ createModuleValyrianCoreDemo();
158
+ }
159
+ }
@@ -0,0 +1,463 @@
1
+ import { Button, Fixed, Input, Pane, Screen, Split, Text, mountTerminal } from "@valyrianjs/terminal";
2
+ import type { TerminalMountOptions, TerminalSession, TerminalTheme } from "@valyrianjs/terminal";
3
+
4
+ type LayoutMode = "wide" | "medium" | "compact";
5
+
6
+ interface Ingredient {
7
+ id: string;
8
+ name: string;
9
+ category: string;
10
+ price: number;
11
+ }
12
+
13
+ interface PizzaBuilderState {
14
+ cursorIndex: number;
15
+ selectedIds: Set<string>;
16
+ orderName: string;
17
+ layoutMode: LayoutMode;
18
+ warning: string;
19
+ confirmed: boolean;
20
+ confirmationId: string;
21
+ terminalCols: number;
22
+ terminalRows: number;
23
+ running: boolean;
24
+ }
25
+
26
+ export interface PizzaBuilderDemo {
27
+ session: TerminalSession;
28
+ dispatchKey(key: string): string;
29
+ output(): string;
30
+ ansiOutput(): string;
31
+ isRunning(): boolean;
32
+ destroy(): void;
33
+ }
34
+
35
+ const BASE_PRICE = 8;
36
+ const INGREDIENTS: Ingredient[] = [
37
+ { id: "mozzarella", name: "Mozzarella", category: "Cheese", price: 1.5 },
38
+ { id: "pepperoni", name: "Pepperoni", category: "Meat", price: 2 },
39
+ { id: "mushrooms", name: "Mushrooms", category: "Veg", price: 1.25 },
40
+ { id: "olives", name: "Olives", category: "Veg", price: 1 },
41
+ { id: "basil", name: "Basil", category: "Herb", price: 0.75 },
42
+ { id: "hot-honey", name: "Hot honey", category: "Finish", price: 1.25 }
43
+ ];
44
+
45
+ const SNAPSHOT_SIZE = { cols: 120, rows: 34 };
46
+ const COMPACT_MAX_COLS = 60;
47
+ const MEDIUM_MAX_COLS = 90;
48
+
49
+ const APP_SHELL_STYLE = { color: "#f5f5f5", background: "#0f0f0f" };
50
+ const APP_FOOTER_STYLE = { color: "#d4d4d4", background: "#1a1a1a" };
51
+ const INGREDIENTS_PANEL_STYLE = {
52
+ color: "#f5f5f5",
53
+ background: "#1a1a1a",
54
+ padding: { left: 1, right: 1 }
55
+ };
56
+ const SUMMARY_PANEL_STYLE = {
57
+ color: "#f5f5f5",
58
+ background: "#151515",
59
+ padding: { left: 1, right: 1 }
60
+ };
61
+ const CHECKOUT_PANEL_STYLE = {
62
+ color: "#f5f5f5",
63
+ background: "#1a140f",
64
+ padding: { left: 1, right: 1 }
65
+ };
66
+ const ROW_CURRENT_STYLE = { color: "#ffffff", background: "#243b32" };
67
+ const ROW_SELECTED_STYLE = { color: "#ffffff", background: "#4a2f16" };
68
+ const ROW_IDLE_STYLE = { color: "#d4d4d4" };
69
+ const TOTAL_VALUE_STYLE = { color: "#ffffff", background: "#005f4f" };
70
+ const CHECKOUT_READY_STYLE = { color: "#ffffff", background: "#2e2600" };
71
+ const MUTED_COPY_STYLE = { color: "#a3a3a3" };
72
+ const WARNING_STYLE = { color: "#ffffff", background: "#7c2d12" };
73
+ const SUCCESS_STYLE = { color: "#ffffff", background: "#166534" };
74
+ const PIZZA_THEME: TerminalTheme = {
75
+ spans: {
76
+ focus: {
77
+ background: "#1f2328"
78
+ }
79
+ }
80
+ };
81
+
82
+ function createInitialState(size: { cols: number; rows: number }): PizzaBuilderState {
83
+ return {
84
+ cursorIndex: 0,
85
+ selectedIds: new Set(),
86
+ orderName: "",
87
+ layoutMode: layoutModeForCols(size.cols),
88
+ warning: "",
89
+ confirmed: false,
90
+ confirmationId: "",
91
+ terminalCols: size.cols,
92
+ terminalRows: size.rows,
93
+ running: true
94
+ };
95
+ }
96
+
97
+ function layoutModeForCols(cols: number): LayoutMode {
98
+ if (cols <= COMPACT_MAX_COLS) return "compact";
99
+ if (cols <= MEDIUM_MAX_COLS) return "medium";
100
+ return "wide";
101
+ }
102
+
103
+ function shouldRunSnapshot() {
104
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
105
+ }
106
+
107
+ function formatPrice(value: number) {
108
+ return `$${value.toFixed(2)}`;
109
+ }
110
+
111
+ function selectedIngredients(state: PizzaBuilderState) {
112
+ return INGREDIENTS.filter((ingredient) => state.selectedIds.has(ingredient.id));
113
+ }
114
+
115
+ function totalPrice(state: PizzaBuilderState) {
116
+ let total = BASE_PRICE;
117
+ for (const ingredient of INGREDIENTS) {
118
+ if (state.selectedIds.has(ingredient.id)) {
119
+ total += ingredient.price;
120
+ }
121
+ }
122
+ return total;
123
+ }
124
+
125
+ function confirmOrder(state: PizzaBuilderState) {
126
+ if (state.orderName.trim().length === 0) {
127
+ state.warning = "Add a name for this order before checkout.";
128
+ state.confirmed = false;
129
+ state.confirmationId = "";
130
+ return;
131
+ }
132
+ state.warning = "";
133
+ state.confirmed = true;
134
+ state.confirmationId = "PIZZA-1001";
135
+ }
136
+
137
+ function layoutLabel(mode: LayoutMode) {
138
+ if (mode === "medium") return "Vista media";
139
+ if (mode === "compact") return "Vista compacta";
140
+ return "Vista amplia: ingredientes, resumen y checkout";
141
+ }
142
+
143
+ function layoutSize(mode: LayoutMode) {
144
+ if (mode === "medium") return { cols: 84, rows: 30 };
145
+ if (mode === "compact") return { cols: 52, rows: 34 };
146
+ return SNAPSHOT_SIZE;
147
+ }
148
+
149
+ function initialTerminalSize(options: TerminalMountOptions) {
150
+ if (isTerminalDimension(options.cols) && isTerminalDimension(options.rows)) {
151
+ return { cols: options.cols, rows: options.rows };
152
+ }
153
+
154
+ if (isTerminalDimension(options.stdout?.columns) && isTerminalDimension(options.stdout?.rows)) {
155
+ return { cols: options.stdout.columns, rows: options.stdout.rows };
156
+ }
157
+
158
+ if (shouldRunSnapshot()) {
159
+ return SNAPSHOT_SIZE;
160
+ }
161
+
162
+ return { cols: process.stdout.columns, rows: process.stdout.rows };
163
+ }
164
+
165
+ function isTerminalDimension(value: number | undefined): value is number {
166
+ return Number.isInteger(value) && Number(value) >= 1;
167
+ }
168
+
169
+ export function App({ state }: { state: PizzaBuilderState }) {
170
+ const selected = selectedIngredients(state);
171
+ const total = totalPrice(state);
172
+ return (
173
+ <Screen title="Pizza Builder">
174
+ <Fixed position="top" size={4}>
175
+ <Pane fill style={APP_SHELL_STYLE}>
176
+ <Text>Pizza Builder</Text>
177
+ <Text>{`${layoutLabel(state.layoutMode)} Terminal: ${state.terminalCols}x${state.terminalRows}`}</Text>
178
+ <Text state="muted" styles={{ muted: MUTED_COPY_STYLE }}>Build a pizza, review the total, and finish with a local demo checkout.</Text>
179
+ </Pane>
180
+ </Fixed>
181
+ <Split
182
+ fill
183
+ direction="row"
184
+ gap={0}
185
+ sizes={["2fr", "1fr", "1fr"]}
186
+ breakpoints={[
187
+ { maxCols: COMPACT_MAX_COLS, direction: "column", sizes: ["2fr", "1fr", "1fr"], gap: 0 },
188
+ { maxCols: MEDIUM_MAX_COLS, sizes: ["3fr", "2fr", "2fr"], gap: 0 }
189
+ ]}
190
+ >
191
+ <Pane id="ingredients-list" focusable style={INGREDIENTS_PANEL_STYLE}>
192
+ <Text>Ingredients | toppings</Text>
193
+ <Text state="muted" styles={{ muted: MUTED_COPY_STYLE }}>Use Up/Down to choose ingredients, then Space to select.</Text>
194
+ {INGREDIENTS.map((ingredient, index) => {
195
+ const marker = index === state.cursorIndex ? ">" : " ";
196
+ const selectedMark = state.selectedIds.has(ingredient.id) ? "[•]" : "[ ]";
197
+ const rowState = index === state.cursorIndex ? "current" : state.selectedIds.has(ingredient.id) ? "selected" : undefined;
198
+ return <Text state={rowState} style={rowState ? undefined : ROW_IDLE_STYLE} styles={{ current: ROW_CURRENT_STYLE, selected: ROW_SELECTED_STYLE }}>{`${marker} ${selectedMark} ${ingredient.name} ${ingredient.category} ${formatPrice(ingredient.price)}`}</Text>;
199
+ })}
200
+ </Pane>
201
+ <Pane style={SUMMARY_PANEL_STYLE}>
202
+ <Text>Order summary</Text>
203
+ <Text>{`Base pizza: ${formatPrice(BASE_PRICE)}`}</Text>
204
+ <Text>{selected.length === 0 ? "No toppings selected" : "Selected toppings:"}</Text>
205
+ {selected.map((ingredient) => (
206
+ <Text>{`- ${ingredient.name} ${formatPrice(ingredient.price)}`}</Text>
207
+ ))}
208
+ <Text style={TOTAL_VALUE_STYLE}>{`Total: ${formatPrice(total)}`}</Text>
209
+ </Pane>
210
+ <Pane style={CHECKOUT_PANEL_STYLE}>
211
+ <Text>Simulated checkout</Text>
212
+ <Text>{`Order name: ${state.orderName}`}</Text>
213
+ <Input
214
+ id="order-name"
215
+ value={state.orderName}
216
+ placeholder="Type a name"
217
+ onchange={(event) => {
218
+ state.orderName = event.value;
219
+ state.warning = "";
220
+ state.confirmed = false;
221
+ state.confirmationId = "";
222
+ }}
223
+ />
224
+ <Text>Payment method: demo card</Text>
225
+ <Text state="muted" styles={{ muted: MUTED_COPY_STYLE }}>No real payment is processed</Text>
226
+ <Text state={state.warning ? "warning" : state.confirmed ? "success" : undefined} style={!state.warning && !state.confirmed ? CHECKOUT_READY_STYLE : undefined} styles={{ warning: WARNING_STYLE, success: SUCCESS_STYLE }}>{state.warning || (state.confirmed ? "Confirmed" : "Press Enter on Confirm to finish.")}</Text>
227
+ <Text>{state.confirmationId ? `Confirmation: ${state.confirmationId}` : "Confirmation: pending"}</Text>
228
+ <Button id="confirm-order" onpress={() => confirmOrder(state)}>Confirm demo checkout</Button>
229
+ </Pane>
230
+ </Split>
231
+ <Fixed position="bottom" size={2}>
232
+ <Pane fill style={APP_FOOTER_STYLE}>
233
+ <Text>{state.warning || (state.confirmed ? `Confirmed Confirmation: ${state.confirmationId}` : "Ready for your order")}</Text>
234
+ <Text>Space: toggle ingredient Enter: next field/finish Tab: move focus R: resize C: clear Ctrl+C: quit</Text>
235
+ </Pane>
236
+ </Fixed>
237
+ </Screen>
238
+ );
239
+ }
240
+
241
+ export function createPizzaBuilderDemo(options: TerminalMountOptions = {}): PizzaBuilderDemo {
242
+ const initialSize = initialTerminalSize(options);
243
+ const state = createInitialState(initialSize);
244
+ let session: TerminalSession;
245
+ let cleanupOutputResize: (() => void) | null = null;
246
+
247
+ function resizeSession(size: { cols: number; rows: number }) {
248
+ state.terminalCols = size.cols;
249
+ state.terminalRows = size.rows;
250
+ state.layoutMode = layoutModeForCols(size.cols);
251
+ session.resize(size.cols, size.rows);
252
+ }
253
+
254
+ function quit() {
255
+ state.running = false;
256
+ cleanupOutputResize?.();
257
+ cleanupOutputResize = null;
258
+ session.destroy();
259
+ }
260
+
261
+ function watchOutputResize() {
262
+ const stdout = options.stdout;
263
+ if (!stdout || typeof stdout.on !== "function") {
264
+ return;
265
+ }
266
+
267
+ const removeResizeListener = typeof stdout.off === "function" ? stdout.off.bind(stdout) : stdout.removeListener?.bind(stdout);
268
+ if (!removeResizeListener) {
269
+ return;
270
+ }
271
+
272
+ const onResize = () => {
273
+ const cols = stdout.columns;
274
+ const rows = stdout.rows;
275
+ if (!isTerminalDimension(cols) || !isTerminalDimension(rows)) {
276
+ return;
277
+ }
278
+
279
+ resizeSession({ cols, rows });
280
+ };
281
+
282
+ try {
283
+ stdout.on("resize", onResize);
284
+ } catch {
285
+ return;
286
+ }
287
+
288
+ cleanupOutputResize = () => removeResizeListener("resize", onResize);
289
+ }
290
+
291
+ function moveCursor(step: number) {
292
+ state.cursorIndex = (state.cursorIndex + step + INGREDIENTS.length) % INGREDIENTS.length;
293
+ }
294
+
295
+ function toggleIngredient() {
296
+ const ingredient = INGREDIENTS[state.cursorIndex];
297
+ if (state.selectedIds.has(ingredient.id)) {
298
+ state.selectedIds.delete(ingredient.id);
299
+ } else {
300
+ state.selectedIds.add(ingredient.id);
301
+ }
302
+ state.confirmed = false;
303
+ state.confirmationId = "";
304
+ }
305
+
306
+ function resizeDemo() {
307
+ if (state.layoutMode === "wide") {
308
+ resizeSession(layoutSize("medium"));
309
+ return;
310
+ }
311
+ if (state.layoutMode === "medium") {
312
+ resizeSession(layoutSize("compact"));
313
+ return;
314
+ }
315
+ resizeSession(initialSize);
316
+ }
317
+
318
+ function resetOrder() {
319
+ state.cursorIndex = 0;
320
+ state.selectedIds.clear();
321
+ state.orderName = "";
322
+ state.warning = "";
323
+ state.confirmed = false;
324
+ state.confirmationId = "";
325
+ resizeSession(initialSize);
326
+ }
327
+
328
+ function clearOrder() {
329
+ resetOrder();
330
+ session.focus("ingredients-list");
331
+ }
332
+
333
+ function completeCheckout() {
334
+ confirmOrder(state);
335
+ if (state.warning) {
336
+ session.focus("order-name");
337
+ return;
338
+ }
339
+ resetOrder();
340
+ session.focus("ingredients-list");
341
+ }
342
+
343
+ session = mountTerminal(<App state={state} />, {
344
+ ...options,
345
+ cols: initialSize.cols,
346
+ rows: initialSize.rows,
347
+ theme: {
348
+ styles: options.theme?.styles,
349
+ spans: {
350
+ ...PIZZA_THEME.spans,
351
+ ...options.theme?.spans
352
+ }
353
+ },
354
+ keymap: {
355
+ ...options.keymap,
356
+ bindings: [
357
+ ...(options.keymap?.bindings || []),
358
+ { key: "UP", command: { id: "ingredients.prev" }, scope: "focus", when: { focusedId: "ingredients-list" } },
359
+ { key: "ARROWUP", command: { id: "ingredients.prev" }, scope: "focus", when: { focusedId: "ingredients-list" } },
360
+ { key: "k", command: { id: "ingredients.prev" }, scope: "focus", when: { focusedId: "ingredients-list" } },
361
+ { key: "DOWN", command: { id: "ingredients.next" }, scope: "focus", when: { focusedId: "ingredients-list" } },
362
+ { key: "ARROWDOWN", command: { id: "ingredients.next" }, scope: "focus", when: { focusedId: "ingredients-list" } },
363
+ { key: "j", command: { id: "ingredients.next" }, scope: "focus", when: { focusedId: "ingredients-list" } },
364
+ { key: "ENTER", command: { id: "focus.orderName" }, scope: "focus", when: { focusedId: "ingredients-list" } },
365
+ { key: "SPACE", command: { id: "ingredients.toggle" }, scope: "focus", when: { focusedId: "ingredients-list" } },
366
+ { key: " ", command: { id: "ingredients.toggle" }, scope: "focus", when: { focusedId: "ingredients-list" } },
367
+ { key: "r", command: { id: "layout.resize" }, scope: "focus", when: { focusedId: "ingredients-list" } },
368
+ { key: "R", command: { id: "layout.resize" }, scope: "focus", when: { focusedId: "ingredients-list" } },
369
+ { key: "c", command: { id: "order.clear" }, scope: "focus", when: { focusedId: "ingredients-list" } },
370
+ { key: "C", command: { id: "order.clear" }, scope: "focus", when: { focusedId: "ingredients-list" } },
371
+ { key: "UP", command: { id: "ingredients.prev" }, scope: "focus", when: { focusedId: "confirm-order" } },
372
+ { key: "ARROWUP", command: { id: "ingredients.prev" }, scope: "focus", when: { focusedId: "confirm-order" } },
373
+ { key: "k", command: { id: "ingredients.prev" }, scope: "focus", when: { focusedId: "confirm-order" } },
374
+ { key: "DOWN", command: { id: "ingredients.next" }, scope: "focus", when: { focusedId: "confirm-order" } },
375
+ { key: "ARROWDOWN", command: { id: "ingredients.next" }, scope: "focus", when: { focusedId: "confirm-order" } },
376
+ { key: "j", command: { id: "ingredients.next" }, scope: "focus", when: { focusedId: "confirm-order" } },
377
+ { key: "ENTER", command: { id: "order.complete" }, scope: "button", when: { focusedId: "confirm-order" } },
378
+ { key: " ", command: { id: "button.press" }, scope: "button", when: { focusedId: "confirm-order" } },
379
+ { key: "r", command: { id: "layout.resize" }, scope: "focus", when: { focusedId: "confirm-order" } },
380
+ { key: "R", command: { id: "layout.resize" }, scope: "focus", when: { focusedId: "confirm-order" } },
381
+ { key: "c", command: { id: "order.clear" }, scope: "focus", when: { focusedId: "confirm-order" } },
382
+ { key: "C", command: { id: "order.clear" }, scope: "focus", when: { focusedId: "confirm-order" } },
383
+ { key: "ENTER", command: { id: "focus.confirmOrder" }, scope: "input", when: { focusedId: "order-name" } },
384
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
385
+ ],
386
+ onCommand(command, context) {
387
+ if (command.id === "ingredients.prev") {
388
+ moveCursor(-1);
389
+ return true;
390
+ }
391
+ if (command.id === "ingredients.next") {
392
+ moveCursor(1);
393
+ return true;
394
+ }
395
+ if (command.id === "ingredients.toggle") {
396
+ toggleIngredient();
397
+ return true;
398
+ }
399
+ if (command.id === "layout.resize") {
400
+ resizeDemo();
401
+ return true;
402
+ }
403
+ if (command.id === "focus.orderName") {
404
+ session.focus("order-name");
405
+ return true;
406
+ }
407
+ if (command.id === "focus.confirmOrder") {
408
+ session.focus("confirm-order");
409
+ return true;
410
+ }
411
+ if (command.id === "order.complete") {
412
+ completeCheckout();
413
+ return true;
414
+ }
415
+ if (command.id === "order.clear") {
416
+ clearOrder();
417
+ return true;
418
+ }
419
+ if (command.id === "quit") {
420
+ quit();
421
+ return true;
422
+ }
423
+ return options.keymap?.onCommand?.(command, context);
424
+ }
425
+ }
426
+ });
427
+
428
+ session.focus("ingredients-list");
429
+ watchOutputResize();
430
+
431
+ return {
432
+ session,
433
+ dispatchKey(key: string) {
434
+ return session.dispatchKey(key);
435
+ },
436
+ output() {
437
+ return session.output();
438
+ },
439
+ ansiOutput() {
440
+ return session.ansiOutput();
441
+ },
442
+ isRunning() {
443
+ return state.running;
444
+ },
445
+ destroy() {
446
+ state.running = false;
447
+ cleanupOutputResize?.();
448
+ cleanupOutputResize = null;
449
+ session.destroy();
450
+ }
451
+ };
452
+ }
453
+
454
+ if (import.meta.main) {
455
+ if (shouldRunSnapshot()) {
456
+ const demo = createPizzaBuilderDemo({ runtime: "headless", ...SNAPSHOT_SIZE });
457
+ process.stdout.write(demo.output());
458
+ process.stdout.write("\n");
459
+ demo.destroy();
460
+ } else {
461
+ createPizzaBuilderDemo();
462
+ }
463
+ }