@pablozaiden/terminatui 0.1.0
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/.devcontainer/devcontainer.json +19 -0
- package/.devcontainer/install-prerequisites.sh +49 -0
- package/.github/workflows/copilot-setup-steps.yml +32 -0
- package/.github/workflows/pull-request.yml +27 -0
- package/.github/workflows/release-npm-package.yml +78 -0
- package/LICENSE +21 -0
- package/README.md +524 -0
- package/examples/tui-app/commands/greet.ts +75 -0
- package/examples/tui-app/commands/index.ts +3 -0
- package/examples/tui-app/commands/math.ts +114 -0
- package/examples/tui-app/commands/status.ts +75 -0
- package/examples/tui-app/index.ts +34 -0
- package/guides/01-hello-world.md +96 -0
- package/guides/02-adding-options.md +103 -0
- package/guides/03-multiple-commands.md +163 -0
- package/guides/04-subcommands.md +206 -0
- package/guides/05-interactive-tui.md +194 -0
- package/guides/06-config-validation.md +264 -0
- package/guides/07-async-cancellation.md +388 -0
- package/guides/08-complete-application.md +673 -0
- package/guides/README.md +74 -0
- package/package.json +32 -0
- package/src/__tests__/application.test.ts +425 -0
- package/src/__tests__/buildCliCommand.test.ts +125 -0
- package/src/__tests__/builtins.test.ts +133 -0
- package/src/__tests__/colors.test.ts +127 -0
- package/src/__tests__/command.test.ts +157 -0
- package/src/__tests__/commandClass.test.ts +130 -0
- package/src/__tests__/context.test.ts +97 -0
- package/src/__tests__/help.test.ts +412 -0
- package/src/__tests__/parser.test.ts +268 -0
- package/src/__tests__/registry.test.ts +195 -0
- package/src/__tests__/registryNew.test.ts +160 -0
- package/src/__tests__/schemaToFields.test.ts +176 -0
- package/src/__tests__/table.test.ts +146 -0
- package/src/__tests__/tui.test.ts +26 -0
- package/src/builtins/help.ts +85 -0
- package/src/builtins/index.ts +4 -0
- package/src/builtins/settings.ts +106 -0
- package/src/builtins/version.ts +72 -0
- package/src/cli/help.ts +174 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/output/colors.ts +74 -0
- package/src/cli/output/index.ts +2 -0
- package/src/cli/output/table.ts +141 -0
- package/src/cli/parser.ts +241 -0
- package/src/commands/help.ts +50 -0
- package/src/commands/index.ts +1 -0
- package/src/components/index.ts +147 -0
- package/src/core/application.ts +461 -0
- package/src/core/command.ts +269 -0
- package/src/core/context.ts +112 -0
- package/src/core/help.ts +214 -0
- package/src/core/index.ts +15 -0
- package/src/core/logger.ts +164 -0
- package/src/core/registry.ts +140 -0
- package/src/hooks/index.ts +131 -0
- package/src/index.ts +137 -0
- package/src/registry/commandRegistry.ts +77 -0
- package/src/registry/index.ts +1 -0
- package/src/tui/TuiApp.tsx +582 -0
- package/src/tui/TuiApplication.tsx +230 -0
- package/src/tui/app.ts +29 -0
- package/src/tui/components/ActionButton.tsx +36 -0
- package/src/tui/components/CliModal.tsx +81 -0
- package/src/tui/components/CommandSelector.tsx +159 -0
- package/src/tui/components/ConfigForm.tsx +148 -0
- package/src/tui/components/EditorModal.tsx +177 -0
- package/src/tui/components/FieldRow.tsx +30 -0
- package/src/tui/components/Header.tsx +31 -0
- package/src/tui/components/JsonHighlight.tsx +128 -0
- package/src/tui/components/LogsPanel.tsx +86 -0
- package/src/tui/components/ResultsPanel.tsx +93 -0
- package/src/tui/components/StatusBar.tsx +59 -0
- package/src/tui/components/index.ts +13 -0
- package/src/tui/components/types.ts +30 -0
- package/src/tui/context/KeyboardContext.tsx +118 -0
- package/src/tui/context/index.ts +7 -0
- package/src/tui/hooks/index.ts +35 -0
- package/src/tui/hooks/useClipboard.ts +66 -0
- package/src/tui/hooks/useCommandExecutor.ts +131 -0
- package/src/tui/hooks/useConfigState.ts +171 -0
- package/src/tui/hooks/useKeyboardHandler.ts +91 -0
- package/src/tui/hooks/useLogStream.ts +96 -0
- package/src/tui/hooks/useSpinner.ts +46 -0
- package/src/tui/index.ts +65 -0
- package/src/tui/theme.ts +21 -0
- package/src/tui/utils/buildCliCommand.ts +90 -0
- package/src/tui/utils/index.ts +13 -0
- package/src/tui/utils/parameterPersistence.ts +96 -0
- package/src/tui/utils/schemaToFields.ts +144 -0
- package/src/types/command.ts +103 -0
- package/src/types/execution.ts +11 -0
- package/src/types/index.ts +1 -0
- package/tsconfig.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
# @pablozaiden/terminatui
|
|
2
|
+
|
|
3
|
+
A type-safe, class-based framework for building CLI and TUI applications in TypeScript.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Type-safe CLI parsing** - Define options with schemas that provide full TypeScript types
|
|
8
|
+
- **Class-based architecture** - Extend `Command` and `Application` classes for structured apps
|
|
9
|
+
- **Unified execution** - Single `execute()` method handles both CLI and TUI modes
|
|
10
|
+
- **Auto-generated TUI** - Interactive terminal UI generated from command definitions
|
|
11
|
+
- **Built-in commands** - Automatic `help` and `version` commands
|
|
12
|
+
- **Nested subcommands** - Hierarchical command structures with path resolution
|
|
13
|
+
- **Lifecycle hooks** - `beforeExecute()` and `afterExecute()` hooks on commands
|
|
14
|
+
- **Service container** - `AppContext` provides dependency injection for services
|
|
15
|
+
- **Integrated logging** - Logger with TUI-aware output handling
|
|
16
|
+
- **Cancellation support** - AbortSignal-based cancellation for long-running commands
|
|
17
|
+
- **Config validation** - `buildConfig()` hook for transforming and validating options
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun add @pablozaiden/terminatui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Define a Command
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { Command, type AppContext, type OptionSchema, type CommandResult } from "@pablozaiden/terminatui";
|
|
31
|
+
|
|
32
|
+
const greetOptions = {
|
|
33
|
+
name: {
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Name to greet",
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
loud: {
|
|
39
|
+
type: "boolean",
|
|
40
|
+
description: "Use uppercase",
|
|
41
|
+
alias: "l",
|
|
42
|
+
default: false,
|
|
43
|
+
},
|
|
44
|
+
} satisfies OptionSchema;
|
|
45
|
+
|
|
46
|
+
class GreetCommand extends Command<typeof greetOptions> {
|
|
47
|
+
readonly name = "greet";
|
|
48
|
+
readonly description = "Greet someone";
|
|
49
|
+
readonly options = greetOptions;
|
|
50
|
+
|
|
51
|
+
override execute(ctx: AppContext, config: { name: string; loud: boolean }): CommandResult {
|
|
52
|
+
const message = `Hello, ${config.name}!`;
|
|
53
|
+
console.log(config.loud ? message.toUpperCase() : message);
|
|
54
|
+
return { success: true, message };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. Create an Application
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
import { Application } from "@pablozaiden/terminatui";
|
|
63
|
+
|
|
64
|
+
class MyApp extends Application {
|
|
65
|
+
constructor() {
|
|
66
|
+
super({
|
|
67
|
+
name: "myapp",
|
|
68
|
+
version: "1.0.0",
|
|
69
|
+
description: "My awesome CLI app",
|
|
70
|
+
commands: [new GreetCommand()],
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. Run the Application
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// index.ts
|
|
80
|
+
await new MyApp().run();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Usage
|
|
85
|
+
myapp greet --name World
|
|
86
|
+
# Output: Hello, World!
|
|
87
|
+
|
|
88
|
+
myapp greet --name World --loud
|
|
89
|
+
# Output: HELLO, WORLD!
|
|
90
|
+
|
|
91
|
+
myapp help
|
|
92
|
+
# Shows all commands
|
|
93
|
+
|
|
94
|
+
myapp greet help
|
|
95
|
+
# Shows greet command options
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Core Concepts
|
|
99
|
+
|
|
100
|
+
### Command
|
|
101
|
+
|
|
102
|
+
The `Command` abstract class is the base for all commands:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
abstract class Command<TOptions extends OptionSchema = OptionSchema, TConfig = unknown> {
|
|
106
|
+
abstract readonly name: string;
|
|
107
|
+
abstract readonly description: string;
|
|
108
|
+
abstract readonly options: TOptions;
|
|
109
|
+
|
|
110
|
+
// Optional properties
|
|
111
|
+
displayName?: string; // Human-readable name for TUI
|
|
112
|
+
subCommands?: Command[]; // Nested subcommands
|
|
113
|
+
aliases?: string[]; // Command aliases
|
|
114
|
+
hidden?: boolean; // Hide from help
|
|
115
|
+
examples?: CommandExample[]; // Usage examples
|
|
116
|
+
longDescription?: string; // Extended description
|
|
117
|
+
|
|
118
|
+
// TUI customization
|
|
119
|
+
actionLabel?: string; // Button text (default: "Run")
|
|
120
|
+
immediateExecution?: boolean; // Execute on selection without config
|
|
121
|
+
|
|
122
|
+
// Required: Main execution method
|
|
123
|
+
abstract execute(
|
|
124
|
+
ctx: AppContext,
|
|
125
|
+
config: TConfig,
|
|
126
|
+
execCtx?: CommandExecutionContext
|
|
127
|
+
): Promise<CommandResult | void> | CommandResult | void;
|
|
128
|
+
|
|
129
|
+
// Optional: Transform/validate options before execute
|
|
130
|
+
buildConfig?(ctx: AppContext, opts: OptionValues<TOptions>): TConfig | Promise<TConfig>;
|
|
131
|
+
|
|
132
|
+
// Optional: Custom result rendering for TUI
|
|
133
|
+
renderResult?(result: CommandResult): ReactNode;
|
|
134
|
+
|
|
135
|
+
// Optional: Custom clipboard content
|
|
136
|
+
getClipboardContent?(result: CommandResult): string | undefined;
|
|
137
|
+
|
|
138
|
+
// Optional: Handle config changes in TUI form
|
|
139
|
+
onConfigChange?(key: string, value: unknown, allValues: Record<string, unknown>): Record<string, unknown> | undefined;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### CommandExecutionContext
|
|
144
|
+
|
|
145
|
+
Provides execution context including cancellation support:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
interface CommandExecutionContext {
|
|
149
|
+
signal: AbortSignal; // For cancellation
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### CommandResult
|
|
154
|
+
|
|
155
|
+
Commands should return a `CommandResult` from `execute()`:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
interface CommandResult {
|
|
159
|
+
success: boolean;
|
|
160
|
+
data?: unknown; // Result data
|
|
161
|
+
error?: string; // Error message if failed
|
|
162
|
+
message?: string; // User-friendly message
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Application
|
|
167
|
+
|
|
168
|
+
The `Application` class manages command registration and execution:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
class Application {
|
|
172
|
+
constructor(config: ApplicationConfig);
|
|
173
|
+
|
|
174
|
+
run(args?: string[]): Promise<void>;
|
|
175
|
+
getContext(): AppContext;
|
|
176
|
+
|
|
177
|
+
// Lifecycle hooks (override in subclass)
|
|
178
|
+
onBeforeRun?(command: Command, options: Record<string, unknown>): void;
|
|
179
|
+
onAfterRun?(command: Command, result: unknown): void;
|
|
180
|
+
onError?(error: Error): void;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface ApplicationConfig {
|
|
184
|
+
name: string;
|
|
185
|
+
version: string;
|
|
186
|
+
commitHash?: string; // Git commit for version display
|
|
187
|
+
description?: string;
|
|
188
|
+
commands: Command[];
|
|
189
|
+
defaultCommand?: string;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### AppContext
|
|
194
|
+
|
|
195
|
+
Access application-wide services and configuration:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
import { AppContext } from "@pablozaiden/terminatui";
|
|
199
|
+
|
|
200
|
+
// Get the current context (set during Application.run())
|
|
201
|
+
const ctx = AppContext.current;
|
|
202
|
+
|
|
203
|
+
// Access the logger
|
|
204
|
+
ctx.logger.info("Hello");
|
|
205
|
+
ctx.logger.warn("Warning");
|
|
206
|
+
ctx.logger.error("Error");
|
|
207
|
+
|
|
208
|
+
// Access app config
|
|
209
|
+
console.log(ctx.config.name, ctx.config.version);
|
|
210
|
+
|
|
211
|
+
// Register and retrieve services
|
|
212
|
+
ctx.setService("myService", myServiceInstance);
|
|
213
|
+
const service = ctx.requireService<MyService>("myService");
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### OptionSchema
|
|
217
|
+
|
|
218
|
+
Define typed options for commands:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
interface OptionDef {
|
|
222
|
+
type: "string" | "boolean" | "number" | "array";
|
|
223
|
+
description: string;
|
|
224
|
+
required?: boolean;
|
|
225
|
+
default?: unknown;
|
|
226
|
+
alias?: string;
|
|
227
|
+
enum?: readonly string[]; // For string type, restrict to values
|
|
228
|
+
|
|
229
|
+
// TUI metadata
|
|
230
|
+
label?: string; // Custom label in form
|
|
231
|
+
order?: number; // Field ordering
|
|
232
|
+
group?: string; // Group fields together
|
|
233
|
+
placeholder?: string; // Placeholder text
|
|
234
|
+
tuiHidden?: boolean; // Hide from TUI form
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
type OptionSchema = Record<string, OptionDef>;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## Config Validation with buildConfig
|
|
241
|
+
|
|
242
|
+
Use `buildConfig()` to transform and validate options before execution:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { Command, ConfigValidationError, type AppContext, type OptionValues } from "@pablozaiden/terminatui";
|
|
246
|
+
|
|
247
|
+
interface MyConfig {
|
|
248
|
+
resolvedPath: string;
|
|
249
|
+
count: number;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
class MyCommand extends Command<typeof myOptions, MyConfig> {
|
|
253
|
+
readonly name = "mycommand";
|
|
254
|
+
readonly description = "Do something";
|
|
255
|
+
readonly options = myOptions;
|
|
256
|
+
|
|
257
|
+
override buildConfig(ctx: AppContext, opts: OptionValues<typeof myOptions>): MyConfig {
|
|
258
|
+
const pathRaw = opts["path"] as string | undefined;
|
|
259
|
+
if (!pathRaw) {
|
|
260
|
+
throw new ConfigValidationError("Missing required option: path", "path");
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const count = parseInt(opts["count"] as string ?? "1", 10);
|
|
264
|
+
if (isNaN(count) || count <= 0) {
|
|
265
|
+
throw new ConfigValidationError("Count must be a positive integer", "count");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
resolvedPath: path.resolve(pathRaw),
|
|
270
|
+
count,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
override async execute(ctx: AppContext, config: MyConfig): Promise<CommandResult> {
|
|
275
|
+
// config is now typed as MyConfig
|
|
276
|
+
ctx.logger.info(`Processing ${config.count} items from ${config.resolvedPath}`);
|
|
277
|
+
return { success: true };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Cancellation Support
|
|
283
|
+
|
|
284
|
+
Commands can support cancellation via AbortSignal:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
class LongRunningCommand extends Command<typeof options> {
|
|
288
|
+
// ...
|
|
289
|
+
|
|
290
|
+
override async execute(
|
|
291
|
+
ctx: AppContext,
|
|
292
|
+
config: Config,
|
|
293
|
+
execCtx?: CommandExecutionContext
|
|
294
|
+
): Promise<CommandResult> {
|
|
295
|
+
for (const item of items) {
|
|
296
|
+
// Check for cancellation
|
|
297
|
+
if (execCtx?.signal.aborted) {
|
|
298
|
+
throw new AbortError("Command was cancelled");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await processItem(item, execCtx?.signal);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { success: true };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Subcommands
|
|
310
|
+
|
|
311
|
+
Commands can have nested subcommands:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
class DbCommand extends Command {
|
|
315
|
+
name = "db";
|
|
316
|
+
description = "Database operations";
|
|
317
|
+
|
|
318
|
+
subCommands = [
|
|
319
|
+
new DbMigrateCommand(),
|
|
320
|
+
new DbSeedCommand(),
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Usage: myapp db migrate
|
|
325
|
+
// myapp db seed
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## Built-in Commands
|
|
329
|
+
|
|
330
|
+
The framework automatically injects:
|
|
331
|
+
|
|
332
|
+
- **`help`** - Shows command help (injected into every command as subcommand)
|
|
333
|
+
- **`version`** - Shows app version (top-level only)
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
myapp help # App-level help
|
|
337
|
+
myapp greet help # Command-level help
|
|
338
|
+
myapp version # Shows version
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## TUI Mode
|
|
342
|
+
|
|
343
|
+
Terminatui provides built-in TUI (Terminal User Interface) support that automatically generates interactive UIs from your command definitions.
|
|
344
|
+
|
|
345
|
+
### TuiApplication
|
|
346
|
+
|
|
347
|
+
Extend `TuiApplication` instead of `Application` to get automatic TUI support:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
import { TuiApplication, Command } from "@pablozaiden/terminatui";
|
|
351
|
+
|
|
352
|
+
class MyApp extends TuiApplication {
|
|
353
|
+
constructor() {
|
|
354
|
+
super({
|
|
355
|
+
name: "myapp",
|
|
356
|
+
displayName: "🚀 My App", // Human-readable name for TUI header
|
|
357
|
+
version: "1.0.0",
|
|
358
|
+
commands: [new RunCommand(), new ConfigCommand()],
|
|
359
|
+
enableTui: true, // default: true
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
When run with no arguments, the app launches an interactive TUI instead of showing help:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
myapp # Launches TUI
|
|
369
|
+
myapp run --verbose # Runs in CLI mode
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### TUI Metadata
|
|
373
|
+
|
|
374
|
+
Add TUI-specific metadata to your option schemas to customize the UI:
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
const myOptions = {
|
|
378
|
+
repo: {
|
|
379
|
+
type: "string",
|
|
380
|
+
description: "Repository path",
|
|
381
|
+
required: true,
|
|
382
|
+
// TUI metadata
|
|
383
|
+
label: "Repository", // Custom label in form
|
|
384
|
+
order: 1, // Field ordering
|
|
385
|
+
group: "Required", // Group fields together
|
|
386
|
+
placeholder: "/path", // Placeholder text
|
|
387
|
+
tuiHidden: false, // Hide from TUI form
|
|
388
|
+
},
|
|
389
|
+
verbose: {
|
|
390
|
+
type: "boolean",
|
|
391
|
+
description: "Verbose output",
|
|
392
|
+
label: "Verbose Mode",
|
|
393
|
+
order: 10,
|
|
394
|
+
group: "Options",
|
|
395
|
+
},
|
|
396
|
+
} satisfies OptionSchema;
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### Command TUI Properties
|
|
400
|
+
|
|
401
|
+
Commands can customize their TUI behavior:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
class RunCommand extends Command<typeof runOptions, RunConfig> {
|
|
405
|
+
readonly name = "run";
|
|
406
|
+
override readonly displayName = "Run Task"; // Shown in command selector
|
|
407
|
+
readonly description = "Run the task";
|
|
408
|
+
readonly options = runOptions;
|
|
409
|
+
|
|
410
|
+
// TUI customization
|
|
411
|
+
override readonly actionLabel = "Start Run"; // Button text
|
|
412
|
+
override readonly immediateExecution = false; // Run immediately on selection
|
|
413
|
+
|
|
414
|
+
// Return structured results for display
|
|
415
|
+
override async execute(ctx: AppContext, config: RunConfig): Promise<CommandResult> {
|
|
416
|
+
const result = await runTask(config);
|
|
417
|
+
return {
|
|
418
|
+
success: true,
|
|
419
|
+
data: result,
|
|
420
|
+
message: "Task completed"
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Custom result rendering (React/TSX)
|
|
425
|
+
override renderResult(result: CommandResult): ReactNode {
|
|
426
|
+
return <MyCustomResultView data={result.data} />;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Content for clipboard (Ctrl+Y in results view)
|
|
430
|
+
override getClipboardContent(result: CommandResult): string | undefined {
|
|
431
|
+
return JSON.stringify(result.data, null, 2);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// React to config changes in the TUI form
|
|
435
|
+
override onConfigChange(
|
|
436
|
+
key: string,
|
|
437
|
+
value: unknown,
|
|
438
|
+
allValues: Record<string, unknown>
|
|
439
|
+
): Record<string, unknown> | undefined {
|
|
440
|
+
if (key === "preset" && value === "fast") {
|
|
441
|
+
return { iterations: 1, parallel: true };
|
|
442
|
+
}
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### TUI Features
|
|
449
|
+
|
|
450
|
+
The built-in TUI provides:
|
|
451
|
+
|
|
452
|
+
- **Command Selector** - Navigate and select commands with arrow keys
|
|
453
|
+
- **Config Form** - Auto-generated forms from option schemas with field groups
|
|
454
|
+
- **Field Editor** - Edit field values (text, number, boolean, enum)
|
|
455
|
+
- **CLI Modal** - View equivalent CLI command (press `C`)
|
|
456
|
+
- **Results Panel** - Display command results with custom rendering
|
|
457
|
+
- **Logs Panel** - View application logs in real-time
|
|
458
|
+
- **Clipboard Support** - Centralized copy with Ctrl+Y
|
|
459
|
+
- **Cancellation** - Cancel running commands with Esc
|
|
460
|
+
- **Parameter Persistence** - Remembers last-used values per command
|
|
461
|
+
|
|
462
|
+
### Keyboard Shortcuts
|
|
463
|
+
|
|
464
|
+
| Key | Action |
|
|
465
|
+
|-----|--------|
|
|
466
|
+
| ↑/↓ | Navigate fields/commands |
|
|
467
|
+
| Enter | Edit field / Execute command |
|
|
468
|
+
| Tab | Cycle focus between panels |
|
|
469
|
+
| C | Show CLI command modal |
|
|
470
|
+
| L | Toggle logs panel |
|
|
471
|
+
| Ctrl+Y | Copy current content to clipboard |
|
|
472
|
+
| Esc | Back / Cancel running command |
|
|
473
|
+
|
|
474
|
+
### TUI Utilities
|
|
475
|
+
|
|
476
|
+
The package exports utilities for building custom TUI components:
|
|
477
|
+
|
|
478
|
+
```typescript
|
|
479
|
+
import {
|
|
480
|
+
// Form utilities
|
|
481
|
+
schemaToFieldConfigs, // Convert OptionSchema to form fields
|
|
482
|
+
getFieldDisplayValue, // Format field values for display
|
|
483
|
+
buildCliCommand, // Build CLI command from config
|
|
484
|
+
|
|
485
|
+
// Hooks
|
|
486
|
+
useKeyboardHandler, // Register keyboard handlers with priority
|
|
487
|
+
useClipboard, // Clipboard operations with OSC 52
|
|
488
|
+
useLogStream, // Stream logs from logger
|
|
489
|
+
useSpinner, // Animated spinner
|
|
490
|
+
useCommandExecutor, // Execute commands with cancellation
|
|
491
|
+
|
|
492
|
+
// Context
|
|
493
|
+
KeyboardProvider, // Keyboard context provider
|
|
494
|
+
KeyboardPriority, // Global < Focused < Modal
|
|
495
|
+
|
|
496
|
+
// Components
|
|
497
|
+
Theme, // TUI color theme
|
|
498
|
+
JsonHighlight, // Syntax-highlighted JSON display
|
|
499
|
+
} from "@pablozaiden/terminatui";
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
## Output Formatting
|
|
503
|
+
|
|
504
|
+
Terminatui includes utilities for formatted CLI output:
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
import { colors, table, bulletList, keyValueList } from "@pablozaiden/terminatui";
|
|
508
|
+
|
|
509
|
+
// Colors
|
|
510
|
+
console.log(colors.red("Error!"));
|
|
511
|
+
console.log(colors.success("Done!")); // ✓ Done!
|
|
512
|
+
console.log(colors.bold(colors.blue("Title")));
|
|
513
|
+
|
|
514
|
+
// Tables
|
|
515
|
+
console.log(table(data, ["name", "value", "status"]));
|
|
516
|
+
|
|
517
|
+
// Lists
|
|
518
|
+
console.log(bulletList(["Item 1", "Item 2", "Item 3"]));
|
|
519
|
+
console.log(keyValueList({ name: "Test", count: 42 }));
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
## License
|
|
523
|
+
|
|
524
|
+
MIT
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Command,
|
|
3
|
+
type AppContext,
|
|
4
|
+
type OptionSchema,
|
|
5
|
+
type OptionValues,
|
|
6
|
+
type CommandResult
|
|
7
|
+
} from "../../../src/index.ts";
|
|
8
|
+
|
|
9
|
+
const greetOptions = {
|
|
10
|
+
name: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Name to greet",
|
|
13
|
+
required: true,
|
|
14
|
+
label: "Name",
|
|
15
|
+
order: 1,
|
|
16
|
+
group: "Required",
|
|
17
|
+
placeholder: "Enter name...",
|
|
18
|
+
},
|
|
19
|
+
loud: {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
description: "Use uppercase",
|
|
22
|
+
alias: "l",
|
|
23
|
+
default: false,
|
|
24
|
+
label: "Loud Mode",
|
|
25
|
+
order: 2,
|
|
26
|
+
group: "Options",
|
|
27
|
+
},
|
|
28
|
+
times: {
|
|
29
|
+
type: "number",
|
|
30
|
+
description: "Number of times to greet",
|
|
31
|
+
default: 1,
|
|
32
|
+
label: "Repeat Count",
|
|
33
|
+
order: 3,
|
|
34
|
+
group: "Options",
|
|
35
|
+
},
|
|
36
|
+
} as const satisfies OptionSchema;
|
|
37
|
+
|
|
38
|
+
export class GreetCommand extends Command<typeof greetOptions> {
|
|
39
|
+
readonly name = "greet";
|
|
40
|
+
readonly description = "Greet someone with a friendly message";
|
|
41
|
+
readonly options = greetOptions;
|
|
42
|
+
|
|
43
|
+
override readonly actionLabel = "Say Hello";
|
|
44
|
+
|
|
45
|
+
override readonly examples = [
|
|
46
|
+
{ command: "greet --name World", description: "Simple greeting" },
|
|
47
|
+
{ command: "greet --name World --loud --times 3", description: "Loud greeting 3 times" },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
override async execute(ctx: AppContext, opts: OptionValues<typeof greetOptions>): Promise<CommandResult> {
|
|
51
|
+
const greeting = this.createGreeting(opts);
|
|
52
|
+
ctx.logger.info(greeting);
|
|
53
|
+
return {
|
|
54
|
+
success: true,
|
|
55
|
+
data: { greeting, timestamp: new Date().toISOString() },
|
|
56
|
+
message: greeting,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override getClipboardContent(result: CommandResult): string | undefined {
|
|
61
|
+
const data = result.data as { greeting?: string } | undefined;
|
|
62
|
+
return data?.greeting;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private createGreeting(opts: OptionValues<typeof greetOptions>): string {
|
|
66
|
+
const name = opts.name as string;
|
|
67
|
+
const loud = opts.loud as boolean;
|
|
68
|
+
const times = (opts.times as number) || 1;
|
|
69
|
+
|
|
70
|
+
let message = `Hello, ${name}!`;
|
|
71
|
+
if (loud) message = message.toUpperCase();
|
|
72
|
+
|
|
73
|
+
return Array(times).fill(message).join("\n");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Command,
|
|
3
|
+
type AppContext,
|
|
4
|
+
type OptionSchema,
|
|
5
|
+
type OptionValues,
|
|
6
|
+
type CommandResult
|
|
7
|
+
} from "../../../src/index.ts";
|
|
8
|
+
|
|
9
|
+
const mathOptions = {
|
|
10
|
+
operation: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "Math operation to perform",
|
|
13
|
+
required: true,
|
|
14
|
+
enum: ["add", "subtract", "multiply", "divide"] as const,
|
|
15
|
+
label: "Operation",
|
|
16
|
+
order: 1,
|
|
17
|
+
group: "Required",
|
|
18
|
+
},
|
|
19
|
+
a: {
|
|
20
|
+
type: "number",
|
|
21
|
+
description: "First number",
|
|
22
|
+
required: true,
|
|
23
|
+
label: "First Number",
|
|
24
|
+
order: 2,
|
|
25
|
+
group: "Required",
|
|
26
|
+
},
|
|
27
|
+
b: {
|
|
28
|
+
type: "number",
|
|
29
|
+
description: "Second number",
|
|
30
|
+
required: true,
|
|
31
|
+
label: "Second Number",
|
|
32
|
+
order: 3,
|
|
33
|
+
group: "Required",
|
|
34
|
+
},
|
|
35
|
+
showSteps: {
|
|
36
|
+
type: "boolean",
|
|
37
|
+
description: "Show calculation steps",
|
|
38
|
+
default: false,
|
|
39
|
+
label: "Show Steps",
|
|
40
|
+
order: 4,
|
|
41
|
+
group: "Options",
|
|
42
|
+
},
|
|
43
|
+
} as const satisfies OptionSchema;
|
|
44
|
+
|
|
45
|
+
export class MathCommand extends Command<typeof mathOptions> {
|
|
46
|
+
readonly name = "math";
|
|
47
|
+
readonly description = "Perform basic math operations";
|
|
48
|
+
readonly options = mathOptions;
|
|
49
|
+
|
|
50
|
+
override readonly actionLabel = "Calculate";
|
|
51
|
+
|
|
52
|
+
override async execute(ctx: AppContext, opts: OptionValues<typeof mathOptions>): Promise<CommandResult> {
|
|
53
|
+
const result = this.calculate(opts);
|
|
54
|
+
if (!result.success) {
|
|
55
|
+
ctx.logger.error(result.message || "Calculation failed");
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
override renderResult(result: CommandResult): string {
|
|
61
|
+
if (!result.success) return result.message || "Error";
|
|
62
|
+
const data = result.data as { expression: string; result: number; steps?: string[] };
|
|
63
|
+
let output = `${data.expression} = ${data.result}`;
|
|
64
|
+
if (data.steps) {
|
|
65
|
+
output += "\n\nSteps:\n" + data.steps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private calculate(opts: OptionValues<typeof mathOptions>): CommandResult {
|
|
71
|
+
const op = opts.operation as string;
|
|
72
|
+
const a = opts.a as number;
|
|
73
|
+
const b = opts.b as number;
|
|
74
|
+
const showSteps = opts.showSteps as boolean;
|
|
75
|
+
|
|
76
|
+
let result: number;
|
|
77
|
+
let expression: string;
|
|
78
|
+
const steps: string[] = [];
|
|
79
|
+
|
|
80
|
+
switch (op) {
|
|
81
|
+
case "add":
|
|
82
|
+
result = a + b;
|
|
83
|
+
expression = `${a} + ${b}`;
|
|
84
|
+
if (showSteps) steps.push(`Adding ${a} and ${b}`, `Result: ${result}`);
|
|
85
|
+
break;
|
|
86
|
+
case "subtract":
|
|
87
|
+
result = a - b;
|
|
88
|
+
expression = `${a} - ${b}`;
|
|
89
|
+
if (showSteps) steps.push(`Subtracting ${b} from ${a}`, `Result: ${result}`);
|
|
90
|
+
break;
|
|
91
|
+
case "multiply":
|
|
92
|
+
result = a * b;
|
|
93
|
+
expression = `${a} × ${b}`;
|
|
94
|
+
if (showSteps) steps.push(`Multiplying ${a} by ${b}`, `Result: ${result}`);
|
|
95
|
+
break;
|
|
96
|
+
case "divide":
|
|
97
|
+
if (b === 0) {
|
|
98
|
+
return { success: false, message: "Cannot divide by zero" };
|
|
99
|
+
}
|
|
100
|
+
result = a / b;
|
|
101
|
+
expression = `${a} ÷ ${b}`;
|
|
102
|
+
if (showSteps) steps.push(`Dividing ${a} by ${b}`, `Result: ${result}`);
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
return { success: false, message: `Unknown operation: ${op}` };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
success: true,
|
|
110
|
+
data: { expression, result, steps: showSteps ? steps : undefined },
|
|
111
|
+
message: `${expression} = ${result}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|