@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,218 @@
1
+ import { FormStore } from "valyrian.js/forms";
2
+ import { Input, 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 ProfileForm = { name: string; email: string };
15
+
16
+ interface SavedProfile {
17
+ name: string;
18
+ email: string;
19
+ }
20
+
21
+ interface FormDemoState {
22
+ form: FormStore<ProfileForm>;
23
+ valid: boolean;
24
+ savedProfile: SavedProfile | null;
25
+ submitMessage: string;
26
+ resetMessage: string;
27
+ running: boolean;
28
+ }
29
+
30
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
31
+ const FIELD_STYLE = { color: "#ffffff", background: "#0f3b3e", padding: { left: 1, right: 1 } };
32
+ const ERROR_STYLE = { color: "#fecaca" };
33
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
34
+
35
+ function shouldRunSnapshot() {
36
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
37
+ }
38
+
39
+ function createProfileForm(onSaved: (profile: SavedProfile) => void) {
40
+ return new FormStore<ProfileForm>({
41
+ state: { name: "", email: "" },
42
+ schema: {
43
+ type: "object",
44
+ properties: {
45
+ name: { type: "string", minLength: 1 },
46
+ email: { type: "string", format: "email" }
47
+ },
48
+ required: ["name", "email"]
49
+ },
50
+ clean: {
51
+ name: (value) => String(value).trim(),
52
+ email: (value) => String(value).trim().toLowerCase()
53
+ },
54
+ onSubmit(values) {
55
+ onSaved({ name: values.name, email: values.email });
56
+ }
57
+ });
58
+ }
59
+
60
+ function fieldError(form: FormStore<ProfileForm>, field: keyof ProfileForm) {
61
+ return String(form.validationErrors[field] || "not checked");
62
+ }
63
+
64
+ function savedSummary(profile: SavedProfile | null) {
65
+ return profile ? `${profile.name} / ${profile.email}` : "none";
66
+ }
67
+
68
+ export function App({ state, onSubmit }: { state: FormDemoState; onSubmit: () => void }) {
69
+ return (
70
+ <Screen title="Profile Editor">
71
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Profile Editor</Text>
72
+ <Pane style={FIELD_STYLE}>
73
+ <Text>Name</Text>
74
+ <Input
75
+ id="profile-name"
76
+ value={state.form.state.name}
77
+ placeholder="Full name"
78
+ onchange={(event) => {
79
+ state.form.setField("name", event.value);
80
+ state.valid = false;
81
+ }}
82
+ onsubmit={onSubmit}
83
+ />
84
+ <Text>{`Name: ${state.form.state.name || "blank"}`}</Text>
85
+ <Text style={ERROR_STYLE}>{`Name error: ${fieldError(state.form, "name")}`}</Text>
86
+ <Text>Email</Text>
87
+ <Input
88
+ id="profile-email"
89
+ value={state.form.state.email}
90
+ placeholder="name@example.com"
91
+ onchange={(event) => {
92
+ state.form.setField("email", event.value);
93
+ state.valid = false;
94
+ }}
95
+ onsubmit={onSubmit}
96
+ />
97
+ <Text>{`Email: ${state.form.state.email || "blank"}`}</Text>
98
+ <Text style={ERROR_STYLE}>{`Email error: ${fieldError(state.form, "email")}`}</Text>
99
+ </Pane>
100
+ <Pane style={PANEL_STYLE}>
101
+ <Text>{`Form valid: ${state.valid}`}</Text>
102
+ <Text>{`Submit: ${state.submitMessage}`}</Text>
103
+ <Text>{`Reset: ${state.resetMessage}`}</Text>
104
+ <Text>{`Saved profile: ${savedSummary(state.savedProfile)}`}</Text>
105
+ </Pane>
106
+ <Text style={FOOTER_STYLE}>Type profile fields | Tab: next field | Shift+Tab: previous field | Enter: save profile | R: reset form | Ctrl+C: quit</Text>
107
+ </Screen>
108
+ );
109
+ }
110
+
111
+ export function createModuleFormsDemo(options: TerminalMountOptions = {}): ModuleDemo {
112
+ const state: FormDemoState = {
113
+ form: null as unknown as FormStore<ProfileForm>,
114
+ valid: false,
115
+ savedProfile: null,
116
+ submitMessage: "not submitted",
117
+ resetMessage: "not reset",
118
+ running: true
119
+ };
120
+ state.form = createProfileForm((profile) => {
121
+ state.savedProfile = profile;
122
+ state.submitMessage = `saved ${profile.name} <${profile.email}>`;
123
+ });
124
+ let session: TerminalSession;
125
+
126
+ function resetForm() {
127
+ state.form.reset();
128
+ state.valid = false;
129
+ state.savedProfile = null;
130
+ state.submitMessage = "not submitted";
131
+ state.resetMessage = "form reset";
132
+ }
133
+
134
+ function submitForm() {
135
+ state.valid = state.form.validate();
136
+ if (!state.valid) {
137
+ state.submitMessage = "blocked by validation";
138
+ session.update();
139
+ return;
140
+ }
141
+
142
+ void state.form.submit().then((ok) => {
143
+ state.valid = ok;
144
+ if (!ok) state.submitMessage = "blocked by validation";
145
+ session.update();
146
+ });
147
+ }
148
+
149
+ function quit() {
150
+ state.running = false;
151
+ session.destroy();
152
+ }
153
+
154
+ session = mountTerminal(<App state={state} onSubmit={submitForm} />, {
155
+ ...options,
156
+ cols: options.cols ?? 92,
157
+ rows: options.rows ?? 20,
158
+ keymap: {
159
+ ...options.keymap,
160
+ bindings: [
161
+ ...(options.keymap?.bindings || []),
162
+ { key: "R", command: { id: "forms.reset" }, scope: "global" },
163
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
164
+ ],
165
+ onCommand(command, context) {
166
+ if (command.id === "forms.reset") {
167
+ resetForm();
168
+ return true;
169
+ }
170
+ if (command.id === "quit") {
171
+ quit();
172
+ return true;
173
+ }
174
+ return options.keymap?.onCommand?.(command, context);
175
+ }
176
+ }
177
+ });
178
+
179
+ session.focus("profile-name");
180
+
181
+ return {
182
+ session,
183
+ dispatchKey(key: string) {
184
+ return session.dispatchKey(key);
185
+ },
186
+ output() {
187
+ return session.output();
188
+ },
189
+ ansiOutput() {
190
+ return session.ansiOutput();
191
+ },
192
+ isRunning() {
193
+ return state.running;
194
+ },
195
+ destroy() {
196
+ state.running = false;
197
+ session.destroy();
198
+ }
199
+ };
200
+ }
201
+
202
+ if (import.meta.main) {
203
+ if (shouldRunSnapshot()) {
204
+ const demo = createModuleFormsDemo({ runtime: "headless", cols: 92, rows: 20 });
205
+ demo.dispatchKey("ENTER");
206
+ await new Promise((resolve) => setTimeout(resolve, 0));
207
+ for (const key of "AdaLovelace") demo.dispatchKey(key);
208
+ demo.dispatchKey("TAB");
209
+ for (const key of "ada@example.com") demo.dispatchKey(key);
210
+ demo.dispatchKey("ENTER");
211
+ await new Promise((resolve) => setTimeout(resolve, 0));
212
+ process.stdout.write(demo.output());
213
+ process.stdout.write("\n");
214
+ demo.destroy();
215
+ } else {
216
+ createModuleFormsDemo();
217
+ }
218
+ }
@@ -0,0 +1,175 @@
1
+ import { Money, formatMoney, parseMoneyInput } from "valyrian.js/money";
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 InvoiceLine = { name: string; unit: Money; quantity: number };
15
+ type InvoiceState = { selected: number; discount: boolean; localeIndex: number; lines: InvoiceLine[] };
16
+
17
+ const LOCALES = [
18
+ { label: "US / USD", locale: "en-US", currency: "USD" },
19
+ { label: "MX / USD", locale: "es-MX", currency: "USD" }
20
+ ] as const;
21
+ const INITIAL_LINES: InvoiceLine[] = [
22
+ { name: "Support seats", unit: parseMoneyInput("49.00", { locale: "en-US" }), quantity: 3 },
23
+ { name: "Priority add-on", unit: Money.fromDecimal(120), quantity: 1 },
24
+ { name: "Storage pack", unit: Money.fromCents(2500), quantity: 2 }
25
+ ];
26
+ const TAX_RATE = 0.16;
27
+ const DISCOUNT_RATE = 0.1;
28
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
29
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
30
+
31
+ function shouldRunSnapshot() {
32
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
33
+ }
34
+
35
+ function cloneLines() {
36
+ return INITIAL_LINES.map((line) => ({ ...line }));
37
+ }
38
+
39
+ function moneyView(value: Money, state: InvoiceState) {
40
+ const locale = LOCALES[state.localeIndex];
41
+ return formatMoney(value, { currency: locale.currency, locale: locale.locale, digits: 2 });
42
+ }
43
+
44
+ function invoiceTotals(state: InvoiceState) {
45
+ const subtotal = state.lines.reduce((total, line) => total.add(line.unit.multiply(line.quantity)), Money.fromCents(0));
46
+ const discount = state.discount ? subtotal.multiply(DISCOUNT_RATE) : Money.fromCents(0);
47
+ const taxable = subtotal.subtract(discount);
48
+ const tax = taxable.multiply(TAX_RATE);
49
+ const total = taxable.add(tax);
50
+ return { subtotal, discount, tax, total };
51
+ }
52
+
53
+ export function App({ state }: { state: InvoiceState }) {
54
+ const totals = invoiceTotals(state);
55
+ const locale = LOCALES[state.localeIndex];
56
+
57
+ return (
58
+ <Screen title="Invoice Calculator">
59
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Invoice Calculator</Text>
60
+ <Pane style={PANEL_STYLE}>
61
+ <Text>Invoice Calculator</Text>
62
+ <Text>{`Locale: ${locale.label}`}</Text>
63
+ {state.lines.map((line, index) => (
64
+ <Text>{`${index === state.selected ? ">" : " "} ${line.name} x${line.quantity} — ${moneyView(line.unit.multiply(line.quantity), state)}`}</Text>
65
+ ))}
66
+ <Text>{`Subtotal: ${moneyView(totals.subtotal, state)}`}</Text>
67
+ <Text>{`Discount: ${state.discount ? moneyView(totals.discount, state) : "none"}`}</Text>
68
+ <Text>{`Tax: ${moneyView(totals.tax, state)}`}</Text>
69
+ <Text>{`Total: ${moneyView(totals.total, state)}`}</Text>
70
+ </Pane>
71
+ <Text style={FOOTER_STYLE}>J/K line +/- quantity D discount L locale Ctrl+C: quit</Text>
72
+ </Screen>
73
+ );
74
+ }
75
+
76
+ export function createModuleMoneyDemo(options: TerminalMountOptions = {}): ModuleDemo {
77
+ const state: InvoiceState = { selected: 0, discount: false, localeIndex: 0, lines: cloneLines() };
78
+ let running = true;
79
+ let session: TerminalSession;
80
+
81
+ function quit() {
82
+ running = false;
83
+ session.destroy();
84
+ }
85
+
86
+ function changeQuantity(delta: number) {
87
+ const line = state.lines[state.selected];
88
+ line.quantity = Math.max(0, line.quantity + delta);
89
+ }
90
+
91
+ session = mountTerminal(<App state={state} />, {
92
+ ...options,
93
+ cols: options.cols ?? 92,
94
+ rows: options.rows ?? 20,
95
+ keymap: {
96
+ ...options.keymap,
97
+ bindings: [
98
+ ...(options.keymap?.bindings || []),
99
+ { key: "j", command: { id: "invoice.next" }, scope: "global" },
100
+ { key: "J", command: { id: "invoice.next" }, scope: "global" },
101
+ { key: "k", command: { id: "invoice.previous" }, scope: "global" },
102
+ { key: "K", command: { id: "invoice.previous" }, scope: "global" },
103
+ { key: "+", command: { id: "invoice.increase" }, scope: "global" },
104
+ { key: "-", command: { id: "invoice.decrease" }, scope: "global" },
105
+ { key: "d", command: { id: "invoice.discount" }, scope: "global" },
106
+ { key: "D", command: { id: "invoice.discount" }, scope: "global" },
107
+ { key: "l", command: { id: "invoice.locale" }, scope: "global" },
108
+ { key: "L", command: { id: "invoice.locale" }, scope: "global" },
109
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
110
+ ],
111
+ onCommand(command, context) {
112
+ if (command.id === "invoice.next") {
113
+ state.selected = (state.selected + 1) % state.lines.length;
114
+ return true;
115
+ }
116
+ if (command.id === "invoice.previous") {
117
+ state.selected = (state.selected - 1 + state.lines.length) % state.lines.length;
118
+ return true;
119
+ }
120
+ if (command.id === "invoice.increase") {
121
+ changeQuantity(1);
122
+ return true;
123
+ }
124
+ if (command.id === "invoice.decrease") {
125
+ changeQuantity(-1);
126
+ return true;
127
+ }
128
+ if (command.id === "invoice.discount") {
129
+ state.discount = !state.discount;
130
+ return true;
131
+ }
132
+ if (command.id === "invoice.locale") {
133
+ state.localeIndex = (state.localeIndex + 1) % LOCALES.length;
134
+ return true;
135
+ }
136
+ if (command.id === "quit") {
137
+ quit();
138
+ return true;
139
+ }
140
+ return options.keymap?.onCommand?.(command, context);
141
+ }
142
+ }
143
+ });
144
+
145
+ return {
146
+ session,
147
+ dispatchKey(key: string) {
148
+ return session.dispatchKey(key);
149
+ },
150
+ output() {
151
+ return session.output();
152
+ },
153
+ ansiOutput() {
154
+ return session.ansiOutput();
155
+ },
156
+ isRunning() {
157
+ return running;
158
+ },
159
+ destroy() {
160
+ running = false;
161
+ session.destroy();
162
+ }
163
+ };
164
+ }
165
+
166
+ if (import.meta.main) {
167
+ if (shouldRunSnapshot()) {
168
+ const demo = createModuleMoneyDemo({ runtime: "headless", cols: 92, rows: 20 });
169
+ process.stdout.write(demo.output());
170
+ process.stdout.write("\n");
171
+ demo.destroy();
172
+ } else {
173
+ createModuleMoneyDemo();
174
+ }
175
+ }
@@ -0,0 +1,188 @@
1
+ import { createNativeStore, StorageType } from "valyrian.js/native-store";
2
+ import { Input, 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 NativeStoreDemoOptions = TerminalMountOptions & {
15
+ storeKey?: string;
16
+ resetOnStart?: boolean;
17
+ };
18
+
19
+ interface WorkspaceSettingsState {
20
+ draftWorkspace: string;
21
+ restoredWorkspace: string;
22
+ savedWorkspace: string;
23
+ status: string;
24
+ }
25
+
26
+ const DEFAULT_WORKSPACE = "Northwind CLI";
27
+ const DEFAULT_STORE_KEY = "valyrian-terminal-docs.workspace-settings";
28
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
29
+ const FIELD_STYLE = { color: "#ffffff", background: "#0f3b3e", padding: { left: 1, right: 1 } };
30
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
31
+
32
+ function shouldRunSnapshot() {
33
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
34
+ }
35
+
36
+
37
+ function normalizeWorkspace(value: unknown) {
38
+ const workspace = String(value ?? "").trim();
39
+ return workspace.length > 0 ? workspace : DEFAULT_WORKSPACE;
40
+ }
41
+
42
+ export function App({ state, onSave }: { state: WorkspaceSettingsState; onSave: () => void }) {
43
+ return (
44
+ <Screen title="Persistent Workspace Settings">
45
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Persistent Workspace Settings</Text>
46
+ <Pane style={FIELD_STYLE}>
47
+ <Text>Workspace name</Text>
48
+ <Input
49
+ id="workspace-name"
50
+ value={state.draftWorkspace}
51
+ placeholder="Workspace name"
52
+ onchange={(event) => {
53
+ state.draftWorkspace = event.value;
54
+ state.status = "editing workspace setting";
55
+ }}
56
+ onsubmit={onSave}
57
+ />
58
+ <Text>{`Draft workspace: ${state.draftWorkspace || "blank"}`}</Text>
59
+ </Pane>
60
+ <Pane style={PANEL_STYLE}>
61
+ <Text>{`Restored workspace: ${state.restoredWorkspace}`}</Text>
62
+ <Text>{`Saved workspace: ${state.savedWorkspace}`}</Text>
63
+ <Text>{`Settings status: ${state.status}`}</Text>
64
+ <Text>Local native-store keeps workspace settings outside the renderer.</Text>
65
+ </Pane>
66
+ <Text style={FOOTER_STYLE}>Type workspace | Enter: save setting | R: reset setting | Ctrl+C: quit</Text>
67
+ </Screen>
68
+ );
69
+ }
70
+
71
+ export function createModuleNativeStoreDemo(options: NativeStoreDemoOptions = {}): ModuleDemo {
72
+ const settings = createNativeStore<{ workspace: string }>(options.storeKey ?? DEFAULT_STORE_KEY, {}, StorageType.Session, true);
73
+ if (options.resetOnStart) {
74
+ settings.clear();
75
+ }
76
+
77
+ function loadWorkspace() {
78
+ const restored = normalizeWorkspace(settings.get("workspace"));
79
+ if (settings.get("workspace") !== restored) settings.set("workspace", restored);
80
+ return restored;
81
+ }
82
+
83
+ const restoredWorkspace = loadWorkspace();
84
+ const state: WorkspaceSettingsState = {
85
+ draftWorkspace: restoredWorkspace,
86
+ restoredWorkspace,
87
+ savedWorkspace: restoredWorkspace,
88
+ status: "loaded saved setting"
89
+ };
90
+ let running = true;
91
+ let session: TerminalSession;
92
+
93
+ function saveWorkspace() {
94
+ const workspace = normalizeWorkspace(state.draftWorkspace);
95
+ settings.set("workspace", workspace);
96
+ state.draftWorkspace = workspace;
97
+ state.savedWorkspace = workspace;
98
+ state.restoredWorkspace = workspace;
99
+ state.status = "saved setting";
100
+ }
101
+
102
+ function reloadWorkspace() {
103
+ settings.load();
104
+ const workspace = loadWorkspace();
105
+ state.draftWorkspace = workspace;
106
+ state.savedWorkspace = workspace;
107
+ state.restoredWorkspace = workspace;
108
+ state.status = "loaded saved setting";
109
+ }
110
+
111
+ function resetWorkspace() {
112
+ settings.clear();
113
+ settings.set("workspace", DEFAULT_WORKSPACE);
114
+ state.draftWorkspace = "";
115
+ state.savedWorkspace = DEFAULT_WORKSPACE;
116
+ state.restoredWorkspace = DEFAULT_WORKSPACE;
117
+ state.status = "reset to default";
118
+ }
119
+
120
+ function quit() {
121
+ running = false;
122
+ settings.cleanup();
123
+ session.destroy();
124
+ }
125
+
126
+ session = mountTerminal(<App state={state} onSave={saveWorkspace} />, {
127
+ ...options,
128
+ cols: options.cols ?? 92,
129
+ rows: options.rows ?? 18,
130
+ keymap: {
131
+ ...options.keymap,
132
+ bindings: [
133
+ ...(options.keymap?.bindings || []),
134
+ { key: "R", command: { id: "store.reset" }, scope: "global" },
135
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
136
+ ],
137
+ onCommand(command, context) {
138
+ if (command.id === "store.reset") {
139
+ resetWorkspace();
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
+ session.focus("workspace-name");
152
+
153
+ return {
154
+ session,
155
+ dispatchKey(key: string) {
156
+ return session.dispatchKey(key);
157
+ },
158
+ output() {
159
+ return session.output();
160
+ },
161
+ ansiOutput() {
162
+ return session.ansiOutput();
163
+ },
164
+ isRunning() {
165
+ return running;
166
+ },
167
+ destroy() {
168
+ running = false;
169
+ settings.cleanup();
170
+ session.destroy();
171
+ }
172
+ };
173
+ }
174
+
175
+ if (import.meta.main) {
176
+ if (shouldRunSnapshot()) {
177
+ const demo = createModuleNativeStoreDemo({ runtime: "headless", cols: 92, rows: 18, resetOnStart: true });
178
+ demo.dispatchKey("R");
179
+ demo.session.focus("workspace-name");
180
+ for (const key of "Support CLI") demo.dispatchKey(key);
181
+ demo.dispatchKey("ENTER");
182
+ process.stdout.write(demo.output());
183
+ process.stdout.write("\n");
184
+ demo.destroy();
185
+ } else {
186
+ createModuleNativeStoreDemo();
187
+ }
188
+ }