From 1b479ee12d3ed7a3a08a6bfed5a9412e8a8e703a Mon Sep 17 00:00:00 2001 From: ArthurSonzogni Date: Wed, 2 Jul 2025 16:53:48 +0200 Subject: [PATCH] Add a task system. --- CMakeLists.txt | 15 +++--- cmake/ftxui_test.cmake | 3 +- src/ftxui/core/task.cpp | 20 +++++++ src/ftxui/core/task.hpp | 42 +++++++++++++++ src/ftxui/core/task_queue.cpp | 54 +++++++++++++++++++ src/ftxui/core/task_queue.hpp | 36 +++++++++++++ src/ftxui/core/task_runner.cpp | 76 +++++++++++++++++++++++++++ src/ftxui/core/task_runner.hpp | 42 +++++++++++++++ src/ftxui/core/task_test.cpp | 95 ++++++++++++++++++++++++++++++++++ 9 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 src/ftxui/core/task.cpp create mode 100644 src/ftxui/core/task.hpp create mode 100644 src/ftxui/core/task_queue.cpp create mode 100644 src/ftxui/core/task_queue.hpp create mode 100644 src/ftxui/core/task_runner.cpp create mode 100644 src/ftxui/core/task_runner.hpp create mode 100644 src/ftxui/core/task_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d871e9a37..7424a40a4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -148,15 +148,16 @@ add_library(component src/ftxui/component/terminal_input_parser.hpp src/ftxui/component/util.cpp src/ftxui/component/window.cpp + src/ftxui/core/task.cpp + src/ftxui/core/task.hpp + src/ftxui/core/task_queue.cpp + src/ftxui/core/task_queue.hpp + src/ftxui/core/task_runner.cpp + src/ftxui/core/task_runner.hpp ) -target_link_libraries(dom - PUBLIC screen -) - -target_link_libraries(component - PUBLIC dom -) +target_link_libraries(dom PUBLIC screen) +target_link_libraries(component PUBLIC dom) if (NOT EMSCRIPTEN) find_package(Threads) diff --git a/cmake/ftxui_test.cmake b/cmake/ftxui_test.cmake index 665b7444b..f92a222b2 100644 --- a/cmake/ftxui_test.cmake +++ b/cmake/ftxui_test.cmake @@ -19,13 +19,13 @@ add_executable(ftxui-tests src/ftxui/component/menu_test.cpp src/ftxui/component/modal_test.cpp src/ftxui/component/radiobox_test.cpp - src/ftxui/util/ref_test.cpp src/ftxui/component/receiver_test.cpp src/ftxui/component/resizable_split_test.cpp src/ftxui/component/screen_interactive_test.cpp src/ftxui/component/slider_test.cpp src/ftxui/component/terminal_input_parser_test.cpp src/ftxui/component/toggle_test.cpp + src/ftxui/core/task_test.cpp src/ftxui/dom/blink_test.cpp src/ftxui/dom/bold_test.cpp src/ftxui/dom/border_test.cpp @@ -51,6 +51,7 @@ add_executable(ftxui-tests src/ftxui/dom/vbox_test.cpp src/ftxui/screen/color_test.cpp src/ftxui/screen/string_test.cpp + src/ftxui/util/ref_test.cpp ) target_link_libraries(ftxui-tests diff --git a/src/ftxui/core/task.cpp b/src/ftxui/core/task.cpp new file mode 100644 index 000000000..5d1e15fae --- /dev/null +++ b/src/ftxui/core/task.cpp @@ -0,0 +1,20 @@ +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +#include "task.hpp" + +namespace ftxui::task { +bool PendingTask::operator<(const PendingTask& other) const { + if (!time && !other.time) { + return false; + } + if (!time) { + return true; + } + if (!other.time) { + return false; + } + return time.value() > other.time.value(); +} +} // namespace ftxui::task + diff --git a/src/ftxui/core/task.hpp b/src/ftxui/core/task.hpp new file mode 100644 index 000000000..dd801d7a6 --- /dev/null +++ b/src/ftxui/core/task.hpp @@ -0,0 +1,42 @@ +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +#ifndef TASK_HPP +#define TASK_HPP + +#include +#include +#include + +namespace ftxui::task { + +/// A task represents a unit of work. +using Task = std::function; + +/// A PendingTask represents a task that is scheduled to be executed at a +/// specific time, or as soon as possible. +struct PendingTask { + // Immediate task: + PendingTask(Task task) : task(std::move(task)) {} // NOLINT + + // Delayed task with a duration + PendingTask(Task task, std::chrono::steady_clock::duration duration) + : task(std::move(task)), + time(std::chrono::steady_clock::now() + duration) {} + + /// The task to be executed. + Task task; + + /// The time when the task should be executed. If the time is empty, the task + /// should be executed as soon as possible. + std::optional time; + + /// Compare two PendingTasks by their time. + /// If both tasks have no time, they are considered equal. + bool operator<(const PendingTask& other) const; +}; + +} // namespace ftxui::task + + +#endif // TASK_HPP_ diff --git a/src/ftxui/core/task_queue.cpp b/src/ftxui/core/task_queue.cpp new file mode 100644 index 000000000..b69f24fca --- /dev/null +++ b/src/ftxui/core/task_queue.cpp @@ -0,0 +1,54 @@ +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +#include "task_queue.hpp" + +namespace ftxui::task { + +auto TaskQueue::PostTask(PendingTask task) -> void { + if (!task.time) { + immediate_tasks_.push(task); + return; + } + + if (task.time.value() < std::chrono::steady_clock::now()) { + immediate_tasks_.push(task); + return; + } + + delayed_tasks_.push(task); +} + +auto TaskQueue::Get() -> MaybeTask { + // Attempt to execute a task immediately. + if (!immediate_tasks_.empty()) { + auto task = immediate_tasks_.front(); + immediate_tasks_.pop(); + return task.task; + } + + // Move all tasks that can be executed to the immediate queue. + auto now = std::chrono::steady_clock::now(); + while (!delayed_tasks_.empty() && delayed_tasks_.top().time.value() <= now) { + immediate_tasks_.push(delayed_tasks_.top()); + delayed_tasks_.pop(); + } + + // Attempt to execute a task immediately. + if (!immediate_tasks_.empty()) { + auto task = immediate_tasks_.front(); + immediate_tasks_.pop(); + return task.task; + } + + // If there are no tasks to execute, return the delay until the next task. + if (!delayed_tasks_.empty()) { + return delayed_tasks_.top().time.value() - now; + } + + // If there are no tasks to execute, return the maximum duration. + return std::monostate{}; +} + +} // namespace ftxui::task + diff --git a/src/ftxui/core/task_queue.hpp b/src/ftxui/core/task_queue.hpp new file mode 100644 index 000000000..121f5f08b --- /dev/null +++ b/src/ftxui/core/task_queue.hpp @@ -0,0 +1,36 @@ +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +#ifndef TASK_QUEUE_HPP +#define TASK_QUEUE_HPP + +#include +#include + +#include "task.hpp" + +namespace ftxui::task { + +/// A task queue that schedules tasks to be executed in the future. Tasks can be +/// scheduled to be executed immediately, or after a certain duration. +/// - The tasks are executed in the order they were scheduled. +/// - If multiple tasks are scheduled to be executed at the same time, they are +/// executed in the order they were scheduled. +/// - If a task is scheduled to be executed in the past, it is executed +/// immediately. +struct TaskQueue { + auto PostTask(PendingTask task) -> void; + + using MaybeTask = + std::variant; + auto Get() -> MaybeTask; + + private: + std::queue immediate_tasks_; + std::priority_queue delayed_tasks_; +}; + +} // namespace ftxui::task + + +#endif diff --git a/src/ftxui/core/task_runner.cpp b/src/ftxui/core/task_runner.cpp new file mode 100644 index 000000000..9b99dfe17 --- /dev/null +++ b/src/ftxui/core/task_runner.cpp @@ -0,0 +1,76 @@ +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +#include "task_runner.hpp" + +#include +#include + +namespace ftxui::task { + +static thread_local TaskRunner* current_task_runner = nullptr; // NOLINT + +TaskRunner::TaskRunner() { + assert(!previous_task_runner_); + previous_task_runner_ = current_task_runner; + current_task_runner = this; +} + +TaskRunner::~TaskRunner() { + current_task_runner = previous_task_runner_; +} + + +// static +auto TaskRunner::Current() -> TaskRunner* { + assert(current_task_runner); + return current_task_runner; +} + +auto TaskRunner::PostTask(Task task) -> void { + queue_.PostTask(PendingTask{std::move(task)}); +} + +auto TaskRunner::PostDelayedTask(Task task, + std::chrono::steady_clock::duration duration) + -> void { + queue_.PostTask(PendingTask{std::move(task), duration}); +} + +/// Runs the tasks in the queue. +auto TaskRunner::RunUntilIdle() + -> std::optional { + while (true) { + auto maybe_task = queue_.Get(); + if (std::holds_alternative(maybe_task)) { + // No more tasks to execute, exit the loop. + return std::nullopt; + } + + if (std::holds_alternative(maybe_task)) { + std::get(maybe_task)(); + continue; + } + + if (std::holds_alternative( + maybe_task)) { + return std::get(maybe_task); + } + } +} + +auto TaskRunner::Run() -> void { + while (true) { + auto duration = RunUntilIdle(); + if (!duration) { + // No more tasks to execute, exit the loop. + return; + } + + // Sleep for the duration until the next task can be executed. + std::this_thread::sleep_for(duration.value()); + } +} + +} // namespace ftxui::task + diff --git a/src/ftxui/core/task_runner.hpp b/src/ftxui/core/task_runner.hpp new file mode 100644 index 000000000..867bd25f0 --- /dev/null +++ b/src/ftxui/core/task_runner.hpp @@ -0,0 +1,42 @@ +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +#ifndef TASK_RUNNER_HPP +#define TASK_RUNNER_HPP + +#include "task.hpp" +#include "task_queue.hpp" + +namespace ftxui::task { + +class TaskRunner { + public: + TaskRunner(); + ~TaskRunner(); + + // Returns the task runner for the current thread. + static auto Current() -> TaskRunner*; + + /// Schedules a task to be executed immediately. + auto PostTask(Task task) -> void; + + /// Schedules a task to be executed after a certain duration. + auto PostDelayedTask(Task task, + std::chrono::steady_clock::duration duration) -> void; + + /// Runs the tasks in the queue, return the delay until the next delayed task + /// can be executed. + auto RunUntilIdle() -> std::optional; + + // Runs the tasks in the queue, blocking until all tasks are executed. + auto Run() -> void; + + private: + TaskRunner* previous_task_runner_ = nullptr; + TaskQueue queue_; +}; + +} // namespace ftxui::task + + +#endif // TASK_RUNNER_HPP diff --git a/src/ftxui/core/task_test.cpp b/src/ftxui/core/task_test.cpp new file mode 100644 index 000000000..1f1ca3a6b --- /dev/null +++ b/src/ftxui/core/task_test.cpp @@ -0,0 +1,95 @@ +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +// Copyright 2024 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. +#include "task.hpp" + +#include + +#include "task_runner.hpp" + +namespace ftxui::task { + +TEST(TaskTest, Basic) { + std::vector values; + + auto task_1 = [&values] { values.push_back(1); }; + auto task_2 = [&values] { values.push_back(2); }; + auto task_3 = [&values] { values.push_back(3); }; + + auto runner = TaskRunner(); + + runner.PostTask(task_1); + runner.PostTask(task_2); + runner.PostTask(task_3); + while (true) { + auto duration = runner.RunUntilIdle(); + if (!duration) { + break; + } + std::this_thread::sleep_for(duration.value()); + } + + EXPECT_EQ(values, (std::vector{1, 2, 3})); +} + +TEST(TaskTest, PostedWithinTask) { + std::vector values; + + auto task_1 = [&values] { + values.push_back(1); + auto task_2 = [&values] { values.push_back(5); }; + TaskRunner::Current()->PostTask(std::move(task_2)); + values.push_back(2); + }; + + auto task_2 = [&values] { + values.push_back(3); + auto task_2 = [&values] { values.push_back(6); }; + TaskRunner::Current()->PostTask(std::move(task_2)); + values.push_back(4); + }; + + auto runner = TaskRunner(); + + runner.PostTask(task_1); + runner.PostTask(task_2); + while (true) { + auto duration = runner.RunUntilIdle(); + if (!duration) { + break; + } + std::this_thread::sleep_for(duration.value()); + } + + EXPECT_EQ(values, (std::vector{1, 2, 3, 4, 5, 6})); +} + +TEST(TaskTest, RunDelayedTask) { + std::vector values; + + auto task_1 = [&values] { values.push_back(1); }; + auto task_2 = [&values] { values.push_back(2); }; + auto task_3 = [&values] { values.push_back(3); }; + + auto runner = TaskRunner(); + + runner.PostDelayedTask(task_3, std::chrono::milliseconds(300)); + runner.PostDelayedTask(task_1, std::chrono::milliseconds(100)); + runner.PostDelayedTask(task_2, std::chrono::milliseconds(200)); + while (true) { + auto duration = runner.RunUntilIdle(); + if (!duration) { + break; + } + std::this_thread::sleep_for(duration.value()); + } + + EXPECT_EQ(values, (std::vector{1, 2, 3})); +} + +} // namespace ftxui::task + +