From 8ef18ab647564a1931f000894c48df10d724cbe1 Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni Date: Sat, 16 Aug 2025 18:40:50 +0200 Subject: [PATCH] Remove pthread dependency --- .bazelrc | 2 + BUILD.bazel | 10 +- CHANGELOG.md | 1 + CMakeLists.txt | 22 +- bazel/ftxui.bzl | 10 - cmake/ftxui_set_options.cmake | 1 - cmake/ftxui_test.cmake | 4 +- examples/CMakeLists.txt | 4 - examples/component/homescreen.cpp | 33 +- examples/index.mjs | 7 +- include/ftxui/component/receiver.hpp | 9 + .../ftxui/component/screen_interactive.hpp | 25 +- include/ftxui/screen/image.hpp | 3 + include/ftxui/screen/screen.hpp | 4 +- src/ftxui/component/receiver.cppm | 6 + src/ftxui/component/receiver_test.cpp | 81 ----- src/ftxui/component/screen_interactive.cpp | 321 ++++++++++-------- src/ftxui/component/task.cpp | 20 ++ src/ftxui/component/task_internal.hpp | 42 +++ src/ftxui/component/task_queue.cpp | 54 +++ src/ftxui/component/task_queue.hpp | 40 +++ src/ftxui/component/task_runner.cpp | 77 +++++ src/ftxui/component/task_runner.hpp | 49 +++ src/ftxui/component/task_test.cpp | 94 +++++ 24 files changed, 640 insertions(+), 279 deletions(-) delete mode 100644 src/ftxui/component/receiver_test.cpp create mode 100644 src/ftxui/component/task.cpp create mode 100644 src/ftxui/component/task_internal.hpp create mode 100644 src/ftxui/component/task_queue.cpp create mode 100644 src/ftxui/component/task_queue.hpp create mode 100644 src/ftxui/component/task_runner.cpp create mode 100644 src/ftxui/component/task_runner.hpp create mode 100644 src/ftxui/component/task_test.cpp diff --git a/.bazelrc b/.bazelrc index bd86ad56..240fd4c8 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,3 +1,5 @@ +common --enable_bzlmod + build --features=layering_check build --enable_bzlmod diff --git a/BUILD.bazel b/BUILD.bazel index 5dfb8c13..41262ad6 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -13,7 +13,6 @@ load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library", "cc_test") load(":bazel/ftxui.bzl", "ftxui_cc_library") load(":bazel/ftxui.bzl", "generate_examples") load(":bazel/ftxui.bzl", "windows_copts") -load(":bazel/ftxui.bzl", "pthread_linkopts") # A meta target depending on all of the ftxui submodules. # Note that component depends on dom and screen, so ftxui is just an alias for @@ -159,11 +158,18 @@ ftxui_cc_library( "src/ftxui/component/resizable_split.cpp", "src/ftxui/component/screen_interactive.cpp", "src/ftxui/component/slider.cpp", + "src/ftxui/component/task.cpp", + "src/ftxui/component/task_internal.hpp", + "src/ftxui/component/task_queue.cpp", + "src/ftxui/component/task_queue.hpp", + "src/ftxui/component/task_runner.cpp", + "src/ftxui/component/task_runner.hpp", "src/ftxui/component/terminal_input_parser.cpp", "src/ftxui/component/terminal_input_parser.hpp", "src/ftxui/component/util.cpp", "src/ftxui/component/window.cpp", + # Private header from ftxui:dom. "src/ftxui/dom/node_decorator.hpp", @@ -184,7 +190,6 @@ ftxui_cc_library( "include/ftxui/component/screen_interactive.hpp", "include/ftxui/component/task.hpp", ], - linkopts = pthread_linkopts(), deps = [ ":dom", ":screen", @@ -207,7 +212,6 @@ cc_test( "src/ftxui/component/menu_test.cpp", "src/ftxui/component/modal_test.cpp", "src/ftxui/component/radiobox_test.cpp", - "src/ftxui/component/receiver_test.cpp", "src/ftxui/component/resizable_split_test.cpp", "src/ftxui/component/slider_test.cpp", "src/ftxui/component/terminal_input_parser_test.cpp", diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cd44cd8..7a1c62b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Next import ftxui.util; ``` Thanks @mikomikotaishi for PR #1015. +- Remove dependency on 'pthread'. ### Component - Fix ScreenInteractive::FixedSize screen stomps on the preceding terminal diff --git a/CMakeLists.txt b/CMakeLists.txt index d871e9a3..6d8115d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,26 +144,20 @@ add_library(component src/ftxui/component/resizable_split.cpp src/ftxui/component/screen_interactive.cpp src/ftxui/component/slider.cpp + src/ftxui/component/task.cpp + src/ftxui/component/task_internal.hpp + src/ftxui/component/task_queue.cpp + src/ftxui/component/task_queue.hpp + src/ftxui/component/task_runner.cpp + src/ftxui/component/task_runner.hpp src/ftxui/component/terminal_input_parser.cpp src/ftxui/component/terminal_input_parser.hpp src/ftxui/component/util.cpp src/ftxui/component/window.cpp ) -target_link_libraries(dom - PUBLIC screen -) - -target_link_libraries(component - PUBLIC dom -) - -if (NOT EMSCRIPTEN) - find_package(Threads) - target_link_libraries(component - PUBLIC Threads::Threads - ) -endif() +target_link_libraries(dom PUBLIC screen) +target_link_libraries(component PUBLIC dom) include(cmake/ftxui_set_options.cmake) ftxui_set_options(screen) diff --git a/bazel/ftxui.bzl b/bazel/ftxui.bzl index e0de2e81..2be26a5e 100644 --- a/bazel/ftxui.bzl +++ b/bazel/ftxui.bzl @@ -43,16 +43,6 @@ def windows_copts(): "//conditions:default": [], }) -def pthread_linkopts(): - return select({ - # With MSVC, threading is already built-in (you don't need -pthread. - "@rules_cc//cc/compiler:msvc-cl": [], - "@rules_cc//cc/compiler:clang-cl": [], - "@rules_cc//cc/compiler:clang": ["-pthread"], - "@rules_cc//cc/compiler:gcc": ["-pthread"], - "//conditions:default": ["-pthread"], - }) - def ftxui_cc_library( name, srcs = [], diff --git a/cmake/ftxui_set_options.cmake b/cmake/ftxui_set_options.cmake index 185739e4..38a80ee9 100644 --- a/cmake/ftxui_set_options.cmake +++ b/cmake/ftxui_set_options.cmake @@ -101,6 +101,5 @@ endfunction() if (EMSCRIPTEN) string(APPEND CMAKE_CXX_FLAGS " -s USE_PTHREADS") - string(APPEND CMAKE_EXE_LINKER_FLAGS " -s ASYNCIFY") string(APPEND CMAKE_EXE_LINKER_FLAGS " -s PROXY_TO_PTHREAD") endif() diff --git a/cmake/ftxui_test.cmake b/cmake/ftxui_test.cmake index 665b7444..9f57416f 100644 --- a/cmake/ftxui_test.cmake +++ b/cmake/ftxui_test.cmake @@ -19,11 +19,10 @@ 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/task_test.cpp src/ftxui/component/terminal_input_parser_test.cpp src/ftxui/component/toggle_test.cpp src/ftxui/dom/blink_test.cpp @@ -51,6 +50,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/examples/CMakeLists.txt b/examples/CMakeLists.txt index 953efa98..0a49fe92 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -15,15 +15,11 @@ add_subdirectory(component) add_subdirectory(dom) if (EMSCRIPTEN) - string(APPEND CMAKE_EXE_LINKER_FLAGS " -s ALLOW_MEMORY_GROWTH=1") - target_link_options(component PUBLIC "SHELL: -s ALLOW_MEMORY_GROWTH=1") - get_property(EXAMPLES GLOBAL PROPERTY FTXUI::EXAMPLES) foreach(file "index.html" "index.mjs" "index.css" - "sw.js" "run_webassembly.py") configure_file(${file} ${file}) endforeach(file) diff --git a/examples/component/homescreen.cpp b/examples/component/homescreen.cpp index 7a5e710c..16edec50 100644 --- a/examples/component/homescreen.cpp +++ b/examples/component/homescreen.cpp @@ -6,6 +6,7 @@ #include // for atomic #include // for operator""s, chrono_literals #include // for sin +#include #include // for ref, reference_wrapper, function #include // for allocator, shared_ptr, __shared_ptr_access #include // for string, basic_string, char_traits, operator+, to_string @@ -269,7 +270,7 @@ int main() { auto spinner_tab_renderer = Renderer([&] { Elements entries; for (int i = 0; i < 22; ++i) { - entries.push_back(spinner(i, shift / 2) | bold | + entries.push_back(spinner(i, shift / 5) | bold | size(WIDTH, GREATER_THAN, 2) | border); } return hflow(std::move(entries)); @@ -512,24 +513,20 @@ int main() { }); }); - std::atomic refresh_ui_continue = true; - std::thread refresh_ui([&] { - while (refresh_ui_continue) { - using namespace std::chrono_literals; - std::this_thread::sleep_for(0.05s); - // The |shift| variable belong to the main thread. `screen.Post(task)` - // will execute the update on the thread where |screen| lives (e.g. the - // main thread). Using `screen.Post(task)` is threadsafe. - screen.Post([&] { shift++; }); - // After updating the state, request a new frame to be drawn. This is done - // by simulating a new "custom" event to be handled. - screen.Post(Event::Custom); - } - }); + Loop loop(&screen, main_renderer); + while(!loop.HasQuitted()) { + // Update the state of the application. + shift++; - screen.Loop(main_renderer); - refresh_ui_continue = false; - refresh_ui.join(); + // Request a new frame to be drawn. + screen.RequestAnimationFrame(); + + // Execute events, and draw the next frame. + loop.RunOnce(); + + // Sleep for a short duration to control the frame rate (60 FPS). + std::this_thread::sleep_for(std::chrono::milliseconds(1000/60)); + } return 0; } diff --git a/examples/index.mjs b/examples/index.mjs index 80365904..63d27326 100644 --- a/examples/index.mjs +++ b/examples/index.mjs @@ -7,7 +7,7 @@ if ("serviceWorker" in navigator && !window.crossOriginIsolated) { const url_sw = new URL("./sw.js", location.href); const registration = await navigator.serviceWorker.register(url_sw); window.location.reload(); // Reload to ensure the COOP/COEP headers are set. -} +} const example_list = "@EXAMPLES@".split(";"); const url_search_params = new URLSearchParams(window.location.search); @@ -55,7 +55,7 @@ const stdout = code => { const stderr = code => { if (code == 0 || code == 10) { console.error(String.fromCodePoint(...stderr_buffer)); - stderr_buffer = []; + stderr_buffer.length = 0; } else { stderr_buffer.push(code) } @@ -89,9 +89,6 @@ window.Module = { const resize_observer = new ResizeObserver(resize_handler); resize_observer.observe(term_element); resize_handler(); - - // Disable scrollbar - //term.write('\x1b[?47h') }, }; diff --git a/include/ftxui/component/receiver.hpp b/include/ftxui/component/receiver.hpp index 55189cf9..d9fed4dc 100644 --- a/include/ftxui/component/receiver.hpp +++ b/include/ftxui/component/receiver.hpp @@ -14,6 +14,8 @@ namespace ftxui { +// Deprecated +// // Usage: // // Initialization: @@ -39,17 +41,24 @@ namespace ftxui { // Receiver::Receive() returns true when there are no more senders. // clang-format off +// Deprecated: template class SenderImpl; +// Deprecated: template class ReceiverImpl; +// Deprecated: +// Deprecated: template using Sender = std::unique_ptr>; +// Deprecated: template using Receiver = std::unique_ptr>; +// Deprecated: template Receiver MakeReceiver(); // clang-format on // ---- Implementation part ---- template +// Deprecated: class SenderImpl { public: SenderImpl(const SenderImpl&) = delete; diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index f9220ff8..c9ba93e7 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -5,11 +5,9 @@ #define FTXUI_COMPONENT_SCREEN_INTERACTIVE_HPP #include // for atomic -#include // for Receiver, Sender #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 @@ -26,6 +24,10 @@ struct Event; using Component = std::shared_ptr; class ScreenInteractivePrivate; +namespace task { + class TaskRunner; +} + /// @brief ScreenInteractive is a `Screen` that can handle events, run a main /// loop, and manage components. /// @@ -40,6 +42,9 @@ class ScreenInteractive : public Screen { static ScreenInteractive FitComponent(); static ScreenInteractive TerminalOutput(); + // Destructor. + ~ScreenInteractive(); + // Options. Must be called before Loop(). void TrackMouse(bool enable = true); @@ -97,6 +102,10 @@ class ScreenInteractive : public Screen { void Signal(int signal); + void FetchTerminalEvents(); + + void PostAnimationTask(); + ScreenInteractive* suspended_screen_ = nullptr; enum class Dimension { FitComponent, @@ -113,15 +122,10 @@ class ScreenInteractive : public Screen { bool track_mouse_ = true; - Sender task_sender_; - Receiver task_receiver_; - std::string set_cursor_position; 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_; @@ -156,8 +160,15 @@ class ScreenInteractive : public Screen { std::unique_ptr selection_; std::function selection_on_change_; + // PIMPL private implementation idiom (Pimpl). + struct Internal; + std::unique_ptr internal_; + friend class Loop; + Component component_; + + public: class Private { public: diff --git a/include/ftxui/screen/image.hpp b/include/ftxui/screen/image.hpp index 77e47199..4ec33571 100644 --- a/include/ftxui/screen/image.hpp +++ b/include/ftxui/screen/image.hpp @@ -20,6 +20,9 @@ class Image { Image() = delete; Image(int dimx, int dimy); + // Destructor: + virtual ~Image() = default; + // Access a character in the grid at a given position. std::string& at(int x, int y); const std::string& at(int x, int y) const; diff --git a/include/ftxui/screen/screen.hpp b/include/ftxui/screen/screen.hpp index aa2f4f8c..efde98e4 100644 --- a/include/ftxui/screen/screen.hpp +++ b/include/ftxui/screen/screen.hpp @@ -11,7 +11,6 @@ #include "ftxui/screen/image.hpp" // for Pixel, Image #include "ftxui/screen/terminal.hpp" // for Dimensions -#include "ftxui/util/autoreset.hpp" // for AutoReset namespace ftxui { @@ -31,6 +30,9 @@ class Screen : public Image { static Screen Create(Dimensions dimension); static Screen Create(Dimensions width, Dimensions height); + // Destructor: + ~Screen() override = default; + std::string ToString() const; // Print the Screen on to the terminal. diff --git a/src/ftxui/component/receiver.cppm b/src/ftxui/component/receiver.cppm index ddf4177b..aee92ff1 100644 --- a/src/ftxui/component/receiver.cppm +++ b/src/ftxui/component/receiver.cppm @@ -12,9 +12,15 @@ export module ftxui.component.receiver; * @brief The FTXUI ftxui:: namespace */ export namespace ftxui { + // Deprecated: using ftxui::SenderImpl; + // Deprecated: using ftxui::ReceiverImpl; + // Deprecated: using ftxui::Sender; + // Deprecated: using ftxui::Receiver; + // Deprecated: using ftxui::MakeReceiver; + // Deprecated: } diff --git a/src/ftxui/component/receiver_test.cpp b/src/ftxui/component/receiver_test.cpp deleted file mode 100644 index cb2cde07..00000000 --- a/src/ftxui/component/receiver_test.cpp +++ /dev/null @@ -1,81 +0,0 @@ -// 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. -#include // for thread -#include // for move - -#include "ftxui/component/receiver.hpp" -#include "gtest/gtest.h" // for AssertionResult, Message, Test, TestPartResult, EXPECT_EQ, EXPECT_TRUE, EXPECT_FALSE, TEST - -// NOLINTBEGIN -namespace ftxui { - -TEST(Receiver, Basic) { - auto receiver = MakeReceiver(); - auto sender = receiver->MakeSender(); - - sender->Send('a'); - sender->Send('b'); - sender->Send('c'); - sender.reset(); - - char a, b, c, d; - EXPECT_TRUE(receiver->Receive(&a)); - EXPECT_TRUE(receiver->Receive(&b)); - EXPECT_TRUE(receiver->Receive(&c)); - EXPECT_FALSE(receiver->Receive(&d)); - - EXPECT_EQ(a, 'a'); - EXPECT_EQ(b, 'b'); - EXPECT_EQ(c, 'c'); -} - -TEST(Receiver, BasicWithThread) { - auto r1 = MakeReceiver(); - auto r2 = MakeReceiver(); - auto r3 = MakeReceiver(); - - auto s1 = r1->MakeSender(); - auto s2 = r2->MakeSender(); - auto s3 = r3->MakeSender(); - - auto s1_bis = r1->MakeSender(); - - auto stream = [](Receiver receiver, Sender sender) { - char c; - while (receiver->Receive(&c)) - sender->Send(c); - }; - - // Convert data from a different thread. - auto t12 = std::thread(stream, std::move(r1), std::move(s2)); - auto t23 = std::thread(stream, std::move(r2), std::move(s3)); - - // Send some data. - s1->Send('1'); - s1_bis->Send('2'); - s1->Send('3'); - s1_bis->Send('4'); - - // Close the stream. - s1.reset(); - s1_bis.reset(); - - char c; - EXPECT_TRUE(r3->Receive(&c)); - EXPECT_EQ(c, '1'); - EXPECT_TRUE(r3->Receive(&c)); - EXPECT_EQ(c, '2'); - EXPECT_TRUE(r3->Receive(&c)); - EXPECT_EQ(c, '3'); - EXPECT_TRUE(r3->Receive(&c)); - EXPECT_EQ(c, '4'); - EXPECT_FALSE(r3->Receive(&c)); - - // Thread will end at the end of the stream. - t12.join(); - t23.join(); -} - -} // namespace ftxui -// NOLINTEND diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index e9f3f0d9..887e21e4 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -17,24 +17,22 @@ #include #include // for stack #include -#include // for thread, sleep_for -#include // for _Swallow_assign, ignore -#include // for decay_t -#include // for move, swap -#include // for visit, variant -#include // for vector +#include // for thread, sleep_for +#include // for _Swallow_assign, ignore +#include // for move, swap +#include // for visit, variant +#include // for vector #include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface #include "ftxui/component/component_base.hpp" // for ComponentBase #include "ftxui/component/event.hpp" // for Event #include "ftxui/component/loop.hpp" // for Loop -#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver +#include "ftxui/component/task_runner.hpp" #include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser #include "ftxui/dom/node.hpp" // for Node, Render -#include "ftxui/dom/requirement.hpp" // for Requirement -#include "ftxui/screen/pixel.hpp" // for Pixel #include "ftxui/screen/terminal.hpp" // for Dimensions, Size #include "ftxui/screen/util.hpp" // for util::clamp +#include "ftxui/util/autoreset.hpp" // for AutoReset #if defined(_WIN32) #define DEFINE_CONSOLEV2_PROPERTIES @@ -47,9 +45,11 @@ #error Must be compiled in UNICODE mode #endif #else +#include #include // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval #include // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME #include // for STDIN_FILENO, read +#include #endif // Quick exit is missing in standard CLang headers @@ -59,6 +59,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(); @@ -82,75 +96,9 @@ constexpr int timeout_milliseconds = 20; timeout_milliseconds * 1000; #if defined(_WIN32) -void EventListener(std::atomic* quit, Sender out) { - auto console = GetStdHandle(STD_INPUT_HANDLE); - auto parser = - TerminalInputParser([&](Event event) { out->Send(std::move(event)); }); - while (!*quit) { - // Throttle ReadConsoleInput by waiting 250ms, this wait function will - // return if there is input in the console. - auto wait_result = WaitForSingleObject(console, timeout_milliseconds); - if (wait_result == WAIT_TIMEOUT) { - parser.Timeout(timeout_milliseconds); - continue; - } - - DWORD number_of_events = 0; - if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) - continue; - if (number_of_events <= 0) - continue; - - std::vector records{number_of_events}; - DWORD number_of_events_read = 0; - ReadConsoleInput(console, records.data(), (DWORD)records.size(), - &number_of_events_read); - records.resize(number_of_events_read); - - for (const auto& r : records) { - switch (r.EventType) { - case KEY_EVENT: { - auto key_event = r.Event.KeyEvent; - // ignore UP key events - if (key_event.bKeyDown == FALSE) - continue; - std::wstring wstring; - wstring += key_event.uChar.UnicodeChar; - for (auto it : to_string(wstring)) { - parser.Add(it); - } - } break; - case WINDOW_BUFFER_SIZE_EVENT: - out->Send(Event::Special({0})); - break; - case MENU_EVENT: - case FOCUS_EVENT: - case MOUSE_EVENT: - // TODO(mauve): Implement later. - break; - } - } - } -} - #elif defined(__EMSCRIPTEN__) #include -// Read char from the terminal. -void EventListener(std::atomic* quit, Sender out) { - auto parser = - TerminalInputParser([&](Event event) { out->Send(std::move(event)); }); - - char c; - while (!*quit) { - while (read(STDIN_FILENO, &c, 1), c) - parser.Add(c); - - emscripten_sleep(1); - parser.Timeout(1); - } -} - extern "C" { EMSCRIPTEN_KEEPALIVE void ftxui_on_resize(int columns, int rows) { @@ -164,8 +112,8 @@ void ftxui_on_resize(int columns, int rows) { #else // POSIX (Linux & Mac) -int CheckStdinReady(int usec_timeout) { - timeval tv = {0, usec_timeout}; // NOLINT +int CheckStdinReady() { + timeval tv = {0, 0}; // NOLINT fd_set fds; FD_ZERO(&fds); // NOLINT FD_SET(STDIN_FILENO, &fds); // NOLINT @@ -173,25 +121,6 @@ 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)); }); - - while (!*quit) { - if (!CheckStdinReady(timeout_microseconds)) { - parser.Timeout(timeout_milliseconds); - continue; - } - - 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 @@ -338,15 +267,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, @@ -356,7 +276,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 @@ -417,6 +338,8 @@ ScreenInteractive ScreenInteractive::TerminalOutput() { }; } +ScreenInteractive::~ScreenInteractive() = default; + /// Create a ScreenInteractive whose width and height match the component being /// drawn. // static @@ -452,13 +375,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. @@ -502,7 +421,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 @@ -659,7 +578,15 @@ void ScreenInteractive::Install() { SetConsoleMode(stdin_handle, in_mode); SetConsoleMode(stdout_handle, out_mode); -#else +#else // POSIX (Linux & Mac) + // #if defined(__EMSCRIPTEN__) + //// Reading stdin isn't blocking. + // int flags = fcntl(0, F_GETFL, 0); + // fcntl(0, F_SETFL, flags | O_NONBLOCK); + + //// Restore the terminal configuration on exit. + // on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); }); + // #endif for (const int signal : {SIGWINCH, SIGTSTP}) { InstallSignalHandler(signal); } @@ -732,40 +659,57 @@ 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); + // Set FPS to 60 at most. + const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps + + auto time = std::chrono::steady_clock::now(); + size_t executed_task = internal_->task_runner.ExecutedTasks(); + + // Wait for at least one task to execute. + while (executed_task == internal_->task_runner.ExecutedTasks() && + !HasQuitted()) { + RunOnce(component); + + const auto now = std::chrono::steady_clock::now(); + const auto delta = now - time; + time = now; + + if (delta < time_per_frame) { + const auto sleep_duration = time_per_frame - delta; + std::this_thread::sleep_for(sleep_duration); + } } - RunOnce(component); } // private void ScreenInteractive::RunOnce(Component component) { - Task task; - while (task_receiver_->ReceiveNonBlocking(&task)) { - HandleTask(component, task); - ExecuteSignalHandlers(); + AutoReset set_component(&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; } - Draw(std::move(component)); + + ExecuteSignalHandlers(); + Draw(component); if (selection_data_previous_ != selection_data_) { selection_data_previous_ = selection_data_; @@ -786,6 +730,7 @@ void ScreenInteractive::HandleTask(Component component, Task& task) { // clang-format off // Handle Event. if constexpr (std::is_same_v) { + if (arg.is_cursor_position()) { cursor_x_ = arg.cursor_x(); cursor_y_ = arg.cursor_y(); @@ -1037,7 +982,6 @@ void ScreenInteractive::Exit() { // private: void ScreenInteractive::ExitNow() { quit_ = true; - task_sender_.reset(); } // private: @@ -1070,6 +1014,117 @@ void ScreenInteractive::Signal(int signal) { #endif } +void ScreenInteractive::FetchTerminalEvents() { +#if defined(_WIN32) + auto get_input_records = [&]() -> std::vector { + // Check if there is input in the console. + auto console = GetStdHandle(STD_INPUT_HANDLE); + DWORD number_of_events = 0; + if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) { + return std::vector(); + } + if (number_of_events <= 0) { + // No input, return. + return std::vector(); + } + // Read the input events. + std::vector records(number_of_events); + DWORD number_of_events_read = 0; + if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(), + &number_of_events_read)) { + return std::vector(); + } + records.resize(number_of_events_read); + return records; + }; + + auto records = get_input_records(); + if (records.size() == 0) { + const auto timeout = + std::chrono::steady_clock::now() - internal_->last_char_time; + const size_t timeout_microseconds = + std::chrono::duration_cast(timeout).count(); + internal_->terminal_input_parser.Timeout(timeout_microseconds); + return; + } + internal_->last_char_time = std::chrono::steady_clock::now(); + + // Convert the input events to FTXUI events. + // For each event, we call the terminal input parser to convert it to + // Event. + for (const auto& r : records) { + switch (r.EventType) { + case KEY_EVENT: { + auto key_event = r.Event.KeyEvent; + // ignore UP key events + if (key_event.bKeyDown == FALSE) + continue; + std::wstring wstring; + wstring += key_event.uChar.UnicodeChar; + for (auto it : to_string(wstring)) { + internal_->terminal_input_parser.Add(it); + } + } break; + case WINDOW_BUFFER_SIZE_EVENT: + Post(Event::Special({0})); + break; + case MENU_EVENT: + case FOCUS_EVENT: + case MOUSE_EVENT: + // TODO(mauve): Implement later. + break; + } + } +#elif defined(__EMSCRIPTEN__) + // Read chars from the terminal. + // We configured it to be non blocking. + std::array out{}; + size_t l = read(STDIN_FILENO, out.data(), out.size()); + if (l == 0) { + const auto timeout = + std::chrono::steady_clock::now() - internal_->last_char_time; + const size_t timeout_microseconds = + std::chrono::duration_cast(timeout).count(); + internal_->terminal_input_parser.Timeout(timeout_microseconds); + return; + } + internal_->last_char_time = std::chrono::steady_clock::now(); + + // Convert the chars to events. + for (size_t i = 0; i < l; ++i) { + internal_->terminal_input_parser.Add(out[i]); + } +#else // POSIX (Linux & Mac) + if (!CheckStdinReady()) { + 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]); + } +#endif +} + +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/component/task.cpp b/src/ftxui/component/task.cpp new file mode 100644 index 00000000..3db2932f --- /dev/null +++ b/src/ftxui/component/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 "ftxui/component/task_internal.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/component/task_internal.hpp b/src/ftxui/component/task_internal.hpp new file mode 100644 index 00000000..28ddcc74 --- /dev/null +++ b/src/ftxui/component/task_internal.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 t) : task(std::move(t)) {} // NOLINT + + // Delayed task with a duration + PendingTask(Task t, std::chrono::steady_clock::duration duration) + : task(std::move(t)), + 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/component/task_queue.cpp b/src/ftxui/component/task_queue.cpp new file mode 100644 index 00000000..351a87ea --- /dev/null +++ b/src/ftxui/component/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 "ftxui/component/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/component/task_queue.hpp b/src/ftxui/component/task_queue.hpp new file mode 100644 index 00000000..035e3646 --- /dev/null +++ b/src/ftxui/component/task_queue.hpp @@ -0,0 +1,40 @@ +// 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 "ftxui/component/task_internal.hpp" // for PendingTask, Task + +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; + + bool HasImmediateTasks() const { + return !immediate_tasks_.empty(); + } + + private: + std::queue immediate_tasks_; + std::priority_queue delayed_tasks_; +}; + +} // namespace ftxui::task + + +#endif diff --git a/src/ftxui/component/task_runner.cpp b/src/ftxui/component/task_runner.cpp new file mode 100644 index 00000000..78a74d74 --- /dev/null +++ b/src/ftxui/component/task_runner.cpp @@ -0,0 +1,77 @@ +// 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 "ftxui/component/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)) { + executed_tasks_++; + 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/component/task_runner.hpp b/src/ftxui/component/task_runner.hpp new file mode 100644 index 00000000..3e5de2f4 --- /dev/null +++ b/src/ftxui/component/task_runner.hpp @@ -0,0 +1,49 @@ +// 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 "ftxui/component/task_internal.hpp" +#include "ftxui/component/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; + + 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 + + +#endif // TASK_RUNNER_HPP diff --git a/src/ftxui/component/task_test.cpp b/src/ftxui/component/task_test.cpp new file mode 100644 index 00000000..f2fb6051 --- /dev/null +++ b/src/ftxui/component/task_test.cpp @@ -0,0 +1,94 @@ +// 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 "ftxui/component/task_internal.hpp" + +#include + +#include // for sleep_for +#include "ftxui/component/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