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