| 'use strict'; |
| |
| const readline = require('readline'); |
| const combos = require('./combos'); |
| |
| /* eslint-disable no-control-regex */ |
| const metaKeyCodeRe = /^(?:\x1b)([a-zA-Z0-9])$/; |
| const fnKeyRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; |
| const keyName = { |
| /* xterm/gnome ESC O letter */ |
| 'OP': 'f1', |
| 'OQ': 'f2', |
| 'OR': 'f3', |
| 'OS': 'f4', |
| /* xterm/rxvt ESC [ number ~ */ |
| '[11~': 'f1', |
| '[12~': 'f2', |
| '[13~': 'f3', |
| '[14~': 'f4', |
| /* from Cygwin and used in libuv */ |
| '[[A': 'f1', |
| '[[B': 'f2', |
| '[[C': 'f3', |
| '[[D': 'f4', |
| '[[E': 'f5', |
| /* common */ |
| '[15~': 'f5', |
| '[17~': 'f6', |
| '[18~': 'f7', |
| '[19~': 'f8', |
| '[20~': 'f9', |
| '[21~': 'f10', |
| '[23~': 'f11', |
| '[24~': 'f12', |
| /* xterm ESC [ letter */ |
| '[A': 'up', |
| '[B': 'down', |
| '[C': 'right', |
| '[D': 'left', |
| '[E': 'clear', |
| '[F': 'end', |
| '[H': 'home', |
| /* xterm/gnome ESC O letter */ |
| 'OA': 'up', |
| 'OB': 'down', |
| 'OC': 'right', |
| 'OD': 'left', |
| 'OE': 'clear', |
| 'OF': 'end', |
| 'OH': 'home', |
| /* xterm/rxvt ESC [ number ~ */ |
| '[1~': 'home', |
| '[2~': 'insert', |
| '[3~': 'delete', |
| '[4~': 'end', |
| '[5~': 'pageup', |
| '[6~': 'pagedown', |
| /* putty */ |
| '[[5~': 'pageup', |
| '[[6~': 'pagedown', |
| /* rxvt */ |
| '[7~': 'home', |
| '[8~': 'end', |
| /* rxvt keys with modifiers */ |
| '[a': 'up', |
| '[b': 'down', |
| '[c': 'right', |
| '[d': 'left', |
| '[e': 'clear', |
| |
| '[2$': 'insert', |
| '[3$': 'delete', |
| '[5$': 'pageup', |
| '[6$': 'pagedown', |
| '[7$': 'home', |
| '[8$': 'end', |
| |
| 'Oa': 'up', |
| 'Ob': 'down', |
| 'Oc': 'right', |
| 'Od': 'left', |
| 'Oe': 'clear', |
| |
| '[2^': 'insert', |
| '[3^': 'delete', |
| '[5^': 'pageup', |
| '[6^': 'pagedown', |
| '[7^': 'home', |
| '[8^': 'end', |
| /* misc. */ |
| '[Z': 'tab', |
| } |
| |
| function isShiftKey(code) { |
| return ['[a', '[b', '[c', '[d', '[e', '[2$', '[3$', '[5$', '[6$', '[7$', '[8$', '[Z'].includes(code) |
| } |
| |
| function isCtrlKey(code) { |
| return [ 'Oa', 'Ob', 'Oc', 'Od', 'Oe', '[2^', '[3^', '[5^', '[6^', '[7^', '[8^'].includes(code) |
| } |
| |
| const keypress = (s = '', event = {}) => { |
| let parts; |
| let key = { |
| name: event.name, |
| ctrl: false, |
| meta: false, |
| shift: false, |
| option: false, |
| sequence: s, |
| raw: s, |
| ...event |
| }; |
| |
| if (Buffer.isBuffer(s)) { |
| if (s[0] > 127 && s[1] === void 0) { |
| s[0] -= 128; |
| s = '\x1b' + String(s); |
| } else { |
| s = String(s); |
| } |
| } else if (s !== void 0 && typeof s !== 'string') { |
| s = String(s); |
| } else if (!s) { |
| s = key.sequence || ''; |
| } |
| |
| key.sequence = key.sequence || s || key.name; |
| |
| if (s === '\r') { |
| // carriage return |
| key.raw = void 0; |
| key.name = 'return'; |
| } else if (s === '\n') { |
| // enter, should have been called linefeed |
| key.name = 'enter'; |
| } else if (s === '\t') { |
| // tab |
| key.name = 'tab'; |
| } else if (s === '\b' || s === '\x7f' || s === '\x1b\x7f' || s === '\x1b\b') { |
| // backspace or ctrl+h |
| key.name = 'backspace'; |
| key.meta = s.charAt(0) === '\x1b'; |
| } else if (s === '\x1b' || s === '\x1b\x1b') { |
| // escape key |
| key.name = 'escape'; |
| key.meta = s.length === 2; |
| } else if (s === ' ' || s === '\x1b ') { |
| key.name = 'space'; |
| key.meta = s.length === 2; |
| } else if (s <= '\x1a') { |
| // ctrl+letter |
| key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); |
| key.ctrl = true; |
| } else if (s.length === 1 && s >= '0' && s <= '9') { |
| // number |
| key.name = 'number'; |
| } else if (s.length === 1 && s >= 'a' && s <= 'z') { |
| // lowercase letter |
| key.name = s; |
| } else if (s.length === 1 && s >= 'A' && s <= 'Z') { |
| // shift+letter |
| key.name = s.toLowerCase(); |
| key.shift = true; |
| } else if ((parts = metaKeyCodeRe.exec(s))) { |
| // meta+character key |
| key.meta = true; |
| key.shift = /^[A-Z]$/.test(parts[1]); |
| } else if ((parts = fnKeyRe.exec(s))) { |
| let segs = [...s]; |
| |
| if (segs[0] === '\u001b' && segs[1] === '\u001b') { |
| key.option = true; |
| } |
| |
| // ansi escape sequence |
| // reassemble the key code leaving out leading \x1b's, |
| // the modifier key bitflag and any meaningless "1;" sequence |
| let code = [parts[1], parts[2], parts[4], parts[6]].filter(Boolean).join(''); |
| let modifier = (parts[3] || parts[5] || 1) - 1; |
| |
| // Parse the key modifier |
| key.ctrl = !!(modifier & 4); |
| key.meta = !!(modifier & 10); |
| key.shift = !!(modifier & 1); |
| key.code = code; |
| |
| key.name = keyName[code]; |
| key.shift = isShiftKey(code) || key.shift; |
| key.ctrl = isCtrlKey(code) || key.ctrl; |
| } |
| return key; |
| }; |
| |
| keypress.listen = (options = {}, onKeypress) => { |
| let { stdin } = options; |
| |
| if (!stdin || (stdin !== process.stdin && !stdin.isTTY)) { |
| throw new Error('Invalid stream passed'); |
| } |
| |
| let rl = readline.createInterface({ terminal: true, input: stdin }); |
| readline.emitKeypressEvents(stdin, rl); |
| |
| let on = (buf, key) => onKeypress(buf, keypress(buf, key), rl); |
| let isRaw = stdin.isRaw; |
| |
| if (stdin.isTTY) stdin.setRawMode(true); |
| stdin.on('keypress', on); |
| rl.resume(); |
| |
| let off = () => { |
| if (stdin.isTTY) stdin.setRawMode(isRaw); |
| stdin.removeListener('keypress', on); |
| rl.pause(); |
| rl.close(); |
| }; |
| |
| return off; |
| }; |
| |
| keypress.action = (buf, key, customActions) => { |
| let obj = { ...combos, ...customActions }; |
| if (key.ctrl) { |
| key.action = obj.ctrl[key.name]; |
| return key; |
| } |
| |
| if (key.option && obj.option) { |
| key.action = obj.option[key.name]; |
| return key; |
| } |
| |
| if (key.shift) { |
| key.action = obj.shift[key.name]; |
| return key; |
| } |
| |
| key.action = obj.keys[key.name]; |
| return key; |
| }; |
| |
| module.exports = keypress; |