• 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         *  - 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