FTXUI 7.1.0
C++ functional terminal UI.
Loading...
Searching...
No Matches
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 <functional> // for std::function
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
16namespace ftxui {
17
18// NOLINTNEXTLINE
19const 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 {std::string({27, 8}), std::string({27, 127})},
29
30 // See: https://github.com/ArthurSonzogni/FTXUI/issues/626
31 //
32 // Depending on the Cursor Key Mode (DECCKM), the terminal sends different
33 // escape sequences:
34 //
35 // Key Normal Application
36 // ----- -------- -----------
37 // Up ESC [ A ESC O A
38 // Down ESC [ B ESC O B
39 // Right ESC [ C ESC O C
40 // Left ESC [ D ESC O D
41 // Home ESC [ H ESC O H
42 // End ESC [ F ESC O F
43 //
44 {"\x1BOA", "\x1B[A"}, // UP
45 {"\x1BOB", "\x1B[B"}, // DOWN
46 {"\x1BOC", "\x1B[C"}, // RIGHT
47 {"\x1BOD", "\x1B[D"}, // LEFT
48 {"\x1BOH", "\x1B[H"}, // HOME
49 {"\x1BOF", "\x1B[F"}, // END
50
51 // Common Home/End sequences from terminals and multiplexers.
52 {"\x1B[1~", "\x1B[H"}, // HOME
53 {"\x1B[4~", "\x1B[F"}, // END
54
55 // Variations around the FN keys.
56 // Internally, we are using:
57 // vt220, xterm-vt200, xterm-xf86-v44, xterm-new, mgt, screen
58 // See: https://invisible-island.net/xterm/xterm-function-keys.html
59
60 // For linux OS console (CTRL+ALT+FN), who do not belong to any
61 // real standard.
62 // See: https://github.com/ArthurSonzogni/FTXUI/issues/685
63 {"\x1B[[A", "\x1BOP"}, // F1
64 {"\x1B[[B", "\x1BOQ"}, // F2
65 {"\x1B[[C", "\x1BOR"}, // F3
66 {"\x1B[[D", "\x1BOS"}, // F4
67 {"\x1B[[E", "\x1B[15~"}, // F5
68
69 // xterm-r5, xterm-r6, rxvt
70 {"\x1B[11~", "\x1BOP"}, // F1
71 {"\x1B[12~", "\x1BOQ"}, // F2
72 {"\x1B[13~", "\x1BOR"}, // F3
73 {"\x1B[14~", "\x1BOS"}, // F4
74
75 // vt100
76 {"\x1BOt", "\x1B[15~"}, // F5
77 {"\x1BOu", "\x1B[17~"}, // F6
78 {"\x1BOv", "\x1B[18~"}, // F7
79 {"\x1BOl", "\x1B[19~"}, // F8
80 {"\x1BOw", "\x1B[20~"}, // F9
81 {"\x1BOx", "\x1B[21~"}, // F10
82
83 // scoansi
84 {"\x1B[M", "\x1BOP"}, // F1
85 {"\x1B[N", "\x1BOQ"}, // F2
86 {"\x1B[O", "\x1BOR"}, // F3
87 {"\x1B[P", "\x1BOS"}, // F4
88 {"\x1B[Q", "\x1B[15~"}, // F5
89 {"\x1B[R", "\x1B[17~"}, // F6
90 {"\x1B[S", "\x1B[18~"}, // F7
91 {"\x1B[T", "\x1B[19~"}, // F8
92 {"\x1B[U", "\x1B[20~"}, // F9
93 {"\x1B[V", "\x1B[21~"}, // F10
94 {"\x1B[W", "\x1B[23~"}, // F11
95 {"\x1B[X", "\x1B[24~"}, // F12
96};
97
98TerminalInputParser::TerminalInputParser(std::function<void(Event)> out)
99 : out_(std::move(out)) {}
100
102 timeout_ += time;
103 const int timeout_threshold = 50;
104 if (timeout_ < timeout_threshold) {
105 return;
106 }
107 timeout_ = 0;
108 if (!pending_.empty()) {
109 Send(SPECIAL);
110 }
111}
112
114 pending_ += c;
115 timeout_ = 0;
116 position_ = -1;
117 Send(Parse());
118}
119
120unsigned char TerminalInputParser::Current() {
121 return pending_[position_];
122}
123
124bool TerminalInputParser::Eat() {
125 position_++;
126 return position_ < static_cast<int>(pending_.size());
127}
128
129void TerminalInputParser::Send(TerminalInputParser::Output output) {
130 switch (output.type) {
131 case UNCOMPLETED:
132 return;
133
134 case DROP:
135 pending_.clear();
136 return;
137
138 case RESYNC: {
139 // The bytes accumulated so far can't be continued by the one at
140 // |position_|, which starts a new sequence. Emit the truncated prefix and
141 // parse the remaining bytes again.
142 std::string next = pending_.substr(position_);
143 pending_.resize(position_);
144 Send(SPECIAL);
145 pending_ = std::move(next);
146 position_ = -1;
147 Send(Parse());
148 return;
149 }
150
151 case CHARACTER:
152 out_(Event::Character(std::move(pending_)));
153 pending_.clear();
154 return;
155
156 case SPECIAL: {
157 auto it = g_uniformize.find(pending_);
158 if (it != g_uniformize.end()) {
159 pending_ = it->second;
160 }
161 out_(Event::Special(std::move(pending_)));
162 pending_.clear();
163 }
164 return;
165
166 case MOUSE:
167 out_(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
168 pending_.clear();
169 return;
170
171 case CURSOR_POSITION:
172 out_(Event::CursorPosition(std::move(pending_), // NOLINT
173 output.cursor.x, // NOLINT
174 output.cursor.y)); // NOLINT
175 pending_.clear();
176 return;
177
178 case CURSOR_SHAPE:
179 out_(Event::CursorShape(std::move(pending_), output.cursor_shape));
180 pending_.clear();
181 return;
182
183 case TERMINAL_NAME_VERSION:
184 out_(Event::TerminalNameVersion(std::move(pending_),
185 std::move(output.terminal_name),
186 output.terminal_version));
187 pending_.clear();
188 return;
189
190 case TERMINAL_EMULATOR:
191 out_(Event::TerminalEmulator(std::move(pending_),
192 std::move(output.terminal_name),
193 std::move(output.terminal_version_string)));
194 pending_.clear();
195 return;
196
197 case TERMINAL_CAPABILITIES:
198 out_(Event::TerminalCapabilities(
199 std::move(pending_), std::move(output.terminal_capabilities)));
200 pending_.clear();
201 return;
202 }
203 // NOT_REACHED().
204}
205
206TerminalInputParser::Output TerminalInputParser::Parse() {
207 if (!Eat()) {
208 return UNCOMPLETED;
209 }
210
211 if (Current() == '\x1B') {
212 return ParseESC();
213 }
214
215 if (Current() < 32) { // C0 NOLINT
216 return SPECIAL;
217 }
218
219 if (Current() == 127) { // Delete // NOLINT
220 return SPECIAL;
221 }
222
223 return ParseUTF8();
224}
225
226// Code point <-> UTF-8 conversion
227//
228// ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
229// ┃Byte 1 ┃Byte 2 ┃Byte 3 ┃Byte 4 ┃
230// ┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
231// │0xxxxxxx│ │ │ │
232// ├────────┼────────┼────────┼────────┤
233// │110xxxxx│10xxxxxx│ │ │
234// ├────────┼────────┼────────┼────────┤
235// │1110xxxx│10xxxxxx│10xxxxxx│ │
236// ├────────┼────────┼────────┼────────┤
237// │11110xxx│10xxxxxx│10xxxxxx│10xxxxxx│
238// └────────┴────────┴────────┴────────┘
239//
240// Then some sequences are illegal if it exist a shorter representation of the
241// same codepoint.
242TerminalInputParser::Output TerminalInputParser::ParseUTF8() {
243 auto head = Current();
244 unsigned char selector = 0b1000'0000; // NOLINT
245
246 // The non code-point part of the first byte.
247 unsigned char mask = selector;
248
249 // Find the first zero in the first byte.
250 unsigned int first_zero = 8; // NOLINT
251 for (unsigned int i = 0; i < 8; ++i) { // NOLINT
252 mask |= selector;
253 if (!(head & selector)) {
254 first_zero = i;
255 break;
256 }
257 selector >>= 1U;
258 }
259
260 // Accumulate the value of the first byte.
261 auto value = uint32_t(head & ~mask); // NOLINT
262
263 // Invalid UTF8, with more than 5 bytes.
264 const unsigned int max_utf8_bytes = 5;
265 if (first_zero == 1 || first_zero >= max_utf8_bytes) {
266 return DROP;
267 }
268
269 // Multi byte UTF-8.
270 for (unsigned int i = 2; i <= first_zero; ++i) {
271 if (!Eat()) {
272 return UNCOMPLETED;
273 }
274
275 // Invalid continuation byte.
276 head = Current();
277 if ((head & 0b1100'0000) != 0b1000'0000) { // NOLINT
278 return DROP;
279 }
280 value <<= 6; // NOLINT
281 value += head & 0b0011'1111; // NOLINT
282 }
283
284 // Check for overlong UTF8 encoding.
285 int extra_byte = 0;
286 if (value <= 0b000'0000'0111'1111) { // NOLINT
287 extra_byte = 0; // NOLINT
288 } else if (value <= 0b000'0111'1111'1111) { // NOLINT
289 extra_byte = 1; // NOLINT
290 } else if (value <= 0b1111'1111'1111'1111) { // NOLINT
291 extra_byte = 2; // NOLINT
292 } else if (value <= 0b1'0000'1111'1111'1111'1111) { // NOLINT
293 extra_byte = 3; // NOLINT
294 } else { // NOLINT
295 return DROP;
296 }
297
298 if (extra_byte != position_) {
299 return DROP;
300 }
301
302 return CHARACTER;
303}
304
305TerminalInputParser::Output TerminalInputParser::ParseESC() {
306 if (!Eat()) {
307 return UNCOMPLETED;
308 }
309 switch (Current()) {
310 case 'P':
311 return ParseDCS();
312 case '[':
313 return ParseCSI();
314 case ']':
315 return ParseOSC();
316
317 // An ESC is not allowed inside a sequence. This one starts a new one.
318 case '\x1B':
319 return RESYNC;
320
321 // Expecting 2 characters.
322 case ' ':
323 case '#':
324 case '%':
325 case '(':
326 case ')':
327 case '*':
328 case '+':
329 case 'O':
330 case 'N': {
331 if (!Eat()) {
332 return UNCOMPLETED;
333 }
334 if (Current() == '\x1B') {
335 return RESYNC;
336 }
337 return SPECIAL;
338 }
339 // Expecting 1 character:
340 default:
341 return SPECIAL;
342 }
343}
344
345// ESC P ... ESC BACKSLASH
346TerminalInputParser::Output TerminalInputParser::ParseDCS() {
347 // Parse until the string terminator ST.
348 while (true) {
349 if (!Eat()) {
350 return UNCOMPLETED;
351 }
352
353 if (Current() != '\x1B') {
354 continue;
355 }
356
357 if (!Eat()) {
358 return UNCOMPLETED;
359 }
360
361 if (Current() != '\\') {
362 continue;
363 }
364
365 // XTVERSION: ESC P > | name version ST
366 if (pending_.size() >= 5 && pending_[2] == '>' && pending_[3] == '|') {
367 // ESC P > | name (version) ST
368 // 0 1 2 3 4
369 const std::string content = pending_.substr(4, pending_.size() - 6);
370 Output output(TERMINAL_EMULATOR);
371 const size_t space = content.find(' ');
372 const size_t open_paren = content.find('(');
373 if (space != std::string::npos) {
374 output.terminal_name = content.substr(0, space);
375 output.terminal_version_string = content.substr(space + 1);
376 } else if (open_paren != std::string::npos) {
377 output.terminal_name = content.substr(0, open_paren);
378 output.terminal_version_string = content.substr(open_paren + 1);
379 if (!output.terminal_version_string.empty() &&
380 output.terminal_version_string.back() == ')') {
381 output.terminal_version_string.pop_back();
382 }
383 } else {
384 output.terminal_name = content;
385 output.terminal_version_string = "unknown";
386 }
387 return output;
388 }
389
390 if (pending_.size() == 10 && //
391 pending_[2] == '1' && //
392 pending_[3] == '$' && //
393 pending_[4] == 'r' && //
394 true) {
395 Output output(CURSOR_SHAPE);
396 output.cursor_shape = pending_[5] - '0';
397 return output;
398 }
399
400 return SPECIAL;
401 }
402}
403
404TerminalInputParser::Output TerminalInputParser::ParseCSI() {
405 bool altered_less = false;
406 bool altered_greater = false;
407 bool altered_question = false;
408 int argument = 0;
409 std::vector<int> arguments;
410 while (true) {
411 if (!Eat()) {
412 return UNCOMPLETED;
413 }
414
415 if (Current() == '<') {
416 altered_less = true;
417 continue;
418 }
419
420 if (Current() == '>') {
421 altered_greater = true;
422 continue;
423 }
424
425 if (Current() == '?') {
426 altered_question = true;
427 continue;
428 }
429
430 if (Current() >= '0' && Current() <= '9') {
431 argument *= 10; // NOLINT
432 argument += Current() - '0';
433 continue;
434 }
435
436 if (Current() == ';') {
437 arguments.push_back(argument);
438 argument = 0;
439 continue;
440 }
441
442 // CSI is terminated by a character in the range 0x40–0x7E
443 // (ASCII @A–Z[\]^_`a–z{|}~),
444 if (Current() >= '@' && Current() <= '~' &&
445 // Note: I don't remember why we exclude '<'
446 Current() != '<' &&
447 // To handle F1-F4, we exclude '['.
448 Current() != '[') {
449 arguments.push_back(argument);
450 argument = 0; // NOLINT
451
452 switch (Current()) {
453 case 'M':
454 return ParseMouse(altered_less, true, std::move(arguments));
455 case 'm':
456 return ParseMouse(altered_less, false, std::move(arguments));
457 case 'R':
458 return ParseCursorPosition(std::move(arguments));
459 case 'c':
460 return ParseDeviceAttributes(altered_greater, altered_question,
461 std::move(arguments));
462 default:
463 return SPECIAL;
464 }
465 }
466
467 // Invalid ESC in CSI. It starts a new sequence.
468 if (Current() == '\x1B') {
469 return RESYNC;
470 }
471 }
472}
473
474TerminalInputParser::Output TerminalInputParser::ParseOSC() {
475 // Parse until the string terminator ST.
476 while (true) {
477 if (!Eat()) {
478 return UNCOMPLETED;
479 }
480 if (Current() != '\x1B') {
481 continue;
482 }
483 if (!Eat()) {
484 return UNCOMPLETED;
485 }
486 if (Current() != '\\') {
487 continue;
488 }
489 return SPECIAL;
490 }
491}
492
493TerminalInputParser::Output TerminalInputParser::ParseMouse( // NOLINT
494 bool altered,
495 bool pressed,
496 std::vector<int> arguments) {
497 if (arguments.size() != 3) {
498 return SPECIAL;
499 }
500
501 (void)altered;
502
503 Output output(MOUSE);
504 output.mouse.motion = Mouse::Motion(pressed); // NOLINT
505
506 // Bits value Modifier Comment
507 // ---- ----- ------- ---------
508 // 0 1 1 2 button 0 = Left, 1 = Middle, 2 = Right, 3 = Release
509 // 2 4 Shift
510 // 3 8 Meta
511 // 4 16 Control
512 // 5 32 Move
513 // 6 64 Wheel
514
515 // clang-format off
516 const int button = arguments[0] & (1 + 2); // NOLINT
517 const bool is_shift = arguments[0] & 4; // NOLINT
518 const bool is_meta = arguments[0] & 8; // NOLINT
519 const bool is_control = arguments[0] & 16; // NOLINT
520 const bool is_move = arguments[0] & 32; // NOLINT
521 const bool is_wheel = arguments[0] & 64; // NOLINT
522 // clang-format on
523
524 output.mouse.motion = is_move ? Mouse::Moved : Mouse::Motion(pressed);
525 output.mouse.button = is_wheel ? Mouse::Button(Mouse::WheelUp + button) //
526 : Mouse::Button(button);
527 output.mouse.shift = is_shift;
528 output.mouse.meta = is_meta;
529 output.mouse.control = is_control;
530 output.mouse.x = arguments[1]; // NOLINT
531 output.mouse.y = arguments[2]; // NOLINT
532
533 // Motion event.
534 return output;
535}
536
537// NOLINTNEXTLINE
538TerminalInputParser::Output TerminalInputParser::ParseCursorPosition(
539 std::vector<int> arguments) {
540 if (arguments.size() != 2) {
541 return SPECIAL;
542 }
543 Output output(CURSOR_POSITION);
544 output.cursor.y = arguments[0]; // NOLINT
545 output.cursor.x = arguments[1]; // NOLINT
546 return output;
547}
548
549// NOLINTNEXTLINE
550TerminalInputParser::Output TerminalInputParser::ParseDeviceAttributes(
551 bool altered_greater,
552 bool altered_question,
553 std::vector<int> arguments) {
554 if (altered_greater) {
555 // Secondary Device Attributes (DA2)
556 // ESC [ > Pp ; Pv ; Pc c
557 if (arguments.size() >= 3) {
558 // Pp: Terminal type
559 // Pv: Firmware version
560 // Pc: Hardware options
561 Output output(TERMINAL_NAME_VERSION);
562 output.terminal_version = arguments[1];
563 switch (arguments[0]) {
564 case 0:
565 output.terminal_name = "xterm";
566 break;
567 case 1:
568 output.terminal_name = "vt220";
569 break;
570 case 2:
571 output.terminal_name = "vt240";
572 break;
573 case 18:
574 output.terminal_name = "vt330";
575 break;
576 case 19:
577 output.terminal_name = "vt340";
578 break;
579 case 24:
580 output.terminal_name = "vt320";
581 break;
582 case 41:
583 output.terminal_name = "vt420";
584 break;
585 case 61:
586 output.terminal_name = "vt510";
587 break;
588 case 64:
589 output.terminal_name = "vt520";
590 break;
591 case 65:
592 output.terminal_name = "vt525";
593 break;
594 case 84:
595 output.terminal_name = "tmux";
596 break;
597 case 85:
598 output.terminal_name = "urxvt";
599 break;
600 default:
601 output.terminal_name = "unknown";
602 break;
603 }
604 // Special case for xterm which often returns 0;pv;0 or similar
605 // but it's not strictly following DEC VT types.
606 return output;
607 }
608 } else if (altered_question) {
609 // Primary Device Attributes (DA1)
610 // ESC [ ? Pp ; ... c
611 Output output(TERMINAL_CAPABILITIES);
612 output.terminal_capabilities = std::move(arguments);
613 return output;
614 }
615 return SPECIAL;
616}
617
618} // namespace ftxui
TerminalInputParser(std::function< void(Event)> out)
The FTXUI ftxui:: namespace.
Definition animation.hpp:11
const std::map< std::string, std::string > g_uniformize
int value
Definition elements.hpp:195