Refactor FetchTerminalEvent

This commit is contained in:
ArthurSonzogni
2025-07-06 18:26:35 +02:00
parent 41116fe8bd
commit f017897b36
5 changed files with 109 additions and 55 deletions

View File

@@ -9,7 +9,6 @@
#include <functional> // for function #include <functional> // for function
#include <memory> // for shared_ptr #include <memory> // for shared_ptr
#include <string> // for string #include <string> // for string
#include <thread> // for thread
#include "ftxui/component/animation.hpp" // for TimePoint #include "ftxui/component/animation.hpp" // for TimePoint
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
@@ -104,6 +103,10 @@ class ScreenInteractive : public Screen {
void Signal(int signal); void Signal(int signal);
void FetchTerminalEvents();
void PostAnimationTask();
ScreenInteractive* suspended_screen_ = nullptr; ScreenInteractive* suspended_screen_ = nullptr;
enum class Dimension { enum class Dimension {
FitComponent, FitComponent,
@@ -127,8 +130,6 @@ class ScreenInteractive : public Screen {
std::string reset_cursor_position; std::string reset_cursor_position;
std::atomic<bool> quit_{false}; std::atomic<bool> quit_{false};
std::thread event_listener_;
std::thread animation_listener_;
bool animation_requested_ = false; bool animation_requested_ = false;
animation::TimePoint previous_animation_time_; animation::TimePoint previous_animation_time_;
@@ -163,10 +164,15 @@ class ScreenInteractive : public Screen {
std::unique_ptr<Selection> selection_; std::unique_ptr<Selection> selection_;
std::function<void()> selection_on_change_; std::function<void()> selection_on_change_;
std::unique_ptr<task::TaskRunner> task_runner_; // PIMPL private implementation idiom (Pimpl).
struct Internal;
std::unique_ptr<Internal> internal_;
friend class Loop; friend class Loop;
Component component_;
public: public:
class Private { class Private {
public: public:

View File

@@ -17,6 +17,7 @@
#include <memory> #include <memory>
#include <stack> // for stack #include <stack> // for stack
#include <string> #include <string>
#include <array>
#include <thread> // for thread, sleep_for #include <thread> // for thread, sleep_for
#include <tuple> // for _Swallow_assign, ignore #include <tuple> // for _Swallow_assign, ignore
#include <type_traits> // for decay_t #include <type_traits> // for decay_t
@@ -60,6 +61,20 @@
namespace ftxui { 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<std::chrono::steady_clock> last_char_time =
std::chrono::steady_clock::now();
explicit Internal(std::function<void(Event)> out)
: terminal_input_parser(std::move(out)) {}
};
namespace animation { namespace animation {
void RequestAnimationFrame() { void RequestAnimationFrame() {
auto* screen = ScreenInteractive::Active(); auto* screen = ScreenInteractive::Active();
@@ -174,25 +189,25 @@ int CheckStdinReady(int usec_timeout) {
return FD_ISSET(STDIN_FILENO, &fds); // NOLINT return FD_ISSET(STDIN_FILENO, &fds); // NOLINT
} }
// Read char from the terminal. // Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Task> out) { //void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto parser = //auto parser =
TerminalInputParser([&](Event event) { out->Send(std::move(event)); }); //TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
while (!*quit) { //while (!*quit) {
if (!CheckStdinReady(timeout_microseconds)) { //auto pending = ReadPendingChars();
parser.Timeout(timeout_milliseconds); //if (pending.empty()) {
continue; //parser.Timeout(timeout_milliseconds);
} //continue;
//}
//for (auto c : pending) {
//parser.Add(c);
//}
//}
//}
const size_t buffer_size = 100;
std::array<char, buffer_size> 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 #endif
std::stack<Closure> on_exit_functions; // NOLINT std::stack<Closure> on_exit_functions; // NOLINT
@@ -339,15 +354,6 @@ class CapturedMouseImpl : public CapturedMouseInterface {
std::function<void(void)> callback_; std::function<void(void)> callback_;
}; };
void AnimationListener(std::atomic<bool>* quit, Sender<Task> 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 } // namespace
ScreenInteractive::ScreenInteractive(Dimension dimension, ScreenInteractive::ScreenInteractive(Dimension dimension,
@@ -357,7 +363,8 @@ ScreenInteractive::ScreenInteractive(Dimension dimension,
: Screen(dimx, dimy), : Screen(dimx, dimy),
dimension_(dimension), dimension_(dimension),
use_alternative_screen_(use_alternative_screen) { use_alternative_screen_(use_alternative_screen) {
task_receiver_ = MakeReceiver<Task>(); internal_ = std::make_unique<Internal>(
[&](Event event) { PostEvent(std::move(event)); });
} }
// static // static
@@ -455,13 +462,9 @@ void ScreenInteractive::TrackMouse(bool enable) {
/// @brief Add a task to the main loop. /// @brief Add a task to the main loop.
/// It will be executed later, after every other scheduled tasks. /// It will be executed later, after every other scheduled tasks.
void ScreenInteractive::Post(Task task) { void ScreenInteractive::Post(Task task) {
// Task/Events sent toward inactive screen or screen waiting to become internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
// inactive are dropped. HandleTask(component_, task);
if (!task_sender_) { });
return;
}
task_sender_->Send(std::move(task));
} }
/// @brief Add an event to the main loop. /// @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. /// @brief Return whether the main loop has been quit.
bool ScreenInteractive::HasQuitted() { bool ScreenInteractive::HasQuitted() {
return task_receiver_->HasQuitted(); return quit_;
} }
// private // private
@@ -735,39 +738,41 @@ void ScreenInteractive::Install() {
Flush(); Flush();
quit_ = false; quit_ = false;
task_sender_ = task_receiver_->MakeSender();
event_listener_ = PostAnimationTask();
std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
animation_listener_ =
std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
} }
// private // private
void ScreenInteractive::Uninstall() { void ScreenInteractive::Uninstall() {
ExitNow(); ExitNow();
event_listener_.join();
animation_listener_.join();
OnExit(); OnExit();
} }
// private // private
// NOLINTNEXTLINE // NOLINTNEXTLINE
void ScreenInteractive::RunOnceBlocking(Component component) { void ScreenInteractive::RunOnceBlocking(Component component) {
ExecuteSignalHandlers(); size_t executed_task = internal_->task_runner.ExecutedTasks();
Task task; while(executed_task == internal_->task_runner.ExecutedTasks() &&
if (task_receiver_->Receive(&task)) { !HasQuitted()) {
HandleTask(component, task); RunOnce(component);
} }
RunOnce(component);
} }
// private // private
void ScreenInteractive::RunOnce(Component component) { void ScreenInteractive::RunOnce(Component component) {
Task task; component_ = component;
while (task_receiver_->ReceiveNonBlocking(&task)) { ExecuteSignalHandlers();
HandleTask(component, task); FetchTerminalEvents();
ExecuteSignalHandlers();
// 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)); Draw(std::move(component));
if (selection_data_previous_ != selection_data_) { if (selection_data_previous_ != selection_data_) {
@@ -777,6 +782,8 @@ void ScreenInteractive::RunOnce(Component component) {
Post(Event::Custom); Post(Event::Custom);
} }
} }
component_.reset();
} }
// private // private
@@ -1040,7 +1047,6 @@ void ScreenInteractive::Exit() {
// private: // private:
void ScreenInteractive::ExitNow() { void ScreenInteractive::ExitNow() {
quit_ = true; quit_ = true;
task_sender_.reset();
} }
// private: // private:
@@ -1073,6 +1079,36 @@ void ScreenInteractive::Signal(int signal) {
#endif #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<std::chrono::milliseconds>(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<char, 128> 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==( bool ScreenInteractive::SelectionData::operator==(
const ScreenInteractive::SelectionData& other) const { const ScreenInteractive::SelectionData& other) const {
if (empty && other.empty) { if (empty && other.empty) {

View File

@@ -25,6 +25,10 @@ struct TaskQueue {
std::variant<Task, std::chrono::steady_clock::duration, std::monostate>; std::variant<Task, std::chrono::steady_clock::duration, std::monostate>;
auto Get() -> MaybeTask; auto Get() -> MaybeTask;
bool HasImmediateTasks() const {
return !immediate_tasks_.empty();
}
private: private:
std::queue<PendingTask> immediate_tasks_; std::queue<PendingTask> immediate_tasks_;
std::priority_queue<PendingTask> delayed_tasks_; std::priority_queue<PendingTask> delayed_tasks_;

View File

@@ -48,6 +48,7 @@ auto TaskRunner::RunUntilIdle()
} }
if (std::holds_alternative<Task>(maybe_task)) { if (std::holds_alternative<Task>(maybe_task)) {
executed_tasks_++;
std::get<Task>(maybe_task)(); std::get<Task>(maybe_task)();
continue; continue;
} }

View File

@@ -31,9 +31,16 @@ class TaskRunner {
// Runs the tasks in the queue, blocking until all tasks are executed. // Runs the tasks in the queue, blocking until all tasks are executed.
auto Run() -> void; auto Run() -> void;
bool HasImmediateTasks() const {
return queue_.HasImmediateTasks();
}
size_t ExecutedTasks() const { return executed_tasks_; }
private: private:
TaskRunner* previous_task_runner_ = nullptr; TaskRunner* previous_task_runner_ = nullptr;
TaskQueue queue_; TaskQueue queue_;
size_t executed_tasks_ = 0;
}; };
} // namespace ftxui::task } // namespace ftxui::task