@textui/terminal 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/LICENSE +21 -0
- package/README.md +83 -0
- package/dist/ansi.d.ts +84 -0
- package/dist/ansi.d.ts.map +1 -0
- package/dist/ansi.js +102 -0
- package/dist/capabilities.d.ts +22 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +130 -0
- package/dist/capture.d.ts +24 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +72 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/input.d.ts +28 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +357 -0
- package/dist/node.d.ts +99 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +233 -0
- package/dist/svg.d.ts +74 -0
- package/dist/svg.d.ts.map +1 -0
- package/dist/svg.js +235 -0
- package/dist/virtual.d.ts +65 -0
- package/dist/virtual.d.ts.map +1 -0
- package/dist/virtual.js +137 -0
- package/dist/writer.d.ts +19 -0
- package/dist/writer.d.ts.map +1 -0
- package/dist/writer.js +173 -0
- package/package.json +62 -0
- package/src/ansi.ts +120 -0
- package/src/capabilities.ts +140 -0
- package/src/capture.ts +101 -0
- package/src/index.ts +17 -0
- package/src/input.ts +394 -0
- package/src/node.ts +329 -0
- package/src/svg.ts +352 -0
- package/src/virtual.ts +185 -0
- package/src/writer.ts +202 -0
package/dist/input.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input decoding.
|
|
3
|
+
*
|
|
4
|
+
* A terminal delivers keys as bytes that may arrive split across reads, so the
|
|
5
|
+
* decoder is a small state machine over a carry buffer rather than a parser
|
|
6
|
+
* over whole strings. When a sequence is incomplete it stays in the buffer and
|
|
7
|
+
* waits, which is what stops a fast paste from being read as a burst of keys.
|
|
8
|
+
*/
|
|
9
|
+
const ESC = '\x1b';
|
|
10
|
+
function key(name, partial = {}) {
|
|
11
|
+
return {
|
|
12
|
+
type: 'key',
|
|
13
|
+
name,
|
|
14
|
+
ctrl: false,
|
|
15
|
+
alt: false,
|
|
16
|
+
shift: false,
|
|
17
|
+
meta: false,
|
|
18
|
+
raw: partial.raw ?? name,
|
|
19
|
+
handled: false,
|
|
20
|
+
...partial,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/** CSI final byte to key name, for the `ESC [ A` family. */
|
|
24
|
+
const CSI_LETTERS = {
|
|
25
|
+
A: 'up', B: 'down', C: 'right', D: 'left',
|
|
26
|
+
E: 'center', F: 'end', H: 'home',
|
|
27
|
+
P: 'f1', Q: 'f2', R: 'f3', S: 'f4',
|
|
28
|
+
Z: 'tab', // shift+tab arrives as CSI Z
|
|
29
|
+
};
|
|
30
|
+
/** `ESC [ <n> ~` numbers. */
|
|
31
|
+
const CSI_NUMBERS = {
|
|
32
|
+
1: 'home', 2: 'insert', 3: 'delete', 4: 'end', 5: 'pageup', 6: 'pagedown',
|
|
33
|
+
7: 'home', 8: 'end',
|
|
34
|
+
11: 'f1', 12: 'f2', 13: 'f3', 14: 'f4', 15: 'f5',
|
|
35
|
+
17: 'f6', 18: 'f7', 19: 'f8', 20: 'f9', 21: 'f10',
|
|
36
|
+
23: 'f11', 24: 'f12',
|
|
37
|
+
};
|
|
38
|
+
/** xterm modifier parameter: 1 + bitmask. */
|
|
39
|
+
function modifiers(param) {
|
|
40
|
+
const bits = (param ?? 1) - 1;
|
|
41
|
+
return {
|
|
42
|
+
shift: (bits & 1) !== 0,
|
|
43
|
+
alt: (bits & 2) !== 0,
|
|
44
|
+
ctrl: (bits & 4) !== 0,
|
|
45
|
+
meta: (bits & 8) !== 0,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const CTRL_NAMES = {
|
|
49
|
+
0x00: 'space', // ctrl+space
|
|
50
|
+
0x08: 'backspace',
|
|
51
|
+
0x09: 'tab',
|
|
52
|
+
0x0a: 'enter',
|
|
53
|
+
0x0d: 'enter',
|
|
54
|
+
0x1b: 'escape',
|
|
55
|
+
0x7f: 'backspace',
|
|
56
|
+
};
|
|
57
|
+
export class InputDecoder {
|
|
58
|
+
emit;
|
|
59
|
+
options;
|
|
60
|
+
carry = '';
|
|
61
|
+
pasting = false;
|
|
62
|
+
pasteBuffer = '';
|
|
63
|
+
escapeTimer = null;
|
|
64
|
+
constructor(emit, options = {}) {
|
|
65
|
+
this.emit = emit;
|
|
66
|
+
this.options = options;
|
|
67
|
+
}
|
|
68
|
+
/** Feed raw input. Emits zero or more events. */
|
|
69
|
+
feed(chunk) {
|
|
70
|
+
this.clearEscapeTimer();
|
|
71
|
+
this.carry += chunk;
|
|
72
|
+
for (;;) {
|
|
73
|
+
const consumed = this.step();
|
|
74
|
+
if (consumed === 0)
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
// A lone ESC is ambiguous until either more bytes arrive or time passes.
|
|
78
|
+
if (this.carry === ESC) {
|
|
79
|
+
this.escapeTimer = setTimeout(() => {
|
|
80
|
+
if (this.carry === ESC) {
|
|
81
|
+
this.carry = '';
|
|
82
|
+
this.emit(key('escape', { raw: ESC }));
|
|
83
|
+
}
|
|
84
|
+
}, this.options.escapeTimeoutMs ?? 30);
|
|
85
|
+
this.escapeTimer.unref?.();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
clearEscapeTimer() {
|
|
89
|
+
if (this.escapeTimer) {
|
|
90
|
+
clearTimeout(this.escapeTimer);
|
|
91
|
+
this.escapeTimer = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Consume one event's worth of bytes. Returns how many were used. */
|
|
95
|
+
step() {
|
|
96
|
+
if (this.carry === '')
|
|
97
|
+
return 0;
|
|
98
|
+
if (this.pasting)
|
|
99
|
+
return this.stepPaste();
|
|
100
|
+
const first = this.carry[0];
|
|
101
|
+
if (first !== ESC)
|
|
102
|
+
return this.stepPlain();
|
|
103
|
+
if (this.carry.length === 1)
|
|
104
|
+
return 0; // wait for more
|
|
105
|
+
const second = this.carry[1];
|
|
106
|
+
if (second === '[')
|
|
107
|
+
return this.stepCsi();
|
|
108
|
+
if (second === 'O')
|
|
109
|
+
return this.stepSs3();
|
|
110
|
+
// ESC followed by a printable character is alt+that.
|
|
111
|
+
if (second !== ESC) {
|
|
112
|
+
const cp = this.carry.codePointAt(1);
|
|
113
|
+
const char = String.fromCodePoint(cp);
|
|
114
|
+
const raw = ESC + char;
|
|
115
|
+
this.carry = this.carry.slice(raw.length);
|
|
116
|
+
/*
|
|
117
|
+
* A control byte after ESC is alt+that key - and only *also* ctrl when
|
|
118
|
+
* the byte has no name of its own.
|
|
119
|
+
*
|
|
120
|
+
* `alt+enter` arrives as ESC then 0x0d, and 0x0d is the enter key, not
|
|
121
|
+
* ctrl+m: reporting `ctrl` beside it filed the stroke as
|
|
122
|
+
* `ctrl+alt+enter`, which is not what anybody binds and not what the
|
|
123
|
+
* same key produces without the ESC. `stepPlain` has always had this
|
|
124
|
+
* rule - `ctrl: cp === 0x00` - and this path did not, so `alt+tab`,
|
|
125
|
+
* `alt+backspace` and `alt+enter` were all unreachable.
|
|
126
|
+
*
|
|
127
|
+
* 0x00 keeps it, because that one really is ctrl+space.
|
|
128
|
+
*/
|
|
129
|
+
// 0x7f as well as 0x20 and below: that is the backspace most terminals
|
|
130
|
+
// send, and `stepPlain` has always counted it as a control. Leaving it
|
|
131
|
+
// out here made `alt+backspace` a key named "\x7f".
|
|
132
|
+
const control = cp < 0x20 || cp === 0x7f;
|
|
133
|
+
const named = control ? CTRL_NAMES[cp] : undefined;
|
|
134
|
+
this.emit(control
|
|
135
|
+
? key(named ?? String.fromCharCode(cp + 96), {
|
|
136
|
+
alt: true,
|
|
137
|
+
...(named === undefined || cp === 0x00 ? { ctrl: true } : {}),
|
|
138
|
+
raw,
|
|
139
|
+
})
|
|
140
|
+
: key(char, { char, alt: true, raw, shift: char !== char.toLowerCase() }));
|
|
141
|
+
return raw.length;
|
|
142
|
+
}
|
|
143
|
+
// Two escapes: report the first as the escape key.
|
|
144
|
+
this.carry = this.carry.slice(1);
|
|
145
|
+
this.emit(key('escape', { raw: ESC }));
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
stepPlain() {
|
|
149
|
+
const cp = this.carry.codePointAt(0);
|
|
150
|
+
const char = String.fromCodePoint(cp);
|
|
151
|
+
if (cp < 0x20 || cp === 0x7f) {
|
|
152
|
+
this.carry = this.carry.slice(char.length);
|
|
153
|
+
const named = CTRL_NAMES[cp];
|
|
154
|
+
if (named) {
|
|
155
|
+
/*
|
|
156
|
+
* 0x0a is `ctrl+enter`; 0x0d is enter.
|
|
157
|
+
*
|
|
158
|
+
* Both were named `enter` with no modifier, which made them the same
|
|
159
|
+
* key - and that is what a composer sees when it offers "enter sends,
|
|
160
|
+
* ctrl+enter is a newline" and the newline never comes. In raw mode
|
|
161
|
+
* the Return key sends CR: the kernel's CR-to-NL translation is off,
|
|
162
|
+
* so a bare LF is not Return, it is ctrl+Return (or ctrl+j, which is
|
|
163
|
+
* the same byte and therefore the same key - there is no encoding in
|
|
164
|
+
* which those two differ).
|
|
165
|
+
*
|
|
166
|
+
* Pasted newlines do not come through here: a bracketed paste is
|
|
167
|
+
* buffered whole by `stepPaste` and emitted as a paste event.
|
|
168
|
+
*
|
|
169
|
+
* 0x00 keeps its ctrl for the same reason it always had it: that byte
|
|
170
|
+
* really is ctrl+space.
|
|
171
|
+
*/
|
|
172
|
+
this.emit(key(named, { raw: char, ctrl: cp === 0x00 || cp === 0x0a }));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
// 0x01..0x1a are ctrl+a .. ctrl+z
|
|
176
|
+
const letter = String.fromCharCode(cp + 96);
|
|
177
|
+
this.emit(key(letter, { ctrl: true, raw: char }));
|
|
178
|
+
}
|
|
179
|
+
return char.length;
|
|
180
|
+
}
|
|
181
|
+
this.carry = this.carry.slice(char.length);
|
|
182
|
+
// Space is the one printable with a name, because it is the one printable
|
|
183
|
+
// people bind to. `KeyName` has always listed it and `Button`, `Checkbox`,
|
|
184
|
+
// `Switch` and `Select` have always tested for it - and none of them ever
|
|
185
|
+
// saw it, because a terminal sends 0x20 and this named it `' '`. The
|
|
186
|
+
// harness synthesised `'space'`, so every one of those tests passed while
|
|
187
|
+
// nothing worked. `char` stays `' '`, so typing one still types one.
|
|
188
|
+
const name = cp === 0x20 ? 'space' : char;
|
|
189
|
+
this.emit(key(name, { char, raw: char, shift: char !== char.toLowerCase() && char.toLowerCase() !== char.toUpperCase() }));
|
|
190
|
+
return char.length;
|
|
191
|
+
}
|
|
192
|
+
stepPaste() {
|
|
193
|
+
const end = this.carry.indexOf(`${ESC}[201~`);
|
|
194
|
+
if (end === -1) {
|
|
195
|
+
this.pasteBuffer += this.carry;
|
|
196
|
+
const used = this.carry.length;
|
|
197
|
+
this.carry = '';
|
|
198
|
+
return used;
|
|
199
|
+
}
|
|
200
|
+
this.pasteBuffer += this.carry.slice(0, end);
|
|
201
|
+
const used = end + 6;
|
|
202
|
+
this.carry = this.carry.slice(used);
|
|
203
|
+
this.pasting = false;
|
|
204
|
+
const text = this.pasteBuffer;
|
|
205
|
+
this.pasteBuffer = '';
|
|
206
|
+
this.emit({ type: 'paste', text, handled: false });
|
|
207
|
+
return used;
|
|
208
|
+
}
|
|
209
|
+
/** `ESC O P` - the application-cursor form of F1..F4 and the arrows. */
|
|
210
|
+
stepSs3() {
|
|
211
|
+
if (this.carry.length < 3)
|
|
212
|
+
return 0;
|
|
213
|
+
const final = this.carry[2];
|
|
214
|
+
const raw = this.carry.slice(0, 3);
|
|
215
|
+
this.carry = this.carry.slice(3);
|
|
216
|
+
const name = CSI_LETTERS[final];
|
|
217
|
+
if (name)
|
|
218
|
+
this.emit(key(name, { raw }));
|
|
219
|
+
return 3;
|
|
220
|
+
}
|
|
221
|
+
stepCsi() {
|
|
222
|
+
// Find the final byte: the first in the range @ to ~ after the parameters.
|
|
223
|
+
let i = 2;
|
|
224
|
+
while (i < this.carry.length) {
|
|
225
|
+
const code = this.carry.charCodeAt(i);
|
|
226
|
+
if (code >= 0x40 && code <= 0x7e)
|
|
227
|
+
break;
|
|
228
|
+
i++;
|
|
229
|
+
}
|
|
230
|
+
if (i >= this.carry.length)
|
|
231
|
+
return 0; // incomplete
|
|
232
|
+
const final = this.carry[i];
|
|
233
|
+
const body = this.carry.slice(2, i);
|
|
234
|
+
const raw = this.carry.slice(0, i + 1);
|
|
235
|
+
// Bracketed paste start.
|
|
236
|
+
if (body === '200' && final === '~') {
|
|
237
|
+
this.carry = this.carry.slice(i + 1);
|
|
238
|
+
this.pasting = true;
|
|
239
|
+
this.pasteBuffer = '';
|
|
240
|
+
return raw.length;
|
|
241
|
+
}
|
|
242
|
+
// SGR mouse: CSI < b ; x ; y M|m
|
|
243
|
+
if (body.startsWith('<') && (final === 'M' || final === 'm')) {
|
|
244
|
+
this.carry = this.carry.slice(i + 1);
|
|
245
|
+
const event = decodeSgrMouse(body.slice(1), final);
|
|
246
|
+
if (event)
|
|
247
|
+
this.emit(event);
|
|
248
|
+
return raw.length;
|
|
249
|
+
}
|
|
250
|
+
// Terminal focus in/out.
|
|
251
|
+
if (final === 'I' || final === 'O') {
|
|
252
|
+
this.carry = this.carry.slice(i + 1);
|
|
253
|
+
this.emit({ type: 'terminal-focus', focused: final === 'I' });
|
|
254
|
+
return raw.length;
|
|
255
|
+
}
|
|
256
|
+
this.carry = this.carry.slice(i + 1);
|
|
257
|
+
const params = body.split(';').map((p) => (p === '' ? undefined : Number.parseInt(p, 10)));
|
|
258
|
+
// Kitty keyboard: CSI codepoint ; modifiers u
|
|
259
|
+
if (final === 'u') {
|
|
260
|
+
const cp = params[0];
|
|
261
|
+
if (cp !== undefined) {
|
|
262
|
+
const mods = modifiers(params[1]);
|
|
263
|
+
const char = cp >= 0x20 ? String.fromCodePoint(cp) : undefined;
|
|
264
|
+
const named = CTRL_NAMES[cp];
|
|
265
|
+
this.emit(key(named ?? char ?? String(cp), { ...mods, char, raw }));
|
|
266
|
+
}
|
|
267
|
+
return raw.length;
|
|
268
|
+
}
|
|
269
|
+
if (final === '~') {
|
|
270
|
+
/*
|
|
271
|
+
* xterm's `modifyOtherKeys`: CSI 27 ; modifiers ; codepoint ~
|
|
272
|
+
*
|
|
273
|
+
* The *other* way a terminal can say `ctrl+enter`, and the one that was
|
|
274
|
+
* going straight in the bin: 27 is not in `CSI_NUMBERS`, so
|
|
275
|
+
* `CSI 27;5;13~` matched nothing, fell through every branch and the key
|
|
276
|
+
* did nothing at all - not "arrived as plain enter", nothing.
|
|
277
|
+
*
|
|
278
|
+
* It carries the same information as the kitty form with the parameters
|
|
279
|
+
* the other way round, so it decodes through the same rules. Terminals
|
|
280
|
+
* that will not do the kitty protocol often do this one, which makes it
|
|
281
|
+
* the difference between `ctrl+enter` existing and not.
|
|
282
|
+
*/
|
|
283
|
+
if (params[0] === 27 && params[2] !== undefined) {
|
|
284
|
+
const cp = params[2];
|
|
285
|
+
const char = cp >= 0x20 ? String.fromCodePoint(cp) : undefined;
|
|
286
|
+
this.emit(key(CTRL_NAMES[cp] ?? char ?? String(cp), {
|
|
287
|
+
...modifiers(params[1]),
|
|
288
|
+
char,
|
|
289
|
+
raw,
|
|
290
|
+
}));
|
|
291
|
+
return raw.length;
|
|
292
|
+
}
|
|
293
|
+
const name = CSI_NUMBERS[params[0] ?? 0];
|
|
294
|
+
if (name)
|
|
295
|
+
this.emit(key(name, { ...modifiers(params[1]), raw }));
|
|
296
|
+
return raw.length;
|
|
297
|
+
}
|
|
298
|
+
const name = CSI_LETTERS[final];
|
|
299
|
+
if (name) {
|
|
300
|
+
if (final === 'Z') {
|
|
301
|
+
this.emit(key('tab', { shift: true, raw }));
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
// CSI 1 ; mod A
|
|
305
|
+
this.emit(key(name, { ...modifiers(params[1]), raw }));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return raw.length;
|
|
309
|
+
}
|
|
310
|
+
/** Drop any half-read sequence. After releasing the terminal. */
|
|
311
|
+
reset() {
|
|
312
|
+
this.clearEscapeTimer();
|
|
313
|
+
this.carry = '';
|
|
314
|
+
this.pasting = false;
|
|
315
|
+
this.pasteBuffer = '';
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function decodeSgrMouse(body, final) {
|
|
319
|
+
const parts = body.split(';').map((p) => Number.parseInt(p, 10));
|
|
320
|
+
if (parts.length < 3)
|
|
321
|
+
return null;
|
|
322
|
+
const [rawButton, col, row] = parts;
|
|
323
|
+
if (!Number.isFinite(rawButton) || !Number.isFinite(col) || !Number.isFinite(row))
|
|
324
|
+
return null;
|
|
325
|
+
const shift = (rawButton & 4) !== 0;
|
|
326
|
+
const alt = (rawButton & 8) !== 0;
|
|
327
|
+
const ctrl = (rawButton & 16) !== 0;
|
|
328
|
+
const motion = (rawButton & 32) !== 0;
|
|
329
|
+
const wheel = (rawButton & 64) !== 0;
|
|
330
|
+
const code = rawButton & 3;
|
|
331
|
+
const base = {
|
|
332
|
+
type: 'mouse',
|
|
333
|
+
x: col - 1,
|
|
334
|
+
y: row - 1,
|
|
335
|
+
ctrl, alt, shift,
|
|
336
|
+
handled: false,
|
|
337
|
+
};
|
|
338
|
+
if (wheel) {
|
|
339
|
+
return { ...base, action: 'wheel', button: 'none', wheel: code === 0 ? -1 : 1 };
|
|
340
|
+
}
|
|
341
|
+
const button = code === 0 ? 'left' : code === 1 ? 'middle' : code === 2 ? 'right' : 'none';
|
|
342
|
+
if (motion) {
|
|
343
|
+
return {
|
|
344
|
+
...base,
|
|
345
|
+
action: button === 'none' ? 'move' : 'drag',
|
|
346
|
+
button: button,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return {
|
|
350
|
+
...base,
|
|
351
|
+
action: final === 'M' ? 'down' : 'up',
|
|
352
|
+
button: button,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
export function createDecoder(emit, options) {
|
|
356
|
+
return new InputDecoder(emit, options);
|
|
357
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { AcquiredState, CapabilityOverrides, Disposable, InputEvent, Size, TerminalAdapter, TerminalCapabilities, TerminalSessionOptions } from '@textui/core';
|
|
2
|
+
/**
|
|
3
|
+
* The Node TTY adapter.
|
|
4
|
+
*
|
|
5
|
+
* Ownership is the whole point of this file. `acquire` records exactly what it
|
|
6
|
+
* turned on and `release` undoes exactly that - so an application that owns
|
|
7
|
+
* the terminal gets a clean alternate screen, and one embedded in a host that
|
|
8
|
+
* already set raw mode does not tear the host's state down on the way out.
|
|
9
|
+
*
|
|
10
|
+
* A terminal left in raw mode with the cursor hidden is a broken shell, so
|
|
11
|
+
* release also runs on exit and on a fatal signal, not only on a tidy stop.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* What this adapter needs from a stream, rather than which stream it is.
|
|
15
|
+
*
|
|
16
|
+
* `process.stdin` and `process.stdout` satisfy these, so nothing changes for
|
|
17
|
+
* a caller - but the published types stop naming Node's stream interfaces,
|
|
18
|
+
* which come from a package this one does not depend on. It compiled here
|
|
19
|
+
* only because those types happened to be installed at the root; a consumer
|
|
20
|
+
* without them got `Cannot find namespace` out of a package that advertises
|
|
21
|
+
* no dependencies.
|
|
22
|
+
*
|
|
23
|
+
* Saying it structurally also makes the requirement legible - "something with
|
|
24
|
+
* `setRawMode` and `columns`" is a contract, where "Node's stream" is a shrug
|
|
25
|
+
* - and makes it true of the runtimes whose streams are not Node's. Bun and
|
|
26
|
+
* Deno satisfy these by shape rather than by luck.
|
|
27
|
+
*/
|
|
28
|
+
export interface TerminalInput {
|
|
29
|
+
isTTY?: boolean;
|
|
30
|
+
/** True while the terminal is delivering keys rather than lines. */
|
|
31
|
+
isRaw?: boolean;
|
|
32
|
+
setRawMode?(raw: boolean): void;
|
|
33
|
+
setEncoding(encoding: 'utf8'): void;
|
|
34
|
+
on(event: 'data', listener: (chunk: string) => void): void;
|
|
35
|
+
off(event: 'data', listener: (chunk: string) => void): void;
|
|
36
|
+
pause(): void;
|
|
37
|
+
resume(): void;
|
|
38
|
+
}
|
|
39
|
+
export interface TerminalOutput {
|
|
40
|
+
isTTY?: boolean;
|
|
41
|
+
columns?: number;
|
|
42
|
+
rows?: number;
|
|
43
|
+
write(data: string): void;
|
|
44
|
+
on(event: 'resize', listener: () => void): void;
|
|
45
|
+
off(event: 'resize', listener: () => void): void;
|
|
46
|
+
}
|
|
47
|
+
/** The three this adapter installs handlers for. */
|
|
48
|
+
export type TerminalSignal = 'SIGINT' | 'SIGTERM' | 'SIGHUP';
|
|
49
|
+
export interface NodeAdapterOptions {
|
|
50
|
+
stdin?: TerminalInput;
|
|
51
|
+
stdout?: TerminalOutput;
|
|
52
|
+
env?: Record<string, string | undefined>;
|
|
53
|
+
capabilities?: CapabilityOverrides;
|
|
54
|
+
/** Install exit and signal handlers that release the terminal. */
|
|
55
|
+
installExitHandlers?: boolean;
|
|
56
|
+
escapeTimeoutMs?: number;
|
|
57
|
+
}
|
|
58
|
+
export declare class NodeTerminalAdapter implements TerminalAdapter {
|
|
59
|
+
private options;
|
|
60
|
+
readonly id = "node";
|
|
61
|
+
private stdin;
|
|
62
|
+
private stdout;
|
|
63
|
+
private env;
|
|
64
|
+
private overrides;
|
|
65
|
+
private caps;
|
|
66
|
+
private state;
|
|
67
|
+
private decoder;
|
|
68
|
+
private inputListeners;
|
|
69
|
+
private resizeListeners;
|
|
70
|
+
private pending;
|
|
71
|
+
private onStdinData;
|
|
72
|
+
private handleResize;
|
|
73
|
+
private exitHandler;
|
|
74
|
+
private signalHandler;
|
|
75
|
+
constructor(options?: NodeAdapterOptions);
|
|
76
|
+
private detect;
|
|
77
|
+
get acquired(): AcquiredState | null;
|
|
78
|
+
size(): Size;
|
|
79
|
+
capabilities(): TerminalCapabilities;
|
|
80
|
+
setCapabilityOverrides(overrides: CapabilityOverrides): void;
|
|
81
|
+
environment(): Record<string, string>;
|
|
82
|
+
acquire(options: TerminalSessionOptions): AcquiredState;
|
|
83
|
+
/**
|
|
84
|
+
* Undo exactly what was acquired, in reverse. Anything this adapter did not
|
|
85
|
+
* turn on is left alone - that is what makes an embedded session safe.
|
|
86
|
+
*/
|
|
87
|
+
release(): void;
|
|
88
|
+
write(data: string): void;
|
|
89
|
+
flush(): void;
|
|
90
|
+
onInput(fn: (event: InputEvent) => void): Disposable;
|
|
91
|
+
onResize(fn: (size: Size) => void): Disposable;
|
|
92
|
+
writeClipboard(text: string): void;
|
|
93
|
+
setTitle(title: string): void;
|
|
94
|
+
private installExitHandlers;
|
|
95
|
+
private removeExitHandlers;
|
|
96
|
+
dispose(): void;
|
|
97
|
+
}
|
|
98
|
+
export declare function createNodeTerminal(options?: NodeAdapterOptions): NodeTerminalAdapter;
|
|
99
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"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"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { toDisposable } from '@textui/core';
|
|
2
|
+
import * as ansi from './ansi.js';
|
|
3
|
+
import { applyOverrides, detectCapabilities, describeEnvironment } from './capabilities.js';
|
|
4
|
+
import { createDecoder } from './input.js';
|
|
5
|
+
const NOTHING_ACQUIRED = {
|
|
6
|
+
altScreen: false, mouse: false, wheel: false, focusEvents: false,
|
|
7
|
+
paste: false, cursorHidden: false, enhancedKeys: false, rawMode: false,
|
|
8
|
+
titleSet: false,
|
|
9
|
+
};
|
|
10
|
+
export class NodeTerminalAdapter {
|
|
11
|
+
options;
|
|
12
|
+
id = 'node';
|
|
13
|
+
stdin;
|
|
14
|
+
stdout;
|
|
15
|
+
env;
|
|
16
|
+
overrides;
|
|
17
|
+
caps;
|
|
18
|
+
state = null;
|
|
19
|
+
decoder = null;
|
|
20
|
+
inputListeners = new Set();
|
|
21
|
+
resizeListeners = new Set();
|
|
22
|
+
pending = [];
|
|
23
|
+
onStdinData = (chunk) => {
|
|
24
|
+
this.decoder?.feed(typeof chunk === 'string' ? chunk : chunk.toString('utf8'));
|
|
25
|
+
};
|
|
26
|
+
handleResize = () => {
|
|
27
|
+
const size = this.size();
|
|
28
|
+
for (const fn of [...this.resizeListeners])
|
|
29
|
+
fn(size);
|
|
30
|
+
};
|
|
31
|
+
exitHandler = null;
|
|
32
|
+
signalHandler = null;
|
|
33
|
+
constructor(options = {}) {
|
|
34
|
+
this.options = options;
|
|
35
|
+
this.stdin = options.stdin ?? process.stdin;
|
|
36
|
+
this.stdout = options.stdout ?? process.stdout;
|
|
37
|
+
this.env = options.env ?? process.env;
|
|
38
|
+
this.overrides = options.capabilities ?? {};
|
|
39
|
+
this.caps = this.detect();
|
|
40
|
+
}
|
|
41
|
+
detect() {
|
|
42
|
+
return applyOverrides(detectCapabilities({
|
|
43
|
+
env: this.env,
|
|
44
|
+
isTTY: Boolean(this.stdout.isTTY),
|
|
45
|
+
columns: this.stdout.columns,
|
|
46
|
+
rows: this.stdout.rows,
|
|
47
|
+
platform: process.platform,
|
|
48
|
+
}), this.overrides);
|
|
49
|
+
}
|
|
50
|
+
get acquired() {
|
|
51
|
+
return this.state;
|
|
52
|
+
}
|
|
53
|
+
size() {
|
|
54
|
+
return {
|
|
55
|
+
width: this.stdout.columns ?? 80,
|
|
56
|
+
height: this.stdout.rows ?? 24,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
capabilities() {
|
|
60
|
+
return this.caps;
|
|
61
|
+
}
|
|
62
|
+
setCapabilityOverrides(overrides) {
|
|
63
|
+
this.overrides = { ...this.overrides, ...overrides };
|
|
64
|
+
this.caps = this.detect();
|
|
65
|
+
}
|
|
66
|
+
environment() {
|
|
67
|
+
return describeEnvironment({
|
|
68
|
+
env: this.env,
|
|
69
|
+
isTTY: Boolean(this.stdout.isTTY),
|
|
70
|
+
columns: this.stdout.columns,
|
|
71
|
+
rows: this.stdout.rows,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
acquire(options) {
|
|
75
|
+
if (this.state)
|
|
76
|
+
return this.state;
|
|
77
|
+
const caps = this.caps;
|
|
78
|
+
const managed = options.managed ?? true;
|
|
79
|
+
const state = { ...NOTHING_ACQUIRED };
|
|
80
|
+
const out = [];
|
|
81
|
+
if (this.stdin.isTTY && typeof this.stdin.setRawMode === 'function' && !this.stdin.isRaw) {
|
|
82
|
+
this.stdin.setRawMode(true);
|
|
83
|
+
state.rawMode = true;
|
|
84
|
+
}
|
|
85
|
+
if (managed && (options.altScreen ?? true) && caps.altScreen) {
|
|
86
|
+
out.push(ansi.altScreenEnter, ansi.eraseScreen, ansi.cursorHome);
|
|
87
|
+
state.altScreen = true;
|
|
88
|
+
}
|
|
89
|
+
if ((options.hideCursor ?? true) && caps.cursor) {
|
|
90
|
+
out.push(ansi.cursorHide);
|
|
91
|
+
state.cursorHidden = true;
|
|
92
|
+
}
|
|
93
|
+
if ((options.mouse ?? false) && caps.mouse) {
|
|
94
|
+
out.push((options.wheel ?? true) ? ansi.mouseOn : ansi.mouseButtonsOn);
|
|
95
|
+
state.mouse = true;
|
|
96
|
+
state.wheel = options.wheel ?? true;
|
|
97
|
+
}
|
|
98
|
+
if ((options.focusEvents ?? false) && caps.focusEvents) {
|
|
99
|
+
out.push(ansi.focusEventsOn);
|
|
100
|
+
state.focusEvents = true;
|
|
101
|
+
}
|
|
102
|
+
if ((options.paste ?? true) && caps.paste) {
|
|
103
|
+
out.push(ansi.bracketedPasteOn);
|
|
104
|
+
state.paste = true;
|
|
105
|
+
}
|
|
106
|
+
if ((options.enhancedKeys ?? false) && caps.kittyKeyboard) {
|
|
107
|
+
out.push(ansi.kittyKeyboardPush);
|
|
108
|
+
state.enhancedKeys = true;
|
|
109
|
+
}
|
|
110
|
+
if (options.title && caps.title) {
|
|
111
|
+
out.push(ansi.setTitle(options.title));
|
|
112
|
+
state.titleSet = true;
|
|
113
|
+
}
|
|
114
|
+
this.stdout.write(out.join(''));
|
|
115
|
+
this.decoder = createDecoder((event) => {
|
|
116
|
+
for (const fn of [...this.inputListeners])
|
|
117
|
+
fn(event);
|
|
118
|
+
}, { escapeTimeoutMs: this.options.escapeTimeoutMs });
|
|
119
|
+
this.stdin.setEncoding('utf8');
|
|
120
|
+
this.stdin.on('data', this.onStdinData);
|
|
121
|
+
this.stdin.resume();
|
|
122
|
+
this.stdout.on('resize', this.handleResize);
|
|
123
|
+
if (this.options.installExitHandlers ?? true)
|
|
124
|
+
this.installExitHandlers();
|
|
125
|
+
this.state = state;
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Undo exactly what was acquired, in reverse. Anything this adapter did not
|
|
130
|
+
* turn on is left alone - that is what makes an embedded session safe.
|
|
131
|
+
*/
|
|
132
|
+
release() {
|
|
133
|
+
const state = this.state;
|
|
134
|
+
if (!state)
|
|
135
|
+
return;
|
|
136
|
+
this.state = null;
|
|
137
|
+
const out = [];
|
|
138
|
+
if (state.enhancedKeys)
|
|
139
|
+
out.push(ansi.kittyKeyboardPop);
|
|
140
|
+
if (state.paste)
|
|
141
|
+
out.push(ansi.bracketedPasteOff);
|
|
142
|
+
if (state.focusEvents)
|
|
143
|
+
out.push(ansi.focusEventsOff);
|
|
144
|
+
if (state.mouse)
|
|
145
|
+
out.push(state.wheel ? ansi.mouseOff : ansi.mouseButtonsOff);
|
|
146
|
+
if (state.altScreen)
|
|
147
|
+
out.push(ansi.altScreenLeave);
|
|
148
|
+
if (state.cursorHidden)
|
|
149
|
+
out.push(ansi.cursorShow);
|
|
150
|
+
out.push(ansi.reset);
|
|
151
|
+
try {
|
|
152
|
+
this.stdout.write(out.join(''));
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// The stream may already be closed during an abrupt exit.
|
|
156
|
+
}
|
|
157
|
+
this.stdin.off('data', this.onStdinData);
|
|
158
|
+
this.stdout.off('resize', this.handleResize);
|
|
159
|
+
this.decoder?.reset();
|
|
160
|
+
this.decoder = null;
|
|
161
|
+
if (state.rawMode && this.stdin.isTTY && typeof this.stdin.setRawMode === 'function') {
|
|
162
|
+
try {
|
|
163
|
+
this.stdin.setRawMode(false);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
/* best effort */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.stdin.pause();
|
|
170
|
+
this.removeExitHandlers();
|
|
171
|
+
}
|
|
172
|
+
write(data) {
|
|
173
|
+
if (data === '')
|
|
174
|
+
return;
|
|
175
|
+
this.pending.push(data);
|
|
176
|
+
}
|
|
177
|
+
flush() {
|
|
178
|
+
if (this.pending.length === 0)
|
|
179
|
+
return;
|
|
180
|
+
const data = this.pending.join('');
|
|
181
|
+
this.pending = [];
|
|
182
|
+
this.stdout.write(data);
|
|
183
|
+
}
|
|
184
|
+
onInput(fn) {
|
|
185
|
+
this.inputListeners.add(fn);
|
|
186
|
+
return toDisposable(() => this.inputListeners.delete(fn));
|
|
187
|
+
}
|
|
188
|
+
onResize(fn) {
|
|
189
|
+
this.resizeListeners.add(fn);
|
|
190
|
+
return toDisposable(() => this.resizeListeners.delete(fn));
|
|
191
|
+
}
|
|
192
|
+
writeClipboard(text) {
|
|
193
|
+
if (!this.caps.clipboard)
|
|
194
|
+
return;
|
|
195
|
+
this.stdout.write(ansi.clipboardWrite(text));
|
|
196
|
+
}
|
|
197
|
+
setTitle(title) {
|
|
198
|
+
if (!this.caps.title)
|
|
199
|
+
return;
|
|
200
|
+
this.stdout.write(ansi.setTitle(title));
|
|
201
|
+
}
|
|
202
|
+
installExitHandlers() {
|
|
203
|
+
this.exitHandler = () => this.release();
|
|
204
|
+
process.on('exit', this.exitHandler);
|
|
205
|
+
this.signalHandler = (signal) => {
|
|
206
|
+
this.release();
|
|
207
|
+
process.exit(signal === 'SIGINT' ? 130 : 143);
|
|
208
|
+
};
|
|
209
|
+
process.on('SIGINT', this.signalHandler);
|
|
210
|
+
process.on('SIGTERM', this.signalHandler);
|
|
211
|
+
process.on('SIGHUP', this.signalHandler);
|
|
212
|
+
}
|
|
213
|
+
removeExitHandlers() {
|
|
214
|
+
if (this.exitHandler) {
|
|
215
|
+
process.off('exit', this.exitHandler);
|
|
216
|
+
this.exitHandler = null;
|
|
217
|
+
}
|
|
218
|
+
if (this.signalHandler) {
|
|
219
|
+
process.off('SIGINT', this.signalHandler);
|
|
220
|
+
process.off('SIGTERM', this.signalHandler);
|
|
221
|
+
process.off('SIGHUP', this.signalHandler);
|
|
222
|
+
this.signalHandler = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
dispose() {
|
|
226
|
+
this.release();
|
|
227
|
+
this.inputListeners.clear();
|
|
228
|
+
this.resizeListeners.clear();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
export function createNodeTerminal(options) {
|
|
232
|
+
return new NodeTerminalAdapter(options);
|
|
233
|
+
}
|