@pablozaiden/terminatui 0.3.0 → 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 +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,82 +0,0 @@
|
|
|
1
|
-
import { useActiveKeyHandler } from "../hooks/useActiveKeyHandler.ts";
|
|
2
|
-
import { Container } from "../semantic/Container.tsx";
|
|
3
|
-
import { ScrollView } from "../semantic/ScrollView.tsx";
|
|
4
|
-
import { ModalBase } from "../components/ModalBase.tsx";
|
|
5
|
-
import { useClipboardProvider } from "../hooks/useClipboardProvider.ts";
|
|
6
|
-
import { Label } from "../semantic/Label.tsx";
|
|
7
|
-
import { Value } from "../semantic/Value.tsx";
|
|
8
|
-
import type { ModalComponent, ModalDefinition } from "../registry.ts";
|
|
9
|
-
|
|
10
|
-
export interface CliModalParams {
|
|
11
|
-
command: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class CliModal implements ModalDefinition<CliModalParams> {
|
|
15
|
-
static readonly Id = "cli";
|
|
16
|
-
|
|
17
|
-
getId(): string {
|
|
18
|
-
return CliModal.Id;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
component(): ModalComponent<CliModalParams> {
|
|
22
|
-
return function CliModalComponentWrapper({ params, onClose }: { params: CliModalParams; onClose: () => void; }) {
|
|
23
|
-
return (
|
|
24
|
-
<CliModalView
|
|
25
|
-
command={params.command}
|
|
26
|
-
visible={true}
|
|
27
|
-
onClose={onClose}
|
|
28
|
-
/>
|
|
29
|
-
);
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
interface CliModalViewProps extends CliModalParams {
|
|
35
|
-
/** Whether the modal is visible */
|
|
36
|
-
visible: boolean;
|
|
37
|
-
/** Called when the modal should close */
|
|
38
|
-
onClose: () => void;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Modal displaying the CLI command equivalent of the current config.
|
|
43
|
-
*/
|
|
44
|
-
function CliModalView({
|
|
45
|
-
command,
|
|
46
|
-
visible,
|
|
47
|
-
onClose,
|
|
48
|
-
}: CliModalViewProps) {
|
|
49
|
-
// Register clipboard provider for CLI command
|
|
50
|
-
useClipboardProvider(
|
|
51
|
-
() => ({ content: command, label: "CLI" }),
|
|
52
|
-
visible
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
// Handle Enter to close (Esc is handled globally)
|
|
56
|
-
useActiveKeyHandler(
|
|
57
|
-
(event) => {
|
|
58
|
-
if (event.name === "return" || event.name === "enter") {
|
|
59
|
-
onClose();
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
63
|
-
},
|
|
64
|
-
{ enabled: visible }
|
|
65
|
-
);
|
|
66
|
-
|
|
67
|
-
if (!visible) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<ModalBase title="CLI Command" width="80%" height={10} top={4} left={4}>
|
|
73
|
-
<ScrollView axis="horizontal" height={3}>
|
|
74
|
-
<Container>
|
|
75
|
-
<Value>{command}</Value>
|
|
76
|
-
</Container>
|
|
77
|
-
</ScrollView>
|
|
78
|
-
|
|
79
|
-
<Label color="mutedText">Enter or Esc to close</Label>
|
|
80
|
-
</ModalBase>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect } from "react";
|
|
2
|
-
import type { FieldConfig } from "../components/types.ts";
|
|
3
|
-
import { ModalBase } from "../components/ModalBase.tsx";
|
|
4
|
-
import { useActiveKeyHandler } from "../hooks/useActiveKeyHandler.ts";
|
|
5
|
-
import { Select } from "../semantic/Select.tsx";
|
|
6
|
-
import { TextInput } from "../semantic/TextInput.tsx";
|
|
7
|
-
import { Label } from "../semantic/Label.tsx";
|
|
8
|
-
import type { ModalComponent, ModalDefinition } from "../registry.ts";
|
|
9
|
-
import { useKeyboardContext } from "../context/KeyboardContext.tsx";
|
|
10
|
-
|
|
11
|
-
export interface EditorModalParams {
|
|
12
|
-
fieldKey: string;
|
|
13
|
-
currentValue: unknown;
|
|
14
|
-
fieldConfigs: FieldConfig[];
|
|
15
|
-
onSubmit: (value: unknown) => void;
|
|
16
|
-
onCancel: () => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export class EditorModal implements ModalDefinition<EditorModalParams> {
|
|
20
|
-
static readonly Id = "property-editor";
|
|
21
|
-
|
|
22
|
-
getId(): string {
|
|
23
|
-
return EditorModal.Id;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
component(): ModalComponent<EditorModalParams> {
|
|
27
|
-
return function EditorModalComponentWrapper({ params, onClose }: { params: EditorModalParams; onClose: () => void; }) {
|
|
28
|
-
return (
|
|
29
|
-
<EditorModalView
|
|
30
|
-
fieldKey={params.fieldKey}
|
|
31
|
-
currentValue={params.currentValue}
|
|
32
|
-
visible={true}
|
|
33
|
-
onSubmit={(value) => {
|
|
34
|
-
params.onSubmit?.(value);
|
|
35
|
-
}}
|
|
36
|
-
onCancel={() => {
|
|
37
|
-
params.onCancel?.();
|
|
38
|
-
onClose();
|
|
39
|
-
}}
|
|
40
|
-
fieldConfigs={params.fieldConfigs}
|
|
41
|
-
/>
|
|
42
|
-
);
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface EditorModalViewProps {
|
|
48
|
-
/** Whether the modal is visible */
|
|
49
|
-
visible: boolean;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Modal for editing field values.
|
|
54
|
-
* Supports text, number, enum, and boolean types.
|
|
55
|
-
*
|
|
56
|
-
* Note: This modal uses native OpenTUI input/select components that handle
|
|
57
|
-
* keyboard events internally. The modal registers as the active handler to
|
|
58
|
-
* block the underlying screen from receiving events, even though most key
|
|
59
|
-
* handling is done by the native components.
|
|
60
|
-
*/
|
|
61
|
-
interface EditorModalViewProps extends EditorModalParams {
|
|
62
|
-
/** Whether the modal is visible */
|
|
63
|
-
visible: boolean;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function EditorModalView({
|
|
67
|
-
fieldKey,
|
|
68
|
-
currentValue,
|
|
69
|
-
visible,
|
|
70
|
-
onSubmit,
|
|
71
|
-
onCancel,
|
|
72
|
-
fieldConfigs,
|
|
73
|
-
}: EditorModalViewProps) {
|
|
74
|
-
const { setInputCaptured } = useKeyboardContext();
|
|
75
|
-
const [inputValue, setInputValue] = useState("");
|
|
76
|
-
const [selectIndex, setSelectIndex] = useState(0);
|
|
77
|
-
|
|
78
|
-
// Reset state when field changes
|
|
79
|
-
useEffect(() => {
|
|
80
|
-
if (fieldKey && visible) {
|
|
81
|
-
setInputValue(String(currentValue ?? ""));
|
|
82
|
-
|
|
83
|
-
// For enums/booleans, find current index
|
|
84
|
-
const fieldConfig = fieldConfigs.find((f) => f.key === fieldKey);
|
|
85
|
-
if (fieldConfig?.type === "boolean") {
|
|
86
|
-
setSelectIndex(currentValue ? 1 : 0);
|
|
87
|
-
} else if (fieldConfig?.options) {
|
|
88
|
-
const idx = fieldConfig.options.findIndex((o) => o.value === currentValue);
|
|
89
|
-
setSelectIndex(idx >= 0 ? idx : 0);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}, [fieldKey, currentValue, visible, fieldConfigs]);
|
|
93
|
-
|
|
94
|
-
// While the editor is open, avoid extra per-keystroke work by preventing the
|
|
95
|
-
// underlying screen from receiving key events.
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (!visible || !fieldKey) {
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
setInputCaptured(true);
|
|
102
|
-
return () => {
|
|
103
|
-
setInputCaptured(false);
|
|
104
|
-
};
|
|
105
|
-
}, [visible, fieldKey, setInputCaptured]);
|
|
106
|
-
|
|
107
|
-
// Register as active handler to block underlying screen from receiving events.
|
|
108
|
-
// The native input/select components handle Enter internally via onSubmit/onSelect,
|
|
109
|
-
// so we don't need to handle it here.
|
|
110
|
-
useActiveKeyHandler(
|
|
111
|
-
(event) => {
|
|
112
|
-
if (event.name === "return" || event.name === "enter") {
|
|
113
|
-
return true;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (event.name === "escape") {
|
|
117
|
-
onCancel();
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return false;
|
|
122
|
-
},
|
|
123
|
-
{ enabled: visible && Boolean(fieldKey) }
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
if (!visible || !fieldKey) {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const fieldConfig = fieldConfigs.find((f) => f.key === fieldKey);
|
|
131
|
-
if (!fieldConfig) {
|
|
132
|
-
return null;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const isNumber = fieldConfig.type === "number";
|
|
136
|
-
|
|
137
|
-
const handleInputSubmit = (value: string) => {
|
|
138
|
-
if (isNumber) {
|
|
139
|
-
onSubmit(parseInt(value.replace(/[^0-9-]/g, ""), 10) || 0);
|
|
140
|
-
onCancel();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
onSubmit(value);
|
|
145
|
-
onCancel();
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const selectOptions =
|
|
149
|
-
fieldConfig.type === "boolean"
|
|
150
|
-
? [
|
|
151
|
-
{ label: "False", value: "false" },
|
|
152
|
-
{ label: "True", value: "true" },
|
|
153
|
-
]
|
|
154
|
-
: fieldConfig.type === "enum"
|
|
155
|
-
? (fieldConfig.options ?? []).map((o) => ({
|
|
156
|
-
label: o.name,
|
|
157
|
-
value: String(o.value),
|
|
158
|
-
}))
|
|
159
|
-
: null;
|
|
160
|
-
|
|
161
|
-
const usesSelect = selectOptions !== null;
|
|
162
|
-
|
|
163
|
-
const handleSelectSubmit = () => {
|
|
164
|
-
const selected = selectOptions?.[selectIndex];
|
|
165
|
-
if (!selected) {
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (fieldConfig.type === "boolean") {
|
|
170
|
-
onSubmit(selected.value === "true");
|
|
171
|
-
onCancel();
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
onSubmit(selected.value);
|
|
176
|
-
onCancel();
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
<ModalBase title={`Edit: ${fieldConfig.label}`} width="60%" height={12} top={4} left={6}>
|
|
181
|
-
{usesSelect && selectOptions && (
|
|
182
|
-
<Select
|
|
183
|
-
options={selectOptions}
|
|
184
|
-
value={selectOptions[selectIndex]?.value ?? selectOptions[0]?.value ?? ""}
|
|
185
|
-
focused={true}
|
|
186
|
-
onChange={(next) => {
|
|
187
|
-
const idx = selectOptions.findIndex((o) => o.value === next);
|
|
188
|
-
setSelectIndex(idx >= 0 ? idx : 0);
|
|
189
|
-
}}
|
|
190
|
-
onSubmit={handleSelectSubmit}
|
|
191
|
-
/>
|
|
192
|
-
)}
|
|
193
|
-
|
|
194
|
-
{!usesSelect && (
|
|
195
|
-
<TextInput
|
|
196
|
-
value={inputValue}
|
|
197
|
-
placeholder={fieldConfig.placeholder ?? `Enter ${fieldConfig.label.toLowerCase()}...`}
|
|
198
|
-
focused={true}
|
|
199
|
-
onChange={(value) => setInputValue(value)}
|
|
200
|
-
onSubmit={() => handleInputSubmit(inputValue)}
|
|
201
|
-
/>
|
|
202
|
-
)}
|
|
203
|
-
|
|
204
|
-
<Label color="mutedText">Enter to save, Esc to cancel</Label>
|
|
205
|
-
</ModalBase>
|
|
206
|
-
);
|
|
207
|
-
}
|
|
@@ -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
|
-
}
|