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