@mariozechner/pi-tui 0.30.2 → 0.31.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/README.md CHANGED
@@ -8,7 +8,8 @@ Minimal terminal UI framework with differential rendering and synchronized outpu
8
8
  - **Synchronized Output**: Uses CSI 2026 for atomic screen updates (no flicker)
9
9
  - **Bracketed Paste Mode**: Handles large pastes correctly with markers for >10 line pastes
10
10
  - **Component-based**: Simple Component interface with render() method
11
- - **Built-in Components**: Text, Input, Editor, Markdown, Loader, SelectList, Spacer, Image, Box, Container
11
+ - **Theme Support**: Components accept theme interfaces for customizable styling
12
+ - **Built-in Components**: Text, TruncatedText, Input, Editor, Markdown, Loader, SelectList, SettingsList, Spacer, Image, Box, Container
12
13
  - **Inline Images**: Renders images in terminals that support Kitty or iTerm2 graphics protocols
13
14
  - **Autocomplete Support**: File paths and slash commands
14
15
 
@@ -26,10 +27,10 @@ const tui = new TUI(terminal);
26
27
  // Add components
27
28
  tui.addChild(new Text("Welcome to my app!"));
28
29
 
29
- const editor = new Editor();
30
+ const editor = new Editor(editorTheme);
30
31
  editor.onSubmit = (text) => {
31
- console.log("Submitted:", text);
32
- tui.addChild(new Text(`You said: ${text}`));
32
+ console.log("Submitted:", text);
33
+ tui.addChild(new Text(`You said: ${text}`));
33
34
  };
34
35
  tui.addChild(editor);
35
36
 
@@ -50,6 +51,9 @@ tui.removeChild(component);
50
51
  tui.start();
51
52
  tui.stop();
52
53
  tui.requestRender(); // Request a re-render
54
+
55
+ // Global debug key handler (Shift+Ctrl+D)
56
+ tui.onDebug = () => console.log("Debug triggered");
53
57
  ```
54
58
 
55
59
  ### Component Interface
@@ -58,11 +62,18 @@ All components implement:
58
62
 
59
63
  ```typescript
60
64
  interface Component {
61
- render(width: number): string[];
62
- handleInput?(data: string): void;
65
+ render(width: number): string[];
66
+ handleInput?(data: string): void;
67
+ invalidate?(): void;
63
68
  }
64
69
  ```
65
70
 
71
+ | Method | Description |
72
+ |--------|-------------|
73
+ | `render(width)` | Returns an array of strings, one per line. Each line **must not exceed `width`** or the TUI will error. Use `truncateToWidth()` or manual wrapping to ensure this. |
74
+ | `handleInput?(data)` | Called when the component has focus and receives keyboard input. The `data` string contains raw terminal input (may include ANSI escape sequences). |
75
+ | `invalidate?()` | Called to clear any cached render state. Components should re-render from scratch on the next `render()` call. |
76
+
66
77
  ## Built-in Components
67
78
 
68
79
  ### Container
@@ -81,11 +92,11 @@ Container that applies padding and background color to all children.
81
92
 
82
93
  ```typescript
83
94
  const box = new Box(
84
- 1, // paddingX (default: 1)
85
- 1, // paddingY (default: 1)
86
- (text) => chalk.bgGray(text) // optional background function
95
+ 1, // paddingX (default: 1)
96
+ 1, // paddingY (default: 1)
97
+ (text) => chalk.bgGray(text) // optional background function
87
98
  );
88
- box.addChild(new Text("Content", 0, 0));
99
+ box.addChild(new Text("Content"));
89
100
  box.setBgFn((text) => chalk.bgBlue(text)); // Change background dynamically
90
101
  ```
91
102
 
@@ -94,8 +105,26 @@ box.setBgFn((text) => chalk.bgBlue(text)); // Change background dynamically
94
105
  Displays multi-line text with word wrapping and padding.
95
106
 
96
107
  ```typescript
97
- const text = new Text("Hello World", paddingX, paddingY); // defaults: 1, 1
108
+ const text = new Text(
109
+ "Hello World", // text content
110
+ 1, // paddingX (default: 1)
111
+ 1, // paddingY (default: 1)
112
+ (text) => chalk.bgGray(text) // optional background function
113
+ );
98
114
  text.setText("Updated text");
