@gjsify/readline 0.4.0 → 0.4.4
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/package.json +45 -42
- package/src/index.spec.ts +0 -932
- package/src/index.ts +0 -561
- package/src/internal/stream-types.ts +0 -72
- package/src/promises.spec.ts +0 -122
- package/src/promises.ts +0 -35
- package/src/test.mts +0 -4
- package/tsconfig.json +0 -29
- package/tsconfig.tsbuildinfo +0 -1
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
|
-
}
|