Skip to content

Commit fbc5805

Browse files
princejwesleyevanlucas
authored andcommitted
readline: keypress trigger for escape character
Fixes: #7379 PR-URL: #7382 Reviewed-By: jasnell - James M Snell <jasnell@gmail.com> Reviewed-By: Roman Reiss <me@silverwind.io>
1 parent 5d37b49 commit fbc5805

File tree

3 files changed

+79
-1
lines changed

3 files changed

+79
-1
lines changed

lib/internal/readline.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,11 +376,15 @@ function* emitKeys(stream) {
376376
key.name = ch.toLowerCase();
377377
key.shift = /^[A-Z]$/.test(ch);
378378
key.meta = escaped;
379+
} else if (escaped) {
380+
// Escape sequence timeout
381+
key.name = ch.length ? undefined : 'escape';
382+
key.meta = true;
379383
}
380384

381385
key.sequence = s;
382386

383-
if (key.name !== undefined) {
387+
if (s.length !== 0 && (key.name !== undefined || escaped)) {
384388
/* Named character or sequence */
385389
stream.emit('keypress', escaped ? undefined : s, key);
386390
} else if (s.length === 1) {

lib/readline.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,9 @@ exports.Interface = Interface;
926926
const KEYPRESS_DECODER = Symbol('keypress-decoder');
927927
const ESCAPE_DECODER = Symbol('escape-decoder');
928928

929+
// GNU readline library - keyseq-timeout is 500ms (default)
930+
const ESCAPE_CODE_TIMEOUT = 500;
931+
929932
function emitKeypressEvents(stream, iface) {
930933
if (stream[KEYPRESS_DECODER]) return;
931934
var StringDecoder = require('string_decoder').StringDecoder; // lazy load
@@ -934,17 +937,26 @@ function emitKeypressEvents(stream, iface) {
934937
stream[ESCAPE_DECODER] = emitKeys(stream);
935938
stream[ESCAPE_DECODER].next();
936939

940+
const escapeCodeTimeout = () => stream[ESCAPE_DECODER].next('');
941+
let timeoutId;
942+
937943
function onData(b) {
938944
if (stream.listenerCount('keypress') > 0) {
939945
var r = stream[KEYPRESS_DECODER].write(b);
940946
if (r) {
947+
clearTimeout(timeoutId);
948+
941949
for (var i = 0; i < r.length; i++) {
942950
if (r[i] === '\t' && typeof r[i + 1] === 'string' && iface) {
943951
iface.isCompletionEnabled = false;
944952
}
945953

946954
try {
947955
stream[ESCAPE_DECODER].next(r[i]);
956+
// Escape letter at the tail position
957+
if (r[i] === '\x1b' && i + 1 === r.length) {
958+
timeoutId = setTimeout(escapeCodeTimeout, ESCAPE_CODE_TIMEOUT);
959+
}
948960
} catch (err) {
949961
// if the generator throws (it could happen in the `keypress`
950962
// event), we need to restart it.

test/parallel/test-readline-keys.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,49 @@ function addTest(sequences, expectedKeys) {
4444
assert.deepStrictEqual(keys, expectedKeys);
4545
}
4646

47+
// Simulate key interval test cases
48+
// Returns a function that takes `next` test case and returns a thunk
49+
// that can be called to run tests in sequence
50+
// e.g.
51+
// addKeyIntervalTest(..)
52+
// (addKeyIntervalTest(..)
53+
// (addKeyIntervalTest(..)(noop)))()
54+
// where noop is a terminal function(() => {}).
55+
56+
const addKeyIntervalTest = (sequences, expectedKeys, interval = 550,
57+
assertDelay = 550) => {
58+
return (next) => () => {
59+
60+
if (!Array.isArray(sequences)) {
61+
sequences = [ sequences ];
62+
}
63+
64+
if (!Array.isArray(expectedKeys)) {
65+
expectedKeys = [ expectedKeys ];
66+
}
67+
68+
expectedKeys = expectedKeys.map(function(k) {
69+
return k ? extend({ ctrl: false, meta: false, shift: false }, k) : k;
70+
});
71+
72+
const keys = [];
73+
fi.on('keypress', (s, k) => keys.push(k));
74+
75+
const emitKeys = ([head, ...tail]) => {
76+
if (head) {
77+
fi.write(head);
78+
setTimeout(() => emitKeys(tail), interval);
79+
} else {
80+
setTimeout(() => {
81+
next();
82+
assert.deepStrictEqual(keys, expectedKeys);
83+
}, assertDelay);
84+
}
85+
};
86+
emitKeys(sequences);
87+
};
88+
};
89+
4790
// regular alphanumerics
4891
addTest('io.JS', [
4992
{ name: 'i', sequence: 'i' },
@@ -149,3 +192,22 @@ addTest('\x1b[31ma\x1b[39ma', [
149192
{ name: 'undefined', sequence: '\x1b[39m', code: '[39m' },
150193
{ name: 'a', sequence: 'a' },
151194
]);
195+
196+
// Reduce array of addKeyIntervalTest(..) right to left
197+
// with () => {} as initial function
198+
const runKeyIntervalTests = [
199+
// escape character
200+
addKeyIntervalTest('\x1b', [
201+
{ name: 'escape', sequence: '\x1b', meta: true }
202+
]),
203+
// chain of escape characters
204+
addKeyIntervalTest('\x1b\x1b\x1b\x1b'.split(''), [
205+
{ name: 'escape', sequence: '\x1b', meta: true },
206+
{ name: 'escape', sequence: '\x1b', meta: true },
207+
{ name: 'escape', sequence: '\x1b', meta: true },
208+
{ name: 'escape', sequence: '\x1b', meta: true }
209+
])
210+
].reverse().reduce((acc, fn) => fn(acc), () => {});
211+
212+
// run key interval tests one after another
213+
runKeyIntervalTests();

0 commit comments

Comments
 (0)