FTXUI  5.0.0
C++ functional terminal UI.
terminal_input_parser.cpp
Go to the documentation of this file.
1 // Copyright 2020 Arthur Sonzogni. All rights reserved.
2 // Use of this source code is governed by the MIT license that can be found in
3 // the LICENSE file.
5 
6 #include <cstdint> // for uint32_t
7 #include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Button, Mouse::Motion
8 #include <ftxui/component/receiver.hpp> // for SenderImpl, Sender
9 #include <map>
10 #include <memory> // for unique_ptr, allocator
11 #include <utility> // for move
12 #include <vector>
13 #include "ftxui/component/event.hpp" // for Event
14 #include "ftxui/component/task.hpp" // for Task
15 
16 namespace ftxui {
17 
18 // NOLINTNEXTLINE
19 const std::map<std::string, std::string> g_uniformize = {
20  // Microsoft's terminal uses a different new line character for the return
21  // key. This also happens with linux with the `bind` command:
22  // See https://github.com/ArthurSonzogni/FTXUI/issues/337
23  // Here, we uniformize the new line character to `\n`.
24  {"\r", "\n"},
25 
26  // See: https://github.com/ArthurSonzogni/FTXUI/issues/508
27  {std::string({8}), std::string({127})},
28 
29  // See: https://github.com/ArthurSonzogni/FTXUI/issues/626
30  //
31  // Depending on the Cursor Key Mode (DECCKM), the terminal sends different
32  // escape sequences:
33  //
34  // Key Normal Application
35  // ----- -------- -----------
36  // Up ESC [ A ESC O A
37  // Down ESC [ B ESC O B
38  // Right ESC [ C ESC O C
39  // Left ESC [ D ESC O D
40  // Home ESC [ H ESC O H
41  // End ESC [ F ESC O F
42  //
43  {"\x1BOA", "\x1B[A"}, // UP
44  {"\x1BOB", "\x1B[B"}, // DOWN
45  {"\x1BOC", "\x1B[C"}, // RIGHT
46  {"\x1BOD", "\x1B[D"}, // LEFT
47  {"\x1BOH", "\x1B[H"}, // HOME
48  {"\x1BOF", "\x1B[F"}, // END
49 
50  // Variations around the FN keys.
51  // Internally, we are using:
52  // vt220, xterm-vt200, xterm-xf86-v44, xterm-new, mgt, screen
53  // See: https://invisible-island.net/xterm/xterm-function-keys.html
54 
55  // For linux OS console (CTRL+ALT+FN), who do not belong to any
56  // real standard.
57  // See: https://github.com/ArthurSonzogni/FTXUI/issues/685
58  {"\x1B[[A", "\x1BOP"}, // F1
59  {"\x1B[[B", "\x1BOQ"}, // F2
60  {"\x1B[[C", "\x1BOR"}, // F3
61  {"\x1B[[D", "\x1BOS"}, // F4
62  {"\x1B[[E", "\x1B[15~"}, // F5
63 
64  // xterm-r5, xterm-r6, rxvt
65  {"\x1B[11~", "\x1BOP"}, // F1
66  {"\x1B[12~", "\x1BOQ"}, // F2
67  {"\x1B[13~", "\x1BOR"}, // F3
68  {"\x1B[14~", "\x1BOS"}, // F4
69 
70  // vt100
71  {"\x1BOt", "\x1B[15~"}, // F5
72  {"\x1BOu", "\x1B[17~"}, // F6
73  {"\x1BOv", "\x1B[18~"}, // F7
74  {"\x1BOl", "\x1B[19~"}, // F8
75  {"\x1BOw", "\x1B[20~"}, // F9
76  {"\x1BOx", "\x1B[21~"}, // F10
77 
78  // scoansi
79  {"\x1B[M", "\x1BOP"}, // F1
80  {"\x1B[N", "\x1BOQ"}, // F2
81  {"\x1B[O", "\x1BOR"}, // F3
82  {"\x1B[P", "\x1BOS"}, // F4
83  {"\x1B[Q", "\x1B[15~"}, // F5
84  {"\x1B[R", "\x1B[17~"}, // F6
85  {"\x1B[S", "\x1B[18~"}, // F7
86  {"\x1B[T", "\x1B[19~"}, // F8
87  {"\x1B[U", "\x1B[20~"}, // F9
88  {"\x1B[V", "\x1B[21~"}, // F10
89  {"\x1B[W", "\x1B[23~"}, // F11
90  {"\x1B[X", "\x1B[24~"}, // F12
91 };
92 
94  : out_(std::move(out)) {}
95 
97  timeout_ += time;
98  const int timeout_threshold = 50;
99  if (timeout_ < timeout_threshold) {
100  return;
101  }
102  timeout_ = 0;
103  if (!pending_.empty()) {
104  Send(SPECIAL);
105  }
106 }
107 
109  pending_ += c;
110  timeout_ = 0;
111  position_ = -1;
112  Send(Parse());
113 }
114 
115 unsigned char TerminalInputParser::Current() {
116  return pending_[position_];
117 }
118 
119 bool TerminalInputParser::Eat() {
120  position_++;
121  return position_ < static_cast<int>(pending_.size());
122 }
123 
124 void TerminalInputParser::Send(TerminalInputParser::Output output) {
125  switch (output.type) {
126  case UNCOMPLETED:
127  return;
128 
129  case DROP:
130  pending_.clear();
131  return;
132 
133  case CHARACTER:
134  out_->Send(Event::Character(std::move(pending_)));
135  pending_.clear();
136  return;
137 
138  case SPECIAL: {
139  auto it = g_uniformize.find(pending_);
140  if (it != g_uniformize.end()) {
141  pending_ = it->second;
142  }
143  out_->Send(Event::Special(std::move(pending_)));
144  pending_.clear();
145  }
146  return;
147 
148  case MOUSE:
149  out_->Send(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
150  pending_.clear();
151  return;
152 
153  case CURSOR_POSITION:
154  out_->Send(Event::CursorPosition(std::move(pending_), // NOLINT
155  output.cursor.x, // NOLINT
156  output.cursor.y)); // NOLINT
157  pending_.clear();
158  return;
159 
160  case CURSOR_SHAPE:
161  out_->Send(Event::CursorShape(std::move(pending_), output.cursor_shape));
162  pending_.clear();
163  return;
164  }
165  // NOT_REACHED().
166 }
167 
168 TerminalInputParser::Output TerminalInputParser::Parse() {
169  if (!Eat()) {
170  return UNCOMPLETED;
171  }
172 
173  if (Current() == '\x1B') {
174  return ParseESC();
175  }
176 
177  if (Current() < 32) { // C0 NOLINT
178  return SPECIAL;
179  }
180 
181  if (Current() == 127) { // Delete // NOLINT
182  return SPECIAL;
183  }
184 
185  return ParseUTF8();
186 }
187 
188 // Code point <-> UTF-8 conversion
189 //
190 // ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
191 // ┃Byte 1 ┃Byte 2 ┃Byte 3 ┃Byte 4 ┃
192 // ┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
193 // │0xxxxxxx│ │ │ │
194 // ├────────┼────────┼────────┼────────┤
195 // │110xxxxx│10xxxxxx│ │ │
196 // ├────────┼────────┼────────┼────────┤
197 // │1110xxxx│10xxxxxx│10xxxxxx│ │
198 // ├────────┼────────┼────────┼────────┤
199 // │11110xxx│10xxxxxx│10xxxxxx│10xxxxxx│
200 // └────────┴────────┴────────┴────────┘
201 //
202 // Then some sequences are illegal if it exist a shorter representation of the
203 // same codepoint.
204 TerminalInputParser::Output TerminalInputParser::ParseUTF8() {
205  auto head = Current();
206  unsigned char selector = 0b1000'0000; // NOLINT
207 
208  // The non code-point part of the first byte.
209  unsigned char mask = selector;
210 
211  // Find the first zero in the first byte.
212  unsigned int first_zero = 8; // NOLINT
213  for (unsigned int i = 0; i < 8; ++i) { // NOLINT
214  mask |= selector;
215  if (!(head & selector)) {
216  first_zero = i;
217  break;
218  }
219  selector >>= 1U;
220  }
221 
222  // Accumulate the value of the first byte.
223  auto value = uint32_t(head & ~mask); // NOLINT
224 
225  // Invalid UTF8, with more than 5 bytes.
226  const unsigned int max_utf8_bytes = 5;
227  if (first_zero == 1 || first_zero >= max_utf8_bytes) {
228  return DROP;
229  }
230 
231  // Multi byte UTF-8.
232  for (unsigned int i = 2; i <= first_zero; ++i) {
233  if (!Eat()) {
234  return UNCOMPLETED;
235  }
236 
237  // Invalid continuation byte.
238  head = Current();
239  if ((head & 0b1100'0000) != 0b1000'0000) { // NOLINT
240  return DROP;
241  }
242  value <<= 6; // NOLINT
243  value += head & 0b0011'1111; // NOLINT
244  }
245 
246  // Check for overlong UTF8 encoding.
247  int extra_byte = 0;
248  if (value <= 0b000'0000'0111'1111) { // NOLINT
249  extra_byte = 0; // NOLINT
250  } else if (value <= 0b000'0111'1111'1111) { // NOLINT
251  extra_byte = 1; // NOLINT
252  } else if (value <= 0b1111'1111'1111'1111) { // NOLINT
253  extra_byte = 2; // NOLINT
254  } else if (value <= 0b1'0000'1111'1111'1111'1111) { // NOLINT
255  extra_byte = 3; // NOLINT
256  } else { // NOLINT
257  return DROP;
258  }
259 
260  if (extra_byte != position_) {
261  return DROP;
262  }
263 
264  return CHARACTER;
265 }
266 
267 TerminalInputParser::Output TerminalInputParser::ParseESC() {
268  if (!Eat()) {
269  return UNCOMPLETED;
270  }
271  switch (Current()) {
272  case 'P':
273  return ParseDCS();
274  case '[':
275  return ParseCSI();
276  case ']':
277  return ParseOSC();
278 
279  // Expecting 2 characters.
280  case ' ':
281  case '#':
282  case '%':
283  case '(':
284  case ')':
285  case '*':
286  case '+':
287  case 'O':
288  case 'N': {
289  if (!Eat()) {
290  return UNCOMPLETED;
291  }
292  return SPECIAL;
293  }
294  // Expecting 1 character:
295  default:
296  return SPECIAL;
297  }
298 }
299 
300 // ESC P ... ESC BACKSLASH
301 TerminalInputParser::Output TerminalInputParser::ParseDCS() {
302  // Parse until the string terminator ST.
303  while (true) {
304  if (!Eat()) {
305  return UNCOMPLETED;
306  }
307 
308  if (Current() != '\x1B') {
309  continue;
310  }
311 
312  if (!Eat()) {
313  return UNCOMPLETED;
314  }
315 
316  if (Current() != '\\') {
317  continue;
318  }
319 
320  if (pending_.size() == 10 && //
321  pending_[2] == '1' && //
322  pending_[3] == '$' && //
323  pending_[4] == 'r' && //
324  true) {
325  Output output(CURSOR_SHAPE);
326  output.cursor_shape = pending_[5] - '0';
327  return output;
328  }
329 
330  return SPECIAL;
331  }
332 }
333 
334 TerminalInputParser::Output TerminalInputParser::ParseCSI() {
335  bool altered = false;
336  int argument = 0;
337  std::vector<int> arguments;
338  while (true) {
339  if (!Eat()) {
340  return UNCOMPLETED;
341  }
342 
343  if (Current() == '<') {
344  altered = true;
345  continue;
346  }
347 
348  if (Current() >= '0' && Current() <= '9') {
349  argument *= 10; // NOLINT
350  argument += Current() - '0';
351  continue;
352  }
353 
354  if (Current() == ';') {
355  arguments.push_back(argument);
356  argument = 0;
357  continue;
358  }
359 
360  // CSI is terminated by a character in the range 0x40–0x7E
361  // (ASCII @A–Z[\]^_`a–z{|}~),
362  if (Current() >= '@' && Current() <= '~' &&
363  // Note: I don't remember why we exclude '<'
364  Current() != '<' &&
365  // To handle F1-F4, we exclude '['.
366  Current() != '[') {
367  arguments.push_back(argument);
368  argument = 0; // NOLINT
369 
370  switch (Current()) {
371  case 'M':
372  return ParseMouse(altered, true, std::move(arguments));
373  case 'm':
374  return ParseMouse(altered, false, std::move(arguments));
375  case 'R':
376  return ParseCursorPosition(std::move(arguments));
377  default:
378  return SPECIAL;
379  }
380  }
381 
382  // Invalid ESC in CSI.
383  if (Current() == '\x1B') {
384  return SPECIAL;
385  }
386  }
387 }
388 
389 TerminalInputParser::Output TerminalInputParser::ParseOSC() {
390  // Parse until the string terminator ST.
391  while (true) {
392  if (!Eat()) {
393  return UNCOMPLETED;
394  }
395  if (Current() != '\x1B') {
396  continue;
397  }
398  if (!Eat()) {
399  return UNCOMPLETED;
400  }
401  if (Current() != '\\') {
402  continue;
403  }
404  return SPECIAL;
405  }
406 }
407 
408 TerminalInputParser::Output TerminalInputParser::ParseMouse( // NOLINT
409  bool altered,
410  bool pressed,
411  std::vector<int> arguments) {
412  if (arguments.size() != 3) {
413  return SPECIAL;
414  }
415 
416  (void)altered;
417 
418  Output output(MOUSE);
419  output.mouse.motion = Mouse::Motion(pressed); // NOLINT
420 
421  // Bits value Modifer Comment
422  // ---- ----- ------- ---------
423  // 0 1 1 2 button 0 = Left, 1 = Middle, 2 = Right, 3 = Release
424  // 2 4 Shift
425  // 3 8 Meta
426  // 4 16 Control
427  // 5 32 Move
428  // 6 64 Wheel
429 
430  // clang-format off
431  const int button = arguments[0] & (1 + 2); // NOLINT
432  const bool is_shift = arguments[0] & 4; // NOLINT
433  const bool is_meta = arguments[0] & 8; // NOLINT
434  const bool is_control = arguments[0] & 16; // NOLINT
435  const bool is_move = arguments[0] & 32; // NOLINT
436  const bool is_wheel = arguments[0] & 64; // NOLINT
437  // clang-format on
438 
439  output.mouse.motion = is_move ? Mouse::Moved : Mouse::Motion(pressed);
440  output.mouse.button = is_wheel ? Mouse::Button(Mouse::WheelUp + button) //
441  : Mouse::Button(button);
442  output.mouse.shift = is_shift;
443  output.mouse.meta = is_meta;
444  output.mouse.control = is_control;
445  output.mouse.x = arguments[1]; // NOLINT
446  output.mouse.y = arguments[2]; // NOLINT
447 
448  // Motion event.
449  return output;
450 }
451 
452 // NOLINTNEXTLINE
453 TerminalInputParser::Output TerminalInputParser::ParseCursorPosition(
454  std::vector<int> arguments) {
455  if (arguments.size() != 2) {
456  return SPECIAL;
457  }
458  Output output(CURSOR_POSITION);
459  output.cursor.y = arguments[0]; // NOLINT
460  output.cursor.x = arguments[1]; // NOLINT
461  return output;
462 }
463 
464 } // namespace ftxui
TerminalInputParser(Sender< Task > out)
Component Button(ButtonOption options)
Draw a button. Execute a function when clicked.
Definition: button.cpp:174
std::unique_ptr< SenderImpl< T > > Sender
Definition: receiver.hpp:45
const std::map< std::string, std::string > g_uniformize
static Event CursorShape(std::string, int shape)
An event corresponding to a terminal DCS (Device Control String).
Definition: event.cpp:67
static Event Mouse(std::string, Mouse mouse)
An event corresponding to a given typed character.
Definition: event.cpp:57
static Event Character(std::string)
An event corresponding to a given typed character.
Definition: event.cpp:29
static Event CursorPosition(std::string, int x, int y)
Definition: event.cpp:87
static Event Special(std::string)
An custom event whose meaning is defined by the user of the library.
Definition: event.cpp:79