@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Guide 5: Interactive TUI
|
|
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
|
|
14
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
74
|
-
ctx.logger.info(`Starting task: ${config.task}`);
|
|
75
|
-
|
|
87
|
+
async execute(config: RunConfig, _execCtx: CommandExecutionContext): Promise<CommandResult> {
|
|
76
88
|
if (config.verbose) {
|
|
77
|
-
|
|
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",
|
|
121
|
+
displayName: "🚀 Task Runner", // Shown in TUI header
|
|
107
122
|
version: "1.0.0",
|
|
108
123
|
commands: [new RunCommand()],
|
|
109
|
-
enableTui: 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** (
|
|
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** (
|
|
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",
|
|
151
|
-
order: 1,
|
|
152
|
-
group: "Settings",
|
|
153
|
-
placeholder: "Enter...",
|
|
154
|
-
tuiHidden: false,
|
|
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 (!
|
|
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(
|
|
161
|
-
|
|
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
|
|
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
|
|
111
|
+
const signal = execCtx.signal;
|
|
117
112
|
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
177
|
+
Bun.write(Bun.stdout, `\rProgress: ${percent}% (${mbDownloaded}/${mbTotal} MB)`);
|
|
181
178
|
} else {
|
|
182
179
|
const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
|
|
183
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|