@pablozaiden/terminatui 0.2.0 → 0.3.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 (181) hide show
  1. package/README.md +64 -43
  2. package/package.json +11 -8
  3. package/src/__tests__/application.test.ts +87 -68
  4. package/src/__tests__/buildCliCommand.test.ts +99 -119
  5. package/src/__tests__/builtins.test.ts +27 -75
  6. package/src/__tests__/command.test.ts +100 -131
  7. package/src/__tests__/configOnChange.test.ts +63 -0
  8. package/src/__tests__/context.test.ts +1 -26
  9. package/src/__tests__/helpCore.test.ts +227 -0
  10. package/src/__tests__/parser.test.ts +98 -244
  11. package/src/__tests__/registry.test.ts +33 -160
  12. package/src/__tests__/schemaToFields.test.ts +75 -158
  13. package/src/builtins/help.ts +12 -4
  14. package/src/builtins/settings.ts +18 -32
  15. package/src/builtins/version.ts +3 -3
  16. package/src/cli/output/colors.ts +1 -1
  17. package/src/cli/parser.ts +26 -95
  18. package/src/core/application.ts +192 -110
  19. package/src/core/command.ts +26 -9
  20. package/src/core/context.ts +31 -20
  21. package/src/core/help.ts +24 -18
  22. package/src/core/knownCommands.ts +13 -0
  23. package/src/core/logger.ts +39 -42
  24. package/src/core/registry.ts +5 -12
  25. package/src/index.ts +22 -137
  26. package/src/tui/TuiApplication.tsx +63 -120
  27. package/src/tui/TuiRoot.tsx +135 -0
  28. package/src/tui/adapters/factory.ts +19 -0
  29. package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
  30. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  31. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  32. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  33. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  34. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  35. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  36. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  37. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  38. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  39. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  40. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  41. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  42. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  43. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  44. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  45. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  46. package/src/tui/adapters/ink/keyboard.ts +97 -0
  47. package/src/tui/adapters/ink/utils.ts +16 -0
  48. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
  49. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  50. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  51. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  52. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  53. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  54. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  55. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  56. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  57. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  58. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  59. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  60. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  61. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  62. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  63. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  64. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  65. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  66. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  67. package/src/tui/adapters/types.ts +71 -0
  68. package/src/tui/components/ActionButton.tsx +0 -36
  69. package/src/tui/components/CommandSelector.tsx +45 -92
  70. package/src/tui/components/ConfigForm.tsx +68 -42
  71. package/src/tui/components/FieldRow.tsx +0 -30
  72. package/src/tui/components/Header.tsx +14 -13
  73. package/src/tui/components/JsonHighlight.tsx +10 -17
  74. package/src/tui/components/ModalBase.tsx +38 -0
  75. package/src/tui/components/ResultsPanel.tsx +27 -36
  76. package/src/tui/components/StatusBar.tsx +24 -39
  77. package/src/tui/components/logColors.ts +12 -0
  78. package/src/tui/context/ClipboardContext.tsx +87 -0
  79. package/src/tui/context/ExecutorContext.tsx +139 -0
  80. package/src/tui/context/KeyboardContext.tsx +85 -71
  81. package/src/tui/context/LogsContext.tsx +35 -0
  82. package/src/tui/context/NavigationContext.tsx +194 -0
  83. package/src/tui/context/RendererContext.tsx +20 -0
  84. package/src/tui/context/TuiAppContext.tsx +58 -0
  85. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  86. package/src/tui/hooks/useBackHandler.ts +34 -0
  87. package/src/tui/hooks/useClipboard.ts +40 -25
  88. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  89. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  90. package/src/tui/modals/CliModal.tsx +82 -0
  91. package/src/tui/modals/EditorModal.tsx +207 -0
  92. package/src/tui/modals/LogsModal.tsx +98 -0
  93. package/src/tui/registry.ts +102 -0
  94. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  95. package/src/tui/screens/ConfigScreen.tsx +165 -0
  96. package/src/tui/screens/ErrorScreen.tsx +58 -0
  97. package/src/tui/screens/ResultsScreen.tsx +68 -0
  98. package/src/tui/screens/RunningScreen.tsx +72 -0
  99. package/src/tui/screens/ScreenBase.ts +6 -0
  100. package/src/tui/semantic/Button.tsx +7 -0
  101. package/src/tui/semantic/Code.tsx +7 -0
  102. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  103. package/src/tui/semantic/Container.tsx +7 -0
  104. package/src/tui/semantic/Field.tsx +7 -0
  105. package/src/tui/semantic/Label.tsx +7 -0
  106. package/src/tui/semantic/MenuButton.tsx +7 -0
  107. package/src/tui/semantic/MenuItem.tsx +7 -0
  108. package/src/tui/semantic/Overlay.tsx +7 -0
  109. package/src/tui/semantic/Panel.tsx +7 -0
  110. package/src/tui/semantic/ScrollView.tsx +9 -0
  111. package/src/tui/semantic/Select.tsx +7 -0
  112. package/src/tui/semantic/Spacer.tsx +7 -0
  113. package/src/tui/semantic/Spinner.tsx +7 -0
  114. package/src/tui/semantic/TextInput.tsx +7 -0
  115. package/src/tui/semantic/Value.tsx +7 -0
  116. package/src/tui/semantic/types.ts +195 -0
  117. package/src/tui/theme.ts +25 -14
  118. package/src/tui/utils/buildCliCommand.ts +1 -0
  119. package/src/tui/utils/getEnumKeys.ts +3 -0
  120. package/src/tui/utils/parameterPersistence.ts +1 -0
  121. package/src/types/command.ts +0 -60
  122. package/.devcontainer/devcontainer.json +0 -19
  123. package/.devcontainer/install-prerequisites.sh +0 -49
  124. package/.github/workflows/copilot-setup-steps.yml +0 -32
  125. package/.github/workflows/pull-request.yml +0 -27
  126. package/.github/workflows/release-npm-package.yml +0 -81
  127. package/AGENTS.md +0 -31
  128. package/bun.lock +0 -236
  129. package/examples/tui-app/commands/config/app/get.ts +0 -66
  130. package/examples/tui-app/commands/config/app/index.ts +0 -27
  131. package/examples/tui-app/commands/config/app/set.ts +0 -86
  132. package/examples/tui-app/commands/config/index.ts +0 -32
  133. package/examples/tui-app/commands/config/user/get.ts +0 -65
  134. package/examples/tui-app/commands/config/user/index.ts +0 -27
  135. package/examples/tui-app/commands/config/user/set.ts +0 -61
  136. package/examples/tui-app/commands/greet.ts +0 -76
  137. package/examples/tui-app/commands/index.ts +0 -4
  138. package/examples/tui-app/commands/math.ts +0 -115
  139. package/examples/tui-app/commands/status.ts +0 -77
  140. package/examples/tui-app/index.ts +0 -35
  141. package/guides/01-hello-world.md +0 -96
  142. package/guides/02-adding-options.md +0 -103
  143. package/guides/03-multiple-commands.md +0 -163
  144. package/guides/04-subcommands.md +0 -206
  145. package/guides/05-interactive-tui.md +0 -194
  146. package/guides/06-config-validation.md +0 -264
  147. package/guides/07-async-cancellation.md +0 -336
  148. package/guides/08-complete-application.md +0 -537
  149. package/guides/README.md +0 -74
  150. package/src/__tests__/colors.test.ts +0 -127
  151. package/src/__tests__/commandClass.test.ts +0 -130
  152. package/src/__tests__/help.test.ts +0 -412
  153. package/src/__tests__/registryNew.test.ts +0 -160
  154. package/src/__tests__/table.test.ts +0 -146
  155. package/src/__tests__/tui.test.ts +0 -26
  156. package/src/builtins/index.ts +0 -4
  157. package/src/cli/help.ts +0 -174
  158. package/src/cli/index.ts +0 -3
  159. package/src/cli/output/index.ts +0 -2
  160. package/src/cli/output/table.ts +0 -141
  161. package/src/commands/help.ts +0 -50
  162. package/src/commands/index.ts +0 -1
  163. package/src/components/index.ts +0 -147
  164. package/src/core/index.ts +0 -15
  165. package/src/hooks/index.ts +0 -131
  166. package/src/registry/commandRegistry.ts +0 -77
  167. package/src/registry/index.ts +0 -1
  168. package/src/tui/TuiApp.tsx +0 -619
  169. package/src/tui/app.ts +0 -29
  170. package/src/tui/components/CliModal.tsx +0 -81
  171. package/src/tui/components/EditorModal.tsx +0 -177
  172. package/src/tui/components/LogsPanel.tsx +0 -86
  173. package/src/tui/components/index.ts +0 -13
  174. package/src/tui/context/index.ts +0 -7
  175. package/src/tui/hooks/index.ts +0 -35
  176. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  177. package/src/tui/hooks/useLogStream.ts +0 -96
  178. package/src/tui/index.ts +0 -65
  179. package/src/tui/utils/index.ts +0 -13
  180. package/src/types/index.ts +0 -1
  181. package/tsconfig.json +0 -25
