| 'use strict'; |
| |
| // Regex used for ansi escape code splitting |
| // Adopted from https://github.com/chalk/ansi-regex/blob/master/index.js |
| // License: MIT, authors: @sindresorhus, Qix-, and arjunmehta |
| // Matches all ansi escape code sequences in a string |
| /* eslint-disable no-control-regex */ |
| const ansi = |
| /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g; |
| /* eslint-enable no-control-regex */ |
| |
| const kEscape = '\x1b'; |
| |
| let getStringWidth; |
| let isFullWidthCodePoint; |
| |
| function CSI(strings, ...args) { |
| let ret = `${kEscape}[`; |
| for (var n = 0; n < strings.length; n++) { |
| ret += strings[n]; |
| if (n < args.length) |
| ret += args[n]; |
| } |
| return ret; |
| } |
| |
| CSI.kEscape = kEscape; |
| CSI.kClearToBeginning = CSI`1K`; |
| CSI.kClearToEnd = CSI`0K`; |
| CSI.kClearLine = CSI`2K`; |
| CSI.kClearScreenDown = CSI`0J`; |
| |
| if (internalBinding('config').hasIntl) { |
| const icu = internalBinding('icu'); |
| getStringWidth = function getStringWidth(str, options) { |
| options = options || {}; |
| if (!Number.isInteger(str)) |
| str = stripVTControlCharacters(String(str)); |
| return icu.getStringWidth( |
| str, |
| Boolean(options.ambiguousAsFullWidth), |
| Boolean(options.expandEmojiSequence) |
| ); |
| }; |
| isFullWidthCodePoint = |
| function isFullWidthCodePoint(code, options) { |
| if (typeof code !== 'number') |
| return false; |
| return icu.getStringWidth(code, options) === 2; |
| }; |
| } else { |
| /** |
| * Returns the number of columns required to display the given string. |
| */ |
| getStringWidth = function getStringWidth(str) { |
| if (Number.isInteger(str)) |
| return isFullWidthCodePoint(str) ? 2 : 1; |
| |
| let width = 0; |
| |
| str = stripVTControlCharacters(String(str)); |
| |
| for (var i = 0; i < str.length; i++) { |
| const code = str.codePointAt(i); |
| |
| if (code >= 0x10000) { // surrogates |
| i++; |
| } |
| |
| if (isFullWidthCodePoint(code)) { |
| width += 2; |
| } else { |
| width++; |
| } |
| } |
| |
| return width; |
| }; |
| |
| /** |
| * Returns true if the character represented by a given |
| * Unicode code point is full-width. Otherwise returns false. |
| */ |
| isFullWidthCodePoint = function isFullWidthCodePoint(code) { |
| if (!Number.isInteger(code)) { |
| return false; |
| } |
| |
| // Code points are derived from: |
| // http://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt |
| if ( |
| code >= 0x1100 && ( |
| code <= 0x115f || // Hangul Jamo |
| code === 0x2329 || // LEFT-POINTING ANGLE BRACKET |
| code === 0x232a || // RIGHT-POINTING ANGLE BRACKET |
| // CJK Radicals Supplement .. Enclosed CJK Letters and Months |
| code >= 0x2e80 && code <= 0x3247 && code !== 0x303f || |
| // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A |
| code >= 0x3250 && code <= 0x4dbf || |
| // CJK Unified Ideographs .. Yi Radicals |
| code >= 0x4e00 && code <= 0xa4c6 || |
| // Hangul Jamo Extended-A |
| code >= 0xa960 && code <= 0xa97c || |
| // Hangul Syllables |
| code >= 0xac00 && code <= 0xd7a3 || |
| // CJK Compatibility Ideographs |
| code >= 0xf900 && code <= 0xfaff || |
| // Vertical Forms |
| code >= 0xfe10 && code <= 0xfe19 || |
| // CJK Compatibility Forms .. Small Form Variants |
| code >= 0xfe30 && code <= 0xfe6b || |
| // Halfwidth and Fullwidth Forms |
| code >= 0xff01 && code <= 0xff60 || |
| code >= 0xffe0 && code <= 0xffe6 || |
| // Kana Supplement |
| code >= 0x1b000 && code <= 0x1b001 || |
| // Enclosed Ideographic Supplement |
| code >= 0x1f200 && code <= 0x1f251 || |
| // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane |
| code >= 0x20000 && code <= 0x3fffd |
| ) |
| ) { |
| return true; |
| } |
| |
| return false; |
| }; |
| } |
| |
| /** |
| * Tries to remove all VT control characters. Use to estimate displayed |
| * string width. May be buggy due to not running a real state machine |
| */ |
| function stripVTControlCharacters(str) { |
| return str.replace(ansi, ''); |
| } |
| |
| |
| /* |
| 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 |
| */ |
| function* emitKeys(stream) { |
| while (true) { |
| let ch = yield; |
| let s = ch; |
| let escaped = false; |
| const key = { |
| sequence: null, |
| name: undefined, |
| ctrl: false, |
| meta: false, |
| shift: false |
| }; |
| |
| if (ch === kEscape) { |
| escaped = true; |
| s += (ch = yield); |
| |
| if (ch === kEscape) { |
| s += (ch = yield); |
| } |
| } |
| |
| if (escaped && (ch === 'O' || ch === '[')) { |
| // ANSI escape sequence |
| let code = ch; |
| let modifier = 0; |
| |
| if (ch === 'O') { |
| // ESC O letter |
| // ESC O modifier letter |
| s += (ch = yield); |
| |
| if (ch >= '0' && ch <= '9') { |
| modifier = (ch >> 0) - 1; |
| s += (ch = yield); |
| } |
| |
| code += ch; |
| } else if (ch === '[') { |
| // ESC [ letter |
| // ESC [ modifier letter |
| // ESC [ [ modifier letter |
| // ESC [ [ num char |
| s += (ch = yield); |
| |
| if (ch === '[') { |
| // \x1b[[A |
| // ^--- escape codes might have a second bracket |
| code += ch; |
| s += (ch = yield); |
| } |
| |
| /* |
| * Here and later we try to buffer just enough data to get |
| * a complete ascii sequence. |
| * |
| * We have basically two classes of ascii characters to process: |
| * |
| * |
| * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 } |
| * |
| * This particular example is featuring Ctrl+F12 in xterm. |
| * |
| * - `;5` part is optional, e.g. it could be `\x1b[24~` |
| * - first part can contain one or two digits |
| * |
| * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/ |
| * |
| * |
| * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 } |
| * |
| * This particular example is featuring Ctrl+Home in xterm. |
| * |
| * - `1;5` part is optional, e.g. it could be `\x1b[H` |
| * - `1;` part is optional, e.g. it could be `\x1b[5H` |
| * |
| * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/ |
| * |
| */ |
| const cmdStart = s.length - 1; |
| |
| // Skip one or two leading digits |
| if (ch >= '0' && ch <= '9') { |
| s += (ch = yield); |
| |
| if (ch >= '0' && ch <= '9') { |
| s += (ch = yield); |
| } |
| } |
| |
| // skip modifier |
| if (ch === ';') { |
| s += (ch = yield); |
| |
| if (ch >= '0' && ch <= '9') { |
| s += yield; |
| } |
| } |
| |
| /* |
| * We buffered enough data, now trying to extract code |
| * and modifier from it |
| */ |
| const cmd = s.slice(cmdStart); |
| let match; |
| |
| if ((match = cmd.match(/^(\d\d?)(;(\d))?([~^$])$/))) { |
| code += match[1] + match[4]; |
| modifier = (match[3] || 1) - 1; |
| } else if ((match = cmd.match(/^((\d;)?(\d))?([A-Za-z])$/))) { |
| code += match[4]; |
| modifier = (match[3] || 1) - 1; |
| } else { |
| code += cmd; |
| } |
| } |
| |
| // Parse the key modifier |
| key.ctrl = !!(modifier & 4); |
| key.meta = !!(modifier & 10); |
| key.shift = !!(modifier & 1); |
| key.code = code; |
| |
| // Parse the key itself |
| switch (code) { |
| /* 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; |
| |
| /* xterm ESC [ letter */ |
| case '[A': key.name = 'up'; break; |
| case '[B': key.name = 'down'; break; |
| case '[C': key.name = 'right'; break; |
| case '[D': key.name = 'left'; break; |
| case '[E': key.name = 'clear'; break; |
| case '[F': key.name = 'end'; break; |
| case '[H': key.name = 'home'; break; |
| |
| /* xterm/gnome ESC O letter */ |
| case 'OA': key.name = 'up'; break; |
| case 'OB': key.name = 'down'; break; |
| case 'OC': key.name = 'right'; break; |
| case 'OD': key.name = 'left'; break; |
| case 'OE': key.name = 'clear'; break; |
| case 'OF': key.name = 'end'; break; |
| case 'OH': key.name = 'home'; break; |
| |
| /* xterm/rxvt ESC [ number ~ */ |
| case '[1~': key.name = 'home'; break; |
| case '[2~': key.name = 'insert'; break; |
| case '[3~': key.name = 'delete'; break; |
| case '[4~': key.name = 'end'; break; |
| case '[5~': key.name = 'pageup'; break; |
| case '[6~': key.name = 'pagedown'; break; |
| |
| /* putty */ |
| case '[[5~': key.name = 'pageup'; break; |
| case '[[6~': key.name = 'pagedown'; break; |
| |
| /* rxvt */ |
| case '[7~': key.name = 'home'; break; |
| case '[8~': key.name = 'end'; break; |
| |
| /* rxvt keys with modifiers */ |
| case '[a': key.name = 'up'; key.shift = true; break; |
| case '[b': key.name = 'down'; key.shift = true; break; |
| case '[c': key.name = 'right'; key.shift = true; break; |
| case '[d': key.name = 'left'; key.shift = true; break; |
| case '[e': key.name = 'clear'; key.shift = true; break; |
| |
| case '[2$': key.name = 'insert'; key.shift = true; break; |
| case '[3$': key.name = 'delete'; key.shift = true; break; |
| case '[5$': key.name = 'pageup'; key.shift = true; break; |
| case '[6$': key.name = 'pagedown'; key.shift = true; break; |
| case '[7$': key.name = 'home'; key.shift = true; break; |
| case '[8$': key.name = 'end'; key.shift = true; break; |
| |
| case 'Oa': key.name = 'up'; key.ctrl = true; break; |
| case 'Ob': key.name = 'down'; key.ctrl = true; break; |
| case 'Oc': key.name = 'right'; key.ctrl = true; break; |
| case 'Od': key.name = 'left'; key.ctrl = true; break; |
| case 'Oe': key.name = 'clear'; key.ctrl = true; break; |
| |
| case '[2^': key.name = 'insert'; key.ctrl = true; break; |
| case '[3^': key.name = 'delete'; key.ctrl = true; break; |
| case '[5^': key.name = 'pageup'; key.ctrl = true; break; |
| case '[6^': key.name = 'pagedown'; key.ctrl = true; break; |
| case '[7^': key.name = 'home'; key.ctrl = true; break; |
| case '[8^': key.name = 'end'; key.ctrl = true; break; |
| |
| /* misc. */ |
| case '[Z': key.name = 'tab'; key.shift = true; break; |
| default: key.name = 'undefined'; break; |
| } |
| } else if (ch === '\r') { |
| // carriage return |
| key.name = 'return'; |
| } else if (ch === '\n') { |
| // Enter, should have been called linefeed |
| key.name = 'enter'; |
| } else if (ch === '\t') { |
| // tab |
| key.name = 'tab'; |
| } else if (ch === '\b' || ch === '\x7f') { |
| // backspace or ctrl+h |
| key.name = 'backspace'; |
| key.meta = escaped; |
| } else if (ch === kEscape) { |
| // escape key |
| key.name = 'escape'; |
| key.meta = escaped; |
| } else if (ch === ' ') { |
| key.name = 'space'; |
| key.meta = escaped; |
| } else if (!escaped && ch <= '\x1a') { |
| // ctrl+letter |
| key.name = String.fromCharCode(ch.charCodeAt(0) + 'a'.charCodeAt(0) - 1); |
| key.ctrl = true; |
| } else if (/^[0-9A-Za-z]$/.test(ch)) { |
| // Letter, number, shift+letter |
| key.name = ch.toLowerCase(); |
| key.shift = /^[A-Z]$/.test(ch); |
| key.meta = escaped; |
| } else if (escaped) { |
| // Escape sequence timeout |
| key.name = ch.length ? undefined : 'escape'; |
| key.meta = true; |
| } |
| |
| key.sequence = s; |
| |
| if (s.length !== 0 && (key.name !== undefined || escaped)) { |
| /* Named character or sequence */ |
| stream.emit('keypress', escaped ? undefined : s, key); |
| } else if (s.length === 1) { |
| /* Single unnamed character, e.g. "." */ |
| stream.emit('keypress', s, key); |
| } |
| /* Unrecognized or broken escape sequence, don't emit anything */ |
| } |
| } |
| |
| module.exports = { |
| emitKeys, |
| getStringWidth, |
| isFullWidthCodePoint, |
| stripVTControlCharacters, |
| CSI |
| }; |