• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2const common = require('../common');
3const PassThrough = require('stream').PassThrough;
4const assert = require('assert');
5const Interface = require('readline').Interface;
6
7class FakeInput extends PassThrough {}
8
9function extend(k) {
10  return Object.assign({ ctrl: false, meta: false, shift: false }, k);
11}
12
13
14const fi = new FakeInput();
15const fo = new FakeInput();
16new Interface({ input: fi, output: fo, terminal: true });
17
18let keys = [];
19fi.on('keypress', (s, k) => {
20  keys.push(k);
21});
22
23
24function addTest(sequences, expectedKeys) {
25  if (!Array.isArray(sequences)) {
26    sequences = [ sequences ];
27  }
28
29  if (!Array.isArray(expectedKeys)) {
30    expectedKeys = [ expectedKeys ];
31  }
32
33  expectedKeys = expectedKeys.map(extend);
34
35  keys = [];
36
37  sequences.forEach((sequence) => {
38    fi.write(sequence);
39  });
40  assert.deepStrictEqual(keys, expectedKeys);
41}
42
43// Simulate key interval test cases
44// Returns a function that takes `next` test case and returns a thunk
45// that can be called to run tests in sequence
46// e.g.
47// addKeyIntervalTest(..)
48//   (addKeyIntervalTest(..)
49//      (addKeyIntervalTest(..)(noop)))()
50// where noop is a terminal function(() => {}).
51
52const addKeyIntervalTest = (sequences, expectedKeys, interval = 550,
53                            assertDelay = 550) => {
54  const fn = common.mustCall((next) => () => {
55
56    if (!Array.isArray(sequences)) {
57      sequences = [ sequences ];
58    }
59
60    if (!Array.isArray(expectedKeys)) {
61      expectedKeys = [ expectedKeys ];
62    }
63
64    expectedKeys = expectedKeys.map(extend);
65
66    const keys = [];
67    fi.on('keypress', (s, k) => keys.push(k));
68
69    const emitKeys = ([head, ...tail]) => {
70      if (head) {
71        fi.write(head);
72        setTimeout(() => emitKeys(tail), interval);
73      } else {
74        setTimeout(() => {
75          next();
76          assert.deepStrictEqual(keys, expectedKeys);
77        }, assertDelay);
78      }
79    };
80    emitKeys(sequences);
81  });
82  return fn;
83};
84
85// Regular alphanumerics
86addTest('io.JS', [
87  { name: 'i', sequence: 'i' },
88  { name: 'o', sequence: 'o' },
89  { name: undefined, sequence: '.' },
90  { name: 'j', sequence: 'J', shift: true },
91  { name: 's', sequence: 'S', shift: true },
92]);
93
94// Named characters
95addTest('\n\r\t\x1b\n\x1b\r\x1b\t', [
96  { name: 'enter', sequence: '\n' },
97  { name: 'return', sequence: '\r' },
98  { name: 'tab', sequence: '\t' },
99  { name: 'enter', sequence: '\x1b\n', meta: true },
100  { name: 'return', sequence: '\x1b\r', meta: true },
101  { name: 'tab', sequence: '\x1b\t', meta: true },
102]);
103
104// Space and backspace
105addTest('\b\x7f\x1b\b\x1b\x7f\x1b\x1b  \x1b ', [
106  { name: 'backspace', sequence: '\b' },
107  { name: 'backspace', sequence: '\x7f' },
108  { name: 'backspace', sequence: '\x1b\b', meta: true },
109  { name: 'backspace', sequence: '\x1b\x7f', meta: true },
110  { name: 'space', sequence: '\x1b\x1b ', meta: true },
111  { name: 'space', sequence: ' ' },
112  { name: 'space', sequence: '\x1b ', meta: true },
113]);
114
115// Escape key
116addTest('\x1b\x1b\x1b', [
117  { name: 'escape', sequence: '\x1b\x1b\x1b', meta: true },
118]);
119
120// Escape sequence
121addTest('\x1b]', [{ name: undefined, sequence: '\x1B]', meta: true }]);
122
123// Control keys
124addTest('\x01\x0b\x10', [
125  { name: 'a', sequence: '\x01', ctrl: true },
126  { name: 'k', sequence: '\x0b', ctrl: true },
127  { name: 'p', sequence: '\x10', ctrl: true },
128]);
129
130// Alt keys
131addTest('a\x1baA\x1bA', [
132  { name: 'a', sequence: 'a' },
133  { name: 'a', sequence: '\x1ba', meta: true },
134  { name: 'a', sequence: 'A', shift: true },
135  { name: 'a', sequence: '\x1bA', meta: true, shift: true },
136]);
137
138// xterm/gnome ESC [ letter (with modifiers)
139/* eslint-disable max-len */
140addTest('\x1b[2P\x1b[3P\x1b[4P\x1b[5P\x1b[6P\x1b[7P\x1b[8P\x1b[3Q\x1b[8Q\x1b[3R\x1b[8R\x1b[3S\x1b[8S', [
141  { name: 'f1', sequence: '\x1b[2P', code: '[P', shift: true, meta: false, ctrl: false },
142  { name: 'f1', sequence: '\x1b[3P', code: '[P', shift: false, meta: true, ctrl: false },
143  { name: 'f1', sequence: '\x1b[4P', code: '[P', shift: true, meta: true, ctrl: false },
144  { name: 'f1', sequence: '\x1b[5P', code: '[P', shift: false, meta: false, ctrl: true },
145  { name: 'f1', sequence: '\x1b[6P', code: '[P', shift: true, meta: false, ctrl: true },
146  { name: 'f1', sequence: '\x1b[7P', code: '[P', shift: false, meta: true, ctrl: true },
147  { name: 'f1', sequence: '\x1b[8P', code: '[P', shift: true, meta: true, ctrl: true },
148  { name: 'f2', sequence: '\x1b[3Q', code: '[Q', meta: true },
149  { name: 'f2', sequence: '\x1b[8Q', code: '[Q', shift: true, meta: true, ctrl: true },
150  { name: 'f3', sequence: '\x1b[3R', code: '[R', meta: true },
151  { name: 'f3', sequence: '\x1b[8R', code: '[R', shift: true, meta: true, ctrl: true },
152  { name: 'f4', sequence: '\x1b[3S', code: '[S', meta: true },
153  { name: 'f4', sequence: '\x1b[8S', code: '[S', shift: true, meta: true, ctrl: true },
154]);
155/* eslint-enable max-len */
156
157// xterm/gnome ESC O letter
158addTest('\x1bOP\x1bOQ\x1bOR\x1bOS', [
159  { name: 'f1', sequence: '\x1bOP', code: 'OP' },
160  { name: 'f2', sequence: '\x1bOQ', code: 'OQ' },
161  { name: 'f3', sequence: '\x1bOR', code: 'OR' },
162  { name: 'f4', sequence: '\x1bOS', code: 'OS' },
163]);
164
165// xterm/rxvt ESC [ number ~ */
166addTest('\x1b[11~\x1b[12~\x1b[13~\x1b[14~', [
167  { name: 'f1', sequence: '\x1b[11~', code: '[11~' },
168  { name: 'f2', sequence: '\x1b[12~', code: '[12~' },
169  { name: 'f3', sequence: '\x1b[13~', code: '[13~' },
170  { name: 'f4', sequence: '\x1b[14~', code: '[14~' },
171]);
172
173// From Cygwin and used in libuv
174addTest('\x1b[[A\x1b[[B\x1b[[C\x1b[[D\x1b[[E', [
175  { name: 'f1', sequence: '\x1b[[A', code: '[[A' },
176  { name: 'f2', sequence: '\x1b[[B', code: '[[B' },
177  { name: 'f3', sequence: '\x1b[[C', code: '[[C' },
178  { name: 'f4', sequence: '\x1b[[D', code: '[[D' },
179  { name: 'f5', sequence: '\x1b[[E', code: '[[E' },
180]);
181
182// Common
183addTest('\x1b[15~\x1b[17~\x1b[18~\x1b[19~\x1b[20~\x1b[21~\x1b[23~\x1b[24~', [
184  { name: 'f5', sequence: '\x1b[15~', code: '[15~' },
185  { name: 'f6', sequence: '\x1b[17~', code: '[17~' },
186  { name: 'f7', sequence: '\x1b[18~', code: '[18~' },
187  { name: 'f8', sequence: '\x1b[19~', code: '[19~' },
188  { name: 'f9', sequence: '\x1b[20~', code: '[20~' },
189  { name: 'f10', sequence: '\x1b[21~', code: '[21~' },
190  { name: 'f11', sequence: '\x1b[23~', code: '[23~' },
191  { name: 'f12', sequence: '\x1b[24~', code: '[24~' },
192]);
193
194// xterm ESC [ letter
195addTest('\x1b[A\x1b[B\x1b[C\x1b[D\x1b[E\x1b[F\x1b[H', [
196  { name: 'up', sequence: '\x1b[A', code: '[A' },
197  { name: 'down', sequence: '\x1b[B', code: '[B' },
198  { name: 'right', sequence: '\x1b[C', code: '[C' },
199  { name: 'left', sequence: '\x1b[D', code: '[D' },
200  { name: 'clear', sequence: '\x1b[E', code: '[E' },
201  { name: 'end', sequence: '\x1b[F', code: '[F' },
202  { name: 'home', sequence: '\x1b[H', code: '[H' },
203]);
204
205// xterm/gnome ESC O letter
206addTest('\x1bOA\x1bOB\x1bOC\x1bOD\x1bOE\x1bOF\x1bOH', [
207  { name: 'up', sequence: '\x1bOA', code: 'OA' },
208  { name: 'down', sequence: '\x1bOB', code: 'OB' },
209  { name: 'right', sequence: '\x1bOC', code: 'OC' },
210  { name: 'left', sequence: '\x1bOD', code: 'OD' },
211  { name: 'clear', sequence: '\x1bOE', code: 'OE' },
212  { name: 'end', sequence: '\x1bOF', code: 'OF' },
213  { name: 'home', sequence: '\x1bOH', code: 'OH' },
214]);
215
216// Old xterm shift-arrows
217addTest('\x1bO2A\x1bO2B', [
218  { name: 'up', sequence: '\x1bO2A', code: 'OA', shift: true },
219  { name: 'down', sequence: '\x1bO2B', code: 'OB', shift: true },
220]);
221
222// xterm/rxvt ESC [ number ~
223addTest('\x1b[1~\x1b[2~\x1b[3~\x1b[4~\x1b[5~\x1b[6~', [
224  { name: 'home', sequence: '\x1b[1~', code: '[1~' },
225  { name: 'insert', sequence: '\x1b[2~', code: '[2~' },
226  { name: 'delete', sequence: '\x1b[3~', code: '[3~' },
227  { name: 'end', sequence: '\x1b[4~', code: '[4~' },
228  { name: 'pageup', sequence: '\x1b[5~', code: '[5~' },
229  { name: 'pagedown', sequence: '\x1b[6~', code: '[6~' },
230]);
231
232// putty
233addTest('\x1b[[5~\x1b[[6~', [
234  { name: 'pageup', sequence: '\x1b[[5~', code: '[[5~' },
235  { name: 'pagedown', sequence: '\x1b[[6~', code: '[[6~' },
236]);
237
238// rxvt
239addTest('\x1b[7~\x1b[8~', [
240  { name: 'home', sequence: '\x1b[7~', code: '[7~' },
241  { name: 'end', sequence: '\x1b[8~', code: '[8~' },
242]);
243
244// gnome terminal
245addTest('\x1b[A\x1b[B\x1b[2A\x1b[2B', [
246  { name: 'up', sequence: '\x1b[A', code: '[A' },
247  { name: 'down', sequence: '\x1b[B', code: '[B' },
248  { name: 'up', sequence: '\x1b[2A', code: '[A', shift: true },
249  { name: 'down', sequence: '\x1b[2B', code: '[B', shift: true },
250]);
251
252// `rxvt` keys with modifiers.
253// eslint-disable-next-line max-len
254addTest('\x1b[20~\x1b[2$\x1b[2^\x1b[3$\x1b[3^\x1b[5$\x1b[5^\x1b[6$\x1b[6^\x1b[7$\x1b[7^\x1b[8$\x1b[8^', [
255  { name: 'f9', sequence: '\x1b[20~', code: '[20~' },
256  { name: 'insert', sequence: '\x1b[2$', code: '[2$', shift: true },
257  { name: 'insert', sequence: '\x1b[2^', code: '[2^', ctrl: true },
258  { name: 'delete', sequence: '\x1b[3$', code: '[3$', shift: true },
259  { name: 'delete', sequence: '\x1b[3^', code: '[3^', ctrl: true },
260  { name: 'pageup', sequence: '\x1b[5$', code: '[5$', shift: true },
261  { name: 'pageup', sequence: '\x1b[5^', code: '[5^', ctrl: true },
262  { name: 'pagedown', sequence: '\x1b[6$', code: '[6$', shift: true },
263  { name: 'pagedown', sequence: '\x1b[6^', code: '[6^', ctrl: true },
264  { name: 'home', sequence: '\x1b[7$', code: '[7$', shift: true },
265  { name: 'home', sequence: '\x1b[7^', code: '[7^', ctrl: true },
266  { name: 'end', sequence: '\x1b[8$', code: '[8$', shift: true },
267  { name: 'end', sequence: '\x1b[8^', code: '[8^', ctrl: true },
268]);
269
270// Misc
271addTest('\x1b[Z', [
272  { name: 'tab', sequence: '\x1b[Z', code: '[Z', shift: true },
273]);
274
275// xterm + modifiers
276addTest('\x1b[20;5~\x1b[6;5^', [
277  { name: 'f9', sequence: '\x1b[20;5~', code: '[20~', ctrl: true },
278  { name: 'pagedown', sequence: '\x1b[6;5^', code: '[6^', ctrl: true },
279]);
280
281addTest('\x1b[H\x1b[5H\x1b[1;5H', [
282  { name: 'home', sequence: '\x1b[H', code: '[H' },
283  { name: 'home', sequence: '\x1b[5H', code: '[H', ctrl: true },
284  { name: 'home', sequence: '\x1b[1;5H', code: '[H', ctrl: true },
285]);
286
287// Escape sequences broken into multiple data chunks
288addTest('\x1b[D\x1b[C\x1b[D\x1b[C'.split(''), [
289  { name: 'left', sequence: '\x1b[D', code: '[D' },
290  { name: 'right', sequence: '\x1b[C', code: '[C' },
291  { name: 'left', sequence: '\x1b[D', code: '[D' },
292  { name: 'right', sequence: '\x1b[C', code: '[C' },
293]);
294
295// Escape sequences mixed with regular ones
296addTest('\x1b[DD\x1b[2DD\x1b[2^D', [
297  { name: 'left', sequence: '\x1b[D', code: '[D' },
298  { name: 'd', sequence: 'D', shift: true },
299  { name: 'left', sequence: '\x1b[2D', code: '[D', shift: true },
300  { name: 'd', sequence: 'D', shift: true },
301  { name: 'insert', sequence: '\x1b[2^', code: '[2^', ctrl: true },
302  { name: 'd', sequence: 'D', shift: true },
303]);
304
305// Color sequences
306addTest('\x1b[31ma\x1b[39ma', [
307  { name: 'undefined', sequence: '\x1b[31m', code: '[31m' },
308  { name: 'a', sequence: 'a' },
309  { name: 'undefined', sequence: '\x1b[39m', code: '[39m' },
310  { name: 'a', sequence: 'a' },
311]);
312
313// `rxvt` keys with modifiers.
314addTest('\x1b[a\x1b[b\x1b[c\x1b[d\x1b[e', [
315  { name: 'up', sequence: '\x1b[a', code: '[a', shift: true },
316  { name: 'down', sequence: '\x1b[b', code: '[b', shift: true },
317  { name: 'right', sequence: '\x1b[c', code: '[c', shift: true },
318  { name: 'left', sequence: '\x1b[d', code: '[d', shift: true },
319  { name: 'clear', sequence: '\x1b[e', code: '[e', shift: true },
320]);
321
322addTest('\x1bOa\x1bOb\x1bOc\x1bOd\x1bOe', [
323  { name: 'up', sequence: '\x1bOa', code: 'Oa', ctrl: true },
324  { name: 'down', sequence: '\x1bOb', code: 'Ob', ctrl: true },
325  { name: 'right', sequence: '\x1bOc', code: 'Oc', ctrl: true },
326  { name: 'left', sequence: '\x1bOd', code: 'Od', ctrl: true },
327  { name: 'clear', sequence: '\x1bOe', code: 'Oe', ctrl: true },
328]);
329
330// Reduce array of addKeyIntervalTest(..) right to left
331// with () => {} as initial function.
332const runKeyIntervalTests = [
333  // Escape character
334  addKeyIntervalTest('\x1b', [
335    { name: 'escape', sequence: '\x1b', meta: true },
336  ]),
337  // Chain of escape characters.
338  addKeyIntervalTest('\x1b\x1b\x1b\x1b'.split(''), [
339    { name: 'escape', sequence: '\x1b', meta: true },
340    { name: 'escape', sequence: '\x1b', meta: true },
341    { name: 'escape', sequence: '\x1b', meta: true },
342    { name: 'escape', sequence: '\x1b', meta: true },
343  ]),
344].reverse().reduce((acc, fn) => fn(acc), () => {});
345
346// Run key interval tests one after another.
347runKeyIntervalTests();
348