1 | import { cursor } from '@arpel/escape';
|
2 | import { DATA, EXIT } from '@geia/enum-events';
|
3 | import { SIGINT } from '@geia/enum-signals';
|
4 | import { ros } from '@spare/logger';
|
5 | import { valid } from '@typen/nullish';
|
6 | import { EventEmitter } from 'events';
|
7 | import { StringDecoder } from 'string_decoder';
|
8 | import { setTimeout } from 'timers/promises';
|
9 | import { KEYPRESS, DATA as DATA$1, NEW_LISTENER } from '@pres/enum-events';
|
10 | import { RETURN, ENTER, TAB, BACKSPACE, ESCAPE, SPACE, UNDEFINED, END, HOME, PAGEDOWN, PAGEUP, DELETE, INSERT, CLEAR, LEFT, RIGHT, DOWN, UP } from '@pres/enum-key-names';
|
11 | import { acquire } from '@vect/vector-merge';
|
12 |
|
13 | const QUERY_CURSOR = 'query-cursor';
|
14 |
|
15 | const ARPEL = ros('arpel');
|
16 | class 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]);
|
99 |
|
100 |
|
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 |
|
119 |
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 |
|
144 |
|
145 | const ANY_META_KEYCODE = /([a-zA-Z0-9])/;
|
146 |
|
147 | const PRE_META_KEYCODE = new RegExp('^' + ANY_META_KEYCODE.source + '$');
|
148 |
|
149 | const ANY_FUNC_KEYCODE = new RegExp('+(O|N|\\[|\\[\\[)(?:' + ['(\\d+)(?:;(\\d+))?([~^$])', '(?:M([@ #!a`])(.)(.))',
|
150 | '(?:1;)?(\\d+)?([a-zA-Z])'].join('|') + ')');
|
151 |
|
152 | const PRE_FUNC_KEYCODE = new RegExp('^' + ANY_FUNC_KEYCODE.source);
|
153 |
|
154 | const ANY_ESCAPE_KEYCODE = new RegExp([ANY_FUNC_KEYCODE.source, ANY_META_KEYCODE.source, /./.source].join('|'));
|
155 |
|
156 |
|
157 |
|
158 |
|
159 | const 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 |
|
176 | const 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 |
|
184 | const 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);
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 | const 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 |
|
203 |
|
204 |
|
205 |
|
206 | const 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 | };
|
221 | const 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 | }
|
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 | }
|
243 | else if (str === '' || str === '') {
|
244 | key.name = ESCAPE, key.meta = str.length === 2;
|
245 | }
|
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 | }
|
251 | else if (str.length === 1 && str >= 'a' && str <= 'z') {
|
252 | key.name = str;
|
253 | }
|
254 | else if (str.length === 1 && str >= 'A' && str <= 'Z') {
|
255 | key.name = str.toLowerCase(), key.shift = true;
|
256 | }
|
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 | }
|
260 | else if (ms = PRE_FUNC_KEYCODE.exec(str)) {
|
261 |
|
262 |
|
263 |
|
264 | const code = (ms[1] || '') + (ms[2] || '') + (ms[4] || '') + (ms[9] || ''),
|
265 | modifier = (ms[3] || ms[8] || 1) - 1;
|
266 |
|
267 | key.shift = !!(modifier & 1);
|
268 | key.ctrl = !!(modifier & 4);
|
269 | key.meta = !!(modifier & 10);
|
270 | key.code = code;
|
271 |
|
272 | switch (code) {
|
273 |
|
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 |
|
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 |
|
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 |
|
359 |
|
360 | case '[[5~':
|
361 | key.name = PAGEUP;
|
362 | break;
|
363 |
|
364 | case '[[6~':
|
365 | key.name = PAGEDOWN;
|
366 | break;
|
367 |
|
368 |
|
369 |
|
370 | case '[7~':
|
371 | key.name = HOME;
|
372 | break;
|
373 |
|
374 | case '[8~':
|
375 | key.name = END;
|
376 | break;
|
377 |
|
378 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
600 |
|
601 |
|
602 |
|
603 | class 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 |
|
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 |
|
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 |
|
650 | export { IO, KeyCodeEvent, isMouseCode, parseKeycodes, parseMouse };
|