@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.
Files changed (175) hide show
  1. package/AGENTS.md +43 -0
  2. package/CLAUDE.md +1 -0
  3. package/README.md +64 -43
  4. package/bun.lock +85 -0
  5. package/examples/tui-app/commands/config/app/get.ts +62 -0
  6. package/examples/tui-app/commands/config/app/index.ts +23 -0
  7. package/examples/tui-app/commands/config/app/set.ts +96 -0
  8. package/examples/tui-app/commands/config/index.ts +28 -0
  9. package/examples/tui-app/commands/config/user/get.ts +61 -0
  10. package/examples/tui-app/commands/config/user/index.ts +23 -0
  11. package/examples/tui-app/commands/config/user/set.ts +57 -0
  12. package/examples/tui-app/commands/greet.ts +14 -11
  13. package/examples/tui-app/commands/math.ts +6 -9
  14. package/examples/tui-app/commands/status.ts +24 -13
  15. package/examples/tui-app/index.ts +7 -3
  16. package/guides/01-hello-world.md +7 -2
  17. package/guides/02-adding-options.md +2 -2
  18. package/guides/03-multiple-commands.md +6 -8
  19. package/guides/04-subcommands.md +8 -8
  20. package/guides/05-interactive-tui.md +45 -30
  21. package/guides/06-config-validation.md +4 -12
  22. package/guides/07-async-cancellation.md +15 -69
  23. package/guides/08-complete-application.md +13 -179
  24. package/guides/README.md +7 -3
  25. package/package.json +4 -8
  26. package/src/__tests__/application.test.ts +87 -68
  27. package/src/__tests__/buildCliCommand.test.ts +99 -119
  28. package/src/__tests__/builtins.test.ts +27 -75
  29. package/src/__tests__/command.test.ts +100 -131
  30. package/src/__tests__/context.test.ts +1 -26
  31. package/src/__tests__/helpCore.test.ts +227 -0
  32. package/src/__tests__/parser.test.ts +98 -244
  33. package/src/__tests__/registry.test.ts +33 -160
  34. package/src/__tests__/schemaToFields.test.ts +75 -158
  35. package/src/builtins/help.ts +19 -4
  36. package/src/builtins/settings.ts +18 -32
  37. package/src/builtins/version.ts +4 -4
  38. package/src/cli/output/colors.ts +1 -1
  39. package/src/cli/parser.ts +26 -95
  40. package/src/core/application.ts +192 -110
  41. package/src/core/command.ts +26 -9
  42. package/src/core/context.ts +31 -20
  43. package/src/core/help.ts +24 -18
  44. package/src/core/knownCommands.ts +13 -0
  45. package/src/core/logger.ts +39 -42
  46. package/src/core/registry.ts +5 -12
  47. package/src/tui/TuiApplication.tsx +63 -120
  48. package/src/tui/TuiRoot.tsx +135 -0
  49. package/src/tui/adapters/factory.ts +19 -0
  50. package/src/tui/adapters/ink/InkRenderer.tsx +135 -0
  51. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  52. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  53. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  54. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  55. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  56. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  57. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  58. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  59. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  60. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  61. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  62. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  63. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  64. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  65. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  66. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  67. package/src/tui/adapters/ink/keyboard.ts +97 -0
  68. package/src/tui/adapters/ink/utils.ts +16 -0
  69. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +115 -0
  70. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  71. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  72. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  73. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  74. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  75. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  76. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  77. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  78. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  79. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  80. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  81. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  82. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  83. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  84. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  85. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  86. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  87. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  88. package/src/tui/adapters/types.ts +70 -0
  89. package/src/tui/components/ActionButton.tsx +0 -36
  90. package/src/tui/components/CommandSelector.tsx +52 -92
  91. package/src/tui/components/ConfigForm.tsx +68 -42
  92. package/src/tui/components/FieldRow.tsx +0 -30
  93. package/src/tui/components/Header.tsx +14 -13
  94. package/src/tui/components/JsonHighlight.tsx +10 -17
  95. package/src/tui/components/ModalBase.tsx +38 -0
  96. package/src/tui/components/ResultsPanel.tsx +27 -36
  97. package/src/tui/components/StatusBar.tsx +24 -39
  98. package/src/tui/components/logColors.ts +12 -0
  99. package/src/tui/context/ClipboardContext.tsx +87 -0
  100. package/src/tui/context/ExecutorContext.tsx +139 -0
  101. package/src/tui/context/KeyboardContext.tsx +85 -71
  102. package/src/tui/context/LogsContext.tsx +35 -0
  103. package/src/tui/context/NavigationContext.tsx +194 -0
  104. package/src/tui/context/RendererContext.tsx +20 -0
  105. package/src/tui/context/TuiAppContext.tsx +58 -0
  106. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  107. package/src/tui/hooks/useBackHandler.ts +34 -0
  108. package/src/tui/hooks/useClipboard.ts +40 -25
  109. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  110. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  111. package/src/tui/modals/CliModal.tsx +82 -0
  112. package/src/tui/modals/EditorModal.tsx +207 -0
  113. package/src/tui/modals/LogsModal.tsx +98 -0
  114. package/src/tui/registry.ts +102 -0
  115. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  116. package/src/tui/screens/ConfigScreen.tsx +160 -0
  117. package/src/tui/screens/ErrorScreen.tsx +58 -0
  118. package/src/tui/screens/ResultsScreen.tsx +60 -0
  119. package/src/tui/screens/RunningScreen.tsx +72 -0
  120. package/src/tui/screens/ScreenBase.ts +6 -0
  121. package/src/tui/semantic/Button.tsx +7 -0
  122. package/src/tui/semantic/Code.tsx +7 -0
  123. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  124. package/src/tui/semantic/Container.tsx +7 -0
  125. package/src/tui/semantic/Field.tsx +7 -0
  126. package/src/tui/semantic/Label.tsx +7 -0
  127. package/src/tui/semantic/MenuButton.tsx +7 -0
  128. package/src/tui/semantic/MenuItem.tsx +7 -0
  129. package/src/tui/semantic/Overlay.tsx +7 -0
  130. package/src/tui/semantic/Panel.tsx +7 -0
  131. package/src/tui/semantic/ScrollView.tsx +9 -0
  132. package/src/tui/semantic/Select.tsx +7 -0
  133. package/src/tui/semantic/Spacer.tsx +7 -0
  134. package/src/tui/semantic/Spinner.tsx +7 -0
  135. package/src/tui/semantic/TextInput.tsx +7 -0
  136. package/src/tui/semantic/Value.tsx +7 -0
  137. package/src/tui/semantic/types.ts +195 -0
  138. package/src/tui/theme.ts +25 -14
  139. package/src/tui/utils/buildCliCommand.ts +1 -0
  140. package/src/tui/utils/getEnumKeys.ts +3 -0
  141. package/src/tui/utils/parameterPersistence.ts +1 -0
  142. package/src/types/command.ts +0 -60
  143. package/examples/tui-app/commands/index.ts +0 -3
  144. package/src/__tests__/colors.test.ts +0 -127
  145. package/src/__tests__/commandClass.test.ts +0 -130
  146. package/src/__tests__/help.test.ts +0 -412
  147. package/src/__tests__/registryNew.test.ts +0 -160
  148. package/src/__tests__/table.test.ts +0 -146
  149. package/src/__tests__/tui.test.ts +0 -26
  150. package/src/builtins/index.ts +0 -4
  151. package/src/cli/help.ts +0 -174
  152. package/src/cli/index.ts +0 -3
  153. package/src/cli/output/index.ts +0 -2
  154. package/src/cli/output/table.ts +0 -141
  155. package/src/commands/help.ts +0 -50
  156. package/src/commands/index.ts +0 -1
  157. package/src/components/index.ts +0 -147
  158. package/src/core/index.ts +0 -15
  159. package/src/hooks/index.ts +0 -131
  160. package/src/index.ts +0 -137
  161. package/src/registry/commandRegistry.ts +0 -77
  162. package/src/registry/index.ts +0 -1
  163. package/src/tui/TuiApp.tsx +0 -582
  164. package/src/tui/app.ts +0 -29
  165. package/src/tui/components/CliModal.tsx +0 -81
  166. package/src/tui/components/EditorModal.tsx +0 -177
  167. package/src/tui/components/LogsPanel.tsx +0 -86
  168. package/src/tui/components/index.ts +0 -13
  169. package/src/tui/context/index.ts +0 -7
  170. package/src/tui/hooks/index.ts +0 -35
  171. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  172. package/src/tui/hooks/useLogStream.ts +0 -96
  173. package/src/tui/index.ts +0 -65
  174. package/src/tui/utils/index.ts +0 -13
  175. 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 list.
