From 406355df8c18d2a33a772fd55cd1c7641ab33dc8 Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni Date: Sun, 25 Oct 2020 01:57:56 +0200 Subject: [PATCH] Fix parsing of keys that are prefix of others. (#58) The ESC key generates sequences that are prefix of others. For instance: - ESC => [27] - F1 => [27, 79, 8] As a result, we can't generate the ESC event when receiving [27], because it might be the start of the [27, 79, 8] sequence (or not). Application usually applies a timeout to help detecting the ESC key. This patch introduce a timeout. It is set to 50ms. Bug: https://github.com/ArthurSonzogni/FTXUI/issues/55 --- CMakeLists.txt | 7 +- examples/dom/color_info_sorted_2d.ipp | 6 +- include/ftxui/component/event.hpp | 2 - include/ftxui/component/receiver.hpp | 3 + src/ftxui/component/event.cpp | 120 ------------- src/ftxui/component/event_test.cpp | 49 ------ src/ftxui/component/screen_interactive.cpp | 55 +++--- src/ftxui/component/terminal_input_parser.cpp | 159 ++++++++++++++++++ src/ftxui/component/terminal_input_parser.hpp | 48 ++++++ .../component/terminal_input_parser_test.cpp | 71 ++++++++ 10 files changed, 311 insertions(+), 209 deletions(-) delete mode 100644 src/ftxui/component/event_test.cpp create mode 100644 src/ftxui/component/terminal_input_parser.cpp create mode 100644 src/ftxui/component/terminal_input_parser.hpp create mode 100644 src/ftxui/component/terminal_input_parser_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b213b10..aaab4b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,6 +91,8 @@ add_library(component src/ftxui/component/radiobox.cpp src/ftxui/component/screen_interactive.cpp src/ftxui/component/toggle.cpp + src/ftxui/component/terminal_input_parser.cpp + src/ftxui/component/terminal_input_parser.hpp ) add_library(ftxui::screen ALIAS screen) @@ -199,7 +201,7 @@ if (FTXUI_BUILD_TESTS AND ${CMAKE_VERSION} VERSION_GREATER "3.11.4") add_executable(tests src/ftxui/component/container_test.cpp - src/ftxui/component/event_test.cpp + src/ftxui/component/terminal_input_parser_test.cpp src/ftxui/component/radiobox_test.cpp src/ftxui/component/receiver_test.cpp src/ftxui/component/toggle_test.cpp @@ -215,6 +217,9 @@ if (FTXUI_BUILD_TESTS AND ${CMAKE_VERSION} VERSION_GREATER "3.11.4") PRIVATE gmock PRIVATE gtest_main ) + target_include_directories(tests + PRIVATE src + ) set_property(TARGET tests PROPERTY CXX_STANDARD 17) endif() diff --git a/examples/dom/color_info_sorted_2d.ipp b/examples/dom/color_info_sorted_2d.ipp index 6e5119d..8f2038f 100644 --- a/examples/dom/color_info_sorted_2d.ipp +++ b/examples/dom/color_info_sorted_2d.ipp @@ -21,7 +21,7 @@ std::vector> ColorInfoSorted2D() { // Make 8 colums, one gray and seven colored. std::vector> info_columns(8); info_columns[0] = info_gray; - for (int i = 0; i < info_color.size(); ++i) { + for (size_t i = 0; i < info_color.size(); ++i) { info_columns[1 + 7 * i / info_color.size()].push_back(info_color[i]); } @@ -31,10 +31,10 @@ std::vector> ColorInfoSorted2D() { [](const ColorInfo& A, const ColorInfo& B) { return A.value < B.value; }); - for (int i = 0; i < column.size() - 1; ++i) { + for (size_t i = 0; i < column.size() - 1; ++i) { int best_index = i + 1; int best_distance = 255 * 255 * 3; - for (int j = i + 1; j < column.size(); ++j) { + for (size_t j = i + 1; j < column.size(); ++j) { int dx = column[i].red - column[j].red; int dy = column[i].green - column[j].green; int dz = column[i].blue - column[j].blue; diff --git a/include/ftxui/component/event.hpp b/include/ftxui/component/event.hpp index 1c255a8..82831b6 100644 --- a/include/ftxui/component/event.hpp +++ b/include/ftxui/component/event.hpp @@ -23,8 +23,6 @@ struct Event { static Event Character(const std::string&); static Event Special(const std::string&); - static void Convert(Receiver& in, Sender& out, char c); - // --- Arrow --- static const Event ArrowLeft; static const Event ArrowRight; diff --git a/include/ftxui/component/receiver.hpp b/include/ftxui/component/receiver.hpp index ef65d81..7b85922 100644 --- a/include/ftxui/component/receiver.hpp +++ b/include/ftxui/component/receiver.hpp @@ -51,6 +51,8 @@ class SenderImpl { void Send(T t) { receiver_->Receive(std::move(t)); } ~SenderImpl() { receiver_->ReleaseSender(); } + Sender Clone() { return receiver_->MakeSender(); } + private: friend class ReceiverImpl; SenderImpl(ReceiverImpl* consumer) : receiver_(consumer) {} @@ -61,6 +63,7 @@ template class ReceiverImpl { public: Sender MakeSender() { + std::unique_lock lock(mutex_); senders_++; return std::unique_ptr>(new SenderImpl(this)); } diff --git a/src/ftxui/component/event.cpp b/src/ftxui/component/event.cpp index 25237af..29fc9c1 100644 --- a/src/ftxui/component/event.cpp +++ b/src/ftxui/component/event.cpp @@ -1,106 +1,10 @@ #include "ftxui/component/event.hpp" #include - #include "ftxui/screen/string.hpp" namespace ftxui { -namespace { - -void ParseUTF8(Receiver& in, Sender& out, std::string& input) { - char c; - unsigned char head = static_cast(input[0]); - for (int i = 0; i < 3; ++i, head <<= 1) { - if ((head & 0b11000000) != 0b11000000) - break; - if (!in->Receive(&c)) - return; - input += c; - } - out->Send(Event::Character(input)); -} - -void ParseCSI(Receiver& in, Sender& out, std::string& input) { - char c; - while (1) { - if (!in->Receive(&c)) - return; - input += c; - - if (c >= '0' && c <= '9') - continue; - - if (c == ';') - continue; - - if (c >= ' ' && c <= '~') - return out->Send(Event::Special(input)); - - // Invalid ESC in CSI. - if (c == '\x1B') - return out->Send(Event::Special(input)); - } -} - -void ParseDCS(Receiver& in, Sender& out, std::string& input) { - char c; - // Parse until the string terminator ST. - while (1) { - if (!in->Receive(&c)) - return; - input += c; - if (input.back() != '\x1B') - continue; - if (!in->Receive(&c)) - return; - input += c; - if (input.back() != '\\') - continue; - return out->Send(Event::Special(input)); - } -} - -void ParseOSC(Receiver& in, Sender& out, std::string& input) { - char c; - // Parse until the string terminator ST. - while (1) { - if (!in->Receive(&c)) - return; - input += c; - if (input.back() != '\x1B') - continue; - if (!in->Receive(&c)) - return; - input += c; - if (input.back() != '\\') - continue; - return out->Send(Event::Special(input)); - } -} - -void ParseESC(Receiver& in, Sender& out, std::string& input) { - char c; - if (!in->Receive(&c)) - return; - input += c; - switch (c) { - case 'P': - return ParseDCS(in, out, input); - case '[': - return ParseCSI(in, out, input); - case ']': - return ParseOSC(in, out, input); - default: - if (!in->Receive(&c)) - return; - input += c; - out->Send(Event::Special(input)); - } -} - -} // namespace - // static Event Event::Character(const std::string& input) { Event event; @@ -131,30 +35,6 @@ Event Event::Special(const std::string& input) { return event; } -// static -void Event::Convert(Receiver& in, Sender& out, char c) { - std::string input; - input += c; - - unsigned char head = input[0]; - switch (head) { - case 24: // CAN - case 26: // SUB - return; - - case '\x1B': - return ParseESC(in, out, input); - } - - if (head < 32) // C0 - return out->Send(Event::Special(input)); - - if (head == 127) // Delete - return out->Send(Event::Special(input)); - - return ParseUTF8(in, out, input); -} - // --- Arrow --- const Event Event::ArrowLeft = Event::Special("\x1B[D"); const Event Event::ArrowRight = Event::Special("\x1B[C"); diff --git a/src/ftxui/component/event_test.cpp b/src/ftxui/component/event_test.cpp deleted file mode 100644 index 5830991..0000000 --- a/src/ftxui/component/event_test.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include "ftxui/component/event.hpp" -#include "ftxui/component/receiver.hpp" - -#include "gtest/gtest.h" - -using namespace ftxui; - -namespace { -// Produce a stream of Event from a stream of char. -void CharToEventStream(Receiver receiver, Sender sender) { - char c; - while (receiver->Receive(&c)) - Event::Convert(receiver, sender, c); -} - -} // namespace - -// Test char |c| to are trivially converted into |Event::Character(c)|. -TEST(Event, Character) { - std::vector basic_char; - for (char c = 'a'; c < 'z'; ++c) - basic_char.push_back(c); - for (char c = 'A'; c < 'Z'; ++c) - basic_char.push_back(c); - - auto char_receiver = MakeReceiver(); - auto char_sender = char_receiver->MakeSender(); - - auto event_receiver = MakeReceiver(); - auto event_sender = event_receiver->MakeSender(); - - for (char c : basic_char) - char_sender->Send(c); - char_sender.reset(); - - CharToEventStream(std::move(char_receiver), std::move(event_sender)); - - Event received; - for (char c : basic_char) { - EXPECT_TRUE(event_receiver->Receive(&received)); - EXPECT_TRUE(received.is_character()); - EXPECT_EQ(c, received.character()); - } - EXPECT_FALSE(event_receiver->Receive(&received)); -} - -// 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. diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 7f7d8e9..45048a7 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -10,6 +10,7 @@ #include #include "ftxui/component/component.hpp" +#include "ftxui/component/terminal_input_parser.hpp" #include "ftxui/screen/string.hpp" #include "ftxui/screen/terminal.hpp" @@ -36,25 +37,23 @@ namespace ftxui { namespace { -// Produce a stream of Event from a stream of char. -void CharToEventStream(Receiver receiver, Sender sender) { - char c; - while (receiver->Receive(&c)) - Event::Convert(receiver, sender, c); -} +constexpr int timeout_milliseconds = 20; +constexpr int timeout_microseconds = timeout_milliseconds * 1000; #if defined(_WIN32) -void Win32EventListener(std::atomic* quit, - Sender char_sender, - Sender event_sender) { +void EventListener(std::atomic* quit, + Sender out) { auto console = GetStdHandle(STD_INPUT_HANDLE); + auto parser = TerminalInputParser(out->Clone()); while (!*quit) { // Throttle ReadConsoleInput by waiting 250ms, this wait function will // return if there is input in the console. - auto wait_result = WaitForSingleObject(console, 250); - if (wait_result == WAIT_TIMEOUT) + auto wait_result = WaitForSingleObject(console, timeout_milliseconds); + if (wait_result == WAIT_TIMEOUT) { + parser.Timeout(timeout_milliseconds); continue; + } DWORD number_of_events = 0; if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) @@ -76,10 +75,10 @@ void Win32EventListener(std::atomic* quit, // ignore UP key events if (key_event.bKeyDown == FALSE) continue; - char_sender->Send((char)key_event.uChar.UnicodeChar); + parser.Add((char)key_event.uChar.UnicodeChar); } break; case WINDOW_BUFFER_SIZE_EVENT: - event_sender->Send(Event::Special({0})); + out->Send(Event::Special({0})); break; case MENU_EVENT: case FOCUS_EVENT: @@ -103,17 +102,21 @@ int CheckStdinReady(int usec_timeout) { } // Read char from the terminal. -void UnixEventListener(std::atomic* quit, Sender sender) { +void EventListener(std::atomic* quit, Sender out) { const int buffer_size = 100; - const int timeout_usec = 50000; + + auto parser = TerminalInputParser(std::move(out)); while (!*quit) { - if (!CheckStdinReady(timeout_usec)) + if (!CheckStdinReady(timeout_microseconds)) { + parser.Timeout(timeout_milliseconds); continue; + } + char buff[buffer_size]; int l = read(fileno(stdin), buff, buffer_size); for (int i = 0; i < l; ++i) - sender->Send(buff[i]); + parser.Add(buff[i]); } } @@ -258,23 +261,8 @@ void ScreenInteractive::Loop(Component* component) { std::cout << std::endl; }); - // Produce a stream of Event from a stream of char. - auto char_receiver = MakeReceiver(); - auto char_sender = char_receiver->MakeSender(); - auto event_sender_1 = event_receiver_->MakeSender(); - auto char_to_event_stream = std::thread( - CharToEventStream, std::move(char_receiver), std::move(event_sender_1)); - - // Depending on the OS, start a thread that will produce events and/or chars. -#if defined(_WIN32) - auto event_sender_2 = event_receiver_->MakeSender(); auto event_listener = - std::thread(&Win32EventListener, &quit_, std::move(char_sender), - std::move(event_sender_2)); -#else - auto event_listener = - std::thread(&UnixEventListener, &quit_, std::move(char_sender)); -#endif + std::thread(&EventListener, &quit_, event_receiver_->MakeSender()); if (use_alternative_screen_) { std::cout << USE_ALTERNATIVE_SCREEN; @@ -294,7 +282,6 @@ void ScreenInteractive::Loop(Component* component) { component->OnEvent(event); } - char_to_event_stream.join(); event_listener.join(); OnExit(0); } diff --git a/src/ftxui/component/terminal_input_parser.cpp b/src/ftxui/component/terminal_input_parser.cpp new file mode 100644 index 0000000..78e0fdd --- /dev/null +++ b/src/ftxui/component/terminal_input_parser.cpp @@ -0,0 +1,159 @@ +#include "ftxui/component/terminal_input_parser.hpp" + +namespace ftxui { + +TerminalInputParser::TerminalInputParser(Sender out) + : out_(std::move(out)) {} + +void TerminalInputParser::Timeout(int time) { + timeout_ += time; + if (timeout_ < 50) + return; + timeout_ = 0; + if (pending_.size()) + 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_ < (int)pending_.size(); +} + +void TerminalInputParser::Send(TerminalInputParser::Type type) { + switch (type) { + case UNCOMPLETED: + return; + + case DROP: + pending_.clear(); + return; + + case CHARACTER: + out_->Send(Event::Character(std::move(pending_))); + pending_.clear(); + return; + + case SPECIAL: + out_->Send(Event::Special(std::move(pending_))); + pending_.clear(); + return; + } +} + +TerminalInputParser::Type TerminalInputParser::Parse() { + if (!Eat()) + return UNCOMPLETED; + + switch (Current()) { + case 24: // CAN + case 26: // SUB + return DROP; + + case '\x1B': + return ParseESC(); + default: + break; + } + + if (Current() < 32) // C0 + return SPECIAL; + + if (Current() == 127) // Delete + return SPECIAL; + + return ParseUTF8(); +} + +TerminalInputParser::Type TerminalInputParser::ParseUTF8() { + unsigned char head = static_cast(Current()); + for (int i = 0; i < 3; ++i, head <<= 1) { + if ((head & 0b11000000) != 0b11000000) + break; + if (!Eat()) + return UNCOMPLETED; + } + return CHARACTER; +} + +TerminalInputParser::Type TerminalInputParser::ParseESC() { + if (!Eat()) + return UNCOMPLETED; + switch (Current()) { + case 'P': + return ParseDCS(); + case '[': + return ParseCSI(); + case ']': + return ParseOSC(); + default: + if (!Eat()) + return UNCOMPLETED; + return SPECIAL; + } +} + +TerminalInputParser::Type TerminalInputParser::ParseDCS() { + // Parse until the string terminator ST. + while (1) { + if (!Eat()) + return UNCOMPLETED; + + if (Current() != '\x1B') + continue; + + if (!Eat()) + return UNCOMPLETED; + + if (Current() != '\\') + continue; + + return SPECIAL; + } +} + +TerminalInputParser::Type TerminalInputParser::ParseCSI() { + while (true) { + if (!Eat()) + return UNCOMPLETED; + + if (Current() >= '0' && Current() <= '9') + continue; + + if (Current() == ';') + continue; + + if (Current() >= ' ' && Current() <= '~') + return SPECIAL; + + // Invalid ESC in CSI. + if (Current() == '\x1B') + return SPECIAL; + } +} + +TerminalInputParser::Type 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; + } +} +} // namespace ftxui diff --git a/src/ftxui/component/terminal_input_parser.hpp b/src/ftxui/component/terminal_input_parser.hpp new file mode 100644 index 0000000..ac0df24 --- /dev/null +++ b/src/ftxui/component/terminal_input_parser.hpp @@ -0,0 +1,48 @@ +#ifndef FTXUI_COMPONENT_TERMINAL_INPUT_PARSER +#define FTXUI_COMPONENT_TERMINAL_INPUT_PARSER + +#include "ftxui/component/event.hpp" +#include "ftxui/component/receiver.hpp" + +#include + +namespace ftxui { + +// Parse a sequence of |char| accross |time|. Produces |Event|. +class TerminalInputParser { + public: + TerminalInputParser(Sender out); + void Timeout(int time); + void Add(char c); + + private: + unsigned char Current(); + bool Eat(); + + enum Type { + UNCOMPLETED = 0, + DROP = 1, + CHARACTER = 2, + SPECIAL = 3, + }; + void Send(Type type); + Type Parse(); + Type ParseUTF8(); + Type ParseESC(); + Type ParseDCS(); + Type ParseCSI(); + Type ParseOSC(); + + Sender out_; + int position_ = -1; + int timeout_ = 0; + std::string pending_; +}; + +} // namespace ftxui + +#endif /* end of include guard: FTXUI_COMPONENT_TERMINAL_INPUT_PARSER */ + +// 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. diff --git a/src/ftxui/component/terminal_input_parser_test.cpp b/src/ftxui/component/terminal_input_parser_test.cpp new file mode 100644 index 0000000..09bcfa4 --- /dev/null +++ b/src/ftxui/component/terminal_input_parser_test.cpp @@ -0,0 +1,71 @@ +#include "ftxui/component/terminal_input_parser.hpp" +#include "ftxui/component/receiver.hpp" + +#include "gtest/gtest.h" + +using namespace ftxui; + +// Test char |c| to are trivially converted into |Event::Character(c)|. +TEST(Event, Character) { + std::vector basic_char; + for (char c = 'a'; c <= 'z'; ++c) + basic_char.push_back(c); + for (char c = 'A'; c <= 'Z'; ++c) + basic_char.push_back(c); + + auto event_receiver = MakeReceiver(); + { + auto parser = TerminalInputParser(event_receiver->MakeSender()); + for (char c : basic_char) + parser.Add(c); + } + + Event received; + for (char c : basic_char) { + EXPECT_TRUE(event_receiver->Receive(&received)); + EXPECT_TRUE(received.is_character()); + EXPECT_EQ(c, received.character()); + } + EXPECT_FALSE(event_receiver->Receive(&received)); +} + +TEST(Event, EscapeKeyWithoutWaiting) { + auto event_receiver = MakeReceiver(); + { + auto parser = TerminalInputParser(event_receiver->MakeSender()); + parser.Add('\x1B'); + } + + Event received; + EXPECT_FALSE(event_receiver->Receive(&received)); +} + +TEST(Event, EscapeKeyNotEnoughWait) { + auto event_receiver = MakeReceiver(); + { + auto parser = TerminalInputParser(event_receiver->MakeSender()); + parser.Add('\x1B'); + parser.Timeout(49); + } + + Event received; + EXPECT_FALSE(event_receiver->Receive(&received)); +} + +TEST(Event, EscapeKeyEnoughWait) { + auto event_receiver = MakeReceiver(); + { + auto parser = TerminalInputParser(event_receiver->MakeSender()); + parser.Add('\x1B'); + parser.Timeout(50); + } + + Event received; + EXPECT_TRUE(event_receiver->Receive(&received)); + EXPECT_EQ(received, Event::Escape); + EXPECT_FALSE(event_receiver->Receive(&received)); +} + +// 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.