@pablozaiden/terminatui 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/package.json +1 -1
  2. package/src/__tests__/adapterNoSharedUi.test.ts +34 -0
  3. package/src/__tests__/schemaToFields.test.ts +0 -4
  4. package/src/__tests__/tuiRootNoCoupling.test.ts +25 -0
  5. package/src/index.ts +2 -2
  6. package/src/tui/TuiApplication.tsx +0 -4
  7. package/src/tui/TuiRoot.tsx +58 -102
  8. package/src/tui/actions.ts +4 -0
  9. package/src/tui/adapters/ink/InkRenderer.tsx +191 -45
  10. package/src/tui/adapters/ink/SemanticInkRenderer.tsx +210 -0
  11. package/src/tui/adapters/ink/components/Button.tsx +10 -2
  12. package/src/tui/adapters/ink/components/Overlay.tsx +8 -2
  13. package/src/tui/adapters/ink/components/Panel.tsx +26 -5
  14. package/src/tui/adapters/ink/components/ScrollView.tsx +44 -3
  15. package/src/tui/adapters/ink/components/Spinner.tsx +8 -2
  16. package/src/tui/adapters/ink/keyboard.ts +0 -3
  17. package/src/tui/adapters/ink/ui/CommandSelector.tsx +56 -0
  18. package/src/tui/adapters/ink/ui/ConfigForm.tsx +77 -0
  19. package/src/tui/adapters/ink/ui/Header.tsx +25 -0
  20. package/src/tui/adapters/ink/ui/JsonHighlight.tsx +21 -0
  21. package/src/tui/adapters/ink/ui/ResultsPanel.tsx +57 -0
  22. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +190 -43
  23. package/src/tui/adapters/opentui/SemanticOpenTuiRenderer.tsx +192 -0
  24. package/src/tui/adapters/opentui/components/Label.tsx +2 -2
  25. package/src/tui/adapters/opentui/components/Overlay.tsx +12 -3
  26. package/src/tui/adapters/opentui/components/Panel.tsx +11 -1
  27. package/src/tui/adapters/opentui/components/ScrollView.tsx +1 -8
  28. package/src/tui/adapters/opentui/components/Spinner.tsx +1 -1
  29. package/src/tui/adapters/opentui/keyboard.ts +0 -3
  30. package/src/tui/adapters/opentui/ui/CommandSelector.tsx +55 -0
  31. package/src/tui/adapters/opentui/ui/ConfigForm.tsx +74 -0
  32. package/src/tui/adapters/opentui/ui/Header.tsx +24 -0
  33. package/src/tui/adapters/opentui/ui/JsonHighlight.tsx +20 -0
  34. package/src/tui/adapters/opentui/ui/LogsPanel.tsx +44 -0
  35. package/src/tui/adapters/opentui/ui/ResultsPanel.tsx +62 -0
  36. package/src/tui/adapters/shared/TerminalClipboard.ts +65 -0
  37. package/src/tui/adapters/{opentui/hooks → shared}/useSpinner.ts +5 -1
  38. package/src/tui/adapters/types.ts +25 -46
  39. package/src/tui/components/JsonHighlight.tsx +41 -111
  40. package/src/tui/context/ActionContext.tsx +51 -0
  41. package/src/tui/context/ExecutorContext.tsx +7 -1
  42. package/src/tui/context/NavigationContext.tsx +20 -4
  43. package/src/tui/controllers/CommandBrowserController.tsx +100 -0
  44. package/src/tui/controllers/ConfigController.tsx +183 -0
  45. package/src/tui/controllers/EditorController.tsx +169 -0
  46. package/src/tui/controllers/LogsController.tsx +48 -0
  47. package/src/tui/controllers/OutcomeController.tsx +110 -0
  48. package/src/tui/driver/TuiDriver.tsx +148 -0
  49. package/src/tui/driver/context/TuiDriverContext.tsx +44 -0
  50. package/src/tui/driver/types.ts +72 -0
  51. package/src/tui/semantic/AppShell.tsx +30 -0
  52. package/src/tui/semantic/CommandBrowserScreen.tsx +16 -0
  53. package/src/tui/semantic/ConfigScreen.tsx +23 -0
  54. package/src/tui/semantic/EditorScreen.tsx +20 -0
  55. package/src/tui/semantic/LogsScreen.tsx +9 -0
  56. package/src/tui/semantic/RunningScreen.tsx +17 -0
  57. package/src/tui/semantic/layoutTypes.ts +72 -0
  58. package/src/tui/semantic/render.tsx +44 -0
  59. package/src/tui/semantic/types.ts +31 -98
  60. package/src/tui/utils/jsonTokenizer.ts +98 -0
  61. package/src/tui/utils/schemaToFields.ts +1 -25
  62. package/src/tui/adapters/ink/components/Code.tsx +0 -6
  63. package/src/tui/adapters/ink/components/Container.tsx +0 -5
  64. package/src/tui/adapters/ink/components/Spacer.tsx +0 -15
  65. package/src/tui/adapters/ink/components/Value.tsx +0 -7
  66. package/src/tui/adapters/opentui/components/Code.tsx +0 -12
  67. package/src/tui/adapters/opentui/components/Container.tsx +0 -56
  68. package/src/tui/adapters/opentui/components/Spacer.tsx +0 -5
  69. package/src/tui/adapters/opentui/components/Value.tsx +0 -13
  70. package/src/tui/components/ActionButton.tsx +0 -0
  71. package/src/tui/components/CommandSelector.tsx +0 -119
  72. package/src/tui/components/ConfigForm.tsx +0 -174
  73. package/src/tui/components/FieldRow.tsx +0 -0
  74. package/src/tui/components/Header.tsx +0 -32
  75. package/src/tui/components/ModalBase.tsx +0 -38
  76. package/src/tui/components/ResultsPanel.tsx +0 -84
  77. package/src/tui/components/StatusBar.tsx +0 -44
  78. package/src/tui/components/logColors.ts +0 -12
  79. package/src/tui/components/types.ts +0 -30
  80. package/src/tui/context/ClipboardContext.tsx +0 -87
  81. package/src/tui/context/KeyboardContext.tsx +0 -132
  82. package/src/tui/hooks/useActiveKeyHandler.ts +0 -75
  83. package/src/tui/hooks/useClipboard.ts +0 -81
  84. package/src/tui/hooks/useClipboardProvider.ts +0 -42
  85. package/src/tui/hooks/useGlobalKeyHandler.ts +0 -54
  86. package/src/tui/modals/CliModal.tsx +0 -82
  87. package/src/tui/modals/EditorModal.tsx +0 -207
  88. package/src/tui/modals/LogsModal.tsx +0 -98
  89. package/src/tui/registry.ts +0 -102
  90. package/src/tui/screens/CommandSelectScreen.tsx +0 -162
  91. package/src/tui/screens/ConfigScreen.tsx +0 -165
  92. package/src/tui/screens/ErrorScreen.tsx +0 -58
  93. package/src/tui/screens/ResultsScreen.tsx +0 -68
  94. package/src/tui/screens/RunningScreen.tsx +0 -72
  95. package/src/tui/screens/ScreenBase.ts +0 -6
  96. package/src/tui/semantic/Button.tsx +0 -7
  97. package/src/tui/semantic/Code.tsx +0 -7
  98. package/src/tui/semantic/CodeHighlight.tsx +0 -7
  99. package/src/tui/semantic/Container.tsx +0 -7
  100. package/src/tui/semantic/Field.tsx +0 -7
  101. package/src/tui/semantic/Label.tsx +0 -7
  102. package/src/tui/semantic/MenuButton.tsx +0 -7
  103. package/src/tui/semantic/MenuItem.tsx +0 -7
  104. package/src/tui/semantic/Overlay.tsx +0 -7
  105. package/src/tui/semantic/Panel.tsx +0 -7
  106. package/src/tui/semantic/ScrollView.tsx +0 -9
  107. package/src/tui/semantic/Select.tsx +0 -7
  108. package/src/tui/semantic/Spacer.tsx +0 -7
  109. package/src/tui/semantic/Spinner.tsx +0 -7
  110. package/src/tui/semantic/TextInput.tsx +0 -7
  111. package/src/tui/semantic/Value.tsx +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablozaiden/terminatui",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Terminal UI and Command Line Application Framework",
