@@ -883,15 +883,25 @@ exports.Interface = Interface;
883883 * accepts a readable Stream instance and makes it emit "keypress" events
884884 */
885885
886+ const KEYPRESS_DECODER = Symbol ( 'keypress-decoder' ) ;
887+ const ESCAPE_DECODER = Symbol ( 'escape-decoder' ) ;
888+
886889function emitKeypressEvents ( stream ) {
887- if ( stream . _keypressDecoder ) return ;
890+ if ( stream [ KEYPRESS_DECODER ] ) return ;
888891 var StringDecoder = require ( 'string_decoder' ) . StringDecoder ; // lazy load
889- stream . _keypressDecoder = new StringDecoder ( 'utf8' ) ;
892+ stream [ KEYPRESS_DECODER ] = new StringDecoder ( 'utf8' ) ;
893+
894+ stream [ ESCAPE_DECODER ] = emitKeys ( stream ) ;
895+ stream [ ESCAPE_DECODER ] . next ( ) ;
890896
891897 function onData ( b ) {
892898 if ( EventEmitter . listenerCount ( stream , 'keypress' ) > 0 ) {
893- var r = stream . _keypressDecoder . write ( b ) ;
894- if ( r ) emitKeys ( stream , r ) ;
899+ var r = stream [ KEYPRESS_DECODER ] . write ( b ) ;
900+ if ( r ) {
901+ for ( var i = 0 ; i < r . length ; i ++ ) {
902+ stream [ ESCAPE_DECODER ] . next ( r [ i ] ) ;
903+ }
904+ }
895905 } else {
896906 // Nobody's watching anyway
897907 stream . removeListener ( 'data' , onData ) ;
@@ -944,102 +954,130 @@ exports.emitKeypressEvents = emitKeypressEvents;
944954
945955// Regexes used for ansi escape code splitting
946956const metaKeyCodeReAnywhere = / (?: \x1b ) ( [ a - z A - Z 0 - 9 ] ) / ;
947- const metaKeyCodeRe = new RegExp ( '^' + metaKeyCodeReAnywhere . source + '$' ) ;
948957const functionKeyCodeReAnywhere = new RegExp ( '(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [
949958 '(\\d+)(?:;(\\d+))?([~^$])' ,
950959 '(?:M([@ #!a`])(.)(.))' , // mouse
951960 '(?:1;)?(\\d+)?([a-zA-Z])'
952961] . join ( '|' ) + ')' ) ;
953- const functionKeyCodeRe = new RegExp ( '^' + functionKeyCodeReAnywhere . source ) ;
954- const escapeCodeReAnywhere = new RegExp ( [
955- functionKeyCodeReAnywhere . source , metaKeyCodeReAnywhere . source , / \x1b ./ . source
956- ] . join ( '|' ) ) ;
957-
958- function emitKeys ( stream , s ) {
959- if ( s instanceof Buffer ) {
960- if ( s [ 0 ] > 127 && s [ 1 ] === undefined ) {
961- s [ 0 ] -= 128 ;
962- s = '\x1b' + s . toString ( stream . encoding || 'utf-8' ) ;
963- } else {
964- s = s . toString ( stream . encoding || 'utf-8' ) ;
965- }
966- }
967962
968- var buffer = [ ] ;
969- var match ;
970- while ( match = escapeCodeReAnywhere . exec ( s ) ) {
971- buffer = buffer . concat ( s . slice ( 0 , match . index ) . split ( '' ) ) ;
972- buffer . push ( match [ 0 ] ) ;
973- s = s . slice ( match . index + match [ 0 ] . length ) ;
974- }
975- buffer = buffer . concat ( s . split ( '' ) ) ;
976-
977- buffer . forEach ( function ( s ) {
978- var ch ,
979- key = {
980- sequence : s ,
981- name : undefined ,
982- ctrl : false ,
983- meta : false ,
984- shift : false
985- } ,
986- parts ;
987-
988- if ( s === '\r' ) {
989- // carriage return
990- key . name = 'return' ;
991963
992- } else if ( s === '\n' ) {
993- // enter, should have been called linefeed
994- key . name = 'enter' ;
964+ function * emitKeys ( stream ) {
965+ while ( true ) {
966+ var ch = yield ;
967+ var s = ch ;
968+ var escaped = false ;
969+ var key = {
970+ sequence : null ,
971+ name : undefined ,
972+ ctrl : false ,
973+ meta : false ,
974+ shift : false
975+ } ;
976+
977+ if ( ch === '\x1b' ) {
978+ escaped = true ;
979+ s += ( ch = yield ) ;
980+
981+ if ( ch === '\x1b' ) {
982+ s += ( ch = yield ) ;
983+ }
984+ }
995985
996- } else if ( s === '\t' ) {
997- // tab
998- key . name = 'tab' ;
986+ if ( escaped && ( ch === 'O' || ch === '[' ) ) {
987+ // ansi escape sequence
988+ var code = ch ;
989+ var modifier = 0 ;
999990
1000- } else if ( s === '\b' || s === '\x7f' ||
1001- s === '\x1b\x7f' || s === '\x1b\b' ) {
1002- // backspace or ctrl+h
1003- key . name = 'backspace' ;
1004- key . meta = ( s . charAt ( 0 ) === '\x1b' ) ;
991+ if ( ch === 'O' ) {
992+ // ESC O letter
993+ // ESC O modifier letter
994+ s += ( ch = yield ) ;
1005995
1006- } else if ( s === '\x1b' || s === '\x1b\x1b ') {
1007- // escape key
1008- key . name = 'escape' ;
1009- key . meta = ( s . length === 2 ) ;
996+ if ( ch >= '0' && ch <= '9 ') {
997+ modifier = ( ch >> 0 ) - 1 ;
998+ s += ( ch = yield ) ;
999+ }
10101000
1011- } else if ( s === ' ' || s === '\x1b ' ) {
1012- key . name = 'space' ;
1013- key . meta = ( s . length === 2 ) ;
1001+ code += ch ;
10141002
1015- } else if ( s . length === 1 && s <= '\x1a' ) {
1016- // ctrl+letter
1017- key . name = String . fromCharCode ( s . charCodeAt ( 0 ) + 'a' . charCodeAt ( 0 ) - 1 ) ;
1018- key . ctrl = true ;
1003+ } else if ( ch === '[' ) {
1004+ // ESC [ letter
1005+ // ESC [ modifier letter
1006+ // ESC [ [ modifier letter
1007+ // ESC [ [ num char
1008+ s += ( ch = yield ) ;
10191009
1020- } else if ( s . length === 1 && s >= 'a' && s <= 'z' ) {
1021- // lowercase letter
1022- key . name = s ;
1010+ if ( ch === '[' ) {
1011+ // \x1b[[A
1012+ // ^--- escape codes might have a second bracket
1013+ code += ch ;
1014+ s += ( ch = yield ) ;
1015+ }
10231016
1024- } else if ( s . length === 1 && s >= 'A' && s <= 'Z' ) {
1025- // shift+letter
1026- key . name = s . toLowerCase ( ) ;
1027- key . shift = true ;
1017+ /*
1018+ * Here and later we try to buffer just enough data to get
1019+ * a complete ascii sequence.
1020+ *
1021+ * We have basically two classes of ascii characters to process:
1022+ *
1023+ *
1024+ * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
1025+ *
1026+ * This particular example is featuring Ctrl+F12 in xterm.
1027+ *
1028+ * - `;5` part is optional, e.g. it could be `\x1b[24~`
1029+ * - first part can contain one or two digits
1030+ *
1031+ * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/
1032+ *
1033+ *
1034+ * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
1035+ *
1036+ * This particular example is featuring Ctrl+Home in xterm.
1037+ *
1038+ * - `1;5` part is optional, e.g. it could be `\x1b[H`
1039+ * - `1;` part is optional, e.g. it could be `\x1b[5H`
1040+ *
1041+ * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
1042+ *
1043+ */
1044+ const cmdStart = s . length - 1 ;
1045+
1046+ // skip one or two leading digits
1047+ if ( ch >= '0' && ch <= '9' ) {
1048+ s += ( ch = yield ) ;
1049+
1050+ if ( ch >= '0' && ch <= '9' ) {
1051+ s += ( ch = yield ) ;
1052+ }
1053+ }
10281054
1029- } else if ( parts = metaKeyCodeRe . exec ( s ) ) {
1030- // meta+character key
1031- key . name = parts [ 1 ] . toLowerCase ( ) ;
1032- key . meta = true ;
1033- key . shift = / ^ [ A - Z ] $ / . test ( parts [ 1 ] ) ;
1055+ // skip modifier
1056+ if ( ch === ';' ) {
1057+ s += ( ch = yield ) ;
10341058
1035- } else if ( parts = functionKeyCodeRe . exec ( s ) ) {
1036- // ansi escape sequence
1059+ if ( ch >= '0' && ch <= '9' ) {
1060+ s += ( ch = yield ) ;
1061+ }
1062+ }
10371063
1038- // reassemble the key code leaving out leading \x1b's,
1039- // the modifier key bitflag and any meaningless "1;" sequence
1040- var code = ( parts [ 1 ] || '' ) + ( parts [ 2 ] || '' ) +
1041- ( parts [ 4 ] || '' ) + ( parts [ 9 ] || '' ) ,
1042- modifier = ( parts [ 3 ] || parts [ 8 ] || 1 ) - 1 ;
1064+ /*
1065+ * We buffered enough data, now trying to extract code
1066+ * and modifier from it
1067+ */
1068+ const cmd = s . slice ( cmdStart ) ;
1069+ var match ;
1070+
1071+ if ( ( match = cmd . match ( / ^ ( \d \d ? ) ( ; ( \d ) ) ? ( [ ~ ^ $ ] ) $ / ) ) ) {
1072+ code += match [ 1 ] + match [ 4 ] ;
1073+ modifier = ( match [ 3 ] || 1 ) - 1 ;
1074+ } else if ( ( match = cmd . match ( / ^ ( ( \d ; ) ? ( \d ) ) ? ( [ A - Z a - z ] ) $ / ) ) ) {
1075+ code += match [ 4 ] ;
1076+ modifier = ( match [ 3 ] || 1 ) - 1 ;
1077+ } else {
1078+ code += cmd ;
1079+ }
1080+ }
10431081
10441082 // Parse the key modifier
10451083 key . ctrl = ! ! ( modifier & 4 ) ;
@@ -1142,23 +1180,58 @@ function emitKeys(stream, s) {
11421180 /* misc. */
11431181 case '[Z' : key . name = 'tab' ; key . shift = true ; break ;
11441182 default : key . name = 'undefined' ; break ;
1145-
11461183 }
1147- }
11481184
1149- // Don't emit a key if no name was found
1150- if ( key . name === undefined ) {
1151- key = undefined ;
1152- }
1185+ } else if ( ch === '\r' ) {
1186+ // carriage return
1187+ key . name = 'return' ;
1188+
1189+ } else if ( ch === '\n' ) {
1190+ // enter, should have been called linefeed
1191+ key . name = 'enter' ;
1192+
1193+ } else if ( ch === '\t' ) {
1194+ // tab
1195+ key . name = 'tab' ;
11531196
1154- if ( s . length === 1 ) {
1155- ch = s ;
1197+ } else if ( ch === '\b' || ch === '\x7f' ) {
1198+ // backspace or ctrl+h
1199+ key . name = 'backspace' ;
1200+ key . meta = escaped ;
1201+
1202+ } else if ( ch === '\x1b' ) {
1203+ // escape key
1204+ key . name = 'escape' ;
1205+ key . meta = escaped ;
1206+
1207+ } else if ( ch === ' ' ) {
1208+ key . name = 'space' ;
1209+ key . meta = escaped ;
1210+
1211+ } else if ( ! escaped && ch <= '\x1a' ) {
1212+ // ctrl+letter
1213+ key . name = String . fromCharCode ( ch . charCodeAt ( 0 ) + 'a' . charCodeAt ( 0 ) - 1 ) ;
1214+ key . ctrl = true ;
1215+
1216+ } else if ( / ^ [ 0 - 9 A - Z a - z ] $ / . test ( ch ) ) {
1217+ // letter, number, shift+letter
1218+ key . name = ch . toLowerCase ( ) ;
1219+ key . shift = / ^ [ A - Z ] $ / . test ( ch ) ;
1220+ key . meta = escaped ;
11561221 }
11571222
1158- if ( key || ch ) {
1159- stream . emit ( 'keypress' , ch , key ) ;
1223+ key . sequence = s ;
1224+
1225+ if ( key . name !== undefined ) {
1226+ /* Named character or sequence */
1227+ stream . emit ( 'keypress' , escaped ? undefined : s , key ) ;
1228+ } else if ( s . length === 1 ) {
1229+ /* Single unnamed character, e.g. "." */
1230+ stream . emit ( 'keypress' , s ) ;
1231+ } else {
1232+ /* Unrecognized or broken escape sequence, don't emit anything */
11601233 }
1161- } ) ;
1234+ }
11621235}
11631236
11641237
0 commit comments