FTXUI 6.1.9
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
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 CHARACTER:
138 out_(Event::Character(std::move(pending_)));
139 pending_.clear();
140 return;
141
142 case SPECIAL: {
143 auto it = g_uniformize.find(pending_);
144 if (it != g_uniformize.end()) {
145 pending_ = it->second;
146 }
147 out_(Event::Special(std::move(pending_)));
148 pending_.clear();
149 }
150 return;
151
152 case MOUSE:
153 out_(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
154 pending_.clear();
155 return;
156
157 case CURSOR_POSITION:
158 out_(Event::CursorPosition(std::move(pending_), // NOLINT
159 output.cursor.x, // NOLINT
160 output.cursor.y)); // NOLINT
161 pending_.clear();
162 return;
163
164 case CURSOR_SHAPE:
165 out_(Event::CursorShape(std::move(pending_), output.cursor_shape));
166 pending_.clear();
167 return;
168
169 case TERMINAL_NAME_VERSION:
170 out_(Event::TerminalNameVersion(std::move(pending_),
171 std::move(output.terminal_name),
172 output.terminal_version));
173 pending_.clear();
174 return;
175
176 case TERMINAL_EMULATOR:
177 out_(Event::TerminalEmulator(std::move(pending_),
178 std::move(output.terminal_name),
179 std::move(output.terminal_version_string)));
180 pending_.clear();
181 return;
182
183 case TERMINAL_CAPABILITIES:
185 std::move(pending_), std::move(output.terminal_capabilities)));
186 pending_.clear();
187 return;
188 }
189 // NOT_REACHED().
190}
191
192TerminalInputParser::Output TerminalInputParser::Parse() {
193 if (!Eat()) {
194 return UNCOMPLETED;
195 }
196
197 if (Current() == '\x1B') {
198 return ParseESC();
199 }
200
201 if (Current() < 32) { // C0 NOLINT
202 return SPECIAL;
203 }
204
205 if (Current() == 127) { // Delete // NOLINT
206 return SPECIAL;
207 }
208
209 return ParseUTF8();
210}
211
212// Code point <-> UTF-8 conversion
213//
214// ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓
215// ┃Byte 1 ┃Byte 2 ┃Byte 3 ┃Byte 4 ┃
216// ┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩
217// │0xxxxxxx│ │ │ │
218// ├────────┼────────┼────────┼────────┤
219// │110xxxxx│10xxxxxx│ │ │
220// ├────────┼────────┼────────┼────────┤
221// │1110xxxx│10xxxxxx│10xxxxxx│ │
222// ├────────┼────────┼────────┼────────┤
223// │11110xxx│10xxxxxx│10xxxxxx│10xxxxxx│
224// └────────┴────────┴────────┴────────┘
225//
226// Then some sequences are illegal if it exist a shorter representation of the
227// same codepoint.
228TerminalInputParser::Output TerminalInputParser::ParseUTF8() {
229 auto head = Current();
230 unsigned char selector = 0b1000'0000; // NOLINT
231
232 // The non code-point part of the first byte.
233 unsigned char mask = selector;
234
235 // Find the first zero in the first byte.
236 unsigned int first_zero = 8; // NOLINT
237 for (unsigned int i = 0; i < 8; ++i) { // NOLINT
238 mask |= selector;
239 if (!(head & selector)) {
240 first_zero = i;
241 break;
242 }
243 selector >>= 1U;
244 }
245
246 // Accumulate the value of the first byte.
247 auto value = uint32_t(head & ~mask); // NOLINT
248
249 // Invalid UTF8, with more than 5 bytes.
250 const unsigned int max_utf8_bytes = 5;
251 if (first_zero == 1 || first_zero >= max_utf8_bytes) {
252 return DROP;
253 }
254
255 // Multi byte UTF-8.
256 for (unsigned int i = 2; i <= first_zero; ++i) {
257 if (!Eat()) {
258 return UNCOMPLETED;
259 }
260
261 // Invalid continuation byte.
262 head = Current();
263 if ((head & 0b1100'0000) != 0b1000'0000) { // NOLINT
264 return DROP;
265 }
266 value <<= 6; // NOLINT
267 value += head & 0b0011'1111; // NOLINT
268 }
269
270 // Check for overlong UTF8 encoding.
271 int extra_byte = 0;
272 if (value <= 0b000'0000'0111'1111) { // NOLINT
273 extra_byte = 0; // NOLINT
274 } else if (value <= 0b000'0111'1111'1111) { // NOLINT
275 extra_byte = 1; // NOLINT
276 } else if (value <= 0b1111'1111'1111'1111) { // NOLINT
277 extra_byte = 2; // NOLINT
278 } else if (value <= 0b1'0000'1111'1111'1111'1111) { // NOLINT
279 extra_byte = 3; // NOLINT
280 } else { // NOLINT
281 return DROP;
282 }
283
284 if (extra_byte != position_) {
285 return DROP;
286 }
287
288 return CHARACTER;
289}
290
291TerminalInputParser::Output TerminalInputParser::ParseESC() {
292 if (!Eat()) {
293 return UNCOMPLETED;
294 }
295 switch (Current()) {
296 case 'P':
297 return ParseDCS();
298 case '[':
299 return ParseCSI();
300 case ']':
301 return ParseOSC();
302
303 // Expecting 2 characters.
304 case ' ':
305 case '#':
306 case '%':
307 case '(':
308 case ')':
309 case '*':
310 case '+':
311 case 'O':
312 case 'N': {
313 if (!Eat()) {
314 return UNCOMPLETED;
315 }
316 return SPECIAL;
317 }
318 // Expecting 1 character:
319 default:
320 return SPECIAL;
321 }
322}
323
324// ESC P ... ESC BACKSLASH
325TerminalInputParser::Output TerminalInputParser::ParseDCS() {
326 // Parse until the string terminator ST.
327 while (true) {
328 if (!Eat()) {
329 return UNCOMPLETED;
330 }
331
332 if (Current() != '\x1B') {
333 continue;
334 }
335
336 if (!Eat()) {
337 return UNCOMPLETED;
338 }
339
340 if (Current() != '\\') {
341 continue;
342 }
343
344 // XTVERSION: ESC P > | name version ST
345 if (pending_.size() >= 5 && pending_[2] == '>' && pending_[3] == '|') {
346 // ESC P > | name (version) ST
347 // 0 1 2 3 4
348 const std::string content = pending_.substr(4, pending_.size() - 6);
349 Output output(TERMINAL_EMULATOR);
350 const size_t space = content.find(' ');
351 const size_t open_paren = content.find('(');
352 if (space != std::string::npos) {
353 output.terminal_name = content.substr(0, space);
354 output.terminal_version_string = content.substr(space + 1);
355 } else if (open_paren != std::string::npos) {
356 output.terminal_name = content.substr(0, open_paren);
357 output.terminal_version_string = content.substr(open_paren + 1);
358 if (!output.terminal_version_string.empty() &&
359 output.terminal_version_string.back() == ')') {
360 output.terminal_version_string.pop_back();
361 }
362 } else {
363 output.terminal_name = content;
364 output.terminal_version_string = "unknown";
365 }
366 return output;
367 }
368
369 if (pending_.size() == 10 && //
370 pending_[2] == '1' && //
371 pending_[3] == '$' && //
372 pending_[4] == 'r' && //
373 true) {
374 Output output(CURSOR_SHAPE);
375 output.cursor_shape = pending_[5] - '0';
376 return output;
377 }
378
379 return SPECIAL;
380 }
381}
382
383TerminalInputParser::Output TerminalInputParser::ParseCSI() {
384 bool altered_less = false;
385 bool altered_greater = false;
386 bool altered_question = false;
387 int argument = 0;
388 std::vector<int> arguments;
389 while (true) {
390 if (!Eat()) {
391 return UNCOMPLETED;
392 }
393
394 if (Current() == '<') {
395 altered_less = true;
396 continue;
397 }
398
399 if (Current() == '>') {
400 altered_greater = true;
401 continue;
402 }
403
404 if (Current() == '?') {
405 altered_question = true;
406 continue;
407 }
408
409 if (Current() >= '0' && Current() <= '9') {
410 argument *= 10; // NOLINT
411 argument += Current() - '0';
412 continue;
413 }
414
415 if (Current() == ';') {
416 arguments.push_back(argument);
417 argument = 0;
418 continue;
419 }
420
421 // CSI is terminated by a character in the range 0x40–0x7E
422 // (ASCII @A–Z[\]^_`a–z{|}~),
423 if (Current() >= '@' && Current() <= '~' &&
424 // Note: I don't remember why we exclude '<'
425 Current() != '<' &&
426 // To handle F1-F4, we exclude '['.
427 Current() != '[') {
428 arguments.push_back(argument);
429 argument = 0; // NOLINT
430
431 switch (Current()) {
432 case 'M':
433 return ParseMouse(altered_less, true, std::move(arguments));
434 case 'm':
435 return ParseMouse(altered_less, false, std::move(arguments));
436 case 'R':
437 return ParseCursorPosition(std::move(arguments));
438 case 'c':
439 return ParseDeviceAttributes(altered_greater, altered_question,
440 std::move(arguments));
441 default:
442 return SPECIAL;
443 }
444 }
445
446 // Invalid ESC in CSI.
447 if (Current() == '\x1B') {
448 return SPECIAL;
449 }
450 }
451}
452
453TerminalInputParser::Output TerminalInputParser::ParseOSC() {
454 // Parse until the string terminator ST.
455 while (true) {
456 if (!Eat()) {
457 return UNCOMPLETED;
458 }
459 if (Current() != '\x1B') {
460 continue;
461 }
462 if (!Eat()) {
463 return UNCOMPLETED;
464 }
465 if (Current() != '\\') {
466 continue;
467 }
468 return SPECIAL;
469 }
470}
471
472TerminalInputParser::Output TerminalInputParser::ParseMouse( // NOLINT
473 bool altered,
474 bool pressed,
475 std::vector<int> arguments) {
476 if (arguments.size() != 3) {
477 return SPECIAL;
478 }
479
480 (void)altered;
481
482 Output output(MOUSE);
483 output.mouse.motion = Mouse::Motion(pressed); // NOLINT
484
485 // Bits value Modifier Comment
486 // ---- ----- ------- ---------
487 // 0 1 1 2 button 0 = Left, 1 = Middle, 2 = Right, 3 = Release
488 // 2 4 Shift
489 // 3 8 Meta
490 // 4 16 Control
491 // 5 32 Move
492 // 6 64 Wheel
493
494 // clang-format off
495 const int button = arguments[0] & (1 + 2); // NOLINT
496 const bool is_shift = arguments[0] & 4; // NOLINT
497 const bool is_meta = arguments[0] & 8; // NOLINT
498 const bool is_control = arguments[0] & 16; // NOLINT
499 const bool is_move = arguments[0] & 32; // NOLINT
500 const bool is_wheel = arguments[0] & 64; // NOLINT
501 // clang-format on
502
503 output.mouse.motion = is_move ? Mouse::Moved : Mouse::Motion(pressed);
504 output.mouse.button = is_wheel ? Mouse::Button(Mouse::WheelUp + button) //
505 : Mouse::Button(button);
506 output.mouse.shift = is_shift;
507 output.mouse.meta = is_meta;
508 output.mouse.control = is_control;
509 output.mouse.x = arguments[1]; // NOLINT
510 output.mouse.y = arguments[2]; // NOLINT
511
512 // Motion event.
513 return output;
514}
515
516// NOLINTNEXTLINE
517TerminalInputParser::Output TerminalInputParser::ParseCursorPosition(
518 std::vector<int> arguments) {
519 if (arguments.size() != 2) {
520 return SPECIAL;
521 }
522 Output output(CURSOR_POSITION);
523 output.cursor.y = arguments[0]; // NOLINT
524 output.cursor.x = arguments[1]; // NOLINT
525 return output;
526}
527
528// NOLINTNEXTLINE
529TerminalInputParser::Output TerminalInputParser::ParseDeviceAttributes(
530 bool altered_greater,
531 bool altered_question,
532 std::vector<int> arguments) {
533 if (altered_greater) {
534 // Secondary Device Attributes (DA2)
535 // ESC [ > Pp ; Pv ; Pc c
536 if (arguments.size() >= 3) {
537 // Pp: Terminal type
538 // Pv: Firmware version
539 // Pc: Hardware options
540 Output output(TERMINAL_NAME_VERSION);
541 output.terminal_version = arguments[1];
542 switch (arguments[0]) {
543 case 0:
544 output.terminal_name = "xterm";
545 break;
546 case 1:
547 output.terminal_name = "vt220";
548 break;
549 case 2:
550 output.terminal_name = "vt240";
551 break;
552 case 18:
553 output.terminal_name = "vt330";
554 break;
555 case 19:
556 output.terminal_name = "vt340";
557 break;
558 case 24:
559 output.terminal_name = "vt320";
560 break;
561 case 41:
562 output.terminal_name = "vt420";
563 break;
564 case 61:
565 output.terminal_name = "vt510";
566 break;
567 case 64:
568 output.terminal_name = "vt520";
569 break;
570 case 65:
571 output.terminal_name = "vt525";
572 break;
573 case 84:
574 output.terminal_name = "tmux";
575 break;
576 case 85:
577 output.terminal_name = "urxvt";
578 break;
579 default:
580 output.terminal_name = "unknown";
581 break;
582 }
583 // Special case for xterm which often returns 0;pv;0 or similar
584 // but it's not strictly following DEC VT types.
585 return output;
586 }
587 } else if (altered_question) {
588 // Primary Device Attributes (DA1)
589 // ESC [ ? Pp ; ... c
590 Output output(TERMINAL_CAPABILITIES);
591 output.terminal_capabilities = std::move(arguments);
592 return output;
593 }
594 return SPECIAL;
595}
596
597} // namespace ftxui
TerminalInputParser(std::function< void(Event)> out)
static Event Special(std::string_view)
An custom event whose meaning is defined by the user of the library.
Definition event.cpp:240
const std::vector< int > & TerminalCapabilities() const
Return the terminal capabilities.
Definition event.cpp:218
Represent an event. It can be key press event, a terminal resize, or more ...
Definition event.hpp:32
The FTXUI ftxui:: namespace.
Definition animation.hpp:10
const std::map< std::string, std::string > g_uniformize