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