From 0cf6f93f77f4d7deedc15ae1c2db7a05e6f09293 Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Sun, 6 Jul 2025 18:26:35 +0200 Subject: [PATCH] Refactor FetchTerminalEvent --- .../ftxui/component/screen_interactive.hpp | 14 +- src/ftxui/component/screen_interactive.cpp | 138 +++++++++++------- src/ftxui/core/task_queue.hpp | 4 + src/ftxui/core/task_runner.cpp | 1 + src/ftxui/core/task_runner.hpp | 7 + 5 files changed, 109 insertions(+), 55 deletions(-) diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index 63c8d9684..039f036d9 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -9,7 +9,6 @@ #include // for function #include // for shared_ptr #include // for string -#include // for thread #include "ftxui/component/animation.hpp" // for TimePoint #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse @@ -104,6 +103,10 @@ class ScreenInteractive : public Screen { void Signal(int signal); + void FetchTerminalEvents(); + + void PostAnimationTask(); + ScreenInteractive* suspended_screen_ = nullptr; enum class Dimension { FitComponent, @@ -127,8 +130,6 @@ class ScreenInteractive : public Screen { std::string reset_cursor_position; std::atomic quit_{false}; - std::thread event_listener_; - std::thread animation_listener_; bool animation_requested_ = false; animation::TimePoint previous_animation_time_; @@ -163,10 +164,15 @@ class ScreenInteractive : public Screen { std::unique_ptr selection_; std::function selection_on_change_; - std::unique_ptr task_runner_; + // PIMPL private implementation idiom (Pimpl). + struct Internal; + std::unique_ptr internal_; friend class Loop; + Component component_; + + public: class Private { public: diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 54c8d091a..979d37727 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -17,6 +17,7 @@ #include #include // for stack #include +#include #include // for thread, sleep_for #include // for _Swallow_assign, ignore #include // for decay_t @@ -60,6 +61,20 @@ namespace ftxui { +struct ScreenInteractive::Internal { + // Convert char to Event. + TerminalInputParser terminal_input_parser; + + task::TaskRunner task_runner; + + // The last time a character was received. + std::chrono::time_point last_char_time = + std::chrono::steady_clock::now(); + + explicit Internal(std::function out) + : terminal_input_parser(std::move(out)) {} +}; + namespace animation { void RequestAnimationFrame() { auto* screen = ScreenInteractive::Active(); @@ -174,25 +189,25 @@ int CheckStdinReady(int usec_timeout) { return FD_ISSET(STDIN_FILENO, &fds); // NOLINT } + // Read char from the terminal. -void EventListener(std::atomic* quit, Sender out) { - auto parser = - TerminalInputParser([&](Event event) { out->Send(std::move(event)); }); +//void EventListener(std::atomic* quit, Sender out) { + //auto parser = + //TerminalInputParser([&](Event event) { out->Send(std::move(event)); }); - while (!*quit) { - if (!CheckStdinReady(timeout_microseconds)) { - parser.Timeout(timeout_milliseconds); - continue; - } + //while (!*quit) { + //auto pending = ReadPendingChars(); + //if (pending.empty()) { + //parser.Timeout(timeout_milliseconds); + //continue; + //} + + //for (auto c : pending) { + //parser.Add(c); + //} + //} +//} - const size_t buffer_size = 100; - std::array buffer; // NOLINT; - size_t l = read(fileno(stdin), buffer.data(), buffer_size); // NOLINT - for (size_t i = 0; i < l; ++i) { - parser.Add(buffer[i]); // NOLINT - } - } -} #endif std::stack on_exit_functions; // NOLINT @@ -339,15 +354,6 @@ class CapturedMouseImpl : public CapturedMouseInterface { std::function callback_; }; -void AnimationListener(std::atomic* quit, Sender out) { - // Animation at around 60fps. - const auto time_delta = std::chrono::milliseconds(15); - while (!*quit) { - out->Send(AnimationTask()); - std::this_thread::sleep_for(time_delta); - } -} - } // namespace ScreenInteractive::ScreenInteractive(Dimension dimension, @@ -357,7 +363,8 @@ ScreenInteractive::ScreenInteractive(Dimension dimension, : Screen(dimx, dimy), dimension_(dimension), use_alternative_screen_(use_alternative_screen) { - task_receiver_ = MakeReceiver(); + internal_ = std::make_unique( + [&](Event event) { PostEvent(std::move(event)); }); } // static @@ -455,13 +462,9 @@ void ScreenInteractive::TrackMouse(bool enable) { /// @brief Add a task to the main loop. /// It will be executed later, after every other scheduled tasks. void ScreenInteractive::Post(Task task) { - // Task/Events sent toward inactive screen or screen waiting to become - // inactive are dropped. - if (!task_sender_) { - return; - } - - task_sender_->Send(std::move(task)); + internal_->task_runner.PostTask([this, task = std::move(task)]() mutable { + HandleTask(component_, task); + }); } /// @brief Add an event to the main loop. @@ -505,7 +508,7 @@ void ScreenInteractive::Loop(Component component) { // NOLINT /// @brief Return whether the main loop has been quit. bool ScreenInteractive::HasQuitted() { - return task_receiver_->HasQuitted(); + return quit_; } // private @@ -735,39 +738,41 @@ void ScreenInteractive::Install() { Flush(); quit_ = false; - task_sender_ = task_receiver_->MakeSender(); - event_listener_ = - std::thread(&EventListener, &quit_, task_receiver_->MakeSender()); - animation_listener_ = - std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender()); + + PostAnimationTask(); } // private void ScreenInteractive::Uninstall() { ExitNow(); - event_listener_.join(); - animation_listener_.join(); OnExit(); } // private // NOLINTNEXTLINE void ScreenInteractive::RunOnceBlocking(Component component) { - ExecuteSignalHandlers(); - Task task; - if (task_receiver_->Receive(&task)) { - HandleTask(component, task); + size_t executed_task = internal_->task_runner.ExecutedTasks(); + while(executed_task == internal_->task_runner.ExecutedTasks() && + !HasQuitted()) { + RunOnce(component); } - RunOnce(component); } // private void ScreenInteractive::RunOnce(Component component) { - Task task; - while (task_receiver_->ReceiveNonBlocking(&task)) { - HandleTask(component, task); - ExecuteSignalHandlers(); + component_ = component; + ExecuteSignalHandlers(); + FetchTerminalEvents(); + + // Execute the pending tasks from the queue. + const size_t executed_task = internal_->task_runner.ExecutedTasks(); + internal_->task_runner.RunUntilIdle(); + // If no executed task, we can return early without redrawing the screen. + if (executed_task == internal_->task_runner.ExecutedTasks()) { + return; } + + ExecuteSignalHandlers(); Draw(std::move(component)); if (selection_data_previous_ != selection_data_) { @@ -777,6 +782,8 @@ void ScreenInteractive::RunOnce(Component component) { Post(Event::Custom); } } + + component_.reset(); } // private @@ -1040,7 +1047,6 @@ void ScreenInteractive::Exit() { // private: void ScreenInteractive::ExitNow() { quit_ = true; - task_sender_.reset(); } // private: @@ -1073,6 +1079,36 @@ void ScreenInteractive::Signal(int signal) { #endif } +void ScreenInteractive::FetchTerminalEvents() { + if (!CheckStdinReady(timeout_microseconds)) { + const auto timeout = + std::chrono::steady_clock::now() - internal_->last_char_time; + const size_t timeout_ms = + std::chrono::duration_cast(timeout).count(); + internal_->terminal_input_parser.Timeout(timeout_ms); + return; + } + internal_->last_char_time = std::chrono::steady_clock::now(); + + // Read chars from the terminal. + std::array out; + size_t l = read(fileno(stdin), out.data(), out.size()); + + // Convert the chars to events. + for(size_t i = 0; i < l; ++i) { + internal_->terminal_input_parser.Add(out[i]); + } +} + +void ScreenInteractive::PostAnimationTask() { + Post(AnimationTask()); + + // Repeat the animation task every 15ms. This correspond to a frame rate + // of around 66fps. + internal_->task_runner.PostDelayedTask( + [this] { PostAnimationTask(); }, std::chrono::milliseconds(15)); +} + bool ScreenInteractive::SelectionData::operator==( const ScreenInteractive::SelectionData& other) const { if (empty && other.empty) { diff --git a/src/ftxui/core/task_queue.hpp b/src/ftxui/core/task_queue.hpp index 121f5f08b..cb0b92c21 100644 --- a/src/ftxui/core/task_queue.hpp +++ b/src/ftxui/core/task_queue.hpp @@ -25,6 +25,10 @@ struct TaskQueue { std::variant; auto Get() -> MaybeTask; + bool HasImmediateTasks() const { + return !immediate_tasks_.empty(); + } + private: std::queue immediate_tasks_; std::priority_queue delayed_tasks_; diff --git a/src/ftxui/core/task_runner.cpp b/src/ftxui/core/task_runner.cpp index 9b99dfe17..4face701d 100644 --- a/src/ftxui/core/task_runner.cpp +++ b/src/ftxui/core/task_runner.cpp @@ -48,6 +48,7 @@ auto TaskRunner::RunUntilIdle() } if (std::holds_alternative(maybe_task)) { + executed_tasks_++; std::get(maybe_task)(); continue; } diff --git a/src/ftxui/core/task_runner.hpp b/src/ftxui/core/task_runner.hpp index 867bd25f0..32c827a5f 100644 --- a/src/ftxui/core/task_runner.hpp +++ b/src/ftxui/core/task_runner.hpp @@ -31,9 +31,16 @@ class TaskRunner { // Runs the tasks in the queue, blocking until all tasks are executed. auto Run() -> void; + bool HasImmediateTasks() const { + return queue_.HasImmediateTasks(); + } + + size_t ExecutedTasks() const { return executed_tasks_; } + private: TaskRunner* previous_task_runner_ = nullptr; TaskQueue queue_; + size_t executed_tasks_ = 0; }; } // namespace ftxui::task