@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
@@ -1,4 +1,4 @@
1
- # Guide 5: Interactive TUI (Normal)
1
+ # Guide 5: Interactive TUI
2
2
 
3
3
  Add an auto-generated Terminal User Interface to your CLI.
4
4
 
@@ -8,20 +8,34 @@ A task runner with both CLI and interactive TUI modes:
8
8
 
9
9
  ```bash
10
10
  # CLI mode
11
- taskr run --task build --env production
11
+ taskr --mode cli run --task build --env production
12
12
 
13
- # TUI mode (interactive)
14
- taskr
13
+ # TUI mode
14
+ # (if your app's default mode is a TUI mode)
15
+ taskr
16
+
17
+ # Force TUI mode
18
+ taskr --mode opentui
19
+ # or
20
+ taskr --mode ink
21
+
22
+ # Force CLI mode
23
+ taskr --mode cli
15
24
  ```
16
25
 
17
- When you run without arguments, an interactive form appears!
26
+ Only the selected mode (`--mode`) or your app's default mode controls whether the app runs in CLI or TUI.
18
27
 
19
28
  ## Step 1: Create the Command with TUI Metadata
20
29
 
21
30
  Create `src/commands/run.ts`:
22
31
 
23
32
  ```typescript
24
- import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
33
+ import {
34
+ Command,
35
+ type OptionSchema,
36
+ type CommandResult,
37
+ type CommandExecutionContext,
38
+ } from "@pablozaiden/terminatui";
25
39
 
26
40
  const options = {
27
41
  task: {
@@ -70,11 +84,9 @@ export class RunCommand extends Command<typeof options, RunConfig> {
70
84
  override readonly displayName = "Run Task";
71
85
  override readonly actionLabel = "Start Task";
72
86
 
73
- async execute(ctx: AppContext, config: RunConfig): Promise<CommandResult> {
74
- ctx.logger.info(`Starting task: ${config.task}`);
75
-
87
+ async execute(config: RunConfig, _execCtx: CommandExecutionContext): Promise<CommandResult> {
76
88
  if (config.verbose) {
77
- ctx.logger.debug(`Environment: ${config.env}`);
89
+ console.debug(`Environment: ${config.env}`);
78
90
  }
79
91
 
80
92
  // Simulate task execution
@@ -82,10 +94,10 @@ export class RunCommand extends Command<typeof options, RunConfig> {
82
94
  await new Promise((resolve) => setTimeout(resolve, 1000));
83
95
  console.log("Task completed!");
84
96
 
85
- return {
86
- success: true,
97
+ return {
98
+ success: true,
87
99
  data: { task: config.task, env: config.env },
88
- message: `Task ${config.task} completed successfully`
100
+ message: `Task ${config.task} completed successfully`,
89
101
  };
90
102
  }
91
103
  }
@@ -100,13 +112,16 @@ import { TuiApplication } from "@pablozaiden/terminatui";
100
112
  import { RunCommand } from "./commands/run";
101
113
 
102
114
  class TaskRunnerApp extends TuiApplication {
115
+ // Default is CLI; each app decides.
116
+ protected override defaultMode = "opentui" as const;
117
+
103
118
  constructor() {
104
119
  super({
105
120
  name: "taskr",
106
- displayName: "🚀 Task Runner", // Shown in TUI header
121
+ displayName: "🚀 Task Runner", // Shown in TUI header
107
122
  version: "1.0.0",
108
123
  commands: [new RunCommand()],
109
- enableTui: true, // Default: true
124
+ enableTui: true, // Default: true
110
125
  });
111
126
  }
112
127
  }
@@ -116,26 +131,26 @@ await new TaskRunnerApp().run();
116
131
 
117
132
  ## Step 3: Test Both Modes
118
133
 
119
- **CLI Mode** (with arguments):
134
+ **CLI Mode** (forced):
120
135
 
121
136
  ```bash
122
- bun src/index.ts run --task build --env production
137
+ bun src/index.ts --mode cli run --task build --env production
123
138
  # Running build in production...
124
139
  # Task completed!
125
140
  ```
126
141
 
127
- **TUI Mode** (no arguments):
142
+ **TUI Mode** (forced):
128
143
 
129
144
  ```bash
130
- bun src/index.ts
145
+ bun src/index.ts --mode opentui
131
146
  ```
132
147
 
133
148
  This opens an interactive interface:
134
149
  - Use ↑/↓ to navigate fields
135
150
  - Press Enter to edit a field
151
+ - Navigate to "CLI Command" button and press Enter to see the CLI command
136
152
  - Press Enter on "Start Task" to run
137
153
  - Press Esc to go back
138
- - Press C to see the CLI command
139
154
 
140
155
  ## TUI Metadata Reference
141
156
 
@@ -145,13 +160,13 @@ Add these properties to your options for TUI customization:
145
160
  {
146
161
  type: "string",
147
162
  description: "...",
148
-
163
+
149
164
  // TUI-specific
150
- label: "Display Label", // Custom field label
151
- order: 1, // Field sort order
152
- group: "Settings", // Group heading
153
- placeholder: "Enter...", // Placeholder text
154
- tuiHidden: false, // Hide from TUI (still in CLI)
165
+ label: "Display Label", // Custom field label
166
+ order: 1, // Field sort order
167
+ group: "Settings", // Group heading
168
+ placeholder: "Enter...", // Placeholder text
169
+ tuiHidden: false, // Hide from TUI (still in CLI)
155
170
  }
156
171
  ```
