@pablozaiden/terminatui 0.3.0-beta-1 → 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 (142) hide show
  1. package/package.json +10 -3
  2. package/src/__tests__/adapterNoSharedUi.test.ts +34 -0
  3. package/src/__tests__/configOnChange.test.ts +63 -0
  4. package/src/__tests__/schemaToFields.test.ts +0 -4
  5. package/src/__tests__/tuiRootNoCoupling.test.ts +25 -0
  6. package/src/builtins/version.ts +1 -1
  7. package/src/index.ts +22 -0
  8. package/src/tui/TuiApplication.tsx +0 -4
  9. package/src/tui/TuiRoot.tsx +58 -102
  10. package/src/tui/actions.ts +4 -0
  11. package/src/tui/adapters/ink/InkRenderer.tsx +191 -41
  12. package/src/tui/adapters/ink/SemanticInkRenderer.tsx +210 -0
  13. package/src/tui/adapters/ink/components/Button.tsx +10 -2
  14. package/src/tui/adapters/ink/components/Overlay.tsx +8 -2
  15. package/src/tui/adapters/ink/components/Panel.tsx +26 -5
  16. package/src/tui/adapters/ink/components/ScrollView.tsx +44 -3
  17. package/src/tui/adapters/ink/components/Spinner.tsx +8 -2
  18. package/src/tui/adapters/ink/keyboard.ts +0 -3
  19. package/src/tui/adapters/ink/ui/CommandSelector.tsx +56 -0
  20. package/src/tui/adapters/ink/ui/ConfigForm.tsx +77 -0
  21. package/src/tui/adapters/ink/ui/Header.tsx +25 -0
  22. package/src/tui/adapters/ink/ui/JsonHighlight.tsx +21 -0
  23. package/src/tui/adapters/ink/ui/ResultsPanel.tsx +57 -0
  24. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +190 -39
  25. package/src/tui/adapters/opentui/SemanticOpenTuiRenderer.tsx +192 -0
  26. package/src/tui/adapters/opentui/components/Label.tsx +2 -2
  27. package/src/tui/adapters/opentui/components/Overlay.tsx +12 -3
  28. package/src/tui/adapters/opentui/components/Panel.tsx +11 -1
  29. package/src/tui/adapters/opentui/components/ScrollView.tsx +1 -8
  30. package/src/tui/adapters/opentui/components/Spinner.tsx +1 -1
  31. package/src/tui/adapters/opentui/keyboard.ts +0 -3
  32. package/src/tui/adapters/opentui/ui/CommandSelector.tsx +55 -0
  33. package/src/tui/adapters/opentui/ui/ConfigForm.tsx +74 -0
  34. package/src/tui/adapters/opentui/ui/Header.tsx +24 -0
  35. package/src/tui/adapters/opentui/ui/JsonHighlight.tsx +20 -0
  36. package/src/tui/adapters/opentui/ui/LogsPanel.tsx +44 -0
  37. package/src/tui/adapters/opentui/ui/ResultsPanel.tsx +62 -0
  38. package/src/tui/adapters/shared/TerminalClipboard.ts +65 -0
  39. package/src/tui/adapters/{opentui/hooks → shared}/useSpinner.ts +5 -1
  40. package/src/tui/adapters/types.ts +25 -45
  41. package/src/tui/components/JsonHighlight.tsx +41 -111
  42. package/src/tui/context/ActionContext.tsx +51 -0
  43. package/src/tui/context/ExecutorContext.tsx +7 -1
  44. package/src/tui/context/NavigationContext.tsx +20 -4
  45. package/src/tui/controllers/CommandBrowserController.tsx +100 -0
  46. package/src/tui/controllers/ConfigController.tsx +183 -0
  47. package/src/tui/controllers/EditorController.tsx +169 -0
  48. package/src/tui/controllers/LogsController.tsx +48 -0
  49. package/src/tui/controllers/OutcomeController.tsx +110 -0
  50. package/src/tui/driver/TuiDriver.tsx +148 -0
  51. package/src/tui/driver/context/TuiDriverContext.tsx +44 -0
  52. package/src/tui/driver/types.ts +72 -0
  53. package/src/tui/semantic/AppShell.tsx +30 -0
  54. package/src/tui/semantic/CommandBrowserScreen.tsx +16 -0
  55. package/src/tui/semantic/ConfigScreen.tsx +23 -0
  56. package/src/tui/semantic/EditorScreen.tsx +20 -0
  57. package/src/tui/semantic/LogsScreen.tsx +9 -0
  58. package/src/tui/semantic/RunningScreen.tsx +17 -0
  59. package/src/tui/semantic/layoutTypes.ts +72 -0
  60. package/src/tui/semantic/render.tsx +44 -0
  61. package/src/tui/semantic/types.ts +31 -98
  62. package/src/tui/utils/jsonTokenizer.ts +98 -0
  63. package/src/tui/utils/schemaToFields.ts +1 -25
  64. package/.devcontainer/devcontainer.json +0 -19
  65. package/.devcontainer/install-prerequisites.sh +0 -49
  66. package/.github/workflows/copilot-setup-steps.yml +0 -32
  67. package/.github/workflows/pull-request.yml +0 -27
  68. package/.github/workflows/release-npm-package.yml +0 -81
  69. package/AGENTS.md +0 -43
  70. package/CLAUDE.md +0 -1
  71. package/bun.lock +0 -321
  72. package/examples/tui-app/commands/config/app/get.ts +0 -62
  73. package/examples/tui-app/commands/config/app/index.ts +0 -23
  74. package/examples/tui-app/commands/config/app/set.ts +0 -96
  75. package/examples/tui-app/commands/config/index.ts +0 -28
  76. package/examples/tui-app/commands/config/user/get.ts +0 -61
  77. package/examples/tui-app/commands/config/user/index.ts +0 -23
  78. package/examples/tui-app/commands/config/user/set.ts +0 -57
  79. package/examples/tui-app/commands/greet.ts +0 -78
  80. package/examples/tui-app/commands/math.ts +0 -111
  81. package/examples/tui-app/commands/status.ts +0 -86
  82. package/examples/tui-app/index.ts +0 -38
  83. package/guides/01-hello-world.md +0 -101
  84. package/guides/02-adding-options.md +0 -103
  85. package/guides/03-multiple-commands.md +0 -161
  86. package/guides/04-subcommands.md +0 -206
  87. package/guides/05-interactive-tui.md +0 -209
  88. package/guides/06-config-validation.md +0 -256
  89. package/guides/07-async-cancellation.md +0 -334
  90. package/guides/08-complete-application.md +0 -507
  91. package/guides/README.md +0 -78
  92. package/src/tui/adapters/ink/components/Code.tsx +0 -6
  93. package/src/tui/adapters/ink/components/Container.tsx +0 -5
  94. package/src/tui/adapters/ink/components/Spacer.tsx +0 -15
  95. package/src/tui/adapters/ink/components/Value.tsx +0 -7
  96. package/src/tui/adapters/opentui/components/Code.tsx +0 -12
  97. package/src/tui/adapters/opentui/components/Container.tsx +0 -56
  98. package/src/tui/adapters/opentui/components/Spacer.tsx +0 -5
  99. package/src/tui/adapters/opentui/components/Value.tsx +0 -13
  100. package/src/tui/components/ActionButton.tsx +0 -0
  101. package/src/tui/components/CommandSelector.tsx +0 -119
  102. package/src/tui/components/ConfigForm.tsx +0 -174
  103. package/src/tui/components/FieldRow.tsx +0 -0
  104. package/src/tui/components/Header.tsx +0 -32
  105. package/src/tui/components/ModalBase.tsx +0 -38
  106. package/src/tui/components/ResultsPanel.tsx +0 -84
  107. package/src/tui/components/StatusBar.tsx +0 -44
  108. package/src/tui/components/logColors.ts +0 -12
  109. package/src/tui/components/types.ts +0 -30
  110. package/src/tui/context/ClipboardContext.tsx +0 -87
  111. package/src/tui/context/KeyboardContext.tsx +0 -132
  112. package/src/tui/hooks/useActiveKeyHandler.ts +0 -75
  113. package/src/tui/hooks/useClipboard.ts +0 -81
  114. package/src/tui/hooks/useClipboardProvider.ts +0 -42
  115. package/src/tui/hooks/useGlobalKeyHandler.ts +0 -54
  116. package/src/tui/modals/CliModal.tsx +0 -82
  117. package/src/tui/modals/EditorModal.tsx +0 -207
  118. package/src/tui/modals/LogsModal.tsx +0 -98
  119. package/src/tui/registry.ts +0 -102
  120. package/src/tui/screens/CommandSelectScreen.tsx +0 -162
  121. package/src/tui/screens/ConfigScreen.tsx +0 -160
  122. package/src/tui/screens/ErrorScreen.tsx +0 -58
  123. package/src/tui/screens/ResultsScreen.tsx +0 -60
  124. package/src/tui/screens/RunningScreen.tsx +0 -72
  125. package/src/tui/screens/ScreenBase.ts +0 -6
  126. package/src/tui/semantic/Button.tsx +0 -7
  127. package/src/tui/semantic/Code.tsx +0 -7
  128. package/src/tui/semantic/CodeHighlight.tsx +0 -7
  129. package/src/tui/semantic/Container.tsx +0 -7
  130. package/src/tui/semantic/Field.tsx +0 -7
  131. package/src/tui/semantic/Label.tsx +0 -7
  132. package/src/tui/semantic/MenuButton.tsx +0 -7
  133. package/src/tui/semantic/MenuItem.tsx +0 -7
  134. package/src/tui/semantic/Overlay.tsx +0 -7
  135. package/src/tui/semantic/Panel.tsx +0 -7
  136. package/src/tui/semantic/ScrollView.tsx +0 -9
  137. package/src/tui/semantic/Select.tsx +0 -7
  138. package/src/tui/semantic/Spacer.tsx +0 -7
  139. package/src/tui/semantic/Spinner.tsx +0 -7
  140. package/src/tui/semantic/TextInput.tsx +0 -7
  141. package/src/tui/semantic/Value.tsx +0 -7
  142. package/tsconfig.json +0 -25
