@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.
Files changed (142) hide show
  1. package/package.json +10 -3
  2. package/src/__tests__/adapterNoSharedUi.test.ts +34 -0
  3. package/src/__tests__/configOnChange.test.ts +63 -0
  4. package/src/__tests__/schemaToFields.test.ts +0 -4
  5. package/src/__tests__/tuiRootNoCoupling.test.ts +25 -0
  6. package/src/builtins/version.ts +1 -1
  7. package/src/index.ts +22 -0
  8. package/src/tui/TuiApplication.tsx +0 -4
  9. package/src/tui/TuiRoot.tsx +58 -102
  10. package/src/tui/actions.ts +4 -0
  11. package/src/tui/adapters/ink/InkRenderer.tsx +191 -41
  12. package/src/tui/adapters/ink/SemanticInkRenderer.tsx +210 -0
  13. package/src/tui/adapters/ink/components/Button.tsx +10 -2
  14. package/src/tui/adapters/ink/components/Overlay.tsx +8 -2
  15. package/src/tui/adapters/ink/components/Panel.tsx +26 -5
  16. package/src/tui/adapters/ink/components/ScrollView.tsx +44 -3
  17. package/src/tui/adapters/ink/components/Spinner.tsx +8 -2
  18. package/src/tui/adapters/ink/keyboard.ts +0 -3
  19. package/src/tui/adapters/ink/ui/CommandSelector.tsx +56 -0
  20. package/src/tui/adapters/ink/ui/ConfigForm.tsx +77 -0
  21. package/src/tui/adapters/ink/ui/Header.tsx +25 -0
  22. package/src/tui/adapters/ink/ui/JsonHighlight.tsx +21 -0
  23. package/src/tui/adapters/ink/ui/ResultsPanel.tsx +57 -0
  24. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +190 -39
  25. package/src/tui/adapters/opentui/SemanticOpenTuiRenderer.tsx +192 -0
  26. package/src/tui/adapters/opentui/components/Label.tsx +2 -2
  27. package/src/tui/adapters/opentui/components/Overlay.tsx +12 -3
  28. package/src/tui/adapters/opentui/components/Panel.tsx +11 -1
  29. package/src/tui/adapters/opentui/components/ScrollView.tsx +1 -8
  30. package/src/tui/adapters/opentui/components/Spinner.tsx +1 -1
  31. package/src/tui/adapters/opentui/keyboard.ts +0 -3
  32. package/src/tui/adapters/opentui/ui/CommandSelector.tsx +55 -0
  33. package/src/tui/adapters/opentui/ui/ConfigForm.tsx +74 -0
  34. package/src/tui/adapters/opentui/ui/Header.tsx +24 -0
  35. package/src/tui/adapters/opentui/ui/JsonHighlight.tsx +20 -0
  36. package/src/tui/adapters/opentui/ui/LogsPanel.tsx +44 -0
  37. package/src/tui/adapters/opentui/ui/ResultsPanel.tsx +62 -0
  38. package/src/tui/adapters/shared/TerminalClipboard.ts +65 -0
  39. package/src/tui/adapters/{opentui/hooks → shared}/useSpinner.ts +5 -1
  40. package/src/tui/adapters/types.ts +25 -45
  41. package/src/tui/components/JsonHighlight.tsx +41 -111
  42. package/src/tui/context/ActionContext.tsx +51 -0
  43. package/src/tui/context/ExecutorContext.tsx +7 -1
  44. package/src/tui/context/NavigationContext.tsx +20 -4
  45. package/src/tui/controllers/CommandBrowserController.tsx +100 -0
  46. package/src/tui/controllers/ConfigController.tsx +183 -0
  47. package/src/tui/controllers/EditorController.tsx +169 -0
  48. package/src/tui/controllers/LogsController.tsx +48 -0
  49. package/src/tui/controllers/OutcomeController.tsx +110 -0
  50. package/src/tui/driver/TuiDriver.tsx +148 -0
  51. package/src/tui/driver/context/TuiDriverContext.tsx +44 -0
  52. package/src/tui/driver/types.ts +72 -0
  53. package/src/tui/semantic/AppShell.tsx +30 -0
  54. package/src/tui/semantic/CommandBrowserScreen.tsx +16 -0
  55. package/src/tui/semantic/ConfigScreen.tsx +23 -0
  56. package/src/tui/semantic/EditorScreen.tsx +20 -0
  57. package/src/tui/semantic/LogsScreen.tsx +9 -0
  58. package/src/tui/semantic/RunningScreen.tsx +17 -0
  59. package/src/tui/semantic/layoutTypes.ts +72 -0
  60. package/src/tui/semantic/render.tsx +44 -0
  61. package/src/tui/semantic/types.ts +31 -98
  62. package/src/tui/utils/jsonTokenizer.ts +98 -0
  63. package/src/tui/utils/schemaToFields.ts +1 -25
  64. package/.devcontainer/devcontainer.json +0 -19
  65. package/.devcontainer/install-prerequisites.sh +0 -49
  66. package/.github/workflows/copilot-setup-steps.yml +0 -32
  67. package/.github/workflows/pull-request.yml +0 -27
  68. package/.github/workflows/release-npm-package.yml +0 -81
  69. package/AGENTS.md +0 -43
  70. package/CLAUDE.md +0 -1
  71. package/bun.lock +0 -321
  72. package/examples/tui-app/commands/config/app/get.ts +0 -62
  73. package/examples/tui-app/commands/config/app/index.ts +0 -23
  74. package/examples/tui-app/commands/config/app/set.ts +0 -96
  75. package/examples/tui-app/commands/config/index.ts +0 -28
  76. package/examples/tui-app/commands/config/user/get.ts +0 -61
  77. package/examples/tui-app/commands/config/user/index.ts +0 -23
  78. package/examples/tui-app/commands/config/user/set.ts +0 -57
  79. package/examples/tui-app/commands/greet.ts +0 -78
  80. package/examples/tui-app/commands/math.ts +0 -111
  81. package/examples/tui-app/commands/status.ts +0 -86
  82. package/examples/tui-app/index.ts +0 -38
  83. package/guides/01-hello-world.md +0 -101
  84. package/guides/02-adding-options.md +0 -103
  85. package/guides/03-multiple-commands.md +0 -161
  86. package/guides/04-subcommands.md +0 -206
  87. package/guides/05-interactive-tui.md +0 -209
  88. package/guides/06-config-validation.md +0 -256
  89. package/guides/07-async-cancellation.md +0 -334
  90. package/guides/08-complete-application.md +0 -507
  91. package/guides/README.md +0 -78
  92. package/src/tui/adapters/ink/components/Code.tsx +0 -6
  93. package/src/tui/adapters/ink/components/Container.tsx +0 -5
  94. package/src/tui/adapters/ink/components/Spacer.tsx +0 -15
  95. package/src/tui/adapters/ink/components/Value.tsx +0 -7
  96. package/src/tui/adapters/opentui/components/Code.tsx +0 -12
  97. package/src/tui/adapters/opentui/components/Container.tsx +0 -56
  98. package/src/tui/adapters/opentui/components/Spacer.tsx +0 -5
  99. package/src/tui/adapters/opentui/components/Value.tsx +0 -13
  100. package/src/tui/components/ActionButton.tsx +0 -0
  101. package/src/tui/components/CommandSelector.tsx +0 -119
  102. package/src/tui/components/ConfigForm.tsx +0 -174
  103. package/src/tui/components/FieldRow.tsx +0 -0
  104. package/src/tui/components/Header.tsx +0 -32
  105. package/src/tui/components/ModalBase.tsx +0 -38
  106. package/src/tui/components/ResultsPanel.tsx +0 -84
  107. package/src/tui/components/StatusBar.tsx +0 -44
  108. package/src/tui/components/logColors.ts +0 -12
  109. package/src/tui/components/types.ts +0 -30
  110. package/src/tui/context/ClipboardContext.tsx +0 -87
  111. package/src/tui/context/KeyboardContext.tsx +0 -132
  112. package/src/tui/hooks/useActiveKeyHandler.ts +0 -75
  113. package/src/tui/hooks/useClipboard.ts +0 -81
  114. package/src/tui/hooks/useClipboardProvider.ts +0 -42
  115. package/src/tui/hooks/useGlobalKeyHandler.ts +0 -54
  116. package/src/tui/modals/CliModal.tsx +0 -82
  117. package/src/tui/modals/EditorModal.tsx +0 -207
  118. package/src/tui/modals/LogsModal.tsx +0 -98
  119. package/src/tui/registry.ts +0 -102
  120. package/src/tui/screens/CommandSelectScreen.tsx +0 -162
  121. package/src/tui/screens/ConfigScreen.tsx +0 -160
  122. package/src/tui/screens/ErrorScreen.tsx +0 -58
  123. package/src/tui/screens/ResultsScreen.tsx +0 -60
  124. package/src/tui/screens/RunningScreen.tsx +0 -72
  125. package/src/tui/screens/ScreenBase.ts +0 -6
  126. package/src/tui/semantic/Button.tsx +0 -7
  127. package/src/tui/semantic/Code.tsx +0 -7
  128. package/src/tui/semantic/CodeHighlight.tsx +0 -7
  129. package/src/tui/semantic/Container.tsx +0 -7
  130. package/src/tui/semantic/Field.tsx +0 -7
  131. package/src/tui/semantic/Label.tsx +0 -7
  132. package/src/tui/semantic/MenuButton.tsx +0 -7
  133. package/src/tui/semantic/MenuItem.tsx +0 -7
  134. package/src/tui/semantic/Overlay.tsx +0 -7
  135. package/src/tui/semantic/Panel.tsx +0 -7
  136. package/src/tui/semantic/ScrollView.tsx +0 -9
  137. package/src/tui/semantic/Select.tsx +0 -7
  138. package/src/tui/semantic/Spacer.tsx +0 -7
  139. package/src/tui/semantic/Spinner.tsx +0 -7
  140. package/src/tui/semantic/TextInput.tsx +0 -7
  141. package/src/tui/semantic/Value.tsx +0 -7
  142. 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
- }
@@ -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
- }