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