Skip to content

Commit

Permalink
readline: keypress trigger for escape character
Browse files Browse the repository at this point in the history
Fixes: #7379
PR-URL: #7382
Reviewed-By: jasnell - James M Snell <jasnell@gmail.com>
Reviewed-By: Roman Reiss <me@silverwind.io>
  • Loading branch information
princejwesley authored and evanlucas committed Aug 20, 2016
1 parent 5d37b49 commit fbc5805
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 1 deletion.
6 changes: 5 additions & 1 deletion lib/internal/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
12 changes: 12 additions & 0 deletions lib/readline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -934,17 +937,26 @@ 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;
}

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.
Expand Down
62 changes: 62 additions & 0 deletions test/parallel/test-readline-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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();

0 comments on commit fbc5805

Please sign in to comment.