@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
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import type { PanelProps } from "../../../semantic/types.ts";
|
|
3
|
+
|
|
4
|
+
export function Panel({ title, children }: PanelProps) {
|
|
5
|
+
return (
|
|
6
|
+
<>
|
|
7
|
+
{title ? (
|
|
8
|
+
<Text bold>
|
|
9
|
+
{title}
|
|
10
|
+
</Text>
|
|
11
|
+
) : null}
|
|
12
|
+
{children}
|
|
13
|
+
</>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import InkSelectInput from "ink-select-input";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import { useMemo } from "react";
|
|
4
|
+
import type { SelectProps } from "../../../semantic/types.ts";
|
|
5
|
+
|
|
6
|
+
type Item = { label: string; value: string };
|
|
7
|
+
|
|
8
|
+
type ItemComponentProps = {
|
|
9
|
+
isSelected?: boolean;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
function ItemComponent({ label }: ItemComponentProps) {
|
|
14
|
+
// ink-select-input already provides its own selection marker.
|
|
15
|
+
// Keep this as plain text to avoid double-marking.
|
|
16
|
+
return <Text>{label}</Text>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Select({ options, value, focused, onChange, onSubmit }: SelectProps) {
|
|
20
|
+
const items = useMemo(
|
|
21
|
+
() => options.map((o) => ({ label: o.label, value: o.value }) as Item),
|
|
22
|
+
[options]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const initialIndex = Math.max(
|
|
26
|
+
0,
|
|
27
|
+
options.findIndex((o) => o.value === value)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Force remount so ink-select-input respects updated initialIndex.
|
|
31
|
+
const key = `${value}:${options.length}`;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<InkSelectInput
|
|
35
|
+
key={key}
|
|
36
|
+
items={items}
|
|
37
|
+
isFocused={focused}
|
|
38
|
+
initialIndex={initialIndex}
|
|
39
|
+
itemComponent={ItemComponent}
|
|
40
|
+
onHighlight={(item) => onChange(item.value)}
|
|
41
|
+
onSelect={() => onSubmit?.()}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import InkTextInput from "ink-text-input";
|
|
2
|
+
import { Text } from "ink";
|
|
3
|
+
import type { TextInputProps } from "../../../semantic/types.ts";
|
|
4
|
+
|
|
5
|
+
export function TextInput({ value, placeholder, focused, onChange, onSubmit }: TextInputProps) {
|
|
6
|
+
// ink-text-input renders nothing if you pass empty placeholder; provide a minimal hint.
|
|
7
|
+
const hint = placeholder ?? "";
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Text>
|
|
11
|
+
<InkTextInput
|
|
12
|
+
value={value}
|
|
13
|
+
placeholder={hint}
|
|
14
|
+
focus={focused}
|
|
15
|
+
onChange={onChange}
|
|
16
|
+
onSubmit={() => {
|
|
17
|
+
onSubmit?.();
|
|
18
|
+
}}
|
|
19
|
+
/>
|
|
20
|
+
</Text>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { useInput, type Key } from "ink";
|
|
2
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
3
|
+
import type { KeyboardAdapter, KeyboardEvent, KeyHandler } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
function normalizeKeyName(input: string, key: Key): KeyboardEvent {
|
|
6
|
+
const event: KeyboardEvent = {
|
|
7
|
+
name: input,
|
|
8
|
+
sequence: input,
|
|
9
|
+
ctrl: Boolean(key.ctrl),
|
|
10
|
+
shift: Boolean(key.shift),
|
|
11
|
+
meta: Boolean(key.meta),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
if (key.return) {
|
|
15
|
+
event.name = "return";
|
|
16
|
+
} else if (key.escape) {
|
|
17
|
+
event.name = "escape";
|
|
18
|
+
} else if (key.backspace) {
|
|
19
|
+
event.name = "backspace";
|
|
20
|
+
} else if (key.delete) {
|
|
21
|
+
// Terminals often send escape sequences for “Delete” that some libraries
|
|
22
|
+
// expose as `delete`, others as `del`. Keep it normalized.
|
|
23
|
+
event.name = "delete";
|
|
24
|
+
} else if (key.tab) {
|
|
25
|
+
event.name = "tab";
|
|
26
|
+
} else if (key.upArrow) {
|
|
27
|
+
event.name = "up";
|
|
28
|
+
} else if (key.downArrow) {
|
|
29
|
+
event.name = "down";
|
|
30
|
+
} else if (key.leftArrow) {
|
|
31
|
+
event.name = "left";
|
|
32
|
+
} else if (key.rightArrow) {
|
|
33
|
+
event.name = "right";
|
|
34
|
+
} else if (key.pageUp) {
|
|
35
|
+
event.name = "pageup";
|
|
36
|
+
} else if (key.pageDown) {
|
|
37
|
+
event.name = "pagedown";
|
|
38
|
+
} else if (key.home) {
|
|
39
|
+
event.name = "home";
|
|
40
|
+
} else if (key.end) {
|
|
41
|
+
event.name = "end";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Normalize enter -> return (some code checks either)
|
|
45
|
+
if (event.name === "enter") {
|
|
46
|
+
event.name = "return";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return event;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useInkKeyboardAdapter(): KeyboardAdapter {
|
|
53
|
+
const handlerStackRef = useRef<{ id: string; handler: KeyHandler }[]>([]);
|
|
54
|
+
const globalHandlerRef = useRef<KeyHandler | null>(null);
|
|
55
|
+
|
|
56
|
+
const setActiveHandler = useCallback((id: string, handler: KeyHandler) => {
|
|
57
|
+
handlerStackRef.current = handlerStackRef.current.filter((h) => h.id !== id);
|
|
58
|
+
handlerStackRef.current.push({ id, handler });
|
|
59
|
+
|
|
60
|
+
return () => {
|
|
61
|
+
handlerStackRef.current = handlerStackRef.current.filter((h) => h.id !== id);
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const setGlobalHandler = useCallback((handler: KeyHandler) => {
|
|
66
|
+
const previous = globalHandlerRef.current;
|
|
67
|
+
globalHandlerRef.current = handler;
|
|
68
|
+
|
|
69
|
+
return () => {
|
|
70
|
+
globalHandlerRef.current = previous;
|
|
71
|
+
};
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
useInput((input, key) => {
|
|
75
|
+
const event = normalizeKeyName(input, key);
|
|
76
|
+
|
|
77
|
+
if (globalHandlerRef.current) {
|
|
78
|
+
const handled = globalHandlerRef.current(event);
|
|
79
|
+
if (handled) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const activeHandler = handlerStackRef.current[handlerStackRef.current.length - 1];
|
|
85
|
+
if (activeHandler) {
|
|
86
|
+
activeHandler.handler(event);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return useMemo(
|
|
91
|
+
() => ({
|
|
92
|
+
setActiveHandler,
|
|
93
|
+
setGlobalHandler,
|
|
94
|
+
}),
|
|
95
|
+
[setActiveHandler, setGlobalHandler]
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function toPlainText(node: unknown): string {
|
|
2
|
+
if (node === null || node === undefined || typeof node === "boolean") {
|
|
3
|
+
return "";
|
|
4
|
+
}
|
|
5
|
+
if (typeof node === "string" || typeof node === "number") {
|
|
6
|
+
return String(node);
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(node)) {
|
|
9
|
+
return node.map(toPlainText).join("");
|
|
10
|
+
}
|
|
11
|
+
if (typeof node === "object" && node && "props" in node) {
|
|
12
|
+
const anyNode = node as any;
|
|
13
|
+
return toPlainText(anyNode.props?.children);
|
|
14
|
+
}
|
|
15
|
+
return "";
|
|
16
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createCliRenderer, type CliRenderer } from "@opentui/core";
|
|
2
|
+
import { createRoot, type Root } from "@opentui/react";
|
|
3
|
+
import { useLayoutEffect, type ReactNode } from "react";
|
|
4
|
+
import { SemanticColors } from "../../theme.ts";
|
|
5
|
+
import type { Renderer, RendererConfig } from "../types.ts";
|
|
6
|
+
import { useOpenTuiKeyboardAdapter } from "./keyboard.ts";
|
|
7
|
+
import { Button } from "./components/Button.tsx";
|
|
8
|
+
import { Code } from "./components/Code.tsx";
|
|
9
|
+
import { CodeHighlight } from "./components/CodeHighlight.tsx";
|
|
10
|
+
import { Container } from "./components/Container.tsx";
|
|
11
|
+
import { Field } from "./components/Field.tsx";
|
|
12
|
+
import { Label } from "./components/Label.tsx";
|
|
13
|
+
import { MenuButton } from "./components/MenuButton.tsx";
|
|
14
|
+
import { MenuItem } from "./components/MenuItem.tsx";
|
|
15
|
+
import { Overlay } from "./components/Overlay.tsx";
|
|
16
|
+
import { Spacer } from "./components/Spacer.tsx";
|
|
17
|
+
import { Spinner } from "./components/Spinner.tsx";
|
|
18
|
+
import { Panel } from "./components/Panel.tsx";
|
|
19
|
+
import { ScrollView as OpenTuiScrollView } from "./components/ScrollView.tsx";
|
|
20
|
+
import { Select } from "./components/Select.tsx";
|
|
21
|
+
import { TextInput } from "./components/TextInput.tsx";
|
|
22
|
+
import { Value } from "./components/Value.tsx";
|
|
23
|
+
|
|
24
|
+
export class OpenTuiRenderer implements Renderer {
|
|
25
|
+
private renderer: CliRenderer | null = null;
|
|
26
|
+
private root: Root | null = null;
|
|
27
|
+
|
|
28
|
+
private activeKeyboardAdapter: Renderer["keyboard"] | null = null;
|
|
29
|
+
|
|
30
|
+
public keyboard: Renderer["keyboard"] = {
|
|
31
|
+
setActiveHandler: (id, handler) => {
|
|
32
|
+
return this.activeKeyboardAdapter?.setActiveHandler(id, handler) ?? (() => {});
|
|
33
|
+
},
|
|
34
|
+
setGlobalHandler: (handler) => {
|
|
35
|
+
return this.activeKeyboardAdapter?.setGlobalHandler(handler) ?? (() => {});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
public components: Renderer["components"] = {
|
|
40
|
+
Field,
|
|
41
|
+
Button,
|
|
42
|
+
MenuButton,
|
|
43
|
+
MenuItem,
|
|
44
|
+
Container,
|
|
45
|
+
Panel,
|
|
46
|
+
ScrollView: OpenTuiScrollView,
|
|
47
|
+
|
|
48
|
+
Overlay,
|
|
49
|
+
Spacer,
|
|
50
|
+
Spinner,
|
|
51
|
+
Label,
|
|
52
|
+
Value,
|
|
53
|
+
Code,
|
|
54
|
+
CodeHighlight,
|
|
55
|
+
|
|
56
|
+
Select,
|
|
57
|
+
TextInput,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
constructor(private readonly config: RendererConfig) {}
|
|
61
|
+
|
|
62
|
+
async initialize(): Promise<void> {
|
|
63
|
+
this.renderer = await createCliRenderer({
|
|
64
|
+
useAlternateScreen: this.config.useAlternateScreen ?? true,
|
|
65
|
+
useConsole: false,
|
|
66
|
+
exitOnCtrlC: true,
|
|
67
|
+
backgroundColor: SemanticColors.background,
|
|
68
|
+
useMouse: true,
|
|
69
|
+
enableMouseMovement: true,
|
|
70
|
+
openConsoleOnError: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.root = createRoot(this.renderer);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
render(node: ReactNode): void {
|
|
77
|
+
if (!this.root) {
|
|
78
|
+
throw new Error("OpenTuiRenderer not initialized");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.root.render(
|
|
82
|
+
<KeyboardBridge
|
|
83
|
+
onReady={(keyboard) => {
|
|
84
|
+
this.activeKeyboardAdapter = keyboard;
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{node}
|
|
88
|
+
</KeyboardBridge>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
this.renderer?.start();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
destroy(): void {
|
|
95
|
+
this.renderer?.destroy();
|
|
96
|
+
this.renderer = null;
|
|
97
|
+
this.root = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function KeyboardBridge({
|
|
102
|
+
children,
|
|
103
|
+
onReady,
|
|
104
|
+
}: {
|
|
105
|
+
children: ReactNode;
|
|
106
|
+
onReady: (keyboard: ReturnType<typeof useOpenTuiKeyboardAdapter>) => void;
|
|
107
|
+
}) {
|
|
108
|
+
const keyboard = useOpenTuiKeyboardAdapter();
|
|
109
|
+
|
|
110
|
+
useLayoutEffect(() => {
|
|
111
|
+
onReady(keyboard);
|
|
112
|
+
}, [onReady, keyboard]);
|
|
113
|
+
|
|
114
|
+
return <>{children}</>;
|
|
115
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ButtonProps } from "../../../semantic/types.ts";
|
|
2
|
+
import { SemanticColors } from "../../../theme.ts";
|
|
3
|
+
|
|
4
|
+
export function Button({ label, selected, onActivate }: ButtonProps) {
|
|
5
|
+
const fg = selected ? SemanticColors.selectionText : SemanticColors.text;
|
|
6
|
+
const bg = selected ? SemanticColors.selectionBackground : undefined;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<text fg={fg} bg={bg} {...({ onClick: onActivate })}>
|
|
10
|
+
{label}
|
|
11
|
+
</text>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CodeHighlightProps, CodeTokenType } from "../../../semantic/types.ts";
|
|
2
|
+
import { SemanticColors } from "../../../theme.ts";
|
|
3
|
+
|
|
4
|
+
const TOKEN_COLORS: Record<CodeTokenType, string> = {
|
|
5
|
+
key: SemanticColors.primary,
|
|
6
|
+
string: SemanticColors.success,
|
|
7
|
+
number: "#d19a66",
|
|
8
|
+
boolean: "#c678dd",
|
|
9
|
+
null: "#c678dd",
|
|
10
|
+
punctuation: SemanticColors.mutedText,
|
|
11
|
+
unknown: SemanticColors.text,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function CodeHighlight({ tokens }: CodeHighlightProps) {
|
|
15
|
+
return (
|
|
16
|
+
<text>
|
|
17
|
+
{tokens.map((token, tokenIdx) => (
|
|
18
|
+
<span key={tokenIdx} fg={TOKEN_COLORS[token.type] ?? SemanticColors.text}>
|
|
19
|
+
{token.value}
|
|
20
|
+
</span>
|
|
21
|
+
))}
|
|
22
|
+
</text>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FieldProps } from "../../../semantic/types.ts";
|
|
2
|
+
import { SemanticColors } from "../../../theme.ts";
|
|
3
|
+
|
|
4
|
+
export function Field({ label, value, selected, onActivate }: FieldProps) {
|
|
5
|
+
const prefix = selected ? "► " : " ";
|
|
6
|
+
const labelColor = selected ? SemanticColors.focusBorder : SemanticColors.mutedText;
|
|
7
|
+
const valueColor = selected ? SemanticColors.value : SemanticColors.text;
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<box flexDirection="row" gap={1} {...({ onClick: onActivate })}>
|
|
11
|
+
<text fg={labelColor}>
|
|
12
|
+
{prefix}
|
|
13
|
+
{label}:
|
|
14
|
+
</text>
|
|
15
|
+
<text fg={valueColor}>{value}</text>
|
|
16
|
+
</box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { LabelProps } from "../../../semantic/types.ts";
|
|
3
|
+
import { SemanticColors } from "../../../theme.ts";
|
|
4
|
+
|
|
5
|
+
export function Label({ color = "text", bold, italic, wrap, children }: LabelProps & { children: ReactNode }) {
|
|
6
|
+
const fg = SemanticColors[color] ?? SemanticColors.text;
|
|
7
|
+
|
|
8
|
+
const content = bold ? <strong>{children}</strong> : children;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<text fg={fg} {...({ wrap } as any)}>
|
|
12
|
+
{italic ? <em>{content}</em> : content}
|
|
13
|
+
</text>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { MenuButtonProps } from "../../../semantic/types.ts";
|
|
2
|
+
import { MenuItem } from "./MenuItem.tsx";
|
|
3
|
+
|
|
4
|
+
export function MenuButton({ label, selected, onActivate }: MenuButtonProps) {
|
|
5
|
+
return (
|
|
6
|
+
<box marginTop={1}>
|
|
7
|
+
<MenuItem
|
|
8
|
+
label={`[ ${label} ]`}
|
|
9
|
+
selected={selected}
|
|
10
|
+
onActivate={onActivate}
|
|
11
|
+
/>
|
|
12
|
+
</box>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MenuItemProps } from "../../../semantic/types.ts";
|
|
2
|
+
import { SemanticColors } from "../../../theme.ts";
|
|
3
|
+
|
|
4
|
+
export function MenuItem({
|
|
5
|
+
label,
|
|
6
|
+
description,
|
|
7
|
+
suffix,
|
|
8
|
+
selected,
|
|
9
|
+
onActivate,
|
|
10
|
+
}: MenuItemProps) {
|
|
11
|
+
const prefix = selected ? "► " : " ";
|
|
12
|
+
const displayLabel = suffix ? `${label} ${suffix}` : label;
|
|
13
|
+
|
|
14
|
+
const fg = selected ? SemanticColors.selectionText : SemanticColors.text;
|
|
15
|
+
const bg = selected ? SemanticColors.selectionBackground : undefined;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<box flexDirection="column">
|
|
19
|
+
<text fg={fg} bg={bg} {...({ onClick: onActivate })}>
|
|
20
|
+
{prefix}{displayLabel}
|
|
21
|
+
</text>
|
|
22
|
+
{description ? (
|
|
23
|
+
<text fg={selected ? SemanticColors.text : SemanticColors.mutedText}>
|
|
24
|
+
{" "}{description}
|
|
25
|
+
</text>
|
|
26
|
+
) : null}
|
|
27
|
+
</box>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { OverlayProps } from "../../../semantic/types.ts";
|
|
3
|
+
|
|
4
|
+
export function Overlay({
|
|
5
|
+
zIndex = 10,
|
|
6
|
+
top,
|
|
7
|
+
left,
|
|
8
|
+
right,
|
|
9
|
+
bottom,
|
|
10
|
+
width,
|
|
11
|
+
height,
|
|
12
|
+
children,
|
|
13
|
+
}: OverlayProps & { children?: ReactNode }) {
|
|
14
|
+
return (
|
|
15
|
+
<box position="absolute" top={0} left={0} right={0} bottom={0} zIndex={zIndex}>
|
|
16
|
+
<box position="absolute" top={top as any} left={left as any} right={right as any} bottom={bottom as any} width={width as any} height={height as any}>
|
|
17
|
+
{children}
|
|
18
|
+
</box>
|
|
19
|
+
</box>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { PanelProps, Spacing } from "../../../semantic/types.ts";
|
|
3
|
+
import { SemanticColors } from "../../../theme.ts";
|
|
4
|
+
|
|
5
|
+
function normalizePadding(
|
|
6
|
+
padding: number | Spacing | undefined,
|
|
7
|
+
opts: { dense: boolean }
|
|
8
|
+
): { padding?: number; paddingTop?: number; paddingRight?: number; paddingBottom?: number; paddingLeft?: number } {
|
|
9
|
+
if (padding === undefined) {
|
|
10
|
+
return opts.dense
|
|
11
|
+
? {
|
|
12
|
+
padding: 0,
|
|
13
|
+
paddingLeft: 1,
|
|
14
|
+
paddingRight: 1,
|
|
15
|
+
}
|
|
16
|
+
: { padding: 1 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof padding === "number") {
|
|
20
|
+
return { padding };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
paddingTop: padding.top ?? 0,
|
|
25
|
+
paddingRight: padding.right ?? 0,
|
|
26
|
+
paddingBottom: padding.bottom ?? 0,
|
|
27
|
+
paddingLeft: padding.left ?? 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Panel({
|
|
32
|
+
title,
|
|
33
|
+
focused,
|
|
34
|
+
border = true,
|
|
35
|
+
surface = "panel",
|
|
36
|
+
dense = false,
|
|
37
|
+
children,
|
|
38
|
+
flex,
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
flexDirection,
|
|
42
|
+
alignItems,
|
|
43
|
+
justifyContent,
|
|
44
|
+
gap,
|
|
45
|
+
padding,
|
|
46
|
+
noShrink,
|
|
47
|
+
}: PanelProps & { children?: ReactNode }) {
|
|
48
|
+
const backgroundColor = surface === "overlay" ? SemanticColors.overlay : SemanticColors.panelBackground;
|
|
49
|
+
|
|
50
|
+
const borderColor = surface === "overlay" ? SemanticColors.warning : focused ? SemanticColors.focusBorder : SemanticColors.border;
|
|
51
|
+
|
|
52
|
+
const resolvedPadding = normalizePadding(padding, { dense });
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<box
|
|
56
|
+
border={border}
|
|
57
|
+
borderStyle={border ? "rounded" : undefined}
|
|
58
|
+
borderColor={borderColor}
|
|
59
|
+
title={title}
|
|
60
|
+
padding={resolvedPadding.padding}
|
|
61
|
+
paddingTop={resolvedPadding.paddingTop}
|
|
62
|
+
paddingRight={resolvedPadding.paddingRight}
|
|
63
|
+
paddingBottom={resolvedPadding.paddingBottom}
|
|
64
|
+
paddingLeft={resolvedPadding.paddingLeft}
|
|
65
|
+
flexGrow={flex}
|
|
66
|
+
flexShrink={noShrink ? 0 : flex === undefined ? undefined : 1}
|
|
67
|
+
width={width as any}
|
|
68
|
+
height={height as any}
|
|
69
|
+
flexDirection={flexDirection as any}
|
|
70
|
+
alignItems={alignItems as any}
|
|
71
|
+
justifyContent={justifyContent as any}
|
|
72
|
+
gap={gap}
|
|
73
|
+
backgroundColor={backgroundColor}
|
|
74
|
+
>
|
|
75
|
+
{children}
|
|
76
|
+
</box>
|
|
77
|
+
);
|
|
78
|
+
}
|