Skip to content

Commit c235708

Browse files
princejwesleyMylesBorins
authored andcommitted
readline: keypress trigger for escape character
Fixes: #7379 PR-URL: #7382 Reviewed-By: jasnell - James M Snell <[email protected]> Reviewed-By: Roman Reiss <[email protected]>
1 parent bdfa3b3 commit c235708

File tree

2 files changed

+83
-1
lines changed

2 files changed

+83
-1
lines changed

lib/readline.js

+17-1
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,9 @@ exports.Interface = Interface;
903903
const KEYPRESS_DECODER = Symbol('keypress-decoder');
904904
const ESCAPE_DECODER = Symbol('escape-decoder');
905905

906+
// GNU readline library - keyseq-timeout is 500ms (default)
907+
const ESCAPE_CODE_TIMEOUT = 500;
908+
906909
function emitKeypressEvents(stream, iface) {
907910
if (stream[KEYPRESS_DECODER]) return;
908911
var StringDecoder = require('string_decoder').StringDecoder; // lazy load
@@ -911,17 +914,26 @@ function emitKeypressEvents(stream, iface) {
911914
stream[ESCAPE_DECODER] = emitKeys(stream);
912915
stream[ESCAPE_DECODER].next();
913916

917+
const escapeCodeTimeout = () => stream[ESCAPE_DECODER].next('');
918+
let timeoutId;
919+
914920
function onData(b) {
915921
if (stream.listenerCount('keypress') > 0) {
916922
var r = stream[KEYPRESS_DECODER].write(b);
917923
if (r) {
924+
clearTimeout(timeoutId);
925+
918926
for (var i = 0; i < r.length; i++) {
919927
if (r[i] === '\t' && typeof r[i + 1] === 'string' && iface) {
920928
iface.isCompletionEnabled = false;
921929
}
922930

923931
try {
924932
stream[ESCAPE_DECODER].next(r[i]);
933+
// Escape letter at the tail position
934+
if (r[i] === '\x1b' && i + 1 === r.length) {
935+
timeoutId = setTimeout(escapeCodeTimeout, ESCAPE_CODE_TIMEOUT);
936+
}
925937
} catch (err) {
926938
// if the generator throws (it could happen in the `keypress`
927939
// event), we need to restart it.
@@ -1252,11 +1264,15 @@ function* emitKeys(stream) {
12521264
key.name = ch.toLowerCase();
12531265
key.shift = /^[A-Z]$/.test(ch);
12541266
key.meta = escaped;
1267+
} else if (escaped) {
1268+
// Escape sequence timeout
1269+
key.name = ch.length ? undefined : 'escape';
1270+
key.meta = true;
12551271
}
12561272

12571273
key.sequence = s;
12581274

1259-
if (key.name !== undefined) {
1275+
if (s.length !== 0 && (key.name !== undefined || escaped)) {
12601276
/* Named character or sequence */
12611277
stream.emit('keypress', escaped ? undefined : s, key);
12621278
} else if (s.length === 1) {

test/parallel/test-readline-keys.js

+66
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,53 @@ 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,
57+
assertDelay) => {
58+
if (!interval) interval = 550;
59+
if (!assertDelay) assertDelay = 550;
60+
return (next) => () => {
61+
62+
if (!Array.isArray(sequences)) {
63+
sequences = [ sequences ];
64+
}
65+
66+
if (!Array.isArray(expectedKeys)) {
67+
expectedKeys = [ expectedKeys ];
68+
}
69+
70+
expectedKeys = expectedKeys.map(function(k) {
71+
return k ? extend({ ctrl: false, meta: false, shift: false }, k) : k;
72+
});
73+
74+
const keys = [];
75+
fi.on('keypress', (s, k) => keys.push(k));
76+
77+
const emitKeys = (arr) => {
78+
var head = arr.shift();
79+
var tail = arr;
80+
if (head) {
81+
fi.write(head);
82+
setTimeout(() => emitKeys(tail), interval);
83+
} else {
84+
setTimeout(() => {
85+
next();
86+
assert.deepStrictEqual(keys, expectedKeys);
87+
}, assertDelay);
88+
}
89+
};
90+
emitKeys(sequences);
91+
};
92+
};
93+
4794
// regular alphanumerics
4895
addTest('io.JS', [
4996
{ name: 'i', sequence: 'i' },
@@ -149,3 +196,22 @@ addTest('\x1b[31ma\x1b[39ma', [
149196
{ name: 'undefined', sequence: '\x1b[39m', code: '[39m' },
150197
{ name: 'a', sequence: 'a' },
151198
]);
199+
200+
// Reduce array of addKeyIntervalTest(..) right to left
201+
// with () => {} as initial function
202+
const runKeyIntervalTests = [
203+
// escape character
204+
addKeyIntervalTest('\x1b', [
205+
{ name: 'escape', sequence: '\x1b', meta: true }
206+
]),
207+
// chain of escape characters
208+
addKeyIntervalTest('\x1b\x1b\x1b\x1b'.split(''), [
209+
{ name: 'escape', sequence: '\x1b', meta: true },
210+
{ name: 'escape', sequence: '\x1b', meta: true },
211+
{ name: 'escape', sequence: '\x1b', meta: true },
212+
{ name: 'escape', sequence: '\x1b', meta: true }
213+
])
214+
].reverse().reduce((acc, fn) => fn(acc), () => {});
215+
216+
// run key interval tests one after another
217+
runKeyIntervalTests();

0 commit comments

Comments
 (0)