@linxiraos/pi-tui 1.0.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/CHANGELOG.md +2219 -0
- package/README.md +705 -0
- package/dist/types/autocomplete.d.ts +116 -0
- package/dist/types/bracketed-paste.d.ts +51 -0
- package/dist/types/components/box.d.ts +31 -0
- package/dist/types/components/cancellable-loader.d.ts +21 -0
- package/dist/types/components/editor.d.ts +162 -0
- package/dist/types/components/image.d.ts +112 -0
- package/dist/types/components/input.d.ts +25 -0
- package/dist/types/components/loader.d.ts +25 -0
- package/dist/types/components/markdown.d.ts +88 -0
- package/dist/types/components/scroll-view.d.ts +62 -0
- package/dist/types/components/select-list.d.ts +69 -0
- package/dist/types/components/settings-list.d.ts +123 -0
- package/dist/types/components/spacer.d.ts +11 -0
- package/dist/types/components/tab-bar.d.ts +89 -0
- package/dist/types/components/text.d.ts +27 -0
- package/dist/types/components/truncated-text.d.ts +10 -0
- package/dist/types/deccara.d.ts +49 -0
- package/dist/types/desktop-notify.d.ts +52 -0
- package/dist/types/editor-component.d.ts +38 -0
- package/dist/types/fuzzy.d.ts +48 -0
- package/dist/types/index.d.ts +32 -0
- package/dist/types/keybindings.d.ts +197 -0
- package/dist/types/keys.d.ts +210 -0
- package/dist/types/kill-ring.d.ts +20 -0
- package/dist/types/kitty-graphics.d.ts +76 -0
- package/dist/types/latex-block.d.ts +8 -0
- package/dist/types/latex-to-unicode.d.ts +50 -0
- package/dist/types/loop-watchdog.d.ts +44 -0
- package/dist/types/mouse.d.ts +67 -0
- package/dist/types/stdin-buffer.d.ts +60 -0
- package/dist/types/symbols.d.ts +25 -0
- package/dist/types/terminal-capabilities.d.ts +285 -0
- package/dist/types/terminal.d.ts +175 -0
- package/dist/types/tmux.d.ts +6 -0
- package/dist/types/ttyid.d.ts +9 -0
- package/dist/types/tui.d.ts +457 -0
- package/dist/types/utils.d.ts +100 -0
- package/package.json +70 -0
- package/src/autocomplete.ts +1079 -0
- package/src/bracketed-paste.ts +123 -0
- package/src/components/box.ts +236 -0
- package/src/components/cancellable-loader.ts +40 -0
- package/src/components/editor.ts +3301 -0
- package/src/components/image.ts +460 -0
- package/src/components/input.ts +482 -0
- package/src/components/loader.ts +174 -0
- package/src/components/markdown.ts +3119 -0
- package/src/components/scroll-view.ts +227 -0
- package/src/components/select-list.ts +539 -0
- package/src/components/settings-list.ts +793 -0
- package/src/components/spacer.ts +32 -0
- package/src/components/tab-bar.ts +300 -0
- package/src/components/text.ts +173 -0
- package/src/components/truncated-text.ts +69 -0
- package/src/deccara.ts +314 -0
- package/src/desktop-notify.ts +192 -0
- package/src/editor-component.ts +74 -0
- package/src/fuzzy.ts +384 -0
- package/src/index.ts +51 -0
- package/src/keybindings.ts +346 -0
- package/src/keys.ts +566 -0
- package/src/kill-ring.ts +51 -0
- package/src/kitty-graphics.ts +171 -0
- package/src/latex-block.ts +1338 -0
- package/src/latex-to-unicode.ts +2017 -0
- package/src/loop-watchdog.ts +115 -0
- package/src/mouse.ts +105 -0
- package/src/stdin-buffer.ts +781 -0
- package/src/symbols.ts +26 -0
- package/src/terminal-capabilities.ts +1211 -0
- package/src/terminal.ts +1854 -0
- package/src/tmux.ts +14 -0
- package/src/ttyid.ts +84 -0
- package/src/tui.ts +4275 -0
- package/src/utils.ts +619 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const PASTE_START = "\x1b[200~";
|
|
2
|
+
const PASTE_END = "\x1b[201~";
|
|
3
|
+
|
|
4
|
+
export type PasteResult = { handled: false } | { handled: true; pasteContent?: string; remaining: string };
|
|
5
|
+
|
|
6
|
+
// Some terminals re-encode the control bytes inside a bracketed paste as key-event
|
|
7
|
+
// escape sequences (observed with tmux extended-keys passthrough under kitty). tmux
|
|
8
|
+
// emits one of two formats depending on `extended-keys-format`:
|
|
9
|
+
// - csi-u: ESC [ <codepoint> ; 5 u (Ctrl+J → ESC [ 106 ; 5 u)
|
|
10
|
+
// - xterm: ESC [ 27 ; 5 ; <codepoint> ~ (Ctrl+J → ESC [ 27 ; 5 ; 106 ~)
|
|
11
|
+
// Callers must decode these back to the literal control byte (Ctrl+J → "\n") before
|
|
12
|
+
// stripping control chars; otherwise ESC is dropped and the printable tail
|
|
13
|
+
// ("[106;5u" / "[27;5;106~") leaks into the editor.
|
|
14
|
+
//
|
|
15
|
+
// Only Ctrl+<letter> is decoded (codepoint a-z/A-Z → 0x01..0x1A). That is the set tmux
|
|
16
|
+
// actually re-encodes from paste content in practice — TAB (Ctrl+I), LF (Ctrl+J), CR
|
|
17
|
+
// (Ctrl+M), VT (Ctrl+K), FF (Ctrl+L), … Non-letter Ctrl combos (NUL, ESC, FS-US, DEL)
|
|
18
|
+
// never appear as re-encoded paste bytes, so they are left untouched rather than
|
|
19
|
+
// synthesized into raw control bytes. Callers still strip leftover control characters
|
|
20
|
+
// after decoding (the editor keeps "\n"; the single-line input strips all of them).
|
|
21
|
+
const REENCODED_CTRL_CSI_U = /\x1b\[(\d+);5u/g;
|
|
22
|
+
const REENCODED_CTRL_XTERM = /\x1b\[27;5;(\d+)~/g;
|
|
23
|
+
|
|
24
|
+
function decodeReencodedCtrlByte(match: string, code: string): string {
|
|
25
|
+
const cp = Number(code);
|
|
26
|
+
if (cp >= 97 && cp <= 122) return String.fromCharCode(cp - 96); // a-z → Ctrl+A..Ctrl+Z
|
|
27
|
+
if (cp >= 65 && cp <= 90) return String.fromCharCode(cp - 64); // A-Z → Ctrl+A..Ctrl+Z
|
|
28
|
+
return match;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Decode tmux's re-encoded control bytes (both `extended-keys-format` variants) inside a
|
|
33
|
+
* bracketed-paste payload back to their literal byte (e.g. Ctrl+J → "\n"). Leaves the rest of
|
|
34
|
+
* the text untouched. Call before any control-character stripping so newlines/tabs survive
|
|
35
|
+
* instead of leaking the printable escape tail into the buffer.
|
|
36
|
+
*/
|
|
37
|
+
export function decodeReencodedPasteControls(text: string): string {
|
|
38
|
+
return text
|
|
39
|
+
.replace(REENCODED_CTRL_CSI_U, decodeReencodedCtrlByte)
|
|
40
|
+
.replace(REENCODED_CTRL_XTERM, decodeReencodedCtrlByte);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Options for {@link BracketedPasteHandler}.
|
|
45
|
+
*/
|
|
46
|
+
export type BracketedPasteHandlerOptions = {
|
|
47
|
+
/**
|
|
48
|
+
* Byte cap for buffered paste content (default: 64 MiB). When exceeded,
|
|
49
|
+
* paste mode is aborted and the accumulated content is delivered as
|
|
50
|
+
* `pasteContent` on the same `process()` call so a lost/corrupted end
|
|
51
|
+
* marker cannot consume unbounded memory. Mirrors `StdinBuffer#abortPaste`
|
|
52
|
+
* — defense in depth for callers that bypass `StdinBuffer` (issue #4073
|
|
53
|
+
* case B). The normal `ProcessTerminal` path re-wraps `StdinBuffer`'s
|
|
54
|
+
* bounded paste with both markers, so this cap only fires on alternate
|
|
55
|
+
* callers.
|
|
56
|
+
*/
|
|
57
|
+
byteLimit?: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const DEFAULT_BYTE_LIMIT = 64 * 1024 * 1024;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handles bracketed paste mode buffering for terminal input components.
|
|
64
|
+
*
|
|
65
|
+
* Bracketed paste mode wraps pasted content between start (\x1b[200~) and
|
|
66
|
+
* end (\x1b[201~) markers, which may arrive split across multiple chunks.
|
|
67
|
+
* This class buffers incoming data and assembles complete paste payloads.
|
|
68
|
+
*/
|
|
69
|
+
export class BracketedPasteHandler {
|
|
70
|
+
#buffer = "";
|
|
71
|
+
#active = false;
|
|
72
|
+
readonly #byteLimit: number;
|
|
73
|
+
|
|
74
|
+
constructor(options: BracketedPasteHandlerOptions = {}) {
|
|
75
|
+
this.#byteLimit = options.byteLimit ?? DEFAULT_BYTE_LIMIT;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Process incoming terminal data for bracketed paste sequences.
|
|
80
|
+
*
|
|
81
|
+
* @returns `{ handled: false }` if the data contains no paste sequence and
|
|
82
|
+
* should be processed normally. `{ handled: true }` if the data was
|
|
83
|
+
* consumed by paste buffering — `pasteContent` is set when a complete
|
|
84
|
+
* paste has been assembled (or the byte cap has aborted a runaway
|
|
85
|
+
* buffer); omitted when still buffering.
|
|
86
|
+
*/
|
|
87
|
+
process(data: string): PasteResult {
|
|
88
|
+
if (data.includes(PASTE_START)) {
|
|
89
|
+
this.#active = true;
|
|
90
|
+
this.#buffer = "";
|
|
91
|
+
data = data.replace(PASTE_START, "");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!this.#active) return { handled: false };
|
|
95
|
+
|
|
96
|
+
this.#buffer += data;
|
|
97
|
+
|
|
98
|
+
const endIndex = this.#buffer.indexOf(PASTE_END);
|
|
99
|
+
if (endIndex !== -1) {
|
|
100
|
+
const pasteContent = this.#buffer.substring(0, endIndex);
|
|
101
|
+
const remaining = this.#buffer.substring(endIndex + PASTE_END.length);
|
|
102
|
+
|
|
103
|
+
this.#buffer = "";
|
|
104
|
+
this.#active = false;
|
|
105
|
+
|
|
106
|
+
return { handled: true, pasteContent, remaining };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Byte cap: a lost/corrupted end marker (ssh/tmux truncation) must not
|
|
110
|
+
// consume unbounded memory. Deliver the accumulated bytes so they are
|
|
111
|
+
// neither lost nor held forever, and reset paste mode so subsequent
|
|
112
|
+
// input recovers. See `StdinBuffer#abortPaste` for the sibling recovery
|
|
113
|
+
// semantics inside `StdinBuffer`.
|
|
114
|
+
if (this.#buffer.length > this.#byteLimit) {
|
|
115
|
+
const pasteContent = this.#buffer;
|
|
116
|
+
this.#buffer = "";
|
|
117
|
+
this.#active = false;
|
|
118
|
+
return { handled: true, pasteContent, remaining: "" };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { handled: true, remaining: "" };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { Component } from "../tui";
|
|
2
|
+
import {
|
|
3
|
+
getPaddingX,
|
|
4
|
+
getPublishedLineWidths,
|
|
5
|
+
getWidthConfigEpoch,
|
|
6
|
+
padding,
|
|
7
|
+
publishLineWidths,
|
|
8
|
+
visibleWidth,
|
|
9
|
+
} from "../utils";
|
|
10
|
+
|
|
11
|
+
type Cache = {
|
|
12
|
+
width: number;
|
|
13
|
+
widthEpoch: number;
|
|
14
|
+
bgSample: string | undefined;
|
|
15
|
+
borderSample: string | undefined;
|
|
16
|
+
childLines: (readonly string[])[];
|
|
17
|
+
childWidths: (readonly number[] | undefined)[];
|
|
18
|
+
childSnapshots: (readonly string[] | undefined)[];
|
|
19
|
+
result: string[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Box-drawing glyphs plus an optional colorizer for an outline drawn around a {@link Box}. */
|
|
23
|
+
export interface BoxBorder {
|
|
24
|
+
chars: {
|
|
25
|
+
topLeft: string;
|
|
26
|
+
topRight: string;
|
|
27
|
+
bottomLeft: string;
|
|
28
|
+
bottomRight: string;
|
|
29
|
+
horizontal: string;
|
|
30
|
+
vertical: string;
|
|
31
|
+
};
|
|
32
|
+
color?: (text: string) => string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Box component - a container that applies padding and background to all children
|
|
37
|
+
*/
|
|
38
|
+
export class Box implements Component {
|
|
39
|
+
children: Component[] = [];
|
|
40
|
+
#paddingX: number;
|
|
41
|
+
#paddingY: number;
|
|
42
|
+
#bgFn?: (text: string) => string;
|
|
43
|
+
#border?: BoxBorder;
|
|
44
|
+
|
|
45
|
+
#ignoreTight = false;
|
|
46
|
+
|
|
47
|
+
setIgnoreTight(ignore: boolean): this {
|
|
48
|
+
this.#ignoreTight = ignore;
|
|
49
|
+
this.#invalidateCache();
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Cache for rendered output
|
|
54
|
+
#cached?: Cache;
|
|
55
|
+
|
|
56
|
+
constructor(paddingX = 1, paddingY = 1, bgFn?: (text: string) => string, border?: BoxBorder) {
|
|
57
|
+
this.#paddingX = paddingX;
|
|
58
|
+
this.#paddingY = paddingY;
|
|
59
|
+
this.#bgFn = bgFn;
|
|
60
|
+
this.#border = border;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
addChild(component: Component): void {
|
|
64
|
+
this.children.push(component);
|
|
65
|
+
if (this.#ignoreTight) {
|
|
66
|
+
component.setIgnoreTight?.(true);
|
|
67
|
+
}
|
|
68
|
+
this.#invalidateCache();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
removeChild(component: Component): void {
|
|
72
|
+
const index = this.children.indexOf(component);
|
|
73
|
+
if (index !== -1) {
|
|
74
|
+
this.children.splice(index, 1);
|
|
75
|
+
this.#invalidateCache();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear(): void {
|
|
80
|
+
this.children = [];
|
|
81
|
+
this.#invalidateCache();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setPaddingX(paddingX: number): void {
|
|
85
|
+
if (this.#paddingX === paddingX) return;
|
|
86
|
+
this.#paddingX = paddingX;
|
|
87
|
+
this.#invalidateCache();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
setPaddingY(paddingY: number): void {
|
|
91
|
+
if (this.#paddingY === paddingY) return;
|
|
92
|
+
this.#paddingY = paddingY;
|
|
93
|
+
this.#invalidateCache();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
setBgFn(bgFn?: (text: string) => string): void {
|
|
97
|
+
this.#bgFn = bgFn;
|
|
98
|
+
// Don't invalidate here - we'll detect bgFn changes by sampling output
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
setBorder(border?: BoxBorder): void {
|
|
102
|
+
this.#border = border;
|
|
103
|
+
this.#invalidateCache();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
#invalidateCache(): void {
|
|
107
|
+
this.#cached = undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
invalidate(): void {
|
|
111
|
+
this.#invalidateCache();
|
|
112
|
+
for (const child of this.children) {
|
|
113
|
+
child.invalidate?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
render(width: number): readonly string[] {
|
|
118
|
+
const children = this.children;
|
|
119
|
+
const count = children.length;
|
|
120
|
+
const paddingX = this.#ignoreTight ? this.#paddingX : getPaddingX(this.#paddingX);
|
|
121
|
+
// A border eats one column on each side; skip it unless the interior can still
|
|
122
|
+
// hold the horizontal padding plus at least one content column, so a bordered
|
|
123
|
+
// Box never overflows the width it was given.
|
|
124
|
+
const border = this.#border && width - 2 >= paddingX * 2 + 1 ? this.#border : undefined;
|
|
125
|
+
const innerWidth = border ? width - 2 : width;
|
|
126
|
+
const contentWidth = Math.max(1, innerWidth - paddingX * 2);
|
|
127
|
+
// bgFn / border output can change without the function reference changing
|
|
128
|
+
// (theme mutation); sample both so a silent palette swap still misses the cache.
|
|
129
|
+
const bgSample = this.#bgFn ? this.#bgFn("test") : undefined;
|
|
130
|
+
const borderSample = border
|
|
131
|
+
? `${border.color ? border.color("|") : "|"}${border.chars.topLeft}${border.chars.vertical}`
|
|
132
|
+
: undefined;
|
|
133
|
+
|
|
134
|
+
// Render every child every frame (renders may carry side effects); the
|
|
135
|
+
// memo only skips re-deriving the padded/background rows.
|
|
136
|
+
const widthEpoch = getWidthConfigEpoch();
|
|
137
|
+
let contentRows = 0;
|
|
138
|
+
const childLines = children.map(child => {
|
|
139
|
+
const lines = child.render(contentWidth);
|
|
140
|
+
contentRows += lines.length;
|
|
141
|
+
return lines;
|
|
142
|
+
});
|
|
143
|
+
const childWidths = childLines.map(lines => getPublishedLineWidths(lines));
|
|
144
|
+
const cached = this.#cached;
|
|
145
|
+
if (
|
|
146
|
+
cached !== undefined &&
|
|
147
|
+
cached.width === width &&
|
|
148
|
+
cached.widthEpoch === widthEpoch &&
|
|
149
|
+
cached.widthEpoch === getWidthConfigEpoch() &&
|
|
150
|
+
cached.bgSample === bgSample &&
|
|
151
|
+
cached.borderSample === borderSample &&
|
|
152
|
+
cached.childLines.length === count &&
|
|
153
|
+
childLines.every((lines, i) => {
|
|
154
|
+
if (cached.childLines[i] !== lines) return false;
|
|
155
|
+
const published = childWidths[i];
|
|
156
|
+
const cachedPublished = cached.childWidths[i];
|
|
157
|
+
if (published !== undefined || cachedPublished !== undefined) {
|
|
158
|
+
return published === cachedPublished;
|
|
159
|
+
}
|
|
160
|
+
const snapshot = cached.childSnapshots[i];
|
|
161
|
+
return (
|
|
162
|
+
snapshot !== undefined &&
|
|
163
|
+
snapshot.length === lines.length &&
|
|
164
|
+
lines.every((line, j) => snapshot[j] === line)
|
|
165
|
+
);
|
|
166
|
+
})
|
|
167
|
+
) {
|
|
168
|
+
return cached.result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result: string[] = [];
|
|
172
|
+
// Exact visible widths of `result` rows, published only when the row
|
|
173
|
+
// bytes are `content + spaces` (no bg/border transform of unknown width).
|
|
174
|
+
const resultWidths: number[] | undefined = !border && !this.#bgFn ? [] : undefined;
|
|
175
|
+
if (contentRows > 0) {
|
|
176
|
+
const leftPad = padding(paddingX);
|
|
177
|
+
const interior: string[] = [];
|
|
178
|
+
const pushRow = (row: string, visLen: number): void => {
|
|
179
|
+
const padNeeded = Math.max(0, innerWidth - visLen);
|
|
180
|
+
const padded = padNeeded > 0 ? row + padding(padNeeded) : row;
|
|
181
|
+
interior.push(this.#bgFn ? this.#bgFn(padded) : padded);
|
|
182
|
+
resultWidths?.push(visLen + padNeeded);
|
|
183
|
+
};
|
|
184
|
+
// Top padding
|
|
185
|
+
for (let i = 0; i < this.#paddingY; i++) {
|
|
186
|
+
pushRow("", 0);
|
|
187
|
+
}
|
|
188
|
+
// Content
|
|
189
|
+
let childIndex = 0;
|
|
190
|
+
for (const lines of childLines) {
|
|
191
|
+
const widths = childWidths[childIndex++];
|
|
192
|
+
for (let j = 0; j < lines.length; j++) {
|
|
193
|
+
const line = lines[j] ?? "";
|
|
194
|
+
const row = paddingX > 0 ? leftPad + line : line;
|
|
195
|
+
const carried = widths?.[j];
|
|
196
|
+
const visLen = carried !== undefined && paddingX === 0 ? carried : visibleWidth(row);
|
|
197
|
+
pushRow(row, visLen);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Bottom padding
|
|
201
|
+
for (let i = 0; i < this.#paddingY; i++) {
|
|
202
|
+
pushRow("", 0);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (border) {
|
|
206
|
+
const paint = border.color ?? (s => s);
|
|
207
|
+
const rule = border.chars.horizontal.repeat(Math.max(0, innerWidth));
|
|
208
|
+
const side = paint(border.chars.vertical);
|
|
209
|
+
result.push(paint(border.chars.topLeft + rule + border.chars.topRight));
|
|
210
|
+
for (const row of interior) {
|
|
211
|
+
result.push(side + row + side);
|
|
212
|
+
}
|
|
213
|
+
result.push(paint(border.chars.bottomLeft + rule + border.chars.bottomRight));
|
|
214
|
+
} else {
|
|
215
|
+
for (const row of interior) {
|
|
216
|
+
result.push(row);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const finalWidthEpoch = getWidthConfigEpoch();
|
|
222
|
+
if (resultWidths !== undefined) publishLineWidths(result, resultWidths);
|
|
223
|
+
const childSnapshots = childLines.map((lines, i) => (childWidths[i] === undefined ? [...lines] : undefined));
|
|
224
|
+
this.#cached = {
|
|
225
|
+
width,
|
|
226
|
+
widthEpoch: finalWidthEpoch,
|
|
227
|
+
bgSample,
|
|
228
|
+
borderSample,
|
|
229
|
+
childLines,
|
|
230
|
+
childWidths,
|
|
231
|
+
childSnapshots,
|
|
232
|
+
result,
|
|
233
|
+
};
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { getKeybindings } from "../keybindings";
|
|
2
|
+
import { Loader } from "./loader";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Loader that can be cancelled with Escape.
|
|
6
|
+
* Extends Loader with an AbortSignal for cancelling async operations.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const loader = new CancellableLoader(tui, cyan, dim, "Working...");
|
|
10
|
+
* loader.onAbort = () => done(null);
|
|
11
|
+
* doWork(loader.signal).then(done);
|
|
12
|
+
*/
|
|
13
|
+
export class CancellableLoader extends Loader {
|
|
14
|
+
#abortController = new AbortController();
|
|
15
|
+
|
|
16
|
+
/** Called when user presses Escape */
|
|
17
|
+
onAbort?: () => void;
|
|
18
|
+
|
|
19
|
+
/** AbortSignal that is aborted when user presses Escape */
|
|
20
|
+
get signal(): AbortSignal {
|
|
21
|
+
return this.#abortController.signal;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Whether the loader was aborted */
|
|
25
|
+
get aborted(): boolean {
|
|
26
|
+
return this.#abortController.signal.aborted;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
handleInput(data: string): void {
|
|
30
|
+
const kb = getKeybindings();
|
|
31
|
+
if (kb.matches(data, "tui.select.cancel")) {
|
|
32
|
+
this.#abortController.abort();
|
|
33
|
+
this.onAbort?.();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override dispose(): void {
|
|
38
|
+
this.stop();
|
|
39
|
+
}
|
|
40
|
+
}
|