115
+ text.setCustomBgFn((text) => chalk.bgBlue(text));
116
+ ```
117
+
118
+ ### TruncatedText
119
+
120
+ Single-line text that truncates to fit viewport width. Useful for status lines and headers.
121
+
122
+ ```typescript
123
+ const truncated = new TruncatedText(
124
+ "This is a very long line that will be truncated...",
125
+ 0, // paddingX (default: 0)
126
+ 0 // paddingY (default: 0)
127
+ );
99
128
  ```
100
129
 
101
130
  ### Input
@@ -106,14 +135,17 @@ Single-line text input with horizontal scrolling.
106
135
  const input = new Input();
107
136
  input.onSubmit = (value) => console.log(value);
108
137
  input.setValue("initial");
138
+ input.getValue();
109
139
  ```
110
140
 
111
141
  **Key Bindings:**
112
142
  - `Enter` - Submit
113
143
  - `Ctrl+A` / `Ctrl+E` - Line start/end
114
- - `Ctrl+W` or `Option+Backspace` - Delete word backwards
144
+ - `Ctrl+W` or `Alt+Backspace` - Delete word backwards
115
145
  - `Ctrl+U` - Delete to start of line
116
146
  - `Ctrl+K` - Delete to end of line
147
+ - `Ctrl+Left` / `Ctrl+Right` - Word navigation
148
+ - `Alt+Left` / `Alt+Right` - Word navigation
117
149
  - Arrow keys, Backspace, Delete work as expected
118
150
 
119
151
  ### Editor
@@ -121,11 +153,17 @@ input.setValue("initial");
121
153
  Multi-line text editor with autocomplete, file completion, and paste handling.
122
154
 
123
155
  ```typescript
124
- const editor = new Editor();
156
+ interface EditorTheme {
157
+ borderColor: (str: string) => string;
158
+ selectList: SelectListTheme;
159
+ }
160
+
161
+ const editor = new Editor(theme);
125
162
  editor.onSubmit = (text) => console.log(text);
126
163
  editor.onChange = (text) => console.log("Changed:", text);
127
164
  editor.disableSubmit = true; // Disable submit temporarily
128
165
  editor.setAutocompleteProvider(provider);
166
+ editor.borderColor = (s) => chalk.blue(s); // Change border dynamically
129
167
  ```
130
168
 
131
169
  **Features:**
@@ -146,24 +184,50 @@ editor.setAutocompleteProvider(provider);
146
184
 
147
185
  ### Markdown
148
186
 
149
- Renders markdown with syntax highlighting and optional background colors.
187
+ Renders markdown with syntax highlighting and theming support.
150
188
 
151
189
  ```typescript
190
+ interface MarkdownTheme {
191
+ heading: (text: string) => string;
192
+ link: (text: string) => string;
193
+ linkUrl: (text: string) => string;
194
+ code: (text: string) => string;
195
+ codeBlock: (text: string) => string;
196
+ codeBlockBorder: (text: string) => string;
197
+ quote: (text: string) => string;
198
+ quoteBorder: (text: string) => string;
199
+ hr: (text: string) => string;
200
+ listBullet: (text: string) => string;
201
+ bold: (text: string) => string;
202
+ italic: (text: string) => string;
203
+ strikethrough: (text: string) => string;
204
+ underline: (text: string) => string;
205
+ highlightCode?: (code: string, lang?: string) => string[];
206
+ }
207
+
208
+ interface DefaultTextStyle {
209
+ color?: (text: string) => string;
210
+ bgColor?: (text: string) => string;
211
+ bold?: boolean;
212
+ italic?: boolean;
213
+ strikethrough?: boolean;
214
+ underline?: boolean;
215
+ }
216
+
152
217
  const md = new Markdown(
153
- "# Hello\n\nSome **bold** text",
154
- bgColor, // optional: "bgRed", "bgBlue", etc.
155
- fgColor, // optional: "white", "cyan", etc.
156
- customBgRgb, // optional: { r: 52, g: 53, b: 65 }
157
- paddingX, // optional: default 1
158
- paddingY // optional: default 1
218
+ "# Hello\n\nSome **bold** text",
219
+ 1, // paddingX
220
+ 1, // paddingY
221
+ theme, // MarkdownTheme
222
+ defaultStyle // optional DefaultTextStyle
159
223
  );
160
224
  md.setText("Updated markdown");
161
225
  ```
