@gjsify/readline 0.4.0 → 0.4.3

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/src/index.ts DELETED
@@ -1,561 +0,0 @@
1
- // Reference: Node.js lib/readline.js
2
- // Reimplemented for GJS without Node.js primordials.
3
-
4
- import { EventEmitter } from 'node:events';
5
- import { StringDecoder } from 'node:string_decoder';
6
- import type { Readable, Writable } from 'node:stream';
7
- import type {
8
- GjsReadableTty,
9
- GjsWritableTty,
10
- KeypressEmitter,
11
- KeypressTaggedStream,
12
- } from './internal/stream-types.js';
13
-
14
- export interface InterfaceOptions {
15
- input?: Readable;
16
- output?: Writable;
17
- prompt?: string;
18
- terminal?: boolean;
19
- historySize?: number;
20
- completer?: (line: string, callback: (err: Error | null, result: [string[], string]) => void) => void;
21
- crlfDelay?: number;
22
- removeHistoryDuplicates?: boolean;
23
- escapeCodeTimeout?: number;
24
- tabSize?: number;
25
- }
26
-
27
- const LINE_END = /\r\n|\r|\n/;
28
-
29
- export class Interface extends EventEmitter {
30
- terminal: boolean;
31
- line = '';
32
- cursor = 0;
33
- // Mirrors `InterfaceOptions.escapeCodeTimeout` so that
34
- // `emitKeypressEvents(stream, this)` can read it via the structural
35
- // `{ escapeCodeTimeout?: number }` parameter shape.
36
- escapeCodeTimeout?: number;
37
-
38
- private _input: Readable | null;
39
- private _output: Writable | null;
40
-
41
- get input(): Readable | null { return this._input; }
42
- get output(): Writable | null { return this._output; }
43
-
44
- private _prompt: string;
45
- private _closed = false;
46
- private _paused = false;
47
- history: string[];
48
- private _historySize: number;
49
- private _crlfDelay: number;
50
- private _lineBuffer = '';
51
- private _questionCallback: ((answer: string) => void) | null = null;
52
- // Per-listener refs so close() removes only our listeners, not the keypress
53
- // parser's 'data' listener — which must survive across sequential prompts.
54
- private _boundOnData: ((chunk: Buffer | string) => void) | null = null;
55
- private _boundOnEnd: (() => void) | null = null;
56
- private _boundOnError: ((err: Error) => void) | null = null;
57
- private _boundOnKeypress: ((str: string | undefined, key: Key) => void) | null = null;
58
-
59
- constructor(input?: Readable | InterfaceOptions, output?: Writable) {
60
- super();
61
-
62
- let opts: InterfaceOptions;
63
- if (input && typeof input === 'object' && !('read' in input && typeof input.read === 'function')) {
64
- opts = input as InterfaceOptions;
65
- } else {
66
- opts = { input: input as Readable, output };
67
- }
68
-
69
- this._input = opts.input || null;
70
- this._output = opts.output || null;
71
- this._prompt = opts.prompt || '> ';
72
- this.terminal = opts.terminal ?? (this._output !== null);
73
- this._historySize = opts.historySize ?? 30;
74
- this.history = [];
75
- this._crlfDelay = opts.crlfDelay ?? 100;
76
- this.escapeCodeTimeout = opts.escapeCodeTimeout;
77
-
78
- if (this._input) {
79
- this._boundOnData = (chunk: Buffer | string) => this._onData(chunk);
80
- this._boundOnEnd = () => this._onEnd();
81
- this._boundOnError = (err: Error) => this.emit('error', err);
82
- this._input.on('data', this._boundOnData);
83
- this._input.on('end', this._boundOnEnd);
84
- this._input.on('error', this._boundOnError);
85
-
86
- if ('setEncoding' in this._input && typeof this._input.setEncoding === 'function') {
87
- this._input.setEncoding('utf8');
88
- }
89
-
90
- if (this.terminal) {
91
- emitKeypressEvents(this._input, this);
92
- const ttyInput = this._input as GjsReadableTty;
93
- if (typeof ttyInput.setRawMode === 'function') {
94
- if (!ttyInput.isRaw) ttyInput.setRawMode(true);
95
- }
96
- if (typeof this._input.resume === 'function') {
97
- this._input.resume();
98
- }
99
-
100
- this._boundOnKeypress = (str: string | undefined, key: Key) => {
101
- if (!key) return;
102
- if (key.name === 'backspace' || key.name === 'delete') {
103
- if (this.cursor > 0) {
104
- this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor);
105
- this.cursor--;
106
- }
107
- } else if (
108
- str && str.length === 1 && !key.ctrl && !key.meta &&
109
- key.name !== 'return' && key.name !== 'enter' && key.name !== 'escape'
110
- ) {
111
- this.line = this.line.slice(0, this.cursor) + str + this.line.slice(this.cursor);
112
- this.cursor++;
113
- }
114
- };
115
- this._input.on('keypress', this._boundOnKeypress as (...args: unknown[]) => void);
116
- }
117
- }
118
- }
119
-
120
- private _onData(chunk: Buffer | string): void {
121
- if (this._closed || this._paused) return;
122
- const str = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
123
- this._lineBuffer += str;
124
- let m: RegExpExecArray | null;
125
- while ((m = LINE_END.exec(this._lineBuffer)) !== null) {
126
- const line = this._lineBuffer.substring(0, m.index);
127
- this._lineBuffer = this._lineBuffer.substring(m.index + m[0].length);
128
- this._onLine(line);
129
- }
130
- }
131
-
132
- private _onLine(line: string): void {
133
- if (line.length > 0 && this._historySize > 0) {
134
- if (this.history.length === 0 || this.history[0] !== line) {
135
- this.history.unshift(line);
136
- if (this.history.length > this._historySize) this.history.pop();
137
- }
138
- }
139
- if (this._questionCallback) {
140
- const cb = this._questionCallback;
141
- this._questionCallback = null;
142
- cb(line);
143
- }
144
- this.emit('line', line);
145
- }
146
-
147
- private _onEnd(): void {
148
- if (this._lineBuffer.length > 0) {
149
- this._onLine(this._lineBuffer);
150
- this._lineBuffer = '';
151
- }
152
- this.close();
153
- }
154
-
155
- setPrompt(prompt: string): void {
156
- this._prompt = prompt;
157
- }
158
-
159
- getPrompt(): string {
160
- return this._prompt;
161
- }
162
-
163
- prompt(_preserveCursor?: boolean): void {
164
- if (this._closed) throw new Error('readline was closed');
165
- this._output?.write(this._prompt);
166
- }
167
-
168
- question(query: string, callback: (answer: string) => void): void;
169
- question(query: string, options: Record<string, unknown>, callback: (answer: string) => void): void;
170
- question(
171
- query: string,
172
- optionsOrCallback: Record<string, unknown> | ((answer: string) => void),
173
- callback?: (answer: string) => void,
174
- ): void {
175
- if (this._closed) throw new Error('readline was closed');
176
- const cb = typeof optionsOrCallback === 'function' ? optionsOrCallback : callback!;
177
- this._questionCallback = cb;
178
- this._output?.write(query);
179
- }
180
-
181
- clearLine(_dir: number, callback?: () => void): boolean {
182
- this.line = '';
183
- this.cursor = 0;
184
- if (callback) callback();
185
- return true;
186
- }
187
-
188
- write(data: string | Buffer | null, key?: { ctrl?: boolean; meta?: boolean; shift?: boolean; name?: string }): void {
189
- if (this._closed) return;
190
- if (key) {
191
- if (this._input) this._input.emit('keypress', data ?? '', key);
192
- return;
193
- }
194
- if (data !== null && data !== undefined) {
195
- const str = typeof data === 'string' ? data : data.toString('utf8');
196
- if (str) {
197
- this.line = this.line.slice(0, this.cursor) + str + this.line.slice(this.cursor);
198
- this.cursor += str.length;
199
- }
200
- this._output?.write(data);
201
- }
202
- }
203
-
204
- close(): void {
205
- if (this._closed) return;
206
- this._closed = true;
207
-
208
- if (this._input) {
209
- // Remove only our own listeners — removeAllListeners would also strip the
210
- // keypress parser's 'data' listener. The _KEYPRESS_DECODER Symbol idempotency
211
- // guard would then prevent emitKeypressEvents from re-installing for the next
212
- // prompt, silencing all keypress events.
213
- if (this._boundOnData) this._input.removeListener('data', this._boundOnData);
214
- if (this._boundOnEnd) this._input.removeListener('end', this._boundOnEnd);
215
- if (this._boundOnError) this._input.removeListener('error', this._boundOnError);
216
- if (this._boundOnKeypress) this._input.removeListener('keypress', this._boundOnKeypress as (...args: unknown[]) => void);
217
- this._boundOnData = null;
218
- this._boundOnEnd = null;
219
- this._boundOnError = null;
220
- this._boundOnKeypress = null;
221
-
222
- const ttyInput = this._input as GjsReadableTty;
223
- if (this.terminal && ttyInput.isRaw && typeof ttyInput.setRawMode === 'function') {
224
- ttyInput.setRawMode(false);
225
- }
226
-
227
- // Pause stdin so ProcessReadStream releases the GLib main loop via
228
- // quitMainLoop(). Mirrors Node.js readline: close() pauses the stream
229
- // so the event loop can drain and the process exits when no more prompts follow.
230
- if (typeof this._input.pause === 'function') {
231
- this._input.pause();
232
- }
233
- }
234
-
235
- this.emit('close');
236
- }
237
-
238
- pause(): this {
239
- if (this._closed) return this;
240
- this._paused = true;
241
- if (this._input && 'pause' in this._input && typeof this._input.pause === 'function') {
242
- this._input.pause();
243
- }
244
- this.emit('pause');
245
- return this;
246
- }
247
-
248
- resume(): this {
249
- if (this._closed) return this;
250
- this._paused = false;
251
- if (this._input && 'resume' in this._input && typeof this._input.resume === 'function') {
252
- this._input.resume();
253
- }
254
- this.emit('resume');
255
- return this;
256
- }
257
-
258
- getCursorPos(): { rows: number; cols: number } {
259
- const columns = (this._output as GjsWritableTty | null)?.columns ?? 80;
260
- const len = this._prompt.length + this.cursor;
261
- return { rows: Math.floor(len / columns), cols: len % columns };
262
- }
263
-
264
- [Symbol.asyncIterator](): AsyncIterableIterator<string> {
265
- const lines: string[] = [];
266
- let resolve: ((value: IteratorResult<string, undefined>) => void) | null = null;
267
- let done = false;
268
-
269
- const onLine = (line: string) => {
270
- if (resolve) {
271
- const r = resolve;
272
- resolve = null;
273
- r({ value: line, done: false });
274
- } else {
275
- lines.push(line);
276
- }
277
- };
278
- const onClose = () => {
279
- done = true;
280
- if (resolve) {
281
- const r = resolve;
282
- resolve = null;
283
- r({ value: undefined, done: true });
284
- }
285
- };
286
-
287
- this.on('line', onLine);
288
- this.on('close', onClose);
289
-
290
- return {
291
- next: (): Promise<IteratorResult<string, undefined>> => {
292
- if (lines.length > 0) return Promise.resolve({ value: lines.shift()!, done: false });
293
- if (done) return Promise.resolve({ value: undefined, done: true });
294
- return new Promise<IteratorResult<string, undefined>>((r) => { resolve = r; });
295
- },
296
- return: (): Promise<IteratorResult<string, undefined>> => {
297
- done = true;
298
- this.removeListener('line', onLine);
299
- this.removeListener('close', onClose);
300
- return Promise.resolve({ value: undefined, done: true });
301
- },
302
- [Symbol.asyncIterator]() { return this; },
303
- };
304
- }
305
- }
306
-
307
- export function createInterface(
308
- input?: Readable | InterfaceOptions,
309
- output?: Writable,
310
- completer?: InterfaceOptions['completer'],
311
- terminal?: boolean,
312
- ): Interface {
313
- if (typeof input === 'object' && input !== null && !('read' in input && typeof input.read === 'function')) {
314
- return new Interface(input);
315
- }
316
- return new Interface({ input: input as Readable, output, completer, terminal });
317
- }
318
-
319
- export function clearLine(stream: Writable, dir: number, callback?: () => void): boolean {
320
- if (!stream || typeof stream.write !== 'function') {
321
- if (callback) callback();
322
- return true;
323
- }
324
- const code = dir < 0 ? '\x1b[1K' : dir > 0 ? '\x1b[0K' : '\x1b[2K';
325
- return stream.write(code, callback);
326
- }
327
-
328
- export function clearScreenDown(stream: Writable, callback?: () => void): boolean {
329
- if (!stream || typeof stream.write !== 'function') {
330
- if (callback) callback();
331
- return true;
332
- }
333
- return stream.write('\x1b[0J', callback);
334
- }
335
-
336
- export function cursorTo(
337
- stream: Writable,
338
- x: number,
339
- y?: number | (() => void),
340
- callback?: () => void,
341
- ): boolean {
342
- if (!stream || typeof stream.write !== 'function') {
343
- if (typeof y === 'function') y();
344
- else if (callback) callback();
345
- return true;
346
- }
347
- if (typeof y === 'function') { callback = y; y = undefined; }
348
- const code = typeof y === 'number' ? `\x1b[${y + 1};${x + 1}H` : `\x1b[${x + 1}G`;
349
- return stream.write(code, callback);
350
- }
351
-
352
- export function moveCursor(
353
- stream: Writable,
354
- dx: number,
355
- dy: number,
356
- callback?: () => void,
357
- ): boolean {
358
- if (!stream || typeof stream.write !== 'function') {
359
- if (callback) callback();
360
- return true;
361
- }
362
- let code = '';
363
- if (dx > 0) code += `\x1b[${dx}C`;
364
- else if (dx < 0) code += `\x1b[${-dx}D`;
365
- if (dy > 0) code += `\x1b[${dy}B`;
366
- else if (dy < 0) code += `\x1b[${-dy}A`;
367
- if (code) return stream.write(code, callback);
368
- if (callback) callback();
369
- return true;
370
- }
371
-
372
- // ── Keypress parser ───────────────────────────────────────────────────────────
373
- // Ported from refs/node/lib/internal/readline/utils.js (emitKeys generator)
374
- // Original: Node.js contributors, MIT.
375
- // Rewritten for TypeScript / GJS without Node.js primordials.
376
-
377
- export interface Key {
378
- sequence: string;
379
- name: string | undefined;
380
- ctrl: boolean;
381
- meta: boolean;
382
- shift: boolean;
383
- code?: string;
384
- }
385
-
386
- const ESCAPE_CODE_TIMEOUT = 500;
387
-
388
- function* emitKeys(stream: { emit(event: string, ...args: unknown[]): boolean }): Generator<void, void, string> {
389
- while (true) {
390
- let ch: string = yield;
391
- let s = ch;
392
- let escaped = false;
393
- const key: Key = { sequence: '', name: undefined, ctrl: false, meta: false, shift: false };
394
-
395
- if (ch === '\x1b') {
396
- escaped = true;
397
- s += (ch = yield);
398
- if (ch === '\x1b') s += (ch = yield);
399
- }
400
-
401
- if (escaped && (ch === 'O' || ch === '[')) {
402
- let code = ch;
403
- let modifier = 0;
404
-
405
- if (ch === 'O') {
406
- s += (ch = yield);
407
- if (ch >= '0' && ch <= '9') { modifier = ch.charCodeAt(0) - 1; s += (ch = yield); }
408
- code += ch;
409
- } else if (ch === '[') {
410
- s += (ch = yield);
411
- if (ch === '[') { code += ch; s += (ch = yield); }
412
- const cmdStart = s.length - 1;
413
- if (ch >= '0' && ch <= '9') {
414
- s += (ch = yield);
415
- if (ch >= '0' && ch <= '9') { s += (ch = yield); if (ch >= '0' && ch <= '9') s += (ch = yield); }
416
- }
417
- if (ch === ';') { s += (ch = yield); if (ch >= '0' && ch <= '9') s += (yield); }
418
- const cmd = s.slice(cmdStart);
419
- let match: RegExpExecArray | null;
420
- if ((match = /^(?:(\d\d?)(?:;(\d))?([~^$])|(\d{3}~))$/.exec(cmd))) {
421
- if (match[4]) { code += match[4]; } else { code += match[1] + match[3]; modifier = (parseInt(match[2] ?? '1', 10) || 1) - 1; }
422
- } else if ((match = /^((\d;)?(\d))?([A-Za-z])$/.exec(cmd))) {
423
- code += match[4]; modifier = (parseInt(match[3] ?? '1', 10) || 1) - 1;
424
- } else { code += cmd; }
425
- }
426
-
427
- key.ctrl = !!(modifier & 4); key.meta = !!(modifier & 10); key.shift = !!(modifier & 1); key.code = code;
428
-
429
- switch (code) {
430
- case '[P': case 'OP': case '[11~': case '[[A': key.name = 'f1'; break;
431
- case '[Q': case 'OQ': case '[12~': case '[[B': key.name = 'f2'; break;
432
- case '[R': case 'OR': case '[13~': case '[[C': key.name = 'f3'; break;
433
- case '[S': case 'OS': case '[14~': case '[[D': key.name = 'f4'; break;
434
- case '[15~': case '[[E': key.name = 'f5'; break;
435
- case '[17~': key.name = 'f6'; break; case '[18~': key.name = 'f7'; break;
436
- case '[19~': key.name = 'f8'; break; case '[20~': key.name = 'f9'; break;
437
- case '[21~': key.name = 'f10'; break; case '[23~': key.name = 'f11'; break;
438
- case '[24~': key.name = 'f12'; break;
439
- case '[200~': key.name = 'paste-start'; break; case '[201~': key.name = 'paste-end'; break;
440
- case '[A': case 'OA': key.name = 'up'; break;
441
- case '[B': case 'OB': key.name = 'down'; break;
442
- case '[C': case 'OC': key.name = 'right'; break;
443
- case '[D': case 'OD': key.name = 'left'; break;
444
- case '[E': case 'OE': key.name = 'clear'; break;
445
- case '[F': case 'OF': key.name = 'end'; break;
446
- case '[H': case 'OH': key.name = 'home'; break;
447
- case '[1~': key.name = 'home'; break; case '[2~': key.name = 'insert'; break;
448
- case '[3~': key.name = 'delete'; break; case '[4~': key.name = 'end'; break;
449
- case '[5~': case '[[5~': key.name = 'pageup'; break;
450
- case '[6~': case '[[6~': key.name = 'pagedown'; break;
451
- case '[7~': key.name = 'home'; break; case '[8~': key.name = 'end'; break;
452
- case '[a': key.name = 'up'; key.shift = true; break;
453
- case '[b': key.name = 'down'; key.shift = true; break;
454
- case '[c': key.name = 'right'; key.shift = true; break;
455
- case '[d': key.name = 'left'; key.shift = true; break;
456
- case '[2$': key.name = 'insert'; key.shift = true; break;
457
- case '[3$': key.name = 'delete'; key.shift = true; break;
458
- case '[5$': key.name = 'pageup'; key.shift = true; break;
459
- case '[6$': key.name = 'pagedown'; key.shift = true; break;
460
- case '[7$': key.name = 'home'; key.shift = true; break;
461
- case '[8$': key.name = 'end'; key.shift = true; break;
462
- case 'Oa': key.name = 'up'; key.ctrl = true; break;
463
- case 'Ob': key.name = 'down'; key.ctrl = true; break;
464
- case 'Oc': key.name = 'right'; key.ctrl = true; break;
465
- case 'Od': key.name = 'left'; key.ctrl = true; break;
466
- case '[2^': key.name = 'insert'; key.ctrl = true; break;
467
- case '[3^': key.name = 'delete'; key.ctrl = true; break;
468
- case '[5^': key.name = 'pageup'; key.ctrl = true; break;
469
- case '[6^': key.name = 'pagedown'; key.ctrl = true; break;
470
- case '[7^': key.name = 'home'; key.ctrl = true; break;
471
- case '[8^': key.name = 'end'; key.ctrl = true; break;
472
- case '[Z': key.name = 'tab'; key.shift = true; break;
473
- default: key.name = 'undefined'; break;
474
- }
475
- } else if (ch === '\r') {
476
- key.name = 'return'; key.meta = escaped;
477
- } else if (ch === '\n') {
478
- key.name = 'enter'; key.meta = escaped;
479
- } else if (ch === '\t') {
480
- key.name = 'tab'; key.meta = escaped;
481
- } else if (ch === '\b' || ch === '\x7f') {
482
- key.name = 'backspace'; key.meta = escaped;
483
- } else if (ch === '\x1b') {
484
- key.name = 'escape'; key.meta = escaped;
485
- } else if (ch === ' ') {
486
- key.name = 'space'; key.meta = escaped;
487
- } else if (!escaped && ch <= '\x1a') {
488
- key.name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
489
- key.ctrl = true;
490
- } else if (/^[0-9A-Za-z]$/.test(ch)) {
491
- key.name = ch.toLowerCase(); key.shift = /^[A-Z]$/.test(ch); key.meta = escaped;
492
- } else if (escaped) {
493
- key.name = ch.length ? undefined : 'escape'; key.meta = true;
494
- }
495
-
496
- key.sequence = s;
497
- if (s.length !== 0 && (key.name !== undefined || escaped)) {
498
- stream.emit('keypress', escaped ? undefined : s, key);
499
- } else if (s.length === 1) {
500
- stream.emit('keypress', s, key);
501
- }
502
- }
503
- }
504
-
505
- const _KEYPRESS_DECODER = Symbol('keypress-decoder');
506
- const _ESCAPE_DECODER = Symbol('escape-decoder');
507
-
508
- // Idempotent — calling twice on the same stream is a no-op.
509
- // Ported from refs/node/lib/internal/readline/emitKeypressEvents.js.
510
- export function emitKeypressEvents(stream: Readable, iface: { escapeCodeTimeout?: number } = {}): void {
511
- // Cast once to the symbol-tagged shape; runtime augmentation is tracked
512
- // via two private symbols (decoder + escape-state generator).
513
- const tagged = stream as KeypressTaggedStream;
514
- if (tagged[_KEYPRESS_DECODER]) return;
515
-
516
- tagged[_KEYPRESS_DECODER] = new StringDecoder('utf8');
517
- tagged[_ESCAPE_DECODER] = emitKeys(stream as KeypressEmitter);
518
- (tagged[_ESCAPE_DECODER] as Generator<void, void, string>).next();
519
-
520
- const escTimeout = iface.escapeCodeTimeout ?? ESCAPE_CODE_TIMEOUT;
521
- let timeoutId: ReturnType<typeof setTimeout> | undefined;
522
- const triggerEscape = () =>
523
- (tagged[_ESCAPE_DECODER] as Generator<void, void, string>).next('');
524
-
525
- function onData(input: Buffer | string): void {
526
- if (stream.listenerCount('keypress') > 0) {
527
- const decoder = tagged[_KEYPRESS_DECODER] as StringDecoder;
528
- const str: string = decoder.write(
529
- typeof input === 'string' ? Buffer.from(input) : input,
530
- );
531
- if (str) {
532
- clearTimeout(timeoutId);
533
- for (const ch of str) {
534
- try {
535
- (tagged[_ESCAPE_DECODER] as Generator<void, void, string>).next(ch);
536
- if (ch === '\x1b') timeoutId = setTimeout(triggerEscape, escTimeout);
537
- } catch {
538
- tagged[_ESCAPE_DECODER] = emitKeys(stream as KeypressEmitter);
539
- (tagged[_ESCAPE_DECODER] as Generator<void, void, string>).next();
540
- }
541
- }
542
- }
543
- } else {
544
- stream.removeListener('data', onData);
545
- delete tagged[_KEYPRESS_DECODER];
546
- delete tagged[_ESCAPE_DECODER];
547
- }
548
- }
549
-
550
- stream.on('data', onData);
551
- }
552
-
553
- export default {
554
- Interface,
555
- createInterface,
556
- clearLine,
557
- clearScreenDown,
558
- cursorTo,
559
- moveCursor,
560
- emitKeypressEvents,
561
- };
@@ -1,72 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- // Internal type helpers for readline — narrowing Node's Readable/Writable
3
- // to the TTY-extended runtime shape we get on both Node tty streams and
4
- // @gjsify/process's stdin/stdout/stderr. NOT exported from the package's
5
- // public surface (per AGENTS.md Rule 2c).
6
- //
7
- // `tty.ReadStream` / `tty.WriteStream` extend `net.Socket` (which extends
8
- // `Readable`/`Writable` via `Duplex`) and add the TTY-specific runtime
9
- // methods used by readline (setRawMode, isRaw, isTTY, columns, rows,
10
- // getColorDepth, hasColors). The standalone `Readable`/`Writable` types
11
- // in `node:stream` do not declare these — but at runtime our public API
12
- // accepts any `Readable`/`Writable`, and either falls back gracefully or
13
- // uses `in`/`typeof` guards before invoking. These interfaces let us
14
- // narrow without `as any`.
15
- //
16
- // Additionally, the keypress parser stores its `StringDecoder` and
17
- // `emitKeys` generator on the input stream via two private symbols
18
- // (`_KEYPRESS_DECODER`, `_ESCAPE_DECODER`). The `KeypressTaggedStream`
19
- // type describes that runtime augmentation.
20
-
21
- import type { Readable, Writable } from 'node:stream';
22
- import type { StringDecoder } from 'node:string_decoder';
23
-
24
- /**
25
- * `Readable` augmented with the TTY-specific runtime methods that exist
26
- * on `tty.ReadStream` and on `@gjsify/process`'s `ProcessReadStream`.
27
- * All members are optional — readline always guards with `'method' in stream`
28
- * + `typeof stream.method === 'function'` before calling.
29
- */
30
- export interface GjsReadableTty extends Readable {
31
- isRaw?: boolean;
32
- isTTY?: boolean;
33
- setRawMode?(enable: boolean): this;
34
- }
35
-
36
- /**
37
- * `Writable` augmented with the TTY-specific runtime properties that exist
38
- * on `tty.WriteStream` and on `@gjsify/process`'s `ProcessWriteStream`.
39
- * All members are optional for the same reason as `GjsReadableTty`.
40
- */
41
- export interface GjsWritableTty extends Writable {
42
- columns?: number;
43
- rows?: number;
44
- isTTY?: boolean;
45
- getColorDepth?(env?: NodeJS.ProcessEnv): number;
46
- hasColors?(count?: number, env?: NodeJS.ProcessEnv): boolean;
47
- }
48
-
49
- /**
50
- * Symbol-keyed runtime state that `emitKeypressEvents` attaches to the
51
- * input stream. The two private symbols carry the per-stream
52
- * `StringDecoder` and the live `emitKeys` generator, plus a re-entrancy
53
- * guard so the second call on the same stream is a no-op.
54
- *
55
- * Modeled as an intersection (rather than `extends Readable` with a
56
- * `[key: symbol]` index signature) because `Readable` already declares
57
- * built-in symbol keys — `EventEmitter.captureRejectionSymbol`,
58
- * `Symbol.asyncDispose`, `Symbol.asyncIterator` — whose value types
59
- * would conflict with a narrow union signature.
60
- */
61
- export type KeypressTaggedStream = Readable & {
62
- [key: symbol]: StringDecoder | Generator<void, void, string> | undefined;
63
- };
64
-
65
- /**
66
- * Minimal `EventEmitter`-like shape used by the keypress parser to dispatch
67
- * `'keypress'` events back onto the stream. `Readable` already satisfies
68
- * this via its `EventEmitter` ancestry.
69
- */
70
- export interface KeypressEmitter {
71
- emit(event: string | symbol, ...args: unknown[]): boolean;
72
- }