summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorAlex Kocharin <alex@kocharin.ru>2015-05-03 18:11:33 +0000
committerRoman Reiss <me@silverwind.io>2015-05-10 04:48:50 +0200
commitaed6bce9064915bda28237b1a5fbf7fcdbf439ef (patch)
tree0196f43cfb6d416c79e569d1333c20c02822d896 /lib
parent64d3210c98acf1d991a413e0993e07886c9e90de (diff)
downloadandroid-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.js261
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 */
}
- });
+ }
}