@pablozaiden/terminatui 0.2.0 → 0.3.0-beta-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/AGENTS.md +14 -2
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +6 -10
- package/examples/tui-app/commands/config/app/index.ts +2 -6
- package/examples/tui-app/commands/config/app/set.ts +23 -13
- package/examples/tui-app/commands/config/index.ts +2 -6
- package/examples/tui-app/commands/config/user/get.ts +6 -10
- package/examples/tui-app/commands/config/user/index.ts +2 -6
- package/examples/tui-app/commands/config/user/set.ts +6 -10
- package/examples/tui-app/commands/greet.ts +13 -11
- package/examples/tui-app/commands/math.ts +5 -9
- package/examples/tui-app/commands/status.ts +21 -12
- package/examples/tui-app/index.ts +6 -3
- package/guides/01-hello-world.md +7 -2
- package/guides/02-adding-options.md +2 -2
- package/guides/03-multiple-commands.md +6 -8
- package/guides/04-subcommands.md +8 -8
- package/guides/05-interactive-tui.md +45 -30
- package/guides/06-config-validation.md +4 -12
- package/guides/07-async-cancellation.md +14 -16
- package/guides/08-complete-application.md +12 -42
- package/guides/README.md +7 -3
- package/package.json +4 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +12 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +4 -4
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +70 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +45 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +160 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +60 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/examples/tui-app/commands/index.ts +0 -4
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/index.ts +0 -137
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -619
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { useRef, useEffect, type ReactNode } from "react";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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";
|
|
6
9
|
import type { FieldConfig } from "./types.ts";
|
|
7
10
|
|
|
8
11
|
interface ConfigFormProps {
|
|
@@ -26,6 +29,10 @@ interface ConfigFormProps {
|
|
|
26
29
|
getDisplayValue?: (key: string, value: unknown, type: string) => string;
|
|
27
30
|
/** The action button component */
|
|
28
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;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/**
|
|
@@ -56,72 +63,80 @@ export function ConfigForm({
|
|
|
56
63
|
onAction,
|
|
57
64
|
getDisplayValue = defaultGetDisplayValue,
|
|
58
65
|
actionButton,
|
|
66
|
+
additionalButtons = [],
|
|
67
|
+
onKeyDown,
|
|
59
68
|
}: ConfigFormProps) {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const totalFields = fieldConfigs.length + 1; // +1 for action button
|
|
69
|
+
const scrollViewRef = useRef<ScrollViewRef | null>(null);
|
|
70
|
+
const totalItems = fieldConfigs.length + additionalButtons.length + 1; // fields + additional buttons + action button
|
|
63
71
|
|
|
64
72
|
// Auto-scroll to keep selected item visible
|
|
65
73
|
useEffect(() => {
|
|
66
|
-
|
|
67
|
-
scrollboxRef.current.scrollTo(selectedIndex);
|
|
68
|
-
}
|
|
74
|
+
scrollViewRef.current?.scrollToIndex(selectedIndex);
|
|
69
75
|
}, [selectedIndex]);
|
|
70
76
|
|
|
71
|
-
// Handle keyboard events
|
|
72
|
-
|
|
73
|
-
(event) => {
|
|
74
|
-
|
|
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;
|
|
75
86
|
|
|
76
87
|
// Arrow key navigation
|
|
77
88
|
if (key.name === "down") {
|
|
78
|
-
const newIndex = Math.min(selectedIndex + 1,
|
|
89
|
+
const newIndex = Math.min(selectedIndex + 1, totalItems - 1);
|
|
79
90
|
onSelectionChange(newIndex);
|
|
80
|
-
|
|
81
|
-
return;
|
|
91
|
+
return true;
|
|
82
92
|
}
|
|
83
93
|
|
|
84
94
|
if (key.name === "up") {
|
|
85
95
|
const newIndex = Math.max(selectedIndex - 1, 0);
|
|
86
96
|
onSelectionChange(newIndex);
|
|
87
|
-
|
|
88
|
-
return;
|
|
97
|
+
return true;
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
// Enter to edit field or run action
|
|
100
|
+
// Enter to edit field, press additional button, or run action
|
|
92
101
|
if (key.name === "return" || key.name === "enter") {
|
|
93
|
-
if (selectedIndex
|
|
94
|
-
|
|
95
|
-
} else {
|
|
102
|
+
if (selectedIndex < fieldConfigs.length) {
|
|
103
|
+
// It's a field
|
|
96
104
|
const fieldConfig = fieldConfigs[selectedIndex];
|
|
97
105
|
if (fieldConfig) {
|
|
98
106
|
onEditField(fieldConfig.key);
|
|
99
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();
|
|
100
115
|
}
|
|
101
|
-
|
|
102
|
-
return;
|
|
116
|
+
return true;
|
|
103
117
|
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
104
120
|
},
|
|
105
|
-
KeyboardPriority.Focused,
|
|
106
121
|
{ enabled: focused }
|
|
107
122
|
);
|
|
108
123
|
|
|
109
124
|
return (
|
|
110
|
-
<
|
|
111
|
-
flexDirection="column"
|
|
112
|
-
border={true}
|
|
113
|
-
borderStyle="rounded"
|
|
114
|
-
borderColor={borderColor}
|
|
125
|
+
<Panel
|
|
115
126
|
title={title}
|
|
116
|
-
|
|
127
|
+
focused={focused}
|
|
128
|
+
flex={1}
|
|
117
129
|
padding={1}
|
|
130
|
+
flexDirection="column"
|
|
118
131
|
>
|
|
119
|
-
<
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
132
|
+
<ScrollView
|
|
133
|
+
axis="vertical"
|
|
134
|
+
flex={1}
|
|
135
|
+
scrollRef={(ref) => {
|
|
136
|
+
scrollViewRef.current = ref;
|
|
137
|
+
}}
|
|
123
138
|
>
|
|
124
|
-
<
|
|
139
|
+
<Container flexDirection="column" gap={0}>
|
|
125
140
|
{fieldConfigs.map((field, idx) => {
|
|
126
141
|
const isSelected = idx === selectedIndex;
|
|
127
142
|
const displayValue = getDisplayValue(
|
|
@@ -131,18 +146,29 @@ export function ConfigForm({
|
|
|
131
146
|
);
|
|
132
147
|
|
|
133
148
|
return (
|
|
134
|
-
<
|
|
149
|
+
<Field
|
|
135
150
|
key={field.key}
|
|
136
151
|
label={field.label}
|
|
137
152
|
value={displayValue}
|
|
138
|
-
|
|
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}
|
|
139
165
|
/>
|
|
140
166
|
);
|
|
141
167
|
})}
|
|
142
168
|
|
|
143
169
|
{actionButton}
|
|
144
|
-
</
|
|
145
|
-
</
|
|
146
|
-
</
|
|
170
|
+
</Container>
|
|
171
|
+
</ScrollView>
|
|
172
|
+
</Panel>
|
|
147
173
|
);
|
|
148
174
|
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Theme } from "../theme.ts";
|
|
2
|
-
|
|
3
|
-
interface FieldRowProps {
|
|
4
|
-
/** Field label */
|
|
5
|
-
label: string;
|
|
6
|
-
/** Field value to display */
|
|
7
|
-
value: string;
|
|
8
|
-
/** Whether this row is selected */
|
|
9
|
-
isSelected: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* A single row in a config form displaying a field label and value.
|
|
14
|
-
*/
|
|
15
|
-
export function FieldRow({ label, value, isSelected }: FieldRowProps) {
|
|
16
|
-
const prefix = isSelected ? "► " : " ";
|
|
17
|
-
const labelColor = isSelected ? Theme.borderFocused : Theme.label;
|
|
18
|
-
const valueColor = isSelected ? Theme.value : Theme.statusText;
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<box flexDirection="row" gap={1}>
|
|
22
|
-
<text fg={labelColor}>
|
|
23
|
-
{prefix}{label}:
|
|
24
|
-
</text>
|
|
25
|
-
<text fg={valueColor}>
|
|
26
|
-
{value}
|
|
27
|
-
</text>
|
|
28
|
-
</box>
|
|
29
|
-
);
|
|
30
|
-
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Container } from "../semantic/Container.tsx";
|
|
2
|
+
import { Label } from "../semantic/Label.tsx";
|
|
3
|
+
import { Spacer } from "../semantic/Spacer.tsx";
|
|
2
4
|
|
|
3
5
|
interface HeaderProps {
|
|
4
6
|
/** Application name */
|
|
@@ -13,19 +15,18 @@ interface HeaderProps {
|
|
|
13
15
|
* Application header with name, version, and optional breadcrumb.
|
|
14
16
|
*/
|
|
15
17
|
export function Header({ name, version, breadcrumb }: HeaderProps) {
|
|
16
|
-
const breadcrumbStr = breadcrumb?.length
|
|
17
|
-
? ` › ${breadcrumb.join(" › ")}`
|
|
18
|
-
: "";
|
|
18
|
+
const breadcrumbStr = breadcrumb?.length ? ` › ${breadcrumb.join(" › ")}` : "";
|
|
19
19
|
|
|
20
20
|
return (
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
v{version}
|
|
28
|
-
</
|
|
29
|
-
|
|
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>
|
|
30
31
|
);
|
|
31
32
|
}
|
|
@@ -1,20 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Container } from "../semantic/Container.tsx";
|
|
2
|
+
import { CodeHighlight } from "../semantic/CodeHighlight.tsx";
|
|
3
|
+
import type { CodeTokenType } from "../semantic/types.ts";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* JSON syntax highlighting types and colors
|
|
5
7
|
*/
|
|
6
|
-
type JsonTokenType =
|
|
8
|
+
type JsonTokenType = Exclude<CodeTokenType, "unknown">;
|
|
7
9
|
type JsonToken = { type: JsonTokenType; value: string };
|
|
8
10
|
type JsonLineTokens = JsonToken[];
|
|
9
11
|
|
|
10
|
-
const TOKEN_COLORS: Record<JsonTokenType, string> = {
|
|
11
|
-
key: "#61afef", // blue
|
|
12
|
-
string: "#98c379", // green
|
|
13
|
-
number: "#d19a66", // orange
|
|
14
|
-
boolean: "#c678dd", // purple
|
|
15
|
-
null: "#c678dd", // purple
|
|
16
|
-
punctuation: Theme.label,
|
|
17
|
-
};
|
|
18
12
|
|
|
19
13
|
function tokenizeJson(value: unknown, indent = 0): JsonLineTokens[] {
|
|
20
14
|
const pad = " ".repeat(indent);
|
|
@@ -115,14 +109,13 @@ export interface JsonHighlightProps {
|
|
|
115
109
|
export function JsonHighlight({ value }: JsonHighlightProps) {
|
|
116
110
|
const lines = tokenizeJson(value);
|
|
117
111
|
return (
|
|
118
|
-
<
|
|
112
|
+
<Container flexDirection="column" gap={0}>
|
|
119
113
|
{lines.map((tokens, lineIdx) => (
|
|
120
|
-
<
|
|
121
|
-
{
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
</text>
|
|
114
|
+
<CodeHighlight
|
|
115
|
+
key={`json-${lineIdx}`}
|
|
116
|
+
tokens={tokens.map((token) => ({ type: token.type, value: token.value }))}
|
|
117
|
+
/>
|
|
125
118
|
))}
|
|
126
|
-
</
|
|
119
|
+
</Container>
|
|
127
120
|
);
|
|
128
121
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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,6 +1,10 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import { Theme } from "../theme.ts";
|
|
3
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";
|
|
4
8
|
|
|
5
9
|
interface ResultsPanelProps {
|
|
6
10
|
/** The result to display */
|
|
@@ -22,21 +26,18 @@ export function ResultsPanel({
|
|
|
22
26
|
focused,
|
|
23
27
|
renderResult,
|
|
24
28
|
}: ResultsPanelProps) {
|
|
25
|
-
const borderColor = focused ? Theme.borderFocused : Theme.border;
|
|
26
29
|
|
|
27
30
|
// Determine content to display
|
|
28
31
|
let content: ReactNode;
|
|
29
32
|
|
|
30
33
|
if (error) {
|
|
31
34
|
content = (
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
</
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
</text>
|
|
39
|
-
</box>
|
|
35
|
+
<Container flexDirection="column" gap={1}>
|
|
36
|
+
<Label color="error" bold>
|
|
37
|
+
Error
|
|
38
|
+
</Label>
|
|
39
|
+
<Label color="error">{error.message}</Label>
|
|
40
|
+
</Container>
|
|
40
41
|
);
|
|
41
42
|
} else if (result) {
|
|
42
43
|
if (renderResult) {
|
|
@@ -44,50 +45,40 @@ export function ResultsPanel({
|
|
|
44
45
|
|
|
45
46
|
if (typeof customContent === "string" || typeof customContent === "number" || typeof customContent === "boolean") {
|
|
46
47
|
// Wrap primitive results so the renderer gets a text node
|
|
47
|
-
content = (
|
|
48
|
-
<text fg={Theme.value}>
|
|
49
|
-
{String(customContent)}
|
|
50
|
-
</text>
|
|
51
|
-
);
|
|
48
|
+
content = <Value>{String(customContent)}</Value>;
|
|
52
49
|
} else {
|
|
53
50
|
content = customContent as ReactNode;
|
|
54
51
|
}
|
|
55
52
|
} else {
|
|
56
53
|
// Default JSON display
|
|
57
54
|
content = (
|
|
58
|
-
<
|
|
55
|
+
<Container flexDirection="column" gap={1}>
|
|
59
56
|
{result.message && (
|
|
60
|
-
<
|
|
61
|
-
{result.message}
|
|
62
|
-
</text>
|
|
57
|
+
<Label color={result.success ? "success" : "error"}>{result.message}</Label>
|
|
63
58
|
)}
|
|
64
59
|
{result.data !== undefined && result.data !== null && (
|
|
65
|
-
<
|
|
66
|
-
{JSON.stringify(result.data, null, 2)}
|
|
67
|
-
</text>
|
|
60
|
+
<Value>{JSON.stringify(result.data, null, 2)}</Value>
|
|
68
61
|
)}
|
|
69
|
-
</
|
|
62
|
+
</Container>
|
|
70
63
|
);
|
|
71
64
|
}
|
|
72
65
|
} else {
|
|
73
|
-
content =
|
|
74
|
-
<text fg={Theme.label}>No results yet...</text>
|
|
75
|
-
);
|
|
66
|
+
content = <Label color="mutedText">No results yet...</Label>;
|
|
76
67
|
}
|
|
77
68
|
|
|
78
69
|
return (
|
|
79
|
-
<
|
|
80
|
-
flexDirection="column"
|
|
81
|
-
border={true}
|
|
82
|
-
borderStyle="rounded"
|
|
83
|
-
borderColor={borderColor}
|
|
70
|
+
<Panel
|
|
84
71
|
title="Results"
|
|
72
|
+
focused={focused}
|
|
73
|
+
flex={1}
|
|
85
74
|
padding={1}
|
|
86
|
-
|
|
75
|
+
flexDirection="column"
|
|
87
76
|
>
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
77
|
+
<ScrollView axis="vertical" flex={1} focused={focused}>
|
|
78
|
+
<Container flexDirection="column">
|
|
79
|
+
{content}
|
|
80
|
+
</Container>
|
|
81
|
+
</ScrollView>
|
|
82
|
+
</Panel>
|
|
92
83
|
);
|
|
93
84
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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";
|
|
3
5
|
|
|
4
6
|
interface StatusBarProps {
|
|
5
7
|
/** Status message to display */
|
|
@@ -15,45 +17,28 @@ interface StatusBarProps {
|
|
|
15
17
|
/**
|
|
16
18
|
* Status bar showing current status, spinner, and keyboard shortcuts.
|
|
17
19
|
*/
|
|
18
|
-
export function StatusBar({
|
|
19
|
-
status,
|
|
20
|
-
isRunning = false,
|
|
20
|
+
export function StatusBar({
|
|
21
|
+
status,
|
|
22
|
+
isRunning = false,
|
|
21
23
|
showShortcuts = true,
|
|
22
|
-
shortcuts = "L Logs • C CLI • Tab Switch • Ctrl+Y Copy • Esc Back"
|
|
24
|
+
shortcuts = "L Logs • C CLI • Tab Switch • Ctrl+Y Copy • Esc Back",
|
|
23
25
|
}: StatusBarProps) {
|
|
24
|
-
const { frame } = useSpinner(isRunning);
|
|
25
|
-
const spinner = isRunning ? `${frame} ` : "";
|
|
26
|
-
|
|
27
26
|
return (
|
|
28
|
-
<
|
|
29
|
-
flexDirection="
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<text fg={isRunning ? "#4ade80" : Theme.statusText}>
|
|
45
|
-
{isRunning ? <strong>{spinner}{status}</strong> : <>{spinner}{status}</>}
|
|
46
|
-
</text>
|
|
47
|
-
</box>
|
|
48
|
-
|
|
49
|
-
{/* Keyboard shortcuts */}
|
|
50
|
-
{showShortcuts && (
|
|
51
|
-
<box paddingLeft={1} paddingRight={1}>
|
|
52
|
-
<text fg={Theme.label}>
|
|
53
|
-
{shortcuts}
|
|
54
|
-
</text>
|
|
55
|
-
</box>
|
|
56
|
-
)}
|
|
57
|
-
</box>
|
|
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>
|
|
58
43
|
);
|
|
59
44
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
}
|