mirror of
https://github.com/ArthurSonzogni/FTXUI.git
synced 2024-11-25 20:27:31 +08:00
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
This commit is contained in:
parent
c13621d1f9
commit
406355df8c
@ -91,6 +91,8 @@ add_library(component
|
|||||||
src/ftxui/component/radiobox.cpp
|
src/ftxui/component/radiobox.cpp
|
||||||
src/ftxui/component/screen_interactive.cpp
|
src/ftxui/component/screen_interactive.cpp
|
||||||
src/ftxui/component/toggle.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)
|
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
|
add_executable(tests
|
||||||
src/ftxui/component/container_test.cpp
|
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/radiobox_test.cpp
|
||||||
src/ftxui/component/receiver_test.cpp
|
src/ftxui/component/receiver_test.cpp
|
||||||
src/ftxui/component/toggle_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 gmock
|
||||||
PRIVATE gtest_main
|
PRIVATE gtest_main
|
||||||
)
|
)
|
||||||
|
target_include_directories(tests
|
||||||
|
PRIVATE src
|
||||||
|
)
|
||||||
|
|
||||||
set_property(TARGET tests PROPERTY CXX_STANDARD 17)
|
set_property(TARGET tests PROPERTY CXX_STANDARD 17)
|
||||||
endif()
|
endif()
|
||||||
|
@ -21,7 +21,7 @@ std::vector<std::vector<ColorInfo>> ColorInfoSorted2D() {
|
|||||||
// Make 8 colums, one gray and seven colored.
|
// Make 8 colums, one gray and seven colored.
|
||||||
std::vector<std::vector<ColorInfo>> info_columns(8);
|
std::vector<std::vector<ColorInfo>> info_columns(8);
|
||||||
info_columns[0] = info_gray;
|
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]);
|
info_columns[1 + 7 * i / info_color.size()].push_back(info_color[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,10 +31,10 @@ std::vector<std::vector<ColorInfo>> ColorInfoSorted2D() {
|
|||||||
[](const ColorInfo& A, const ColorInfo& B) {
|
[](const ColorInfo& A, const ColorInfo& B) {
|
||||||
return A.value < B.value;
|
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_index = i + 1;
|
||||||
int best_distance = 255 * 255 * 3;
|
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 dx = column[i].red - column[j].red;
|
||||||
int dy = column[i].green - column[j].green;
|
int dy = column[i].green - column[j].green;
|
||||||
int dz = column[i].blue - column[j].blue;
|
int dz = column[i].blue - column[j].blue;
|
||||||
|
@ -23,8 +23,6 @@ struct Event {
|
|||||||
static Event Character(const std::string&);
|
static Event Character(const std::string&);
|
||||||
static Event Special(const std::string&);
|
static Event Special(const std::string&);
|
||||||
|
|
||||||
static void Convert(Receiver<char>& in, Sender<Event>& out, char c);
|
|
||||||
|
|
||||||
// --- Arrow ---
|
// --- Arrow ---
|
||||||
static const Event ArrowLeft;
|
static const Event ArrowLeft;
|
||||||
static const Event ArrowRight;
|
static const Event ArrowRight;
|
||||||
|
@ -51,6 +51,8 @@ class SenderImpl {
|
|||||||
void Send(T t) { receiver_->Receive(std::move(t)); }
|
void Send(T t) { receiver_->Receive(std::move(t)); }
|
||||||
~SenderImpl() { receiver_->ReleaseSender(); }
|
~SenderImpl() { receiver_->ReleaseSender(); }
|
||||||
|
|
||||||
|
Sender<T> Clone() { return receiver_->MakeSender(); }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class ReceiverImpl<T>;
|
friend class ReceiverImpl<T>;
|
||||||
SenderImpl(ReceiverImpl<T>* consumer) : receiver_(consumer) {}
|
SenderImpl(ReceiverImpl<T>* consumer) : receiver_(consumer) {}
|
||||||
@ -61,6 +63,7 @@ template <class T>
|
|||||||
class ReceiverImpl {
|
class ReceiverImpl {
|
||||||
public:
|
public:
|
||||||
Sender<T> MakeSender() {
|
Sender<T> MakeSender() {
|
||||||
|
std::unique_lock<std::mutex> lock(mutex_);
|
||||||
senders_++;
|
senders_++;
|
||||||
return std::unique_ptr<SenderImpl<T>>(new SenderImpl<T>(this));
|
return std::unique_ptr<SenderImpl<T>>(new SenderImpl<T>(this));
|
||||||
}
|
}
|
||||||
|
@ -1,106 +1,10 @@
|
|||||||
#include "ftxui/component/event.hpp"
|
#include "ftxui/component/event.hpp"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
#include "ftxui/screen/string.hpp"
|
#include "ftxui/screen/string.hpp"
|
||||||
|
|
||||||
namespace ftxui {
|
namespace ftxui {
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void ParseUTF8(Receiver<char>& in, Sender<Event>& out, std::string& input) {
|
|
||||||
char c;
|
|
||||||
unsigned char head = static_cast<unsigned char>(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<char>& in, Sender<Event>& 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<char>& in, Sender<Event>& 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<char>& in, Sender<Event>& 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<char>& in, Sender<Event>& 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
|
// static
|
||||||
Event Event::Character(const std::string& input) {
|
Event Event::Character(const std::string& input) {
|
||||||
Event event;
|
Event event;
|
||||||
@ -131,30 +35,6 @@ Event Event::Special(const std::string& input) {
|
|||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
// static
|
|
||||||
void Event::Convert(Receiver<char>& in, Sender<Event>& 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 ---
|
// --- Arrow ---
|
||||||
const Event Event::ArrowLeft = Event::Special("\x1B[D");
|
const Event Event::ArrowLeft = Event::Special("\x1B[D");
|
||||||
const Event Event::ArrowRight = Event::Special("\x1B[C");
|
const Event Event::ArrowRight = Event::Special("\x1B[C");
|
||||||
|
@ -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<char> receiver, Sender<Event> 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<char> 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<char>();
|
|
||||||
auto char_sender = char_receiver->MakeSender();
|
|
||||||
|
|
||||||
auto event_receiver = MakeReceiver<Event>();
|
|
||||||
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.
|
|
@ -10,6 +10,7 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include "ftxui/component/component.hpp"
|
#include "ftxui/component/component.hpp"
|
||||||
|
#include "ftxui/component/terminal_input_parser.hpp"
|
||||||
#include "ftxui/screen/string.hpp"
|
#include "ftxui/screen/string.hpp"
|
||||||
#include "ftxui/screen/terminal.hpp"
|
#include "ftxui/screen/terminal.hpp"
|
||||||
|
|
||||||
@ -36,25 +37,23 @@
|
|||||||
namespace ftxui {
|
namespace ftxui {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Produce a stream of Event from a stream of char.
|
|
||||||
void CharToEventStream(Receiver<char> receiver, Sender<Event> 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)
|
#if defined(_WIN32)
|
||||||
|
|
||||||
void Win32EventListener(std::atomic<bool>* quit,
|
void EventListener(std::atomic<bool>* quit,
|
||||||
Sender<char> char_sender,
|
Sender<Event> out) {
|
||||||
Sender<Event> event_sender) {
|
|
||||||
auto console = GetStdHandle(STD_INPUT_HANDLE);
|
auto console = GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
auto parser = TerminalInputParser(out->Clone());
|
||||||
while (!*quit) {
|
while (!*quit) {
|
||||||
// Throttle ReadConsoleInput by waiting 250ms, this wait function will
|
// Throttle ReadConsoleInput by waiting 250ms, this wait function will
|
||||||
// return if there is input in the console.
|
// return if there is input in the console.
|
||||||
auto wait_result = WaitForSingleObject(console, 250);
|
auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
|
||||||
if (wait_result == WAIT_TIMEOUT)
|
if (wait_result == WAIT_TIMEOUT) {
|
||||||
|
parser.Timeout(timeout_milliseconds);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
DWORD number_of_events = 0;
|
DWORD number_of_events = 0;
|
||||||
if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
|
if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
|
||||||
@ -76,10 +75,10 @@ void Win32EventListener(std::atomic<bool>* quit,
|
|||||||
// ignore UP key events
|
// ignore UP key events
|
||||||
if (key_event.bKeyDown == FALSE)
|
if (key_event.bKeyDown == FALSE)
|
||||||
continue;
|
continue;
|
||||||
char_sender->Send((char)key_event.uChar.UnicodeChar);
|
parser.Add((char)key_event.uChar.UnicodeChar);
|
||||||
} break;
|
} break;
|
||||||
case WINDOW_BUFFER_SIZE_EVENT:
|
case WINDOW_BUFFER_SIZE_EVENT:
|
||||||
event_sender->Send(Event::Special({0}));
|
out->Send(Event::Special({0}));
|
||||||
break;
|
break;
|
||||||
case MENU_EVENT:
|
case MENU_EVENT:
|
||||||
case FOCUS_EVENT:
|
case FOCUS_EVENT:
|
||||||
@ -103,17 +102,21 @@ int CheckStdinReady(int usec_timeout) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read char from the terminal.
|
// Read char from the terminal.
|
||||||
void UnixEventListener(std::atomic<bool>* quit, Sender<char> sender) {
|
void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
|
||||||
const int buffer_size = 100;
|
const int buffer_size = 100;
|
||||||
const int timeout_usec = 50000;
|
|
||||||
|
auto parser = TerminalInputParser(std::move(out));
|
||||||
|
|
||||||
while (!*quit) {
|
while (!*quit) {
|
||||||
if (!CheckStdinReady(timeout_usec))
|
if (!CheckStdinReady(timeout_microseconds)) {
|
||||||
|
parser.Timeout(timeout_milliseconds);
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
char buff[buffer_size];
|
char buff[buffer_size];
|
||||||
int l = read(fileno(stdin), buff, buffer_size);
|
int l = read(fileno(stdin), buff, buffer_size);
|
||||||
for (int i = 0; i < l; ++i)
|
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;
|
std::cout << std::endl;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Produce a stream of Event from a stream of char.
|
|
||||||
auto char_receiver = MakeReceiver<char>();
|
|
||||||
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 =
|
auto event_listener =
|
||||||
std::thread(&Win32EventListener, &quit_, std::move(char_sender),
|
std::thread(&EventListener, &quit_, event_receiver_->MakeSender());
|
||||||
std::move(event_sender_2));
|
|
||||||
#else
|
|
||||||
auto event_listener =
|
|
||||||
std::thread(&UnixEventListener, &quit_, std::move(char_sender));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (use_alternative_screen_) {
|
if (use_alternative_screen_) {
|
||||||
std::cout << USE_ALTERNATIVE_SCREEN;
|
std::cout << USE_ALTERNATIVE_SCREEN;
|
||||||
@ -294,7 +282,6 @@ void ScreenInteractive::Loop(Component* component) {
|
|||||||
component->OnEvent(event);
|
component->OnEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
char_to_event_stream.join();
|
|
||||||
event_listener.join();
|
event_listener.join();
|
||||||
OnExit(0);
|
OnExit(0);
|
||||||
}
|
}
|
||||||
|
159
src/ftxui/component/terminal_input_parser.cpp
Normal file
159
src/ftxui/component/terminal_input_parser.cpp
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
#include "ftxui/component/terminal_input_parser.hpp"
|
||||||
|
|
||||||
|
namespace ftxui {
|
||||||
|
|
||||||
|
TerminalInputParser::TerminalInputParser(Sender<Event> 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<unsigned char>(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
|
48
src/ftxui/component/terminal_input_parser.hpp
Normal file
48
src/ftxui/component/terminal_input_parser.hpp
Normal file
@ -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 <string>
|
||||||
|
|
||||||
|
namespace ftxui {
|
||||||
|
|
||||||
|
// Parse a sequence of |char| accross |time|. Produces |Event|.
|
||||||
|
class TerminalInputParser {
|
||||||
|
public:
|
||||||
|
TerminalInputParser(Sender<Event> 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<Event> 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.
|
71
src/ftxui/component/terminal_input_parser_test.cpp
Normal file
71
src/ftxui/component/terminal_input_parser_test.cpp
Normal file
@ -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<char> 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<Event>();
|
||||||
|
{
|
||||||
|
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<Event>();
|
||||||
|
{
|
||||||
|
auto parser = TerminalInputParser(event_receiver->MakeSender());
|
||||||
|
parser.Add('\x1B');
|
||||||
|
}
|
||||||
|
|
||||||
|
Event received;
|
||||||
|
EXPECT_FALSE(event_receiver->Receive(&received));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Event, EscapeKeyNotEnoughWait) {
|
||||||
|
auto event_receiver = MakeReceiver<Event>();
|
||||||
|
{
|
||||||
|
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<Event>();
|
||||||
|
{
|
||||||
|
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.
|
Loading…
Reference in New Issue
Block a user