@pablozaiden/terminatui 0.2.0 → 0.3.0

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