@pablozaiden/terminatui 0.1.2 → 0.3.0-beta-1

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 (175) hide show
  1. package/AGENTS.md +43 -0
  2. package/CLAUDE.md +1 -0
  3. package/README.md +64 -43
  4. package/bun.lock +85 -0
  5. package/examples/tui-app/commands/config/app/get.ts +62 -0
  6. package/examples/tui-app/commands/config/app/index.ts +23 -0
  7. package/examples/tui-app/commands/config/app/set.ts +96 -0
  8. package/examples/tui-app/commands/config/index.ts +28 -0
  9. package/examples/tui-app/commands/config/user/get.ts +61 -0
  10. package/examples/tui-app/commands/config/user/index.ts +23 -0
  11. package/examples/tui-app/commands/config/user/set.ts +57 -0
  12. package/examples/tui-app/commands/greet.ts +14 -11
  13. package/examples/tui-app/commands/math.ts +6 -9
  14. package/examples/tui-app/commands/status.ts +24 -13
  15. package/examples/tui-app/index.ts +7 -3
  16. package/guides/01-hello-world.md +7 -2
  17. package/guides/02-adding-options.md +2 -2
  18. package/guides/03-multiple-commands.md +6 -8
  19. package/guides/04-subcommands.md +8 -8
  20. package/guides/05-interactive-tui.md +45 -30
  21. package/guides/06-config-validation.md +4 -12
  22. package/guides/07-async-cancellation.md +15 -69
  23. package/guides/08-complete-application.md +13 -179
  24. package/guides/README.md +7 -3
  25. package/package.json +4 -8
  26. package/src/__tests__/application.test.ts +87 -68
  27. package/src/__tests__/buildCliCommand.test.ts +99 -119
  28. package/src/__tests__/builtins.test.ts +27 -75
  29. package/src/__tests__/command.test.ts +100 -131
  30. package/src/__tests__/context.test.ts +1 -26
  31. package/src/__tests__/helpCore.test.ts +227 -0
  32. package/src/__tests__/parser.test.ts +98 -244
  33. package/src/__tests__/registry.test.ts +33 -160
  34. package/src/__tests__/schemaToFields.test.ts +75 -158
  35. package/src/builtins/help.ts +19 -4
  36. package/src/builtins/settings.ts +18 -32
  37. package/src/builtins/version.ts +4 -4
  38. package/src/cli/output/colors.ts +1 -1
  39. package/src/cli/parser.ts +26 -95
  40. package/src/core/application.ts +192 -110
  41. package/src/core/command.ts +26 -9
  42. package/src/core/context.ts +31 -20
  43. package/src/core/help.ts +24 -18
  44. package/src/core/knownCommands.ts +13 -0
  45. package/src/core/logger.ts +39 -42
  46. package/src/core/registry.ts +5 -12
  47. package/src/tui/TuiApplication.tsx +63 -120
  48. package/src/tui/TuiRoot.tsx +135 -0
  49. package/src/tui/adapters/factory.ts +19 -0
  50. package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
  51. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  52. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  53. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  54. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  55. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  56. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  57. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  58. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  59. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  60. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  61. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  62. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  63. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  64. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  65. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  66. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  67. package/src/tui/adapters/ink/keyboard.ts +97 -0
  68. package/src/tui/adapters/ink/utils.ts +16 -0
  69. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
  70. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  71. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  72. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  73. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  74. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  75. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  76. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  77. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  78. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  79. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  80. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  81. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  82. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  83. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  84. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  85. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  86. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  87. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  88. package/src/tui/adapters/types.ts +70 -0
  89. package/src/tui/components/ActionButton.tsx +0 -36
  90. package/src/tui/components/CommandSelector.tsx +52 -92
  91. package/src/tui/components/ConfigForm.tsx +68 -42
  92. package/src/tui/components/FieldRow.tsx +0 -30
  93. package/src/tui/components/Header.tsx +14 -13
  94. package/src/tui/components/JsonHighlight.tsx +10 -17
  95. package/src/tui/components/ModalBase.tsx +38 -0
  96. package/src/tui/components/ResultsPanel.tsx +27 -36
  97. package/src/tui/components/StatusBar.tsx +24 -39
  98. package/src/tui/components/logColors.ts +12 -0
  99. package/src/tui/context/ClipboardContext.tsx +87 -0
  100. package/src/tui/context/ExecutorContext.tsx +139 -0
  101. package/src/tui/context/KeyboardContext.tsx +85 -71
  102. package/src/tui/context/LogsContext.tsx +35 -0
  103. package/src/tui/context/NavigationContext.tsx +194 -0
  104. package/src/tui/context/RendererContext.tsx +20 -0
  105. package/src/tui/context/TuiAppContext.tsx +58 -0
  106. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  107. package/src/tui/hooks/useBackHandler.ts +34 -0
  108. package/src/tui/hooks/useClipboard.ts +40 -25
  109. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  110. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  111. package/src/tui/modals/CliModal.tsx +82 -0
  112. package/src/tui/modals/EditorModal.tsx +207 -0
  113. package/src/tui/modals/LogsModal.tsx +98 -0
  114. package/src/tui/registry.ts +102 -0
  115. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  116. package/src/tui/screens/ConfigScreen.tsx +160 -0
  117. package/src/tui/screens/ErrorScreen.tsx +58 -0
  118. package/src/tui/screens/ResultsScreen.tsx +60 -0
  119. package/src/tui/screens/RunningScreen.tsx +72 -0
  120. package/src/tui/screens/ScreenBase.ts +6 -0
  121. package/src/tui/semantic/Button.tsx +7 -0
  122. package/src/tui/semantic/Code.tsx +7 -0
  123. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  124. package/src/tui/semantic/Container.tsx +7 -0
  125. package/src/tui/semantic/Field.tsx +7 -0
  126. package/src/tui/semantic/Label.tsx +7 -0
  127. package/src/tui/semantic/MenuButton.tsx +7 -0
  128. package/src/tui/semantic/MenuItem.tsx +7 -0
  129. package/src/tui/semantic/Overlay.tsx +7 -0
  130. package/src/tui/semantic/Panel.tsx +7 -0
  131. package/src/tui/semantic/ScrollView.tsx +9 -0
  132. package/src/tui/semantic/Select.tsx +7 -0
  133. package/src/tui/semantic/Spacer.tsx +7 -0
  134. package/src/tui/semantic/Spinner.tsx +7 -0
  135. package/src/tui/semantic/TextInput.tsx +7 -0
  136. package/src/tui/semantic/Value.tsx +7 -0
  137. package/src/tui/semantic/types.ts +195 -0
  138. package/src/tui/theme.ts +25 -14
  139. package/src/tui/utils/buildCliCommand.ts +1 -0
  140. package/src/tui/utils/getEnumKeys.ts +3 -0
  141. package/src/tui/utils/parameterPersistence.ts +1 -0
  142. package/src/types/command.ts +0 -60
  143. package/examples/tui-app/commands/index.ts +0 -3
  144. package/src/__tests__/colors.test.ts +0 -127
  145. package/src/__tests__/commandClass.test.ts +0 -130
  146. package/src/__tests__/help.test.ts +0 -412
  147. package/src/__tests__/registryNew.test.ts +0 -160
  148. package/src/__tests__/table.test.ts +0 -146
  149. package/src/__tests__/tui.test.ts +0 -26
  150. package/src/builtins/index.ts +0 -4
  151. package/src/cli/help.ts +0 -174
  152. package/src/cli/index.ts +0 -3
  153. package/src/cli/output/index.ts +0 -2
  154. package/src/cli/output/table.ts +0 -141
  155. package/src/commands/help.ts +0 -50
  156. package/src/commands/index.ts +0 -1
  157. package/src/components/index.ts +0 -147
  158. package/src/core/index.ts +0 -15
  159. package/src/hooks/index.ts +0 -131
  160. package/src/index.ts +0 -137
  161. package/src/registry/commandRegistry.ts +0 -77
  162. package/src/registry/index.ts +0 -1
  163. package/src/tui/TuiApp.tsx +0 -582
  164. package/src/tui/app.ts +0 -29
  165. package/src/tui/components/CliModal.tsx +0 -81
  166. package/src/tui/components/EditorModal.tsx +0 -177
  167. package/src/tui/components/LogsPanel.tsx +0 -86
  168. package/src/tui/components/index.ts +0 -13
  169. package/src/tui/context/index.ts +0 -7
  170. package/src/tui/hooks/index.ts +0 -35
  171. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  172. package/src/tui/hooks/useLogStream.ts +0 -96
  173. package/src/tui/index.ts +0 -65
  174. package/src/tui/utils/index.ts +0 -13
  175. package/src/types/index.ts +0 -1
