From 09a1b16613385e0c33ff89d4271af852c9229dee Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Tue, 24 Mar 2020 23:26:55 +0100 Subject: [PATCH] Add a Producer/Consumer system. It allow you to create the two end of a pipe: A producer and consumer. The producer can be moved into another thread. Several producer can be created if necessary. This will ease merging: https://github.com/ArthurSonzogni/FTXUI/pull/11 --- CMakeLists.txt | 18 ++++ include/ftxui/component/event.hpp | 3 +- include/ftxui/component/producer_consumer.hpp | 101 ++++++++++++++++++ .../ftxui/component/screen_interactive.hpp | 9 +- src/ftxui/component/event.cpp | 97 ++++++++++------- src/ftxui/component/pipe.hpp | 30 ++++++ src/ftxui/component/screen_interactive.cpp | 52 +++++---- 7 files changed, 244 insertions(+), 66 deletions(-) create mode 100644 include/ftxui/component/producer_consumer.hpp create mode 100644 src/ftxui/component/pipe.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2bbec9f..14c77f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,9 +18,17 @@ add_library(screen src/ftxui/screen/string.cpp src/ftxui/screen/terminal.cpp src/ftxui/screen/wcwidth.cpp + include/ftxui/screen/box.hpp + include/ftxui/screen/color.hpp + include/ftxui/screen/screen.hpp + include/ftxui/screen/string.hpp ) add_library(dom + include/ftxui/dom/elements.hpp + include/ftxui/dom/node.hpp + include/ftxui/dom/requirement.hpp + include/ftxui/dom/take_any_args.hpp src/ftxui/dom/blink.cpp src/ftxui/dom/bold.cpp src/ftxui/dom/border.cpp @@ -57,6 +65,16 @@ add_library(component src/ftxui/component/radiobox.cpp src/ftxui/component/screen_interactive.cpp src/ftxui/component/toggle.cpp + include/ftxui/component/checkbox.hpp + include/ftxui/component/component.hpp + include/ftxui/component/container.hpp + include/ftxui/component/event.hpp + include/ftxui/component/input.hpp + include/ftxui/component/menu.hpp + include/ftxui/component/radiobox.hpp + include/ftxui/component/screen_interactive.hpp + include/ftxui/component/producer_consumer.hpp + include/ftxui/component/toggle.hpp ) add_library(ftxui::screen ALIAS screen) diff --git a/include/ftxui/component/event.hpp b/include/ftxui/component/event.hpp index 3bb00a7..01c5e13 100644 --- a/include/ftxui/component/event.hpp +++ b/include/ftxui/component/event.hpp @@ -5,6 +5,7 @@ #include #include #include +#include namespace ftxui { @@ -20,7 +21,7 @@ struct Event { static Event Character(const std::string&); static Event Special(const std::string&); - static Event GetEvent(std::function getchar); + static void Convert(Consumer& in, Producer& out, char c); // --- Arrow --- static Event ArrowLeft; diff --git a/include/ftxui/component/producer_consumer.hpp b/include/ftxui/component/producer_consumer.hpp new file mode 100644 index 0000000..82d4f71 --- /dev/null +++ b/include/ftxui/component/producer_consumer.hpp @@ -0,0 +1,101 @@ +#ifndef FTXUI_COMPONENTS_CONSUMER_PRODUCER_H_ +#define FTXUI_COMPONENTS_CONSUMER_PRODUCER_H_ + +#include +#include +#include +#include +#include +#include + +namespace ftxui { + +// Usage: +// +// Initialization: +// --------------- +// +// auto consumer = MakeConsumer(); +// auto producer_1 = consumer.MakeProducer(); +// auto producer_2 = consumer.MakeProducer(); +// +// Then move one producers elsewhere, potentially in a different thread. +// ---------------------- +// [thread 1] producer_1->Send("hello"); +// [thread 2] producer_2->Send("world"); +// +// On the consumer side: +// --------------------- +// char c; +// while(consumer_->Receive(&c)) // Return true as long as there is a producer. +// print(c) +// +// Consumer::Receive returns true when the last Producer is released. + +// clang-format off +template class ProducerImpl; +template class ConsumerImpl; +template using Producer = std::unique_ptr>; +template using Consumer = std::unique_ptr>; +template Consumer MakeConsumer(); +// clang-format on + +// ---- Implementation part ---- + +template +class ProducerImpl { + public: + void Send(T t) { consumer_->Receive(std::move(t)); } + ~ProducerImpl() { consumer_->producers_--; } + + private: + friend class ConsumerImpl; + ProducerImpl(ConsumerImpl* consumer) : consumer_(consumer) {} + ConsumerImpl* consumer_; +}; + +template +class ConsumerImpl { + public: + Producer MakeProducer() { + producers_++; + return std::unique_ptr>(new ProducerImpl(this)); + } + + bool Receive(T* t) { + while (producers_) { + std::unique_lock lock(mutex_); + while (queue_.empty()) + notifier_.wait(lock); + if (queue_.empty()) + continue; + *t = std::move(queue_.front()); + queue_.pop(); + return true; + } + return false; + } + + private: + friend class ProducerImpl; + + void Receive(T t) { + std::unique_lock lock(mutex_); + queue_.push(std::move(t)); + notifier_.notify_one(); + } + + std::mutex mutex_; + std::queue queue_; + std::condition_variable notifier_; + std::atomic producers_ = 0; +}; + +template +Consumer MakeConsumer() { + return std::make_unique>(); +} + +} // namespace ftxui + +#endif // FTXUI_COMPONENTS_CONSUMER_PRODUCER_H_ diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index d335330..0dcb1d8 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -10,6 +10,7 @@ #include "ftxui/component/event.hpp" #include "ftxui/screen/screen.hpp" +#include namespace ftxui { class Component; @@ -40,13 +41,13 @@ class ScreenInteractive : public Screen { Dimension dimension_ = Dimension::Fixed; ScreenInteractive(int dimx, int dimy, Dimension dimension); - std::condition_variable events_queue_cv; - std::mutex events_queue_mutex; - std::queue events_queue; - std::atomic quit_ = false; + Producer event_producer_; + Consumer event_consumer_; std::string set_cursor_position; std::string reset_cursor_position; + + std::atomicquit_ = false; }; } // namespace ftxui diff --git a/src/ftxui/component/event.cpp b/src/ftxui/component/event.cpp index 4fc467e..b6c8d4a 100644 --- a/src/ftxui/component/event.cpp +++ b/src/ftxui/component/event.cpp @@ -36,19 +36,25 @@ Event Event::Special(const std::string& input) { return event; } -Event ParseUTF8(std::function& getchar, std::string& input) { - if ((input[0] & 0b11000000) == 0b11000000) - input += getchar(); - if ((input[0] & 0b11100000) == 0b11100000) - input += getchar(); - if ((input[0] & 0b11110000) == 0b11110000) - input += getchar(); - return Event::Character(input); +void ParseUTF8(Consumer& in, Producer& out, std::string& input) { + char c; + char mask = 0b11000000; + for (int i = 0; i < 3; ++i) { + if ((input[0] & mask) == mask) { + if (!in->Receive(&c)) + return; + input += c; + } + mask = mask >> 1 | 0b10000000; + } + out->Send(Event::Character(input)); } -Event ParseCSI(std::function getchar, std::string& input) { +void ParseCSI(Consumer& in, Producer& out, std::string& input) { + char c; while (1) { - char c = getchar(); + if (!in->Receive(&c)) + return; input += c; if (c >= '0' && c <= '9') @@ -58,80 +64,95 @@ Event ParseCSI(std::function getchar, std::string& input) { continue; if (c >= ' ' && c <= '~') - return Event::Special(input); + return out->Send(Event::Special(input)); // Invalid ESC in CSI. if (c == '\x1B') - return Event::Special(input); + return out->Send(Event::Special(input)); } } -Event ParseDCS(std::function getchar, std::string& input) { +void ParseDCS(Consumer& in, Producer& out, std::string& input) { + char c; // Parse until the string terminator ST. while (1) { - input += getchar(); + if (!in->Receive(&c)) + return; + input += c; if (input.back() != '\x1B') continue; - input += getchar(); + if (!in->Receive(&c)) + return; + input += c; if (input.back() != '\\') continue; - return Event::Special(input); + return out->Send(Event::Special(input)); } } -Event ParseOSC(std::function getchar, std::string& input) { +void ParseOSC(Consumer& in, Producer& out, std::string& input) { + char c; // Parse until the string terminator ST. while (1) { - input += getchar(); + if (!in->Receive(&c)) + return; + input += c; if (input.back() != '\x1B') continue; - input += getchar(); + if (!in->Receive(&c)) + return; + input += c; if (input.back() != '\\') continue; - return Event::Special(input); + return out->Send(Event::Special(input)); } } -Event ParseESC(std::function getchar, std::string& input) { - input += getchar(); - switch (input.back()) { +void ParseESC(Consumer& in, Producer& out, std::string& input) { + char c; + if (!in->Receive(&c)) + return; + input += c; + switch (c) { case 'P': - return ParseDCS(getchar, input); + return ParseDCS(in, out, input); case '[': - return ParseCSI(getchar, input); + return ParseCSI(in, out, input); case ']': - return ParseOSC(getchar, input); + return ParseOSC(in, out, input); default: - input += getchar(); - return Event::Special(input); + if (!in->Receive(&c)) + return; + input += c; + out->Send(Event::Special(input)); } } // static -Event Event::GetEvent(std::function getchar) { +void Event::Convert(Consumer& in, Producer& out, char c) { std::string input; - input += getchar(); + input += c; unsigned char head = input[0]; switch (head) { - case 24: // CAN - case 26: // SUB - return Event(); // Ignored. + case 24: // CAN + case 26: // SUB + return; case 'P': - return ParseDCS(getchar, input); + return ParseDCS(in, out, input); case '\x1B': - return ParseESC(getchar, input); + return ParseESC(in, out, input); } if (head < 32) // C0 - return Event::Special(input); + return out->Send(Event::Special(input)); if (head == 127) // Delete - return Event::Special(input); + return out->Send(Event::Special(input)); - return ParseUTF8(getchar, input); + return ParseUTF8(in, out, input); } // --- Arrow --- diff --git a/src/ftxui/component/pipe.hpp b/src/ftxui/component/pipe.hpp new file mode 100644 index 0000000..71bb653 --- /dev/null +++ b/src/ftxui/component/pipe.hpp @@ -0,0 +1,30 @@ +#ifndef FTXUI_COMPONENTS_TASK_QUEUE_H_ +#define FTXUI_COMPONENTS_TASK_QUEUE_H_ + +#include +#include +#include +#include +#include +#include + + + +template +class TaskQueue { + public: + void Post(T task); + void Close(); + bool Take(T& task); + private: + std::unique_lock lock(events_queue_mutex); + events_queue.push(event); + events_queue_cv.notify_one(); + + std::condition_variable events_queue_cv; + std::mutex events_queue_mutex; + std::queue events_queue; + std::atomic quit_ = false; +}; + +#endif // FTXUI_COMPONENTS_TASK_QUEUE_H_ diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index b62e718..2f4d375 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -57,7 +57,11 @@ void OnResize(int /* signal */) { } ScreenInteractive::ScreenInteractive(int dimx, int dimy, Dimension dimension) - : Screen(dimx, dimy), dimension_(dimension) {} + : Screen(dimx, dimy), dimension_(dimension) { + event_consumer_ = MakeConsumer(); + event_producer_ = event_consumer_->MakeProducer(); +} + ScreenInteractive::~ScreenInteractive() {} // static @@ -81,21 +85,7 @@ ScreenInteractive ScreenInteractive::FitComponent() { } void ScreenInteractive::PostEvent(Event event) { - std::unique_lock lock(events_queue_mutex); - events_queue.push(event); - events_queue_cv.notify_one(); -} - -void ScreenInteractive::EventLoop(Component* component) { - std::unique_lock lock(events_queue_mutex); - while (!quit_ && events_queue.empty()) - events_queue_cv.wait(lock); - - // After the wait, we own the lock. - while (!events_queue.empty()) { - component->OnEvent(events_queue.front()); - events_queue.pop(); - } + event_producer_->Send(event); } void ScreenInteractive::Loop(Component* component) { @@ -150,12 +140,25 @@ void ScreenInteractive::Loop(Component* component) { std::cout << std::endl; }); - // Spawn a thread. It will listen for new characters being typed. - std::thread read_char([this] { - while (!quit_) { - auto event = Event::GetEvent([] { return (char)getchar(); }); - PostEvent(std::move(event)); - } + auto char_consumer = MakeConsumer(); + + // Spawn a thread to produce char. + auto char_producer = char_consumer->MakeProducer(); + std::thread read_char([&] { + // TODO(arthursonzogni): Use a timeout so that it doesn't block even if the + // user doesn't generate new chars. + while (!quit_) + char_producer->Send((char)getchar()); + char_producer.reset(); + }); + + // Spawn a thread producing events and consumer chars. + auto event_producer = event_consumer_->MakeProducer(); + std::thread convert_char_to_event([&] { + char c; + while (char_consumer->Receive(&c)) + Event::Convert(char_consumer, event_producer, c); + event_producer.reset(); }); // The main loop. @@ -164,9 +167,12 @@ void ScreenInteractive::Loop(Component* component) { Draw(component); std::cout << ToString() << set_cursor_position << std::flush; Clear(); - EventLoop(component); + Event event; + if (event_consumer_->Receive(&event)) + component->OnEvent(event); } read_char.join(); + convert_char_to_event.join(); OnExit(0); }