157
172
 
@@ -161,10 +176,10 @@ Add these properties to your options for TUI customization:
161
176
  class MyCommand extends Command {
162
177
  // Display name in command selector
163
178
  override readonly displayName = "My Command";
164
-
179
+
165
180
  // Button text (default: "Run")
166
181
  override readonly actionLabel = "Execute";
167
-
182
+
168
183
  // Skip config screen, run immediately
169
184
  override readonly immediateExecution = false;
170
185
  }
@@ -175,9 +190,8 @@ class MyCommand extends Command {
175
190
  | Key | Action |
176
191
  |-----|--------|
177
192
  | ↑/↓ | Navigate fields |
178
- | Enter | Edit field / Run |
193
+ | Enter | Edit field / Press button / Run |
179
194
  | Tab | Cycle focus |
180
- | C | Show CLI command |
181
195
  | L | Toggle logs |
182
196
  | Ctrl+Y | Copy to clipboard |
183
197
  | Esc | Back / Cancel |
@@ -185,6 +199,7 @@ class MyCommand extends Command {
185
199
  ## What You Learned
186
200
 
187
201
  - Use `TuiApplication` instead of `Application`
202
+ - Use `--mode` (or app default mode) to control CLI vs TUI
188
203
  - Add TUI metadata to options (label, order, group)
189
204
  - Customize with `displayName` and `actionLabel`
190
205
  - Both CLI and TUI work with the same command
@@ -16,11 +16,9 @@ Create `src/commands/deploy.ts`:
16
16
 
17
17
  ```typescript
18
18
  import path from "node:path";
19
- import { existsSync } from "node:fs";
20
19
  import {
21
20
  Command,
22
21
  ConfigValidationError,
23
- type AppContext,
24
22
  type OptionSchema,
25
23
  type OptionValues,
26
24
  type CommandResult
@@ -84,10 +82,7 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
84
82
  * Transform and validate raw options into DeployConfig.
85
83
  * Runs before execute() - errors here show helpful messages.
86
84
  */
87
- override buildConfig(
88
- _ctx: AppContext,
89
- opts: OptionValues<typeof options>
90
- ): DeployConfig {
85
+ override async buildConfig(opts: OptionValues<typeof options>): Promise<DeployConfig> {
91
86
  // 1. Validate app path exists
92
87
  const appRaw = opts["app"] as string | undefined;
93
88
  if (!appRaw) {
@@ -98,7 +93,7 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
98
93
  }
99
94
 
100
95
  const appPath = path.resolve(appRaw);
101
- if (!existsSync(appPath)) {
96
+ if (!(await Bun.file(appPath).exists())) {
102
97
  throw new ConfigValidationError(
103
98
  `Application path does not exist: ${appPath}`,
104
99
  "app"
@@ -157,11 +152,8 @@ export class DeployCommand extends Command<typeof options, DeployConfig> {
157
152
  * Execute with fully validated DeployConfig.
158
153
  * No need to validate here - buildConfig already did it!
159
154
  */
160
- async execute(ctx: AppContext, config: DeployConfig): Promise<CommandResult> {
161
- ctx.logger.info(`Deploying ${config.appName} to ${config.environment}`);
162
- ctx.logger.debug(`Path: ${config.appPath}`);
163
- ctx.logger.debug(`Replicas: ${config.replicas}`);
164
- ctx.logger.debug(`URL: ${config.envConfig.url}`);
155
+ async execute(config: DeployConfig): Promise<CommandResult> {
156
+ console.log(`Deploying ${config.appName} to ${config.environment}`);
165
157
 
166
158
  if (config.dryRun) {
167
159
  console.log("DRY RUN - Would deploy:");
@@ -23,7 +23,6 @@ import path from "node:path";
23
23
  import {
24
24
  Command,
25
25
  ConfigValidationError,
26
- type AppContext,
27
26
  type OptionSchema,
28
27
  type OptionValues,
29
28
  type CommandResult,
@@ -69,10 +68,7 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
69
68
  readonly displayName = "File Downloader";
70
69
  readonly actionLabel = "Download";
71
70
 
72
- override buildConfig(
73
- _ctx: AppContext,
74
- opts: OptionValues<typeof options>
75
- ): DownloadConfig {
71
+ override buildConfig(opts: OptionValues<typeof options>): DownloadConfig {
76
72
  // Validate URL
77
73
  const urlStr = opts["url"] as string;
78
74
  if (!urlStr) {
@@ -107,16 +103,15 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
107
103
 
108
104
  ```typescript
109
105
  async execute(
110
- ctx: AppContext,
111
106
  config: DownloadConfig,
112
- execCtx?: CommandExecutionContext
107
+ execCtx: CommandExecutionContext
113
108
  ): Promise<CommandResult> {
114
109
  const { url, outputDir, fileName, chunkSize } = config;
115
110
  const outputPath = path.join(outputDir, fileName);
116
- const signal = execCtx?.signal;
111
+ const signal = execCtx.signal;
117
112
 
118
- ctx.logger.info(`Starting download: ${url}`);
119
- ctx.logger.info(`Output: ${outputPath}`);
113
+ console.log(`Starting download: ${url}`);
114
+ console.log(`Output: ${outputPath}`);
120
115
 
121
116
  // Create output directory
122
117
  await Bun.write(path.join(outputDir, ".keep"), "");
@@ -128,7 +123,8 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
128
123
 
129
124
  try {
130
125
  // Check for cancellation before starting
131
- if (signal?.aborted) {
126
+ if (signal.aborted) {
127
+
132
128
  return { success: false, message: "Download cancelled before start" };
133
129
  }
134
130
 
@@ -157,8 +153,9 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
157
153
  // Read chunks with cancellation checks
158
154
  while (true) {
159
155
  // Check for cancellation between chunks
160
- if (signal?.aborted) {
161
- ctx.logger.warn("Download cancelled by user");
156
+ if (signal.aborted) {
157
+
158
+ console.warn("Download cancelled by user");
162
159
  throw new Error("AbortError");
163
160
  }
164
161
 
@@ -177,10 +174,10 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
177
174
  const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
178
175
  const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
179
176
  const mbTotal = (totalBytes / 1024 / 1024).toFixed(2);
180
- process.stdout.write(`\rProgress: ${percent}% (${mbDownloaded}/${mbTotal} MB)`);
177
+ Bun.write(Bun.stdout, `\rProgress: ${percent}% (${mbDownloaded}/${mbTotal} MB)`);
181
178
  } else {
182
179
  const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
183
- process.stdout.write(`\rDownloaded: ${mbDownloaded} MB`);
180
+ Bun.write(Bun.stdout, `\rDownloaded: ${mbDownloaded} MB`);
184
181
  }
185
182
  }
186
183
 
@@ -200,7 +197,8 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
200
197
 
201
198
  } catch (error) {
202
199
  // Handle cancellation
203
- if (signal?.aborted || (error as Error).name === "AbortError") {
200
+ if (signal.aborted || (error as Error).name === "AbortError") {
201
+
204
202
  console.log("\nDownload cancelled.");
205
203
 
206
204
  // Cleanup partial file
@@ -246,58 +244,7 @@ export class DownloadCommand extends Command<typeof options, DownloadConfig> {
246
244
  }
247
245
  ```
248
246
 
249
- ## Step 4: Add Result Rendering
250
-
251
- ```typescript
252
- import React from "react";
253
- import { Text, Box } from "ink";
254
-
255
- export class DownloadCommand extends Command<typeof options, DownloadConfig> {
256
- // ... previous code ...
257
-
258
- /**
259
- * Custom TUI result display
260
- */
261
- override renderResult(result: CommandResult): React.ReactNode {
262
- if (!result.success && result.data?.cancelled) {
263
- return (
264
- <Box flexDirection="column">
265
- <Text color="yellow">⚠ Download Cancelled</Text>
266
- {result.data?.downloadedBytes > 0 && (
267
- <Text dimColor>
268
- Partial: {(result.data.downloadedBytes / 1024 / 1024).toFixed(2)} MB
269
- </Text>
270
- )}
271
- </Box>
272
- );
273
- }
274
-
275
- if (!result.success) {
276
- return <Text color="red">✗ {result.message}</Text>;
277
- }
278
-
279
- return (
280
- <Box flexDirection="column">
281
- <Text color="green">✓ Download Complete</Text>
282
- <Text>File: {result.data?.file}</Text>
283
- <Text>Size: {(result.data?.size / 1024 / 1024).toFixed(2)} MB</Text>
284
- </Box>
285
- );
286
- }
287
-
288
- /**
289
- * Copy file path to clipboard
290
- */
291
- override getClipboardContent(result: CommandResult): string | undefined {
292
- if (result.success && result.data?.file) {
293
- return result.data.file;
294
- }
295
- return undefined;
296
- }
297
- }
298
- ```
299
-
300
- ## Step 5: Create the Application
247
+ ## Step 4: Create the Application
301
248
 
302
249
  Create `src/index.ts`:
303
250
 
@@ -381,7 +328,6 @@ try {
381
328
  - Pass signal to `fetch` and other async APIs
382
329
  - Clean up resources on cancellation
383
330
  - Return meaningful results for cancelled operations
384
- - Render different UI for cancelled vs success vs error
385
331
 
386
332
  ## Next Steps
387
333