You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
420 lines
11 KiB
420 lines
11 KiB
2 weeks ago
|
// 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 <cstdint> // for uint32_t
|
||
|
#include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Button, Mouse::Motion
|
||
|
#include <ftxui/component/receiver.hpp> // for SenderImpl, Sender
|
||
|
#include <map>
|
||
|
#include <memory> // for unique_ptr, allocator
|
||
|
#include <utility> // for move
|
||
|
|
||
|
#include "ftxui/component/event.hpp" // for Event
|
||
|
#include "ftxui/component/task.hpp" // for Task
|
||
|
|
||
|
namespace ftxui {
|
||
|
|
||
|
// NOLINTNEXTLINE
|
||
|
const std::map<std::string, std::string> 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<Task> 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<int>(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<int> 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<int> 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<int> 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
|