@pablozaiden/terminatui 0.2.0 → 0.3.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.
Files changed (181) hide show
  1. package/README.md +64 -43
  2. package/package.json +11 -8
  3. package/src/__tests__/application.test.ts +87 -68
  4. package/src/__tests__/buildCliCommand.test.ts +99 -119
  5. package/src/__tests__/builtins.test.ts +27 -75
  6. package/src/__tests__/command.test.ts +100 -131
  7. package/src/__tests__/configOnChange.test.ts +63 -0
  8. package/src/__tests__/context.test.ts +1 -26
  9. package/src/__tests__/helpCore.test.ts +227 -0
  10. package/src/__tests__/parser.test.ts +98 -244
  11. package/src/__tests__/registry.test.ts +33 -160
  12. package/src/__tests__/schemaToFields.test.ts +75 -158
  13. package/src/builtins/help.ts +12 -4
  14. package/src/builtins/settings.ts +18 -32
  15. package/src/builtins/version.ts +3 -3
  16. package/src/cli/output/colors.ts +1 -1
  17. package/src/cli/parser.ts +26 -95
  18. package/src/core/application.ts +192 -110
  19. package/src/core/command.ts +26 -9
  20. package/src/core/context.ts +31 -20
  21. package/src/core/help.ts +24 -18
  22. package/src/core/knownCommands.ts +13 -0
  23. package/src/core/logger.ts +39 -42
  24. package/src/core/registry.ts +5 -12
  25. package/src/index.ts +22 -137
  26. package/src/tui/TuiApplication.tsx +63 -120
  27. package/src/tui/TuiRoot.tsx +135 -0
  28. package/src/tui/adapters/factory.ts +19 -0
  29. package/src/tui/adapters/ink/InkRenderer.tsx +139 -0
  30. package/src/tui/adapters/ink/components/Button.tsx +12 -0
  31. package/src/tui/adapters/ink/components/Code.tsx +6 -0
  32. package/src/tui/adapters/ink/components/CodeHighlight.tsx +6 -0
  33. package/src/tui/adapters/ink/components/Container.tsx +5 -0
  34. package/src/tui/adapters/ink/components/Field.tsx +12 -0
  35. package/src/tui/adapters/ink/components/Label.tsx +24 -0
  36. package/src/tui/adapters/ink/components/MenuButton.tsx +12 -0
  37. package/src/tui/adapters/ink/components/MenuItem.tsx +17 -0
  38. package/src/tui/adapters/ink/components/Overlay.tsx +5 -0
  39. package/src/tui/adapters/ink/components/Panel.tsx +15 -0
  40. package/src/tui/adapters/ink/components/ScrollView.tsx +5 -0
  41. package/src/tui/adapters/ink/components/Select.tsx +44 -0
  42. package/src/tui/adapters/ink/components/Spacer.tsx +15 -0
  43. package/src/tui/adapters/ink/components/Spinner.tsx +5 -0
  44. package/src/tui/adapters/ink/components/TextInput.tsx +22 -0
  45. package/src/tui/adapters/ink/components/Value.tsx +7 -0
  46. package/src/tui/adapters/ink/keyboard.ts +97 -0
  47. package/src/tui/adapters/ink/utils.ts +16 -0
  48. package/src/tui/adapters/opentui/OpenTuiRenderer.tsx +119 -0
  49. package/src/tui/adapters/opentui/components/Button.tsx +13 -0
  50. package/src/tui/adapters/opentui/components/Code.tsx +12 -0
  51. package/src/tui/adapters/opentui/components/CodeHighlight.tsx +24 -0
  52. package/src/tui/adapters/opentui/components/Container.tsx +56 -0
  53. package/src/tui/adapters/opentui/components/Field.tsx +18 -0
  54. package/src/tui/adapters/opentui/components/Label.tsx +15 -0
  55. package/src/tui/adapters/opentui/components/MenuButton.tsx +14 -0
  56. package/src/tui/adapters/opentui/components/MenuItem.tsx +29 -0
  57. package/src/tui/adapters/opentui/components/Overlay.tsx +21 -0
  58. package/src/tui/adapters/opentui/components/Panel.tsx +78 -0
  59. package/src/tui/adapters/opentui/components/ScrollView.tsx +85 -0
  60. package/src/tui/adapters/opentui/components/Select.tsx +59 -0
  61. package/src/tui/adapters/opentui/components/Spacer.tsx +5 -0
  62. package/src/tui/adapters/opentui/components/Spinner.tsx +12 -0
  63. package/src/tui/adapters/opentui/components/TextInput.tsx +13 -0
  64. package/src/tui/adapters/opentui/components/Value.tsx +13 -0
  65. package/src/tui/{hooks → adapters/opentui/hooks}/useSpinner.ts +2 -11
  66. package/src/tui/adapters/opentui/keyboard.ts +61 -0
  67. package/src/tui/adapters/types.ts +71 -0
  68. package/src/tui/components/ActionButton.tsx +0 -36
  69. package/src/tui/components/CommandSelector.tsx +45 -92
  70. package/src/tui/components/ConfigForm.tsx +68 -42
  71. package/src/tui/components/FieldRow.tsx +0 -30
  72. package/src/tui/components/Header.tsx +14 -13
  73. package/src/tui/components/JsonHighlight.tsx +10 -17
  74. package/src/tui/components/ModalBase.tsx +38 -0
  75. package/src/tui/components/ResultsPanel.tsx +27 -36
  76. package/src/tui/components/StatusBar.tsx +24 -39
  77. package/src/tui/components/logColors.ts +12 -0
  78. package/src/tui/context/ClipboardContext.tsx +87 -0
  79. package/src/tui/context/ExecutorContext.tsx +139 -0
  80. package/src/tui/context/KeyboardContext.tsx +85 -71
  81. package/src/tui/context/LogsContext.tsx +35 -0
  82. package/src/tui/context/NavigationContext.tsx +194 -0
  83. package/src/tui/context/RendererContext.tsx +20 -0
  84. package/src/tui/context/TuiAppContext.tsx +58 -0
  85. package/src/tui/hooks/useActiveKeyHandler.ts +75 -0
  86. package/src/tui/hooks/useBackHandler.ts +34 -0
  87. package/src/tui/hooks/useClipboard.ts +40 -25
  88. package/src/tui/hooks/useClipboardProvider.ts +42 -0
  89. package/src/tui/hooks/useGlobalKeyHandler.ts +54 -0
  90. package/src/tui/modals/CliModal.tsx +82 -0
  91. package/src/tui/modals/EditorModal.tsx +207 -0
  92. package/src/tui/modals/LogsModal.tsx +98 -0
  93. package/src/tui/registry.ts +102 -0
  94. package/src/tui/screens/CommandSelectScreen.tsx +162 -0
  95. package/src/tui/screens/ConfigScreen.tsx +165 -0
  96. package/src/tui/screens/ErrorScreen.tsx +58 -0
  97. package/src/tui/screens/ResultsScreen.tsx +68 -0
  98. package/src/tui/screens/RunningScreen.tsx +72 -0
  99. package/src/tui/screens/ScreenBase.ts +6 -0
  100. package/src/tui/semantic/Button.tsx +7 -0
  101. package/src/tui/semantic/Code.tsx +7 -0
  102. package/src/tui/semantic/CodeHighlight.tsx +7 -0
  103. package/src/tui/semantic/Container.tsx +7 -0
  104. package/src/tui/semantic/Field.tsx +7 -0
  105. package/src/tui/semantic/Label.tsx +7 -0
  106. package/src/tui/semantic/MenuButton.tsx +7 -0
  107. package/src/tui/semantic/MenuItem.tsx +7 -0
  108. package/src/tui/semantic/Overlay.tsx +7 -0
  109. package/src/tui/semantic/Panel.tsx +7 -0
  110. package/src/tui/semantic/ScrollView.tsx +9 -0
  111. package/src/tui/semantic/Select.tsx +7 -0
  112. package/src/tui/semantic/Spacer.tsx +7 -0
  113. package/src/tui/semantic/Spinner.tsx +7 -0
  114. package/src/tui/semantic/TextInput.tsx +7 -0
  115. package/src/tui/semantic/Value.tsx +7 -0
  116. package/src/tui/semantic/types.ts +195 -0
  117. package/src/tui/theme.ts +25 -14
  118. package/src/tui/utils/buildCliCommand.ts +1 -0
  119. package/src/tui/utils/getEnumKeys.ts +3 -0
  120. package/src/tui/utils/parameterPersistence.ts +1 -0
  121. package/src/types/command.ts +0 -60
  122. package/.devcontainer/devcontainer.json +0 -19
  123. package/.devcontainer/install-prerequisites.sh +0 -49
  124. package/.github/workflows/copilot-setup-steps.yml +0 -32
  125. package/.github/workflows/pull-request.yml +0 -27
  126. package/.github/workflows/release-npm-package.yml +0 -81
  127. package/AGENTS.md +0 -31
  128. package/bun.lock +0 -236
  129. package/examples/tui-app/commands/config/app/get.ts +0 -66
  130. package/examples/tui-app/commands/config/app/index.ts +0 -27
  131. package/examples/tui-app/commands/config/app/set.ts +0 -86
  132. package/examples/tui-app/commands/config/index.ts +0 -32
  133. package/examples/tui-app/commands/config/user/get.ts +0 -65
  134. package/examples/tui-app/commands/config/user/index.ts +0 -27
  135. package/examples/tui-app/commands/config/user/set.ts +0 -61
  136. package/examples/tui-app/commands/greet.ts +0 -76
  137. package/examples/tui-app/commands/index.ts +0 -4
  138. package/examples/tui-app/commands/math.ts +0 -115
  139. package/examples/tui-app/commands/status.ts +0 -77
  140. package/examples/tui-app/index.ts +0 -35
  141. package/guides/01-hello-world.md +0 -96
  142. package/guides/02-adding-options.md +0 -103
  143. package/guides/03-multiple-commands.md +0 -163
  144. package/guides/04-subcommands.md +0 -206
  145. package/guides/05-interactive-tui.md +0 -194
  146. package/guides/06-config-validation.md +0 -264
  147. package/guides/07-async-cancellation.md +0 -336
  148. package/guides/08-complete-application.md +0 -537
  149. package/guides/README.md +0 -74
  150. package/src/__tests__/colors.test.ts +0 -127
  151. package/src/__tests__/commandClass.test.ts +0 -130
  152. package/src/__tests__/help.test.ts +0 -412
  153. package/src/__tests__/registryNew.test.ts +0 -160
  154. package/src/__tests__/table.test.ts +0 -146
  155. package/src/__tests__/tui.test.ts +0 -26
  156. package/src/builtins/index.ts +0 -4
  157. package/src/cli/help.ts +0 -174
  158. package/src/cli/index.ts +0 -3
  159. package/src/cli/output/index.ts +0 -2
  160. package/src/cli/output/table.ts +0 -141
  161. package/src/commands/help.ts +0 -50
  162. package/src/commands/index.ts +0 -1
  163. package/src/components/index.ts +0 -147
  164. package/src/core/index.ts +0 -15
  165. package/src/hooks/index.ts +0 -131
  166. package/src/registry/commandRegistry.ts +0 -77
  167. package/src/registry/index.ts +0 -1
  168. package/src/tui/TuiApp.tsx +0 -619
  169. package/src/tui/app.ts +0 -29
  170. package/src/tui/components/CliModal.tsx +0 -81
  171. package/src/tui/components/EditorModal.tsx +0 -177
  172. package/src/tui/components/LogsPanel.tsx +0 -86
  173. package/src/tui/components/index.ts +0 -13
  174. package/src/tui/context/index.ts +0 -7
  175. package/src/tui/hooks/index.ts +0 -35
  176. package/src/tui/hooks/useKeyboardHandler.ts +0 -91
  177. package/src/tui/hooks/useLogStream.ts +0 -96
  178. package/src/tui/index.ts +0 -65
  179. package/src/tui/utils/index.ts +0 -13
  180. package/src/types/index.ts +0 -1
  181. package/tsconfig.json +0 -25
