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,6 +976,16 @@ function emitKey(stream, 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(''));
buffer.forEach(function(s) {
key.sequence = s; key.sequence = s;
if (s === '\r') { if (s === '\r') {
@ -1025,8 +1041,8 @@ function emitKey(stream, s) {
// reassemble the key code leaving out leading \x1b's, // reassemble the key code leaving out leading \x1b's,
// the modifier key bitflag and any meaningless "1;" sequence // the modifier key bitflag and any meaningless "1;" sequence
var code = (parts[1] || '') + (parts[2] || '') + var code = (parts[1] || '') + (parts[2] || '') +
(parts[4] || '') + (parts[6] || ''), (parts[4] || '') + (parts[9] || ''),
modifier = (parts[3] || parts[5] || 1) - 1; modifier = (parts[3] || parts[8] || 1) - 1;
// Parse the key modifier // Parse the key modifier
key.ctrl = !!(modifier & 4); key.ctrl = !!(modifier & 4);
@ -1131,13 +1147,6 @@ function emitKey(stream, s) {
default: key.name = 'undefined'; 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
@ -1152,6 +1161,7 @@ function emitKey(stream, 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();