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