@pablozaiden/terminatui 0.3.0 → 0.4.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/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
package/package.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { Glob } from "bun";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const adaptersDir = fileURLToPath(new URL("../tui/adapters", import.meta.url));
|
|
6
|
+
|
|
7
|
+
async function getAdapterSourceFiles(): Promise<string[]> {
|
|
8
|
+
const glob = new Glob("**/*.{ts,tsx}");
|
|
9
|
+
const files: string[] = [];
|
|
10
|
+
for await (const file of glob.scan({ cwd: adaptersDir, absolute: true })) {
|
|
11
|
+
// skip shared behavior (allowed)
|
|
12
|
+
if (!file.includes("/shared/")) {
|
|
13
|
+
files.push(file);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return files;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("Adapters must not import src/tui/components/*", async () => {
|
|
20
|
+
const files = await getAdapterSourceFiles();
|
|
21
|
+
expect(files.length).toBeGreaterThan(0);
|
|
22
|
+
|
|
23
|
+
const forbidden = /from\s+['"][^'"]*tui\/components\//;
|
|
24
|
+
|
|
25
|
+
for (const filePath of files) {
|
|
26
|
+
const source = await Bun.file(filePath).text();
|
|
27
|
+
const match = source.match(forbidden);
|
|
28
|
+
if (match) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Adapter file imports shared component:\n File: ${filePath}\n Match: ${match[0]}`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
});
|
|
@@ -21,8 +21,6 @@ describe("schemaToFields", () => {
|
|
|
21
21
|
label: "Repository",
|
|
22
22
|
},
|
|
23
23
|
hidden: { type: "string", description: "Hidden", tuiHidden: true },
|
|
24
|
-
path: { type: "string", description: "Path", placeholder: "Enter path here" },
|
|
25
|
-
grouped: { type: "string", description: "Grouped", group: "Basic" },
|
|
26
24
|
};
|
|
27
25
|
|
|
28
26
|
const fields = schemaToFieldConfigs(schema);
|
|
@@ -42,8 +40,6 @@ describe("schemaToFields", () => {
|
|
|
42
40
|
// Decorations
|
|
43
41
|
expect(fields.find((f) => f.key === "repoPath")?.label).toBe("Repository");
|
|
44
42
|
expect(fields.some((f) => f.key === "hidden")).toBe(false);
|
|
45
|
-
expect(fields.find((f) => f.key === "path")?.placeholder).toBe("Enter path here");
|
|
46
|
-
expect(fields.find((f) => f.key === "grouped")?.group).toBe("Basic");
|
|
47
43
|
});
|
|
48
44
|
|
|
49
45
|
test("sorts by order", () => {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
const tuiRootPath = new URL("../tui/TuiRoot.tsx", import.meta.url);
|
|
4
|
+
|
|
5
|
+
async function readTuiRootSource() {
|
|
6
|
+
return await Bun.file(tuiRootPath).text();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
test("TuiRoot stays a thin host (no domain helper imports)", async () => {
|
|
10
|
+
const source = await readTuiRootSource();
|
|
11
|
+
|
|
12
|
+
const forbiddenFragments = [
|
|
13
|
+
"buildCliCommand",
|
|
14
|
+
"schemaToFieldConfigs",
|
|
15
|
+
"schemaToFields",
|
|
16
|
+
"initializeConfigValues",
|
|
17
|
+
"loadPersistedParameters",
|
|
18
|
+
"savePersistedParameters",
|
|
19
|
+
"getCommandsAtPath",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
for (const fragment of forbiddenFragments) {
|
|
23
|
+
expect(source).not.toContain(fragment);
|
|
24
|
+
}
|
|
25
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -15,8 +15,8 @@ export * from "./core/registry.ts";
|
|
|
15
15
|
|
|
16
16
|
export * from "./tui/TuiApplication.tsx";
|
|
17
17
|
export * from "./tui/TuiRoot.tsx";
|
|
18
|
-
export * from "./tui/registry.ts";
|
|
19
18
|
export * from "./tui/theme.ts";
|
|
20
19
|
export * from "./types/command.ts";
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
// Kept for external consumers.
|
|
22
|
+
export * from "./tui/components/JsonHighlight.tsx";
|
|
@@ -8,7 +8,6 @@ import { createSettingsCommand } from "../builtins/settings.ts";
|
|
|
8
8
|
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
9
9
|
import { loadPersistedParameters } from "./utils/parameterPersistence.ts";
|
|
10
10
|
import { AppContext } from "../core/context.ts";
|
|
11
|
-
import { registerAllModals, registerAllScreens } from "./registry.ts";
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Extended configuration for TUI-enabled applications.
|
|
@@ -87,9 +86,6 @@ export class TuiApplication extends Application {
|
|
|
87
86
|
* Launch the TUI.
|
|
88
87
|
*/
|
|
89
88
|
async runTui(rendererType: TuiModeOptions): Promise<void> {
|
|
90
|
-
await registerAllScreens();
|
|
91
|
-
await registerAllModals();
|
|
92
|
-
|
|
93
89
|
// Get all commands that support TUI or have options
|
|
94
90
|
const commands = this.getExecutableCommands();
|
|
95
91
|
|
package/src/tui/TuiRoot.tsx
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
import type { AnyCommand } from "../core/command.ts";
|
|
2
|
-
import {
|
|
3
|
-
import { KeyboardProvider } from "./context/KeyboardContext.tsx";
|
|
4
|
-
import { useGlobalKeyHandler } from "./hooks/useGlobalKeyHandler.ts";
|
|
2
|
+
import { useState } from "react";
|
|
5
3
|
import { LogsProvider } from "./context/LogsContext.tsx";
|
|
6
4
|
import { NavigationProvider, useNavigation } from "./context/NavigationContext.tsx";
|
|
7
|
-
import { ClipboardProviderComponent, useClipboardContext } from "./context/ClipboardContext.tsx";
|
|
8
5
|
import { TuiAppContextProvider, useTuiApp } from "./context/TuiAppContext.tsx";
|
|
9
|
-
import { ExecutorProvider
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
import { CommandSelectScreen, type CommandSelectParams } from "./screens/CommandSelectScreen.tsx";
|
|
6
|
+
import { ExecutorProvider } from "./context/ExecutorContext.tsx";
|
|
7
|
+
import { ActionProvider, useAction } from "./context/ActionContext.tsx";
|
|
8
|
+
import { useRenderer } from "./context/RendererContext.tsx";
|
|
9
|
+
|
|
10
|
+
import { TuiDriverProvider, useTuiDriver } from "./driver/context/TuiDriverContext.tsx";
|
|
11
|
+
|
|
16
12
|
|
|
17
13
|
interface TuiRootProps {
|
|
18
14
|
name: string;
|
|
@@ -24,28 +20,26 @@ interface TuiRootProps {
|
|
|
24
20
|
|
|
25
21
|
export function TuiRoot({ name, displayName, version, commands, onExit }: TuiRootProps) {
|
|
26
22
|
return (
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
>
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
</ClipboardProviderComponent>
|
|
48
|
-
</KeyboardProvider>
|
|
23
|
+
<TuiAppContextProvider
|
|
24
|
+
name={name}
|
|
25
|
+
displayName={displayName}
|
|
26
|
+
version={version}
|
|
27
|
+
commands={commands}
|
|
28
|
+
onExit={onExit}
|
|
29
|
+
>
|
|
30
|
+
<LogsProvider>
|
|
31
|
+
<ExecutorProvider>
|
|
32
|
+
<NavigationProvider<{ commandPath: string[] }>
|
|
33
|
+
initialScreen={{ route: "commandBrowser", params: { commandPath: [] as string[] } }}
|
|
34
|
+
onExit={onExit}
|
|
35
|
+
>
|
|
36
|
+
<TuiDriverProvider appName={name} commands={commands}>
|
|
37
|
+
<TuiRootContent />
|
|
38
|
+
</TuiDriverProvider>
|
|
39
|
+
</NavigationProvider>
|
|
40
|
+
</ExecutorProvider>
|
|
41
|
+
</LogsProvider>
|
|
42
|
+
</TuiAppContextProvider>
|
|
49
43
|
);
|
|
50
44
|
}
|
|
51
45
|
|
|
@@ -55,81 +49,43 @@ export function TuiRoot({ name, displayName, version, commands, onExit }: TuiRoo
|
|
|
55
49
|
*/
|
|
56
50
|
function TuiRootContent() {
|
|
57
51
|
const { displayName, name, version } = useTuiApp();
|
|
52
|
+
const driver = useTuiDriver();
|
|
58
53
|
const navigation = useNavigation();
|
|
59
|
-
const
|
|
60
|
-
const clipboard = useClipboardContext();
|
|
61
|
-
const { copyWithMessage, lastAction } = useClipboard();
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
const [copyToast, setCopyToast] = useState<string | null>(null);
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return true;
|
|
80
|
-
}
|
|
56
|
+
return (
|
|
57
|
+
<ActionProvider navigation={navigation}>
|
|
58
|
+
<TuiRootKeyboardHandler onCopyToastChange={setCopyToast} />
|
|
59
|
+
{driver.renderAppShell({
|
|
60
|
+
app: {
|
|
61
|
+
name,
|
|
62
|
+
displayName,
|
|
63
|
+
version,
|
|
64
|
+
},
|
|
65
|
+
copyToast,
|
|
66
|
+
})}
|
|
67
|
+
</ActionProvider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
81
70
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Renders the adapter-specific keyboard handler component.
|
|
73
|
+
* This component uses hooks properly since it's rendered as a React component.
|
|
74
|
+
*/
|
|
75
|
+
function TuiRootKeyboardHandler({ onCopyToastChange }: { onCopyToastChange: (toast: string | null) => void }) {
|
|
76
|
+
const renderer = useRenderer();
|
|
77
|
+
const { dispatchAction } = useAction();
|
|
89
78
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
if (!renderer.renderKeyboardHandler) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
93
82
|
|
|
94
|
-
|
|
83
|
+
return renderer.renderKeyboardHandler({
|
|
84
|
+
dispatchAction,
|
|
85
|
+
getScreenKeyHandler: () => null,
|
|
86
|
+
onCopyToastChange,
|
|
95
87
|
});
|
|
88
|
+
}
|
|
96
89
|
|
|
97
|
-
// Get current screen component from registry
|
|
98
|
-
const ScreenComponent = getScreen(navigation.current.route);
|
|
99
|
-
|
|
100
|
-
// Get breadcrumb from current screen params (if available)
|
|
101
|
-
const params = navigation.current.params as { commandPath?: string[] } | undefined;
|
|
102
|
-
const breadcrumb = params?.commandPath;
|
|
103
|
-
|
|
104
|
-
return (
|
|
105
|
-
<Panel flexDirection="column" flex={1} padding={1} border={false}>
|
|
106
|
-
<Container flexDirection="column" flex={1}>
|
|
107
|
-
<Header name={displayName ?? name} version={version} breadcrumb={breadcrumb} />
|
|
108
|
-
|
|
109
|
-
<Container flexDirection="column" flex={1}>
|
|
110
|
-
{ScreenComponent ? <ScreenComponent /> : null}
|
|
111
|
-
</Container>
|
|
112
|
-
|
|
113
|
-
<StatusBar
|
|
114
|
-
status={lastAction || (executor.isExecuting ? "Executing..." : "Ready")}
|
|
115
|
-
isRunning={executor.isExecuting}
|
|
116
|
-
shortcuts="Esc Back • Ctrl+Y Copy • Ctrl+L Logs"
|
|
117
|
-
/>
|
|
118
90
|
|
|
119
|
-
{/* Render modals from registry */}
|
|
120
|
-
{navigation.modalStack.map((modal, idx) => {
|
|
121
|
-
const ModalComponent = getModal(modal.id);
|
|
122
|
-
if (!ModalComponent) return null;
|
|
123
91
|
|
|
124
|
-
return (
|
|
125
|
-
<ModalComponent
|
|
126
|
-
key={`modal-${modal.id}-${idx}`}
|
|
127
|
-
params={modal.params}
|
|
128
|
-
onClose={() => navigation.closeModal()}
|
|
129
|
-
/>
|
|
130
|
-
);
|
|
131
|
-
})}
|
|
132
|
-
</Container>
|
|
133
|
-
</Panel>
|
|
134
|
-
);
|
|
135
|
-
}
|
|
@@ -1,35 +1,183 @@
|
|
|
1
1
|
import { render } from "ink";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
|
-
import { useLayoutEffect } from "react";
|
|
3
|
+
import { useEffect, useLayoutEffect } from "react";
|
|
4
4
|
|
|
5
|
-
import type { Renderer, RendererConfig } from "../types.ts";
|
|
5
|
+
import type { KeyboardEvent, Renderer, RendererConfig } from "../types.ts";
|
|
6
|
+
import { SemanticInkRenderer } from "./SemanticInkRenderer.tsx";
|
|
6
7
|
import { useInkKeyboardAdapter } from "./keyboard.ts";
|
|
8
|
+
import { copyToTerminalClipboard } from "../shared/TerminalClipboard.ts";
|
|
9
|
+
import { useTuiDriver } from "../../driver/context/TuiDriverContext.tsx";
|
|
7
10
|
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
import type { TuiAction } from "../../actions.ts";
|
|
12
|
+
|
|
13
|
+
function InkKeyboardHandler({
|
|
14
|
+
dispatchAction,
|
|
15
|
+
getScreenKeyHandler,
|
|
16
|
+
keyboard,
|
|
17
|
+
onCopyToastChange,
|
|
18
|
+
}: {
|
|
19
|
+
dispatchAction: (action: TuiAction) => void;
|
|
20
|
+
getScreenKeyHandler: () => ((event: KeyboardEvent) => boolean) | null;
|
|
21
|
+
keyboard: Renderer["keyboard"];
|
|
22
|
+
onCopyToastChange?: (toast: string | null) => void;
|
|
23
|
+
}) {
|
|
24
|
+
const driver = useTuiDriver();
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const cleanup = keyboard.setGlobalHandler((event) => {
|
|
28
|
+
// Debug: log keyboard events
|
|
29
|
+
// console.log("Key event:", event.name, "screenHandler:", getScreenKeyHandler() ? "yes" : "no");
|
|
30
|
+
|
|
31
|
+
if (event.name === "escape") {
|
|
32
|
+
dispatchAction({ type: "nav.back" });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (event.ctrl && event.name === "y") {
|
|
37
|
+
const payload = driver.getActiveCopyPayload();
|
|
38
|
+
if (payload) {
|
|
39
|
+
// Show toast immediately for instant feedback
|
|
40
|
+
onCopyToastChange?.(`Copied ${payload.label}`);
|
|
41
|
+
void copyToTerminalClipboard(payload.content).then((success) => {
|
|
42
|
+
if (!success) {
|
|
43
|
+
onCopyToastChange?.("Copy failed");
|
|
44
|
+
}
|
|
45
|
+
setTimeout(() => onCopyToastChange?.(null), 1500);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (event.ctrl && event.name === "l") {
|
|
52
|
+
dispatchAction({ type: "logs.open" });
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const screenHandler = getScreenKeyHandler();
|
|
57
|
+
if (screenHandler) {
|
|
58
|
+
return screenHandler(event);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return false;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return cleanup;
|
|
65
|
+
}, [dispatchAction, getScreenKeyHandler, keyboard, driver, onCopyToastChange]);
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
24
69
|
|
|
25
70
|
export class InkRenderer implements Renderer {
|
|
71
|
+
|
|
72
|
+
private readonly semanticRenderer = new SemanticInkRenderer();
|
|
73
|
+
|
|
74
|
+
private semanticScreenKeyHandler: ((event: KeyboardEvent) => boolean) | null = null;
|
|
75
|
+
|
|
76
|
+
public renderSemanticAppShell: Renderer["renderSemanticAppShell"] = (props) => {
|
|
77
|
+
return this.semanticRenderer.renderAppShell(props);
|
|
78
|
+
};
|
|
79
|
+
public renderSemanticCommandBrowserScreen: Renderer["renderSemanticCommandBrowserScreen"] = (props) => {
|
|
80
|
+
this.semanticScreenKeyHandler = (event) => {
|
|
81
|
+
if (event.ctrl && event.name === "l") {
|
|
82
|
+
// Logs open is adapter-owned.
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (event.name === "up") {
|
|
87
|
+
props.onSelectCommand(props.selectedCommandIndex - 1);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (event.name === "down") {
|
|
92
|
+
props.onSelectCommand(props.selectedCommandIndex + 1);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (event.name === "return") {
|
|
97
|
+
props.onRunSelected();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return false;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return this.semanticRenderer.renderCommandBrowserScreen(props);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
public renderSemanticConfigScreen: Renderer["renderSemanticConfigScreen"] = (props) => {
|
|
108
|
+
this.semanticScreenKeyHandler = (event) => {
|
|
109
|
+
if (event.ctrl && event.name === "l") {
|
|
110
|
+
// Adapter-owned logs open.
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (event.name === "up") {
|
|
115
|
+
props.onSelectionChange(props.selectedFieldIndex - 1);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (event.name === "down") {
|
|
120
|
+
props.onSelectionChange(props.selectedFieldIndex + 1);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (event.name === "return") {
|
|
125
|
+
const fieldConfig = props.fieldConfigs[props.selectedFieldIndex];
|
|
126
|
+
if (fieldConfig) {
|
|
127
|
+
props.onEditField(fieldConfig.key);
|
|
128
|
+
} else {
|
|
129
|
+
props.onRun();
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return false;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return this.semanticRenderer.renderConfigScreen(props);
|
|
138
|
+
};
|
|
139
|
+
public renderSemanticRunningScreen: Renderer["renderSemanticRunningScreen"] = (props) => {
|
|
140
|
+
this.semanticScreenKeyHandler = null;
|
|
141
|
+
return this.semanticRenderer.renderRunningScreen(props);
|
|
142
|
+
};
|
|
143
|
+
public renderSemanticLogsScreen: Renderer["renderSemanticLogsScreen"] = (props) => {
|
|
144
|
+
this.semanticScreenKeyHandler = (event) => {
|
|
145
|
+
if (event.name === "return") {
|
|
146
|
+
props.onClose();
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return this.semanticRenderer.renderLogsScreen(props);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
public renderSemanticEditorScreen: Renderer["renderSemanticEditorScreen"] = (props) => {
|
|
156
|
+
this.semanticScreenKeyHandler = (event) => {
|
|
157
|
+
if (event.name === "return") {
|
|
158
|
+
props.onSubmit?.();
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return this.semanticRenderer.renderEditorScreen(props);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
public renderKeyboardHandler: Renderer["renderKeyboardHandler"] = ({ dispatchAction, onCopyToastChange }) => {
|
|
168
|
+
return (
|
|
169
|
+
<InkKeyboardHandlerWrapper
|
|
170
|
+
dispatchAction={dispatchAction}
|
|
171
|
+
getScreenKeyHandler={() => this.semanticScreenKeyHandler}
|
|
172
|
+
keyboard={this.keyboard}
|
|
173
|
+
onCopyToastChange={onCopyToastChange}
|
|
174
|
+
/>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
|
|
26
178
|
private instance: ReturnType<typeof render> | null = null;
|
|
27
179
|
private activeKeyboardAdapter: Renderer["keyboard"] | null = null;
|
|
28
180
|
|
|
29
|
-
public supportCustomRendering(): boolean {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
181
|
public keyboard: Renderer["keyboard"] = {
|
|
34
182
|
setActiveHandler: (id, handler) => {
|
|
35
183
|
return this.activeKeyboardAdapter?.setActiveHandler(id, handler) ?? (() => {});
|
|
@@ -39,29 +187,6 @@ export class InkRenderer implements Renderer {
|
|
|
39
187
|
},
|
|
40
188
|
};
|
|
41
189
|
|
|
42
|
-
public components: Renderer["components"] = {
|
|
43
|
-
Field,
|
|
44
|
-
Button,
|
|
45
|
-
MenuButton,
|
|
46
|
-
MenuItem,
|
|
47
|
-
|
|
48
|
-
Container,
|
|
49
|
-
Panel,
|
|
50
|
-
ScrollView,
|
|
51
|
-
|
|
52
|
-
Overlay,
|
|
53
|
-
Spacer,
|
|
54
|
-
Spinner,
|
|
55
|
-
|
|
56
|
-
Label,
|
|
57
|
-
Value,
|
|
58
|
-
Code,
|
|
59
|
-
CodeHighlight,
|
|
60
|
-
|
|
61
|
-
Select,
|
|
62
|
-
TextInput,
|
|
63
|
-
};
|
|
64
|
-
|
|
65
190
|
constructor(_config: RendererConfig) {}
|
|
66
191
|
|
|
67
192
|
async initialize(): Promise<void> {
|
|
@@ -137,3 +262,24 @@ function KeyboardBridge({
|
|
|
137
262
|
|
|
138
263
|
return node;
|
|
139
264
|
}
|
|
265
|
+
|
|
266
|
+
function InkKeyboardHandlerWrapper({
|
|
267
|
+
dispatchAction,
|
|
268
|
+
getScreenKeyHandler,
|
|
269
|
+
keyboard,
|
|
270
|
+
onCopyToastChange,
|
|
271
|
+
}: {
|
|
272
|
+
dispatchAction: (action: TuiAction) => void;
|
|
273
|
+
getScreenKeyHandler: () => ((event: KeyboardEvent) => boolean) | null;
|
|
274
|
+
keyboard: Renderer["keyboard"];
|
|
275
|
+
onCopyToastChange?: (toast: string | null) => void;
|
|
276
|
+
}) {
|
|
277
|
+
return (
|
|
278
|
+
<InkKeyboardHandler
|
|
279
|
+
dispatchAction={dispatchAction}
|
|
280
|
+
getScreenKeyHandler={getScreenKeyHandler}
|
|
281
|
+
keyboard={keyboard}
|
|
282
|
+
onCopyToastChange={onCopyToastChange}
|
|
283
|
+
/>
|
|
284
|
+
);
|
|
285
|
+
}
|