diff options
author | Alex Kocharin <alex@kocharin.ru> | 2015-05-03 18:11:33 +0000 |
---|---|---|
committer | Roman Reiss <me@silverwind.io> | 2015-05-10 04:48:50 +0200 |
commit | aed6bce9064915bda28237b1a5fbf7fcdbf439ef (patch) | |
tree | 0196f43cfb6d416c79e569d1333c20c02822d896 /lib | |
parent | 64d3210c98acf1d991a413e0993e07886c9e90de (diff) | |
download | android-node-v8-aed6bce9064915bda28237b1a5fbf7fcdbf439ef.tar.gz android-node-v8-aed6bce9064915bda28237b1a5fbf7fcdbf439ef.tar.bz2 android-node-v8-aed6bce9064915bda28237b1a5fbf7fcdbf439ef.zip |
readline: turn emitKeys into a streaming parser
In certain environments escape sequences could be splitted into
multiple chunks. For example, when user presses left arrow,
`\x1b[D` sequence could appear as two keypresses (`\x1b` + `[D`).
PR-URL: https://github.com/iojs/io.js/pull/1601
Fixes: https://github.com/iojs/io.js/issues/1403
Reviewed-By: Jeremiah Senkpiel <fishrock123@rocketmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/readline.js | 261 |
1 files changed, 167 insertions, 94 deletions
diff --git a/lib/readline.js b/lib/readline.js index d6ae9dad3d..5782eb86e3 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -893,15 +893,25 @@ exports.Interface = Interface; * accepts a readable Stream instance and makes it emit "keypress" events */ +const KEYPRESS_DECODER = Symbol('keypress-decoder'); +const ESCAPE_DECODER = Symbol('escape-decoder'); + function emitKeypressEvents(stream) { - if (stream._keypressDecoder) return; + if (stream[KEYPRESS_DECODER]) return; var StringDecoder = require('string_decoder').StringDecoder; // lazy load - stream._keypressDecoder = new StringDecoder('utf8'); + stream[KEYPRESS_DECODER] = new StringDecoder('utf8'); + + stream[ESCAPE_DECODER] = emitKeys(stream); + stream[ESCAPE_DECODER].next(); function onData(b) { if (EventEmitter.listenerCount(stream, 'keypress') > 0) { - var r = stream._keypressDecoder.write(b); - if (r) emitKeys(stream, r); + var r = stream[KEYPRESS_DECODER].write(b); + if (r) { + for (var i = 0; i < r.length; i++) { + stream[ESCAPE_DECODER].next(r[i]); + } + } } else { // Nobody's watching anyway stream.removeListener('data', onData); @@ -954,102 +964,130 @@ exports.emitKeypressEvents = emitKeypressEvents; // Regexes used for ansi escape code splitting const metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/; -const metaKeyCodeRe = new RegExp('^' + metaKeyCodeReAnywhere.source + '$'); const functionKeyCodeReAnywhere = new RegExp('(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [ '(\\d+)(?:;(\\d+))?([~^$])', '(?:M([@ #!a`])(.)(.))', // mouse '(?:1;)?(\\d+)?([a-zA-Z])' ].join('|') + ')'); -const functionKeyCodeRe = new RegExp('^' + functionKeyCodeReAnywhere.source); -const escapeCodeReAnywhere = new RegExp([ - functionKeyCodeReAnywhere.source, metaKeyCodeReAnywhere.source, /\x1b./.source -].join('|')); - -function emitKeys(stream, s) { - if (s instanceof Buffer) { - if (s[0] > 127 && s[1] === undefined) { - s[0] -= 128; - s = '\x1b' + s.toString(stream.encoding || 'utf-8'); - } else { - s = s.toString(stream.encoding || 'utf-8'); - } - } - var buffer = []; - var match; - while (match = escapeCodeReAnywhere.exec(s)) { - buffer = buffer.concat(s.slice(0, match.index).split('')); - buffer.push(match[0]); - s = s.slice(match.index + match[0].length); - } - buffer = buffer.concat(s.split('')); - - buffer.forEach(function(s) { - var ch, - key = { - sequence: s, - name: undefined, - ctrl: false, - meta: false, - shift: false - }, - parts; - - if (s === '\r') { - // carriage return - key.name = 'return'; - } else if (s === '\n') { - // enter, should have been called linefeed - key.name = 'enter'; +function* emitKeys(stream) { + while (true) { + var ch = yield; + var s = ch; + var escaped = false; + var key = { + sequence: null, + name: undefined, + ctrl: false, + meta: false, + shift: false + }; + + if (ch === '\x1b') { + escaped = true; + s += (ch = yield); + + if (ch === '\x1b') { + s += (ch = yield); + } + } - } else if (s === '\t') { - // tab - key.name = 'tab'; + if (escaped && (ch === 'O' || ch === '[')) { + // ansi escape sequence + var code = ch; + var modifier = 0; - } 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'); + if (ch === 'O') { + // ESC O letter + // ESC O modifier letter + s += (ch = yield); - } else if (s === '\x1b' || s === '\x1b\x1b') { - // escape key - key.name = 'escape'; - key.meta = (s.length === 2); + if (ch >= '0' && ch <= '9') { + modifier = (ch >> 0) - 1; + s += (ch = yield); + } - } else if (s === ' ' || s === '\x1b ') { - key.name = 'space'; - key.meta = (s.length === 2); + code += ch; - } else if (s.length === 1 && s <= '\x1a') { - // ctrl+letter - key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); - key.ctrl = true; + } else if (ch === '[') { + // ESC [ letter + // ESC [ modifier letter + // ESC [ [ modifier letter + // ESC [ [ num char + s += (ch = yield); - } else if (s.length === 1 && s >= 'a' && s <= 'z') { - // lowercase letter - key.name = s; + if (ch === '[') { + // \x1b[[A + // ^--- escape codes might have a second bracket + code += ch; + s += (ch = yield); + } - } else if (s.length === 1 && s >= 'A' && s <= 'Z') { - // shift+letter - key.name = s.toLowerCase(); - key.shift = true; + /* + * 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); + } + } - } else if (parts = metaKeyCodeRe.exec(s)) { - // meta+character key - key.name = parts[1].toLowerCase(); - key.meta = true; - key.shift = /^[A-Z]$/.test(parts[1]); + // skip modifier + if (ch === ';') { + s += (ch = yield); - } else if (parts = functionKeyCodeRe.exec(s)) { - // ansi escape sequence + if (ch >= '0' && ch <= '9') { + s += (ch = yield); + } + } - // reassemble the key code leaving out leading \x1b's, - // the modifier key bitflag and any meaningless "1;" sequence - var code = (parts[1] || '') + (parts[2] || '') + - (parts[4] || '') + (parts[9] || ''), - modifier = (parts[3] || parts[8] || 1) - 1; + /* + * We buffered enough data, now trying to extract code + * and modifier from it + */ + const cmd = s.slice(cmdStart); + var 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); @@ -1152,23 +1190,58 @@ function emitKeys(stream, s) { /* misc. */ case '[Z': key.name = 'tab'; key.shift = true; break; default: key.name = 'undefined'; break; - } - } - // Don't emit a key if no name was found - if (key.name === undefined) { - key = undefined; - } + } 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'; - if (s.length === 1) { - ch = s; + } else if (ch === '\b' || ch === '\x7f') { + // backspace or ctrl+h + key.name = 'backspace'; + key.meta = escaped; + + } else if (ch === '\x1b') { + // 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; } - if (key || ch) { - stream.emit('keypress', ch, key); + key.sequence = s; + + if (key.name !== undefined) { + /* 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); + } else { + /* Unrecognized or broken escape sequence, don't emit anything */ } - }); + } } |