@@ -1,13 +0,0 @@
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
- }
File without changes
@@ -1,119 +0,0 @@
1
- import { useActiveKeyHandler } from "../hooks/useActiveKeyHandler.ts";
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";
7
-
8
- interface CommandItem {
9
- /** The command object */
10
- command: Command;
11
- /** Display label (defaults to command name) */
12
- label?: string;
13
- /** Description (defaults to command description) */
14
- description?: string;
15
- }
16
-
17
- interface CommandSelectorProps {
18
- /** Commands to display */
19
- commands: CommandItem[];
20
- /** Currently selected index */
21
- selectedIndex: number;
22
- /** Called when selection changes */
23
- onSelectionChange: (index: number) => void;
24
- /** Called when a command is selected */
25
- onSelect: (command: Command) => void;
26
- /** Breadcrumb path for nested commands */
27
- breadcrumb?: string[];
28
- }
29
-
30
- /**
31
- * Command selection menu.
32
- */
33
- export function CommandSelector({
34
- commands,
35
- selectedIndex,
36
- onSelectionChange,
37
- onSelect,
38
- breadcrumb,
39
- }: CommandSelectorProps) {
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
- }
48
-
49
- if (key.name === "up") {
50
- const newIndex = Math.max(selectedIndex - 1, 0);
51
- onSelectionChange(newIndex);
52
- return true;
53
- }
54
-
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);
60
- }
61
- return true;
62
- }
63
-
64
- return false;
65
- });
66
-
67
- const title = breadcrumb?.length
68
- ? `Select Command (${breadcrumb.join(" › ")})`
69
- : "Select Command";
70
-
71
- return (
72
- <Container flexDirection="column" flex={1} justifyContent="center" alignItems="center" gap={1}>
73
- <Panel
74
- flexDirection="column"
75
- title={title}
76
- padding={undefined}
77
- width={60}
78
- focused
79
- >
80
- <Container flexDirection="column" gap={1}>
81
- {commands.map((item, idx) => {
82
- const isSelected = idx === selectedIndex;
83
- const label = item.label ?? item.command.displayName ?? item.command.name;
84
- const description = item.description ?? item.command.description;
85
-
86
- const modeIndicator = getModeIndicator(item.command);
87
-
88
- return (
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
- />
97
- );
98
- })}
99
- </Container>
100
- </Panel>
101
-
102
- <Label color="mutedText">↑/↓ Navigate • Enter Select • Esc {breadcrumb?.length ? "Back" : "Exit"}</Label>
103
- </Container>
104
- );
105
- }
106
-
107
- /**
108
- * Get mode indicator for a command (e.g., "[cli]", "[tui]", "→" for subcommands).
109
- */
110
- function getModeIndicator(command: Command): string {
111
- // Show navigation indicator for container commands with navigable subcommands
112
- // (excluding commands that don't support TUI)
113
- const navigableSubCommands = command.subCommands?.filter((sub) => sub.supportsTui()) ?? [];
114
- if (navigableSubCommands.length > 0) {
115
- return "→";
116
- }
117
-
118
- return "";
119
- }
@@ -1,174 +0,0 @@
1
- import { useRef, useEffect, type ReactNode } from "react";
2
- import { Field } from "../semantic/Field.tsx";
3
- import { MenuButton } from "../semantic/MenuButton.tsx";
4
- import { Panel } from "../semantic/Panel.tsx";
5
- import { ScrollView, type ScrollViewRef } from "../semantic/ScrollView.tsx";
6
- import { Container } from "../semantic/Container.tsx";
7
- import { useActiveKeyHandler } from "../hooks/useActiveKeyHandler.ts";
8
- import type { KeyboardEvent } from "../adapters/types.ts";
9
- import type { FieldConfig } from "./types.ts";
10
-
11
- interface ConfigFormProps {
12
- /** Title for the form border */
13
- title: string;
14
- /** Field configurations */
15
- fieldConfigs: FieldConfig[];
16
- /** Current values */
17
- values: Record<string, unknown>;
18
- /** Currently selected index */
19
- selectedIndex: number;
20
- /** Whether the form is focused */
21
- focused: boolean;
22
- /** Called when selection changes */
23
- onSelectionChange: (index: number) => void;
24
- /** Called when a field should be edited */
25
- onEditField: (fieldKey: string) => void;
26
- /** Called when the action button is pressed */
27
- onAction: () => void;
28
- /** Function to get display value for a field */
29
- getDisplayValue?: (key: string, value: unknown, type: string) => string;
30
- /** The action button component */
31
- actionButton: ReactNode;
32
- /** Optional additional buttons rendered before the main action button */
33
- additionalButtons?: { label: string; onPress: () => void }[];
34
- /** Optional handler for additional keys (called before default handling) */
35
- onKeyDown?: (event: KeyboardEvent) => boolean;
36
- }
37
-
38
- /**
39
- * Default display value formatter.
40
- */
41
- function defaultGetDisplayValue(_key: string, value: unknown, type: string): string {
42
- if (type === "boolean") {
43
- return value ? "True" : "False";
44
- }
45
- const strValue = String(value ?? "");
46
- if (strValue === "") {
47
- return "(empty)";
48
- }
49
- return strValue.length > 60 ? strValue.substring(0, 57) + "..." : strValue;
50
- }
51
-
52
- /**
53
- * Generic config form that renders fields from a schema.
54
- */
55
- export function ConfigForm({
56
- title,
57
- fieldConfigs,
58
- values,
59
- selectedIndex,
60
- focused,
61
- onSelectionChange,
62
- onEditField,
63
- onAction,
64
- getDisplayValue = defaultGetDisplayValue,
65
- actionButton,
66
- additionalButtons = [],
67
- onKeyDown,
68
- }: ConfigFormProps) {
69
- const scrollViewRef = useRef<ScrollViewRef | null>(null);
70
- const totalItems = fieldConfigs.length + additionalButtons.length + 1; // fields + additional buttons + action button
71
-
72
- // Auto-scroll to keep selected item visible
73
- useEffect(() => {
74
- scrollViewRef.current?.scrollToIndex(selectedIndex);
75
- }, [selectedIndex]);
76
-
77
- // Handle keyboard events (only when focused)
78
- useActiveKeyHandler(
79
- (event: KeyboardEvent) => {
80
- // Let parent handle first if provided
81
- if (onKeyDown?.(event)) {
82
- return true;
83
- }
84
-
85
- const key = event;
86
-
87
- // Arrow key navigation
88
- if (key.name === "down") {
89
- const newIndex = Math.min(selectedIndex + 1, totalItems - 1);
90
- onSelectionChange(newIndex);
91
- return true;
92
- }
93
-
94
- if (key.name === "up") {
95
- const newIndex = Math.max(selectedIndex - 1, 0);
96
- onSelectionChange(newIndex);
97
- return true;
98
- }
99
-
100
- // Enter to edit field, press additional button, or run action
101
- if (key.name === "return" || key.name === "enter") {
102
- if (selectedIndex < fieldConfigs.length) {
103
- // It's a field
104
- const fieldConfig = fieldConfigs[selectedIndex];
105
- if (fieldConfig) {
106
- onEditField(fieldConfig.key);
107
- }
108
- } else if (selectedIndex < fieldConfigs.length + additionalButtons.length) {
109
- // It's an additional button
110
- const buttonIndex = selectedIndex - fieldConfigs.length;
111
- additionalButtons[buttonIndex]?.onPress();
112
- } else {
113
- // It's the main action button
114
- onAction();
115
- }
116
- return true;
117
- }
118
-
119
- return false;
120
- },
121
- { enabled: focused }
122
- );
123
-
124
- return (
125
- <Panel
126
- title={title}
127
- focused={focused}
128
- flex={1}
129
- padding={1}
130
- flexDirection="column"
131
- >
132
- <ScrollView
133
- axis="vertical"
134
- flex={1}
135
- scrollRef={(ref) => {
136
- scrollViewRef.current = ref;
137
- }}
138
- >
139
- <Container flexDirection="column" gap={0}>
140
- {fieldConfigs.map((field, idx) => {
141
- const isSelected = idx === selectedIndex;
142
- const displayValue = getDisplayValue(
143
- field.key,
144
- values[field.key],
145
- field.type
146
- );
147
-
148
- return (
149
- <Field
150
- key={field.key}
151
- label={field.label}
152
- value={displayValue}
153
- selected={isSelected}
154
- />
155
- );
156
- })}
157
-
158
- {additionalButtons.map((btn, idx) => {
159
- const buttonSelectedIndex = fieldConfigs.length + idx;
160
- return (
161
- <MenuButton
162
- key={btn.label}
163
- label={btn.label}
164
- selected={selectedIndex === buttonSelectedIndex}
165
- />
166
- );
167
- })}
168
-
169
- {actionButton}
170
- </Container>
171
- </ScrollView>
172
- </Panel>
173
- );
174
- }
File without changes
@@ -1,32 +0,0 @@
1
- import { Container } from "../semantic/Container.tsx";
2
- import { Label } from "../semantic/Label.tsx";
3
- import { Spacer } from "../semantic/Spacer.tsx";
4
-
5
- interface HeaderProps {
6
- /** Application name */
7
- name: string;
8
- /** Application version */
9
- version: string;
10
- /** Optional breadcrumb path (e.g., ["run", "config"]) */
11
- breadcrumb?: string[];
12
- }
13
-
14
- /**
15
- * Application header with name, version, and optional breadcrumb.
16
- */
17
- export function Header({ name, version, breadcrumb }: HeaderProps) {
18
- const breadcrumbStr = breadcrumb?.length ? ` › ${breadcrumb.join(" › ")}` : "";
19
-
20
- return (
21
- <Container flexDirection="column" noShrink>
22
- <Container flexDirection="row" justifyContent="space-between">
23
- <Label color="mutedText" bold>
24
- {name}
25
- {breadcrumbStr}
26
- </Label>
27
- <Label color="mutedText">v{version}</Label>
28
- </Container>
29
- <Spacer size={1} />
30
- </Container>
31
- );
32
- }
@@ -1,38 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import { Panel } from "../semantic/Panel.tsx";
3
- import { Container } from "../semantic/Container.tsx";
4
- import { Overlay } from "../semantic/Overlay.tsx";
5
-
6
- type Dim = number | `${number}%` | "auto";
7
-
8
- interface ModalBaseProps {
9
- title?: string;
10
- width?: Dim;
11
- height?: Dim;
12
- top?: Dim;
13
- left?: Dim;
14
- right?: Dim;
15
- bottom?: Dim;
16
- children: ReactNode;
17
- }
18
-
19
- export function ModalBase({
20
- title,
21
- width = "80%",
22
- height = "auto",
23
- top = 4,
24
- left = 4,
25
- right,
26
- bottom,
27
- children,
28
- }: ModalBaseProps) {
29
- return (
30
- <Overlay zIndex={20} top={top} left={left} right={right} bottom={bottom} width={width} height={height}>
31
- <Panel title={title} border={true} flexDirection="column" flex={1} padding={1} surface="overlay">
32
- <Container flexDirection="column" gap={1} flex={1}>
33
- {children}
34
- </Container>
35
- </Panel>
36
- </Overlay>
37
- );
38
- }
@@ -1,84 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import type { CommandResult } from "../../core/command.ts";
3
- import { Container } from "../semantic/Container.tsx";
4
- import { Panel } from "../semantic/Panel.tsx";
5
- import { ScrollView } from "../semantic/ScrollView.tsx";
6
- import { Label } from "../semantic/Label.tsx";
7
- import { Value } from "../semantic/Value.tsx";
8
-
9
- interface ResultsPanelProps {
10
- /** The result to display */
11
- result: CommandResult | null;
12
- /** Error to display (if any) */
13
- error: Error | null;
14
- /** Whether the panel is focused */
15
- focused: boolean;
16
- /** Custom result renderer */
17
- renderResult?: (result: CommandResult) => ReactNode;
18
- }
19
-
20
- /**
21
- * Panel displaying command execution results.
22
- */
23
- export function ResultsPanel({
24
- result,
25
- error,
26
- focused,
27
- renderResult,
28
- }: ResultsPanelProps) {
29
-
30
- // Determine content to display
31
- let content: ReactNode;
32
-
33
- if (error) {
34
- content = (
35
- <Container flexDirection="column" gap={1}>
36
- <Label color="error" bold>
37
- Error
38
- </Label>
39
- <Label color="error">{error.message}</Label>
40
- </Container>
41
- );
42
- } else if (result) {
43
- if (renderResult) {
44
- const customContent = renderResult(result);
45
-
46
- if (typeof customContent === "string" || typeof customContent === "number" || typeof customContent === "boolean") {
47
- // Wrap primitive results so the renderer gets a text node
48
- content = <Value>{String(customContent)}</Value>;
49
- } else {
50
- content = customContent as ReactNode;
51
- }
52
- } else {
53
- // Default JSON display
54
- content = (
55
- <Container flexDirection="column" gap={1}>
56
- {result.message && (
57
- <Label color={result.success ? "success" : "error"}>{result.message}</Label>
58
- )}
59
- {result.data !== undefined && result.data !== null && (
60
- <Value>{JSON.stringify(result.data, null, 2)}</Value>
61
- )}
62
- </Container>
63
- );
64
- }
65
- } else {
66
- content = <Label color="mutedText">No results yet...</Label>;
67
- }
68
-
69
- return (
70
- <Panel
71
- title="Results"
72
- focused={focused}
73
- flex={1}
74
- padding={1}
75
- flexDirection="column"
76
- >
77
- <ScrollView axis="vertical" flex={1} focused={focused}>
78
- <Container flexDirection="column">
79
- {content}
80
- </Container>
81
- </ScrollView>
82
- </Panel>
83
- );
84
- }
@@ -1,44 +0,0 @@
1
- import { Label } from "../semantic/Label.tsx";
2
- import { Spinner } from "../semantic/Spinner.tsx";
3
- import { Panel } from "../semantic/Panel.tsx";
4
- import { Container } from "../semantic/Container.tsx";
5
-
6
- interface StatusBarProps {
7
- /** Status message to display */
8
- status: string;
9
- /** Whether the app is currently running a command */
10
- isRunning?: boolean;
11
- /** Whether to show keyboard shortcuts */
12
- showShortcuts?: boolean;
13
- /** Custom shortcuts string (defaults to standard shortcuts) */
14
- shortcuts?: string;
15
- }
16
-
17
- /**
18
- * Status bar showing current status, spinner, and keyboard shortcuts.
19
- */
20
- export function StatusBar({
21
- status,
22
- isRunning = false,
23
- showShortcuts = true,
24
- shortcuts = "L Logs • C CLI • Tab Switch • Ctrl+Y Copy • Esc Back",
25
- }: StatusBarProps) {
26
- return (
27
- <Panel dense border={true} flexDirection="column" gap={0} height={showShortcuts ? 4 : 2}>
28
- <Container flexDirection="row" justifyContent="space-between" padding={{ left: 1, right: 1 }}>
29
- <Container flexDirection="row">
30
- <Spinner active={isRunning} />
31
- <Label color="success" bold>
32
- {status}
33
- </Label>
34
- </Container>
35
- </Container>
36
-
37
- {showShortcuts ? (
38
- <Container padding={{ left: 1, right: 1 }}>
39
- <Label color="mutedText">{shortcuts}</Label>
40
- </Container>
41
- ) : null}
42
- </Panel>
43
- );
44
- }
@@ -1,12 +0,0 @@
1
- import { LogLevel } from "../../core/logger";
2
-
3
- // Shared colors for log levels used across debug views.
4
- export const LogColors: Record<LogLevel, string> = {
5
- [LogLevel.silly]: "#8c8c8c",
6
- [LogLevel.trace]: "#6dd6ff",
7
- [LogLevel.debug]: "#7bdcb5",
8
- [LogLevel.info]: "#d6dde6",
9
- [LogLevel.warn]: "#f5c542",
10
- [LogLevel.error]: "#f78888",
11
- [LogLevel.fatal]: "#ff5c8d",
12
- };
@@ -1,30 +0,0 @@
1
- /**
2
- * Field type for TUI forms.
3
- */
4
- export type FieldType = "text" | "number" | "enum" | "boolean";
5
-
6
- /**
7
- * Option for enum/select fields.
8
- */
9
- export interface FieldOption {
10
- name: string;
11
- value: unknown;
12
- }
13
-
14
- /**
15
- * Field configuration for TUI forms.
16
- */
17
- export interface FieldConfig {
18
- /** Field key (must match a key in values) */
19
- key: string;
20
- /** Display label */
21
- label: string;
22
- /** Field type */
23
- type: FieldType;
24
- /** Options for enum type */
25
- options?: FieldOption[];
26
- /** Placeholder text for input fields */
27
- placeholder?: string;
28
- /** Group name for organizing fields */
29
- group?: string;
30
- }
@@ -1,87 +0,0 @@
1
- import { createContext, useContext, useRef, useCallback, type ReactNode } from "react";
2
-
3
- /**
4
- * Clipboard content that can be provided by a screen or modal.
5
- */
6
- export interface ClipboardContent {
7
- content: string;
8
- label: string;
9
- }
10
-
11
- /**
12
- * Provider function that returns clipboard content or null.
13
- */
14
- export type ClipboardProvider = () => ClipboardContent | null;
15
-
16
- interface ClipboardContextValue {
17
- /**
18
- * Register a clipboard provider. Returns an unregister function.
19
- * Providers are stacked - the most recently registered provider is checked first.
20
- */
21
- register: (id: string, provider: ClipboardProvider) => () => void;
22
-
23
- /**
24
- * Get clipboard content from the topmost provider that returns content.
25
- */
26
- getContent: () => ClipboardContent | null;
27
- }
28
-
29
- const ClipboardContext = createContext<ClipboardContextValue | null>(null);
30
-
31
- interface ClipboardProviderProps {
32
- children: ReactNode;
33
- }
34
-
35
- /**
36
- * Provider that manages clipboard content providers from screens and modals.
37
- * Providers are stacked - modals register on top of screens, so modal content
38
- * takes precedence when copying.
39
- */
40
- export function ClipboardProviderComponent({ children }: ClipboardProviderProps) {
41
- const providersRef = useRef<Map<string, ClipboardProvider>>(new Map());
42
- const orderRef = useRef<string[]>([]);
43
-
44
- const register = useCallback((id: string, provider: ClipboardProvider) => {
45
- providersRef.current.set(id, provider);
46
- // Add to end (most recent)
47
- orderRef.current = orderRef.current.filter((i) => i !== id);
48
- orderRef.current.push(id);
49
-
50
- return () => {
51
- providersRef.current.delete(id);
52
- orderRef.current = orderRef.current.filter((i) => i !== id);
53
- };
54
- }, []);
55
-
56
- const getContent = useCallback((): ClipboardContent | null => {
57
- // Check providers in reverse order (most recent first)
58
- for (let i = orderRef.current.length - 1; i >= 0; i--) {
59
- const id = orderRef.current[i];
60
- const provider = providersRef.current.get(id!);
61
- if (provider) {
62
- const content = provider();
63
- if (content) {
64
- return content;
65
- }
66
- }
67
- }
68
- return null;
69
- }, []);
70
-
71
- return (
72
- <ClipboardContext.Provider value={{ register, getContent }}>
73
- {children}
74
- </ClipboardContext.Provider>
75
- );
76
- }
77
-
78
- /**
79
- * Access the clipboard context.
80
- */
81
- export function useClipboardContext(): ClipboardContextValue {
82
- const context = useContext(ClipboardContext);
83
- if (!context) {
84
- throw new Error("useClipboardContext must be used within a ClipboardProviderComponent");
85
- }
86
- return context;
87
- }