@@ -0,0 +1,85 @@
1
+ import { useRef, type ReactNode } from "react";
2
+ import type { ScrollBoxRenderable } from "@opentui/core";
3
+ import type { ScrollViewProps, ScrollViewRef, Spacing } from "../../../semantic/types.ts";
4
+
5
+ function normalizePadding(padding: number | Spacing | undefined): any {
6
+ if (padding === undefined) {
7
+ return undefined;
8
+ }
9
+
10
+ if (typeof padding === "number") {
11
+ return padding;
12
+ }
13
+
14
+ return {
15
+ top: padding.top ?? 0,
16
+ right: padding.right ?? 0,
17
+ bottom: padding.bottom ?? 0,
18
+ left: padding.left ?? 0,
19
+ };
20
+ }
21
+
22
+ export function ScrollView({
23
+ axis = "vertical",
24
+ stickyToEnd,
25
+ focused,
26
+ scrollRef: onScrollRef,
27
+ children,
28
+ flex,
29
+ width,
30
+ height,
31
+ flexDirection,
32
+ alignItems,
33
+ justifyContent,
34
+ gap,
35
+ padding,
36
+ }: ScrollViewProps & { children?: ReactNode }) {
37
+ const scrollRef = useRef<ScrollBoxRenderable>(null);
38
+
39
+ const imperativeApi: ScrollViewRef = {
40
+ scrollToTop: () => {
41
+ scrollRef.current?.scrollTo(0);
42
+ },
43
+ scrollToBottom: () => {
44
+ // No public "bottom" API in ScrollBoxRenderable; use large index.
45
+ scrollRef.current?.scrollTo(Number.MAX_SAFE_INTEGER);
46
+ },
47
+ scrollToIndex: (index: number) => {
48
+ scrollRef.current?.scrollTo(index);
49
+ },
50
+ };
51
+
52
+ // Provide the imperative API via callback.
53
+ if (onScrollRef) {
54
+ onScrollRef(imperativeApi);
55
+ }
56
+
57
+ const scrollY = axis === "vertical" || axis === "both";
58
+ const scrollX = axis === "horizontal" || axis === "both";
59
+
60
+ const resolvedStickyToEnd = stickyToEnd ? true : undefined;
61
+
62
+
63
+ return (
64
+ <scrollbox
65
+ ref={scrollRef}
66
+ scrollY={scrollY}
67
+ scrollX={scrollX}
68
+ focused={focused}
69
+ {...({ stickyToEnd: resolvedStickyToEnd })}
70
+ flexGrow={flex}
71
+ width={width as any}
72
+ height={height as any}
73
+ >
74
+ <box
75
+ flexDirection={flexDirection as any}
76
+ alignItems={alignItems as any}
77
+ justifyContent={justifyContent as any}
78
+ gap={gap}
79
+ padding={normalizePadding(padding)}
80
+ >
81
+ {children}
82
+ </box>
83
+ </scrollbox>
84
+ );
85
+ }
@@ -0,0 +1,59 @@
1
+ import type { SelectProps } from "../../../semantic/types.ts";
2
+ import type { SelectOption as OpenTuiSelectOption } from "@opentui/core";
3
+ import { SemanticColors } from "../../../theme.ts";
4
+
5
+ export function Select<TValue extends string>({
6
+ options,
7
+ value,
8
+ focused,
9
+ onChange,
10
+ onSubmit,
11
+ }: SelectProps) {
12
+ const selectedIndex = Math.max(
13
+ 0,
14
+ options.findIndex((opt) => opt.value === value)
15
+ );
16
+
17
+ return (
18
+ <select
19
+ options={
20
+ options.map(
21
+ (opt): OpenTuiSelectOption => ({
22
+ name: opt.label,
23
+ description: "",
24
+ value: opt.value,
25
+ })
26
+ )
27
+ }
28
+ selectedIndex={selectedIndex}
29
+ focused={focused}
30
+ onChange={(idx: number) => {
31
+ const next = options[idx];
32
+ if (next) {
33
+ onChange(next.value);
34
+ }
35
+ }}
36
+ onSelect={(idx: number, option: OpenTuiSelectOption | null) => {
37
+ if (option) {
38
+ onChange(option.value as TValue);
39
+ } else {
40
+ const next = options[idx];
41
+ if (next) {
42
+ onChange(next.value);
43
+ }
44
+ }
45
+
46
+ // Only submit when OpenTUI triggers selection (Enter).
47
+ // Arrow navigation uses onChange only.
48
+ onSubmit?.();
49
+ }}
50
+ showScrollIndicator={false}
51
+ showDescription={false}
52
+ height={Math.min(options.length, 10)}
53
+ width="100%"
54
+ wrapSelection={true}
55
+ selectedBackgroundColor={SemanticColors.focusBorder}
56
+ selectedTextColor={SemanticColors.inverseText}
57
+ />
58
+ );
59
+ }
@@ -0,0 +1,5 @@
1
+ import type { SpacerProps } from "../../../semantic/types.ts";
2
+
3
+ export function Spacer({ size, axis = "vertical" }: SpacerProps) {
4
+ return axis === "horizontal" ? <box width={size} flexShrink={0} /> : <box height={size} flexShrink={0} />;
5
+ }
@@ -0,0 +1,12 @@
1
+ import type { SpinnerProps } from "../../../semantic/types.ts";
2
+ import { useSpinner } from "../hooks/useSpinner.ts";
3
+
4
+ export function Spinner({ active }: SpinnerProps) {
5
+ const { frame } = useSpinner(active);
6
+
7
+ if (!active) {
8
+ return "";
9
+ }
10
+
11
+ return <text>{frame} </text>;
12
+ }
@@ -0,0 +1,13 @@
1
+ import type { TextInputProps } from "../../../semantic/types.ts";
2
+
3
+ export function TextInput({ value, placeholder, focused, onChange, onSubmit }: TextInputProps) {
4
+ return (
5
+ <input
6
+ value={value}
7
+ placeholder={placeholder}
8
+ focused={focused}
9
+ onInput={(next: string) => onChange(next)}
10
+ onSubmit={() => onSubmit?.()}
11
+ />
12
+ );
13
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ValueProps } from "../../../semantic/types.ts";
3
+ import { SemanticColors } from "../../../theme.ts";
4
+
5
+ export function Value({ color = "value", truncate, children }: ValueProps & { children: ReactNode }) {
6
+ const fg = SemanticColors[color] ?? SemanticColors.value;
7
+
8
+ return (
9
+ <text fg={fg} {...({ truncate })}>
10
+ {children}
11
+ </text>
12
+ );
13
+ }
@@ -1,21 +1,13 @@
1
- import { useState, useEffect, useMemo } from "react";
1
+ import { useEffect, useMemo, useState } from "react";
2
2
 
