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 <[email protected]> Reviewed-By: Roman Reiss <[email protected]>
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+
}elseif(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
}elseif(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
constKEYPRESS_DECODER=Symbol('keypress-decoder');
927927
constESCAPE_DECODER=Symbol('escape-decoder');
928928

929+
// GNU readline library - keyseq-timeout is 500ms (default)
930+
constESCAPE_CODE_TIMEOUT=500;
931+
929932
functionemitKeypressEvents(stream,iface){
930933
if(stream[KEYPRESS_DECODER])return;
931934
varStringDecoder=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+
constescapeCodeTimeout=()=>stream[ESCAPE_DECODER].next('');
941+
lettimeoutId;
942+
937943
functiononData(b){
938944
if(stream.listenerCount('keypress')>0){
939945
varr=stream[KEYPRESS_DECODER].write(b);
940946
if(r){
947+
clearTimeout(timeoutId);
948+
941949
for(vari=0;i<r.length;i++){
942950
if(r[i]==='\t'&&typeofr[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+
constaddKeyIntervalTest=(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+
returnk ? extend({ctrl: false,meta: false,shift: false},k) : k;
70+
});
71+
72+
constkeys=[];
73+
fi.on('keypress',(s,k)=>keys.push(k));
74+
75+
constemitKeys=([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+
construnKeyIntervalTests=[
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)