UNPKG

15.2 kBJavaScriptView Raw
1import { cursor } from '@arpel/escape';
2import { DATA, EXIT } from '@geia/enum-events';
3import { SIGINT } from '@geia/enum-signals';
4import { ros } from '@spare/logger';
5import { valid } from '@typen/nullish';
6import { EventEmitter } from 'events';
7import { StringDecoder } from 'string_decoder';
8import { setTimeout } from 'timers/promises';
9import { KEYPRESS, DATA as DATA$1, NEW_LISTENER } from '@pres/enum-events';
10import { RETURN, ENTER, TAB, BACKSPACE, ESCAPE, SPACE, UNDEFINED, END, HOME, PAGEDOWN, PAGEUP, DELETE, INSERT, CLEAR, LEFT, RIGHT, DOWN, UP } from '@pres/enum-key-names';
11import { acquire } from '@vect/vector-merge';
12
13const QUERY_CURSOR = 'query-cursor';
14
15const ARPEL = ros('arpel');
16class IO extends EventEmitter {
17 #i = process.stdin;
18 #o = process.stdout;
19 #d = new StringDecoder('utf-8');
20 #h = {};
21 #raw = this.#i.isRaw;
22 #log = true;
23
24 constructor(options = {}) {
25 super();
26 if (options.input) this.#i = options.input;
27 if (options.output) this.#o = options.output;
28 if (valid(options.log)) this.#log = options.log;
29 this.configEvents();
30 }
31
32 static build(options) {
33 return new IO(options);
34 }
35
36 static logger(...args) {
37 console.debug(`${ARPEL} >>`, ...args);
38 }
39
40 configEvents() {
41 this.#i.setRawMode(true);
42 this.#h.queryCursor = this.#queryCursorResolver.bind(this);
43 this.#i.on(DATA, this.#h.data = this.#dataHandler.bind(this));
44 process.on(SIGINT, this.#h.sigint = this.#sigintHandler.bind(this));
45 process.on(EXIT, this.#h.exit = this.#exitHandler.bind(this));
46 }
47
48 removeEvents() {
49 this.#i.setRawMode(this.#raw);
50 if (this.#h.queryCursor) this.removeAllListeners(QUERY_CURSOR), delete this.#h.queryCursor;
51 if (this.#h.data) this.#i.off(DATA, this.#h.data), delete this.#h.data;
52 if (this.#h.sigint) process.off(SIGINT, this.#h.sigint), delete this.#h.sigint;
53 if (this.#h.exit) process.off(EXIT, this.#h.exit), delete this.#h.exit;
54 if (this.#log) IO.logger('remove events', ros(QUERY_CURSOR), this.listenerCount(QUERY_CURSOR), ros(DATA), this.#i.listenerCount(DATA), ros(SIGINT), process.listenerCount(SIGINT), ros(EXIT), process.listenerCount(EXIT));
55 }
56
57 get input() {
58 return this.#i;
59 }
60
61 get output() {
62 return this.#o;
63 }
64
65 get decoder() {
66 return this.#d;
67 }
68
69 get handlers() {
70 return this.#h;
71 }
72
73 get size() {
74 return this.#o.getWindowSize();
75 }
76
77 get width() {
78 return this.#o.columns;
79 }
80
81 get height() {
82 return this.#o.rows;
83 }
84
85 asyncCursorPos() {
86 return new Promise(this.#h.queryCursor);
87 }
88
89 #queryCursorResolver(cursorHandler) {
90 this.#o.write(cursor.QUERY_POS);
91 this.once(QUERY_CURSOR, coord => cursorHandler(coord));
92 Promise.resolve(setTimeout(800)).then(() => cursorHandler([null, null]));
93 }
94
95 #dataHandler(buffer) {
96 const str = this.#d.write(buffer);
97 let ms, r, c;
98 if ((ms = /\[\??(\d+);(\d+)R/.exec(str)) && ([, r, c] = ms)) this.emit(QUERY_CURSOR, [r, c]); // console.error('>>', ros('query-mouse-position'), str.replace(//, ros('ESC')), [ --r, --c ],)
99 // else if (isMouseCode(str)) {}
100 // else IO.logger(ros(KEYPRESS), decoSamples(parseKeycodes(str)))
101
102 if (str === '') process.emit(SIGINT, SIGINT);
103 }
104
105 #sigintHandler(signal) {
106 if (this.#log) IO.logger(`receive signal: ${ros(signal)}, closing`);
107 process.exit(signal);
108 }
109
110 #exitHandler(code) {
111 this.removeEvents();
112 if (this.#log) IO.logger(`exit with code: ${ros(String(code))}`);
113 }
114
115}
116
117/*
118 Some patterns seen in terminal key escape codes, derived from combos seen
119 at http://www.midnight-commander.org/browser/lib/tty/key.c
120
121 ESC letter
122 ESC [ letter
123 ESC [ modifier letter
124 ESC [ 1 ; modifier letter
125 ESC [ num char
126 ESC [ num ; modifier char
127 ESC O letter
128 ESC O modifier letter
129 ESC O 1 ; modifier letter
130 ESC N letter
131 ESC [ [ num ; modifier char
132 ESC [ [ 1 ; modifier letter
133 ESC ESC [ num char
134 ESC ESC O letter
135
136 - char is usually ~ but $ and ^ also happen with rxvt
137 - modifier is 1 +
138 (shift * 1) +
139 (left_alt * 2) +
140 (ctrl * 4) +
141 (right_alt * 8)
142 - two leading ESCs apparently mean the same as one leading ESC
143*/
144// Regexes used for ansi escape code splitting
145const ANY_META_KEYCODE = /([a-zA-Z0-9])/; // metaKeyCodeReAnywhere
146
147const PRE_META_KEYCODE = new RegExp('^' + ANY_META_KEYCODE.source + '$'); // metaKeyCodeRe
148
149const ANY_FUNC_KEYCODE = new RegExp('+(O|N|\\[|\\[\\[)(?:' + ['(\\d+)(?:;(\\d+))?([~^$])', '(?:M([@ #!a`])(.)(.))', // mouse
150'(?:1;)?(\\d+)?([a-zA-Z])'].join('|') + ')'); // functionKeyCodeReAnywhere
151
152const PRE_FUNC_KEYCODE = new RegExp('^' + ANY_FUNC_KEYCODE.source); // functionKeyCodeRe
153
154const ANY_ESCAPE_KEYCODE = new RegExp([ANY_FUNC_KEYCODE.source, ANY_META_KEYCODE.source, /./.source].join('|')); // escapeCodeReAnywhere
155
156// str begins with \x1b[M
157// reuse the key array albeit its name
158// otherwise recompute as the mouse event is structured differently
159const parseMouse = str => {
160 const modifier = str.charCodeAt(3);
161 const isScroll = (modifier & 96) === 96;
162 return {
163 name: isScroll ? 'scroll' : modifier & 64 ? 'move' : 'click',
164 shift: !!(modifier & 4),
165 meta: !!(modifier & 8),
166 ctrl: !!(modifier & 16),
167 x: str.charCodeAt(4) - 32,
168 y: str.charCodeAt(5) - 32,
169 button: isScroll ? modifier & 1 ? 'down' : 'up' : mouseButtonName(modifier & 3),
170 seq: str,
171 buf: Buffer.from(str),
172 modifier
173 };
174};
175
176const mouseButtonName = bv => {
177 if (bv === 0) return 'left';
178 if (bv === 1) return 'middle';
179 if (bv === 2) return 'right';
180 if (bv === 3) return 'none';
181 return null;
182};
183
184const isMouseCode = text => /\[M/.test(text) || /\[M([\x00\u0020-\uffff]{3})/.test(text) || /\[(\d+;\d+;\d+)M/.test(text) || /\[<(\d+;\d+;\d+)([mM])/.test(text) || /\[<(\d+;\d+;\d+;\d+)&w/.test(text) || /\[24([0135])~\[(\d+),(\d+)\]\r/.test(text) || /\[(O|I)/.test(text); // if (( modifier & 96 ) === 96) {
185// key.name = 'scroll'
186// key.button = modifier & 1 ? 'down' : 'up'
187// }
188// else {
189// key.name = modifier & 64 ? 'move' : 'click'
190// key.button = mouseButtonName(modifier & 3)
191// }
192
193const processBuffer = text => {
194 if (text[0] > 127 && text[1] === undefined) {
195 text[0] -= 128;
196 return '' + text.toString();
197 } else {
198 return text.toString();
199 }
200};
201/**
202 * accepts a readable Stream instance and makes it emit "keypress" events
203 */
204
205
206const parseKeycodes = text => {
207 if (Buffer.isBuffer(text)) text = processBuffer(text);
208 const keycodes = [];
209 if (isMouseCode(text)) return keycodes;
210 let ms, ph;
211
212 while ((ms = ANY_ESCAPE_KEYCODE.exec(text)) && ([ph] = ms)) {
213 acquire(keycodes, text.slice(0, ms.index).split(''));
214 keycodes.push(ph);
215 text = text.slice(ms.index + ph.length);
216 }
217
218 acquire(keycodes, text.split(''));
219 return keycodes.map(parseKeycode);
220};
221const parseKeycode = str => {
222 const key = {
223 name: undefined,
224 shift: false,
225 meta: false,
226 ctrl: false,
227 ch: str.length === 1 ? str : undefined,
228 seq: str,
229 buf: Buffer.from(str)
230 };
231 let ms;
232
233 if (str === '\r') {
234 key.name = RETURN;
235 } else if (str === '\n') {
236 key.name = ENTER;
237 } // enter, should have been called linefeed // linefeed // key.name = 'linefeed';
238 else if (str === '\t') {
239 key.name = TAB;
240 } else if (str === '\b' || str === '' || str === '' || str === '\b') {
241 key.name = BACKSPACE, key.meta = str.charAt(0) === '';
242 } // backspace or ctrl+h
243 else if (str === '' || str === '') {
244 key.name = ESCAPE, key.meta = str.length === 2;
245 } // escape key
246 else if (str === ' ' || str === ' ') {
247 key.name = SPACE, key.meta = str.length === 2;
248 } else if (str.length === 1 && str <= '') {
249 key.name = String.fromCharCode(str.charCodeAt(0) + 'a'.charCodeAt(0) - 1), key.ctrl = true;
250 } // ctrl+letter
251 else if (str.length === 1 && str >= 'a' && str <= 'z') {
252 key.name = str;
253 } // lowercase letter
254 else if (str.length === 1 && str >= 'A' && str <= 'Z') {
255 key.name = str.toLowerCase(), key.shift = true;
256 } // shift+letter
257 else if (ms = PRE_META_KEYCODE.exec(str)) {
258 key.name = ms[1].toLowerCase(), key.meta = true, key.shift = /^[A-Z]$/.test(ms[1]);
259 } // meta+character key
260 else if (ms = PRE_FUNC_KEYCODE.exec(str)) {
261 // ansi escape sequence
262 // reassemble the key code leaving out leading 's,
263 // the modifier key bitflag and any meaningless "1;" sequence
264 const code = (ms[1] || '') + (ms[2] || '') + (ms[4] || '') + (ms[9] || ''),
265 modifier = (ms[3] || ms[8] || 1) - 1; // Parse the key modifier
266
267 key.shift = !!(modifier & 1);
268 key.ctrl = !!(modifier & 4);
269 key.meta = !!(modifier & 10);
270 key.code = code; // Parse the key itself
271
272 switch (code) {
273 /* xterm ESC [ letter */
274 case '[A':
275 key.name = UP;
276 break;
277
278 case '[B':
279 key.name = DOWN;
280 break;
281
282 case '[C':
283 key.name = RIGHT;
284 break;
285
286 case '[D':
287 key.name = LEFT;
288 break;
289
290 case '[E':
291 key.name = CLEAR;
292 break;
293
294 case '[F':
295 key.name = END;
296 break;
297
298 case '[H':
299 key.name = HOME;
300 break;
301
302 /* xterm/gnome ESC O letter */
303
304 case 'OA':
305 key.name = UP;
306 break;
307
308 case 'OB':
309 key.name = DOWN;
310 break;
311
312 case 'OC':
313 key.name = RIGHT;
314 break;
315
316 case 'OD':
317 key.name = LEFT;
318 break;
319
320 case 'OE':
321 key.name = CLEAR;
322 break;
323
324 case 'OF':
325 key.name = END;
326 break;
327
328 case 'OH':
329 key.name = HOME;
330 break;
331
332 /* xterm/rxvt ESC [ number ~ */
333
334 case '[1~':
335 key.name = HOME;
336 break;
337
338 case '[2~':
339 key.name = INSERT;
340 break;
341
342 case '[3~':
343 key.name = DELETE;
344 break;
345
346 case '[4~':
347 key.name = END;
348 break;
349
350 case '[5~':
351 key.name = PAGEUP;
352 break;
353
354 case '[6~':
355 key.name = PAGEDOWN;
356 break;
357
358 /* putty */
359
360 case '[[5~':
361 key.name = PAGEUP;
362 break;
363
364 case '[[6~':
365 key.name = PAGEDOWN;
366 break;
367
368 /* rxvt */
369
370 case '[7~':
371 key.name = HOME;
372 break;
373
374 case '[8~':
375 key.name = END;
376 break;
377
378 /* rxvt keys with modifiers */
379
380 case '[a':
381 key.name = UP;
382 key.shift = true;
383 break;
384
385 case '[b':
386 key.name = DOWN;
387 key.shift = true;
388 break;
389
390 case '[c':
391 key.name = RIGHT;
392 key.shift = true;
393 break;
394
395 case '[d':
396 key.name = LEFT;
397 key.shift = true;
398 break;
399
400 case '[e':
401 key.name = CLEAR;
402 key.shift = true;
403 break;
404
405 case '[2$':
406 key.name = INSERT;
407 key.shift = true;
408 break;
409
410 case '[3$':
411 key.name = DELETE;
412 key.shift = true;
413 break;
414
415 case '[5$':
416 key.name = PAGEUP;
417 key.shift = true;
418 break;
419
420 case '[6$':
421 key.name = PAGEDOWN;
422 key.shift = true;
423 break;
424
425 case '[7$':
426 key.name = HOME;
427 key.shift = true;
428 break;
429
430 case '[8$':
431 key.name = END;
432 key.shift = true;
433 break;
434
435 case 'Oa':
436 key.name = UP;
437 key.ctrl = true;
438 break;
439
440 case 'Ob':
441 key.name = DOWN;
442 key.ctrl = true;
443 break;
444
445 case 'Oc':
446 key.name = RIGHT;
447 key.ctrl = true;
448 break;
449
450 case 'Od':
451 key.name = LEFT;
452 key.ctrl = true;
453 break;
454
455 case 'Oe':
456 key.name = CLEAR;
457 key.ctrl = true;
458 break;
459
460 case '[2^':
461 key.name = INSERT;
462 key.ctrl = true;
463 break;
464
465 case '[3^':
466 key.name = DELETE;
467 key.ctrl = true;
468 break;
469
470 case '[5^':
471 key.name = PAGEUP;
472 key.ctrl = true;
473 break;
474
475 case '[6^':
476 key.name = PAGEDOWN;
477 key.ctrl = true;
478 break;
479
480 case '[7^':
481 key.name = HOME;
482 key.ctrl = true;
483 break;
484
485 case '[8^':
486 key.name = END;
487 key.ctrl = true;
488 break;
489
490 /* xterm/gnome ESC O letter */
491
492 case 'OP':
493 key.name = 'f1';
494 break;
495
496 case 'OQ':
497 key.name = 'f2';
498 break;
499
500 case 'OR':
501 key.name = 'f3';
502 break;
503
504 case 'OS':
505 key.name = 'f4';
506 break;
507
508 /* xterm/rxvt ESC [ number ~ */
509
510 case '[11~':
511 key.name = 'f1';
512 break;
513
514 case '[12~':
515 key.name = 'f2';
516 break;
517
518 case '[13~':
519 key.name = 'f3';
520 break;
521
522 case '[14~':
523 key.name = 'f4';
524 break;
525
526 /* from Cygwin and used in libuv */
527
528 case '[[A':
529 key.name = 'f1';
530 break;
531
532 case '[[B':
533 key.name = 'f2';
534 break;
535
536 case '[[C':
537 key.name = 'f3';
538 break;
539
540 case '[[D':
541 key.name = 'f4';
542 break;
543
544 case '[[E':
545 key.name = 'f5';
546 break;
547
548 /* common */
549
550 case '[15~':
551 key.name = 'f5';
552 break;
553
554 case '[17~':
555 key.name = 'f6';
556 break;
557
558 case '[18~':
559 key.name = 'f7';
560 break;
561
562 case '[19~':
563 key.name = 'f8';
564 break;
565
566 case '[20~':
567 key.name = 'f9';
568 break;
569
570 case '[21~':
571 key.name = 'f10';
572 break;
573
574 case '[23~':
575 key.name = 'f11';
576 break;
577
578 case '[24~':
579 key.name = 'f12';
580 break;
581
582 /* misc. */
583
584 case '[Z':
585 key.name = TAB;
586 key.shift = true;
587 break;
588
589 default:
590 key.name = UNDEFINED;
591 break;
592 }
593 }
594
595 return key;
596};
597
598/**
599 * keys.js - emit key presses
600 * Copyright (c) 2010-2015, Joyent, Inc. and other contributors (MIT License)
601 * https://github.com/chjj/blessed
602 */
603class KeyCodeEvent {
604 static registerKeypress(stream, keypressEventName = KEYPRESS) {
605 if (stream.keycodeDecoder) {
606 return void 0;
607 } else {
608 stream.keycodeDecoder = new StringDecoder('utf8');
609 }
610
611 function dataHandler(buffer) {
612 if (stream.listenerCount(KEYPRESS) > 0) {
613 /** @type {StringDecoder }*/
614 const keycodeDecoder = stream.keycodeDecoder;
615 const text = keycodeDecoder.write(buffer);
616
617 if (text) {
618 const buffers = parseKeycodes(text);
619
620 for (let {
621 key,
622 ch
623 } of buffers) {
624 if (key || ch) stream.emit(keypressEventName, ch, key);
625 }
626 }
627 } else {
628 // Nobody's watching anyway
629 stream.off(DATA$1, dataHandler);
630 stream.on(NEW_LISTENER, enrollHandler);
631 }
632 }
633
634 function enrollHandler(event) {
635 if (event === KEYPRESS) {
636 stream.on(DATA$1, dataHandler);
637 stream.off(NEW_LISTENER, enrollHandler);
638 }
639 }
640
641 if (stream.listenerCount(KEYPRESS) > 0) {
642 stream.on(DATA$1, dataHandler);
643 } else {
644 stream.on(NEW_LISTENER, enrollHandler);
645 }
646 }
647
648}
649
650export { IO, KeyCodeEvent, isMouseCode, parseKeycodes, parseMouse };