@@ -893,15 +893,25 @@ exports.Interface = Interface;
893893 * accepts a readable Stream instance and makes it emit "keypress" events
894894 */
895895
896+ const KEYPRESS_DECODER = Symbol ( 'keypress-decoder' ) ;
897+ const ESCAPE_DECODER = Symbol ( 'escape-decoder' ) ;
898+
896899function emitKeypressEvents ( stream ) {
897- if ( stream . _keypressDecoder ) return ;
900+ if ( stream [ KEYPRESS_DECODER ] ) return ;
898901 var StringDecoder = require ( 'string_decoder' ) . StringDecoder ; // lazy load
899- stream . _keypressDecoder = new StringDecoder ( 'utf8' ) ;
902+ stream [ KEYPRESS_DECODER ] = new StringDecoder ( 'utf8' ) ;
903+
904+ stream [ ESCAPE_DECODER ] = emitKeys ( stream ) ;
905+ stream [ ESCAPE_DECODER ] . next ( ) ;
900906
901907 function onData ( b ) {
902908 if ( EventEmitter . listenerCount ( stream , 'keypress' ) > 0 ) {
903- var r = stream . _keypressDecoder . write ( b ) ;
904- if ( r ) emitKeys ( stream , r ) ;
909+ var r = stream [ KEYPRESS_DECODER ] . write ( b ) ;
910+ if ( r ) {
911+ for ( var i = 0 ; i < r . length ; i ++ ) {
912+ stream [ ESCAPE_DECODER ] . next ( r [ i ] ) ;
913+ }
914+ }
905915 } else {
906916 // Nobody's watching anyway
907917 stream . removeListener ( 'data' , onData ) ;
@@ -954,102 +964,130 @@ exports.emitKeypressEvents = emitKeypressEvents;
954964
955965// Regexes used for ansi escape code splitting
956966const metaKeyCodeReAnywhere = / (?: \x1b ) ( [ a - z A - Z 0 - 9 ] ) / ;
957- const metaKeyCodeRe = new RegExp ( '^' + metaKeyCodeReAnywhere . source + '$' ) ;
958967const functionKeyCodeReAnywhere = new RegExp ( '(?:\x1b+)(O|N|\\[|\\[\\[)(?:' + [
959968 '(\\d+)(?:;(\\d+))?([~^$])' ,
960969 '(?:M([@ #!a`])(.)(.))' , // mouse
961970 '(?:1;)?(\\d+)?([a-zA-Z])'
962971] . join ( '|' ) + ')' ) ;
963- const functionKeyCodeRe = new RegExp ( '^' + functionKeyCodeReAnywhere . source ) ;
964- const escapeCodeReAnywhere = new RegExp ( [
965- functionKeyCodeReAnywhere . source , metaKeyCodeReAnywhere . source , / \x1b ./ . source
966- ] . join ( '|' ) ) ;
967-
968- function emitKeys ( stream , s ) {
969- if ( s instanceof Buffer ) {
970- if ( s [ 0 ] > 127 && s [ 1 ] === undefined ) {
971- s [ 0 ] -= 128 ;
972- s = '\x1b' + s . toString ( stream . encoding || 'utf-8' ) ;
973- } else {
974- s = s . toString ( stream . encoding || 'utf-8' ) ;
975- }
976- }
977972
978- var buffer = [ ] ;
979- var match ;
980- while ( match = escapeCodeReAnywhere . exec ( s ) ) {
981- buffer = buffer . concat ( s . slice ( 0 , match . index ) . split ( '' ) ) ;
982- buffer . push ( match [ 0 ] ) ;
983- s = s . slice ( match . index + match [ 0 ] . length ) ;
984- }
985- buffer = buffer . concat ( s . split ( '' ) ) ;
986-
987- buffer . forEach ( function ( s ) {
988- var ch ,
989- key = {
990- sequence : s ,
991- name : undefined ,
992- ctrl : false ,
993- meta : false ,
994- shift : false
995- } ,
996- parts ;
997-
998- if ( s === '\r' ) {
999- // carriage return
1000- key . name = 'return' ;
1001973
1002- } else if ( s === '\n' ) {
1003- // enter, should have been called linefeed
1004- key . name = 'enter' ;
974+ function * emitKeys ( stream ) {
975+ while ( true ) {
976+ var ch = yield ;
977+ var s = ch ;
978+ var escaped = false ;
979+ var key = {
980+ sequence : null ,
981+ name : undefined ,
982+ ctrl : false ,
983+ meta : false ,
984+ shift : false
985+ } ;
986+
987+ if ( ch === '\x1b' ) {
988+ escaped = true ;
989+ s += ( ch = yield ) ;
990+
991+ if ( ch === '\x1b' ) {
992+ s += ( ch = yield ) ;
993+ }
994+ }
1005995
1006- } else if ( s === '\t' ) {
1007- // tab
1008- key . name = 'tab' ;
996+ if ( escaped && ( ch === 'O' || ch === '[' ) ) {
997+ // ansi escape sequence
998+ var code = ch ;
999+ var modifier = 0 ;
10091000
1010- } else if ( s === '\b' || s === '\x7f' ||
1011- s === '\x1b\x7f' || s === '\x1b\b' ) {
1012- // backspace or ctrl+h
1013- key . name = 'backspace' ;
1014- key . meta = ( s . charAt ( 0 ) === '\x1b' ) ;
1001+ if ( ch === 'O' ) {
1002+ // ESC O letter
1003+ // ESC O modifier letter
1004+ s += ( ch = yield ) ;
10151005
1016- } else if ( s === '\x1b' || s === '\x1b\x1b ') {
1017- // escape key
1018- key . name = 'escape' ;
1019- key . meta = ( s . length === 2 ) ;
1006+ if ( ch >= '0' && ch <= '9 ') {
1007+ modifier = ( ch >> 0 ) - 1 ;
1008+ s += ( ch = yield ) ;
1009+ }
10201010
1021- } else if ( s === ' ' || s === '\x1b ' ) {
1022- key . name = 'space' ;
1023- key . meta = ( s . length === 2 ) ;
1011+ code += ch ;
10241012
1025- } else if ( s . length === 1 && s <= '\x1a' ) {
1026- // ctrl+letter
1027- key . name = String . fromCharCode ( s . charCodeAt ( 0 ) + 'a' . charCodeAt ( 0 ) - 1 ) ;
1028- key . ctrl = true ;
1013+ } else if ( ch === '[' ) {
1014+ // ESC [ letter
1015+ // ESC [ modifier letter
1016+ // ESC [ [ modifier letter
1017+ // ESC [ [ num char
1018+ s += ( ch = yield ) ;
10291019
1030- } else if ( s . length === 1 && s >= 'a' && s <= 'z' ) {
1031- // lowercase letter
1032- key . name = s ;
1020+ if ( ch === '[' ) {
1021+ // \x1b[[A
1022+ // ^--- escape codes might have a second bracket
1023+ code += ch ;
1024+ s += ( ch = yield ) ;
1025+ }
10331026
1034- } else if ( s . length === 1 && s >= 'A' && s <= 'Z' ) {
1035- // shift+letter
1036- key . name = s . toLowerCase ( ) ;
1037- key . shift = true ;
1027+ /*
1028+ * Here and later we try to buffer just enough data to get
1029+ * a complete ascii sequence.
1030+ *
1031+ * We have basically two classes of ascii characters to process:
1032+ *
1033+ *
1034+ * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 }
1035+ *
1036+ * This particular example is featuring Ctrl+F12 in xterm.
1037+ *
1038+ * - `;5` part is optional, e.g. it could be `\x1b[24~`
1039+ * - first part can contain one or two digits
1040+ *
1041+ * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/
1042+ *
1043+ *
1044+ * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 }
1045+ *
1046+ * This particular example is featuring Ctrl+Home in xterm.
1047+ *
1048+ * - `1;5` part is optional, e.g. it could be `\x1b[H`
1049+ * - `1;` part is optional, e.g. it could be `\x1b[5H`
1050+ *
1051+ * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/
1052+ *
1053+ */
1054+ const cmdStart = s . length - 1 ;
1055+
1056+ // skip one or two leading digits
1057+ if ( ch >= '0' && ch <= '9' ) {
1058+ s += ( ch = yield ) ;
1059+
1060+ if ( ch >= '0' && ch <= '9' ) {
1061+ s += ( ch = yield ) ;
1062+ }
1063+ }
10381064
1039- } else if ( parts = metaKeyCodeRe . exec ( s ) ) {
1040- // meta+character key
1041- key . name = parts [ 1 ] . toLowerCase ( ) ;
1042- key . meta = true ;
1043- key . shift = / ^ [ A - Z ] $ / . test ( parts [ 1 ] ) ;
1065+ // skip modifier
1066+ if ( ch === ';' ) {
1067+ s += ( ch = yield ) ;
10441068
1045- } else if ( parts = functionKeyCodeRe . exec ( s ) ) {
1046- // ansi escape sequence
1069+ if ( ch >= '0' && ch <= '9' ) {
1070+ s += ( ch = yield ) ;
1071+ }
1072+ }
10471073
1048- // reassemble the key code leaving out leading \x1b's,
1049- // the modifier key bitflag and any meaningless "1;" sequence
1050- var code = ( parts [ 1 ] || '' ) + ( parts [ 2 ] || '' ) +
1051- ( parts [ 4 ] || '' ) + ( parts [ 9 ] || '' ) ,
1052- modifier = ( parts [ 3 ] || parts [ 8 ] || 1 ) - 1 ;
1074+ /*
1075+ * We buffered enough data, now trying to extract code
1076+ * and modifier from it
1077+ */
1078+ const cmd = s . slice ( cmdStart ) ;
1079+ var match ;
1080+
1081+ if ( ( match = cmd . match ( / ^ ( \d \d ? ) ( ; ( \d ) ) ? ( [ ~ ^ $ ] ) $ / ) ) ) {
1082+ code += match [ 1 ] + match [ 4 ] ;
1083+ modifier = ( match [ 3 ] || 1 ) - 1 ;
1084+ } else if ( ( match = cmd . match ( / ^ ( ( \d ; ) ? ( \d ) ) ? ( [ A - Z a - z ] ) $ / ) ) ) {
1085+ code += match [ 4 ] ;
1086+ modifier = ( match [ 3 ] || 1 ) - 1 ;
1087+ } else {
1088+ code += cmd ;
1089+ }
1090+ }
10531091
10541092 // Parse the key modifier
10551093 key . ctrl = ! ! ( modifier & 4 ) ;
@@ -1152,23 +1190,58 @@ function emitKeys(stream, s) {
11521190 /* misc. */
11531191 case '[Z' : key . name = 'tab' ; key . shift = true ; break ;
11541192 default : key . name = 'undefined' ; break ;
1155-
11561193 }
1157- }
11581194
1159- // Don't emit a key if no name was found
1160- if ( key . name === undefined ) {
1161- key = undefined ;
1162- }
1195+ } else if ( ch === '\r' ) {
1196+ // carriage return
1197+ key . name = 'return' ;
1198+
1199+ } else if ( ch === '\n' ) {
1200+ // enter, should have been called linefeed
1201+ key . name = 'enter' ;
1202+
1203+ } else if ( ch === '\t' ) {
1204+ // tab
1205+ key . name = 'tab' ;
11631206
1164- if ( s . length === 1 ) {
1165- ch = s ;
1207+ } else if ( ch === '\b' || ch === '\x7f' ) {
1208+ // backspace or ctrl+h
1209+ key . name = 'backspace' ;
1210+ key . meta = escaped ;
1211+
1212+ } else if ( ch === '\x1b' ) {
1213+ // escape key
1214+ key . name = 'escape' ;
1215+ key . meta = escaped ;
1216+
1217+ } else if ( ch === ' ' ) {
1218+ key . name = 'space' ;
1219+ key . meta = escaped ;
1220+
1221+ } else if ( ! escaped && ch <= '\x1a' ) {
1222+ // ctrl+letter
1223+ key . name = String . fromCharCode ( ch . charCodeAt ( 0 ) + 'a' . charCodeAt ( 0 ) - 1 ) ;
1224+ key . ctrl = true ;
1225+
1226+ } else if ( / ^ [ 0 - 9 A - Z a - z ] $ / . test ( ch ) ) {
1227+ // letter, number, shift+letter
1228+ key . name = ch . toLowerCase ( ) ;
1229+ key . shift = / ^ [ A - Z ] $ / . test ( ch ) ;
1230+ key . meta = escaped ;
11661231 }
11671232
1168- if ( key || ch ) {
1169- stream . emit ( 'keypress' , ch , key ) ;
1233+ key . sequence = s ;
1234+
1235+ if ( key . name !== undefined ) {
1236+ /* Named character or sequence */
1237+ stream . emit ( 'keypress' , escaped ? undefined : s , key ) ;
1238+ } else if ( s . length === 1 ) {
1239+ /* Single unnamed character, e.g. "." */
1240+ stream . emit ( 'keypress' , s ) ;
1241+ } else {
1242+ /* Unrecognized or broken escape sequence, don't emit anything */
11701243 }
1171- } ) ;
1244+ }
11721245}
11731246
11741247
0 commit comments