5
5
  "repository": {
6
6
  "url": "https://github.com/PabloZaiden/terminatui"
@@ -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
- export * from "./tui/components/JsonHighlight.tsx";
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
 
@@ -1,18 +1,14 @@
1
1
  import type { AnyCommand } from "../core/command.ts";
2
- import { useClipboard } from "./hooks/useClipboard.ts";
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, useExecutor } from "./context/ExecutorContext.tsx";
10
- import { Header } from "./components/Header.tsx";
11
- import { StatusBar } from "./components/StatusBar.tsx";
12
- import { Container } from "./semantic/Container.tsx";
13
- import { Panel } from "./semantic/Panel.tsx";
14
- import { getScreen, getModal } from "./registry.ts";
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
- <KeyboardProvider>
28
- <ClipboardProviderComponent>
29
- <TuiAppContextProvider
30
- name={name}
31
- displayName={displayName}
32
- version={version}
33
- commands={commands}
34
- onExit={onExit}
35
- >
36
- <LogsProvider>
37
- <ExecutorProvider>
38
- <NavigationProvider<CommandSelectParams>
39
- initialScreen={{ route: CommandSelectScreen.Id, params: { commandPath: [] } }}
40
- onExit={onExit}
41
- >
42
- <TuiRootContent />
43
- </NavigationProvider>
44
- </ExecutorProvider>
45
- </LogsProvider>
46
- </TuiAppContextProvider>
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 executor = useExecutor();
60
- const clipboard = useClipboardContext();
61
- const { copyWithMessage, lastAction } = useClipboard();
62
-
63
-
54
+ const [copyToast, setCopyToast] = useState<string | null>(null);
64
55
 