@@ -1,582 +0,0 @@
1
- import { useState, useCallback, useMemo } from "react";
2
- import { KeyboardProvider } from "./context/index.ts";
3
- import {
4
- Header,
5
- StatusBar,
6
- CommandSelector,
7
- ConfigForm,
8
- EditorModal,
9
- CliModal,
10
- LogsPanel,
11
- ResultsPanel,
12
- ActionButton,
13
- } from "./components/index.ts";
14
- import {
15
- useKeyboardHandler,
16
- KeyboardPriority,
17
- useClipboard,
18
- useLogStream,
19
- useCommandExecutor,
20
- type LogSource,
21
- } from "./hooks/index.ts";
22
- import { schemaToFieldConfigs, getFieldDisplayValue, buildCliCommand, loadPersistedParameters, savePersistedParameters } from "./utils/index.ts";
23
- import type { AnyCommand } from "../core/command.ts";
24
- import type { AppContext } from "../core/context.ts";
25
- import type { OptionValues, OptionSchema, OptionDef } from "../types/command.ts";
26
- import type { CustomField } from "./TuiApplication.tsx";
27
-
28
- /**
29
- * TUI application mode.
30
- */
31
- enum Mode {
32
- CommandSelect,
33
- Config,
34
- Running,
35
- Results,
36
- Error,
37
- }
38
-
39
- /**
40
- * Focused section for keyboard navigation.
41
- */
42
- enum FocusedSection {
43
- Config,
44
- Logs,
45
- Results,
46
- }
47
-
48
- interface TuiAppProps {
49
- /** Application name (CLI name) */
50
- name: string;
51
- /** Display name for TUI header (human-readable) */
52
- displayName?: string;
53
- /** Application version */
54
- version: string;
55
- /** Available commands */
56
- commands: AnyCommand[];
57
- /** Application context */
58
- context: AppContext;
59
- /** Log source for log panel */
60
- logSource?: LogSource;
61
- /** Custom fields to add to the TUI form */
62
- customFields?: CustomField[];
63
- /** Called when user wants to exit */
64
- onExit: () => void;
65
- }
66
-
67
- /**
68
- * Main TUI application component.
69
- * Wraps content with KeyboardProvider.
70
- */
71
- export function TuiApp(props: TuiAppProps) {
72
- return (
73
- <KeyboardProvider>
74
- <TuiAppContent {...props} />
75
- </KeyboardProvider>
76
- );
77
- }
78
-
79
- function TuiAppContent({
80
- name,
81
- displayName,
82
- version,
83
- commands,
84
- context,
85
- logSource,
86
- customFields,
87
- onExit,
88
- }: TuiAppProps) {
89
- // State
90
- const [mode, setMode] = useState<Mode>(Mode.CommandSelect);
91
- const [selectedCommand, setSelectedCommand] = useState<AnyCommand | null>(null);
92
- const [commandPath, setCommandPath] = useState<string[]>([]);
93
- const [commandSelectorIndex, setCommandSelectorIndex] = useState(0);
94
- const [selectedFieldIndex, setSelectedFieldIndex] = useState(0);
95
- const [editingField, setEditingField] = useState<string | null>(null);
96
- const [focusedSection, setFocusedSection] = useState<FocusedSection>(FocusedSection.Config);
97
- const [logsVisible, setLogsVisible] = useState(false);
98
- const [cliModalVisible, setCliModalVisible] = useState(false);
99
- const [configValues, setConfigValues] = useState<Record<string, unknown>>({});
100
-
101
- // Hooks
102
- const { logs, clearLogs } = useLogStream(logSource);
103
- const { copyWithMessage, lastAction } = useClipboard();
104
-
105
- // Command executor
106
- const executeCommand = useCallback(async (cmd: AnyCommand, values: Record<string, unknown>, signal: AbortSignal) => {
107
- // If the command provides buildConfig, build and validate before executing
108
- let configOrValues: unknown = values;
109
- if (cmd.buildConfig) {
110
- configOrValues = await cmd.buildConfig(context, values as OptionValues<OptionSchema>);
111
- }
112
-
113
- return await cmd.execute(context, configOrValues as OptionValues<OptionSchema>, { signal });
114
- }, [context]);
115
-
116
- const { isExecuting, result, error, execute, cancel, reset: resetExecutor } = useCommandExecutor(
117
- (cmd: unknown, values: unknown, signal: unknown) => executeCommand(cmd as AnyCommand, values as Record<string, unknown>, signal as AbortSignal)
118
- );
119
-
120
- // Computed values
121
- const fieldConfigs = useMemo(() => {
122
- if (!selectedCommand) return [];
123
- const commandFields = schemaToFieldConfigs(selectedCommand.options);
124
- // Merge custom fields if provided
125
- if (customFields && customFields.length > 0) {
126
- return [...commandFields, ...customFields];
127
- }
128
- return commandFields;
129
- }, [selectedCommand, customFields]);
130
-
131
- const cliCommand = useMemo(() => {
132
- if (!selectedCommand) return "";
133
- return buildCliCommand(name, commandPath, selectedCommand.options, configValues as OptionValues<OptionSchema>);
134
- }, [name, commandPath, selectedCommand, configValues]);
135
-
136
- const breadcrumb = useMemo(() => {
137
- return commandPath.length > 0 ? commandPath : undefined;
138
- }, [commandPath]);
139
-
140
- // Initialize config values when command changes
141
- const initializeConfigValues = useCallback((cmd: AnyCommand) => {
142
- const defaults: Record<string, unknown> = {};
143
- const optionDefs = cmd.options as OptionSchema;
144
- for (const [key, def] of Object.entries(optionDefs)) {
145
- const typedDef = def as OptionDef;
146
- if (typedDef.default !== undefined) {
147
- defaults[key] = typedDef.default;
148
- } else {
149
- switch (typedDef.type) {
150
- case "string":
151
- defaults[key] = typedDef.enum?.[0] ?? "";
152
- break;
153
- case "number":
154
- defaults[key] = typedDef.min ?? 0;
155
- break;
156
- case "boolean":
157
- defaults[key] = false;
158
- break;
159
- case "array":
160
- defaults[key] = [];
161
- break;
162
- }
163
- }
164
- }
165
- // Initialize custom field defaults
166
- if (customFields) {
167
- for (const field of customFields) {
168
- if (field.default !== undefined) {
169
- defaults[field.key] = field.default;
170
- }
171
- }
172
- }
173
-
174
- // Load persisted parameters and merge with defaults
175
- const persisted = loadPersistedParameters(name, cmd.name);
176
- const merged = { ...defaults, ...persisted };
177
-
178
- setConfigValues(merged);
179
- }, [customFields, name]);
180
-
181
- // Handlers
182
- const handleCommandSelect = useCallback((cmd: AnyCommand) => {
183
- // Check if command has subcommands to navigate into
184
- if (cmd.subCommands && cmd.subCommands.length > 0 && !cmd.supportsTui() && !cmd.supportsCli()) {
185
- // Navigate into subcommands
186
- setCommandPath((prev) => [...prev, cmd.name]);
187
- setCommandSelectorIndex(0);
188
- return;
189
- }
190
-
191
- setSelectedCommand(cmd);
192
- setCommandPath((prev) => [...prev, cmd.name]);
193
- initializeConfigValues(cmd);
194
- setSelectedFieldIndex(0);
195
- setFocusedSection(FocusedSection.Config);
196
- setLogsVisible(false);
197
-
198
- // Check if command should execute immediately
199
- if (cmd.immediateExecution) {
200
- handleRunCommand(cmd);
201
- } else {
202
- setMode(Mode.Config);
203
- }
204
- }, [initializeConfigValues]);
205
-
206
- const handleBack = useCallback(() => {
207
- if (mode === Mode.Running) {
208
- // Cancel the running command and go back
209
- cancel();
210
- // If command was immediate execution, go back to command select
211
- if (selectedCommand?.immediateExecution) {
212
- setMode(Mode.CommandSelect);
213
- setSelectedCommand(null);
214
- setCommandPath((prev) => prev.slice(0, -1));
215
- setSelectedFieldIndex(0);
216
- setFocusedSection(FocusedSection.Config);
217
- setLogsVisible(false);
218
- } else {
219
- setMode(Mode.Config);
220
- setFocusedSection(FocusedSection.Config);
221
- }
222
- resetExecutor();
223
- } else if (mode === Mode.Config) {
224
- setMode(Mode.CommandSelect);
225
- setSelectedCommand(null);
226
- setCommandPath((prev) => prev.slice(0, -1));
227
- setSelectedFieldIndex(0);
228
- setFocusedSection(FocusedSection.Config);
229
- setLogsVisible(false);
230
- } else if (mode === Mode.Results || mode === Mode.Error) {
231
- // If command was immediate execution, go back to command select
232
- if (selectedCommand?.immediateExecution) {
233
- setMode(Mode.CommandSelect);
234
- setSelectedCommand(null);
235
- setCommandPath((prev) => prev.slice(0, -1));
236
- setSelectedFieldIndex(0);
237
- setFocusedSection(FocusedSection.Config);
238
- setLogsVisible(false);
239
- } else {
240
- setMode(Mode.Config);
241
- setFocusedSection(FocusedSection.Config);
242
- }
243
- resetExecutor();
244
- } else if (mode === Mode.CommandSelect && commandPath.length > 0) {
245
- setCommandPath((prev) => prev.slice(0, -1));
246
- setCommandSelectorIndex(0);
247
- } else {
248
- onExit();
249
- }
250
- }, [mode, commandPath, selectedCommand, cancel, onExit, resetExecutor]);
251
-
252
- const handleRunCommand = useCallback(async (cmd?: AnyCommand) => {
253
- const cmdToRun = cmd ?? selectedCommand;
254
- if (!cmdToRun) return;
255
-
256
- // Save parameters before running
257
- savePersistedParameters(name, cmdToRun.name, configValues);
258
-
259
- // Set up for running
260
- setMode(Mode.Running);
261
- clearLogs();
262
- setLogsVisible(true);
263
- setFocusedSection(FocusedSection.Logs);
264
-
265
- // Execute and wait for result
266
- const outcome = await execute(cmdToRun, configValues);
267
-
268
- // If cancelled, don't transition - handleBack already handled it
269
- if (outcome.cancelled) {
270
- return;
271
- }
272
-
273
- // Transition based on outcome
274
- if (outcome.success) {
275
- setMode(Mode.Results);
276
- } else {
277
- setMode(Mode.Error);
278
- }
279
- setFocusedSection(FocusedSection.Results);
280
- }, [selectedCommand, configValues, clearLogs, execute, name]);
281
-
282
- const handleEditField = useCallback((fieldKey: string) => {
283
- setEditingField(fieldKey);
284
- }, []);
285
-
286
- const handleFieldSubmit = useCallback((value: unknown) => {
287
- if (editingField) {
288
- setConfigValues((prev) => {
289
- let newValues = { ...prev, [editingField]: value };
290
-
291
- // Call command's onConfigChange if available
292
- if (selectedCommand?.onConfigChange) {
293
- const updates = selectedCommand.onConfigChange(editingField, value, newValues);
294
- if (updates) {
295
- newValues = { ...newValues, ...updates };
296
- }
297
- }
298
-
299
- // Call custom field onChange if applicable
300
- const customField = customFields?.find((f) => f.key === editingField);
301
- if (customField?.onChange) {
302
- customField.onChange(value, newValues);
303
- }
304
- return newValues;
305
- });
306
- }
307
- setEditingField(null);
308
- }, [editingField, customFields, selectedCommand]);
309
-
310
- const handleCopy = useCallback((content: string, label: string) => {
311
- copyWithMessage(content, label);
312
- }, [copyWithMessage]);
313
-
314
- /**
315
- * Get clipboard content based on current mode and focused section.
316
- * Returns { content, label } or null if nothing to copy.
317
- */
318
- const getClipboardContent = useCallback((): { content: string; label: string } | null => {
319
- // In Running mode or when logs are focused, copy logs
320
- if (mode === Mode.Running || (logsVisible && focusedSection === FocusedSection.Logs)) {
321
- if (logs.length > 0) {
322
- const content = logs
323
- .map((log) => `[${log.level}] ${log.timestamp.toISOString()} ${log.message}`)
324
- .join("\n");
325
- return { content, label: "Logs" };
326
- }
327
- return null;
328
- }
329
-
330
- // In Results/Error mode with results focused
331
- if ((mode === Mode.Results || mode === Mode.Error) && focusedSection === FocusedSection.Results) {
332
- if (error) {
333
- return { content: error.message, label: "Error" };
334
- }
335
- if (result) {
336
- // Use command's getClipboardContent if available
337
- if (selectedCommand?.getClipboardContent) {
338
- const customContent = selectedCommand.getClipboardContent(result);
339
- if (customContent) {
340
- return { content: customContent, label: "Results" };
341
- }
342
- }
343
- return { content: JSON.stringify(result.data ?? result, null, 2), label: "Results" };
344
- }
345
- return null;
346
- }
347
-
348
- // In Config mode with config focused, copy config JSON
349
- if (mode === Mode.Config && focusedSection === FocusedSection.Config) {
350
- return { content: JSON.stringify(configValues, null, 2), label: "Config" };
351
- }
352
-
353
- return null;
354
- }, [mode, focusedSection, logsVisible, logs, error, result, configValues, selectedCommand]);
355
-
356
- const cycleFocusedSection = useCallback(() => {
357
- const sections: FocusedSection[] = [];
358
- if (mode === Mode.Config) sections.push(FocusedSection.Config);
359
- if (mode === Mode.Results || mode === Mode.Error) sections.push(FocusedSection.Results);
360
- if (logsVisible) sections.push(FocusedSection.Logs);
361
-
362
- if (sections.length <= 1) return;
363
-
364
- const currentIdx = sections.indexOf(focusedSection);
365
- const nextIdx = (currentIdx + 1) % sections.length;
366
- setFocusedSection(sections[nextIdx]!);
367
- }, [mode, logsVisible, focusedSection]);
368
-
369
- // Global keyboard handler
370
- useKeyboardHandler(
371
- (event) => {
372
- const { key } = event;
373
-
374
- // Escape to go back
375
- if (key.name === "escape") {
376
- handleBack();
377
- event.stopPropagation();
378
- return;
379
- }
380
-
381
- // Y to copy content based on current mode and focus
382
- if ((key.name === "y")) {
383
- const clipboardData = getClipboardContent();
384
- if (clipboardData) {
385
- handleCopy(clipboardData.content, clipboardData.label);
386
- }
387
- event.stopPropagation();
388
- return;
389
- }
390
-
391
- // Tab to cycle focus
392
- if (key.name === "tab") {
393
- cycleFocusedSection();
394
- event.stopPropagation();
395
- return;
396
- }
397
-
398
- // L to toggle logs
399
- if (key.name === "l" && !editingField) {
400
- setLogsVisible((prev) => !prev);
401
- event.stopPropagation();
402
- return;
403
- }
404
-
405
- // C to show CLI command
406
- if (key.name === "c" && !editingField && mode === Mode.Config) {
407
- setCliModalVisible(true);
408
- event.stopPropagation();
409
- return;
410
- }
411
- },
412
- KeyboardPriority.Global,
413
- { enabled: !editingField && !cliModalVisible }
414
- );
415
-
416
- // Get current commands for selector
417
- const currentCommands = useMemo(() => {
418
- if (commandPath.length === 0) {
419
- return commands;
420
- }
421
-
422
- // Navigate to current path
423
- let current: AnyCommand[] = commands;
424
- for (const pathPart of commandPath.slice(0, -1)) {
425
- const found = current.find((c) => c.name === pathPart);
426
- if (found?.subCommands) {
427
- current = found.subCommands;
428
- }
429
- }
430
- return current;
431
- }, [commands, commandPath]);
432
-
433
- // Status message
434
- const statusMessage = useMemo(() => {
435
- if (lastAction) return lastAction;
436
- if (isExecuting) return "Running benchmark...";
437
- if (mode === Mode.Error) return "Error occurred. Press Esc to go back.";
438
- if (mode === Mode.Results) return "Run completed. Press Esc to return to config.";
439
- if (mode === Mode.CommandSelect) return "Select a command to get started.";
440
- if (mode === Mode.Config) {
441
- return `Ready. Select [${selectedCommand?.actionLabel ?? "Run"}] and press Enter.`;
442
- }
443
- return "";
444
- }, [lastAction, isExecuting, mode, selectedCommand]);
445
-
446
- const shortcuts = useMemo(() => {
447
- const parts: string[] = [];
448
- if (mode === Mode.Config) {
449
- parts.push("↑↓ Navigate", "Enter Edit", "Y Copy", "C CLI", "L Logs", "Esc Back");
450
- } else if (mode === Mode.Running) {
451
- parts.push("Y Copy", "Esc Cancel");
452
- } else if (mode === Mode.Results || mode === Mode.Error) {
453
- parts.push("Tab Focus", "Y Copy", "Esc Back");
454
- } else {
455
- parts.push("↑↓ Navigate", "Enter Select", "Esc Exit");
456
- }
457
- return parts.join(" • ");
458
- }, [mode]);
459
-
460
- // Get display value for fields
461
- const getDisplayValue = useCallback((key: string, value: unknown, _type: string) => {
462
- const fieldConfig = fieldConfigs.find((f) => f.key === key);
463
- if (fieldConfig) {
464
- return getFieldDisplayValue(value, fieldConfig);
465
- }
466
- return String(value ?? "");
467
- }, [fieldConfigs]);
468
-
469
- // Render the main content based on current mode
470
- const renderContent = () => {
471
- switch (mode) {
472
- case Mode.CommandSelect:
473
- return (
474
- <CommandSelector
475
- commands={currentCommands.map((cmd) => ({ command: cmd }))}
476
- selectedIndex={commandSelectorIndex}
477
- onSelectionChange={setCommandSelectorIndex}
478
- onSelect={handleCommandSelect}
479
- onExit={handleBack}
480
- breadcrumb={commandPath.length > 0 ? commandPath : undefined}
481
- />
482
- );
483
-
484
- case Mode.Config:
485
- if (!selectedCommand) return null;
486
- return (
487
- <box flexDirection="column" flexGrow={1}>
488
- <ConfigForm
489
- title={`Configure: ${selectedCommand.name}`}
490
- fieldConfigs={fieldConfigs}
491
- values={configValues}
492
- selectedIndex={selectedFieldIndex}
493
- focused={focusedSection === FocusedSection.Config}
494
- onSelectionChange={setSelectedFieldIndex}
495
- onEditField={handleEditField}
496
- onAction={() => handleRunCommand()}
497
- getDisplayValue={getDisplayValue}
498
- actionButton={
499
- <ActionButton
500
- label={selectedCommand.actionLabel ?? "Run"}
501
- isSelected={selectedFieldIndex === fieldConfigs.length}
502
- />
503
- }
504
- />
505
- {logsVisible && (
506
- <LogsPanel
507
- logs={logs}
508
- visible={true}
509
- focused={focusedSection === FocusedSection.Logs}
510
- />
511
- )}
512
- </box>
513
- );
514
-
515
- case Mode.Running:
516
- return (
517
- <LogsPanel
518
- logs={logs}
519
- visible={true}
520
- focused={true}
521
- expanded={true}
522
- />
523
- );
524
-
525
- case Mode.Results:
526
- case Mode.Error:
527
- return (
528
- <box flexDirection="column" flexGrow={1} gap={1}>
529
- <ResultsPanel
530
- result={result}
531
- error={error}
532
- focused={focusedSection === FocusedSection.Results}
533
- renderResult={selectedCommand?.renderResult}
534
- />
535
- {logsVisible && (
536
- <LogsPanel
537
- logs={logs}
538
- visible={true}
539
- focused={focusedSection === FocusedSection.Logs}
540
- />
541
- )}
542
- </box>
543
- );
544
-
545
- default:
546
- return null;
547
- }
548
- };
549
-
550
- return (
551
- <box flexDirection="column" flexGrow={1} padding={1}>
552
- <Header name={displayName ?? name} version={version} breadcrumb={breadcrumb} />
553
-
554
- <box key={`content-${mode}-${isExecuting}`} flexDirection="column" flexGrow={1}>
555
- {renderContent()}
556
- </box>
557
-
558
- <StatusBar
559
- status={statusMessage}
560
- isRunning={isExecuting}
561
- shortcuts={shortcuts}
562
- />
563
-
564
- {/* Modals */}
565
- <EditorModal
566
- fieldKey={editingField}
567
- currentValue={editingField ? configValues[editingField] : undefined}
568
- visible={editingField !== null}
569
- onSubmit={handleFieldSubmit}
570
- onCancel={() => setEditingField(null)}
571
- fieldConfigs={fieldConfigs}
572
- />
573
-
574
- <CliModal
575
- command={cliCommand}
576
- visible={cliModalVisible}
577
- onClose={() => setCliModalVisible(false)}
578
- onCopy={handleCopy}
579
- />
580
- </box>
581
- );
582
- }
package/src/tui/app.ts DELETED
@@ -1,29 +0,0 @@
1
- /**
2
- * App configuration
3
- */
4
- export interface AppConfig {
5
- name: string;
6
- version?: string;
7
- description?: string;
8
- }
9
-
10
- /**
11
- * App state
12
- */
13
- export interface AppState {
14
- currentView: string;
15
- isLoading: boolean;
16
- error?: Error;
17
- }
18
-
19
- /**
20
- * Create a TUI application
21
- */
22
- export function createApp(_config: AppConfig) {
23
- return {
24
- run: async () => {
25
- // Placeholder for TUI app runner
26
- console.log("TUI app started");
27
- },
28
- };
29
- }
@@ -1,81 +0,0 @@
1
- import { Theme } from "../theme.ts";
2
- import { useKeyboardHandler, KeyboardPriority } from "../hooks/useKeyboardHandler.ts";
3
-
4
- interface CliModalProps {
5
- /** CLI command to display */
6
- command: string;
7
- /** Whether the modal is visible */
8
- visible: boolean;
9
- /** Called when the modal should close */
10
- onClose: () => void;
11
- /** Called when the command should be copied */
12
- onCopy?: (content: string, label: string) => void;
13
- }
14
-
15
- /**
16
- * Modal displaying the CLI command equivalent of the current config.
17
- */
18
- export function CliModal({
19
- command,
20
- visible,
21
- onClose,
22
- onCopy,
23
- }: CliModalProps) {
24
- // Modal keyboard handler
25
- useKeyboardHandler(
26
- (event) => {
27
- const { key } = event;
28
-
29
- if (key.name === "escape" || key.name === "return" || key.name === "enter") {
30
- onClose();
31
- event.stopPropagation();
32
- return;
33
- }
34
-
35
- // Y to copy
36
- if (key.name === "y") {
37
- onCopy?.(command, "CLI command");
38
- event.stopPropagation();
39
- return;
40
- }
41
- },
42
- KeyboardPriority.Modal,
43
- { enabled: visible, modal: true }
44
- );
45
-
46
- if (!visible) {
47
- return null;
48
- }
49
-
50
- return (
51
- <box
52
- position="absolute"
53
- top={4}
54
- left={4}
55
- width="80%"
56
- height={10}
57
- backgroundColor={Theme.overlay}
58
- border={true}
59
- borderStyle="rounded"
60
- borderColor={Theme.overlayTitle}
61
- padding={1}
62
- flexDirection="column"
63
- gap={1}
64
- zIndex={20}
65
- >
66
- <text fg={Theme.overlayTitle}>
67
- <strong>CLI Command</strong>
68
- </text>
69
-
70
- <scrollbox scrollX={true} height={3}>
71
- <text fg={Theme.value}>
72
- {command}
73
- </text>
74
- </scrollbox>
75
-
76
- <text fg={Theme.statusText}>
77
- Ctrl+Y to copy • Enter or Esc to close
78
- </text>
79
- </box>
80
- );
81
- }