@pablozaiden/terminatui 0.3.0-beta-1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -3
- package/src/__tests__/adapterNoSharedUi.test.ts +34 -0
- package/src/__tests__/configOnChange.test.ts +63 -0
- package/src/__tests__/schemaToFields.test.ts +0 -4
- package/src/__tests__/tuiRootNoCoupling.test.ts +25 -0
- package/src/builtins/version.ts +1 -1
- package/src/index.ts +22 -0
- package/src/tui/TuiApplication.tsx +0 -4
- package/src/tui/TuiRoot.tsx +58 -102
- package/src/tui/actions.ts +4 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +191 -41
- package/src/tui/adapters/ink/SemanticInkRenderer.tsx +210 -0
- package/src/tui/adapters/ink/components/Button.tsx +10 -2
- package/src/tui/adapters/ink/components/Overlay.tsx +8 -2
- package/src/tui/adapters/ink/components/Panel.tsx +26 -5
- package/src/tui/adapters/ink/components/ScrollView.tsx +44 -3
- package/src/tui/adapters/ink/components/Spinner.tsx +8 -2
- package/src/tui/adapters/ink/keyboard.ts +0 -3
- package/src/tui/adapters/ink/ui/CommandSelector.tsx +56 -0
- package/src/tui/adapters/ink/ui/ConfigForm.tsx +77 -0
- package/src/tui/adapters/ink/ui/Header.tsx +25 -0
- package/src/tui/adapters/ink/ui/JsonHighlight.tsx +21 -0
- package/src/tui/adapters/ink/ui/ResultsPanel.tsx +57 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +190 -39
- package/src/tui/adapters/opentui/SemanticOpenTuiRenderer.tsx +192 -0
- package/src/tui/adapters/opentui/components/Label.tsx +2 -2
- package/src/tui/adapters/opentui/components/Overlay.tsx +12 -3
- package/src/tui/adapters/opentui/components/Panel.tsx +11 -1
- package/src/tui/adapters/opentui/components/ScrollView.tsx +1 -8
- package/src/tui/adapters/opentui/components/Spinner.tsx +1 -1
- package/src/tui/adapters/opentui/keyboard.ts +0 -3
- package/src/tui/adapters/opentui/ui/CommandSelector.tsx +55 -0
- package/src/tui/adapters/opentui/ui/ConfigForm.tsx +74 -0
- package/src/tui/adapters/opentui/ui/Header.tsx +24 -0
- package/src/tui/adapters/opentui/ui/JsonHighlight.tsx +20 -0
- package/src/tui/adapters/opentui/ui/LogsPanel.tsx +44 -0
- package/src/tui/adapters/opentui/ui/ResultsPanel.tsx +62 -0
- package/src/tui/adapters/shared/TerminalClipboard.ts +65 -0
- package/src/tui/adapters/{opentui/hooks → shared}/useSpinner.ts +5 -1
- package/src/tui/adapters/types.ts +25 -45
- package/src/tui/components/JsonHighlight.tsx +41 -111
- package/src/tui/context/ActionContext.tsx +51 -0
- package/src/tui/context/ExecutorContext.tsx +7 -1
- package/src/tui/context/NavigationContext.tsx +20 -4
- package/src/tui/controllers/CommandBrowserController.tsx +100 -0
- package/src/tui/controllers/ConfigController.tsx +183 -0
- package/src/tui/controllers/EditorController.tsx +169 -0
- package/src/tui/controllers/LogsController.tsx +48 -0
- package/src/tui/controllers/OutcomeController.tsx +110 -0
- package/src/tui/driver/TuiDriver.tsx +148 -0
- package/src/tui/driver/context/TuiDriverContext.tsx +44 -0
- package/src/tui/driver/types.ts +72 -0
- package/src/tui/semantic/AppShell.tsx +30 -0
- package/src/tui/semantic/CommandBrowserScreen.tsx +16 -0
- package/src/tui/semantic/ConfigScreen.tsx +23 -0
- package/src/tui/semantic/EditorScreen.tsx +20 -0
- package/src/tui/semantic/LogsScreen.tsx +9 -0
- package/src/tui/semantic/RunningScreen.tsx +17 -0
- package/src/tui/semantic/layoutTypes.ts +72 -0
- package/src/tui/semantic/render.tsx +44 -0
- package/src/tui/semantic/types.ts +31 -98
- package/src/tui/utils/jsonTokenizer.ts +98 -0
- package/src/tui/utils/schemaToFields.ts +1 -25
- package/.devcontainer/devcontainer.json +0 -19
- package/.devcontainer/install-prerequisites.sh +0 -49
- package/.github/workflows/copilot-setup-steps.yml +0 -32
- package/.github/workflows/pull-request.yml +0 -27
- package/.github/workflows/release-npm-package.yml +0 -81
- package/AGENTS.md +0 -43
- package/CLAUDE.md +0 -1
- package/bun.lock +0 -321
- package/examples/tui-app/commands/config/app/get.ts +0 -62
- package/examples/tui-app/commands/config/app/index.ts +0 -23
- package/examples/tui-app/commands/config/app/set.ts +0 -96
- package/examples/tui-app/commands/config/index.ts +0 -28
- package/examples/tui-app/commands/config/user/get.ts +0 -61
- package/examples/tui-app/commands/config/user/index.ts +0 -23
- package/examples/tui-app/commands/config/user/set.ts +0 -57
- package/examples/tui-app/commands/greet.ts +0 -78
- package/examples/tui-app/commands/math.ts +0 -111
- package/examples/tui-app/commands/status.ts +0 -86
- package/examples/tui-app/index.ts +0 -38
- package/guides/01-hello-world.md +0 -101
- package/guides/02-adding-options.md +0 -103
- package/guides/03-multiple-commands.md +0 -161
- package/guides/04-subcommands.md +0 -206
- package/guides/05-interactive-tui.md +0 -209
- package/guides/06-config-validation.md +0 -256
- package/guides/07-async-cancellation.md +0 -334
- package/guides/08-complete-application.md +0 -507
- package/guides/README.md +0 -78
- package/src/tui/adapters/ink/components/Code.tsx +0 -6
- package/src/tui/adapters/ink/components/Container.tsx +0 -5
- package/src/tui/adapters/ink/components/Spacer.tsx +0 -15
- package/src/tui/adapters/ink/components/Value.tsx +0 -7
- package/src/tui/adapters/opentui/components/Code.tsx +0 -12
- package/src/tui/adapters/opentui/components/Container.tsx +0 -56
- package/src/tui/adapters/opentui/components/Spacer.tsx +0 -5
- package/src/tui/adapters/opentui/components/Value.tsx +0 -13
- package/src/tui/components/ActionButton.tsx +0 -0
- package/src/tui/components/CommandSelector.tsx +0 -119
- package/src/tui/components/ConfigForm.tsx +0 -174
- package/src/tui/components/FieldRow.tsx +0 -0
- package/src/tui/components/Header.tsx +0 -32
- package/src/tui/components/ModalBase.tsx +0 -38
- package/src/tui/components/ResultsPanel.tsx +0 -84
- package/src/tui/components/StatusBar.tsx +0 -44
- package/src/tui/components/logColors.ts +0 -12
- package/src/tui/components/types.ts +0 -30
- package/src/tui/context/ClipboardContext.tsx +0 -87
- package/src/tui/context/KeyboardContext.tsx +0 -132
- package/src/tui/hooks/useActiveKeyHandler.ts +0 -75
- package/src/tui/hooks/useClipboard.ts +0 -81
- package/src/tui/hooks/useClipboardProvider.ts +0 -42
- package/src/tui/hooks/useGlobalKeyHandler.ts +0 -54
- package/src/tui/modals/CliModal.tsx +0 -82
- package/src/tui/modals/EditorModal.tsx +0 -207
- package/src/tui/modals/LogsModal.tsx +0 -98
- package/src/tui/registry.ts +0 -102
- package/src/tui/screens/CommandSelectScreen.tsx +0 -162
- package/src/tui/screens/ConfigScreen.tsx +0 -160
- package/src/tui/screens/ErrorScreen.tsx +0 -58
- package/src/tui/screens/ResultsScreen.tsx +0 -60
- package/src/tui/screens/RunningScreen.tsx +0 -72
- package/src/tui/screens/ScreenBase.ts +0 -6
- package/src/tui/semantic/Button.tsx +0 -7
- package/src/tui/semantic/Code.tsx +0 -7
- package/src/tui/semantic/CodeHighlight.tsx +0 -7
- package/src/tui/semantic/Container.tsx +0 -7
- package/src/tui/semantic/Field.tsx +0 -7
- package/src/tui/semantic/Label.tsx +0 -7
- package/src/tui/semantic/MenuButton.tsx +0 -7
- package/src/tui/semantic/MenuItem.tsx +0 -7
- package/src/tui/semantic/Overlay.tsx +0 -7
- package/src/tui/semantic/Panel.tsx +0 -7
- package/src/tui/semantic/ScrollView.tsx +0 -9
- package/src/tui/semantic/Select.tsx +0 -7
- package/src/tui/semantic/Spacer.tsx +0 -7
- package/src/tui/semantic/Spinner.tsx +0 -7
- package/src/tui/semantic/TextInput.tsx +0 -7
- package/src/tui/semantic/Value.tsx +0 -7
- package/tsconfig.json +0 -25
|
@@ -1,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
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
import type { CommandResult } from "../../core/command.ts";
|
|
3
|
-
import { Container } from "../semantic/Container.tsx";
|
|
4
|
-
import { Panel } from "../semantic/Panel.tsx";
|
|
5
|
-
import { ScrollView } from "../semantic/ScrollView.tsx";
|
|
6
|
-
import { Label } from "../semantic/Label.tsx";
|
|
7
|
-
import { Value } from "../semantic/Value.tsx";
|
|
8
|
-
|
|
9
|
-
interface ResultsPanelProps {
|
|
10
|
-
/** The result to display */
|
|
11
|
-
result: CommandResult | null;
|
|
12
|
-
/** Error to display (if any) */
|
|
13
|
-
error: Error | null;
|
|
14
|
-
/** Whether the panel is focused */
|
|
15
|
-
focused: boolean;
|
|
16
|
-
/** Custom result renderer */
|
|
17
|
-
renderResult?: (result: CommandResult) => ReactNode;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Panel displaying command execution results.
|
|
22
|
-
*/
|
|
23
|
-
export function ResultsPanel({
|
|
24
|
-
result,
|
|
25
|
-
error,
|
|
26
|
-
focused,
|
|
27
|
-
renderResult,
|
|
28
|
-
}: ResultsPanelProps) {
|
|
29
|
-
|
|
30
|
-
// Determine content to display
|
|
31
|
-
let content: ReactNode;
|
|
32
|
-
|
|
33
|
-
if (error) {
|
|
34
|
-
content = (
|
|
35
|
-
<Container flexDirection="column" gap={1}>
|
|
36
|
-
<Label color="error" bold>
|
|
37
|
-
Error
|
|
38
|
-
</Label>
|
|
39
|
-
<Label color="error">{error.message}</Label>
|
|
40
|
-
</Container>
|
|
41
|
-
);
|
|
42
|
-
} else if (result) {
|
|
43
|
-
if (renderResult) {
|
|
44
|
-
const customContent = renderResult(result);
|
|
45
|
-
|
|
46
|
-
if (typeof customContent === "string" || typeof customContent === "number" || typeof customContent === "boolean") {
|
|
47
|
-
// Wrap primitive results so the renderer gets a text node
|
|
48
|
-
content = <Value>{String(customContent)}</Value>;
|
|
49
|
-
} else {
|
|
50
|
-
content = customContent as ReactNode;
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
// Default JSON display
|
|
54
|
-
content = (
|
|
55
|
-
<Container flexDirection="column" gap={1}>
|
|
56
|
-
{result.message && (
|
|
57
|
-
<Label color={result.success ? "success" : "error"}>{result.message}</Label>
|
|
58
|
-
)}
|
|
59
|
-
{result.data !== undefined && result.data !== null && (
|
|
60
|
-
<Value>{JSON.stringify(result.data, null, 2)}</Value>
|
|
61
|
-
)}
|
|
62
|
-
</Container>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
content = <Label color="mutedText">No results yet...</Label>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return (
|
|
70
|
-
<Panel
|
|
71
|
-
title="Results"
|
|
72
|
-
focused={focused}
|
|
73
|
-
flex={1}
|
|
74
|
-
padding={1}
|
|
75
|
-
flexDirection="column"
|
|
76
|
-
>
|
|
77
|
-
<ScrollView axis="vertical" flex={1} focused={focused}>
|
|
78
|
-
<Container flexDirection="column">
|
|
79
|
-
{content}
|
|
80
|
-
</Container>
|
|
81
|
-
</ScrollView>
|
|
82
|
-
</Panel>
|
|
83
|
-
);
|
|
84
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Label } from "../semantic/Label.tsx";
|
|
2
|
-
import { Spinner } from "../semantic/Spinner.tsx";
|
|
3
|
-
import { Panel } from "../semantic/Panel.tsx";
|
|
4
|
-
import { Container } from "../semantic/Container.tsx";
|
|
5
|
-
|
|
6
|
-
interface StatusBarProps {
|
|
7
|
-
/** Status message to display */
|
|
8
|
-
status: string;
|
|
9
|
-
/** Whether the app is currently running a command */
|
|
10
|
-
isRunning?: boolean;
|
|
11
|
-
/** Whether to show keyboard shortcuts */
|
|
12
|
-
showShortcuts?: boolean;
|
|
13
|
-
/** Custom shortcuts string (defaults to standard shortcuts) */
|
|
14
|
-
shortcuts?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Status bar showing current status, spinner, and keyboard shortcuts.
|
|
19
|
-
*/
|
|
20
|
-
export function StatusBar({
|
|
21
|
-
status,
|
|
22
|
-
isRunning = false,
|
|
23
|
-
showShortcuts = true,
|
|
24
|
-
shortcuts = "L Logs • C CLI • Tab Switch • Ctrl+Y Copy • Esc Back",
|
|
25
|
-
}: StatusBarProps) {
|
|
26
|
-
return (
|
|
27
|
-
<Panel dense border={true} flexDirection="column" gap={0} height={showShortcuts ? 4 : 2}>
|
|
28
|
-
<Container flexDirection="row" justifyContent="space-between" padding={{ left: 1, right: 1 }}>
|
|
29
|
-
<Container flexDirection="row">
|
|
30
|
-
<Spinner active={isRunning} />
|
|
31
|
-
<Label color="success" bold>
|
|
32
|
-
{status}
|
|
33
|
-
</Label>
|
|
34
|
-
</Container>
|
|
35
|
-
</Container>
|
|
36
|
-
|
|
37
|
-
{showShortcuts ? (
|
|
38
|
-
<Container padding={{ left: 1, right: 1 }}>
|
|
39
|
-
<Label color="mutedText">{shortcuts}</Label>
|
|
40
|
-
</Container>
|
|
41
|
-
) : null}
|
|
42
|
-
</Panel>
|
|
43
|
-
);
|
|
44
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { LogLevel } from "../../core/logger";
|
|
2
|
-
|
|
3
|
-
// Shared colors for log levels used across debug views.
|
|
4
|
-
export const LogColors: Record<LogLevel, string> = {
|
|
5
|
-
[LogLevel.silly]: "#8c8c8c",
|
|
6
|
-
[LogLevel.trace]: "#6dd6ff",
|
|
7
|
-
[LogLevel.debug]: "#7bdcb5",
|
|
8
|
-
[LogLevel.info]: "#d6dde6",
|
|
9
|
-
[LogLevel.warn]: "#f5c542",
|
|
10
|
-
[LogLevel.error]: "#f78888",
|
|
11
|
-
[LogLevel.fatal]: "#ff5c8d",
|
|
12
|
-
};
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Field type for TUI forms.
|
|
3
|
-
*/
|
|
4
|
-
export type FieldType = "text" | "number" | "enum" | "boolean";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Option for enum/select fields.
|
|
8
|
-
*/
|
|
9
|
-
export interface FieldOption {
|
|
10
|
-
name: string;
|
|
11
|
-
value: unknown;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Field configuration for TUI forms.
|
|
16
|
-
*/
|
|
17
|
-
export interface FieldConfig {
|
|
18
|
-
/** Field key (must match a key in values) */
|
|
19
|
-
key: string;
|
|
20
|
-
/** Display label */
|
|
21
|
-
label: string;
|
|
22
|
-
/** Field type */
|
|
23
|
-
type: FieldType;
|
|
24
|
-
/** Options for enum type */
|
|
25
|
-
options?: FieldOption[];
|
|
26
|
-
/** Placeholder text for input fields */
|
|
27
|
-
placeholder?: string;
|
|
28
|
-
/** Group name for organizing fields */
|
|
29
|
-
group?: string;
|
|
30
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useRef, useCallback, type ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Clipboard content that can be provided by a screen or modal.
|
|
5
|
-
*/
|
|
6
|
-
export interface ClipboardContent {
|
|
7
|
-
content: string;
|
|
8
|
-
label: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Provider function that returns clipboard content or null.
|
|
13
|
-
*/
|
|
14
|
-
export type ClipboardProvider = () => ClipboardContent | null;
|
|
15
|
-
|
|
16
|
-
interface ClipboardContextValue {
|
|
17
|
-
/**
|
|
18
|
-
* Register a clipboard provider. Returns an unregister function.
|
|
19
|
-
* Providers are stacked - the most recently registered provider is checked first.
|
|
20
|
-
*/
|
|
21
|
-
register: (id: string, provider: ClipboardProvider) => () => void;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Get clipboard content from the topmost provider that returns content.
|
|
25
|
-
*/
|
|
26
|
-
getContent: () => ClipboardContent | null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const ClipboardContext = createContext<ClipboardContextValue | null>(null);
|
|
30
|
-
|
|
31
|
-
interface ClipboardProviderProps {
|
|
32
|
-
children: ReactNode;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Provider that manages clipboard content providers from screens and modals.
|
|
37
|
-
* Providers are stacked - modals register on top of screens, so modal content
|
|
38
|
-
* takes precedence when copying.
|
|
39
|
-
*/
|
|
40
|
-
export function ClipboardProviderComponent({ children }: ClipboardProviderProps) {
|
|
41
|
-
const providersRef = useRef<Map<string, ClipboardProvider>>(new Map());
|
|
42
|
-
const orderRef = useRef<string[]>([]);
|
|
43
|
-
|
|
44
|
-
const register = useCallback((id: string, provider: ClipboardProvider) => {
|
|
45
|
-
providersRef.current.set(id, provider);
|
|
46
|
-
// Add to end (most recent)
|
|
47
|
-
orderRef.current = orderRef.current.filter((i) => i !== id);
|
|
48
|
-
orderRef.current.push(id);
|
|
49
|
-
|
|
50
|
-
return () => {
|
|
51
|
-
providersRef.current.delete(id);
|
|
52
|
-
orderRef.current = orderRef.current.filter((i) => i !== id);
|
|
53
|
-
};
|
|
54
|
-
}, []);
|
|
55
|
-
|
|
56
|
-
const getContent = useCallback((): ClipboardContent | null => {
|
|
57
|
-
// Check providers in reverse order (most recent first)
|
|
58
|
-
for (let i = orderRef.current.length - 1; i >= 0; i--) {
|
|
59
|
-
const id = orderRef.current[i];
|
|
60
|
-
const provider = providersRef.current.get(id!);
|
|
61
|
-
if (provider) {
|
|
62
|
-
const content = provider();
|
|
63
|
-
if (content) {
|
|
64
|
-
return content;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}, []);
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<ClipboardContext.Provider value={{ register, getContent }}>
|
|
73
|
-
{children}
|
|
74
|
-
</ClipboardContext.Provider>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Access the clipboard context.
|
|
80
|
-
*/
|
|
81
|
-
export function useClipboardContext(): ClipboardContextValue {
|
|
82
|
-
const context = useContext(ClipboardContext);
|
|
83
|
-
if (!context) {
|
|
84
|
-
throw new Error("useClipboardContext must be used within a ClipboardProviderComponent");
|
|
85
|
-
}
|
|
86
|
-
return context;
|
|
87
|
-
}
|