162
226
 
163
227
  **Features:**
164
228
  - Headings, bold, italic, code blocks, lists, links, blockquotes
165
- - Syntax highlighting with chalk
166
- - Optional background colors (including custom RGB)
229
+ - HTML tags rendered as plain text
230
+ - Optional syntax highlighting via `highlightCode`
167
231
  - Padding support
168
232
  - Render caching for performance
169
233
 
@@ -172,29 +236,114 @@ md.setText("Updated markdown");
172
236
  Animated loading spinner.
173
237
 
174
238
  ```typescript
175
- const loader = new Loader(tui, "Loading...");
239
+ const loader = new Loader(
240
+ tui, // TUI instance for render updates
241
+ (s) => chalk.cyan(s), // spinner color function
242
+ (s) => chalk.gray(s), // message color function
243
+ "Loading..." // message (default: "Loading...")
244
+ );
176
245
  loader.start();
246
+ loader.setMessage("Still loading...");
177
247
  loader.stop();
178
248
  ```
179
249
 
250
+ ### CancellableLoader
251
+
252
+ Extends Loader with Escape key handling and an AbortSignal for cancelling async operations.
253
+
254
+ ```typescript
255
+ const loader = new CancellableLoader(
256
+ tui, // TUI instance for render updates
257
+ (s) => chalk.cyan(s), // spinner color function
258
+ (s) => chalk.gray(s), // message color function
259
+ "Working..." // message
260
+ );
261
+ loader.onAbort = () => done(null); // Called when user presses Escape
262
+ doAsyncWork(loader.signal).then(done);
263
+ ```
264
+
265
+ **Properties:**
266
+ - `signal: AbortSignal` - Aborted when user presses Escape
267
+ - `aborted: boolean` - Whether the loader was aborted
268
+ - `onAbort?: () => void` - Callback when user presses Escape
269
+
180
270
  ### SelectList
181
271
 
182
272
  Interactive selection list with keyboard navigation.
183
273
 
184
274
  ```typescript
185
- const list = new SelectList([
186
- { value: "opt1", label: "Option 1", description: "First option" },
187
- { value: "opt2", label: "Option 2", description: "Second option" },
188
- ], 5); // maxVisible
275
+ interface SelectItem {
276
+ value: string;
277
+ label: string;
278
+ description?: string;
279
+ }
280
+
281
+ interface SelectListTheme {
282
+ selectedPrefix: (text: string) => string;
283
+ selectedText: (text: string) => string;
284
+ description: (text: string) => string;
285
+ scrollInfo: (text: string) => string;
286
+ noMatch: (text: string) => string;
287
+ }
288
+
289
+ const list = new SelectList(
290
+ [
291
+ { value: "opt1", label: "Option 1", description: "First option" },
292
+ { value: "opt2", label: "Option 2", description: "Second option" },
293
+ ],
294
+ 5, // maxVisible
295
+ theme // SelectListTheme
296
+ );
189
297
 
190
298
  list.onSelect = (item) => console.log("Selected:", item);
191
299
  list.onCancel = () => console.log("Cancelled");
300
+ list.onSelectionChange = (item) => console.log("Highlighted:", item);
192
301
  list.setFilter("opt"); // Filter items
193
302
  ```
194
303
 
195
304
  **Controls:**
196
305
  - Arrow keys: Navigate
