@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.
- package/AGENTS.md +43 -0
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +62 -0
- package/examples/tui-app/commands/config/app/index.ts +23 -0
- package/examples/tui-app/commands/config/app/set.ts +96 -0
- package/examples/tui-app/commands/config/index.ts +28 -0
- package/examples/tui-app/commands/config/user/get.ts +61 -0
- package/examples/tui-app/commands/config/user/index.ts +23 -0
- package/examples/tui-app/commands/config/user/set.ts +57 -0
- package/examples/tui-app/commands/greet.ts +14 -11
- package/examples/tui-app/commands/math.ts +6 -9
- package/examples/tui-app/commands/status.ts +24 -13
- package/examples/tui-app/index.ts +7 -3
- package/guides/01-hello-world.md +7 -2
- package/guides/02-adding-options.md +2 -2
- package/guides/03-multiple-commands.md +6 -8
- package/guides/04-subcommands.md +8 -8
- package/guides/05-interactive-tui.md +45 -30
- package/guides/06-config-validation.md +4 -12
- package/guides/07-async-cancellation.md +15 -69
- package/guides/08-complete-application.md +13 -179
- package/guides/README.md +7 -3
- package/package.json +4 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +19 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +4 -4
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +70 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +52 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +160 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +60 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/examples/tui-app/commands/index.ts +0 -3
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/index.ts +0 -137
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -582
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
package/src/tui/TuiApp.tsx
DELETED
|
@@ -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
|
-
}
|