@pablozaiden/terminatui 0.2.0 → 0.3.0-beta-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -2
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +6 -10
- package/examples/tui-app/commands/config/app/index.ts +2 -6
- package/examples/tui-app/commands/config/app/set.ts +23 -13
- package/examples/tui-app/commands/config/index.ts +2 -6
- package/examples/tui-app/commands/config/user/get.ts +6 -10
- package/examples/tui-app/commands/config/user/index.ts +2 -6
- package/examples/tui-app/commands/config/user/set.ts +6 -10
- package/examples/tui-app/commands/greet.ts +13 -11
- package/examples/tui-app/commands/math.ts +5 -9
- package/examples/tui-app/commands/status.ts +21 -12
- package/examples/tui-app/index.ts +6 -3
- package/guides/01-hello-world.md +7 -2
- package/guides/02-adding-options.md +2 -2
- package/guides/03-multiple-commands.md +6 -8
- package/guides/04-subcommands.md +8 -8
- package/guides/05-interactive-tui.md +45 -30
- package/guides/06-config-validation.md +4 -12
- package/guides/07-async-cancellation.md +14 -16
- package/guides/08-complete-application.md +12 -42
- package/guides/README.md +7 -3
- package/package.json +4 -8
- package/src/__tests__/application.test.ts +87 -68
- package/src/__tests__/buildCliCommand.test.ts +99 -119
- package/src/__tests__/builtins.test.ts +27 -75
- package/src/__tests__/command.test.ts +100 -131
- package/src/__tests__/context.test.ts +1 -26
- package/src/__tests__/helpCore.test.ts +227 -0
- package/src/__tests__/parser.test.ts +98 -244
- package/src/__tests__/registry.test.ts +33 -160
- package/src/__tests__/schemaToFields.test.ts +75 -158
- package/src/builtins/help.ts +12 -4
- package/src/builtins/settings.ts +18 -32
- package/src/builtins/version.ts +4 -4
- package/src/cli/output/colors.ts +1 -1
- package/src/cli/parser.ts +26 -95
- package/src/core/application.ts +192 -110
- package/src/core/command.ts +26 -9
- package/src/core/context.ts +31 -20
- package/src/core/help.ts +24 -18
- package/src/core/knownCommands.ts +13 -0
- package/src/core/logger.ts +39 -42
- package/src/core/registry.ts +5 -12
- package/src/tui/TuiApplication.tsx +63 -120
- package/src/tui/TuiRoot.tsx +135 -0
- package/src/tui/adapters/factory.ts +19 -0
- package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
- package/src/tui/adapters/ink/components/Button.tsx +12 -0
- package/src/tui/adapters/ink/components/Code.tsx +6 -0
- package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
- package/src/tui/adapters/ink/components/Container.tsx +5 -0
- package/src/tui/adapters/ink/components/Field.tsx +12 -0
- package/src/tui/adapters/ink/components/Label.tsx +24 -0
- package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
- package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
- package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
- package/src/tui/adapters/ink/components/Panel.tsx +15 -0
- package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
- package/src/tui/adapters/ink/components/Select.tsx +44 -0
- package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
- package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
- package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
- package/src/tui/adapters/ink/components/Value.tsx +7 -0
- package/src/tui/adapters/ink/keyboard.ts +97 -0
- package/src/tui/adapters/ink/utils.ts +16 -0
- package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
- package/src/tui/adapters/opentui/components/Button.tsx +13 -0
- package/src/tui/adapters/opentui/components/Code.tsx +12 -0
- package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
- package/src/tui/adapters/opentui/components/Container.tsx +56 -0
- package/src/tui/adapters/opentui/components/Field.tsx +18 -0
- package/src/tui/adapters/opentui/components/Label.tsx +15 -0
- package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
- package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
- package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
- package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
- package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
- package/src/tui/adapters/opentui/components/Select.tsx +59 -0
- package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
- package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
- package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
- package/src/tui/adapters/opentui/components/Value.tsx +13 -0
- package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
- package/src/tui/adapters/opentui/keyboard.ts +61 -0
- package/src/tui/adapters/types.ts +70 -0
- package/src/tui/components/ActionButton.tsx +0 -36
- package/src/tui/components/CommandSelector.tsx +45 -92
- package/src/tui/components/ConfigForm.tsx +68 -42
- package/src/tui/components/FieldRow.tsx +0 -30
- package/src/tui/components/Header.tsx +14 -13
- package/src/tui/components/JsonHighlight.tsx +10 -17
- package/src/tui/components/ModalBase.tsx +38 -0
- package/src/tui/components/ResultsPanel.tsx +27 -36
- package/src/tui/components/StatusBar.tsx +24 -39
- package/src/tui/components/logColors.ts +12 -0
- package/src/tui/context/ClipboardContext.tsx +87 -0
- package/src/tui/context/ExecutorContext.tsx +139 -0
- package/src/tui/context/KeyboardContext.tsx +85 -71
- package/src/tui/context/LogsContext.tsx +35 -0
- package/src/tui/context/NavigationContext.tsx +194 -0
- package/src/tui/context/RendererContext.tsx +20 -0
- package/src/tui/context/TuiAppContext.tsx +58 -0
- package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
- package/src/tui/hooks/useBackHandler.ts +34 -0
- package/src/tui/hooks/useClipboard.ts +40 -25
- package/src/tui/hooks/useClipboardProvider.ts +42 -0
- package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
- package/src/tui/modals/CliModal.tsx +82 -0
- package/src/tui/modals/EditorModal.tsx +207 -0
- package/src/tui/modals/LogsModal.tsx +98 -0
- package/src/tui/registry.ts +102 -0
- package/src/tui/screens/CommandSelectScreen.tsx +162 -0
- package/src/tui/screens/ConfigScreen.tsx +160 -0
- package/src/tui/screens/ErrorScreen.tsx +58 -0
- package/src/tui/screens/ResultsScreen.tsx +60 -0
- package/src/tui/screens/RunningScreen.tsx +72 -0
- package/src/tui/screens/ScreenBase.ts +6 -0
- package/src/tui/semantic/Button.tsx +7 -0
- package/src/tui/semantic/Code.tsx +7 -0
- package/src/tui/semantic/CodeHighlight.tsx +7 -0
- package/src/tui/semantic/Container.tsx +7 -0
- package/src/tui/semantic/Field.tsx +7 -0
- package/src/tui/semantic/Label.tsx +7 -0
- package/src/tui/semantic/MenuButton.tsx +7 -0
- package/src/tui/semantic/MenuItem.tsx +7 -0
- package/src/tui/semantic/Overlay.tsx +7 -0
- package/src/tui/semantic/Panel.tsx +7 -0
- package/src/tui/semantic/ScrollView.tsx +9 -0
- package/src/tui/semantic/Select.tsx +7 -0
- package/src/tui/semantic/Spacer.tsx +7 -0
- package/src/tui/semantic/Spinner.tsx +7 -0
- package/src/tui/semantic/TextInput.tsx +7 -0
- package/src/tui/semantic/Value.tsx +7 -0
- package/src/tui/semantic/types.ts +195 -0
- package/src/tui/theme.ts +25 -14
- package/src/tui/utils/buildCliCommand.ts +1 -0
- package/src/tui/utils/getEnumKeys.ts +3 -0
- package/src/tui/utils/parameterPersistence.ts +1 -0
- package/src/types/command.ts +0 -60
- package/examples/tui-app/commands/index.ts +0 -4
- package/src/__tests__/colors.test.ts +0 -127
- package/src/__tests__/commandClass.test.ts +0 -130
- package/src/__tests__/help.test.ts +0 -412
- package/src/__tests__/registryNew.test.ts +0 -160
- package/src/__tests__/table.test.ts +0 -146
- package/src/__tests__/tui.test.ts +0 -26
- package/src/builtins/index.ts +0 -4
- package/src/cli/help.ts +0 -174
- package/src/cli/index.ts +0 -3
- package/src/cli/output/index.ts +0 -2
- package/src/cli/output/table.ts +0 -141
- package/src/commands/help.ts +0 -50
- package/src/commands/index.ts +0 -1
- package/src/components/index.ts +0 -147
- package/src/core/index.ts +0 -15
- package/src/hooks/index.ts +0 -131
- package/src/index.ts +0 -137
- package/src/registry/commandRegistry.ts +0 -77
- package/src/registry/index.ts +0 -1
- package/src/tui/TuiApp.tsx +0 -619
- package/src/tui/app.ts +0 -29
- package/src/tui/components/CliModal.tsx +0 -81
- package/src/tui/components/EditorModal.tsx +0 -177
- package/src/tui/components/LogsPanel.tsx +0 -86
- package/src/tui/components/index.ts +0 -13
- package/src/tui/context/index.ts +0 -7
- package/src/tui/hooks/index.ts +0 -35
- package/src/tui/hooks/useKeyboardHandler.ts +0 -91
- package/src/tui/hooks/useLogStream.ts +0 -96
- package/src/tui/index.ts +0 -65
- package/src/tui/utils/index.ts +0 -13
- package/src/types/index.ts +0 -1
|
@@ -1,44 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { Application, type ApplicationConfig } from "../core/application.ts";
|
|
1
|
+
import { createRenderer } from "./adapters/factory.ts";
|
|
2
|
+
import { RendererProvider } from "./context/RendererContext.tsx";
|
|
3
|
+
import { Application, type ModeOptions, type ApplicationConfig, type TuiModeOptions } from "../core/application.ts";
|
|
4
4
|
import type { AnyCommand } from "../core/command.ts";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import type { LogSource, LogEvent } from "./hooks/index.ts";
|
|
8
|
-
import { LogLevel as TuiLogLevel } from "./hooks/index.ts";
|
|
9
|
-
import { LogLevel as CoreLogLevel, type LogEvent as CoreLogEvent } from "../core/logger.ts";
|
|
10
|
-
import type { FieldConfig } from "./components/types.ts";
|
|
5
|
+
import { TuiRoot } from "./TuiRoot.tsx";
|
|
6
|
+
import { LogLevel } from "../core/logger.ts";
|
|
11
7
|
import { createSettingsCommand } from "../builtins/settings.ts";
|
|
8
|
+
import { KNOWN_COMMANDS } from "../core/knownCommands.ts";
|
|
12
9
|
import { loadPersistedParameters } from "./utils/parameterPersistence.ts";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* Custom field configuration for TUI forms.
|
|
16
|
-
* Allows adding application-specific fields that aren't part of command options.
|
|
17
|
-
*/
|
|
18
|
-
export interface CustomField extends FieldConfig {
|
|
19
|
-
/** Default value for the field */
|
|
20
|
-
default?: unknown;
|
|
21
|
-
/** Called when the field value changes */
|
|
22
|
-
onChange?: (value: unknown, allValues: Record<string, unknown>) => void;
|
|
23
|
-
}
|
|
10
|
+
import { AppContext } from "../core/context.ts";
|
|
11
|
+
import { registerAllModals, registerAllScreens } from "./registry.ts";
|
|
24
12
|
|
|
25
13
|
/**
|
|
26
14
|
* Extended configuration for TUI-enabled applications.
|
|
27
15
|
*/
|
|
28
16
|
export interface TuiApplicationConfig extends ApplicationConfig {
|
|
29
|
-
/** Enable
|
|
17
|
+
/** Enable TUI mode (when renderer is opentui/ink/default) */
|
|
30
18
|
enableTui?: boolean;
|
|
31
|
-
/** Log source for TUI log panel */
|
|
32
|
-
logSource?: LogSource;
|
|
33
|
-
/** Custom fields to add to the TUI form */
|
|
34
|
-
customFields?: CustomField[];
|
|
35
19
|
}
|
|
36
20
|
|
|
37
21
|
/**
|
|
38
22
|
* Application class with built-in TUI support.
|
|
39
23
|
*
|
|
40
24
|
* Extends the base Application to provide automatic TUI rendering
|
|
41
|
-
* when running
|
|
25
|
+
* when running with `--renderer` set to a TUI renderer (or default).
|
|
42
26
|
*
|
|
43
27
|
* @example
|
|
44
28
|
* ```typescript
|
|
@@ -53,19 +37,15 @@ export interface TuiApplicationConfig extends ApplicationConfig {
|
|
|
53
37
|
* }
|
|
54
38
|
* }
|
|
55
39
|
*
|
|
56
|
-
* await new MyApp().run(
|
|
40
|
+
* await new MyApp().run();
|
|
57
41
|
* ```
|
|
58
42
|
*/
|
|
59
43
|
export class TuiApplication extends Application {
|
|
60
44
|
private readonly enableTui: boolean;
|
|
61
|
-
private readonly logSource?: LogSource;
|
|
62
|
-
private readonly customFields?: CustomField[];
|
|
63
45
|
|
|
64
46
|
constructor(config: TuiApplicationConfig) {
|
|
65
47
|
super(config);
|
|
66
48
|
this.enableTui = config.enableTui ?? true;
|
|
67
|
-
this.logSource = config.logSource;
|
|
68
|
-
this.customFields = config.customFields;
|
|
69
49
|
}
|
|
70
50
|
|
|
71
51
|
/**
|
|
@@ -74,130 +54,92 @@ export class TuiApplication extends Application {
|
|
|
74
54
|
* If no arguments are provided and TUI is enabled, launches the TUI.
|
|
75
55
|
* Otherwise, runs in CLI mode.
|
|
76
56
|
*/
|
|
77
|
-
override async run(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
57
|
+
override async run(): Promise<void> {
|
|
58
|
+
return this.runFromArgs(Bun.argv.slice(2));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
override async runFromArgs(argv: string[]): Promise<void> {
|
|
62
|
+
const { globalOptions } = this.parseGlobalOptions(argv);
|
|
63
|
+
|
|
64
|
+
const mode = globalOptions["mode"] as ModeOptions ?? "default";
|
|
65
|
+
const resolvedMode = mode === "default" ? this.defaultMode : mode;
|
|
66
|
+
|
|
67
|
+
if (resolvedMode === "cli") {
|
|
68
|
+
await super.runFromArgs(argv);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!this.enableTui) {
|
|
73
|
+
throw new Error("TUI mode is disabled for this application");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (resolvedMode === "opentui" || resolvedMode === "ink") {
|
|
77
|
+
this.applyGlobalOptions(globalOptions);
|
|
78
|
+
|
|
79
|
+
await this.runTui(resolvedMode);
|
|
87
80
|
return;
|
|
88
81
|
}
|
|
89
82
|
|
|
90
|
-
|
|
91
|
-
await super.run(filteredArgs);
|
|
83
|
+
throw new Error(`Unknown mode '${resolvedMode}'`);
|
|
92
84
|
}
|
|
93
85
|
|
|
94
86
|
/**
|
|
95
|
-
* Launch the
|
|
87
|
+
* Launch the TUI.
|
|
96
88
|
*/
|
|
97
|
-
async runTui(): Promise<void> {
|
|
89
|
+
async runTui(rendererType: TuiModeOptions): Promise<void> {
|
|
90
|
+
await registerAllScreens();
|
|
91
|
+
await registerAllModals();
|
|
92
|
+
|
|
98
93
|
// Get all commands that support TUI or have options
|
|
99
94
|
const commands = this.getExecutableCommands();
|
|
100
95
|
|
|
101
96
|
// Load and apply persisted settings (log-level, detailed-logs)
|
|
102
97
|
this.loadPersistedSettings();
|
|
103
98
|
|
|
104
|
-
|
|
105
|
-
// instead of stderr (which would corrupt the TUI display)
|
|
106
|
-
this.context.logger.setTuiMode(true);
|
|
107
|
-
|
|
108
|
-
// Create a log source from the logger if one wasn't provided
|
|
109
|
-
const logSource = this.logSource ?? this.createLogSourceFromLogger();
|
|
110
|
-
|
|
111
|
-
const renderer = await createCliRenderer({
|
|
99
|
+
const renderer = await createRenderer(rendererType, {
|
|
112
100
|
useAlternateScreen: true,
|
|
113
|
-
useConsole: false,
|
|
114
|
-
exitOnCtrlC: true,
|
|
115
|
-
backgroundColor: Theme.background,
|
|
116
|
-
useMouse: true,
|
|
117
|
-
enableMouseMovement: true,
|
|
118
|
-
openConsoleOnError: false,
|
|
119
101
|
});
|
|
120
102
|
|
|
121
103
|
return new Promise<void>((resolve) => {
|
|
122
104
|
const handleExit = () => {
|
|
123
|
-
// Restore CLI mode on exit
|
|
124
|
-
this.context.logger.setTuiMode(false);
|
|
125
105
|
renderer.destroy();
|
|
126
106
|
resolve();
|
|
127
107
|
};
|
|
128
108
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
onExit={handleExit}
|
|
140
|
-
/>
|
|
109
|
+
renderer.render(
|
|
110
|
+
<RendererProvider renderer={renderer}>
|
|
111
|
+
<TuiRoot
|
|
112
|
+
name={this.name}
|
|
113
|
+
displayName={this.displayName}
|
|
114
|
+
version={this.version}
|
|
115
|
+
commands={commands}
|
|
116
|
+
onExit={handleExit}
|
|
117
|
+
/>
|
|
118
|
+
</RendererProvider>
|
|
141
119
|
);
|
|
142
|
-
|
|
143
|
-
renderer.start();
|
|
144
120
|
});
|
|
145
121
|
}
|
|
146
122
|
|
|
147
|
-
/**
|
|
148
|
-
* Create a LogSource adapter from the application logger.
|
|
149
|
-
*/
|
|
150
|
-
private createLogSourceFromLogger(): LogSource {
|
|
151
|
-
const logger = this.context.logger;
|
|
152
|
-
|
|
153
|
-
// Map core log levels to TUI log levels
|
|
154
|
-
const mapLogLevel = (level: CoreLogLevel): TuiLogLevel => {
|
|
155
|
-
switch (level) {
|
|
156
|
-
case CoreLogLevel.Silly: return TuiLogLevel.Silly;
|
|
157
|
-
case CoreLogLevel.Trace: return TuiLogLevel.Trace;
|
|
158
|
-
case CoreLogLevel.Debug: return TuiLogLevel.Debug;
|
|
159
|
-
case CoreLogLevel.Info: return TuiLogLevel.Info;
|
|
160
|
-
case CoreLogLevel.Warn: return TuiLogLevel.Warn;
|
|
161
|
-
case CoreLogLevel.Error: return TuiLogLevel.Error;
|
|
162
|
-
case CoreLogLevel.Fatal: return TuiLogLevel.Fatal;
|
|
163
|
-
default: return TuiLogLevel.Info;
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
subscribe: (callback: (event: LogEvent) => void) => {
|
|
169
|
-
return logger.onLogEvent((coreEvent: CoreLogEvent) => {
|
|
170
|
-
callback({
|
|
171
|
-
level: mapLogLevel(coreEvent.level),
|
|
172
|
-
message: coreEvent.message,
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
123
|
/**
|
|
180
124
|
* Load persisted settings and apply them to the logger.
|
|
181
125
|
* Settings are saved when the user uses the Settings command.
|
|
182
126
|
*/
|
|
183
127
|
private loadPersistedSettings(): void {
|
|
184
128
|
try {
|
|
185
|
-
const settings = loadPersistedParameters(this.name,
|
|
186
|
-
|
|
129
|
+
const settings = loadPersistedParameters(this.name, KNOWN_COMMANDS.settings);
|
|
130
|
+
|
|
187
131
|
// Apply log-level if set
|
|
188
132
|
if (settings["log-level"]) {
|
|
189
133
|
const levelStr = String(settings["log-level"]).toLowerCase();
|
|
190
|
-
const level =
|
|
191
|
-
([key, val]) => typeof val === "number" && key.toLowerCase() === levelStr
|
|
192
|
-
)?.[1] as CoreLogLevel | undefined;
|
|
134
|
+
const level = LogLevel[levelStr as keyof typeof LogLevel];
|
|
193
135
|
if (level !== undefined) {
|
|
194
|
-
|
|
136
|
+
AppContext.current.logger.setMinLevel(level);
|
|
195
137
|
}
|
|
196
138
|
}
|
|
197
|
-
|
|
139
|
+
|
|
198
140
|
// Apply detailed-logs if set
|
|
199
141
|
if (settings["detailed-logs"] !== undefined) {
|
|
200
|
-
|
|
142
|
+
AppContext.current.logger.setDetailed(Boolean(settings["detailed-logs"]));
|
|
201
143
|
}
|
|
202
144
|
} catch {
|
|
203
145
|
// Silently ignore errors loading settings
|
|
@@ -212,15 +154,16 @@ export class TuiApplication extends Application {
|
|
|
212
154
|
const userCommands = this.registry
|
|
213
155
|
.list()
|
|
214
156
|
.filter((cmd) => {
|
|
215
|
-
// Exclude
|
|
216
|
-
if (cmd.
|
|
157
|
+
// Exclude internal/built-in commands from the TUI main menu
|
|
158
|
+
if (cmd.tuiHidden) {
|
|
217
159
|
return false;
|
|
218
160
|
}
|
|
219
|
-
|
|
220
|
-
|
|
161
|
+
|
|
162
|
+
// Extra safety: keep known internal command names out
|
|
163
|
+
if (cmd.name === KNOWN_COMMANDS.help || cmd.name === KNOWN_COMMANDS.version || cmd.name === KNOWN_COMMANDS.settings) {
|
|
221
164
|
return false;
|
|
222
165
|
}
|
|
223
|
-
|
|
166
|
+
|
|
224
167
|
return true;
|
|
225
168
|
});
|
|
226
169
|
|
|
@@ -0,0 +1,135 @@
|
|
|
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";
|
|
5
|
+
import { LogsProvider } from "./context/LogsContext.tsx";
|
|
6
|
+
import { NavigationProvider, useNavigation } from "./context/NavigationContext.tsx";
|
|
7
|
+
import { ClipboardProviderComponent, useClipboardContext } from "./context/ClipboardContext.tsx";
|
|
8
|
+
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";
|
|
16
|
+
|
|
17
|
+
interface TuiRootProps {
|
|
18
|
+
name: string;
|
|
19
|
+
displayName?: string;
|
|
20
|
+
version: string;
|
|
21
|
+
commands: AnyCommand[];
|
|
22
|
+
onExit: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function TuiRoot({ name, displayName, version, commands, onExit }: TuiRootProps) {
|
|
26
|
+
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>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Main TUI content - renders current screen, modals, and handles global shortcuts.
|
|
54
|
+
* This component knows NOTHING about specific screens or their logic.
|
|
55
|
+
*/
|
|
56
|
+
function TuiRootContent() {
|
|
57
|
+
const { displayName, name, version } = useTuiApp();
|
|
58
|
+
const navigation = useNavigation();
|
|
59
|
+
const executor = useExecutor();
|
|
60
|
+
const clipboard = useClipboardContext();
|
|
61
|
+
const { copyWithMessage, lastAction } = useClipboard();
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
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
|
+
}
|
|
81
|
+
|
|
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");
|
|
89
|
+
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return false;
|
|
95
|
+
});
|
|
96
|
+
|
|
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
|
+
|
|
119
|
+
{/* Render modals from registry */}
|
|
120
|
+
{navigation.modalStack.map((modal, idx) => {
|
|
121
|
+
const ModalComponent = getModal(modal.id);
|
|
122
|
+
if (!ModalComponent) return null;
|
|
123
|
+
|
|
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,19 @@
|
|
|
1
|
+
import type { Renderer, RendererConfig } from "./types.ts";
|
|
2
|
+
import { OpenTuiRenderer } from "./opentui/OpenTuiRenderer.tsx";
|
|
3
|
+
import { InkRenderer } from "./ink/InkRenderer.tsx";
|
|
4
|
+
import type { TuiModeOptions } from "../../core/application.ts";
|
|
5
|
+
|
|
6
|
+
export async function createRenderer(type: TuiModeOptions, config: RendererConfig): Promise<Renderer> {
|
|
7
|
+
switch (type) {
|
|
8
|
+
case "opentui": {
|
|
9
|
+
const renderer = new OpenTuiRenderer(config);
|
|
10
|
+
await renderer.initialize();
|
|
11
|
+
return renderer;
|
|
12
|
+
}
|
|
13
|
+
case "ink": {
|
|
14
|
+
const renderer = new InkRenderer(config);
|
|
15
|
+
await renderer.initialize();
|
|
16
|
+
return renderer;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { render } from "ink";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import { useLayoutEffect } from "react";
|
|
4
|
+
|
|
5
|
+
import type { Renderer, RendererConfig } from "../types.ts";
|
|
6
|
+
import { useInkKeyboardAdapter } from "./keyboard.ts";
|
|
7
|
+
|
|
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";
|
|
24
|
+
|
|
25
|
+
export class InkRenderer implements Renderer {
|
|
26
|
+
private instance: ReturnType<typeof render> | null = null;
|
|
27
|
+
private activeKeyboardAdapter: Renderer["keyboard"] | null = null;
|
|
28
|
+
|
|
29
|
+
public keyboard: Renderer["keyboard"] = {
|
|
30
|
+
setActiveHandler: (id, handler) => {
|
|
31
|
+
return this.activeKeyboardAdapter?.setActiveHandler(id, handler) ?? (() => {});
|
|
32
|
+
},
|
|
33
|
+
setGlobalHandler: (handler) => {
|
|
34
|
+
return this.activeKeyboardAdapter?.setGlobalHandler(handler) ?? (() => {});
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
public components: Renderer["components"] = {
|
|
39
|
+
Field,
|
|
40
|
+
Button,
|
|
41
|
+
MenuButton,
|
|
42
|
+
MenuItem,
|
|
43
|
+
|
|
44
|
+
Container,
|
|
45
|
+
Panel,
|
|
46
|
+
ScrollView,
|
|
47
|
+
|
|
48
|
+
Overlay,
|
|
49
|
+
Spacer,
|
|
50
|
+
Spinner,
|
|
51
|
+
|
|
52
|
+
Label,
|
|
53
|
+
Value,
|
|
54
|
+
Code,
|
|
55
|
+
CodeHighlight,
|
|
56
|
+
|
|
57
|
+
Select,
|
|
58
|
+
TextInput,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
constructor(_config: RendererConfig) {}
|
|
62
|
+
|
|
63
|
+
async initialize(): Promise<void> {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
render(node: ReactNode): void {
|
|
68
|
+
if (process.stdin.isTTY) {
|
|
69
|
+
try {
|
|
70
|
+
process.stdin.setRawMode(true);
|
|
71
|
+
} catch {
|
|
72
|
+
// Ignore.
|
|
73
|
+
}
|
|
74
|
+
if (process.stdin.isPaused()) {
|
|
75
|
+
process.stdin.resume();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.instance) {
|
|
80
|
+
this.instance.rerender(
|
|
81
|
+
<KeyboardBridge
|
|
82
|
+
node={node}
|
|
83
|
+
onReady={(keyboard) => {
|
|
84
|
+
this.activeKeyboardAdapter = keyboard;
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.instance = render(
|
|
92
|
+
<KeyboardBridge
|
|
93
|
+
node={node}
|
|
94
|
+
onReady={(keyboard) => {
|
|
95
|
+
this.activeKeyboardAdapter = keyboard;
|
|
96
|
+
}}
|
|
97
|
+
/>,
|
|
98
|
+
{
|
|
99
|
+
exitOnCtrlC: true,
|
|
100
|
+
patchConsole: false,
|
|
101
|
+
stdout: process.stdout,
|
|
102
|
+
stdin: process.stdin,
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
destroy(): void {
|
|
108
|
+
this.instance?.unmount();
|
|
109
|
+
this.instance = null;
|
|
110
|
+
|
|
111
|
+
if (process.stdin.isTTY) {
|
|
112
|
+
try {
|
|
113
|
+
process.stdin.setRawMode(false);
|
|
114
|
+
} catch {
|
|
115
|
+
// Ignore.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function KeyboardBridge({
|
|
122
|
+
node,
|
|
123
|
+
onReady,
|
|
124
|
+
}: {
|
|
125
|
+
node: ReactNode;
|
|
126
|
+
onReady: (keyboard: ReturnType<typeof useInkKeyboardAdapter>) => void;
|
|
127
|
+
}) {
|
|
128
|
+
const keyboard = useInkKeyboardAdapter();
|
|
129
|
+
|
|
130
|
+
useLayoutEffect(() => {
|
|
131
|
+
onReady(keyboard);
|
|
132
|
+
}, [onReady, keyboard]);
|
|
133
|
+
|
|
134
|
+
return node;
|
|
135
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import type { ButtonProps } from "../../../semantic/types.ts";
|
|
3
|
+
|
|
4
|
+
export function Button({ label, selected }: ButtonProps) {
|
|
5
|
+
const prefix = selected ? "> " : " ";
|
|
6
|
+
return (
|
|
7
|
+
<Text>
|
|
8
|
+
{prefix}
|
|
9
|
+
{label}
|
|
10
|
+
</Text>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import type { FieldProps } from "../../../semantic/types.ts";
|
|
3
|
+
|
|
4
|
+
export function Field({ label, value, selected }: FieldProps) {
|
|
5
|
+
const prefix = selected ? "> " : " ";
|
|
6
|
+
return (
|
|
7
|
+
<Text>
|
|
8
|
+
{prefix}
|
|
9
|
+
{label}: {value as any}
|
|
10
|
+
</Text>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import type { LabelProps } from "../../../semantic/types.ts";
|
|
3
|
+
import { toPlainText } from "../utils.ts";
|
|
4
|
+
|
|
5
|
+
const COLOR_MAP: Record<string, string> = {
|
|
6
|
+
text: "white",
|
|
7
|
+
mutedText: "gray",
|
|
8
|
+
primary: "cyan",
|
|
9
|
+
success: "green",
|
|
10
|
+
warning: "yellow",
|
|
11
|
+
error: "red",
|
|
12
|
+
value: "magenta",
|
|
13
|
+
code: "gray",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function Label({ color, bold, italic, children }: LabelProps) {
|
|
17
|
+
const text = toPlainText(children);
|
|
18
|
+
const inkColor = color ? (COLOR_MAP[color] ?? color) : undefined;
|
|
19
|
+
return (
|
|
20
|
+
<Text color={inkColor} bold={bold} italic={italic}>
|
|
21
|
+
{text}
|
|
22
|
+
</Text>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import type { MenuButtonProps } from "../../../semantic/types.ts";
|
|
3
|
+
|
|
4
|
+
export function MenuButton({ label, selected }: MenuButtonProps) {
|
|
5
|
+
const prefix = selected ? "> " : " ";
|
|
6
|
+
return (
|
|
7
|
+
<Text>
|
|
8
|
+
{prefix}
|
|
9
|
+
{label}
|
|
10
|
+
</Text>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Text } from "ink";
|
|
2
|
+
import type { MenuItemProps } from "../../../semantic/types.ts";
|
|
3
|
+
|
|
4
|
+
export function MenuItem({ label, description, suffix, selected }: MenuItemProps) {
|
|
5
|
+
const prefix = selected ? "> " : " ";
|
|
6
|
+
const desc = description ? ` — ${description}` : "";
|
|
7
|
+
const suffixText = suffix ? ` ${suffix}` : "";
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<Text>
|
|
11
|
+
{prefix}
|
|
12
|
+
{label}
|
|
13
|
+
{desc}
|
|
14
|
+
{suffixText}
|
|
15
|
+
</Text>
|
|
16
|
+
);
|
|
17
|
+
}
|