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 * 152 * So the generic regexp is like /^\d\d?(;\d)?[~^$]$/ 153 * 154 * 155 * 2. `\x1b[1;5H` should be parsed as { code: '[H', modifier: 5 } 156 * 157 * This particular example is featuring Ctrl+Home in xterm. 158 * 159 * - `1;5` part is optional, e.g. it could be `\x1b[H` 160 * - `1;` part is optional, e.g. it could be `\x1b[5H` 161 * 162 * So the generic regexp is like /^((\d;)?\d)?[A-Za-z]$/ 163 * 164 */ 165 const cmdStart = s.length - 1; 166 167 // Skip one or two leading digits 168 if (ch >= '0' && ch <= '9') { 169 s += (ch = yield); 170 171 if (ch >= '0' && ch <= '9') { 172 s += (ch = yield); 173 } 174 } 175 176 // skip modifier 177 if (ch === ';') { 178 s += (ch = yield); 179 180 if (ch >= '0' && ch <= '9') { 181 s += yield; 182 } 183 } 184 185 /* 186 * We buffered enough data, now trying to extract code 187 * and modifier from it 188 */ 189 const cmd = StringPrototypeSlice(s, cmdStart); 190 let match; 191 192 if ((match = RegExpPrototypeExec(/^(\d\d?)(;(\d))?([~^$])$/, cmd))) { 193 code += match[1] + match[4]; 194 modifier = (match[3] || 1) - 1; 195 } else if ( 196 (match = RegExpPrototypeExec(/^((\d;)?(\d))?([A-Za-z])$/, cmd)) 197 ) { 198 code += match[4]; 199 modifier = (match[3] || 1) - 1; 200 } else { 201 code += cmd; 202 } 203 } 204 205 // Parse the key modifier 206 key.ctrl = !!(modifier & 4); 207 key.meta = !!(modifier & 10); 208 key.shift = !!(modifier & 1); 209 key.code = code; 210 211 // Parse the key itself 212 switch (code) { 213 /* xterm/gnome ESC [ letter (with modifier) */ 214 case '[P': key.name = 'f1'; break; 215 case '[Q': key.name = 'f2'; break; 216 case '[R': key.name = 'f3'; break; 217 case '[S': key.name = 'f4'; break; 218 219 /* xterm/gnome ESC O letter (without modifier) */ 220 case 'OP': key.name = 'f1'; break; 221 case 'OQ': key.name = 'f2'; break; 222 case 'OR': key.name = 'f3'; break; 223 case 'OS': key.name = 'f4'; break; 224 225 /* xterm/rxvt ESC [ number ~ */ 226 case '[11~': key.name = 'f1'; break; 227 case '[12~': key.name = 'f2'; break; 228 case '[13~': key.name = 'f3'; break; 229 case '[14~': key.name = 'f4'; break; 230 231 /* from Cygwin and used in libuv */ 232 case '[[A': key.name = 'f1'; break; 233 case '[[B': key.name = 'f2'; break; 234 case '[[C': key.name = 'f3'; break; 235 case '[[D': key.name = 'f4'; break; 236 case '[[E': key.name = 'f5'; break; 237 238 /* common */ 239 case '[15~': key.name = 'f5'; break; 240 case '[17~': key.name = 'f6'; break; 241 case '[18~': key.name = 'f7'; break; 242 case '[19~': key.name = 'f8'; break; 243 case '[20~': key.name = 'f9'; break; 244 case '[21~': key.name = 'f10'; break; 245 case '[23~': key.name = 'f11'; break; 246 case '[24~': key.name = 'f12'; break; 247 248 /* xterm ESC [ letter */ 249 case '[A': key.name = 'up'; break; 250 case '[B': key.name = 'down'; break; 251 case '[C': key.name = 'right'; break; 252 case '[D': key.name = 'left'; break; 253 case '[E': key.name = 'clear'; break; 254 case '[F': key.name = 'end'; break; 255 case '[H': key.name = 'home'; break; 256 257 /* xterm/gnome ESC O letter */ 258 case 'OA': key.name = 'up'; break; 259 case 'OB': key.name = 'down'; break; 260 case 'OC': key.name = 'right'; break; 261 case 'OD': key.name = 'left'; break; 262 case 'OE': key.name = 'clear'; break; 263 case 'OF': key.name = 'end'; break; 264 case 'OH': key.name = 'home'; break; 265 266 /* xterm/rxvt ESC [ number ~ */ 267 case '[1~': key.name = 'home'; break; 268 case '[2~': key.name = 'insert'; break; 269 case '[3~': key.name = 'delete'; break; 270 case '[4~': key.name = 'end'; break; 271 case '[5~': key.name = 'pageup'; break; 272 case '[6~': key.name = 'pagedown'; break; 273 274 /* putty */ 275 case '[[5~': key.name = 'pageup'; break; 276 case '[[6~': key.name = 'pagedown'; break; 277 278 /* rxvt */ 279 case '[7~': key.name = 'home'; break; 280 case '[8~': key.name = 'end'; break; 281 282 /* rxvt keys with modifiers */ 283 case '[a': key.name = 'up'; key.shift = true; break; 284 case '[b': key.name = 'down'; key.shift = true; break; 285 case '[c': key.name = 'right'; key.shift = true; break; 286 case '[d': key.name = 'left'; key.shift = true; break; 287 case '[e': key.name = 'clear'; key.shift = true; break; 288 289 case '[2$': key.name = 'insert'; key.shift = true; break; 290 case '[3$': key.name = 'delete'; key.shift = true; break; 291 case '[5$': key.name = 'pageup'; key.shift = true; break; 292 case '[6$': key.name = 'pagedown'; key.shift = true; break; 293 case '[7$': key.name = 'home'; key.shift = true; break; 294 case '[8$': key.name = 'end'; key.shift = true; break; 295 296 case 'Oa': key.name = 'up'; key.ctrl = true; break; 297 case 'Ob': key.name = 'down'; key.ctrl = true; break; 298 case 'Oc': key.name = 'right'; key.ctrl = true; break; 299 case 'Od': key.name = 'left'; key.ctrl = true; break; 300 case 'Oe': key.name = 'clear'; key.ctrl = true; break; 301 302 case '[2^': key.name = 'insert'; key.ctrl = true; break; 303 case '[3^': key.name = 'delete'; key.ctrl = true; break; 304 case '[5^': key.name = 'pageup'; key.ctrl = true; break; 305 case '[6^': key.name = 'pagedown'; key.ctrl = true; break; 306 case '[7^': key.name = 'home'; key.ctrl = true; break; 307 case '[8^': key.name = 'end'; key.ctrl = true; break; 308 309 /* misc. */ 310 case '[Z': key.name = 'tab'; key.shift = true; break; 311 default: key.name = 'undefined'; break; 312 } 313 } else if (ch === '\r') { 314 // carriage return 315 key.name = 'return'; 316 key.meta = escaped; 317 } else if (ch === '\n') { 318 // Enter, should have been called linefeed 319 key.name = 'enter'; 320 key.meta = escaped; 321 } else if (ch === '\t') { 322 // tab 323 key.name = 'tab'; 324 key.meta = escaped; 325 } else if (ch === '\b' || ch === '\x7f') { 326 // backspace or ctrl+h 327 key.name = 'backspace'; 328 key.meta = escaped; 329 } else if (ch === kEscape) { 330 // escape key 331 key.name = 'escape'; 332 key.meta = escaped; 333 } else if (ch === ' ') { 334 key.name = 'space'; 335 key.meta = escaped; 336 } else if (!escaped && ch <= '\x1a') { 337 // ctrl+letter 338 key.name = StringFromCharCode( 339 StringPrototypeCharCodeAt(ch) + StringPrototypeCharCodeAt('a') - 1, 340 ); 341 key.ctrl = true; 342 } else if (RegExpPrototypeExec(/^[0-9A-Za-z]$/, ch) !== null) { 343 // Letter, number, shift+letter 344 key.name = StringPrototypeToLowerCase(ch); 345 key.shift = RegExpPrototypeExec(/^[A-Z]$/, ch) !== null; 346 key.meta = escaped; 347 } else if (escaped) { 348 // Escape sequence timeout 349 key.name = ch.length ? undefined : 'escape'; 350 key.meta = true; 351 } 352 353 key.sequence = s; 354 355 if (s.length !== 0 && (key.name !== undefined || escaped)) { 356 /* Named character or sequence */ 357 stream.emit('keypress', escaped ? undefined : s, key); 358 } else if (charLengthAt(s, 0) === s.length) { 359 /* Single unnamed character, e.g. "." */ 360 stream.emit('keypress', s, key); 361 } 362 /* Unrecognized or broken escape sequence, don't emit anything */ 363 } 364} 365 366// This runs in O(n log n). 367function commonPrefix(strings) { 368 if (strings.length === 0) { 369 return ''; 370 } 371 if (strings.length === 1) { 372 return strings[0]; 373 } 374 const sorted = ArrayPrototypeSort(ArrayPrototypeSlice(strings)); 375 const min = sorted[0]; 376 const max = sorted[sorted.length - 1]; 377 for (let i = 0; i < min.length; i++) { 378 if (min[i] !== max[i]) { 379 return StringPrototypeSlice(min, 0, i); 380 } 381 } 382 return min; 383} 384 385module.exports = { 386 charLengthAt, 387 charLengthLeft, 388 commonPrefix, 389 emitKeys, 390 kSubstringSearch, 391 CSI, 392}; 393