Browse Source

readline: keypress trigger for escape character

Fixes: https://github.com/nodejs/node/issues/7379
PR-URL: https://github.com/nodejs/node/pull/7382
Reviewed-By: jasnell - James M Snell <jasnell@gmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
v6.x
Prince J Wesley 9 years ago
committed by Evan Lucas
parent
commit
fbc5805e65
  1. 6
      lib/internal/readline.js
  2. 12
      lib/readline.js
  3. 62
      test/parallel/test-readline-keys.js

6
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) {

12
lib/readline.js

@ -926,6 +926,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
@ -934,10 +937,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;
@ -945,6 +953,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.

62
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();

Loading…
Cancel
Save