65
- // Global keyboard handler - only truly global shortcuts
66
- useGlobalKeyHandler((key) => {
67
- // Esc - back/close (delegates to navigation which delegates to screen)
68
- if (key.name === "escape") {
69
- navigation.goBack();
70
- return true;
71
- }
72
-
73
- // Ctrl+Y - copy
74
- if (key.ctrl && key.name === "y") {
75
- const content = clipboard.getContent();
76
- if (content) {
77
- copyWithMessage(content.content, content.label);
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
- // Ctrl+L - toggle logs modal
83
- if (key.ctrl && key.name === "l") {
84
- const isLogsOpen = navigation.modalStack.some((m) => m.id === "logs");
85
- if (isLogsOpen) {
86
- navigation.closeModal();
87
- } else {
88
- navigation.openModal("logs");
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
- return true;
92
- }
79
+ if (!renderer.renderKeyboardHandler) {
80
+ return null;
81
+ }
93
82
 
94
- return false;
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
- }
@@ -0,0 +1,4 @@
1
+ export type TuiAction =
2
+ | { type: "nav.back" }
3
+ | { type: "copy" }
4
+ | { type: "logs.open" };
@@ -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 { Button } from "./components/Button.tsx";
9
- import { Container } from "./components/Container.tsx";
10
- import { Field } from "./components/Field.tsx";
11
- import { Label } from "./components/Label.tsx";
12
- import { MenuButton } from "./components/MenuButton.tsx";
13
- import { MenuItem } from "./components/MenuItem.tsx";
14
- import { Overlay } from "./components/Overlay.tsx";
15
- import { Panel } from "./components/Panel.tsx";
16
- import { ScrollView } from "./components/ScrollView.tsx";
17
- import { Select } from "./components/Select.tsx";
18
- import { Spacer } from "./components/Spacer.tsx";
19
- import { Spinner } from "./components/Spinner.tsx";
20
- import { TextInput } from "./components/TextInput.tsx";
21
- import { Value } from "./components/Value.tsx";
22
- import { Code } from "./components/Code.tsx";
23
- import { CodeHighlight } from "./components/CodeHighlight.tsx";
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
+ }