@pablozaiden/terminatui 0.1.2 → 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 +43 -0
- package/CLAUDE.md +1 -0
- package/README.md +64 -43
- package/bun.lock +85 -0
- package/examples/tui-app/commands/config/app/get.ts +62 -0
- package/examples/tui-app/commands/config/app/index.ts +23 -0
- package/examples/tui-app/commands/config/app/set.ts +96 -0
- package/examples/tui-app/commands/config/index.ts +28 -0
- package/examples/tui-app/commands/config/user/get.ts +61 -0
- package/examples/tui-app/commands/config/user/index.ts +23 -0
- package/examples/tui-app/commands/config/user/set.ts +57 -0
- package/examples/tui-app/commands/greet.ts +14 -11
- package/examples/tui-app/commands/math.ts +6 -9
- package/examples/tui-app/commands/status.ts +24 -13
- package/examples/tui-app/index.ts +7 -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 +15 -69
- package/guides/08-complete-application.md +13 -179
- 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 +19 -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 +52 -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 -3
- 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 -582
- 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
package/src/core/help.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface HelpOptions {
|
|
|
12
12
|
version?: string;
|
|
13
13
|
/** Command path leading to this command (e.g., ["app", "remote", "add"]) */
|
|
14
14
|
commandPath?: string[];
|
|
15
|
+
/** Global options schema for this app */
|
|
16
|
+
globalOptionsSchema?: Record<string, OptionDef>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -59,38 +61,34 @@ export function formatSubCommands(command: AnyCommand): string {
|
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
/**
|
|
62
|
-
* Format options
|
|
64
|
+
* Format an options schema into a help section.
|
|
63
65
|
*/
|
|
64
|
-
export function
|
|
65
|
-
if (
|
|
66
|
+
export function formatOptionSchema(title: string, schema: Record<string, OptionDef>): string {
|
|
67
|
+
if (Object.keys(schema).length === 0) return "";
|
|
66
68
|
|
|
67
|
-
const entries = Object.entries(
|
|
68
|
-
const def = defUntyped as OptionDef;
|
|
69
|
+
const entries = Object.entries(schema).map(([name, def]) => {
|
|
69
70
|
const alias = def.alias ? `-${def.alias}, ` : " ";
|
|
70
|
-
const flag = `${alias}--${name}`;
|
|
71
71
|
const required = def.required ? colors.red(" (required)") : "";
|
|
72
72
|
const defaultVal =
|
|
73
73
|
def.default !== undefined ? colors.dim(` [default: ${def.default}]`) : "";
|
|
74
74
|
const enumVals = def.enum ? colors.dim(` [${def.enum.join(" | ")}]`) : "";
|
|
75
|
-
|
|
75
|
+
|
|
76
|
+
const noVariant = def.type === "boolean" ? `, --no-${name}` : "";
|
|
77
|
+
const flag = `${alias}--${name}${noVariant}`;
|
|
78
|
+
const typeHint = def.type === "boolean" ? "" : colors.dim(` <${def.type}>`);
|
|
76
79
|
|
|
77
80
|
return ` ${colors.yellow(flag)}${typeHint}${required}\n ${def.description}${enumVals}${defaultVal}`;
|
|
78
81
|
});
|
|
79
82
|
|
|
80
|
-
return [colors.bold("
|
|
83
|
+
return [colors.bold(title + ":"), ...entries].join("\n");
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/**
|
|
84
|
-
* Format
|
|
87
|
+
* Format options list.
|
|
85
88
|
*/
|
|
86
|
-
export function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
` ${colors.yellow(" --detailed-logs")}\n Include timestamp and level prefix in log output`,
|
|
90
|
-
` ${colors.yellow(" --no-detailed-logs")}\n Disable detailed log format`,
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
return [colors.bold("Global Options:"), ...entries].join("\n");
|
|
89
|
+
export function formatOptions(command: AnyCommand): string {
|
|
90
|
+
if (!command.options || Object.keys(command.options).length === 0) return "";
|
|
91
|
+
return formatOptionSchema("Options", command.options as Record<string, OptionDef>);
|
|
94
92
|
}
|
|
95
93
|
|
|
96
94
|
/**
|
|
@@ -154,7 +152,9 @@ export function generateCommandHelp(command: AnyCommand, options: HelpOptions =
|
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
// Global options (available on all commands)
|
|
157
|
-
|
|
155
|
+
if (options.globalOptionsSchema && Object.keys(options.globalOptionsSchema).length > 0) {
|
|
156
|
+
sections.push(`\n${formatOptionSchema("Global Options", options.globalOptionsSchema)}`);
|
|
157
|
+
}
|
|
158
158
|
|
|
159
159
|
// Examples
|
|
160
160
|
const examplesSection = formatExamples(command);
|
|
@@ -191,6 +191,12 @@ export function generateAppHelp(commands: AnyCommand[], options: HelpOptions = {
|
|
|
191
191
|
// Usage
|
|
192
192
|
sections.push(`${colors.bold("Usage:")}\n ${appName} [command] [options]\n`);
|
|
193
193
|
|
|
194
|
+
// Global options
|
|
195
|
+
if (options.globalOptionsSchema && Object.keys(options.globalOptionsSchema).length > 0) {
|
|
196
|
+
sections.push(formatOptionSchema("Global Options", options.globalOptionsSchema));
|
|
197
|
+
sections.push("");
|
|
198
|
+
}
|
|
199
|
+
|
|
194
200
|
// Commands
|
|
195
201
|
if (commands.length > 0) {
|
|
196
202
|
const entries = commands.map((cmd) => {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const KNOWN_COMMANDS = {
|
|
2
|
+
help: "help",
|
|
3
|
+
settings: "settings",
|
|
4
|
+
version: "version",
|
|
5
|
+
} as const;
|
|
6
|
+
|
|
7
|
+
export type KnownCommandName = (typeof KNOWN_COMMANDS)[keyof typeof KNOWN_COMMANDS];
|
|
8
|
+
|
|
9
|
+
export const RESERVED_TOP_LEVEL_COMMAND_NAMES = new Set<KnownCommandName>([
|
|
10
|
+
KNOWN_COMMANDS.help,
|
|
11
|
+
KNOWN_COMMANDS.settings,
|
|
12
|
+
KNOWN_COMMANDS.version,
|
|
13
|
+
]);
|
package/src/core/logger.ts
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import { EventEmitter } from "events";
|
|
2
|
-
import { Logger as TsLogger } from "tslog";
|
|
2
|
+
import { Logger as TsLogger, type IMeta } from "tslog";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Log levels from least to most severe.
|
|
6
6
|
*/
|
|
7
7
|
export enum LogLevel {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
silly = 0,
|
|
9
|
+
trace = 1,
|
|
10
|
+
debug = 2,
|
|
11
|
+
info = 3,
|
|
12
|
+
warn = 4,
|
|
13
|
+
error = 5,
|
|
14
|
+
fatal = 6,
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Event emitted when a log message is written.
|
|
19
19
|
*/
|
|
20
20
|
export interface LogEvent {
|
|
21
|
+
rawMessage: string;
|
|
21
22
|
message: string;
|
|
22
23
|
level: LogLevel;
|
|
23
24
|
timestamp: Date;
|
|
@@ -42,14 +43,12 @@ export interface LoggerConfig {
|
|
|
42
43
|
export class Logger {
|
|
43
44
|
private tsLogger: TsLogger<unknown>;
|
|
44
45
|
private readonly eventEmitter = new EventEmitter();
|
|
45
|
-
private tuiMode: boolean;
|
|
46
46
|
private detailed: boolean;
|
|
47
47
|
private minLevel: LogLevel;
|
|
48
48
|
|
|
49
49
|
constructor(config: LoggerConfig = {}) {
|
|
50
|
-
this.tuiMode = config.tuiMode ?? false;
|
|
51
50
|
this.detailed = config.detailed ?? false;
|
|
52
|
-
this.minLevel = config.minLevel ?? LogLevel.
|
|
51
|
+
this.minLevel = config.minLevel ?? LogLevel.info;
|
|
53
52
|
|
|
54
53
|
this.tsLogger = this.createTsLogger(this.minLevel);
|
|
55
54
|
}
|
|
@@ -63,27 +62,19 @@ export class Logger {
|
|
|
63
62
|
logMetaMarkup: string,
|
|
64
63
|
logArgs: unknown[],
|
|
65
64
|
logErrors: string[],
|
|
66
|
-
logMeta
|
|
65
|
+
logMeta?: IMeta
|
|
67
66
|
) => {
|
|
68
67
|
const baseLine = `${logMetaMarkup}${(logArgs as string[]).join(" ")}${logErrors.join("")}`;
|
|
69
68
|
const simpleLine = `${(logArgs as string[]).join(" ")}${logErrors.join("")}`;
|
|
70
|
-
const
|
|
71
|
-
const levelFromMeta =
|
|
72
|
-
typeof meta?.["logLevelId"] === "number"
|
|
73
|
-
? (meta["logLevelId"] as LogLevel)
|
|
74
|
-
: LogLevel.Info;
|
|
75
|
-
|
|
69
|
+
const level = logMeta?.logLevelId as LogLevel ?? LogLevel.info;
|
|
76
70
|
const output = this.detailed ? baseLine : simpleLine;
|
|
77
71
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} else {
|
|
85
|
-
process.stderr.write(output + "\n");
|
|
86
|
-
}
|
|
72
|
+
this.eventEmitter.emit("log", {
|
|
73
|
+
message: output,
|
|
74
|
+
rawMessage: simpleLine,
|
|
75
|
+
level: level,
|
|
76
|
+
timestamp: new Date(),
|
|
77
|
+
} satisfies LogEvent);
|
|
87
78
|
},
|
|
88
79
|
},
|
|
89
80
|
});
|
|
@@ -97,13 +88,6 @@ export class Logger {
|
|
|
97
88
|
return () => this.eventEmitter.off("log", listener);
|
|
98
89
|
}
|
|
99
90
|
|
|
100
|
-
/**
|
|
101
|
-
* Enable or disable TUI mode.
|
|
102
|
-
*/
|
|
103
|
-
setTuiMode(enabled: boolean): void {
|
|
104
|
-
this.tuiMode = enabled;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
91
|
/**
|
|
108
92
|
* Enable or disable detailed log format.
|
|
109
93
|
*/
|
|
@@ -127,35 +111,48 @@ export class Logger {
|
|
|
127
111
|
}
|
|
128
112
|
|
|
129
113
|
// Logging methods
|
|
130
|
-
|
|
131
|
-
this.tsLogger.silly(...args);
|
|
114
|
+
silly(...args: unknown[]): void {
|
|
115
|
+
this.tsLogger.silly(...asStringArray(args));
|
|
132
116
|
}
|
|
133
117
|
|
|
134
118
|
trace(...args: unknown[]): void {
|
|
135
|
-
this.tsLogger.trace(...args);
|
|
119
|
+
this.tsLogger.trace(...asStringArray(args));
|
|
136
120
|
}
|
|
137
121
|
|
|
138
122
|
debug(...args: unknown[]): void {
|
|
139
|
-
this.tsLogger.debug(...args);
|
|
123
|
+
this.tsLogger.debug(...asStringArray(args));
|
|
140
124
|
}
|
|
141
125
|
|
|
142
126
|
info(...args: unknown[]): void {
|
|
143
|
-
this.tsLogger.info(...args);
|
|
127
|
+
this.tsLogger.info(...asStringArray(args));
|
|
144
128
|
}
|
|
145
129
|
|
|
146
130
|
warn(...args: unknown[]): void {
|
|
147
|
-
this.tsLogger.warn(...args);
|
|
131
|
+
this.tsLogger.warn(...asStringArray(args));
|
|
148
132
|
}
|
|
149
133
|
|
|
150
134
|
error(...args: unknown[]): void {
|
|
151
|
-
this.tsLogger.error(...args);
|
|
135
|
+
this.tsLogger.error(...asStringArray(args));
|
|
152
136
|
}
|
|
153
137
|
|
|
154
138
|
fatal(...args: unknown[]): void {
|
|
155
|
-
this.tsLogger.fatal(...args);
|
|
139
|
+
this.tsLogger.fatal(...asStringArray(args));
|
|
156
140
|
}
|
|
157
141
|
}
|
|
158
142
|
|
|
143
|
+
function asStringArray(args: unknown[]): string[] {
|
|
144
|
+
return args.map(arg => {
|
|
145
|
+
if (typeof arg === "string") {
|
|
146
|
+
return arg;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
return JSON.stringify(arg);
|
|
150
|
+
} catch {
|
|
151
|
+
return String(arg);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
159
156
|
/**
|
|
160
157
|
* Create a new logger instance with the given configuration.
|
|
161
158
|
*/
|
package/src/core/registry.ts
CHANGED
|
@@ -72,7 +72,11 @@ export class CommandRegistry {
|
|
|
72
72
|
* @param path Array of command names forming the path
|
|
73
73
|
* @returns Object with resolved command, remaining path, and full path
|
|
74
74
|
*/
|
|
75
|
-
resolve(path: string[]):
|
|
75
|
+
resolve(path: string[]): {
|
|
76
|
+
command: AnyCommand | undefined;
|
|
77
|
+
remainingPath: string[];
|
|
78
|
+
resolvedPath: string[];
|
|
79
|
+
} {
|
|
76
80
|
if (path.length === 0) {
|
|
77
81
|
return { command: undefined, remainingPath: [], resolvedPath: [] };
|
|
78
82
|
}
|
|
@@ -127,14 +131,3 @@ export class CommandRegistry {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
|
|
130
|
-
/**
|
|
131
|
-
* Result of resolving a command path.
|
|
132
|
-
*/
|
|
133
|
-
export interface ResolveResult {
|
|
134
|
-
/** The resolved command, or undefined if not found */
|
|
135
|
-
command: AnyCommand | undefined;
|
|
136
|
-
/** Path elements that couldn't be resolved (remaining after last matched command) */
|
|
137
|
-
remainingPath: string[];
|
|
138
|
-
/** Path elements that were successfully resolved */
|
|
139
|
-
resolvedPath: string[];
|
|
140
|
-
}
|
|
@@ -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
|
|