197
- - Enter or Tab: Select
306
+ - Enter: Select
307
+ - Escape: Cancel
308
+
309
+ ### SettingsList
310
+
311
+ Settings panel with value cycling and submenus.
312
+
313
+ ```typescript
314
+ interface SettingItem {
315
+ id: string;
316
+ label: string;
317
+ description?: string;
318
+ currentValue: string;
319
+ values?: string[]; // If provided, Enter/Space cycles through these
320
+ submenu?: (currentValue: string, done: (selectedValue?: string) => void) => Component;
321
+ }
322
+
323
+ interface SettingsListTheme {
324
+ label: (text: string, selected: boolean) => string;
325
+ value: (text: string, selected: boolean) => string;
326
+ description: (text: string) => string;
327
+ cursor: string;
328
+ hint: (text: string) => string;
329
+ }
330
+
331
+ const settings = new SettingsList(
332
+ [
333
+ { id: "theme", label: "Theme", currentValue: "dark", values: ["dark", "light"] },
334
+ { id: "model", label: "Model", currentValue: "gpt-4", submenu: (val, done) => modelSelector },
335
+ ],
336
+ 10, // maxVisible
337
+ theme, // SettingsListTheme
338
+ (id, newValue) => console.log(`${id} changed to ${newValue}`),
339
+ () => console.log("Cancelled")
340
+ );
341
+ settings.updateValue("theme", "light");
342
+ ```
343
+
344
+ **Controls:**
345
+ - Arrow keys: Navigate
346
+ - Enter/Space: Activate (cycle value or open submenu)
198
347
  - Escape: Cancel
199
348
 
200
349
  ### Spacer
@@ -210,13 +359,21 @@ const spacer = new Spacer(2); // 2 empty lines (default: 1)
210
359
  Renders images inline for terminals that support the Kitty graphics protocol (Kitty, Ghostty, WezTerm) or iTerm2 inline images. Falls back to a text placeholder on unsupported terminals.
211
360
 
212
361
  ```typescript
213
- import { Image } from "@mariozechner/pi-tui";
362
+ interface ImageTheme {
363
+ fallbackColor: (str: string) => string;
364
+ }
365
+
366
+ interface ImageOptions {
367
+ maxWidthCells?: number;
368
+ maxHeightCells?: number;
369
+ filename?: string;
370
+ }
214
371
 
215
372
  const image = new Image(
216
- base64Data, // base64-encoded image data
217
- "image/png", // MIME type
218
- { fallbackColor: (s) => s }, // theme for fallback text
219
- { maxWidthCells: 60 } // optional: limit width
373
+ base64Data, // base64-encoded image data
374
+ "image/png", // MIME type
375
+ theme, // ImageTheme
376
+ options // optional ImageOptions
220
377
  );
221
378
  tui.addChild(image);
222
379
  ```
@@ -233,12 +390,12 @@ Supports both slash commands and file paths.
233
390
  import { CombinedAutocompleteProvider } from "@mariozechner/pi-tui";
234
391
 
235
392
  const provider = new CombinedAutocompleteProvider(
236
- [
237
- { name: "help", description: "Show help" },
238
- { name: "clear", description: "Clear screen" },
239
- { name: "delete", description: "Delete last message" },
240
- ],
241
- process.cwd() // base path for file completion
393
+ [
394
+ { name: "help", description: "Show help" },
395
+ { name: "clear", description: "Clear screen" },
396
+ { name: "delete", description: "Delete last message" },
397
+ ],
398
+ process.cwd() // base path for file completion
242
399
  );
243
400
 
244
401
  editor.setAutocompleteProvider(provider);
@@ -250,6 +407,27 @@ editor.setAutocompleteProvider(provider);
250
407
  - Works with `~/`, `./`, `../`, and `@` prefix
251
408
  - Filters to attachable files for `@` prefix
252
409
 
410
+ ## Key Detection
411
+
412
+ Helper functions for detecting keyboard input (supports Kitty keyboard protocol):
413
+
414
+ ```typescript
415
+ import {
416
+ isEnter, isEscape, isTab, isShiftTab,
417
+ isArrowUp, isArrowDown, isArrowLeft, isArrowRight,
418
+ isCtrlA, isCtrlC, isCtrlE, isCtrlK, isCtrlO, isCtrlP,
419
+ isCtrlLeft, isCtrlRight, isAltLeft, isAltRight,
420
+ isShiftEnter, isAltEnter,
421
+ isShiftCtrlO, isShiftCtrlD, isShiftCtrlP,
422
+ isBackspace, isDelete, isHome, isEnd,
423
+ // ... and more
424
+ } from "@mariozechner/pi-tui";
425
+
426
+ if (isCtrlC(data)) {
427
+ process.exit(0);
428
+ }
429
+ ```
430
+
253
431
  ## Differential Rendering
