summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/internal/readline.js6
-rw-r--r--lib/readline.js12
-rw-r--r--test/parallel/test-readline-keys.js62
3 files changed, 79 insertions, 1 deletions
diff --git a/lib/internal/readline.js b/lib/internal/readline.js
index 1cc0b58d67..ce22fb9ffb 100644
--- a/lib/internal/readline.js
+++ b/lib/internal/readline.js
@@ -376,11 +376,15 @@ function* emitKeys(stream) {
key.name = ch.toLowerCase();
key.shift = /^[A-Z]$/.test(ch);
key.meta = escaped;
+ } else if (escaped) {
+ // Escape sequence timeout
+ key.name = ch.length ? undefined : 'escape';
+ key.meta = true;
}
key.sequence = s;
- if (key.name !== undefined) {
+ if (s.length !== 0 && (key.name !== undefined || escaped)) {
/* Named character or sequence */
stream.emit('keypress', escaped ? undefined : s, key);
} else if (s.length === 1) {
diff --git a/lib/readline.js b/lib/readline.js
index 9d34bb740d..f7591b7cc1 100644
--- a/lib/readline.js
+++ b/lib/readline.js
@@ -927,6 +927,9 @@ exports.Interface = Interface;
const KEYPRESS_DECODER = Symbol('keypress-decoder');
const ESCAPE_DECODER = Symbol('escape-decoder');
+// GNU readline library - keyseq-timeout is 500ms (default)
+const ESCAPE_CODE_TIMEOUT = 500;
+
function emitKeypressEvents(stream, iface) {
if (stream[KEYPRESS_DECODER]) return;
var StringDecoder = require('string_decoder').StringDecoder; // lazy load
@@ -935,10 +938,15 @@ function emitKeypressEvents(stream, iface) {
stream[ESCAPE_DECODER] = emitKeys(stream);
stream[ESCAPE_DECODER].next();
+ const escapeCodeTimeout = () => stream[ESCAPE_DECODER].next('');
+ let timeoutId;
+
function onData(b) {
if (stream.listenerCount('keypress') > 0) {
var r = stream[KEYPRESS_DECODER].write(b);
if (r) {
+ clearTimeout(timeoutId);
+
for (var i = 0; i < r.length; i++) {
if (r[i] === '\t' && typeof r[i + 1] === 'string' && iface) {
iface.isCompletionEnabled = false;
@@ -946,6 +954,10 @@ function emitKeypressEvents(stream, iface) {
try {
stream[ESCAPE_DECODER].next(r[i]);
+ // Escape letter at the tail position
+ if (r[i] === '\x1b' && i + 1 === r.length) {
+ timeoutId = setTimeout(escapeCodeTimeout, ESCAPE_CODE_TIMEOUT);
+ }
} catch (err) {
// if the generator throws (it could happen in the `keypress`
// event), we need to restart it.
diff --git a/test/parallel/test-readline-keys.js b/test/parallel/test-readline-keys.js
index ef9e2eba90..6bbbb2e918 100644
--- a/test/parallel/test-readline-keys.js
+++ b/test/parallel/test-readline-keys.js
@@ -44,6 +44,49 @@ function addTest(sequences, expectedKeys) {
assert.deepStrictEqual(keys, expectedKeys);
}
+// Simulate key interval test cases
+// Returns a function that takes `next` test case and returns a thunk
+// that can be called to run tests in sequence
+// e.g.
+// addKeyIntervalTest(..)
+// (addKeyIntervalTest(..)
+// (addKeyIntervalTest(..)(noop)))()
+// where noop is a terminal function(() => {}).
+
+const addKeyIntervalTest = (sequences, expectedKeys, interval = 550,
+ assertDelay = 550) => {
+ return (next) => () => {
+
+ if (!Array.isArray(sequences)) {
+ sequences = [ sequences ];
+ }
+
+ if (!Array.isArray(expectedKeys)) {
+ expectedKeys = [ expectedKeys ];
+ }
+
+ expectedKeys = expectedKeys.map(function(k) {
+ return k ? extend({ ctrl: false, meta: false, shift: false }, k) : k;
+ });
+
+ const keys = [];
+ fi.on('keypress', (s, k) => keys.push(k));
+
+ const emitKeys = ([head, ...tail]) => {
+ if (head) {
+ fi.write(head);
+ setTimeout(() => emitKeys(tail), interval);
+ } else {
+ setTimeout(() => {
+ next();
+ assert.deepStrictEqual(keys, expectedKeys);
+ }, assertDelay);
+ }
+ };
+ emitKeys(sequences);
+ };
+};
+
// regular alphanumerics
addTest('io.JS', [
{ name: 'i', sequence: 'i' },
@@ -149,3 +192,22 @@ addTest('\x1b[31ma\x1b[39ma', [
{ name: 'undefined', sequence: '\x1b[39m', code: '[39m' },
{ name: 'a', sequence: 'a' },
]);
+
+// Reduce array of addKeyIntervalTest(..) right to left
+// with () => {} as initial function
+const runKeyIntervalTests = [
+ // escape character
+ addKeyIntervalTest('\x1b', [
+ { name: 'escape', sequence: '\x1b', meta: true }
+ ]),
+ // chain of escape characters
+ addKeyIntervalTest('\x1b\x1b\x1b\x1b'.split(''), [
+ { name: 'escape', sequence: '\x1b', meta: true },
+ { name: 'escape', sequence: '\x1b', meta: true },
+ { name: 'escape', sequence: '\x1b', meta: true },
+ { name: 'escape', sequence: '\x1b', meta: true }
+ ])
+].reverse().reduce((acc, fn) => fn(acc), () => {});
+
+// run key interval tests one after another
+runKeyIntervalTests();