From 4a90f51bfe9934048520f6a3ffb717fc7ddbe843 Mon Sep 17 00:00:00 2001 From: Dan Kaplun Date: Mon, 12 May 2014 04:42:21 -0500 Subject: [PATCH] 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 --- lib/readline.js | 318 +++++++++++++------------ test/simple/test-readline-interface.js | 24 ++ 2 files changed, 188 insertions(+), 154 deletions(-) diff --git a/lib/readline.js b/lib/readline.js index 19ef759fbb1..5fb9345b1bf 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -893,7 +893,7 @@ function emitKeypressEvents(stream) { function onData(b) { if (EventEmitter.listenerCount(stream, 'keypress') > 0) { var r = stream._keypressDecoder.write(b); - if (r) emitKey(stream, r); + if (r) emitKeys(stream, r); } else { // Nobody's watching anyway stream.removeListener('data', onData); @@ -947,11 +947,17 @@ exports.emitKeypressEvents = emitKeypressEvents; // Regexes used for ansi escape code splitting var metaKeyCodeReAnywhere = /(?:\x1b)([a-zA-Z0-9])/; var metaKeyCodeRe = new RegExp('^' + metaKeyCodeReAnywhere.source + '$'); -var functionKeyCodeReAnywhere = - /(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/; +var functionKeyCodeReAnywhere = new RegExp('(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [ + '(\\d+)(?:;(\\d+))?([~^$])', + '(?:M([@ #!a`])(.)(.))', // mouse + '(?:1;)?(\\d+)?([a-zA-Z])' +].join('|') + ')'); 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, key = { 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') { - // carriage return - key.name = 'return'; + buffer.forEach(function(s) { + key.sequence = s; - } else if (s === '\n') { - // enter, should have been called linefeed - key.name = 'enter'; + if (s === '\r') { + // carriage return + key.name = 'return'; - } else if (s === '\t') { - // tab - key.name = 'tab'; + } else if (s === '\n') { + // enter, should have been called linefeed + key.name = 'enter'; - } 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'); + } else if (s === '\t') { + // tab + key.name = 'tab'; - } else if (s === '\x1b' || s === '\x1b\x1b') { - // escape key - key.name = 'escape'; - key.meta = (s.length === 2); + } 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'); - } else if (s === ' ' || s === '\x1b ') { - key.name = 'space'; - key.meta = (s.length === 2); + } else if (s === '\x1b' || s === '\x1b\x1b') { + // escape key + key.name = 'escape'; + key.meta = (s.length === 2); - } 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 (s === ' ' || s === '\x1b ') { + key.name = 'space'; + key.meta = (s.length === 2); - } else if (s.length === 1 && s >= 'a' && s <= 'z') { - // lowercase letter - key.name = s; + } 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 (s.length === 1 && s >= 'A' && s <= 'Z') { - // shift+letter - key.name = s.toLowerCase(); - key.shift = true; + } else if (s.length === 1 && s >= 'a' && s <= 'z') { + // lowercase letter + key.name = s; - } else if (parts = metaKeyCodeRe.exec(s)) { - // meta+character key - key.name = parts[1].toLowerCase(); - key.meta = true; - key.shift = /^[A-Z]$/.test(parts[1]); + } else if (s.length === 1 && s >= 'A' && s <= 'Z') { + // shift+letter + key.name = s.toLowerCase(); + key.shift = true; - } else if (parts = functionKeyCodeRe.exec(s)) { - // ansi escape sequence + } else if (parts = metaKeyCodeRe.exec(s)) { + // 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, - // the modifier key bitflag and any meaningless "1;" sequence - var code = (parts[1] || '') + (parts[2] || '') + - (parts[4] || '') + (parts[6] || ''), - modifier = (parts[3] || parts[5] || 1) - 1; + } else if (parts = functionKeyCodeRe.exec(s)) { + // ansi escape sequence - // Parse the key modifier - key.ctrl = !!(modifier & 4); - key.meta = !!(modifier & 10); - key.shift = !!(modifier & 1); - key.code = code; + // 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; - // 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; + // Parse the key modifier + key.ctrl = !!(modifier & 4); + key.meta = !!(modifier & 10); + key.shift = !!(modifier & 1); + key.code = code; - /* 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; + // 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; - /* 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; + /* 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; - /* 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; + /* 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; - /* 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; + /* 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/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 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/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; + /* 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; - /* putty */ - case '[[5~': key.name = 'pageup'; break; - case '[[6~': key.name = 'pagedown'; 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; - /* rxvt */ - case '[7~': key.name = 'home'; break; - case '[8~': key.name = 'end'; break; + /* putty */ + case '[[5~': key.name = 'pageup'; break; + case '[[6~': key.name = 'pagedown'; 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; + /* rxvt */ + case '[7~': key.name = 'home'; break; + case '[8~': key.name = 'end'; 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; + /* 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 '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.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 '[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; + 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; - /* misc. */ - case '[Z': key.name = 'tab'; key.shift = true; break; - default: key.name = 'undefined'; 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 (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 - if (util.isUndefined(key.name)) { - key = undefined; - } + // Don't emit a key if no name was found + if (util.isUndefined(key.name)) { + key = undefined; + } - if (s.length === 1) { - ch = s; - } + if (s.length === 1) { + ch = s; + } - if (key || ch) { - stream.emit('keypress', ch, key); - } + if (key || ch) { + stream.emit('keypress', ch, key); + } + }); } diff --git a/test/simple/test-readline-interface.js b/test/simple/test-readline-interface.js index abd3681134e..052602bf138 100644 --- a/test/simple/test-readline-interface.js +++ b/test/simple/test-readline-interface.js @@ -214,6 +214,30 @@ function isWarned(emitter) { assert.equal(callCount, 1); 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) { // question fi = new FakeInput();