diff --git a/CHANGELOG.md b/CHANGELOG.md index cddd9ba..248c285 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ Element gaugeDown(float ratio); Element gaugeDirection(float ratio, GaugeDirection); ``` +#### Component +- Support SIGTSTP. (ctrl+z). +- Support task posting. `ScreenInteractive::Post(Task)`. + 2.0.0 ----- diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a3c268..51e25f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,6 +90,7 @@ add_library(component include/ftxui/component/mouse.hpp include/ftxui/component/receiver.hpp include/ftxui/component/screen_interactive.hpp + include/ftxui/component/task.hpp src/ftxui/component/button.cpp src/ftxui/component/catch_event.cpp src/ftxui/component/checkbox.cpp diff --git a/examples/component/focus.cpp b/examples/component/focus.cpp index edadfe4..71522af 100644 --- a/examples/component/focus.cpp +++ b/examples/component/focus.cpp @@ -32,8 +32,8 @@ Element make_grid() { }; int main(int argc, const char* argv[]) { - float focus_x = 0.0f; - float focus_y = 0.0f; + float focus_x = 0.5f; + float focus_y = 0.5f; auto slider_x = Slider("x", &focus_x, 0.f, 1.f, 0.01f); auto slider_y = Slider("y", &focus_y, 0.f, 1.f, 0.01f); diff --git a/examples/component/with_restored_io.cpp b/examples/component/with_restored_io.cpp index 4ca089d..9157fc2 100644 --- a/examples/component/with_restored_io.cpp +++ b/examples/component/with_restored_io.cpp @@ -18,14 +18,12 @@ int main() { // temporarily uninstall the terminal hook and execute the provided callback // function. This allow running the application in a non-interactive mode. auto btn_run = Button("Execute with restored IO", screen.WithRestoredIO([] { - std::system("bash"); std::cout << "This is a child program using stdin/stdout." << std::endl; for (int i = 0; i < 10; ++i) { std::cout << "Please enter 10 strings (" << i << "/10)" << std::flush; std::string input; std::getline(std::cin, input); } - std::system("bash"); })); auto btn_quit = Button("Quit", screen.ExitLoopClosure()); diff --git a/examples/dom/color_info_sorted_2d.ipp b/examples/dom/color_info_sorted_2d.ipp index 9c62779..2bf8c23 100644 --- a/examples/dom/color_info_sorted_2d.ipp +++ b/examples/dom/color_info_sorted_2d.ipp @@ -34,7 +34,7 @@ std::vector> ColorInfoSorted2D() { for (int i = 0; i < int(column.size()) - 1; ++i) { int best_index = i + 1; int best_distance = 255 * 255 * 3; - for (size_t j = i + 1; j < column.size(); ++j) { + for (int j = i + 1; j < column.size(); ++j) { int dx = column[i].red - column[j].red; int dy = column[i].green - column[j].green; int dz = column[i].blue - column[j].blue; diff --git a/include/ftxui/component/event.hpp b/include/ftxui/component/event.hpp index 56e73a7..0d356cf 100644 --- a/include/ftxui/component/event.hpp +++ b/include/ftxui/component/event.hpp @@ -2,7 +2,8 @@ #define FTXUI_COMPONENT_EVENT_HPP #include // for Mouse -#include // for string, operator== +#include +#include // for string, operator== #include namespace ftxui { diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index 0451b21..d9a1b09 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -7,9 +7,11 @@ #include // for shared_ptr #include // for string #include // for thread +#include // for variant #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse #include "ftxui/component/event.hpp" // for Event +#include "ftxui/component/task.hpp" // for Closure, Task #include "ftxui/screen/screen.hpp" // for Screen namespace ftxui { @@ -17,37 +19,38 @@ class ComponentBase; struct Event; using Component = std::shared_ptr; +class ScreenInteractivePrivate; class ScreenInteractive : public Screen { public: - using Callback = std::function; - static ScreenInteractive FixedSize(int dimx, int dimy); static ScreenInteractive Fullscreen(); static ScreenInteractive FitComponent(); static ScreenInteractive TerminalOutput(); void Loop(Component); - Callback ExitLoopClosure(); + Closure ExitLoopClosure(); + void Post(Task task); void PostEvent(Event event); + CapturedMouse CaptureMouse(); // Decorate a function. The outputted one will execute similarly to the // inputted one, but with the currently active screen terminal hooks // temporarily uninstalled. - Callback WithRestoredIO(Callback); + Closure WithRestoredIO(Closure); private: void Install(); void Uninstall(); void Main(Component component); - ScreenInteractive* suspended_screen_ = nullptr; void Draw(Component component); - void EventLoop(Component component); + void SigStop(); + ScreenInteractive* suspended_screen_ = nullptr; enum class Dimension { FitComponent, Fixed, @@ -61,8 +64,8 @@ class ScreenInteractive : public Screen { Dimension dimension, bool use_alternative_screen); - Sender event_sender_; - Receiver event_receiver_; + Sender task_sender_; + Receiver task_receiver_; std::string set_cursor_position; std::string reset_cursor_position; @@ -75,6 +78,13 @@ class ScreenInteractive : public Screen { bool mouse_captured = false; bool previous_frame_resized_ = false; + + public: + class Private { + public: + static void SigStop(ScreenInteractive& s) { return s.SigStop(); } + }; + friend Private; }; } // namespace ftxui diff --git a/include/ftxui/component/task.hpp b/include/ftxui/component/task.hpp new file mode 100644 index 0000000..2c5c9a9 --- /dev/null +++ b/include/ftxui/component/task.hpp @@ -0,0 +1,12 @@ +#include +#include +#include "ftxui/component/event.hpp" + +namespace ftxui { +using Closure = std::function; +using Task = std::variant; +} // namespace ftxui + +// Copyright 2022 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/src/ftxui/component/component_fuzzer.cpp b/src/ftxui/component/component_fuzzer.cpp index f3aa698..6611527 100644 --- a/src/ftxui/component/component_fuzzer.cpp +++ b/src/ftxui/component/component_fuzzer.cpp @@ -151,16 +151,16 @@ extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) { auto screen = Screen::Create(Dimension::Fixed(width), Dimension::Fixed(height)); - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); for (size_t i = 0; i < size; ++i) parser.Add(data[i]); } - Event event; + Task event; while (event_receiver->Receive(&event)) { - component->OnEvent(event); + component->OnEvent(std::get(event)); auto document = component->Render(); Render(screen, document); } diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 1017779..b68dc86 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -1,18 +1,20 @@ #include // for fileno, stdin #include // for copy, max, min -#include // for signal, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGWINCH -#include // for NULL +#include // for signal, raise, SIGTSTP, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGWINCH +#include // for NULL +#include // for function #include // for initializer_list #include // for cout, ostream, basic_ostream, operator<<, endl, flush #include // for stack #include // for thread -#include // for swap, move -#include // for vector +#include // for decay_t +#include // for swap, move +#include // for visit +#include // for vector #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/mouse.hpp" // for Mouse #include "ftxui/component/receiver.hpp" // for ReceiverImpl, MakeReceiver, Sender, SenderImpl, Receiver #include "ftxui/component/screen_interactive.hpp" #include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser @@ -32,8 +34,8 @@ #endif #else #include // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set -#include // for tcsetattr, tcgetattr, cc_t -#include // for STDIN_FILENO, read +#include // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME +#include // for STDIN_FILENO, read #endif // Quick exit is missing in standard CLang headers @@ -45,6 +47,8 @@ namespace ftxui { namespace { +ScreenInteractive* g_active_screen = nullptr; + void Flush() { // Emscripten doesn't implement flush. We interpret zero as flush. std::cout << '\0' << std::flush; @@ -54,7 +58,7 @@ constexpr int timeout_milliseconds = 20; constexpr int timeout_microseconds = timeout_milliseconds * 1000; #if defined(_WIN32) -void EventListener(std::atomic* quit, Sender out) { +void EventListener(std::atomic* quit, Sender out) { auto console = GetStdHandle(STD_INPUT_HANDLE); auto parser = TerminalInputParser(out->Clone()); while (!*quit) { @@ -104,7 +108,7 @@ void EventListener(std::atomic* quit, Sender out) { #include // Read char from the terminal. -void EventListener(std::atomic* quit, Sender out) { +void EventListener(std::atomic* quit, Sender out) { (void)timeout_microseconds; auto parser = TerminalInputParser(std::move(out)); @@ -131,7 +135,7 @@ int CheckStdinReady(int usec_timeout) { } // Read char from the terminal. -void EventListener(std::atomic* quit, Sender out) { +void EventListener(std::atomic* quit, Sender out) { const int buffer_size = 100; auto parser = TerminalInputParser(std::move(out)); @@ -200,7 +204,7 @@ const std::string DeviceStatusReport(DSRMode ps) { } using SignalHandler = void(int); -std::stack on_exit_functions; +std::stack on_exit_functions; void OnExit(int signal) { (void)signal; while (!on_exit_functions.empty()) { @@ -211,14 +215,18 @@ void OnExit(int signal) { auto install_signal_handler = [](int sig, SignalHandler handler) { auto old_signal_handler = std::signal(sig, handler); - on_exit_functions.push([&] { std::signal(sig, old_signal_handler); }); + on_exit_functions.push([=] { std::signal(sig, old_signal_handler); }); }; -ScreenInteractive::Callback on_resize = [] {}; +Closure on_resize = [] {}; void OnResize(int /* signal */) { on_resize(); } +void OnSigStop(int /*signal*/) { + ScreenInteractive::Private::SigStop(*g_active_screen); +} + class CapturedMouseImpl : public CapturedMouseInterface { public: CapturedMouseImpl(std::function callback) : callback_(callback) {} @@ -230,8 +238,6 @@ class CapturedMouseImpl : public CapturedMouseInterface { } // namespace -ScreenInteractive* g_active_screen = nullptr; - ScreenInteractive::ScreenInteractive(int dimx, int dimy, Dimension dimension, @@ -239,8 +245,7 @@ ScreenInteractive::ScreenInteractive(int dimx, : Screen(dimx, dimy), dimension_(dimension), use_alternative_screen_(use_alternative_screen) { - event_receiver_ = MakeReceiver(); - event_sender_ = event_receiver_->MakeSender(); + task_receiver_ = MakeReceiver(); } // static @@ -263,9 +268,12 @@ ScreenInteractive ScreenInteractive::FitComponent() { return ScreenInteractive(0, 0, Dimension::FitComponent, false); } -void ScreenInteractive::PostEvent(Event event) { +void ScreenInteractive::Post(Task task) { if (!quit_) - event_sender_->Send(event); + task_sender_->Send(task); +} +void ScreenInteractive::PostEvent(Event event) { + Post(event); } CapturedMouse ScreenInteractive::CaptureMouse() { @@ -314,7 +322,7 @@ void ScreenInteractive::Loop(Component component) { /// @brief Decorate a function. It executes the same way, but with the currently /// active screen terminal hooks temporarilly uninstalled during its execution. /// @param fn The function to decorate. -ScreenInteractive::Callback ScreenInteractive::WithRestoredIO(Callback fn) { +Closure ScreenInteractive::WithRestoredIO(Closure fn) { return [this, fn] { Uninstall(); fn(); @@ -381,8 +389,11 @@ void ScreenInteractive::Install() { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); // Handle resize. - on_resize = [&] { event_sender_->Send(Event::Special({0})); }; + on_resize = [&] { task_sender_->Send(Event::Special({0})); }; install_signal_handler(SIGWINCH, OnResize); + + // Handle SIGTSTP/SIGCONT. + install_signal_handler(SIGTSTP, OnSigStop); #endif auto enable = [&](std::vector parameters) { @@ -418,8 +429,9 @@ void ScreenInteractive::Install() { Flush(); quit_ = false; + task_sender_ = task_receiver_->MakeSender(); event_listener_ = - std::thread(&EventListener, &quit_, event_receiver_->MakeSender()); + std::thread(&EventListener, &quit_, task_receiver_->MakeSender()); } void ScreenInteractive::Uninstall() { @@ -431,30 +443,43 @@ void ScreenInteractive::Uninstall() { void ScreenInteractive::Main(Component component) { while (!quit_) { - if (!event_receiver_->HasPending()) { + if (!task_receiver_->HasPending()) { Draw(component); std::cout << ToString() << set_cursor_position; Flush(); Clear(); } - Event event; - if (!event_receiver_->Receive(&event)) + Task task; + if (!task_receiver_->Receive(&task)) break; - if (event.is_cursor_reporting()) { - cursor_x_ = event.cursor_x(); - cursor_y_ = event.cursor_y(); - continue; - } + std::visit( + [&](auto&& arg) { + using T = std::decay_t; - if (event.is_mouse()) { - event.mouse().x -= cursor_x_; - event.mouse().y -= cursor_y_; - } + // Handle Event. + if constexpr (std::is_same_v) { + if (arg.is_cursor_reporting()) { + cursor_x_ = arg.cursor_x(); + cursor_y_ = arg.cursor_y(); + return; + } - event.screen_ = this; - component->OnEvent(event); + if (arg.is_mouse()) { + arg.mouse().x -= cursor_x_; + arg.mouse().y -= cursor_y_; + } + + arg.screen_ = this; + component->OnEvent(arg); + } + + // Handle callback + if constexpr (std::is_same_v) + arg(); + }, + task); } } @@ -537,13 +562,31 @@ void ScreenInteractive::Draw(Component component) { } } -ScreenInteractive::Callback ScreenInteractive::ExitLoopClosure() { +Closure ScreenInteractive::ExitLoopClosure() { return [this] { quit_ = true; - event_sender_.reset(); + task_sender_.reset(); }; } +void ScreenInteractive::SigStop() { +#if defined(_WIN32) + // Windows do no support SIGTSTP. +#else + Post([&] { + Uninstall(); + std::cout << reset_cursor_position; + reset_cursor_position = ""; + std::cout << ResetPosition(/*clear=*/true); + dimx_ = 0; + dimy_ = 0; + Flush(); + std::raise(SIGTSTP); + Install(); + }); +#endif +} + } // namespace ftxui. // Copyright 2020 Arthur Sonzogni. All rights reserved. diff --git a/src/ftxui/component/terminal_input_parser.cpp b/src/ftxui/component/terminal_input_parser.cpp index b9526ad..22da7dc 100644 --- a/src/ftxui/component/terminal_input_parser.cpp +++ b/src/ftxui/component/terminal_input_parser.cpp @@ -1,15 +1,15 @@ #include "ftxui/component/terminal_input_parser.hpp" -#include // for max -#include +#include // for uint32_t #include // for unique_ptr #include // for move #include "ftxui/component/event.hpp" // for Event +#include "ftxui/component/task.hpp" // for Task namespace ftxui { -TerminalInputParser::TerminalInputParser(Sender out) +TerminalInputParser::TerminalInputParser(Sender out) : out_(std::move(out)) {} void TerminalInputParser::Timeout(int time) { diff --git a/src/ftxui/component/terminal_input_parser.hpp b/src/ftxui/component/terminal_input_parser.hpp index 899fcd5..772631b 100644 --- a/src/ftxui/component/terminal_input_parser.hpp +++ b/src/ftxui/component/terminal_input_parser.hpp @@ -8,6 +8,7 @@ #include "ftxui/component/event.hpp" // for Event (ptr only) #include "ftxui/component/mouse.hpp" // for Mouse #include "ftxui/component/receiver.hpp" // for Sender +#include "ftxui/component/task.hpp" // for Task namespace ftxui { struct Event; @@ -15,7 +16,7 @@ struct Event; // Parse a sequence of |char| accross |time|. Produces |Event|. class TerminalInputParser { public: - TerminalInputParser(Sender out); + TerminalInputParser(Sender out); void Timeout(int time); void Add(char c); @@ -57,7 +58,7 @@ class TerminalInputParser { Output ParseMouse(bool altered, bool pressed, std::vector arguments); Output ParseCursorReporting(std::vector arguments); - Sender out_; + Sender out_; int position_ = -1; int timeout_ = 0; std::string pending_; diff --git a/src/ftxui/component/terminal_input_parser_test.cpp b/src/ftxui/component/terminal_input_parser_test.cpp index 6b9340c..f2bb0e2 100644 --- a/src/ftxui/component/terminal_input_parser_test.cpp +++ b/src/ftxui/component/terminal_input_parser_test.cpp @@ -2,6 +2,7 @@ #include // for TestPartResult, SuiteApiResolver, TestFactoryImpl #include // for max #include // for unique_ptr, allocator +#include // for get #include "ftxui/component/event.hpp" // for Event, Event::Escape #include "ftxui/component/receiver.hpp" // for MakeReceiver, ReceiverImpl @@ -18,61 +19,61 @@ TEST(Event, Character) { for (char c = 'A'; c <= 'Z'; ++c) basic_char.push_back(c); - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); for (char c : basic_char) parser.Add(c); } - Event received; + Task received; for (char c : basic_char) { EXPECT_TRUE(event_receiver->Receive(&received)); - EXPECT_TRUE(received.is_character()); - EXPECT_EQ(c, received.character()[0]); + EXPECT_TRUE(std::get(received).is_character()); + EXPECT_EQ(c, std::get(received).character()[0]); } EXPECT_FALSE(event_receiver->Receive(&received)); } TEST(Event, EscapeKeyWithoutWaiting) { - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); parser.Add('\x1B'); } - Event received; + Task received; EXPECT_FALSE(event_receiver->Receive(&received)); } TEST(Event, EscapeKeyNotEnoughWait) { - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); parser.Add('\x1B'); parser.Timeout(49); } - Event received; + Task received; EXPECT_FALSE(event_receiver->Receive(&received)); } TEST(Event, EscapeKeyEnoughWait) { - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); parser.Add('\x1B'); parser.Timeout(50); } - Event received; + Task received; EXPECT_TRUE(event_receiver->Receive(&received)); - EXPECT_EQ(received, Event::Escape); + EXPECT_EQ(std::get(received), Event::Escape); EXPECT_FALSE(event_receiver->Receive(&received)); } TEST(Event, MouseLeftClick) { - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); parser.Add('\x1B'); @@ -88,17 +89,17 @@ TEST(Event, MouseLeftClick) { parser.Add('M'); } - Event received; + Task received; EXPECT_TRUE(event_receiver->Receive(&received)); - EXPECT_TRUE(received.is_mouse()); - EXPECT_EQ(Mouse::Left, received.mouse().button); - EXPECT_EQ(12, received.mouse().x); - EXPECT_EQ(42, received.mouse().y); + EXPECT_TRUE(std::get(received).is_mouse()); + EXPECT_EQ(Mouse::Left, std::get(received).mouse().button); + EXPECT_EQ(12, std::get(received).mouse().x); + EXPECT_EQ(42, std::get(received).mouse().y); EXPECT_FALSE(event_receiver->Receive(&received)); } TEST(Event, MouseMiddleClick) { - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); parser.Add('\x1B'); @@ -114,17 +115,17 @@ TEST(Event, MouseMiddleClick) { parser.Add('M'); } - Event received; + Task received; EXPECT_TRUE(event_receiver->Receive(&received)); - EXPECT_TRUE(received.is_mouse()); - EXPECT_EQ(Mouse::Middle, received.mouse().button); - EXPECT_EQ(12, received.mouse().x); - EXPECT_EQ(42, received.mouse().y); + EXPECT_TRUE(std::get(received).is_mouse()); + EXPECT_EQ(Mouse::Middle, std::get(received).mouse().button); + EXPECT_EQ(12, std::get(received).mouse().x); + EXPECT_EQ(42, std::get(received).mouse().y); EXPECT_FALSE(event_receiver->Receive(&received)); } TEST(Event, MouseRightClick) { - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); parser.Add('\x1B'); @@ -140,12 +141,12 @@ TEST(Event, MouseRightClick) { parser.Add('M'); } - Event received; + Task received; EXPECT_TRUE(event_receiver->Receive(&received)); - EXPECT_TRUE(received.is_mouse()); - EXPECT_EQ(Mouse::Right, received.mouse().button); - EXPECT_EQ(12, received.mouse().x); - EXPECT_EQ(42, received.mouse().y); + EXPECT_TRUE(std::get(received).is_mouse()); + EXPECT_EQ(Mouse::Right, std::get(received).mouse().button); + EXPECT_EQ(12, std::get(received).mouse().x); + EXPECT_EQ(42, std::get(received).mouse().y); EXPECT_FALSE(event_receiver->Receive(&received)); } @@ -216,16 +217,16 @@ TEST(Event, UTF8) { }; for (auto test : kTestCase) { - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); for (auto input : test.input) parser.Add(input); } - Event received; + Task received; if (test.valid) { EXPECT_TRUE(event_receiver->Receive(&received)); - EXPECT_TRUE(received.is_character()); + EXPECT_TRUE(std::get(received).is_character()); } EXPECT_FALSE(event_receiver->Receive(&received)); } diff --git a/src/ftxui/component/terminal_input_parser_test_fuzzer.cpp b/src/ftxui/component/terminal_input_parser_test_fuzzer.cpp index 6dd4946..6aa9422 100644 --- a/src/ftxui/component/terminal_input_parser_test_fuzzer.cpp +++ b/src/ftxui/component/terminal_input_parser_test_fuzzer.cpp @@ -5,14 +5,14 @@ extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) { using namespace ftxui; - auto event_receiver = MakeReceiver(); + auto event_receiver = MakeReceiver(); { auto parser = TerminalInputParser(event_receiver->MakeSender()); for (size_t i = 0; i < size; ++i) parser.Add(data[i]); } - Event received; + Task received; while (event_receiver->Receive(&received)) ; return 0; // Non-zero return values are reserved for future use.