@pablozaiden/terminatui 0.3.0-beta-1 → 0.4.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.
- package/package.json +10 -3
- package/src/__tests__/adapterNoSharedUi.test.ts +34 -0
- package/src/__tests__/configOnChange.test.ts +63 -0
- package/src/__tests__/schemaToFields.test.ts +0 -4
- package/src/__tests__/tuiRootNoCoupling.test.ts +25 -0
- package/src/builtins/version.ts +1 -1
- package/src/index.ts +22 -0
- package/src/tui/TuiApplication.tsx +0 -4
- package/src/tui/TuiRoot.tsx +58 -102
- package/src/tui/actions.ts +4 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +191 -41
- package/src/tui/adapters/ink/SemanticInkRenderer.tsx +210 -0
- package/src/tui/adapters/ink/components/Button.tsx +10 -2
- package/src/tui/adapters/ink/components/Overlay.tsx +8 -2
- package/src/tui/adapters/ink/components/Panel.tsx +26 -5
- package/src/tui/adapters/ink/components/ScrollView.tsx +44 -3
- package/src/tui/adapters/ink/components/Spinner.tsx +8 -2
- package/src/tui/adapters/ink/keyboard.ts +0 -3
- package/src/tui/adapters/ink/ui/CommandSelector.tsx +56 -0
- package/src/tui/adapters/ink/ui/ConfigForm.tsx +77 -0
- package/src/tui/adapters/ink/ui/Header.tsx +25 -0
- package/src/tui/adapters/ink/ui/JsonHighlight.tsx +21 -0
- package/src/tui/adapters/ink/ui/ResultsPanel.tsx +57 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +190 -39
- package/src/tui/adapters/opentui/SemanticOpenTuiRenderer.tsx +192 -0
- package/src/tui/adapters/opentui/components/Label.tsx +2 -2
- package/src/tui/adapters/opentui/components/Overlay.tsx +12 -3
- package/src/tui/adapters/opentui/components/Panel.tsx +11 -1
- package/src/tui/adapters/opentui/components/ScrollView.tsx +1 -8
- package/src/tui/adapters/opentui/components/Spinner.tsx +1 -1
- package/src/tui/adapters/opentui/keyboard.ts +0 -3
- package/src/tui/adapters/opentui/ui/CommandSelector.tsx +55 -0
- package/src/tui/adapters/opentui/ui/ConfigForm.tsx +74 -0
- package/src/tui/adapters/opentui/ui/Header.tsx +24 -0
- package/src/tui/adapters/opentui/ui/JsonHighlight.tsx +20 -0
- package/src/tui/adapters/opentui/ui/LogsPanel.tsx +44 -0
- package/src/tui/adapters/opentui/ui/ResultsPanel.tsx +62 -0
- package/src/tui/adapters/shared/TerminalClipboard.ts +65 -0
- package/src/tui/adapters/{opentui/hooks → shared}/useSpinner.ts +5 -1
- package/src/tui/adapters/types.ts +25 -45
- package/src/tui/components/JsonHighlight.tsx +41 -111
- package/src/tui/context/ActionContext.tsx +51 -0
- package/src/tui/context/ExecutorContext.tsx +7 -1
- package/src/tui/context/NavigationContext.tsx +20 -4
- package/src/tui/controllers/CommandBrowserController.tsx +100 -0
- package/src/tui/controllers/ConfigController.tsx +183 -0
- package/src/tui/controllers/EditorController.tsx +169 -0
- package/src/tui/controllers/LogsController.tsx +48 -0
- package/src/tui/controllers/OutcomeController.tsx +110 -0
- package/src/tui/driver/TuiDriver.tsx +148 -0
- package/src/tui/driver/context/TuiDriverContext.tsx +44 -0
- package/src/tui/driver/types.ts +72 -0
- package/src/tui/semantic/AppShell.tsx +30 -0
- package/src/tui/semantic/CommandBrowserScreen.tsx +16 -0
- package/src/tui/semantic/ConfigScreen.tsx +23 -0
- package/src/tui/semantic/EditorScreen.tsx +20 -0
- package/src/tui/semantic/LogsScreen.tsx +9 -0
- package/src/tui/semantic/RunningScreen.tsx +17 -0
- package/src/tui/semantic/layoutTypes.ts +72 -0
- package/src/tui/semantic/render.tsx +44 -0
- package/src/tui/semantic/types.ts +31 -98
- package/src/tui/utils/jsonTokenizer.ts +98 -0
- package/src/tui/utils/schemaToFields.ts +1 -25
- package/.devcontainer/devcontainer.json +0 -19
- package/.devcontainer/install-prerequisites.sh +0 -49
- package/.github/workflows/copilot-setup-steps.yml +0 -32
- package/.github/workflows/pull-request.yml +0 -27
- package/.github/workflows/release-npm-package.yml +0 -81
- package/AGENTS.md +0 -43
- package/CLAUDE.md +0 -1
- package/bun.lock +0 -321
- package/examples/tui-app/commands/config/app/get.ts +0 -62
- package/examples/tui-app/commands/config/app/index.ts +0 -23
- package/examples/tui-app/commands/config/app/set.ts +0 -96
- package/examples/tui-app/commands/config/index.ts +0 -28
- package/examples/tui-app/commands/config/user/get.ts +0 -61
- package/examples/tui-app/commands/config/user/index.ts +0 -23
- package/examples/tui-app/commands/config/user/set.ts +0 -57
- package/examples/tui-app/commands/greet.ts +0 -78
- package/examples/tui-app/commands/math.ts +0 -111
- package/examples/tui-app/commands/status.ts +0 -86
- package/examples/tui-app/index.ts +0 -38
- package/guides/01-hello-world.md +0 -101
- package/guides/02-adding-options.md +0 -103
- package/guides/03-multiple-commands.md +0 -161
- package/guides/04-subcommands.md +0 -206
- package/guides/05-interactive-tui.md +0 -209
- package/guides/06-config-validation.md +0 -256
- package/guides/07-async-cancellation.md +0 -334
- package/guides/08-complete-application.md +0 -507
- package/guides/README.md +0 -78
- package/src/tui/adapters/ink/components/Code.tsx +0 -6
- package/src/tui/adapters/ink/components/Container.tsx +0 -5
- package/src/tui/adapters/ink/components/Spacer.tsx +0 -15
- package/src/tui/adapters/ink/components/Value.tsx +0 -7
- package/src/tui/adapters/opentui/components/Code.tsx +0 -12
- package/src/tui/adapters/opentui/components/Container.tsx +0 -56
- package/src/tui/adapters/opentui/components/Spacer.tsx +0 -5
- package/src/tui/adapters/opentui/components/Value.tsx +0 -13
- package/src/tui/components/ActionButton.tsx +0 -0
- package/src/tui/components/CommandSelector.tsx +0 -119
- package/src/tui/components/ConfigForm.tsx +0 -174
- package/src/tui/components/FieldRow.tsx +0 -0
- package/src/tui/components/Header.tsx +0 -32
- package/src/tui/components/ModalBase.tsx +0 -38
- package/src/tui/components/ResultsPanel.tsx +0 -84
- package/src/tui/components/StatusBar.tsx +0 -44
- package/src/tui/components/logColors.ts +0 -12
- package/src/tui/components/types.ts +0 -30
- package/src/tui/context/ClipboardContext.tsx +0 -87
- package/src/tui/context/KeyboardContext.tsx +0 -132
- package/src/tui/hooks/useActiveKeyHandler.ts +0 -75
- package/src/tui/hooks/useClipboard.ts +0 -81
- package/src/tui/hooks/useClipboardProvider.ts +0 -42
- package/src/tui/hooks/useGlobalKeyHandler.ts +0 -54
- package/src/tui/modals/CliModal.tsx +0 -82
- package/src/tui/modals/EditorModal.tsx +0 -207
- package/src/tui/modals/LogsModal.tsx +0 -98
- package/src/tui/registry.ts +0 -102
- package/src/tui/screens/CommandSelectScreen.tsx +0 -162
- package/src/tui/screens/ConfigScreen.tsx +0 -160
- package/src/tui/screens/ErrorScreen.tsx +0 -58
- package/src/tui/screens/ResultsScreen.tsx +0 -60
- package/src/tui/screens/RunningScreen.tsx +0 -72
- package/src/tui/screens/ScreenBase.ts +0 -6
- package/src/tui/semantic/Button.tsx +0 -7
- package/src/tui/semantic/Code.tsx +0 -7
- package/src/tui/semantic/CodeHighlight.tsx +0 -7
- package/src/tui/semantic/Container.tsx +0 -7
- package/src/tui/semantic/Field.tsx +0 -7
- package/src/tui/semantic/Label.tsx +0 -7
- package/src/tui/semantic/MenuButton.tsx +0 -7
- package/src/tui/semantic/MenuItem.tsx +0 -7
- package/src/tui/semantic/Overlay.tsx +0 -7
- package/src/tui/semantic/Panel.tsx +0 -7
- package/src/tui/semantic/ScrollView.tsx +0 -9
- package/src/tui/semantic/Select.tsx +0 -7
- package/src/tui/semantic/Spacer.tsx +0 -7
- package/src/tui/semantic/Spinner.tsx +0 -7
- package/src/tui/semantic/TextInput.tsx +0 -7
- package/src/tui/semantic/Value.tsx +0 -7
- package/tsconfig.json +0 -25
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import { Container } from "../semantic/Container.tsx";
|
|
3
|
-
import { ScrollView } from "../semantic/ScrollView.tsx";
|
|
4
|
-
import { useActiveKeyHandler } from "../hooks/useActiveKeyHandler.ts";
|
|
5
|
-
import { useClipboardProvider } from "../hooks/useClipboardProvider.ts";
|
|
6
|
-
import { Label } from "../semantic/Label.tsx";
|
|
7
|
-
import { LogColors } from "../components/logColors.ts";
|
|
8
|
-
import { ModalBase } from "../components/ModalBase.tsx";
|
|
9
|
-
import { useLogs } from "../context/LogsContext.tsx";
|
|
10
|
-
import type { ModalComponent, ModalDefinition } from "../registry.ts";
|
|
11
|
-
import { LogLevel } from "../../core/logger.ts";
|
|
12
|
-
|
|
13
|
-
export interface LogsModalParams {}
|
|
14
|
-
|
|
15
|
-
export class LogsModal implements ModalDefinition<LogsModalParams> {
|
|
16
|
-
static readonly Id = "logs";
|
|
17
|
-
|
|
18
|
-
getId(): string {
|
|
19
|
-
return LogsModal.Id;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
component(): ModalComponent<LogsModalParams> {
|
|
23
|
-
return function LogsModalComponentWrapper({ params: _params, onClose }: { params: LogsModalParams; onClose: () => void; }) {
|
|
24
|
-
return (
|
|
25
|
-
<LogsModalView
|
|
26
|
-
visible={true}
|
|
27
|
-
onClose={onClose}
|
|
28
|
-
/>
|
|
29
|
-
);
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface LogsModalViewProps {
|
|
35
|
-
/** Whether the panel is visible */
|
|
36
|
-
visible: boolean;
|
|
37
|
-
/** Callback when the modal is closed */
|
|
38
|
-
onClose: () => void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Panel displaying log entries with color-coded levels.
|
|
43
|
-
*/
|
|
44
|
-
function LogsModalView({
|
|
45
|
-
visible,
|
|
46
|
-
onClose,
|
|
47
|
-
}: LogsModalViewProps) {
|
|
48
|
-
const { logs } = useLogs();
|
|
49
|
-
// Handle Enter to close (Esc and Ctrl+L are handled globally)
|
|
50
|
-
useActiveKeyHandler(
|
|
51
|
-
(event) => {
|
|
52
|
-
if (event.name === "return" || event.name === "enter") {
|
|
53
|
-
onClose();
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
return false;
|
|
57
|
-
},
|
|
58
|
-
{ enabled: visible }
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
// Register clipboard provider - logs content takes precedence when modal is open
|
|
62
|
-
useClipboardProvider(
|
|
63
|
-
useCallback(() => ({
|
|
64
|
-
content: logs.map((l) => l.message).join("\n"),
|
|
65
|
-
label: "Logs",
|
|
66
|
-
}), [logs]),
|
|
67
|
-
visible
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
if (!visible) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const title = `Logs - ${logs.length}`;
|
|
75
|
-
|
|
76
|
-
return (
|
|
77
|
-
<ModalBase title={title} top={4} bottom={4} left={4} right={4}>
|
|
78
|
-
<ScrollView axis="vertical" flex={1} stickyToEnd={true} focused={true}>
|
|
79
|
-
<Container flexDirection="column" gap={0}>
|
|
80
|
-
{logs.map((log, idx) => {
|
|
81
|
-
const color = LogColors[log.level] ?? LogColors[LogLevel.info];
|
|
82
|
-
const sanitized = Bun.stripANSI(log.message).trim();
|
|
83
|
-
|
|
84
|
-
return (
|
|
85
|
-
<Label key={`${log.timestamp.getTime()}-${idx}`}>
|
|
86
|
-
<span fg={color}>{sanitized}</span>
|
|
87
|
-
</Label>
|
|
88
|
-
);
|
|
89
|
-
})}
|
|
90
|
-
|
|
91
|
-
{logs.length === 0 && (
|
|
92
|
-
<Label color="mutedText">No logs yet...</Label>
|
|
93
|
-
)}
|
|
94
|
-
</Container>
|
|
95
|
-
</ScrollView>
|
|
96
|
-
</ModalBase>
|
|
97
|
-
);
|
|
98
|
-
}
|
package/src/tui/registry.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { CliModal } from "./modals/CliModal.tsx";
|
|
3
|
-
import { EditorModal } from "./modals/EditorModal.tsx";
|
|
4
|
-
import { LogsModal } from "./modals/LogsModal.tsx";
|
|
5
|
-
import type { ScreenBase } from "./screens/ScreenBase";
|
|
6
|
-
import { CommandSelectScreen } from "./screens/CommandSelectScreen";
|
|
7
|
-
import { ConfigScreen } from "./screens/ConfigScreen";
|
|
8
|
-
import { ErrorScreen } from "./screens/ErrorScreen";
|
|
9
|
-
import { ResultsScreen } from "./screens/ResultsScreen";
|
|
10
|
-
import { RunningScreen } from "./screens/RunningScreen";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Screen component type.
|
|
14
|
-
* Screens receive no props - they get everything from context.
|
|
15
|
-
*/
|
|
16
|
-
export type ScreenComponent = () => ReactNode;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Modal component type.
|
|
20
|
-
* Modals receive their params and a close function.
|
|
21
|
-
*/
|
|
22
|
-
export type ModalComponent<TParams> = (props: {
|
|
23
|
-
params: TParams;
|
|
24
|
-
onClose: () => void;
|
|
25
|
-
}) => ReactNode;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Screen registry - maps route names to screen components.
|
|
30
|
-
*/
|
|
31
|
-
const screenRegistry = new Map<string, ScreenComponent>();
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Modal registry - maps modal IDs to modal components.
|
|
35
|
-
*/
|
|
36
|
-
const modalRegistry = new Map<string, ModalComponent<any>>();
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Register a screen component for a route.
|
|
40
|
-
* Typically called at module load time.
|
|
41
|
-
*/
|
|
42
|
-
|
|
43
|
-
export function registerScreen(screen: ScreenBase): void {
|
|
44
|
-
screenRegistry.set(screen.getRoute(), screen.component());
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Register a modal component for a modal ID.
|
|
49
|
-
* Typically called at module load time.
|
|
50
|
-
*/
|
|
51
|
-
export interface ModalDefinition<TParams> {
|
|
52
|
-
getId(): string;
|
|
53
|
-
component(): ModalComponent<TParams>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function registerModal<TParams>(modal: ModalDefinition<TParams>): void {
|
|
57
|
-
modalRegistry.set(modal.getId(), modal.component());
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get a screen component by route.
|
|
62
|
-
* Returns undefined if not registered.
|
|
63
|
-
*/
|
|
64
|
-
export function getScreen(route: string): ScreenComponent | undefined {
|
|
65
|
-
return screenRegistry.get(route);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Get a modal component by ID.
|
|
70
|
-
* Returns undefined if not registered.
|
|
71
|
-
*/
|
|
72
|
-
export function getModal<TParams>(id: string): ModalComponent<TParams> | undefined {
|
|
73
|
-
return modalRegistry.get(id);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get all registered screen routes.
|
|
78
|
-
*/
|
|
79
|
-
export function getRegisteredScreens(): string[] {
|
|
80
|
-
return Array.from(screenRegistry.keys());
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Get all registered modal IDs.
|
|
85
|
-
*/
|
|
86
|
-
export function getRegisteredModals(): string[] {
|
|
87
|
-
return Array.from(modalRegistry.keys());
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export function registerAllScreens(): void {
|
|
91
|
-
registerScreen(new CommandSelectScreen());
|
|
92
|
-
registerScreen(new ConfigScreen());
|
|
93
|
-
registerScreen(new RunningScreen());
|
|
94
|
-
registerScreen(new ResultsScreen());
|
|
95
|
-
registerScreen(new ErrorScreen());
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function registerAllModals(): void {
|
|
99
|
-
registerModal(new EditorModal());
|
|
100
|
-
registerModal(new CliModal());
|
|
101
|
-
registerModal(new LogsModal());
|
|
102
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { useState, useMemo, useCallback } from "react";
|
|
2
|
-
import type { AnyCommand } from "../../core/command.ts";
|
|
3
|
-
import { CommandSelector } from "../components/CommandSelector.tsx";
|
|
4
|
-
import { useTuiApp } from "../context/TuiAppContext.tsx";
|
|
5
|
-
import { useNavigation } from "../context/NavigationContext.tsx";
|
|
6
|
-
import { useBackHandler } from "../hooks/useBackHandler.ts";
|
|
7
|
-
import type { ScreenComponent } from "../registry.ts";
|
|
8
|
-
import { loadPersistedParameters } from "../utils/parameterPersistence.ts";
|
|
9
|
-
import { schemaToFieldConfigs } from "../utils/schemaToFields.ts";
|
|
10
|
-
import type { OptionDef, OptionSchema } from "../../types/command.ts";
|
|
11
|
-
import { ScreenBase } from "./ScreenBase.ts";
|
|
12
|
-
import { type ConfigParams, ConfigScreen } from "./ConfigScreen.tsx";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Screen state stored in navigation params.
|
|
16
|
-
*/
|
|
17
|
-
export interface CommandSelectParams {
|
|
18
|
-
commandPath: string[];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Command selection screen.
|
|
23
|
-
* Fully self-contained - gets all data from context and handles its own transitions.
|
|
24
|
-
*/
|
|
25
|
-
export class CommandSelectScreen extends ScreenBase {
|
|
26
|
-
static readonly Id = "command-select";
|
|
27
|
-
|
|
28
|
-
getRoute(): string {
|
|
29
|
-
return CommandSelectScreen.Id;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
override component(): ScreenComponent {
|
|
33
|
-
return function CommandSelectScreenComponent() {
|
|
34
|
-
const { name, commands } = useTuiApp();
|
|
35
|
-
const navigation = useNavigation();
|
|
36
|
-
|
|
37
|
-
// Get params from navigation, with defaults
|
|
38
|
-
const params = (navigation.current.params ?? { commandPath: [] }) as CommandSelectParams;
|
|
39
|
-
const commandPath = params.commandPath ?? [];
|
|
40
|
-
|
|
41
|
-
// Local selection state
|
|
42
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
43
|
-
|
|
44
|
-
// Get current commands based on path
|
|
45
|
-
const currentCommands = useMemo<AnyCommand[]>(() => {
|
|
46
|
-
if (commandPath.length === 0) {
|
|
47
|
-
return commands.filter((cmd) => cmd.supportsTui());
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let current: AnyCommand[] = commands;
|
|
51
|
-
for (const pathPart of commandPath) {
|
|
52
|
-
const found = current.find((c) => c.name === pathPart);
|
|
53
|
-
if (found?.subCommands) {
|
|
54
|
-
current = found.subCommands.filter((sub) => sub.supportsTui());
|
|
55
|
-
} else {
|
|
56
|
-
break;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return current;
|
|
60
|
-
}, [commands, commandPath]);
|
|
61
|
-
|
|
62
|
-
// Build breadcrumb from path
|
|
63
|
-
const breadcrumb = useMemo(() => {
|
|
64
|
-
if (commandPath.length === 0) return undefined;
|
|
65
|
-
|
|
66
|
-
const displayNames: string[] = [];
|
|
67
|
-
let current: AnyCommand[] = commands;
|
|
68
|
-
|
|
69
|
-
for (const pathPart of commandPath) {
|
|
70
|
-
const found = current.find((c) => c.name === pathPart);
|
|
71
|
-
if (found) {
|
|
72
|
-
displayNames.push(found.displayName ?? found.name);
|
|
73
|
-
if (found.subCommands) {
|
|
74
|
-
current = found.subCommands;
|
|
75
|
-
}
|
|
76
|
-
} else {
|
|
77
|
-
displayNames.push(pathPart);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return displayNames;
|
|
82
|
-
}, [commandPath, commands]);
|
|
83
|
-
|
|
84
|
-
const items = currentCommands.map((cmd) => ({
|
|
85
|
-
command: cmd,
|
|
86
|
-
label: cmd.displayName ?? cmd.name,
|
|
87
|
-
description: cmd.description,
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
|
-
// Handle command selection - this screen decides where to go next
|
|
91
|
-
const handleSelect = useCallback((cmd: AnyCommand) => {
|
|
92
|
-
// If command has runnable subcommands, navigate deeper
|
|
93
|
-
if (cmd.subCommands && cmd.subCommands.some((c) => c.supportsTui())) {
|
|
94
|
-
navigation.replace<CommandSelectParams>(CommandSelectScreen.Id, { commandPath: [...commandPath, cmd.name] });
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Otherwise, push to config screen
|
|
99
|
-
navigation.push<ConfigParams>(ConfigScreen.Id, {
|
|
100
|
-
command: cmd,
|
|
101
|
-
commandPath: [...commandPath, cmd.name],
|
|
102
|
-
values: initializeConfigValues(name, cmd),
|
|
103
|
-
fieldConfigs: schemaToFieldConfigs(cmd.options),
|
|
104
|
-
});
|
|
105
|
-
}, [navigation, commandPath, name]);
|
|
106
|
-
|
|
107
|
-
// Register back handler - this screen decides what back means
|
|
108
|
-
useBackHandler(useCallback(() => {
|
|
109
|
-
if (commandPath.length > 0) {
|
|
110
|
-
// Go up one level
|
|
111
|
-
navigation.replace<CommandSelectParams>(CommandSelectScreen.Id, { commandPath: commandPath.slice(0, -1) });
|
|
112
|
-
return true; // We handled it
|
|
113
|
-
}
|
|
114
|
-
// At root - let navigation call onExit
|
|
115
|
-
return false;
|
|
116
|
-
}, [navigation, commandPath]));
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<CommandSelector
|
|
120
|
-
commands={items}
|
|
121
|
-
selectedIndex={selectedIndex}
|
|
122
|
-
onSelectionChange={setSelectedIndex}
|
|
123
|
-
onSelect={handleSelect}
|
|
124
|
-
breadcrumb={breadcrumb}
|
|
125
|
-
/>
|
|
126
|
-
);
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Initialize config values from defaults and persisted values.
|
|
133
|
-
*/
|
|
134
|
-
function initializeConfigValues(appName: string, cmd: AnyCommand): Record<string, unknown> {
|
|
135
|
-
const defaults: Record<string, unknown> = {};
|
|
136
|
-
const optionDefs = cmd.options as OptionSchema;
|
|
137
|
-
|
|
138
|
-
for (const [key, def] of Object.entries(optionDefs)) {
|
|
139
|
-
const typedDef = def as OptionDef;
|
|
140
|
-
if (typedDef.default !== undefined) {
|
|
141
|
-
defaults[key] = typedDef.default;
|
|
142
|
-
} else {
|
|
143
|
-
switch (typedDef.type) {
|
|
144
|
-
case "string":
|
|
145
|
-
defaults[key] = typedDef.enum?.[0] ?? "";
|
|
146
|
-
break;
|
|
147
|
-
case "number":
|
|
148
|
-
defaults[key] = typedDef.min ?? 0;
|
|
149
|
-
break;
|
|
150
|
-
case "boolean":
|
|
151
|
-
defaults[key] = false;
|
|
152
|
-
break;
|
|
153
|
-
case "array":
|
|
154
|
-
defaults[key] = [];
|
|
155
|
-
break;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const persisted = loadPersistedParameters(appName, cmd.name);
|
|
161
|
-
return { ...defaults, ...persisted };
|
|
162
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { useCallback, useMemo, useState } from "react";
|
|
2
|
-
import type { AnyCommand } from "../../core/command.ts";
|
|
3
|
-
import type { FieldConfig } from "../components/types.ts";
|
|
4
|
-
import { ConfigForm } from "../components/ConfigForm.tsx";
|
|
5
|
-
import { Container } from "../semantic/Container.tsx";
|
|
6
|
-
import { MenuButton } from "../semantic/MenuButton.tsx";
|
|
7
|
-
import { schemaToFieldConfigs } from "../utils/schemaToFields.ts";
|
|
8
|
-
import { useClipboardProvider } from "../hooks/useClipboardProvider.ts";
|
|
9
|
-
import { buildCliCommand } from "../utils/buildCliCommand.ts";
|
|
10
|
-
import { useTuiApp } from "../context/TuiAppContext.tsx";
|
|
11
|
-
import { useNavigation } from "../context/NavigationContext.tsx";
|
|
12
|
-
import { useExecutor } from "../context/ExecutorContext.tsx";
|
|
13
|
-
import type { ScreenComponent } from "../registry.ts";
|
|
14
|
-
import { savePersistedParameters } from "../utils/parameterPersistence.ts";
|
|
15
|
-
import type { OptionSchema, OptionValues } from "../../types/command.ts";
|
|
16
|
-
import { ScreenBase } from "./ScreenBase.ts";
|
|
17
|
-
import type { EditorModalParams } from "../modals/EditorModal.tsx";
|
|
18
|
-
import type { CliModalParams } from "../modals/CliModal.tsx";
|
|
19
|
-
import { RunningScreen, type RunningParams } from "./RunningScreen.tsx";
|
|
20
|
-
import { type ErrorParams, ErrorScreen } from "./ErrorScreen.tsx";
|
|
21
|
-
import { type ResultsParams, ResultsScreen } from "./ResultsScreen.tsx";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Screen state stored in navigation params.
|
|
25
|
-
*/
|
|
26
|
-
export interface ConfigParams {
|
|
27
|
-
command: AnyCommand;
|
|
28
|
-
commandPath: string[];
|
|
29
|
-
values: Record<string, unknown>;
|
|
30
|
-
fieldConfigs: FieldConfig[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Config screen for editing command options before execution.
|
|
35
|
-
* Fully self-contained - gets all data from context and handles its own transitions.
|
|
36
|
-
*/
|
|
37
|
-
export class ConfigScreen extends ScreenBase {
|
|
38
|
-
static readonly Id = "config";
|
|
39
|
-
getRoute(): string {
|
|
40
|
-
return ConfigScreen.Id;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
override component(): ScreenComponent {
|
|
44
|
-
return function ConfigScreenComponent() {
|
|
45
|
-
const { name: appName } = useTuiApp();
|
|
46
|
-
const navigation = useNavigation();
|
|
47
|
-
const executor = useExecutor();
|
|
48
|
-
|
|
49
|
-
// Get params from navigation
|
|
50
|
-
const params = navigation.current.params as ConfigParams | undefined;
|
|
51
|
-
if (!params) return null;
|
|
52
|
-
|
|
53
|
-
const { command, commandPath, values, fieldConfigs } = params;
|
|
54
|
-
|
|
55
|
-
// Local selection state for the form
|
|
56
|
-
const [selectedFieldIndex, setSelectedFieldIndex] = useState(0);
|
|
57
|
-
|
|
58
|
-
// Derive field configs (in case they weren't passed)
|
|
59
|
-
const derivedFieldConfigs = useMemo(
|
|
60
|
-
() => fieldConfigs ?? schemaToFieldConfigs(command.options),
|
|
61
|
-
[fieldConfigs, command.options]
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
// Register clipboard provider for this screen
|
|
65
|
-
useClipboardProvider(
|
|
66
|
-
useCallback(() => ({
|
|
67
|
-
content: JSON.stringify(values, null, 2),
|
|
68
|
-
label: "Config",
|
|
69
|
-
}), [values])
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
// Handle running the command
|
|
73
|
-
const handleRun = useCallback(async () => {
|
|
74
|
-
// Save parameters for next time
|
|
75
|
-
savePersistedParameters(appName, command.name, values);
|
|
76
|
-
|
|
77
|
-
// Push to running screen
|
|
78
|
-
navigation.push<RunningParams>(RunningScreen.Id, {
|
|
79
|
-
command,
|
|
80
|
-
commandPath,
|
|
81
|
-
values,
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
// Execute the command
|
|
85
|
-
const outcome = await executor.execute(command, values);
|
|
86
|
-
|
|
87
|
-
if (outcome.cancelled) {
|
|
88
|
-
// If cancelled, pop back to config
|
|
89
|
-
navigation.pop();
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (outcome.success) {
|
|
94
|
-
// Replace running with results
|
|
95
|
-
navigation.replace<ResultsParams>(ResultsScreen.Id, {
|
|
96
|
-
command,
|
|
97
|
-
commandPath,
|
|
98
|
-
values,
|
|
99
|
-
result: outcome.result ?? null,
|
|
100
|
-
});
|
|
101
|
-
} else {
|
|
102
|
-
// Replace running with error
|
|
103
|
-
navigation.replace<ErrorParams>(ErrorScreen.Id, {
|
|
104
|
-
command,
|
|
105
|
-
commandPath,
|
|
106
|
-
values,
|
|
107
|
-
error: outcome.error ?? new Error("Unknown error"),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}, [appName, command, commandPath, values, navigation, executor]);
|
|
111
|
-
|
|
112
|
-
// Handle editing a field - open property editor modal
|
|
113
|
-
const handleEditField = useCallback((fieldKey: string) => {
|
|
114
|
-
navigation.openModal<EditorModalParams>("property-editor", {
|
|
115
|
-
fieldKey,
|
|
116
|
-
currentValue: values[fieldKey],
|
|
117
|
-
fieldConfigs: derivedFieldConfigs,
|
|
118
|
-
onSubmit: (value: unknown) => {
|
|
119
|
-
const nextValues = { ...values, [fieldKey]: value };
|
|
120
|
-
navigation.replace<ConfigParams>(ConfigScreen.Id, { ...params, values: nextValues });
|
|
121
|
-
navigation.closeModal();
|
|
122
|
-
},
|
|
123
|
-
onCancel: () => navigation.closeModal(),
|
|
124
|
-
});
|
|
125
|
-
}, [navigation, values, derivedFieldConfigs, params]);
|
|
126
|
-
|
|
127
|
-
// Handle opening the CLI Args modal
|
|
128
|
-
const handleShowCliArgs = useCallback(() => {
|
|
129
|
-
const cli = buildCliCommand(appName, commandPath, command.options, values as OptionValues<OptionSchema>);
|
|
130
|
-
navigation.openModal<CliModalParams>("cli", { command: cli });
|
|
131
|
-
}, [appName, commandPath, command.options, values, navigation]);
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<Container flexDirection="column" flex={1}>
|
|
135
|
-
<ConfigForm
|
|
136
|
-
title={`Configure: ${command.displayName ?? command.name}`}
|
|
137
|
-
fieldConfigs={derivedFieldConfigs}
|
|
138
|
-
values={values}
|
|
139
|
-
selectedIndex={selectedFieldIndex}
|
|
140
|
-
focused={true}
|
|
141
|
-
onSelectionChange={setSelectedFieldIndex}
|
|
142
|
-
onEditField={handleEditField}
|
|
143
|
-
onAction={handleRun}
|
|
144
|
-
additionalButtons={
|
|
145
|
-
command.supportsCli()
|
|
146
|
-
? [{ label: "CLI Command", onPress: handleShowCliArgs }]
|
|
147
|
-
: []
|
|
148
|
-
}
|
|
149
|
-
actionButton={
|
|
150
|
-
<MenuButton
|
|
151
|
-
label={command.actionLabel ?? "Run"}
|
|
152
|
-
selected={selectedFieldIndex === derivedFieldConfigs.length + 1}
|
|
153
|
-
/>
|
|
154
|
-
}
|
|
155
|
-
/>
|
|
156
|
-
</Container>
|
|
157
|
-
);
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import type { AnyCommand } from "../../core/command.ts";
|
|
3
|
-
import { useNavigation } from "../context/NavigationContext.tsx";
|
|
4
|
-
import { ResultsPanel } from "../components/ResultsPanel.tsx";
|
|
5
|
-
import { useClipboardProvider } from "../hooks/useClipboardProvider.ts";
|
|
6
|
-
import type { ScreenComponent } from "../registry.ts";
|
|
7
|
-
import { ScreenBase } from "./ScreenBase.ts";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Screen state stored in navigation params.
|
|
11
|
-
*/
|
|
12
|
-
export interface ErrorParams {
|
|
13
|
-
command: AnyCommand;
|
|
14
|
-
commandPath: string[];
|
|
15
|
-
values: Record<string, unknown>;
|
|
16
|
-
error: Error;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Error screen - shows command execution errors.
|
|
21
|
-
* Fully self-contained - gets all data from context and handles its own transitions.
|
|
22
|
-
*/
|
|
23
|
-
export class ErrorScreen extends ScreenBase {
|
|
24
|
-
static readonly Id = "error";
|
|
25
|
-
|
|
26
|
-
getRoute(): string {
|
|
27
|
-
return ErrorScreen.Id;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
override component(): ScreenComponent {
|
|
31
|
-
return function ErrorScreenComponent() {
|
|
32
|
-
const navigation = useNavigation();
|
|
33
|
-
|
|
34
|
-
// Get params from navigation
|
|
35
|
-
const params = navigation.current.params as ErrorParams | undefined;
|
|
36
|
-
if (!params) return null;
|
|
37
|
-
|
|
38
|
-
const { error, command } = params;
|
|
39
|
-
|
|
40
|
-
// Register clipboard provider for this screen
|
|
41
|
-
useClipboardProvider(
|
|
42
|
-
useCallback(() => ({
|
|
43
|
-
content: error.message,
|
|
44
|
-
label: "Error",
|
|
45
|
-
}), [error])
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
<ResultsPanel
|
|
50
|
-
result={null}
|
|
51
|
-
error={error}
|
|
52
|
-
focused={true}
|
|
53
|
-
renderResult={command.renderResult}
|
|
54
|
-
/>
|
|
55
|
-
);
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { useCallback } from "react";
|
|
2
|
-
import type { AnyCommand, CommandResult } from "../../core/command.ts";
|
|
3
|
-
import { useNavigation } from "../context/NavigationContext.tsx";
|
|
4
|
-
import { ResultsPanel } from "../components/ResultsPanel.tsx";
|
|
5
|
-
import { useClipboardProvider } from "../hooks/useClipboardProvider.ts";
|
|
6
|
-
import { type ScreenComponent } from "../registry.ts";
|
|
7
|
-
import { ScreenBase } from "./ScreenBase.ts";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Screen state stored in navigation params.
|
|
11
|
-
*/
|
|
12
|
-
export interface ResultsParams {
|
|
13
|
-
command: AnyCommand;
|
|
14
|
-
commandPath: string[];
|
|
15
|
-
values: Record<string, unknown>;
|
|
16
|
-
result: unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class ResultsScreen extends ScreenBase {
|
|
20
|
-
static readonly Id = "results";
|
|
21
|
-
|
|
22
|
-
getRoute(): string {
|
|
23
|
-
return ResultsScreen.Id;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Results screen - shows command execution results.
|
|
27
|
-
* Fully self-contained - gets all data from context and handles its own transitions.
|
|
28
|
-
*/
|
|
29
|
-
override component(): ScreenComponent {
|
|
30
|
-
return function ResultsScreenComponent() {
|
|
31
|
-
const navigation = useNavigation();
|
|
32
|
-
|
|
33
|
-
// Get params from navigation
|
|
34
|
-
const params = navigation.current.params as ResultsParams | undefined;
|
|
35
|
-
if (!params) return null;
|
|
36
|
-
|
|
37
|
-
const { result, command } = params;
|
|
38
|
-
|
|
39
|
-
// Register clipboard provider for this screen
|
|
40
|
-
useClipboardProvider(
|
|
41
|
-
useCallback(() => {
|
|
42
|
-
if (command.getClipboardContent) {
|
|
43
|
-
const custom = command.getClipboardContent(result as CommandResult);
|
|
44
|
-
if (custom) return { content: custom, label: "Results" };
|
|
45
|
-
}
|
|
46
|
-
return { content: JSON.stringify(result, null, 2), label: "Results" };
|
|
47
|
-
}, [result, command])
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<ResultsPanel
|
|
52
|
-
result={result as CommandResult | null}
|
|
53
|
-
error={null}
|
|
54
|
-
focused={true}
|
|
55
|
-
renderResult={command.renderResult}
|
|
56
|
-
/>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|