64
+ * Format an options schema into a help section.
63
65
  */
64
- export function formatOptions(command: AnyCommand): string {
65
- if (!command.options || Object.keys(command.options).length === 0) return "";
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(command.options).map(([name, defUntyped]) => {
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
- const typeHint = colors.dim(` <${def.type}>`);
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("Options:"), ...entries].join("\n");
83
+ return [colors.bold(title + ":"), ...entries].join("\n");
81
84
  }
82
85
 
83
86
  /**
84
- * Format global options section (available on all commands).
87
+ * Format options list.
85
88
  */
86
- export function formatGlobalOptions(): string {
87
- const entries = [
88
- ` ${colors.yellow(" --log-level")}${colors.dim(" <string>")}\n Set minimum log level${colors.dim(" [silly | trace | debug | info | warn | error | fatal]")}`,
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
- sections.push(`\n${formatGlobalOptions()}`);
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
+ ]);
@@ -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
- Silly = 0,
9
- Trace = 1,
10
- Debug = 2,
11
- Info = 3,
12
- Warn = 4,
13
- Error = 5,
14
- Fatal = 6,
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.Info;
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: unknown
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 meta = logMeta as Record<string, unknown>;
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
- if (this.tuiMode) {
79
- this.eventEmitter.emit("log", {
80
- message: output,
81
- level: levelFromMeta,
82
- timestamp: new Date(),
83
- } satisfies LogEvent);
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
- silly(...args: unknown[]): void {
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
  */
@@ -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[]): ResolveResult {
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 { createCliRenderer } from "@opentui/core";
2
- import { createRoot } from "@opentui/react";
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 { TuiApp } from "./TuiApp.tsx";
6
- import { Theme } from "./theme.ts";
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 interactive TUI mode */
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 interactively or with the --interactive flag.
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(process.argv.slice(2));
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(argv: string[] = process.argv.slice(2)): Promise<void> {
78
- // Check for --interactive or -i flag
79
- const hasInteractiveFlag = argv.includes("--interactive") || argv.includes("-i");
80
- const filteredArgs = argv.filter((arg) => arg !== "--interactive" && arg !== "-i");
81
-
82
- // Launch TUI if:
83
- // 1. Explicit --interactive flag, or
84
- // 2. No args and TUI is enabled
85
- if (hasInteractiveFlag || (filteredArgs.length === 0 && this.enableTui)) {
86
- await this.runTui();
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
- // Otherwise run CLI mode
91
- await super.run(filteredArgs);
83
+ throw new Error(`Unknown mode '${resolvedMode}'`);
92
84
  }
93
85
 
94
86
  /**
95
- * Launch the interactive TUI.
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
- // Enable TUI mode on the logger so logs go to the event emitter
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
- const root = createRoot(renderer);
130
- root.render(
131
- <TuiApp
132
- name={this.name}
133
- displayName={this.displayName}
134
- version={this.version}
135
- commands={commands}
136
- context={this.context}
137
- logSource={logSource}
138
- customFields={this.customFields}
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, "settings");
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 = Object.entries(CoreLogLevel).find(
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
- this.context.logger.setMinLevel(level);
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
- this.context.logger.setDetailed(Boolean(settings["detailed-logs"]));
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 version and help from main menu
216
- if (cmd.name === "version" || cmd.name === "help") {
157
+ // Exclude internal/built-in commands from the TUI main menu
158
+ if (cmd.tuiHidden) {
217
159
  return false;
218
160
  }
219
- // Exclude settings if already defined by user (they shouldn't)
220
- if (cmd.name === "settings") {
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
- // Include commands that have options or execute methods
166
+
224
167
  return true;
225
168
  });
226
169