readline: implements keypress buffering

There was an underlying assumption in readline.emitKeypressEvents (and
by extension emitKey) that the given stream (usually process.stdin)
would emit 'data' once per keypress, which is not always the case.

This commit buffers the input stream and ensures a 'keypress' event is
triggered for every keypress (including escape codes).

Signed-off-by: Fedor Indutny <fedor@indutny.com>
This commit is contained in:
Dan Kaplun 2014-05-12 04:42:21 -05:00 committed by Fedor Indutny
parent 6569812531
commit 4a90f51bfe
2 changed files with 188 additions and 154 deletions

View File

@ -893,7 +893,7 @@ function emitKeypressEvents(stream) {
function onData(b) { function onData(b) {
if (EventEmitter.listenerCount(stream, 'keypress') > 0) { if (EventEmitter.listenerCount(stream, 'keypress') > 0) {
var r = stream._keypressDecoder.write(b); var r = stream._keypressDecoder.write(b);
if (r) emitKey(stream, r); if (r) emitKeys(stream, r);
} else { } else {
// Nobody's watching anyway // Nobody's watching anyway
stream.removeListener('data', onData); stream.removeListener('data', onData);
@ -947,11 +947,17 @@ exports.emitKeypressEvents = emitKeypressEvents;
// Regexes used for ansi escape code splitting // Regexes used for ansi escape code splitting
var metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/; var metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/;
var metaKeyCodeRe = new RegExp('^' + metaKeyCodeReAnywhere.source + '$'); var metaKeyCodeRe = new RegExp('^' + metaKeyCodeReAnywhere.source + '$');
var functionKeyCodeReAnywhere = var functionKeyCodeReAnywhere = new RegExp('(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [
/(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; '(\\d+)(?:;(\\d+))?([~^$])',
'(?:M([@ #!a`])(.)(.))', // mouse
'(?:1;)?(\\d+)?([a-zA-Z])'
].join('|') + ')');
var functionKeyCodeRe = new RegExp('^' + functionKeyCodeReAnywhere.source); var functionKeyCodeRe = new RegExp('^' + functionKeyCodeReAnywhere.source);
var escapeCodeReAnywhere = new RegExp([
functionKeyCodeReAnywhere.source, metaKeyCodeReAnywhere.source, /\x1b./.source
].join('|'));
function emitKey(stream, s) { function emitKeys(stream, s) {
var ch, var ch,
key = { key = {
name: undefined, name: undefined,
@ -970,188 +976,192 @@ function emitKey(stream, s) {
} }
} }
key.sequence = s; 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(''));
if (s === '\r') { buffer.forEach(function(s) {
// carriage return key.sequence = s;
key.name = 'return';
} else if (s === '\n') { if (s === '\r') {
// enter, should have been called linefeed // carriage return
key.name = 'enter'; key.name = 'return';
} else if (s === '\t') { } else if (s === '\n') {
// tab // enter, should have been called linefeed
key.name = 'tab'; key.name = 'enter';
} else if (s === '\b' || s === '\x7f' || } else if (s === '\t') {
s === '\x1b\x7f' || s === '\x1b\b') { // tab
// backspace or ctrl+h key.name = 'tab';
key.name = 'backspace';
key.meta = (s.charAt(0) === '\x1b');
} else if (s === '\x1b' || s === '\x1b\x1b') { } else if (s === '\b' || s === '\x7f' ||
// escape key s === '\x1b\x7f' || s === '\x1b\b') {
key.name = 'escape'; // backspace or ctrl+h
key.meta = (s.length === 2); key.name = 'backspace';
key.meta = (s.charAt(0) === '\x1b');
} else if (s === ' ' || s === '\x1b ') { } else if (s === '\x1b' || s === '\x1b\x1b') {
key.name = 'space'; // escape key
key.meta = (s.length === 2); key.name = 'escape';
key.meta = (s.length === 2);
} else if (s.length === 1 && s <= '\x1a') { } else if (s === ' ' || s === '\x1b ') {
// ctrl+letter key.name = 'space';
key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1); key.meta = (s.length === 2);
key.ctrl = true;
} else if (s.length === 1 && s >= 'a' && s <= 'z') { } else if (s.length === 1 && s <= '\x1a') {
// lowercase letter // ctrl+letter
key.name = s; key.name = String.fromCharCode(s.charCodeAt(0) + 'a'.charCodeAt(0) - 1);
key.ctrl = true;
} else if (s.length === 1 && s >= 'A' && s <= 'Z') { } else if (s.length === 1 && s >= 'a' && s <= 'z') {
// shift+letter // lowercase letter
key.name = s.toLowerCase(); key.name = s;
key.shift = true;
} else if (parts = metaKeyCodeRe.exec(s)) { } else if (s.length === 1 && s >= 'A' && s <= 'Z') {
// meta+character key // shift+letter
key.name = parts[1].toLowerCase(); key.name = s.toLowerCase();
key.meta = true; key.shift = true;
key.shift = /^[A-Z]$/.test(parts[1]);
} else if (parts = functionKeyCodeRe.exec(s)) { } else if (parts = metaKeyCodeRe.exec(s)) {
// ansi escape sequence // meta+character key
key.name = parts[1].toLowerCase();
key.meta = true;
key.shift = /^[A-Z]$/.test(parts[1]);
// reassemble the key code leaving out leading \x1b's, } else if (parts = functionKeyCodeRe.exec(s)) {
// the modifier key bitflag and any meaningless "1;" sequence // ansi escape sequence
var code = (parts[1] || '') + (parts[2] || '') +
(parts[4] || '') + (parts[6] || ''),
modifier = (parts[3] || parts[5] || 1) - 1;
// Parse the key modifier // reassemble the key code leaving out leading \x1b's,
key.ctrl = !!(modifier & 4); // the modifier key bitflag and any meaningless "1;" sequence
key.meta = !!(modifier & 10); var code = (parts[1] || '') + (parts[2] || '') +
key.shift = !!(modifier & 1); (parts[4] || '') + (parts[9] || ''),
key.code = code; modifier = (parts[3] || parts[8] || 1) - 1;
// Parse the key itself // Parse the key modifier
switch (code) { key.ctrl = !!(modifier & 4);
/* xterm/gnome ESC O letter */ key.meta = !!(modifier & 10);
case 'OP': key.name = 'f1'; break; key.shift = !!(modifier & 1);
case 'OQ': key.name = 'f2'; break; key.code = code;
case 'OR': key.name = 'f3'; break;
case 'OS': key.name = 'f4'; break;
/* xterm/rxvt ESC [ number ~ */ // Parse the key itself
case '[11~': key.name = 'f1'; break; switch (code) {
case '[12~': key.name = 'f2'; break; /* xterm/gnome ESC O letter */
case '[13~': key.name = 'f3'; break; case 'OP': key.name = 'f1'; break;
case '[14~': key.name = 'f4'; break; case 'OQ': key.name = 'f2'; break;
case 'OR': key.name = 'f3'; break;
case 'OS': key.name = 'f4'; break;
/* from Cygwin and used in libuv */ /* xterm/rxvt ESC [ number ~ */
case '[[A': key.name = 'f1'; break; case '[11~': key.name = 'f1'; break;
case '[[B': key.name = 'f2'; break; case '[12~': key.name = 'f2'; break;
case '[[C': key.name = 'f3'; break; case '[13~': key.name = 'f3'; break;
case '[[D': key.name = 'f4'; break; case '[14~': key.name = 'f4'; break;
case '[[E': key.name = 'f5'; break;
/* common */ /* from Cygwin and used in libuv */
case '[15~': key.name = 'f5'; break; case '[[A': key.name = 'f1'; break;
case '[17~': key.name = 'f6'; break; case '[[B': key.name = 'f2'; break;
case '[18~': key.name = 'f7'; break; case '[[C': key.name = 'f3'; break;
case '[19~': key.name = 'f8'; break; case '[[D': key.name = 'f4'; break;
case '[20~': key.name = 'f9'; break; case '[[E': key.name = 'f5'; break;
case '[21~': key.name = 'f10'; break;
case '[23~': key.name = 'f11'; break;
case '[24~': key.name = 'f12'; break;
/* xterm ESC [ letter */ /* common */
case '[A': key.name = 'up'; break; case '[15~': key.name = 'f5'; break;
case '[B': key.name = 'down'; break; case '[17~': key.name = 'f6'; break;
case '[C': key.name = 'right'; break; case '[18~': key.name = 'f7'; break;
case '[D': key.name = 'left'; break; case '[19~': key.name = 'f8'; break;
case '[E': key.name = 'clear'; break; case '[20~': key.name = 'f9'; break;
case '[F': key.name = 'end'; break; case '[21~': key.name = 'f10'; break;
case '[H': key.name = 'home'; break; case '[23~': key.name = 'f11'; break;
case '[24~': key.name = 'f12'; break;
/* xterm/gnome ESC O letter */ /* xterm ESC [ letter */
case 'OA': key.name = 'up'; break; case '[A': key.name = 'up'; break;
case 'OB': key.name = 'down'; break; case '[B': key.name = 'down'; break;
case 'OC': key.name = 'right'; break; case '[C': key.name = 'right'; break;
case 'OD': key.name = 'left'; break; case '[D': key.name = 'left'; break;
case 'OE': key.name = 'clear'; break; case '[E': key.name = 'clear'; break;
case 'OF': key.name = 'end'; break; case '[F': key.name = 'end'; break;
case 'OH': key.name = 'home'; break; case '[H': key.name = 'home'; break;
/* xterm/rxvt ESC [ number ~ */ /* xterm/gnome ESC O letter */
case '[1~': key.name = 'home'; break; case 'OA': key.name = 'up'; break;
case '[2~': key.name = 'insert'; break; case 'OB': key.name = 'down'; break;
case '[3~': key.name = 'delete'; break; case 'OC': key.name = 'right'; break;
case '[4~': key.name = 'end'; break; case 'OD': key.name = 'left'; break;
case '[5~': key.name = 'pageup'; break; case 'OE': key.name = 'clear'; break;
case '[6~': key.name = 'pagedown'; break; case 'OF': key.name = 'end'; break;
case 'OH': key.name = 'home'; break;
/* putty */ /* xterm/rxvt ESC [ number ~ */
case '[[5~': key.name = 'pageup'; break; case '[1~': key.name = 'home'; break;
case '[[6~': key.name = 'pagedown'; 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;
/* rxvt */ /* putty */
case '[7~': key.name = 'home'; break; case '[[5~': key.name = 'pageup'; break;
case '[8~': key.name = 'end'; break; case '[[6~': key.name = 'pagedown'; break;
/* rxvt keys with modifiers */ /* rxvt */
case '[a': key.name = 'up'; key.shift = true; break; case '[7~': key.name = 'home'; break;
case '[b': key.name = 'down'; key.shift = true; break; case '[8~': key.name = 'end'; 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; /* rxvt keys with modifiers */
case '[3$': key.name = 'delete'; key.shift = true; break; case '[a': key.name = 'up'; key.shift = true; break;
case '[5$': key.name = 'pageup'; key.shift = true; break; case '[b': key.name = 'down'; key.shift = true; break;
case '[6$': key.name = 'pagedown'; key.shift = true; break; case '[c': key.name = 'right'; key.shift = true; break;
case '[7$': key.name = 'home'; key.shift = true; break; case '[d': key.name = 'left'; key.shift = true; break;
case '[8$': key.name = 'end'; key.shift = true; break; case '[e': key.name = 'clear'; key.shift = true; break;
case 'Oa': key.name = 'up'; key.ctrl = true; break; case '[2$': key.name = 'insert'; key.shift = true; break;
case 'Ob': key.name = 'down'; key.ctrl = true; break; case '[3$': key.name = 'delete'; key.shift = true; break;
case 'Oc': key.name = 'right'; key.ctrl = true; break; case '[5$': key.name = 'pageup'; key.shift = true; break;
case 'Od': key.name = 'left'; key.ctrl = true; break; case '[6$': key.name = 'pagedown'; key.shift = true; break;
case 'Oe': key.name = 'clear'; key.ctrl = true; break; case '[7$': key.name = 'home'; key.shift = true; break;
case '[8$': key.name = 'end'; key.shift = true; break;
case '[2^': key.name = 'insert'; key.ctrl = true; break; case 'Oa': key.name = 'up'; key.ctrl = true; break;
case '[3^': key.name = 'delete'; key.ctrl = true; break; case 'Ob': key.name = 'down'; key.ctrl = true; break;
case '[5^': key.name = 'pageup'; key.ctrl = true; break; case 'Oc': key.name = 'right'; key.ctrl = true; break;
case '[6^': key.name = 'pagedown'; key.ctrl = true; break; case 'Od': key.name = 'left'; key.ctrl = true; break;
case '[7^': key.name = 'home'; key.ctrl = true; break; case 'Oe': key.name = 'clear'; key.ctrl = true; break;
case '[8^': key.name = 'end'; key.ctrl = true; break;
/* misc. */ case '[2^': key.name = 'insert'; key.ctrl = true; break;
case '[Z': key.name = 'tab'; key.shift = true; break; case '[3^': key.name = 'delete'; key.ctrl = true; break;
default: key.name = 'undefined'; 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 (s.length > 1 && s[0] !== '\x1b') {
// Got a longer-than-one string of characters.
// Probably a paste, since it wasn't a control sequence.
Array.prototype.forEach.call(s, function(c) {
emitKey(stream, c);
});
return;
}
// Don't emit a key if no name was found // Don't emit a key if no name was found
if (util.isUndefined(key.name)) { if (util.isUndefined(key.name)) {
key = undefined; key = undefined;
} }
if (s.length === 1) { if (s.length === 1) {
ch = s; ch = s;
} }
if (key || ch) { if (key || ch) {
stream.emit('keypress', ch, key); stream.emit('keypress', ch, key);
} }
});
} }

View File

@ -214,6 +214,30 @@ function isWarned(emitter) {
assert.equal(callCount, 1); assert.equal(callCount, 1);
rli.close(); rli.close();
// keypress
[
['a'],
['\x1b'],
['\x1b[31m'],
['\x1b[31m', '\x1b[39m'],
['\x1b[31m', 'a', '\x1b[39m', 'a']
].forEach(function (keypresses) {
fi = new FakeInput();
callCount = 0;
var remainingKeypresses = keypresses.slice();
function keypressListener (ch, key) {
callCount++;
assert.equal(key.sequence, remainingKeypresses.shift());
};
readline.emitKeypressEvents(fi);
fi.on('keypress', keypressListener);
fi.emit('data', keypresses.join(''));
assert.equal(callCount, keypresses.length);
assert.equal(remainingKeypresses.length, 0);
fi.removeListener('keypress', keypressListener);
fi.emit('data', ''); // removes listener
});
if (terminal) { if (terminal) {
// question // question
fi = new FakeInput(); fi = new FakeInput();