• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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