@pablozaiden/terminatui 0.3.0 → 0.4.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/package.json +1 -1
- package/src/__tests__/adapterNoSharedUi.test.ts +34 -0
- package/src/__tests__/schemaToFields.test.ts +0 -4
- package/src/__tests__/tuiRootNoCoupling.test.ts +25 -0
- package/src/index.ts +2 -2
- 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 -45
- 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 -43
- 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 -46
- 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/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 -165
- package/src/tui/screens/ErrorScreen.tsx +0 -58
- package/src/tui/screens/ResultsScreen.tsx +0 -68
- 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
|
@@ -1,36 +1,183 @@
|
|
|
1
1
|
import { createCliRenderer, type CliRenderer } from "@opentui/core";
|
|
2
2
|
import { createRoot, type Root } from "@opentui/react";
|
|
3
|
-
import { useLayoutEffect, type ReactNode } from "react";
|
|
3
|
+
import { useEffect, useLayoutEffect, type ReactNode } from "react";
|
|
4
4
|
import { SemanticColors } from "../../theme.ts";
|
|
5
|
-
import type { Renderer, RendererConfig } from "../types.ts";
|
|
5
|
+
import type { KeyboardEvent, Renderer, RendererConfig } from "../types.ts";
|
|
6
|
+
import { SemanticOpenTuiRenderer } from "./SemanticOpenTuiRenderer.tsx";
|
|
6
7
|
import { useOpenTuiKeyboardAdapter } from "./keyboard.ts";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
|
|
9
|
+
import { copyToTerminalClipboard } from "../shared/TerminalClipboard.ts";
|
|
10
|
+
import { useTuiDriver } from "../../driver/context/TuiDriverContext.tsx";
|
|
11
|
+
import type { TuiAction } from "../../actions.ts";
|
|
12
|
+
|
|
13
|
+
function OpenTuiKeyboardHandler({
|
|
14
|
+
dispatchAction,
|
|
15
|
+
getScreenKeyHandler,
|
|
16
|
+
keyboard,
|
|
17
|
+
onCopyToastChange,
|
|
18
|
+
}: {
|
|
19
|
+
dispatchAction: (action: TuiAction) => void;
|
|
20
|
+
getScreenKeyHandler: () => ((event: KeyboardEvent) => boolean) | null;
|
|
21
|
+
keyboard: Renderer["keyboard"];
|
|
22
|
+
onCopyToastChange?: (toast: string | null) => void;
|
|
23
|
+
}) {
|
|
24
|
+
const driver = useTuiDriver();
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const cleanup = keyboard.setGlobalHandler((event) => {
|
|
28
|
+
if (event.name === "escape") {
|
|
29
|
+
dispatchAction({ type: "nav.back" });
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (event.ctrl && event.name === "y") {
|
|
34
|
+
const payload = driver.getActiveCopyPayload();
|
|
35
|
+
if (payload) {
|
|
36
|
+
// Show toast immediately for instant feedback
|
|
37
|
+
onCopyToastChange?.(`Copied ${payload.label}`);
|
|
38
|
+
void copyToTerminalClipboard(payload.content).then((success) => {
|
|
39
|
+
if (!success) {
|
|
40
|
+
onCopyToastChange?.("Copy failed");
|
|
41
|
+
}
|
|
42
|
+
setTimeout(() => onCopyToastChange?.(null), 1500);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (event.ctrl && event.name === "l") {
|
|
49
|
+
dispatchAction({ type: "logs.open" });
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const screenHandler = getScreenKeyHandler();
|
|
54
|
+
if (screenHandler) {
|
|
55
|
+
return screenHandler(event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return false;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return cleanup;
|
|
62
|
+
}, [dispatchAction, getScreenKeyHandler, keyboard, driver, onCopyToastChange]);
|
|
63
|
+
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
23
66
|
|
|
24
67
|
export class OpenTuiRenderer implements Renderer {
|
|
68
|
+
private readonly semanticRenderer = new SemanticOpenTuiRenderer();
|
|
69
|
+
|
|
70
|
+
private semanticScreenKeyHandler: ((event: KeyboardEvent) => boolean) | null = null;
|
|
71
|
+
|
|
72
|
+
public renderSemanticAppShell: Renderer["renderSemanticAppShell"] = (props) => {
|
|
73
|
+
return this.semanticRenderer.renderAppShell(props);
|
|
74
|
+
};
|
|
75
|
+
public renderSemanticCommandBrowserScreen: Renderer["renderSemanticCommandBrowserScreen"] = (props) => {
|
|
76
|
+
this.semanticScreenKeyHandler = (event) => {
|
|
77
|
+
if (event.ctrl && event.name === "l") {
|
|
78
|
+
// Adapter-owned logs open — let global handler process it.
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (event.name === "up") {
|
|
83
|
+
props.onSelectCommand(props.selectedCommandIndex - 1);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (event.name === "down") {
|
|
88
|
+
props.onSelectCommand(props.selectedCommandIndex + 1);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (event.name === "return") {
|
|
93
|
+
props.onRunSelected();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return this.semanticRenderer.renderCommandBrowserScreen(props);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
public renderSemanticConfigScreen: Renderer["renderSemanticConfigScreen"] = (props) => {
|
|
104
|
+
this.semanticScreenKeyHandler = (event) => {
|
|
105
|
+
if (event.ctrl && event.name === "l") {
|
|
106
|
+
// Adapter-owned logs open — let global handler process it.
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (event.name === "up") {
|
|
111
|
+
props.onSelectionChange(props.selectedFieldIndex - 1);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (event.name === "down") {
|
|
116
|
+
props.onSelectionChange(props.selectedFieldIndex + 1);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (event.name === "return") {
|
|
121
|
+
const fieldConfig = props.fieldConfigs[props.selectedFieldIndex];
|
|
122
|
+
if (fieldConfig) {
|
|
123
|
+
props.onEditField(fieldConfig.key);
|
|
124
|
+
} else {
|
|
125
|
+
props.onRun();
|
|
126
|
+
}
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return this.semanticRenderer.renderConfigScreen(props);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
public renderSemanticRunningScreen: Renderer["renderSemanticRunningScreen"] = (props) => {
|
|
137
|
+
this.semanticScreenKeyHandler = null;
|
|
138
|
+
return this.semanticRenderer.renderRunningScreen(props);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
public renderSemanticLogsScreen: Renderer["renderSemanticLogsScreen"] = (props) => {
|
|
142
|
+
this.semanticScreenKeyHandler = (event) => {
|
|
143
|
+
if (event.name === "return") {
|
|
144
|
+
props.onClose();
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return this.semanticRenderer.renderLogsScreen(props);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
public renderSemanticEditorScreen: Renderer["renderSemanticEditorScreen"] = (props) => {
|
|
154
|
+
this.semanticScreenKeyHandler = (event) => {
|
|
155
|
+
if (event.name === "return") {
|
|
156
|
+
props.onSubmit?.();
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return this.semanticRenderer.renderEditorScreen(props);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
public renderKeyboardHandler: Renderer["renderKeyboardHandler"] = ({ dispatchAction, onCopyToastChange }) => {
|
|
166
|
+
return (
|
|
167
|
+
<OpenTuiKeyboardHandlerWrapper
|
|
168
|
+
dispatchAction={dispatchAction}
|
|
169
|
+
getScreenKeyHandler={() => this.semanticScreenKeyHandler}
|
|
170
|
+
keyboard={this.keyboard}
|
|
171
|
+
onCopyToastChange={onCopyToastChange}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
};
|
|
175
|
+
|
|
25
176
|
private renderer: CliRenderer | null = null;
|
|
26
177
|
private root: Root | null = null;
|
|
27
178
|
|
|
28
179
|
private activeKeyboardAdapter: Renderer["keyboard"] | null = null;
|
|
29
180
|
|
|
30
|
-
public supportCustomRendering(): boolean {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
181
|
public keyboard: Renderer["keyboard"] = {
|
|
35
182
|
setActiveHandler: (id, handler) => {
|
|
36
183
|
return this.activeKeyboardAdapter?.setActiveHandler(id, handler) ?? (() => {});
|
|
@@ -40,27 +187,6 @@ export class OpenTuiRenderer implements Renderer {
|
|
|
40
187
|
},
|
|
41
188
|
};
|
|
42
189
|
|
|
43
|
-
public components: Renderer["components"] = {
|
|
44
|
-
Field,
|
|
45
|
-
Button,
|
|
46
|
-
MenuButton,
|
|
47
|
-
MenuItem,
|
|
48
|
-
Container,
|
|
49
|
-
Panel,
|
|
50
|
-
ScrollView: OpenTuiScrollView,
|
|
51
|
-
|
|
52
|
-
Overlay,
|
|
53
|
-
Spacer,
|
|
54
|
-
Spinner,
|
|
55
|
-
Label,
|
|
56
|
-
Value,
|
|
57
|
-
Code,
|
|
58
|
-
CodeHighlight,
|
|
59
|
-
|
|
60
|
-
Select,
|
|
61
|
-
TextInput,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
190
|
constructor(private readonly config: RendererConfig) {}
|
|
65
191
|
|
|
66
192
|
async initialize(): Promise<void> {
|
|
@@ -117,3 +243,24 @@ function KeyboardBridge({
|
|
|
117
243
|
|
|
118
244
|
return <>{children}</>;
|
|
119
245
|
}
|
|
246
|
+
|
|
247
|
+
function OpenTuiKeyboardHandlerWrapper({
|
|
248
|
+
dispatchAction,
|
|
249
|
+
getScreenKeyHandler,
|
|
250
|
+
keyboard,
|
|
251
|
+
onCopyToastChange,
|
|
252
|
+
}: {
|
|
253
|
+
dispatchAction: (action: TuiAction) => void;
|
|
254
|
+
getScreenKeyHandler: () => ((event: KeyboardEvent) => boolean) | null;
|
|
255
|
+
keyboard: Renderer["keyboard"];
|
|
256
|
+
onCopyToastChange?: (toast: string | null) => void;
|
|
257
|
+
}) {
|
|
258
|
+
return (
|
|
259
|
+
<OpenTuiKeyboardHandler
|
|
260
|
+
dispatchAction={dispatchAction}
|
|
261
|
+
getScreenKeyHandler={getScreenKeyHandler}
|
|
262
|
+
keyboard={keyboard}
|
|
263
|
+
onCopyToastChange={onCopyToastChange}
|
|
264
|
+
/>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { AppShellProps } from "../../semantic/AppShell.tsx";
|
|
3
|
+
import type { CommandBrowserScreenProps } from "../../semantic/CommandBrowserScreen.tsx";
|
|
4
|
+
import type { ConfigScreenProps } from "../../semantic/ConfigScreen.tsx";
|
|
5
|
+
import type { RunningScreenProps } from "../../semantic/RunningScreen.tsx";
|
|
6
|
+
import type { LogsScreenProps } from "../../semantic/LogsScreen.tsx";
|
|
7
|
+
import type { EditorScreenProps } from "../../semantic/EditorScreen.tsx";
|
|
8
|
+
|
|
9
|
+
// Platform-native components (OpenTUI)
|
|
10
|
+
import { Panel } from "./components/Panel.tsx";
|
|
11
|
+
import { Label } from "./components/Label.tsx";
|
|
12
|
+
import { Overlay } from "./components/Overlay.tsx";
|
|
13
|
+
import { TextInput } from "./components/TextInput.tsx";
|
|
14
|
+
import { Select } from "./components/Select.tsx";
|
|
15
|
+
import { MenuButton } from "./components/MenuButton.tsx";
|
|
16
|
+
import { Spinner } from "./components/Spinner.tsx";
|
|
17
|
+
|
|
18
|
+
// Adapter-local UI components
|
|
19
|
+
import { Header } from "./ui/Header.tsx";
|
|
20
|
+
import { CommandSelector } from "./ui/CommandSelector.tsx";
|
|
21
|
+
import { ConfigForm } from "./ui/ConfigForm.tsx";
|
|
22
|
+
import { ResultsPanel } from "./ui/ResultsPanel.tsx";
|
|
23
|
+
import { LogsPanel } from "./ui/LogsPanel.tsx";
|
|
24
|
+
|
|
25
|
+
export class SemanticOpenTuiRenderer {
|
|
26
|
+
renderAppShell(props: AppShellProps): ReactNode {
|
|
27
|
+
return (
|
|
28
|
+
<Panel flexDirection="column" flex={1} padding={1} border={false}>
|
|
29
|
+
<box flexDirection="column" flexGrow={1}>
|
|
30
|
+
<Header
|
|
31
|
+
name={props.app.displayName ?? props.app.name}
|
|
32
|
+
version={props.app.version}
|
|
33
|
+
breadcrumb={props.app.breadcrumb}
|
|
34
|
+
/>
|
|
35
|
+
|
|
36
|
+
<box flexDirection="column" flexGrow={1}>
|
|
37
|
+
{props.screen}
|
|
38
|
+
</box>
|
|
39
|
+
|
|
40
|
+
<Panel dense border={true} flexDirection="column" gap={0} height={4}>
|
|
41
|
+
<box flexDirection="row" justifyContent="space-between" paddingLeft={1} paddingRight={1}>
|
|
42
|
+
<box flexDirection="row" gap={1}>
|
|
43
|
+
<Spinner active={props.status.isExecuting} />
|
|
44
|
+
<Label color="mutedText">
|
|
45
|
+
{props.status.isCancelling
|
|
46
|
+
? "Cancelling..."
|
|
47
|
+
: props.status.isExecuting
|
|
48
|
+
? "Executing..."
|
|
49
|
+
: "Ready"}
|
|
50
|
+
</Label>
|
|
51
|
+
{props.copyToast ? (
|
|
52
|
+
<Label color="success" bold>{props.copyToast}</Label>
|
|
53
|
+
) : null}
|
|
54
|
+
</box>
|
|
55
|
+
<Label color="mutedText">Esc Back Ctrl+L Logs Ctrl+Y Copy</Label>
|
|
56
|
+
</box>
|
|
57
|
+
</Panel>
|
|
58
|
+
|
|
59
|
+
{props.modals}
|
|
60
|
+
</box>
|
|
61
|
+
</Panel>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
renderCommandBrowserScreen(props: CommandBrowserScreenProps): ReactNode {
|
|
66
|
+
const commandItems = props.commands.map((command) => ({ command }));
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<CommandSelector
|
|
70
|
+
commands={commandItems}
|
|
71
|
+
selectedIndex={props.selectedCommandIndex}
|
|
72
|
+
onSelect={() => {
|
|
73
|
+
// Controller handles subcommand navigation in onRunSelected
|
|
74
|
+
props.onRunSelected();
|
|
75
|
+
}}
|
|
76
|
+
breadcrumb={props.commandId}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
renderConfigScreen(props: ConfigScreenProps): ReactNode {
|
|
82
|
+
const additionalButtons: { label: string; onPress: () => void }[] = [];
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<box flexDirection="column" flexGrow={1}>
|
|
86
|
+
<ConfigForm
|
|
87
|
+
title={props.title}
|
|
88
|
+
fieldConfigs={props.fieldConfigs}
|
|
89
|
+
values={props.values}
|
|
90
|
+
selectedIndex={props.selectedFieldIndex}
|
|
91
|
+
focused={true}
|
|
92
|
+
additionalButtons={additionalButtons}
|
|
93
|
+
actionButton={
|
|
94
|
+
<MenuButton
|
|
95
|
+
label={"Done"}
|
|
96
|
+
selected={props.selectedFieldIndex === props.fieldConfigs.length + additionalButtons.length}
|
|
97
|
+
/>
|
|
98
|
+
}
|
|
99
|
+
/>
|
|
100
|
+
<box flexDirection="column" paddingTop={1}>
|
|
101
|
+
<Label color="mutedText">CLI: {props.cliCommand}</Label>
|
|
102
|
+
</box>
|
|
103
|
+
</box>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
renderRunningScreen(props: RunningScreenProps): ReactNode {
|
|
108
|
+
if (props.kind === "running") {
|
|
109
|
+
return (
|
|
110
|
+
<box flexDirection="column" flexGrow={1}>
|
|
111
|
+
<ResultsPanel result={{ success: true, message: props.title }} error={null} focused={true} />
|
|
112
|
+
</box>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (props.kind === "error") {
|
|
117
|
+
return (
|
|
118
|
+
<box flexDirection="column" flexGrow={1}>
|
|
119
|
+
<ResultsPanel result={null} error={new Error(props.message ?? "Unknown error")} focused={true} />
|
|
120
|
+
</box>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// kind === "results"
|
|
125
|
+
// If customContent is provided, render it directly instead of using ResultsPanel's default rendering
|
|
126
|
+
if (props.customContent !== undefined) {
|
|
127
|
+
return (
|
|
128
|
+
<box flexDirection="column" flexGrow={1}>
|
|
129
|
+
<ResultsPanel
|
|
130
|
+
result={props.result ?? { success: true, message: props.message }}
|
|
131
|
+
error={null}
|
|
132
|
+
focused={true}
|
|
133
|
+
renderResult={() => props.customContent}
|
|
134
|
+
/>
|
|
135
|
+
</box>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<box flexDirection="column" flexGrow={1}>
|
|
141
|
+
<ResultsPanel
|
|
142
|
+
result={props.result ?? { success: true, message: props.message }}
|
|
143
|
+
error={null}
|
|
144
|
+
focused={true}
|
|
145
|
+
/>
|
|
146
|
+
</box>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
renderLogsScreen(props: LogsScreenProps): ReactNode {
|
|
151
|
+
return <LogsPanel {...props} />;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
renderEditorScreen(props: EditorScreenProps): ReactNode {
|
|
155
|
+
// For text input, use more height to give a proper editing area
|
|
156
|
+
const isTextEditor = props.editorType !== "select";
|
|
157
|
+
const panelHeight = isTextEditor ? 8 : undefined;
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<Overlay>
|
|
161
|
+
<Panel flexDirection="column" padding={1} border={true} width={80} height={panelHeight} surface="overlay">
|
|
162
|
+
<Label bold>{props.label ?? props.fieldId}</Label>
|
|
163
|
+
|
|
164
|
+
<box flexDirection="column" gap={1} flexGrow={1}>
|
|
165
|
+
{props.editorType === "select" ? (
|
|
166
|
+
<Select
|
|
167
|
+
options={props.selectOptions ?? []}
|
|
168
|
+
value={props.valueString}
|
|
169
|
+
focused={true}
|
|
170
|
+
onChange={(value: string) => {
|
|
171
|
+
const index = (props.selectOptions ?? []).findIndex((o) => o.value === value);
|
|
172
|
+
props.onChangeSelectIndex?.(Math.max(0, index));
|
|
173
|
+
}}
|
|
174
|
+
onSubmit={() => props.onSubmit?.()}
|
|
175
|
+
/>
|
|
176
|
+
) : (
|
|
177
|
+
<TextInput
|
|
178
|
+
value={props.valueString}
|
|
179
|
+
placeholder=""
|
|
180
|
+
focused={true}
|
|
181
|
+
onChange={(next: string) => props.onChangeText?.(next)}
|
|
182
|
+
onSubmit={() => props.onSubmit?.()}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</box>
|
|
186
|
+
|
|
187
|
+
<Label color="mutedText">Enter to submit Esc to cancel</Label>
|
|
188
|
+
</Panel>
|
|
189
|
+
</Overlay>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -2,13 +2,13 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import type { LabelProps } from "../../../semantic/types.ts";
|
|
3
3
|
import { SemanticColors } from "../../../theme.ts";
|
|
4
4
|
|
|
5
|
-
export function Label({ color = "text", bold, italic,
|
|
5
|
+
export function Label({ color = "text", bold, italic, children }: LabelProps & { children: ReactNode }) {
|
|
6
6
|
const fg = SemanticColors[color] ?? SemanticColors.text;
|
|
7
7
|
|
|
8
8
|
const content = bold ? <strong>{children}</strong> : children;
|
|
9
9
|
|
|
10
10
|
return (
|
|
11
|
-
<text fg={fg}
|
|
11
|
+
<text fg={fg}>
|
|
12
12
|
{italic ? <em>{content}</em> : content}
|
|
13
13
|
</text>
|
|
14
14
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { OverlayProps } from "../../../semantic/
|
|
2
|
+
import type { OverlayProps } from "../../../semantic/layoutTypes.ts";
|
|
3
3
|
|
|
4
4
|
export function Overlay({
|
|
5
5
|
zIndex = 10,
|
|
@@ -12,8 +12,17 @@ export function Overlay({
|
|
|
12
12
|
children,
|
|
13
13
|
}: OverlayProps & { children?: ReactNode }) {
|
|
14
14
|
return (
|
|
15
|
-
<box
|
|
16
|
-
|
|
15
|
+
<box
|
|
16
|
+
position="absolute"
|
|
17
|
+
top={0}
|
|
18
|
+
left={0}
|
|
19
|
+
right={0}
|
|
20
|
+
bottom={0}
|
|
21
|
+
zIndex={zIndex}
|
|
22
|
+
alignItems="center"
|
|
23
|
+
justifyContent="center"
|
|
24
|
+
>
|
|
25
|
+
<box top={top as any} left={left as any} right={right as any} bottom={bottom as any} width={width as any} height={height as any}>
|
|
17
26
|
{children}
|
|
18
27
|
</box>
|
|
19
28
|
</box>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { PanelProps, Spacing } from "../../../semantic/
|
|
2
|
+
import type { PanelProps, Spacing } from "../../../semantic/layoutTypes.ts";
|
|
3
3
|
import { SemanticColors } from "../../../theme.ts";
|
|
4
4
|
|
|
5
5
|
function normalizePadding(
|
|
@@ -38,6 +38,11 @@ export function Panel({
|
|
|
38
38
|
flex,
|
|
39
39
|
width,
|
|
40
40
|
height,
|
|
41
|
+
maxHeight,
|
|
42
|
+
top,
|
|
43
|
+
left,
|
|
44
|
+
right,
|
|
45
|
+
bottom,
|
|
41
46
|
flexDirection,
|
|
42
47
|
alignItems,
|
|
43
48
|
justifyContent,
|
|
@@ -66,6 +71,11 @@ export function Panel({
|
|
|
66
71
|
flexShrink={noShrink ? 0 : flex === undefined ? undefined : 1}
|
|
67
72
|
width={width as any}
|
|
68
73
|
height={height as any}
|
|
74
|
+
maxHeight={maxHeight as any}
|
|
75
|
+
top={top as any}
|
|
76
|
+
left={left as any}
|
|
77
|
+
right={right as any}
|
|
78
|
+
bottom={bottom as any}
|
|
69
79
|
flexDirection={flexDirection as any}
|
|
70
80
|
alignItems={alignItems as any}
|
|
71
81
|
justifyContent={justifyContent as any}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useRef, type ReactNode } from "react";
|
|
2
2
|
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
-
import type { ScrollViewProps, ScrollViewRef, Spacing } from "../../../semantic/
|
|
3
|
+
import type { ScrollViewProps, ScrollViewRef, Spacing } from "../../../semantic/layoutTypes.ts";
|
|
4
4
|
|
|
5
5
|
function normalizePadding(padding: number | Spacing | undefined): any {
|
|
6
6
|
if (padding === undefined) {
|
|
@@ -37,13 +37,6 @@ export function ScrollView({
|
|
|
37
37
|
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
38
38
|
|
|
39
39
|
const imperativeApi: ScrollViewRef = {
|
|
40
|
-
scrollToTop: () => {
|
|
41
|
-
scrollRef.current?.scrollTo(0);
|
|
42
|
-
},
|
|
43
|
-
scrollToBottom: () => {
|
|
44
|
-
// No public "bottom" API in ScrollBoxRenderable; use large index.
|
|
45
|
-
scrollRef.current?.scrollTo(Number.MAX_SAFE_INTEGER);
|
|
46
|
-
},
|
|
47
40
|
scrollToIndex: (index: number) => {
|
|
48
41
|
scrollRef.current?.scrollTo(index);
|
|
49
42
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SpinnerProps } from "../../../semantic/types.ts";
|
|
2
|
-
import { useSpinner } from "
|
|
2
|
+
import { useSpinner } from "../../shared/useSpinner.ts";
|
|
3
3
|
|
|
4
4
|
export function Spinner({ active }: SpinnerProps) {
|
|
5
5
|
const { frame } = useSpinner(active);
|
|
@@ -6,10 +6,7 @@ import type { KeyboardAdapter, KeyboardEvent, KeyHandler } from "../types.ts";
|
|
|
6
6
|
function normalizeKeyEvent(key: KeyEvent): KeyboardEvent {
|
|
7
7
|
return {
|
|
8
8
|
name: key.name,
|
|
9
|
-
sequence: key.sequence,
|
|
10
9
|
ctrl: key.ctrl,
|
|
11
|
-
shift: key.shift,
|
|
12
|
-
meta: key.meta,
|
|
13
10
|
};
|
|
14
11
|
}
|
|
15
12
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Command } from "../../../../core/command.ts";
|
|
2
|
+
import { MenuItem } from "../components/MenuItem.tsx";
|
|
3
|
+
import { Panel } from "../components/Panel.tsx";
|
|
4
|
+
|
|
5
|
+
interface CommandItem {
|
|
6
|
+
command: Command;
|
|
7
|
+
label?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface CommandSelectorProps {
|
|
12
|
+
commands: CommandItem[];
|
|
13
|
+
selectedIndex: number;
|
|
14
|
+
onSelect: (command: Command) => void;
|
|
15
|
+
breadcrumb?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function CommandSelector({ commands, selectedIndex, onSelect, breadcrumb }: CommandSelectorProps) {
|
|
19
|
+
const title = breadcrumb?.length ? `Select Command (${breadcrumb.join(" > ")})` : "Select Command";
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<box flexDirection="column" flexGrow={1} justifyContent="center" alignItems="center" gap={1}>
|
|
23
|
+
<Panel flexDirection="column" title={title} padding={undefined} width={"80%"} focused>
|
|
24
|
+
<box flexDirection="column" gap={1}>
|
|
25
|
+
{commands.map((item, idx) => {
|
|
26
|
+
const isSelected = idx === selectedIndex;
|
|
27
|
+
const label = item.label ?? item.command.displayName ?? item.command.name;
|
|
28
|
+
const description = item.description ?? item.command.description;
|
|
29
|
+
const modeIndicator = getModeIndicator(item.command);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<MenuItem
|
|
33
|
+
key={item.command.name}
|
|
34
|
+
label={label}
|
|
35
|
+
description={description}
|
|
36
|
+
suffix={modeIndicator}
|
|
37
|
+
selected={isSelected}
|
|
38
|
+
onActivate={() => onSelect(item.command)}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</box>
|
|
43
|
+
</Panel>
|
|
44
|
+
</box>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getModeIndicator(command: Command): string {
|
|
49
|
+
const navigableSubCommands = command.subCommands?.filter((sub) => sub.supportsTui()) ?? [];
|
|
50
|
+
if (navigableSubCommands.length > 0) {
|
|
51
|
+
return ">";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return "";
|
|
55
|
+
}
|