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