'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _escape = require('@arpel/escape'); var enumEvents = require('@geia/enum-events'); var enumSignals = require('@geia/enum-signals'); var logger = require('@spare/logger'); var nullish = require('@typen/nullish'); var events = require('events'); var string_decoder = require('string_decoder'); var promises = require('timers/promises'); var enumEvents$1 = require('@pres/enum-events'); var enumKeyNames = require('@pres/enum-key-names'); var vectorMerge = require('@vect/vector-merge'); const QUERY_CURSOR = 'query-cursor'; const ARPEL = logger.ros('arpel'); class IO extends events.EventEmitter { #i = process.stdin; #o = process.stdout; #d = new string_decoder.StringDecoder('utf-8'); #h = {}; #raw = this.#i.isRaw; #log = true; constructor(options = {}) { super(); if (options.input) this.#i = options.input; if (options.output) this.#o = options.output; if (nullish.valid(options.log)) this.#log = options.log; this.configEvents(); } static build(options) { return new IO(options); } static logger(...args) { console.debug(`${ARPEL} >>`, ...args); } configEvents() { this.#i.setRawMode(true); this.#h.queryCursor = this.#queryCursorResolver.bind(this); this.#i.on(enumEvents.DATA, this.#h.data = this.#dataHandler.bind(this)); process.on(enumSignals.SIGINT, this.#h.sigint = this.#sigintHandler.bind(this)); process.on(enumEvents.EXIT, this.#h.exit = this.#exitHandler.bind(this)); } removeEvents() { this.#i.setRawMode(this.#raw); if (this.#h.queryCursor) this.removeAllListeners(QUERY_CURSOR), delete this.#h.queryCursor; if (this.#h.data) this.#i.off(enumEvents.DATA, this.#h.data), delete this.#h.data; if (this.#h.sigint) process.off(enumSignals.SIGINT, this.#h.sigint), delete this.#h.sigint; if (this.#h.exit) process.off(enumEvents.EXIT, this.#h.exit), delete this.#h.exit; if (this.#log) IO.logger('remove events', logger.ros(QUERY_CURSOR), this.listenerCount(QUERY_CURSOR), logger.ros(enumEvents.DATA), this.#i.listenerCount(enumEvents.DATA), logger.ros(enumSignals.SIGINT), process.listenerCount(enumSignals.SIGINT), logger.ros(enumEvents.EXIT), process.listenerCount(enumEvents.EXIT)); } get input() { return this.#i; } get output() { return this.#o; } get decoder() { return this.#d; } get handlers() { return this.#h; } get size() { return this.#o.getWindowSize(); } get width() { return this.#o.columns; } get height() { return this.#o.rows; } asyncCursorPos() { return new Promise(this.#h.queryCursor); } #queryCursorResolver(cursorHandler) { this.#o.write(_escape.cursor.QUERY_POS); this.once(QUERY_CURSOR, coord => cursorHandler(coord)); Promise.resolve(promises.setTimeout(800)).then(() => cursorHandler([null, null])); } #dataHandler(buffer) { const str = this.#d.write(buffer); let ms, r, c; 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 ],) // else if (isMouseCode(str)) {} // else IO.logger(ros(KEYPRESS), decoSamples(parseKeycodes(str))) if (str === '') process.emit(enumSignals.SIGINT, enumSignals.SIGINT); } #sigintHandler(signal) { if (this.#log) IO.logger(`receive signal: ${logger.ros(signal)}, closing`); process.exit(signal); } #exitHandler(code) { this.removeEvents(); if (this.#log) IO.logger(`exit with code: ${logger.ros(String(code))}`); } } /* Some patterns seen in terminal key escape codes, derived from combos seen at http://www.midnight-commander.org/browser/lib/tty/key.c ESC letter ESC [ letter ESC [ modifier letter ESC [ 1 ; modifier letter ESC [ num char ESC [ num ; modifier char ESC O letter ESC O modifier letter ESC O 1 ; modifier letter ESC N letter ESC [ [ num ; modifier char ESC [ [ 1 ; modifier letter ESC ESC [ num char ESC ESC O letter - char is usually ~ but $ and ^ also happen with rxvt - modifier is 1 + (shift * 1) + (left_alt * 2) + (ctrl * 4) + (right_alt * 8) - two leading ESCs apparently mean the same as one leading ESC */ // Regexes used for ansi escape code splitting const ANY_META_KEYCODE = /([a-zA-Z0-9])/; // metaKeyCodeReAnywhere const PRE_META_KEYCODE = new RegExp('^' + ANY_META_KEYCODE.source + '$'); // metaKeyCodeRe const ANY_FUNC_KEYCODE = new RegExp('+(O|N|\\[|\\[\\[)(?:' + ['(\\d+)(?:;(\\d+))?([~^$])', '(?:M([@ #!a`])(.)(.))', // mouse '(?:1;)?(\\d+)?([a-zA-Z])'].join('|') + ')'); // functionKeyCodeReAnywhere const PRE_FUNC_KEYCODE = new RegExp('^' + ANY_FUNC_KEYCODE.source); // functionKeyCodeRe const ANY_ESCAPE_KEYCODE = new RegExp([ANY_FUNC_KEYCODE.source, ANY_META_KEYCODE.source, /./.source].join('|')); // escapeCodeReAnywhere // str begins with \x1b[M // reuse the key array albeit its name // otherwise recompute as the mouse event is structured differently const parseMouse = str => { const modifier = str.charCodeAt(3); const isScroll = (modifier & 96) === 96; return { name: isScroll ? 'scroll' : modifier & 64 ? 'move' : 'click', shift: !!(modifier & 4), meta: !!(modifier & 8), ctrl: !!(modifier & 16), x: str.charCodeAt(4) - 32, y: str.charCodeAt(5) - 32, button: isScroll ? modifier & 1 ? 'down' : 'up' : mouseButtonName(modifier & 3), seq: str, buf: Buffer.from(str), modifier }; }; const mouseButtonName = bv => { if (bv === 0) return 'left'; if (bv === 1) return 'middle'; if (bv === 2) return 'right'; if (bv === 3) return 'none'; return null; }; 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); // if (( modifier & 96 ) === 96) { // key.name = 'scroll' // key.button = modifier & 1 ? 'down' : 'up' // } // else { // key.name = modifier & 64 ? 'move' : 'click' // key.button = mouseButtonName(modifier & 3) // } const processBuffer = text => { if (text[0] > 127 && text[1] === undefined) { text[0] -= 128; return '' + text.toString(); } else { return text.toString(); } }; /** * accepts a readable Stream instance and makes it emit "keypress" events */ const parseKeycodes = text => { if (Buffer.isBuffer(text)) text = processBuffer(text); const keycodes = []; if (isMouseCode(text)) return keycodes; let ms, ph; while ((ms = ANY_ESCAPE_KEYCODE.exec(text)) && ([ph] = ms)) { vectorMerge.acquire(keycodes, text.slice(0, ms.index).split('')); keycodes.push(ph); text = text.slice(ms.index + ph.length); } vectorMerge.acquire(keycodes, text.split('')); return keycodes.map(parseKeycode); }; const parseKeycode = str => { const key = { name: undefined, shift: false, meta: false, ctrl: false, ch: str.length === 1 ? str : undefined, seq: str, buf: Buffer.from(str) }; let ms; if (str === '\r') { key.name = enumKeyNames.RETURN; } else if (str === '\n') { key.name = enumKeyNames.ENTER; } // enter, should have been called linefeed // linefeed // key.name = 'linefeed'; else if (str === '\t') { key.name = enumKeyNames.TAB; } else if (str === '\b' || str === '' || str === '' || str === '\b') { key.name = enumKeyNames.BACKSPACE, key.meta = str.charAt(0) === ''; } // backspace or ctrl+h else if (str === '' || str === '') { key.name = enumKeyNames.ESCAPE, key.meta = str.length === 2; } // escape key else if (str === ' ' || str === ' ') { key.name = enumKeyNames.SPACE, key.meta = str.length === 2; } else if (str.length === 1 && str <= '') { key.name = String.fromCharCode(str.charCodeAt(0) + 'a'.charCodeAt(0) - 1), key.ctrl = true; } // ctrl+letter else if (str.length === 1 && str >= 'a' && str <= 'z') { key.name = str; } // lowercase letter else if (str.length === 1 && str >= 'A' && str <= 'Z') { key.name = str.toLowerCase(), key.shift = true; } // shift+letter else if (ms = PRE_META_KEYCODE.exec(str)) { key.name = ms[1].toLowerCase(), key.meta = true, key.shift = /^[A-Z]$/.test(ms[1]); } // meta+character key else if (ms = PRE_FUNC_KEYCODE.exec(str)) { // ansi escape sequence // reassemble the key code leaving out leading 's, // the modifier key bitflag and any meaningless "1;" sequence const code = (ms[1] || '') + (ms[2] || '') + (ms[4] || '') + (ms[9] || ''), modifier = (ms[3] || ms[8] || 1) - 1; // Parse the key modifier key.shift = !!(modifier & 1); key.ctrl = !!(modifier & 4); key.meta = !!(modifier & 10); key.code = code; // Parse the key itself switch (code) { /* xterm ESC [ letter */ case '[A': key.name = enumKeyNames.UP; break; case '[B': key.name = enumKeyNames.DOWN; break; case '[C': key.name = enumKeyNames.RIGHT; break; case '[D': key.name = enumKeyNames.LEFT; break; case '[E': key.name = enumKeyNames.CLEAR; break; case '[F': key.name = enumKeyNames.END; break; case '[H': key.name = enumKeyNames.HOME; break; /* xterm/gnome ESC O letter */ case 'OA': key.name = enumKeyNames.UP; break; case 'OB': key.name = enumKeyNames.DOWN; break; case 'OC': key.name = enumKeyNames.RIGHT; break; case 'OD': key.name = enumKeyNames.LEFT; break; case 'OE': key.name = enumKeyNames.CLEAR; break; case 'OF': key.name = enumKeyNames.END; break; case 'OH': key.name = enumKeyNames.HOME; break; /* xterm/rxvt ESC [ number ~ */ case '[1~': key.name = enumKeyNames.HOME; break; case '[2~': key.name = enumKeyNames.INSERT; break; case '[3~': key.name = enumKeyNames.DELETE; break; case '[4~': key.name = enumKeyNames.END; break; case '[5~': key.name = enumKeyNames.PAGEUP; break; case '[6~': key.name = enumKeyNames.PAGEDOWN; break; /* putty */ case '[[5~': key.name = enumKeyNames.PAGEUP; break; case '[[6~': key.name = enumKeyNames.PAGEDOWN; break; /* rxvt */ case '[7~': key.name = enumKeyNames.HOME; break; case '[8~': key.name = enumKeyNames.END; break; /* rxvt keys with modifiers */ case '[a': key.name = enumKeyNames.UP; key.shift = true; break; case '[b': key.name = enumKeyNames.DOWN; key.shift = true; break; case '[c': key.name = enumKeyNames.RIGHT; key.shift = true; break; case '[d': key.name = enumKeyNames.LEFT; key.shift = true; break; case '[e': key.name = enumKeyNames.CLEAR; key.shift = true; break; case '[2$': key.name = enumKeyNames.INSERT; key.shift = true; break; case '[3$': key.name = enumKeyNames.DELETE; key.shift = true; break; case '[5$': key.name = enumKeyNames.PAGEUP; key.shift = true; break; case '[6$': key.name = enumKeyNames.PAGEDOWN; key.shift = true; break; case '[7$': key.name = enumKeyNames.HOME; key.shift = true; break; case '[8$': key.name = enumKeyNames.END; key.shift = true; break; case 'Oa': key.name = enumKeyNames.UP; key.ctrl = true; break; case 'Ob': key.name = enumKeyNames.DOWN; key.ctrl = true; break; case 'Oc': key.name = enumKeyNames.RIGHT; key.ctrl = true; break; case 'Od': key.name = enumKeyNames.LEFT; key.ctrl = true; break; case 'Oe': key.name = enumKeyNames.CLEAR; key.ctrl = true; break; case '[2^': key.name = enumKeyNames.INSERT; key.ctrl = true; break; case '[3^': key.name = enumKeyNames.DELETE; key.ctrl = true; break; case '[5^': key.name = enumKeyNames.PAGEUP; key.ctrl = true; break; case '[6^': key.name = enumKeyNames.PAGEDOWN; key.ctrl = true; break; case '[7^': key.name = enumKeyNames.HOME; key.ctrl = true; break; case '[8^': key.name = enumKeyNames.END; key.ctrl = true; break; /* xterm/gnome ESC O letter */ case 'OP': key.name = 'f1'; break; case 'OQ': key.name = 'f2'; break; case 'OR': key.name = 'f3'; break; case 'OS': key.name = 'f4'; break; /* xterm/rxvt ESC [ number ~ */ case '[11~': key.name = 'f1'; break; case '[12~': key.name = 'f2'; break; case '[13~': key.name = 'f3'; break; case '[14~': key.name = 'f4'; break; /* from Cygwin and used in libuv */ case '[[A': key.name = 'f1'; break; case '[[B': key.name = 'f2'; break; case '[[C': key.name = 'f3'; break; case '[[D': key.name = 'f4'; break; case '[[E': key.name = 'f5'; break; /* common */ case '[15~': key.name = 'f5'; break; case '[17~': key.name = 'f6'; break; case '[18~': key.name = 'f7'; break; case '[19~': key.name = 'f8'; break; case '[20~': key.name = 'f9'; break; case '[21~': key.name = 'f10'; break; case '[23~': key.name = 'f11'; break; case '[24~': key.name = 'f12'; break; /* misc. */ case '[Z': key.name = enumKeyNames.TAB; key.shift = true; break; default: key.name = enumKeyNames.UNDEFINED; break; } } return key; }; /** * keys.js - emit key presses * Copyright (c) 2010-2015, Joyent, Inc. and other contributors (MIT License) * https://github.com/chjj/blessed */ class KeyCodeEvent { static registerKeypress(stream, keypressEventName = enumEvents$1.KEYPRESS) { if (stream.keycodeDecoder) { return void 0; } else { stream.keycodeDecoder = new string_decoder.StringDecoder('utf8'); } function dataHandler(buffer) { if (stream.listenerCount(enumEvents$1.KEYPRESS) > 0) { /** @type {StringDecoder }*/ const keycodeDecoder = stream.keycodeDecoder; const text = keycodeDecoder.write(buffer); if (text) { const buffers = parseKeycodes(text); for (let { key, ch } of buffers) { if (key || ch) stream.emit(keypressEventName, ch, key); } } } else { // Nobody's watching anyway stream.off(enumEvents$1.DATA, dataHandler); stream.on(enumEvents$1.NEW_LISTENER, enrollHandler); } } function enrollHandler(event) { if (event === enumEvents$1.KEYPRESS) { stream.on(enumEvents$1.DATA, dataHandler); stream.off(enumEvents$1.NEW_LISTENER, enrollHandler); } } if (stream.listenerCount(enumEvents$1.KEYPRESS) > 0) { stream.on(enumEvents$1.DATA, dataHandler); } else { stream.on(enumEvents$1.NEW_LISTENER, enrollHandler); } } } exports.IO = IO; exports.KeyCodeEvent = KeyCodeEvent; exports.isMouseCode = isMouseCode; exports.parseKeycodes = parseKeycodes; exports.parseMouse = parseMouse;