// Copyright 2020 Arthur Sonzogni. All rights reserved. // Use of this source code is governed by the MIT license that can be found in // the LICENSE file. #include "input_parser.hpp" #include // for uint32_t #include // for Mouse, Mouse::Button, Mouse::Motion #include // for SenderImpl, Sender #include #include // for unique_ptr, allocator #include // for move #include "ftxui/component/event.hpp" // for Event #include "ftxui/component/task.hpp" // for Task namespace ftxui { // NOLINTNEXTLINE const std::map g_uniformize = { // Microsoft's terminal uses a different new line character for the return // key. This also happens with linux with the `bind` command: // See https://github.com/ArthurSonzogni/FTXUI/issues/337 // Here, we uniformize the new line character to `\n`. {"\r", "\n"}, // See: https://github.com/ArthurSonzogni/FTXUI/issues/508 {std::string({8}), std::string({127})}, // See: https://github.com/ArthurSonzogni/FTXUI/issues/626 // // Depending on the Cursor Key Mode (DECCKM), the terminal sends different // escape sequences: // // Key Normal Application // ----- -------- ----------- // Up ESC [ A ESC O A // Down ESC [ B ESC O B // Right ESC [ C ESC O C // Left ESC [ D ESC O D // Home ESC [ H ESC O H // End ESC [ F ESC O F // {"\x1BOA", "\x1B[A"}, // UP {"\x1BOB", "\x1B[B"}, // DOWN {"\x1BOC", "\x1B[C"}, // RIGHT {"\x1BOD", "\x1B[D"}, // LEFT {"\x1BOH", "\x1B[H"}, // HOME {"\x1BOF", "\x1B[F"}, // END // Variations around the FN keys. // Internally, we are using: // vt220, xterm-vt200, xterm-xf86-v44, xterm-new, mgt, screen // See: https://invisible-island.net/xterm/xterm-function-keys.html // For linux OS console (CTRL+ALT+FN), who do not belong to any // real standard. // See: https://github.com/ArthurSonzogni/FTXUI/issues/685 {"\x1B[[A", "\x1BOP"}, // F1 {"\x1B[[B", "\x1BOQ"}, // F2 {"\x1B[[C", "\x1BOR"}, // F3 {"\x1B[[D", "\x1BOS"}, // F4 {"\x1B[[E", "\x1B[15~"}, // F5 // xterm-r5, xterm-r6, rxvt {"\x1B[11~", "\x1BOP"}, // F1 {"\x1B[12~", "\x1BOQ"}, // F2 {"\x1B[13~", "\x1BOR"}, // F3 {"\x1B[14~", "\x1BOS"}, // F4 // vt100 {"\x1BOt", "\x1B[15~"}, // F5 {"\x1BOu", "\x1B[17~"}, // F6 {"\x1BOv", "\x1B[18~"}, // F7 {"\x1BOl", "\x1B[19~"}, // F8 {"\x1BOw", "\x1B[20~"}, // F9 {"\x1BOx", "\x1B[21~"}, // F10 // scoansi {"\x1B[M", "\x1BOP"}, // F1 {"\x1B[N", "\x1BOQ"}, // F2 {"\x1B[O", "\x1BOR"}, // F3 {"\x1B[P", "\x1BOS"}, // F4 {"\x1B[Q", "\x1B[15~"}, // F5 {"\x1B[R", "\x1B[17~"}, // F6 {"\x1B[S", "\x1B[18~"}, // F7 {"\x1B[T", "\x1B[19~"}, // F8 {"\x1B[U", "\x1B[20~"}, // F9 {"\x1B[V", "\x1B[21~"}, // F10 {"\x1B[W", "\x1B[23~"}, // F11 {"\x1B[X", "\x1B[24~"}, // F12 }; TerminalInputParser::TerminalInputParser(Sender out) : out_(std::move(out)) {} void TerminalInputParser::Timeout(int time) { timeout_ += time; const int timeout_threshold = 50; if (timeout_ < timeout_threshold) { return; } timeout_ = 0; if (!pending_.empty()) { Send(SPECIAL); } } void TerminalInputParser::Add(char c) { pending_ += c; timeout_ = 0; position_ = -1; Send(Parse()); } unsigned char TerminalInputParser::Current() { return pending_[position_]; } bool TerminalInputParser::Eat() { position_++; return position_ < static_cast(pending_.size()); } void TerminalInputParser::Send(TerminalInputParser::Output output) { switch (output.type) { case UNCOMPLETED: return; case DROP: pending_.clear(); return; case CHARACTER: out_->Send(Event::Character(std::move(pending_))); pending_.clear(); return; case SPECIAL: { auto it = g_uniformize.find(pending_); if (it != g_uniformize.end()) { pending_ = it->second; } out_->Send(Event::Special(std::move(pending_))); pending_.clear(); } return; case MOUSE: out_->Send(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT pending_.clear(); return; case CURSOR_REPORTING: out_->Send(Event::CursorReporting(std::move(pending_), // NOLINT output.cursor.x, // NOLINT output.cursor.y)); // NOLINT pending_.clear(); return; } // NOT_REACHED(). } TerminalInputParser::Output TerminalInputParser::Parse() { if (!Eat()) { return UNCOMPLETED; } switch (Current()) { case 24: // CAN NOLINT case 26: // SUB NOLINT return DROP; case '\x1B': return ParseESC(); default: break; } if (Current() < 32) { // C0 NOLINT return SPECIAL; } if (Current() == 127) { // Delete // NOLINT return SPECIAL; } return ParseUTF8(); } // Code point <-> UTF-8 conversion // // ┏━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━┓ // ┃Byte 1 ┃Byte 2 ┃Byte 3 ┃Byte 4 ┃ // ┡━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━┩ // │0xxxxxxx│ │ │ │ // ├────────┼────────┼────────┼────────┤ // │110xxxxx│10xxxxxx│ │ │ // ├────────┼────────┼────────┼────────┤ // │1110xxxx│10xxxxxx│10xxxxxx│ │ // ├────────┼────────┼────────┼────────┤ // │11110xxx│10xxxxxx│10xxxxxx│10xxxxxx│ // └────────┴────────┴────────┴────────┘ // // Then some sequences are illegal if it exist a shorter representation of the // same codepoint. TerminalInputParser::Output TerminalInputParser::ParseUTF8() { auto head = Current(); unsigned char selector = 0b1000'0000; // NOLINT // The non code-point part of the first byte. unsigned char mask = selector; // Find the first zero in the first byte. unsigned int first_zero = 8; // NOLINT for (unsigned int i = 0; i < 8; ++i) { // NOLINT mask |= selector; if (!(head & selector)) { first_zero = i; break; } selector >>= 1U; } // Accumulate the value of the first byte. auto value = uint32_t(head & ~mask); // NOLINT // Invalid UTF8, with more than 5 bytes. const unsigned int max_utf8_bytes = 5; if (first_zero == 1 || first_zero >= max_utf8_bytes) { return DROP; } // Multi byte UTF-8. for (unsigned int i = 2; i <= first_zero; ++i) { if (!Eat()) { return UNCOMPLETED; } // Invalid continuation byte. head = Current(); if ((head & 0b1100'0000) != 0b1000'0000) { // NOLINT return DROP; } value <<= 6; // NOLINT value += head & 0b0011'1111; // NOLINT } // Check for overlong UTF8 encoding. int extra_byte = 0; if (value <= 0b000'0000'0111'1111) { // NOLINT extra_byte = 0; // NOLINT } else if (value <= 0b000'0111'1111'1111) { // NOLINT extra_byte = 1; // NOLINT } else if (value <= 0b1111'1111'1111'1111) { // NOLINT extra_byte = 2; // NOLINT } else if (value <= 0b1'0000'1111'1111'1111'1111) { // NOLINT extra_byte = 3; // NOLINT } else { // NOLINT return DROP; } if (extra_byte != position_) { return DROP; } return CHARACTER; } TerminalInputParser::Output TerminalInputParser::ParseESC() { if (!Eat()) { return UNCOMPLETED; } switch (Current()) { case 'P': return ParseDCS(); case '[': return ParseCSI(); case ']': return ParseOSC(); default: if (!Eat()) { return UNCOMPLETED; } else { return SPECIAL; } } } TerminalInputParser::Output TerminalInputParser::ParseDCS() { // Parse until the string terminator ST. while (true) { if (!Eat()) { return UNCOMPLETED; } if (Current() != '\x1B') { continue; } if (!Eat()) { return UNCOMPLETED; } if (Current() != '\\') { continue; } return SPECIAL; } } TerminalInputParser::Output TerminalInputParser::ParseCSI() { bool altered = false; int argument = 0; std::vector arguments; while (true) { if (!Eat()) { return UNCOMPLETED; } if (Current() == '<') { altered = true; continue; } if (Current() >= '0' && Current() <= '9') { argument *= 10; // NOLINT argument += Current() - '0'; continue; } if (Current() == ';') { arguments.push_back(argument); argument = 0; continue; } // CSI is terminated by a character in the range 0x40–0x7E // (ASCII @A–Z[\]^_`a–z{|}~), if (Current() >= '@' && Current() <= '~' && // Note: I don't remember why we exclude '<' Current() != '<' && // To handle F1-F4, we exclude '['. Current() != '[') { arguments.push_back(argument); argument = 0; // NOLINT switch (Current()) { case 'M': return ParseMouse(altered, true, std::move(arguments)); case 'm': return ParseMouse(altered, false, std::move(arguments)); case 'R': return ParseCursorReporting(std::move(arguments)); default: return SPECIAL; } } // Invalid ESC in CSI. if (Current() == '\x1B') { return SPECIAL; } } } TerminalInputParser::Output TerminalInputParser::ParseOSC() { // Parse until the string terminator ST. while (true) { if (!Eat()) { return UNCOMPLETED; } if (Current() != '\x1B') { continue; } if (!Eat()) { return UNCOMPLETED; } if (Current() != '\\') { continue; } return SPECIAL; } } TerminalInputParser::Output TerminalInputParser::ParseMouse( // NOLINT bool altered, bool pressed, std::vector arguments) { if (arguments.size() != 3) { return SPECIAL; } (void)altered; Output output(MOUSE); output.mouse.button = Mouse::Button((arguments[0] & 3) + // NOLINT ((arguments[0] & 64) >> 4)); // NOLINT output.mouse.motion = Mouse::Motion(pressed); // NOLINT output.mouse.shift = bool(arguments[0] & 4); // NOLINT output.mouse.meta = bool(arguments[0] & 8); // NOLINT output.mouse.x = arguments[1]; // NOLINT output.mouse.y = arguments[2]; // NOLINT return output; } // NOLINTNEXTLINE TerminalInputParser::Output TerminalInputParser::ParseCursorReporting( std::vector arguments) { if (arguments.size() != 2) { return SPECIAL; } Output output(CURSOR_REPORTING); output.cursor.y = arguments[0]; // NOLINT output.cursor.x = arguments[1]; // NOLINT return output; } } // namespace ftxui