@@ -1,336 +0,0 @@
1
- # Guide 7: Async Commands with Cancellation (Complex)
2
-
3
- Build commands that support cancellation with proper cleanup for long-running operations.
4
-
5
- ## What You'll Build
6
-
7
- A download manager that:
8
- - Downloads files asynchronously
9
- - Shows progress updates
10
- - Supports cancellation (Ctrl+C in CLI, Esc in TUI)
11
- - Cleans up partial downloads when cancelled
12
-
13
- ```bash
14
- download https://example.com/large-file.zip --output ./downloads/
15
- ```
16
-
17
- ## Step 1: Define the Command
18
-
19
- Create `src/commands/download.ts`:
20
-
21
- ```typescript
22
- import path from "node:path";
23
- import {
24
- Command,
25
- ConfigValidationError,
26
- type AppContext,
27
- type OptionSchema,
28
- type OptionValues,
29
- type CommandResult,
30
- type CommandExecutionContext
31
- } from "@pablozaiden/terminatui";
32
-
33
- const options = {
34
- url: {
35
- type: "string",
36
- description: "URL to download",
37
- required: true,
38
- label: "Download URL",
39
- },
40
- output: {
41
- type: "string",
42
- description: "Output directory",
43
- default: "./downloads",
44
- label: "Output Directory",
45
- },
46
- "chunk-size": {
47
- type: "string",
48
- description: "Download chunk size in KB",
49
- default: "1024",
50
- label: "Chunk Size (KB)",
51
- },
52
- } satisfies OptionSchema;
53
-
54
- interface DownloadConfig {
55
- url: URL;
56
- outputDir: string;
57
- fileName: string;
58
- chunkSize: number;
59
- }
60
- ```
61
-
62
- ## Step 2: Implement buildConfig
63
-
64
- ```typescript
65
- export class DownloadCommand extends Command<typeof options, DownloadConfig> {
66
- readonly name = "download";
67
- readonly description = "Download a file from URL";
68
- readonly options = options;
69
- readonly displayName = "File Downloader";
70
- readonly actionLabel = "Download";
71
-
72
- override buildConfig(
73
- _ctx: AppContext,
74
- opts: OptionValues<typeof options>
75
- ): DownloadConfig {
76
- // Validate URL
77
- const urlStr = opts["url"] as string;
78
- if (!urlStr) {
79
- throw new ConfigValidationError("URL is required", "url");
80
- }
81
-
82
- let url: URL;
83
- try {
84
- url = new URL(urlStr);
85
- } catch {
86
- throw new ConfigValidationError("Invalid URL format", "url");
87
- }
88
-
89
- // Validate output directory
90
- const outputDir = path.resolve(opts["output"] as string ?? "./downloads");
91
-
92
- // Extract filename from URL
93
- const fileName = path.basename(url.pathname) || "download";
94
-
95
- // Parse chunk size
96
- const chunkSizeStr = opts["chunk-size"] as string ?? "1024";
97
- const chunkSize = parseInt(chunkSizeStr, 10) * 1024; // Convert KB to bytes
98
- if (isNaN(chunkSize) || chunkSize <= 0) {
99
- throw new ConfigValidationError("Chunk size must be a positive number", "chunk-size");
100
- }
101
-
102
- return { url, outputDir, fileName, chunkSize };
103
- }
104
- ```
105
-
106
- ## Step 3: Implement Cancellable Download
107
-
108
- ```typescript
109
- async execute(
110
- ctx: AppContext,
111
- config: DownloadConfig,
112
- execCtx?: CommandExecutionContext
113
- ): Promise<CommandResult> {
114
- const { url, outputDir, fileName, chunkSize } = config;
115
- const outputPath = path.join(outputDir, fileName);
116
- const signal = execCtx?.signal;
117
-
118
- ctx.logger.info(`Starting download: ${url}`);
119
- ctx.logger.info(`Output: ${outputPath}`);
120
-
121
- // Create output directory
122
- await Bun.write(path.join(outputDir, ".keep"), "");
123
-
124
- // Track download state for cleanup
125
- let downloadedBytes = 0;
126
- let totalBytes = 0;
127
- let partialFile: Bun.FileSink | null = null;
128
-
129
- try {
130
- // Check for cancellation before starting
131
- if (signal?.aborted) {
132
- return { success: false, message: "Download cancelled before start" };
133
- }
134
-
135
- // Fetch with AbortSignal
136
- const response = await fetch(url.toString(), { signal });
137
-
138
- if (!response.ok) {
139
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
140
- }
141
-
142
- totalBytes = parseInt(response.headers.get("content-length") ?? "0", 10);
143
- const reader = response.body?.getReader();
144
-
145
- if (!reader) {
146
- throw new Error("No response body");
147
- }
148
-
149
- // Open file for writing
150
- partialFile = Bun.file(outputPath).writer();
151
-
152
- console.log(`Downloading ${fileName}...`);
153
- if (totalBytes > 0) {
154
- console.log(`Total size: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
155
- }
156
-
157
- // Read chunks with cancellation checks
158
- while (true) {
159
- // Check for cancellation between chunks
160
- if (signal?.aborted) {
161
- ctx.logger.warn("Download cancelled by user");
162
- throw new Error("AbortError");
163
- }
164
-
165
- const { done, value } = await reader.read();
166
-
167
- if (done) {
168
- break;
169
- }
170
-
171
- // Write chunk
172
- partialFile.write(value);
173
- downloadedBytes += value.byteLength;
174
-
175
- // Log progress
176
- if (totalBytes > 0) {
177
- const percent = ((downloadedBytes / totalBytes) * 100).toFixed(1);
178
- const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
179
- const mbTotal = (totalBytes / 1024 / 1024).toFixed(2);
180
- process.stdout.write(`\rProgress: ${percent}% (${mbDownloaded}/${mbTotal} MB)`);
181
- } else {
182
- const mbDownloaded = (downloadedBytes / 1024 / 1024).toFixed(2);
183
- process.stdout.write(`\rDownloaded: ${mbDownloaded} MB`);
184
- }
185
- }
186
-
187
- // Finalize file
188
- await partialFile.end();
189
- console.log("\nDownload complete!");
190
-
191
- return {
192
- success: true,
193
- data: {
194
- file: outputPath,
195
- size: downloadedBytes,
196
- url: url.toString(),
197
- },
198
- message: `Downloaded ${fileName} (${(downloadedBytes / 1024 / 1024).toFixed(2)} MB)`,
199
- };
200
-
201
- } catch (error) {
202
- // Handle cancellation
203
- if (signal?.aborted || (error as Error).name === "AbortError") {
204
- console.log("\nDownload cancelled.");
205
-
206
- // Cleanup partial file
207
- await this.cleanup(outputPath, partialFile);
208
-
209
- return {
210
- success: false,
211
- message: "Download cancelled by user",
212
- data: {
213
- downloadedBytes,
214
- cancelled: true,
215
- },
216
- };
217
- }
218
-
219
- // Handle other errors
220
- await this.cleanup(outputPath, partialFile);
221
- throw error;
222
- }
223
- }
224
-
225
- private async cleanup(
226
- outputPath: string,
227
- sink: Bun.FileSink | null
228
- ): Promise<void> {
229
- try {
230
- // Close file handle
231
- if (sink) {
232
- await sink.end();
233
- }
234
-
235
- // Remove partial file
236
- const file = Bun.file(outputPath);
237
- if (await file.exists()) {
238
- await Bun.write(outputPath, ""); // Clear file
239
- // In production: fs.unlinkSync(outputPath);
240
- console.log("Cleaned up partial download.");
241
- }
242
- } catch (e) {
243
- // Ignore cleanup errors
244
- }
245
- }
246
- }
247
- ```
248
-
249
- ## Step 4: Create the Application
250
-
251
- Create `src/index.ts`:
252
-
253
- ```typescript
254
- import { TuiApplication } from "@pablozaiden/terminatui";
255
- import { DownloadCommand } from "./commands/download";
256
-
257
- class DownloadManager extends TuiApplication {
258
- constructor() {
259
- super({
260
- name: "download-manager",
261
- version: "1.0.0",
262
- commands: [new DownloadCommand()],
263
- });
264
- }
265
- }
266
-
267
- await new DownloadManager().run();
268
- ```
269
-
270
- ## Step 6: Test Cancellation
271
-
272
- ```bash
273
- # Start a large download
274
- bun src/index.ts download https://speed.hetzner.de/100MB.bin --output ./test-downloads
275
-
276
- # While downloading, press Ctrl+C
277
- # Should see: "Download cancelled. Cleaned up partial download."
278
-
279
- # Run TUI mode
280
- bun src/index.ts --tui
281
-
282
- # Start download, then press Esc to cancel
283
- # Same cancellation behavior with cleanup
284
- ```
285
-
286
- ## Cancellation Patterns
287
-
288
- ### 1. Check Signal Before Long Operations
289
-
290
- ```typescript
291
- if (signal?.aborted) {
292
- return { success: false, message: "Cancelled" };
293
- }
294
- ```
295
-
296
- ### 2. Pass Signal to fetch/APIs
297
-
298
- ```typescript
299
- await fetch(url, { signal });
300
- await someAsyncApi({ signal });
301
- ```
302
-
303
- ### 3. Check Between Iterations
304
-
305
- ```typescript
306
- for (const item of items) {
307
- if (signal?.aborted) break;
308
- await processItem(item);
309
- }
310
- ```
311
-
312
- ### 4. Always Cleanup
313
-
314
- ```typescript
315
- try {
316
- // ... cancellable work ...
317
- } catch (error) {
318
- if (signal?.aborted) {
319
- await cleanup();
320
- return { success: false, message: "Cancelled" };
321
- }
322
- throw error;
323
- }
324
- ```
325
-
326
- ## What You Learned
327
-
328
- - Accept `CommandExecutionContext` with `AbortSignal`
329
- - Check `signal.aborted` between operations
330
- - Pass signal to `fetch` and other async APIs
331
- - Clean up resources on cancellation
332
- - Return meaningful results for cancelled operations
333
-
334
- ## Next Steps
335
-
336
- → [Guide 8: Building a Complete Application](08-complete-application.md)