1'use strict'; 2 3const { 4 ArrayPrototypeSlice, 5 ArrayPrototypeSort, 6 RegExpPrototypeTest, 7 StringFromCharCode, 8 StringPrototypeCharCodeAt, 9 StringPrototypeCodePointAt, 10 StringPrototypeMatch, 11 StringPrototypeSlice, 12 StringPrototypeToLowerCase, 13 Symbol, 14} = primordials; 15 16const kUTF16SurrogateThreshold = 0x10000; // 2 ** 16 17const kEscape = '\x1b'; 18const kSubstringSearch = Symbol('kSubstringSearch'); 19 20function CSI(strings, ...args) { 21 let ret = `${kEscape}[`; 22 for (let n = 0; n < strings.length; n++) { 23 ret += strings[n]; 24 if (n < args.length) 25 ret += args[n]; 26 } 27 return ret; 28} 29 30CSI.kEscape = kEscape; 31CSI.kClearToLineBeginning = CSI`1K`; 32CSI.kClearToLineEnd = CSI`0K`; 33CSI.kClearLine = CSI`2K`; 34CSI.kClearScreenDown = CSI`0J`; 35 36// TODO(BridgeAR): Treat combined characters as single character, i.e, 37// 'a\u0301' and '\u0301a' (both have the same visual output). 38// Check Canonical_Combining_Class in 39// http://userguide.icu-project.org/strings/properties 40function charLengthLeft(str, i) { 41 if (i <= 0) 42 return 0; 43 if ((i > 1 && 44 StringPrototypeCodePointAt(str, i - 2) >= kUTF16SurrogateThreshold) || 45 StringPrototypeCodePointAt(str, i - 1) >= kUTF16SurrogateThreshold) { 46 return 2; 47 } 48 return 1; 49} 50 51function charLengthAt(str, i) { 52 if (str.length <= i) { 53 // Pretend to move to the right. This is necessary to autocomplete while 54 // moving to the right. 55 return 1; 56 } 57 return StringPrototypeCodePointAt(str, i) >= kUTF16SurrogateThreshold ? 2 : 1; 58} 59 60/* 61 Some patterns seen in terminal key escape codes, derived from combos seen 62 at http://www.midnight-commander.org/browser/lib/tty/key.c 63 64 ESC letter 65 ESC [ letter 66 ESC [ modifier letter 67 ESC [ 1 ; modifier letter 68 ESC [ num char 69 ESC [ num ; modifier char 70 ESC O letter 71 ESC O modifier letter 72 ESC O 1 ; modifier letter 73 ESC N letter 74 ESC [ [ num ; modifier char 75 ESC [ [ 1 ; modifier letter 76 ESC ESC [ num char 77 ESC ESC O letter 78 79 - char is usually ~ but $ and ^ also happen with rxvt 80 - modifier is 1 + 81 (shift * 1) + 82 (left_alt * 2) + 83 (ctrl * 4) + 84 (right_alt * 8) 85 - two leading ESCs apparently mean the same as one leading ESC 86*/ 87function* emitKeys(stream) { 88 while (true) { 89 let ch = yield; 90 let s = ch; 91 let escaped = false; 92 const key = { 93 sequence: null, 94 name: undefined, 95 ctrl: false, 96 meta: false, 97 shift: false 98 }; 99 100 if (ch === kEscape) { 101 escaped = true; 102 s += (ch = yield); 103 104 if (ch === kEscape) { 105 s += (ch = yield); 106 } 107 } 108 109 if (escaped && (ch === 'O' || ch === '[')) { 110 // ANSI escape sequence 111 let code = ch; 112 let modifier = 0; 113 114 if (ch === 'O') { 115 // ESC O letter 116 // ESC O modifier letter 117 s += (ch = yield); 118 119 if (ch >= '0' && ch <= '9') { 120 modifier = (ch >> 0) - 1; 121 s += (ch = yield); 122 } 123 124 code += ch; 125 } else if (ch === '[') { 126 // ESC [ letter 127 // ESC [ modifier letter 128 // ESC [ [ modifier letter 129 // ESC [ [ num char 130 s += (ch = yield); 131 132 if (ch === '[') { 133 // \x1b[[A 134 // ^--- escape codes might have a second bracket 135 code += ch; 136 s += (ch = yield); 137 } 138 139 /* 140 * Here and later we try to buffer just enough data to get 141 * a complete ascii sequence. 142 * 143 * We have basically two classes of ascii characters to process: 144 * 145 * 146 * 1. `\x1b[24;5~` should be parsed as { code: '[24~', modifier: 5 } 147 * 148 * This particular example is featuring Ctrl+F12 in xterm. 149 * 150 * - `;5` part is optional, e.g. it could be `\x1b[24~` 151 * - first part can contain one or two digits 152 * 153 * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/ 154 * 155 * 156 * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 } 157 * 158 * This particular example is featuring Ctrl+Home in xterm. 159 * 160 * - `1;5` part is optional, e.g. it could be `\x1b[H` 161 * - `1;` part is optional, e.g. it could be `\x1b[5H` 162 * 163 * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/ 164 * 165 */ 166 const cmdStart = s.length - 1; 167 168 // Skip one or two leading digits 169 if (ch >= '0' && ch <= '9') { 170 s += (ch = yield); 171 172 if (ch >= '0' && ch <= '9') { 173 s += (ch = yield); 174 } 175 } 176 177 // skip modifier 178 if (ch === ';') { 179 s += (ch = yield); 180 181 if (ch >= '0' && ch <= '9') { 182 s += yield; 183 } 184 } 185 186 /* 187 * We buffered enough data, now trying to extract code 188 * and modifier from it 189 */ 190 const cmd = StringPrototypeSlice(s, cmdStart); 191 let match; 192 193 if ((match = StringPrototypeMatch(cmd, /^(\d\d?)(;(\d))?([~^$])$/))) { 194 code += match[1] + match[4]; 195 modifier = (match[3] || 1) - 1; 196 } else if ( 197 (match = StringPrototypeMatch(cmd, /^((\d;)?(\d))?([A-Za-z])$/)) 198 ) { 199 code += match[4]; 200 modifier = (match[3] || 1) - 1; 201 } else { 202 code += cmd; 203 } 204 } 205 206 // Parse the key modifier 207 key.ctrl = !!(modifier & 4); 208 key.meta = !!(modifier & 10); 209 key.shift = !!(modifier & 1); 210 key.code = code; 211 212 // Parse the key itself 213 switch (code) { 214 /* xterm/gnome ESC [ letter (with modifier) */ 215 case '[P': key.name = 'f1'; break; 216 case '[Q': key.name = 'f2'; break; 217 case '[R': key.name = 'f3'; break; 218 case '[S': key.name = 'f4'; break; 219 220 /* xterm/gnome ESC O letter (without modifier) */ 221 case 'OP': key.name = 'f1'; break; 222 case 'OQ': key.name = 'f2'; break; 223 case 'OR': key.name = 'f3'; break; 224 case 'OS': key.name = 'f4'; break; 225 226 /* xterm/rxvt ESC [ number ~ */ 227 case '[11~': key.name = 'f1'; break; 228 case '[12~': key.name = 'f2'; break; 229 case '[13~': key.name = 'f3'; break; 230 case '[14~': key.name = 'f4'; break; 231 232 /* from Cygwin and used in libuv */ 233 case '[[A': key.name = 'f1'; break; 234 case '[[B': key.name = 'f2'; break; 235 case '[[C': key.name = 'f3'; break; 236 case '[[D': key.name = 'f4'; break; 237 case '[[E': key.name = 'f5'; break; 238 239 /* common */ 240 case '[15~': key.name = 'f5'; break; 241 case '[17~': key.name = 'f6'; break; 242 case '[18~': key.name = 'f7'; break; 243 case '[19~': key.name = 'f8'; break; 244 case '[20~': key.name = 'f9'; break; 245 case '[21~': key.name = 'f10'; break; 246 case '[23~': key.name = 'f11'; break; 247 case '[24~': key.name = 'f12'; break; 248 249 /* xterm ESC [ letter */ 250 case '[A': key.name = 'up'; break; 251 case '[B': key.name = 'down'; break; 252 case '[C': key.name = 'right'; break; 253 case '[D': key.name = 'left'; break; 254 case '[E': key.name = 'clear'; break; 255 case '[F': key.name = 'end'; break; 256 case '[H': key.name = 'home'; break; 257 258 /* xterm/gnome ESC O letter */ 259 case 'OA': key.name = 'up'; break; 260 case 'OB': key.name = 'down'; break; 261 case 'OC': key.name = 'right'; break; 262 case 'OD': key.name = 'left'; break; 263 case 'OE': key.name = 'clear'; break; 264 case 'OF': key.name = 'end'; break; 265 case 'OH': key.name = 'home'; break; 266 267 /* xterm/rxvt ESC [ number ~ */ 268 case '[1~': key.name = 'home'; break; 269 case '[2~': key.name = 'insert'; break; 270 case '[3~': key.name = 'delete'; break; 271 case '[4~': key.name = 'end'; break; 272 case '[5~': key.name = 'pageup'; break; 273 case '[6~': key.name = 'pagedown'; break; 274 275 /* putty */ 276 case '[[5~': key.name = 'pageup'; break; 277 case '[[6~': key.name = 'pagedown'; break; 278 279 /* rxvt */ 280 case '[7~': key.name = 'home'; break; 281 case '[8~': key.name = 'end'; break; 282 283 /* rxvt keys with modifiers */ 284 case '[a': key.name = 'up'; key.shift = true; break; 285 case '[b': key.name = 'down'; key.shift = true; break; 286 case '[c': key.name = 'right'; key.shift = true; break; 287 case '[d': key.name = 'left'; key.shift = true; break; 288 case '[e': key.name = 'clear'; key.shift = true; break; 289 290 case '[2$': key.name = 'insert'; key.shift = true; break; 291 case '[3$': key.name = 'delete'; key.shift = true; break; 292 case '[5$': key.name = 'pageup'; key.shift = true; break; 293 case '[6$': key.name = 'pagedown'; key.shift = true; break; 294 case '[7$': key.name = 'home'; key.shift = true; break; 295 case '[8$': key.name = 'end'; key.shift = true; break; 296 297 case 'Oa': key.name = 'up'; key.ctrl = true; break; 298 case 'Ob': key.name = 'down'; key.ctrl = true; break; 299 case 'Oc': key.name = 'right'; key.ctrl = true; break; 300 case 'Od': key.name = 'left'; key.ctrl = true; break; 301 case 'Oe': key.name = 'clear'; key.ctrl = true; break; 302 303 case '[2^': key.name = 'insert'; key.ctrl = true; break; 304 case '[3^': key.name = 'delete'; key.ctrl = true; break; 305 case '[5^': key.name = 'pageup'; key.ctrl = true; break; 306 case '[6^': key.name = 'pagedown'; key.ctrl = true; break; 307 case '[7^': key.name = 'home'; key.ctrl = true; break; 308 case '[8^': key.name = 'end'; key.ctrl = true; break; 309 310 /* misc. */ 311 case '[Z': key.name = 'tab'; key.shift = true; break; 312 default: key.name = 'undefined'; break; 313 } 314 } else if (ch === '\r') { 315 // carriage return 316 key.name = 'return'; 317 key.meta = escaped; 318 } else if (ch === '\n') { 319 // Enter, should have been called linefeed 320 key.name = 'enter'; 321 key.meta = escaped; 322 } else if (ch === '\t') { 323 // tab 324 key.name = 'tab'; 325 key.meta = escaped; 326 } else if (ch === '\b' || ch === '\x7f') { 327 // backspace or ctrl+h 328 key.name = 'backspace'; 329 key.meta = escaped; 330 } else if (ch === kEscape) { 331 // escape key 332 key.name = 'escape'; 333 key.meta = escaped; 334 } else if (ch === ' ') { 335 key.name = 'space'; 336 key.meta = escaped; 337 } else if (!escaped && ch <= '\x1a') { 338 // ctrl+letter 339 key.name = StringFromCharCode( 340 StringPrototypeCharCodeAt(ch) + StringPrototypeCharCodeAt('a') - 1 341 ); 342 key.ctrl = true; 343 } else if (RegExpPrototypeTest(/^[0-9A-Za-z]$/, ch)) { 344 // Letter, number, shift+letter 345 key.name = StringPrototypeToLowerCase(ch); 346 key.shift = RegExpPrototypeTest(/^[A-Z]$/, ch); 347 key.meta = escaped; 348 } else if (escaped) { 349 // Escape sequence timeout 350 key.name = ch.length ? undefined : 'escape'; 351 key.meta = true; 352 } 353 354 key.sequence = s; 355 356 if (s.length !== 0 && (key.name !== undefined || escaped)) { 357 /* Named character or sequence */ 358 stream.emit('keypress', escaped ? undefined : s, key); 359 } else if (charLengthAt(s, 0) === s.length) { 360 /* Single unnamed character, e.g. "." */ 361 stream.emit('keypress', s, key); 362 } 363 /* Unrecognized or broken escape sequence, don't emit anything */ 364 } 365} 366 367// This runs in O(n log n). 368function commonPrefix(strings) { 369 if (strings.length === 1) { 370 return strings[0]; 371 } 372 const sorted = ArrayPrototypeSort(ArrayPrototypeSlice(strings)); 373 const min = sorted[0]; 374 const max = sorted[sorted.length - 1]; 375 for (let i = 0; i < min.length; i++) { 376 if (min[i] !== max[i]) { 377 return StringPrototypeSlice(min, 0, i); 378 } 379 } 380 return min; 381} 382 383module.exports = { 384 charLengthAt, 385 charLengthLeft, 386 commonPrefix, 387 emitKeys, 388 kSubstringSearch, 389 CSI 390}; 391