3
3
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
4
4
  const SPINNER_INTERVAL = 80;
5
5
 
6
- export interface UseSpinnerResult {
7
- /** Current frame index */
6
+ interface UseSpinnerResult {
8
7
  frameIndex: number;
9
- /** Current spinner character */
10
8
  frame: string;
11
9
  }
12
10
 
13
- /**
14
- * Hook for animated spinner.
15
- *
16
- * @param active - Whether the spinner is active
17
- * @returns Spinner state with current frame
18
- */
19
11
  export function useSpinner(active: boolean): UseSpinnerResult {
20
12
  const [frameIndex, setFrameIndex] = useState(0);
21
13
 
@@ -27,7 +19,6 @@ export function useSpinner(active: boolean): UseSpinnerResult {
27
19
 
28
20
  const interval = setInterval(() => {
29
21
  setFrameIndex((prev) => {
30
- // Reset to avoid overflow
31
22
  if (prev >= Number.MAX_SAFE_INTEGER / 2) {
32
23
  return 0;
33
24
  }
@@ -0,0 +1,61 @@
1
+ import { useKeyboard } from "@opentui/react";
2
+ import type { KeyEvent } from "@opentui/core";
3
+ import { useCallback, useMemo, useRef } from "react";
4
+ import type { KeyboardAdapter, KeyboardEvent, KeyHandler } from "../types.ts";
5
+
6
+ function normalizeKeyEvent(key: KeyEvent): KeyboardEvent {
7
+ return {
8
+ name: key.name,
9
+ sequence: key.sequence,
10
+ ctrl: key.ctrl,
11
+ shift: key.shift,
12
+ meta: key.meta,
13
+ };
14
+ }
15
+
16
+ export function useOpenTuiKeyboardAdapter(): KeyboardAdapter {
17
+ const handlerStackRef = useRef<{ id: string; handler: KeyHandler }[]>([]);
18
+ const globalHandlerRef = useRef<KeyHandler | null>(null);
19
+
20
+ const setActiveHandler = useCallback((id: string, handler: KeyHandler) => {
21
+ handlerStackRef.current = handlerStackRef.current.filter((h) => h.id !== id);
22
+ handlerStackRef.current.push({ id, handler });
23
+
24
+ return () => {
25
+ handlerStackRef.current = handlerStackRef.current.filter((h) => h.id !== id);
26
+ };
27
+ }, []);
28
+
29
+ const setGlobalHandler = useCallback((handler: KeyHandler) => {
30
+ const previous = globalHandlerRef.current;
31
+ globalHandlerRef.current = handler;
32
+
33
+ return () => {
34
+ globalHandlerRef.current = previous;
35
+ };
36
+ }, []);
37
+
38
+ useKeyboard((key: KeyEvent) => {
39
+ const event = normalizeKeyEvent(key);
40
+
41
+ if (globalHandlerRef.current) {
42
+ const handled = globalHandlerRef.current(event);
43
+ if (handled) {
44
+ return;
45
+ }
46
+ }
47
+
48
+ const activeHandler = handlerStackRef.current[handlerStackRef.current.length - 1];
49
+ if (activeHandler) {
50
+ activeHandler.handler(event);
51
+ }
52
+ });
53
+
54
+ return useMemo(
55
+ () => ({
56
+ setActiveHandler,
57
+ setGlobalHandler,
58
+ }),
59
+ [setActiveHandler, setGlobalHandler]
60
+ );
61
+ }
@@ -0,0 +1,71 @@
1
+ import type { ReactNode } from "react";
2
+ import type {
3
+ ButtonProps,
4
+ CodeHighlightProps,
5
+ CodeProps,
6
+ ContainerProps,
7
+ FieldProps,
8
+ LabelProps,
9
+ MenuButtonProps,
10
+ MenuItemProps,
11
+ OverlayProps,
12
+ PanelProps,
13
+ ScrollViewProps,
14
+ SelectProps,
15
+ SpacerProps,
16
+ SpinnerProps,
17
+ TextInputProps,
18
+ ValueProps,
19
+ } from "../semantic/types.ts";
20
+
21
+ export interface KeyboardEvent {
22
+ name: string;
23
+ sequence?: string;
24
+ ctrl?: boolean;
25
+ shift?: boolean;
26
+ meta?: boolean;
27
+ }
28
+
29
+ export type KeyHandler = (event: KeyboardEvent) => boolean;
30
+
31
+ export interface KeyboardAdapter {
32
+ setActiveHandler: (id: string, handler: KeyHandler) => () => void;
33
+ setGlobalHandler: (handler: KeyHandler) => () => void;
34
+ }
35
+
36
+ export interface RendererConfig {
37
+ useAlternateScreen?: boolean;
38
+ }
39
+
40
+ export interface RendererComponents {
41
+ Field: (props: FieldProps) => ReactNode;
42
+ Button: (props: ButtonProps) => ReactNode;
43
+ MenuButton: (props: MenuButtonProps) => ReactNode;
44
+ MenuItem: (props: MenuItemProps) => ReactNode;
45
+
46
+ Container: (props: ContainerProps) => ReactNode;
47
+ Panel: (props: PanelProps) => ReactNode;
48
+ ScrollView: (props: ScrollViewProps) => ReactNode;
49
+
50
+ Overlay: (props: OverlayProps) => ReactNode;
51
+ Spacer: (props: SpacerProps) => ReactNode;
52
+ Spinner: (props: SpinnerProps) => ReactNode;
53
+
54
+ Label: (props: LabelProps) => ReactNode;
55
+ Value: (props: ValueProps) => ReactNode;
56
+ Code: (props: CodeProps) => ReactNode;
57
+ CodeHighlight: (props: CodeHighlightProps) => ReactNode;
58
+
59
+ TextInput: (props: TextInputProps) => ReactNode;
60
+ Select: (props: SelectProps) => ReactNode;
61
+ }
62
+
63
+ export interface Renderer {
64
+ initialize: () => Promise<void>;
65
+ render: (node: ReactNode) => void;
66
+ destroy: () => void;
67
+ supportCustomRendering: () => boolean;
68
+
69
+ keyboard: KeyboardAdapter;
70
+ components: RendererComponents;
71
+ }
@@ -1,36 +0,0 @@
1
- import { Theme } from "../theme.ts";
2
-
3
- interface ActionButtonProps {
4
- /** Button label */
5
- label: string;
6
- /** Whether this button is selected */
7
- isSelected: boolean;
8
- /** Optional spinner frame for loading state */
9
- spinnerFrame?: string;
10
- }
11
-
12
- /**
13
- * Action button displayed at the bottom of a config form.
14
- */
15
- export function ActionButton({ label, isSelected, spinnerFrame }: ActionButtonProps) {
16
- const prefix = isSelected ? "► " : " ";
17
- const displayLabel = spinnerFrame ? `${spinnerFrame} ${label}...` : `[ ${label} ]`;
18
-
19
- if (isSelected) {
20
- return (
21
- <box marginTop={1}>
22
- <text fg="#000000" bg={Theme.actionButton}>
23
- {prefix}{displayLabel}
24
- </text>
25
- </box>
26
- );
27
- }
28
-
29
- return (
30
- <box marginTop={1}>
31
- <text fg={Theme.actionButton}>
32
- {prefix}{displayLabel}
33
- </text>
34
- </box>
35
- );
36
- }
@@ -1,6 +1,9 @@
1
- import { Theme } from "../theme.ts";
2
- import { useKeyboardHandler, KeyboardPriority } from "../hooks/useKeyboardHandler.ts";
1
+ import { useActiveKeyHandler } from "../hooks/useActiveKeyHandler.ts";
3
2
  import type { Command } from "../../core/command.ts";
3
+ import { MenuItem } from "../semantic/MenuItem.tsx";
4
+ import { Container } from "../semantic/Container.tsx";
5
+ import { Panel } from "../semantic/Panel.tsx";
6
+ import { Label } from "../semantic/Label.tsx";
4
7
 
5
8
  interface CommandItem {
6
9
  /** The command object */
@@ -20,8 +23,6 @@ interface CommandSelectorProps {
20
23
  onSelectionChange: (index: number) => void;
21
24
  /** Called when a command is selected */
22
25
  onSelect: (command: Command) => void;
23
- /** Called when user wants to exit */
24
- onExit: () => void;
25
26
  /** Breadcrumb path for nested commands */
26
27
  breadcrumb?: string[];
27
28
  }
@@ -34,114 +35,72 @@ export function CommandSelector({
34
35
  selectedIndex,
35
36
  onSelectionChange,
36
37
  onSelect,
37
- onExit,
38
38
  breadcrumb,
39
39
  }: CommandSelectorProps) {
40
- // Keyboard handler for navigation
41
- useKeyboardHandler(
42
- (event) => {
43
- const { key } = event;
40
+ // Active keyboard handler for navigation
41
+ useActiveKeyHandler((key) => {
42
+ // Arrow key navigation
43
+ if (key.name === "down") {
44
+ const newIndex = Math.min(selectedIndex + 1, commands.length - 1);
45
+ onSelectionChange(newIndex);
46
+ return true;
47
+ }
44
48
 
45
- // Arrow key navigation
46
- if (key.name === "down") {
47
- const newIndex = Math.min(selectedIndex + 1, commands.length - 1);
48
- onSelectionChange(newIndex);
49
- event.stopPropagation();
50
- return;
51
- }
49
+ if (key.name === "up") {
50
+ const newIndex = Math.max(selectedIndex - 1, 0);
51
+ onSelectionChange(newIndex);
52
+ return true;
53
+ }
52
54
 
53
- if (key.name === "up") {
54
- const newIndex = Math.max(selectedIndex - 1, 0);
55
- onSelectionChange(newIndex);
56
- event.stopPropagation();
57
- return;
55
+ // Enter to select command
56
+ if (key.name === "return" || key.name === "enter") {
57
+ const selected = commands[selectedIndex];
58
+ if (selected) {
59
+ onSelect(selected.command);
58
60
  }
61
+ return true;
62
+ }
59
63
 
60
- // Enter to select command
61
- if (key.name === "return" || key.name === "enter") {
62
- const selected = commands[selectedIndex];
63
- if (selected) {
64
- onSelect(selected.command);
65
- }
66
- event.stopPropagation();
67
- return;
68
- }
69
-
70
- // Escape to exit or go back
71
- if (key.name === "escape") {
72
- onExit();
73
- event.stopPropagation();
74
- return;
75
- }
76
- },
77
- KeyboardPriority.Focused
78
- );
64
+ return false;
65
+ });
79
66
 
80
67
  const title = breadcrumb?.length
81
68
  ? `Select Command (${breadcrumb.join(" › ")})`
82
69
  : "Select Command";
83
70
 
84
71
  return (
85
- <box
86
- flexDirection="column"
87
- flexGrow={1}
88
- justifyContent="center"
89
- alignItems="center"
90
- gap={1}
91
- >
92
- <box
72
+ <Container flexDirection="column" flex={1} justifyContent="center" alignItems="center" gap={1}>
73
+ <Panel
93
74
  flexDirection="column"
94
- border={true}
95
- borderStyle="rounded"
96
- borderColor={Theme.borderFocused}
97
75
  title={title}
98
- paddingLeft={3}
99
- paddingRight={3}
100
- paddingTop={1}
101
- paddingBottom={1}
102
- minWidth={60}
76
+ padding={undefined}
77
+ width={60}
78
+ focused
103
79
  >
104
- <box flexDirection="column" gap={1}>
80
+ <Container flexDirection="column" gap={1}>
105
81
  {commands.map((item, idx) => {
106
82
  const isSelected = idx === selectedIndex;
107
- const prefix = isSelected ? "► " : " ";
108
83
  const label = item.label ?? item.command.displayName ?? item.command.name;
109
84
  const description = item.description ?? item.command.description;
110
85
 
111
- // Show mode indicators
112
86
  const modeIndicator = getModeIndicator(item.command);
113
87
 
114
- if (isSelected) {
115
- return (
116
- <box key={item.command.name} flexDirection="column">
117
- <text fg="#000000" bg="cyan">
118
- {prefix}{label} {modeIndicator}
119
- </text>
120
- <text fg={Theme.label}>
121
- {" "}{description}
122
- </text>
123
- </box>
124
- );
125
- }
126
-
127
88
  return (
128
- <box key={item.command.name} flexDirection="column">
129
- <text fg={Theme.value}>
130
- {prefix}{label} {modeIndicator}
131
- </text>
132
- <text fg={Theme.border}>
133
- {" "}{description}
134
- </text>
135
- </box>
89
+ <MenuItem
90
+ key={item.command.name}
91
+ label={label}
92
+ description={description}
93
+ suffix={modeIndicator}
94
+ selected={isSelected}
95
+ onActivate={() => onSelect(item.command)}
96
+ />
136
97
  );
137
98
  })}
138
- </box>
139
- </box>
99
+ </Container>
100
+ </Panel>
140
101
 
141
- <text fg={Theme.label}>
142
- ↑↓ Navigate • Enter Select • Esc {breadcrumb?.length ? "Back" : "Exit"}
143
- </text>
144
- </box>
102
+ <Label color="mutedText">↑/↓ Navigate • Enter Select • Esc {breadcrumb?.length ? "Back" : "Exit"}</Label>
103
+ </Container>
145
104
  );
146
105
  }
147
106
 
@@ -156,11 +115,5 @@ function getModeIndicator(command: Command): string {
156
115
  return "→";
157
116
  }
158
117
 
159
- const cli = command.supportsCli();
160
- const tui = command.supportsTui();
161
-
162
- if (cli && tui) return "";
163
- if (cli) return "[cli]";
164
- if (tui) return "[tui]";
165
118
  return "";
166
119
  }