From c235708befc4f21602da1c063616d3173fc64bf0 Mon Sep 17 00:00:00 2001 From: Prince J Wesley Date: Thu, 23 Jun 2016 08:42:57 +0530 Subject: [PATCH] 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 Reviewed-By: Roman Reiss --- lib/readline.js | 18 +++++++- test/parallel/test-readline-keys.js | 66 +++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/readline.js b/lib/readline.js index 1ff09d1db66e5a..1962f822ee4fac 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -903,6 +903,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 @@ -911,10 +914,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; @@ -922,6 +930,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. @@ -1252,11 +1264,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/test/parallel/test-readline-keys.js b/test/parallel/test-readline-keys.js index e026c0b583cd9e..d9d04ac03289c3 100644 --- a/test/parallel/test-readline-keys.js +++ b/test/parallel/test-readline-keys.js @@ -44,6 +44,53 @@ 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, + assertDelay) => { + if (!interval) interval = 550; + if (!assertDelay) 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 = (arr) => { + var head = arr.shift(); + var tail = arr; + 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 +196,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();