254
432
 
255
433
  The TUI uses three rendering strategies:
@@ -266,17 +444,17 @@ The TUI works with any object implementing the `Terminal` interface:
266
444
 
267
445
  ```typescript
268
446
  interface Terminal {
269
- start(onInput: (data: string) => void, onResize: () => void): void;
270
- stop(): void;
271
- write(data: string): void;
272
- get columns(): number;
273
- get rows(): number;
274
- moveBy(lines: number): void;
275
- hideCursor(): void;
276
- showCursor(): void;
277
- clearLine(): void;
278
- clearFromCursor(): void;
279
- clearScreen(): void;
447
+ start(onInput: (data: string) => void, onResize: () => void): void;
448
+ stop(): void;
449
+ write(data: string): void;
450
+ get columns(): number;
451
+ get rows(): number;
452
+ moveBy(lines: number): void;
453
+ hideCursor(): void;
454
+ showCursor(): void;
455
+ clearLine(): void;
456
+ clearFromCursor(): void;
457
+ clearScreen(): void;
280
458
  }
281
459
  ```
282
460
 
@@ -284,6 +462,143 @@ interface Terminal {
284
462
  - `ProcessTerminal` - Uses `process.stdin/stdout`
285
463
  - `VirtualTerminal` - For testing (uses `@xterm/headless`)
286
464
 
465
+ ## Utilities
466
+
467
+ ```typescript
468
+ import { visibleWidth, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
469
+
470
+ // Get visible width of string (ignoring ANSI codes)
471
+ const width = visibleWidth("\x1b[31mHello\x1b[0m"); // 5
472
+
473
+ // Truncate string to width (preserving ANSI codes, adds ellipsis)
474
+ const truncated = truncateToWidth("Hello World", 8); // "Hello..."
475
+
476
+ // Truncate without ellipsis
477
+ const truncatedNoEllipsis = truncateToWidth("Hello World", 8, ""); // "Hello Wo"
478
+
479
+ // Wrap text to width (preserving ANSI codes across line breaks)
480
+ const lines = wrapTextWithAnsi("This is a long line that needs wrapping", 20);
481
+ // ["This is a long line", "that needs wrapping"]
482
+ ```
483
+
484
+ ## Creating Custom Components
485
+
486
+ When creating custom components, **each line returned by `render()` must not exceed the `width` parameter**. The TUI will error if any line is wider than the terminal.
487
+
488
+ ### Handling Input
489
+
490
+ Use the key detection utilities to handle keyboard input:
491
+
492
+ ```typescript
493
+ import {
494
+ isEnter, isEscape, isArrowUp, isArrowDown,
495
+ isCtrlC, isTab, isBackspace
496
+ } from "@mariozechner/pi-tui";
497
+ import type { Component } from "@mariozechner/pi-tui";
498
+
499
+ class MyInteractiveComponent implements Component {
500
+ private selectedIndex = 0;
501
+ private items = ["Option 1", "Option 2", "Option 3"];
502
+
503
+ public onSelect?: (index: number) => void;
504
+ public onCancel?: () => void;
505
+
506
+ handleInput(data: string): void {
507
+ if (isArrowUp(data)) {
508
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
509
+ } else if (isArrowDown(data)) {
510
+ this.selectedIndex = Math.min(this.items.length - 1, this.selectedIndex + 1);
511
+ } else if (isEnter(data)) {
512
+ this.onSelect?.(this.selectedIndex);
513
+ } else if (isEscape(data) || isCtrlC(data)) {
514
+ this.onCancel?.();
515
+ }
516
+ }
517
+
518
+ render(width: number): string[] {
519
+ return this.items.map((item, i) => {
520
+ const prefix = i === this.selectedIndex ? "> " : " ";
521
+ return truncateToWidth(prefix + item, width);
522
+ });
523
+ }
524
+ }
525
+ ```
526
+
527
+ ### Handling Line Width
528
+
529
+ Use the provided utilities to ensure lines fit:
530
+
531
+ ```typescript
532
+ import { visibleWidth, truncateToWidth } from "@mariozechner/pi-tui";
533
+ import type { Component } from "@mariozechner/pi-tui";
534
+
535
+ class MyComponent implements Component {
536
+ private text: string;
537
+
538
+ constructor(text: string) {
539
+ this.text = text;
540
+ }
541
+
542
+ render(width: number): string[] {
543
+ // Option 1: Truncate long lines
544
+ return [truncateToWidth(this.text, width)];
545
+
546
+ // Option 2: Check and pad to exact width
547
+ const line = this.text;
548
+ const visible = visibleWidth(line);
549
+ if (visible > width) {
550
+ return [truncateToWidth(line, width)];
551
+ }
552
+ // Pad to exact width (optional, for backgrounds)
553
+ return [line + " ".repeat(width - visible)];
554
+ }
555
+ }
556
+ ```
557
+
558
+ ### ANSI Code Considerations
559
+
560
+ Both `visibleWidth()` and `truncateToWidth()` correctly handle ANSI escape codes:
561
+
562
+ - `visibleWidth()` ignores ANSI codes when calculating width
563
+ - `truncateToWidth()` preserves ANSI codes and properly closes them when truncating
564
+
565
+ ```typescript
566
+ import chalk from "chalk";
567
+
568
+ const styled = chalk.red("Hello") + " " + chalk.blue("World");
569
+ const width = visibleWidth(styled); // 11 (not counting ANSI codes)
570
+ const truncated = truncateToWidth(styled, 8); // Red "Hello" + " W..." with proper reset
571
+ ```
572
+
573
+ ### Caching
574
+
575
+ For performance, components should cache their rendered output and only re-render when necessary:
576
+
577
+ ```typescript
578
+ class CachedComponent implements Component {
579
+ private text: string;
580
+ private cachedWidth?: number;
581
+ private cachedLines?: string[];
582
+
583
+ render(width: number): string[] {
584
+ if (this.cachedLines && this.cachedWidth === width) {
585
+ return this.cachedLines;
586
+ }
587
+
588
+ const lines = [truncateToWidth(this.text, width)];
589
+
590
+ this.cachedWidth = width;
591
+ this.cachedLines = lines;
592
+ return lines;
593
+ }
594
+
595
+ invalidate(): void {
596
+ this.cachedWidth = undefined;
597
+ this.cachedLines = undefined;
598
+ }
599
+ }
600
+ ```
601
+
287
602
  ## Example
288
603
 
289
604
  See `test/chat-simple.ts` for a complete chat interface example with:
@@ -0,0 +1,22 @@
1
+ import { Loader } from "./loader.js";
2
+ /**
3
+ * Loader that can be cancelled with Escape.
4
+ * Extends Loader with an AbortSignal for cancelling async operations.
5
+ *
6
+ * @example
7
+ * const loader = new CancellableLoader(tui, cyan, dim, "Working...");
8
+ * loader.onAbort = () => done(null);
9
+ * doWork(loader.signal).then(done);
10
+ */
11
+ export declare class CancellableLoader extends Loader {
12
+ private abortController;
13
+ /** Called when user presses Escape */
14
+ onAbort?: () => void;
15
+ /** AbortSignal that is aborted when user presses Escape */
16
+ get signal(): AbortSignal;
17
+ /** Whether the loader was aborted */
18
+ get aborted(): boolean;
19
+ handleInput(data: string): void;
20
+ dispose(): void;
21
+ }
22
+ //# sourceMappingURL=cancellable-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cancellable-loader.d.ts","sourceRoot":"","sources":["../../src/components/cancellable-loader.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AACH,qBAAa,iBAAkB,SAAQ,MAAM;IAC5C,OAAO,CAAC,eAAe,CAAyB;IAEhD,sCAAsC;IACtC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAErB,2DAA2D;IAC3D,IAAI,MAAM,IAAI,WAAW,CAExB;IAED,qCAAqC;IACrC,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAK9B;IAED,OAAO,IAAI,IAAI,CAEd;CACD","sourcesContent":["import { isEscape } from \"../keys.js\";\nimport { Loader } from \"./loader.js\";\n\n/**\n * Loader that can be cancelled with Escape.\n * Extends Loader with an AbortSignal for cancelling async operations.\n *\n * @example\n * const loader = new CancellableLoader(tui, cyan, dim, \"Working...\");\n * loader.onAbort = () => done(null);\n * doWork(loader.signal).then(done);\n */\nexport class CancellableLoader extends Loader {\n\tprivate abortController = new AbortController();\n\n\t/** Called when user presses Escape */\n\tonAbort?: () => void;\n\n\t/** AbortSignal that is aborted when user presses Escape */\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\t/** Whether the loader was aborted */\n\tget aborted(): boolean {\n\t\treturn this.abortController.signal.aborted;\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (isEscape(data)) {\n\t\t\tthis.abortController.abort();\n\t\t\tthis.onAbort?.();\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.stop();\n\t}\n}\n"]}
@@ -0,0 +1,34 @@
1
+ import { isEscape } from "../keys.js";
2
+ import { Loader } from "./loader.js";
3
+ /**
4
+ * Loader that can be cancelled with Escape.
5
+ * Extends Loader with an AbortSignal for cancelling async operations.
6
+ *
7
+ * @example
8
+ * const loader = new CancellableLoader(tui, cyan, dim, "Working...");
9
+ * loader.onAbort = () => done(null);
10
+ * doWork(loader.signal).then(done);
11
+ */
12
+ export class CancellableLoader extends Loader {
13
+ abortController = new AbortController();
14
+ /** Called when user presses Escape */
15
+ onAbort;
16
+ /** AbortSignal that is aborted when user presses Escape */
17
+ get signal() {
18
+ return this.abortController.signal;
19
+ }
20
+ /** Whether the loader was aborted */
21
+ get aborted() {
22
+ return this.abortController.signal.aborted;
23
+ }
24
+ handleInput(data) {
25
+ if (isEscape(data)) {
26
+ this.abortController.abort();
27
+ this.onAbort?.();
28
+ }
29
+ }
30
+ dispose() {
31
+ this.stop();
32
+ }
33
+ }
34
+ //# sourceMappingURL=cancellable-loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cancellable-loader.js","sourceRoot":"","sources":["../../src/components/cancellable-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;GAQG;AACH,MAAM,OAAO,iBAAkB,SAAQ,MAAM;IACpC,eAAe,GAAG,IAAI,eAAe,EAAE,CAAC;IAEhD,sCAAsC;IACtC,OAAO,CAAc;IAErB,2DAA2D;IAC3D,IAAI,MAAM,GAAgB;QACzB,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC;IAAA,CACnC;IAED,qCAAqC;IACrC,IAAI,OAAO,GAAY;QACtB,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC;IAAA,CAC3C;IAED,WAAW,CAAC,IAAY,EAAQ;QAC/B,IAAI,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;QAClB,CAAC;IAAA,CACD;IAED,OAAO,GAAS;QACf,IAAI,CAAC,IAAI,EAAE,CAAC;IAAA,CACZ;CACD","sourcesContent":["import { isEscape } from \"../keys.js\";\nimport { Loader } from \"./loader.js\";\n\n/**\n * Loader that can be cancelled with Escape.\n * Extends Loader with an AbortSignal for cancelling async operations.\n *\n * @example\n * const loader = new CancellableLoader(tui, cyan, dim, \"Working...\");\n * loader.onAbort = () => done(null);\n * doWork(loader.signal).then(done);\n */\nexport class CancellableLoader extends Loader {\n\tprivate abortController = new AbortController();\n\n\t/** Called when user presses Escape */\n\tonAbort?: () => void;\n\n\t/** AbortSignal that is aborted when user presses Escape */\n\tget signal(): AbortSignal {\n\t\treturn this.abortController.signal;\n\t}\n\n\t/** Whether the loader was aborted */\n\tget aborted(): boolean {\n\t\treturn this.abortController.signal.aborted;\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (isEscape(data)) {\n\t\t\tthis.abortController.abort();\n\t\t\tthis.onAbort?.();\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.stop();\n\t}\n}\n"]}