@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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { CodeTokenType } from "../semantic/types.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A single token in a JSON line.
|
|
5
|
+
*/
|
|
6
|
+
export type JsonToken = { type: Exclude<CodeTokenType, "unknown">; value: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A line of JSON tokens.
|
|
10
|
+
*/
|
|
11
|
+
export type JsonLineTokens = JsonToken[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tokenize a JavaScript value into lines of syntax-highlighted JSON tokens.
|
|
15
|
+
*
|
|
16
|
+
* This is a pure behavioral helper with no React dependencies.
|
|
17
|
+
* Adapters can use this to build their own rendering.
|
|
18
|
+
*/
|
|
19
|
+
export function tokenizeJsonValue(value: unknown, indent = 0): JsonLineTokens[] {
|
|
20
|
+
const pad = " ".repeat(indent);
|
|
21
|
+
const padToken = (): JsonToken => ({ type: "punctuation", value: pad });
|
|
22
|
+
|
|
23
|
+
if (value === null) {
|
|
24
|
+
return [[padToken(), { type: "null", value: "null" }]];
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "boolean") {
|
|
27
|
+
return [[padToken(), { type: "boolean", value: String(value) }]];
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === "number") {
|
|
30
|
+
return [[padToken(), { type: "number", value: String(value) }]];
|
|
31
|
+
}
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
return [[padToken(), { type: "string", value: JSON.stringify(value) }]];
|
|
34
|
+
}
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
if (value.length === 0) {
|
|
37
|
+
return [[padToken(), { type: "punctuation", value: "[]" }]];
|
|
38
|
+
}
|
|
39
|
+
const lines: JsonLineTokens[] = [[padToken(), { type: "punctuation", value: "[" }]];
|
|
40
|
+
value.forEach((item, idx) => {
|
|
41
|
+
const itemLines = tokenizeJsonValue(item, indent + 1);
|
|
42
|
+
const isLast = idx === value.length - 1;
|
|
43
|
+
itemLines.forEach((line, lineIdx) => {
|
|
44
|
+
if (lineIdx === itemLines.length - 1 && !isLast) {
|
|
45
|
+
lines.push([...line, { type: "punctuation", value: "," }]);
|
|
46
|
+
} else {
|
|
47
|
+
lines.push(line);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
lines.push([padToken(), { type: "punctuation", value: "]" }]);
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
if (typeof value === "object") {
|
|
55
|
+
const entries = Object.entries(value);
|
|
56
|
+
if (entries.length === 0) {
|
|
57
|
+
return [[padToken(), { type: "punctuation", value: "{}" }]];
|
|
58
|
+
}
|
|
59
|
+
const lines: JsonLineTokens[] = [[padToken(), { type: "punctuation", value: "{" }]];
|
|
60
|
+
const innerPad = " ".repeat(indent + 1);
|
|
61
|
+
|
|
62
|
+
entries.forEach(([key, val], idx) => {
|
|
63
|
+
const valLines = tokenizeJsonValue(val, indent + 1);
|
|
64
|
+
const isLast = idx === entries.length - 1;
|
|
65
|
+
|
|
66
|
+
// First value line - prepend key
|
|
67
|
+
const firstValLine = valLines[0] ?? [];
|
|
68
|
+
// Remove the padding from value's first line (we'll add our own with the key)
|
|
69
|
+
const valTokens = firstValLine.filter((t) => t.value !== " ".repeat(indent + 1));
|
|
70
|
+
|
|
71
|
+
const keyLine: JsonLineTokens = [
|
|
72
|
+
{ type: "punctuation", value: innerPad },
|
|
73
|
+
{ type: "key", value: `"${key}"` },
|
|
74
|
+
{ type: "punctuation", value: ": " },
|
|
75
|
+
...valTokens,
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
if (valLines.length === 1) {
|
|
79
|
+
// Single line value
|
|
80
|
+
if (!isLast) keyLine.push({ type: "punctuation", value: "," });
|
|
81
|
+
lines.push(keyLine);
|
|
82
|
+
} else {
|
|
83
|
+
// Multi-line value
|
|
84
|
+
lines.push(keyLine);
|
|
85
|
+
valLines.slice(1, -1).forEach((line) => lines.push(line));
|
|
86
|
+
const lastLine = valLines[valLines.length - 1] ?? [];
|
|
87
|
+
if (!isLast) {
|
|
88
|
+
lines.push([...lastLine, { type: "punctuation", value: "," }]);
|
|
89
|
+
} else {
|
|
90
|
+
lines.push(lastLine);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
lines.push([padToken(), { type: "punctuation", value: "}" }]);
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OptionSchema, OptionDef } from "../../types/command.ts";
|
|
2
|
-
import type { FieldConfig, FieldType, FieldOption } from "../
|
|
2
|
+
import type { FieldConfig, FieldType, FieldOption } from "../semantic/types.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Convert an option type to a field type.
|
|
@@ -75,8 +75,6 @@ export function schemaToFieldConfigs(schema: OptionSchema): FieldConfig[] {
|
|
|
75
75
|
label: def.label ?? keyToLabel(key),
|
|
76
76
|
type: optionTypeToFieldType(def),
|
|
77
77
|
options: createFieldOptions(def),
|
|
78
|
-
placeholder: def.placeholder,
|
|
79
|
-
group: def.group,
|
|
80
78
|
};
|
|
81
79
|
|
|
82
80
|
fields.push(fieldConfig);
|
|
@@ -92,28 +90,6 @@ export function schemaToFieldConfigs(schema: OptionSchema): FieldConfig[] {
|
|
|
92
90
|
return fields;
|
|
93
91
|
}
|
|
94
92
|
|
|
95
|
-
/**
|
|
96
|
-
* Group field configs by their group property.
|
|
97
|
-
*
|
|
98
|
-
* @param fields - Field configs to group
|
|
99
|
-
* @returns Map of group name to field configs
|
|
100
|
-
*/
|
|
101
|
-
export function groupFieldConfigs(
|
|
102
|
-
fields: FieldConfig[]
|
|
103
|
-
): Map<string | undefined, FieldConfig[]> {
|
|
104
|
-
const groups = new Map<string | undefined, FieldConfig[]>();
|
|
105
|
-
|
|
106
|
-
for (const field of fields) {
|
|
107
|
-
const group = field.group;
|
|
108
|
-
if (!groups.has(group)) {
|
|
109
|
-
groups.set(group, []);
|
|
110
|
-
}
|
|
111
|
-
groups.get(group)!.push(field);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return groups;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
93
|
/**
|
|
118
94
|
* Get display value for a field.
|
|
119
95
|
*
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Text } from "ink";
|
|
2
|
-
import type { SpacerProps } from "../../../semantic/types.ts";
|
|
3
|
-
|
|
4
|
-
export function Spacer({ size, axis }: SpacerProps) {
|
|
5
|
-
if (axis === "horizontal") {
|
|
6
|
-
return <Text>{" ".repeat(size)}</Text>;
|
|
7
|
-
}
|
|
8
|
-
return (
|
|
9
|
-
<>
|
|
10
|
-
{Array.from({ length: size }).map((_, idx) => (
|
|
11
|
-
<Text key={idx} />
|
|
12
|
-
))}
|
|
13
|
-
</>
|
|
14
|
-
);
|
|
15
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { CodeProps } from "../../../semantic/types.ts";
|
|
2
|
-
import { SemanticColors } from "../../../theme.ts";
|
|
3
|
-
|
|
4
|
-
export function Code({ color = "code", children }: CodeProps) {
|
|
5
|
-
const fg = SemanticColors[color] ?? SemanticColors.code;
|
|
6
|
-
|
|
7
|
-
return (
|
|
8
|
-
<text fg={fg}>
|
|
9
|
-
{children}
|
|
10
|
-
</text>
|
|
11
|
-
);
|
|
12
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import type { ContainerProps, Spacing } from "../../../semantic/types.ts";
|
|
3
|
-
|
|
4
|
-
function normalizePadding(padding: number | Spacing | undefined):
|
|
5
|
-
| { padding?: number; paddingTop?: number; paddingRight?: number; paddingBottom?: number; paddingLeft?: number }
|
|
6
|
-
| undefined {
|
|
7
|
-
if (padding === undefined) {
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
if (typeof padding === "number") {
|
|
12
|
-
return { padding };
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return {
|
|
16
|
-
paddingTop: padding.top ?? 0,
|
|
17
|
-
paddingRight: padding.right ?? 0,
|
|
18
|
-
paddingBottom: padding.bottom ?? 0,
|
|
19
|
-
paddingLeft: padding.left ?? 0,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function Container({
|
|
24
|
-
children,
|
|
25
|
-
flex,
|
|
26
|
-
width,
|
|
27
|
-
height,
|
|
28
|
-
flexDirection,
|
|
29
|
-
alignItems,
|
|
30
|
-
justifyContent,
|
|
31
|
-
gap,
|
|
32
|
-
padding,
|
|
33
|
-
noShrink,
|
|
34
|
-
}: ContainerProps & { children?: ReactNode }) {
|
|
35
|
-
const resolvedPadding = normalizePadding(padding);
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<box
|
|
39
|
-
flexGrow={flex}
|
|
40
|
-
flexShrink={noShrink ? 0 : flex === undefined ? undefined : 1}
|
|
41
|
-
width={width as any}
|
|
42
|
-
height={height as any}
|
|
43
|
-
flexDirection={flexDirection as any}
|
|
44
|
-
alignItems={alignItems as any}
|
|
45
|
-
justifyContent={justifyContent as any}
|
|
46
|
-
gap={gap}
|
|
47
|
-
padding={resolvedPadding?.padding}
|
|
48
|
-
paddingTop={resolvedPadding?.paddingTop}
|
|
49
|
-
paddingRight={resolvedPadding?.paddingRight}
|
|
50
|
-
paddingBottom={resolvedPadding?.paddingBottom}
|
|
51
|
-
paddingLeft={resolvedPadding?.paddingLeft}
|
|
52
|
-
>
|
|
53
|
-
{children}
|
|
54
|
-
</box>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import type { ValueProps } from "../../../semantic/types.ts";
|
|
3
|
-
import { SemanticColors } from "../../../theme.ts";
|
|
4
|
-
|
|
5
|
-
export function Value({ color = "value", truncate, children }: ValueProps & { children: ReactNode }) {
|
|
6
|
-
const fg = SemanticColors[color] ?? SemanticColors.value;
|
|
7
|
-
|
|
8
|
-
return (
|
|
9
|
-
<text fg={fg} {...({ truncate })}>
|
|
10
|
-
{children}
|
|
11
|
-
</text>
|
|
12
|
-
);
|
|
13
|
-
}
|
|
File without changes
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { useActiveKeyHandler } from "../hooks/useActiveKeyHandler.ts";
|
|
2
|
-
import type { Command } from "../../core/command.ts";
|
|
3
|
-
import { MenuItem } from "../semantic/MenuItem.tsx";
|
|
4
|
-
import { Container } from "../semantic/Container.tsx";
|
|
5
|
-
import { Panel } from "../semantic/Panel.tsx";
|
|
6
|
-
import { Label } from "../semantic/Label.tsx";
|
|
7
|
-
|
|
8
|
-
interface CommandItem {
|
|
9
|
-
/** The command object */
|
|
10
|
-
command: Command;
|
|
11
|
-
/** Display label (defaults to command name) */
|
|
12
|
-
label?: string;
|
|
13
|
-
/** Description (defaults to command description) */
|
|
14
|
-
description?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface CommandSelectorProps {
|
|
18
|
-
/** Commands to display */
|
|
19
|
-
commands: CommandItem[];
|
|
20
|
-
/** Currently selected index */
|
|
21
|
-
selectedIndex: number;
|
|
22
|
-
/** Called when selection changes */
|
|
23
|
-
onSelectionChange: (index: number) => void;
|
|
24
|
-
/** Called when a command is selected */
|
|
25
|
-
onSelect: (command: Command) => void;
|
|
26
|
-
/** Breadcrumb path for nested commands */
|
|
27
|
-
breadcrumb?: string[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Command selection menu.
|
|
32
|
-
*/
|
|
33
|
-
export function CommandSelector({
|
|
34
|
-
commands,
|
|
35
|
-
selectedIndex,
|
|
36
|
-
onSelectionChange,
|
|
37
|
-
onSelect,
|
|
38
|
-
breadcrumb,
|
|
39
|
-
}: CommandSelectorProps) {
|
|
40
|
-
// Active keyboard handler for navigation
|
|
41
|
-
useActiveKeyHandler((key) => {
|
|
42
|
-
// Arrow key navigation
|
|
43
|
-
if (key.name === "down") {
|
|
44
|
-
const newIndex = Math.min(selectedIndex + 1, commands.length - 1);
|
|
45
|
-
onSelectionChange(newIndex);
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (key.name === "up") {
|
|
50
|
-
const newIndex = Math.max(selectedIndex - 1, 0);
|
|
51
|
-
onSelectionChange(newIndex);
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Enter to select command
|
|
56
|
-
if (key.name === "return" || key.name === "enter") {
|
|
57
|
-
const selected = commands[selectedIndex];
|
|
58
|
-
if (selected) {
|
|
59
|
-
onSelect(selected.command);
|
|
60
|
-
}
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return false;
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const title = breadcrumb?.length
|
|
68
|
-
? `Select Command (${breadcrumb.join(" › ")})`
|
|
69
|
-
: "Select Command";
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<Container flexDirection="column" flex={1} justifyContent="center" alignItems="center" gap={1}>
|
|
73
|
-
<Panel
|
|
74
|
-
flexDirection="column"
|
|
75
|
-
title={title}
|
|
76
|
-
padding={undefined}
|
|
77
|
-
width={60}
|
|
78
|
-
focused
|
|
79
|
-
>
|
|
80
|
-
<Container flexDirection="column" gap={1}>
|
|
81
|
-
{commands.map((item, idx) => {
|
|
82
|
-
const isSelected = idx === selectedIndex;
|
|
83
|
-
const label = item.label ?? item.command.displayName ?? item.command.name;
|
|
84
|
-
const description = item.description ?? item.command.description;
|
|
85
|
-
|
|
86
|
-
const modeIndicator = getModeIndicator(item.command);
|
|
87
|
-
|
|
88
|
-
return (
|
|
89
|
-
<MenuItem
|
|
90
|
-
key={item.command.name}
|
|
91
|
-
label={label}
|
|
92
|
-
description={description}
|
|
93
|
-
suffix={modeIndicator}
|
|
94
|
-
selected={isSelected}
|
|
95
|
-
onActivate={() => onSelect(item.command)}
|
|
96
|
-
/>
|
|
97
|
-
);
|
|
98
|
-
})}
|
|
99
|
-
</Container>
|
|
100
|
-
</Panel>
|
|
101
|
-
|
|
102
|
-
<Label color="mutedText">↑/↓ Navigate • Enter Select • Esc {breadcrumb?.length ? "Back" : "Exit"}</Label>
|
|
103
|
-
</Container>
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Get mode indicator for a command (e.g., "[cli]", "[tui]", "→" for subcommands).
|
|
109
|
-
*/
|
|
110
|
-
function getModeIndicator(command: Command): string {
|
|
111
|
-
// Show navigation indicator for container commands with navigable subcommands
|
|
112
|
-
// (excluding commands that don't support TUI)
|
|
113
|
-
const navigableSubCommands = command.subCommands?.filter((sub) => sub.supportsTui()) ?? [];
|
|
114
|
-
if (navigableSubCommands.length > 0) {
|
|
115
|
-
return "→";
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return "";
|
|
119
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { useRef, useEffect, type ReactNode } from "react";
|
|
2
|
-
import { Field } from "../semantic/Field.tsx";
|
|
3
|
-
import { MenuButton } from "../semantic/MenuButton.tsx";
|
|
4
|
-
import { Panel } from "../semantic/Panel.tsx";
|
|
5
|
-
import { ScrollView, type ScrollViewRef } from "../semantic/ScrollView.tsx";
|
|
6
|
-
import { Container } from "../semantic/Container.tsx";
|
|
7
|
-
import { useActiveKeyHandler } from "../hooks/useActiveKeyHandler.ts";
|
|
8
|
-
import type { KeyboardEvent } from "../adapters/types.ts";
|
|
9
|
-
import type { FieldConfig } from "./types.ts";
|
|
10
|
-
|
|
11
|
-
interface ConfigFormProps {
|
|
12
|
-
/** Title for the form border */
|
|
13
|
-
title: string;
|
|
14
|
-
/** Field configurations */
|
|
15
|
-
fieldConfigs: FieldConfig[];
|
|
16
|
-
/** Current values */
|
|
17
|
-
values: Record<string, unknown>;
|
|
18
|
-
/** Currently selected index */
|
|
19
|
-
selectedIndex: number;
|
|
20
|
-
/** Whether the form is focused */
|
|
21
|
-
focused: boolean;
|
|
22
|
-
/** Called when selection changes */
|
|
23
|
-
onSelectionChange: (index: number) => void;
|
|
24
|
-
/** Called when a field should be edited */
|
|
25
|
-
onEditField: (fieldKey: string) => void;
|
|
26
|
-
/** Called when the action button is pressed */
|
|
27
|
-
onAction: () => void;
|
|
28
|
-
/** Function to get display value for a field */
|
|
29
|
-
getDisplayValue?: (key: string, value: unknown, type: string) => string;
|
|
30
|
-
/** The action button component */
|
|
31
|
-
actionButton: ReactNode;
|
|
32
|
-
/** Optional additional buttons rendered before the main action button */
|
|
33
|
-
additionalButtons?: { label: string; onPress: () => void }[];
|
|
34
|
-
/** Optional handler for additional keys (called before default handling) */
|
|
35
|
-
onKeyDown?: (event: KeyboardEvent) => boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Default display value formatter.
|
|
40
|
-
*/
|
|
41
|
-
function defaultGetDisplayValue(_key: string, value: unknown, type: string): string {
|
|
42
|
-
if (type === "boolean") {
|
|
43
|
-
return value ? "True" : "False";
|
|
44
|
-
}
|
|
45
|
-
const strValue = String(value ?? "");
|
|
46
|
-
if (strValue === "") {
|
|
47
|
-
return "(empty)";
|
|
48
|
-
}
|
|
49
|
-
return strValue.length > 60 ? strValue.substring(0, 57) + "..." : strValue;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Generic config form that renders fields from a schema.
|
|
54
|
-
*/
|
|
55
|
-
export function ConfigForm({
|
|
56
|
-
title,
|
|
57
|
-
fieldConfigs,
|
|
58
|
-
values,
|
|
59
|
-
selectedIndex,
|
|
60
|
-
focused,
|
|
61
|
-
onSelectionChange,
|
|
62
|
-
onEditField,
|
|
63
|
-
onAction,
|
|
64
|
-
getDisplayValue = defaultGetDisplayValue,
|
|
65
|
-
actionButton,
|
|
66
|
-
additionalButtons = [],
|
|
67
|
-
onKeyDown,
|
|
68
|
-
}: ConfigFormProps) {
|
|
69
|
-
const scrollViewRef = useRef<ScrollViewRef | null>(null);
|
|
70
|
-
const totalItems = fieldConfigs.length + additionalButtons.length + 1; // fields + additional buttons + action button
|
|
71
|
-
|
|
72
|
-
// Auto-scroll to keep selected item visible
|
|
73
|
-
useEffect(() => {
|
|
74
|
-
scrollViewRef.current?.scrollToIndex(selectedIndex);
|
|
75
|
-
}, [selectedIndex]);
|
|
76
|
-
|
|
77
|
-
// Handle keyboard events (only when focused)
|
|
78
|
-
useActiveKeyHandler(
|
|
79
|
-
(event: KeyboardEvent) => {
|
|
80
|
-
// Let parent handle first if provided
|
|
81
|
-
if (onKeyDown?.(event)) {
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const key = event;
|
|
86
|
-
|
|
87
|
-
// Arrow key navigation
|
|
88
|
-
if (key.name === "down") {
|
|
89
|
-
const newIndex = Math.min(selectedIndex + 1, totalItems - 1);
|
|
90
|
-
onSelectionChange(newIndex);
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (key.name === "up") {
|
|
95
|
-
const newIndex = Math.max(selectedIndex - 1, 0);
|
|
96
|
-
onSelectionChange(newIndex);
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Enter to edit field, press additional button, or run action
|
|
101
|
-
if (key.name === "return" || key.name === "enter") {
|
|
102
|
-
if (selectedIndex < fieldConfigs.length) {
|
|
103
|
-
// It's a field
|
|
104
|
-
const fieldConfig = fieldConfigs[selectedIndex];
|
|
105
|
-
if (fieldConfig) {
|
|
106
|
-
onEditField(fieldConfig.key);
|
|
107
|
-
}
|
|
108
|
-
} else if (selectedIndex < fieldConfigs.length + additionalButtons.length) {
|
|
109
|
-
// It's an additional button
|
|
110
|
-
const buttonIndex = selectedIndex - fieldConfigs.length;
|
|
111
|
-
additionalButtons[buttonIndex]?.onPress();
|
|
112
|
-
} else {
|
|
113
|
-
// It's the main action button
|
|
114
|
-
onAction();
|
|
115
|
-
}
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
return false;
|
|
120
|
-
},
|
|
121
|
-
{ enabled: focused }
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<Panel
|
|
126
|
-
title={title}
|
|
127
|
-
focused={focused}
|
|
128
|
-
flex={1}
|
|
129
|
-
padding={1}
|
|
130
|
-
flexDirection="column"
|
|
131
|
-
>
|
|
132
|
-
<ScrollView
|
|
133
|
-
axis="vertical"
|
|
134
|
-
flex={1}
|
|
135
|
-
scrollRef={(ref) => {
|
|
136
|
-
scrollViewRef.current = ref;
|
|
137
|
-
}}
|
|
138
|
-
>
|
|
139
|
-
<Container flexDirection="column" gap={0}>
|
|
140
|
-
{fieldConfigs.map((field, idx) => {
|
|
141
|
-
const isSelected = idx === selectedIndex;
|
|
142
|
-
const displayValue = getDisplayValue(
|
|
143
|
-
field.key,
|
|
144
|
-
values[field.key],
|
|
145
|
-
field.type
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<Field
|
|
150
|
-
key={field.key}
|
|
151
|
-
label={field.label}
|
|
152
|
-
value={displayValue}
|
|
153
|
-
selected={isSelected}
|
|
154
|
-
/>
|
|
155
|
-
);
|
|
156
|
-
})}
|
|
157
|
-
|
|
158
|
-
{additionalButtons.map((btn, idx) => {
|
|
159
|
-
const buttonSelectedIndex = fieldConfigs.length + idx;
|
|
160
|
-
return (
|
|
161
|
-
<MenuButton
|
|
162
|
-
key={btn.label}
|
|
163
|
-
label={btn.label}
|
|
164
|
-
selected={selectedIndex === buttonSelectedIndex}
|
|
165
|
-
/>
|
|
166
|
-
);
|
|
167
|
-
})}
|
|
168
|
-
|
|
169
|
-
{actionButton}
|
|
170
|
-
</Container>
|
|
171
|
-
</ScrollView>
|
|
172
|
-
</Panel>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
File without changes
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { Container } from "../semantic/Container.tsx";
|
|
2
|
-
import { Label } from "../semantic/Label.tsx";
|
|
3
|
-
import { Spacer } from "../semantic/Spacer.tsx";
|
|
4
|
-
|
|
5
|
-
interface HeaderProps {
|
|
6
|
-
/** Application name */
|
|
7
|
-
name: string;
|
|
8
|
-
/** Application version */
|
|
9
|
-
version: string;
|
|
10
|
-
/** Optional breadcrumb path (e.g., ["run", "config"]) */
|
|
11
|
-
breadcrumb?: string[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Application header with name, version, and optional breadcrumb.
|
|
16
|
-
*/
|
|
17
|
-
export function Header({ name, version, breadcrumb }: HeaderProps) {
|
|
18
|
-
const breadcrumbStr = breadcrumb?.length ? ` › ${breadcrumb.join(" › ")}` : "";
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<Container flexDirection="column" noShrink>
|
|
22
|
-
<Container flexDirection="row" justifyContent="space-between">
|
|
23
|
-
<Label color="mutedText" bold>
|
|
24
|
-
{name}
|
|
25
|
-
{breadcrumbStr}
|
|
26
|
-
</Label>
|
|
27
|
-
<Label color="mutedText">v{version}</Label>
|
|
28
|
-
</Container>
|
|
29
|
-
<Spacer size={1} />
|
|
30
|
-
</Container>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import { Panel } from "../semantic/Panel.tsx";
|
|
3
|
-
import { Container } from "../semantic/Container.tsx";
|
|
4
|
-
import { Overlay } from "../semantic/Overlay.tsx";
|
|
5
|
-
|
|
6
|
-
type Dim = number | `${number}%` | "auto";
|
|
7
|
-
|
|
8
|
-
interface ModalBaseProps {
|
|
9
|
-
title?: string;
|
|
10
|
-
width?: Dim;
|
|
11
|
-
height?: Dim;
|
|
12
|
-
top?: Dim;
|
|
13
|
-
left?: Dim;
|
|
14
|
-
right?: Dim;
|
|
15
|
-
bottom?: Dim;
|
|
16
|
-
children: ReactNode;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function ModalBase({
|
|
20
|
-
title,
|
|
21
|
-
width = "80%",
|
|
22
|
-
height = "auto",
|
|
23
|
-
top = 4,
|
|
24
|
-
left = 4,
|
|
25
|
-
right,
|
|
26
|
-
bottom,
|
|
27
|
-
children,
|
|
28
|
-
}: ModalBaseProps) {
|
|
29
|
-
return (
|
|
30
|
-
<Overlay zIndex={20} top={top} left={left} right={right} bottom={bottom} width={width} height={height}>
|
|
31
|
-
<Panel title={title} border={true} flexDirection="column" flex={1} padding={1} surface="overlay">
|
|
32
|
-
<Container flexDirection="column" gap={1} flex={1}>
|
|
33
|
-
{children}
|
|
34
|
-
</Container>
|
|
35
|
-
</Panel>
|
|
36
|
-
</Overlay>
|
|
37
|
-
);
|
|
38
|
-
}
|