@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/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
+ }