Support SIGTSTP and task posting. (#331)

- Add support for SIGTSTP:
https://github.com/ArthurSonzogni/FTXUI/issues/330
This

- Add support for task posting.
This allows folks to defer function execution, and execute it directly
below the main loop. The task are executed in a FIFO order.
This commit is contained in:
Arthur Sonzogni 2022-02-13 11:11:34 +01:00 committed by GitHub
parent 62747a49b6
commit 9c4218c2a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 165 additions and 94 deletions

View File

@ -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
-----

View File

@ -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

View File

@ -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);

View File

@ -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());

View File

@ -34,7 +34,7 @@ std::vector<std::vector<ColorInfo>> 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;

View File

@ -2,7 +2,8 @@
#define FTXUI_COMPONENT_EVENT_HPP
#include <ftxui/component/mouse.hpp> // for Mouse
#include <string> // for string, operator==
#include <functional>
#include <string> // for string, operator==
#include <vector>
namespace ftxui {

View File

@ -7,9 +7,11 @@
#include <memory> // for shared_ptr
#include <string> // for string
#include <thread> // for thread
#include <variant> // 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<ComponentBase>;
class ScreenInteractivePrivate;
class ScreenInteractive : public Screen {
public:
using Callback = std::function<void()>;
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> event_sender_;
Receiver<Event> event_receiver_;
Sender<Task> task_sender_;
Receiver<Task> 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

View File

@ -0,0 +1,12 @@
#include <functional>
#include <variant>
#include "ftxui/component/event.hpp"
namespace ftxui {
using Closure = std::function<void()>;
using Task = std::variant<Event, Closure>;
} // 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.

View File

@ -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<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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>(event));
auto document = component->Render();
Render(screen, document);
}

View File

@ -1,18 +1,20 @@
#include <stdio.h> // for fileno, stdin
#include <algorithm> // for copy, max, min
#include <csignal> // for signal, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGWINCH
#include <cstdlib> // for NULL
#include <csignal> // for signal, raise, SIGTSTP, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGWINCH
#include <cstdlib> // for NULL
#include <functional> // for function
#include <initializer_list> // for initializer_list
#include <iostream> // for cout, ostream, basic_ostream, operator<<, endl, flush
#include <stack> // for stack
#include <thread> // for thread
#include <utility> // for swap, move
#include <vector> // for vector
#include <type_traits> // for decay_t
#include <utility> // for swap, move
#include <variant> // for visit
#include <vector> // 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 <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set
#include <termios.h> // for tcsetattr, tcgetattr, cc_t
#include <unistd.h> // for STDIN_FILENO, read
#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
#include <unistd.h> // 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<bool>* quit, Sender<Event> out) {
void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto console = GetStdHandle(STD_INPUT_HANDLE);
auto parser = TerminalInputParser(out->Clone());
while (!*quit) {
@ -104,7 +108,7 @@ void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
#include <emscripten.h>
// Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
void EventListener(std::atomic<bool>* quit, Sender<Task> 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<bool>* quit, Sender<Event> out) {
void EventListener(std::atomic<bool>* quit, Sender<Task> 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<ScreenInteractive::Callback> on_exit_functions;
std::stack<Closure> 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<void(void)> 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>();
event_sender_ = event_receiver_->MakeSender();
task_receiver_ = MakeReceiver<Task>();
}
// 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<DECMode> 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<decltype(arg)>;
if (event.is_mouse()) {
event.mouse().x -= cursor_x_;
event.mouse().y -= cursor_y_;
}
// Handle Event.
if constexpr (std::is_same_v<T, Event>) {
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<T, Closure>)
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.

View File

@ -1,15 +1,15 @@
#include "ftxui/component/terminal_input_parser.hpp"
#include <algorithm> // for max
#include <cstdint>
#include <cstdint> // for uint32_t
#include <memory> // for unique_ptr
#include <utility> // for move
#include "ftxui/component/event.hpp" // for Event
#include "ftxui/component/task.hpp" // for Task
namespace ftxui {
TerminalInputParser::TerminalInputParser(Sender<Event> out)
TerminalInputParser::TerminalInputParser(Sender<Task> out)
: out_(std::move(out)) {}
void TerminalInputParser::Timeout(int time) {

View File

@ -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<Event> out);
TerminalInputParser(Sender<Task> out);
void Timeout(int time);
void Add(char c);
@ -57,7 +58,7 @@ class TerminalInputParser {
Output ParseMouse(bool altered, bool pressed, std::vector<int> arguments);
Output ParseCursorReporting(std::vector<int> arguments);
Sender<Event> out_;
Sender<Task> out_;
int position_ = -1;
int timeout_ = 0;
std::string pending_;

View File

@ -2,6 +2,7 @@
#include <gtest/gtest-test-part.h> // for TestPartResult, SuiteApiResolver, TestFactoryImpl
#include <algorithm> // for max
#include <memory> // for unique_ptr, allocator
#include <variant> // 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<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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<Event>(received).is_character());
EXPECT_EQ(c, std::get<Event>(received).character()[0]);
}
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, EscapeKeyWithoutWaiting) {
auto event_receiver = MakeReceiver<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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<Event>(received), Event::Escape);
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, MouseLeftClick) {
auto event_receiver = MakeReceiver<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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<Event>(received).is_mouse());
EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button);
EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, MouseMiddleClick) {
auto event_receiver = MakeReceiver<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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<Event>(received).is_mouse());
EXPECT_EQ(Mouse::Middle, std::get<Event>(received).mouse().button);
EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, MouseRightClick) {
auto event_receiver = MakeReceiver<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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<Event>(received).is_mouse());
EXPECT_EQ(Mouse::Right, std::get<Event>(received).mouse().button);
EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
}
@ -216,16 +217,16 @@ TEST(Event, UTF8) {
};
for (auto test : kTestCase) {
auto event_receiver = MakeReceiver<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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<Event>(received).is_character());
}
EXPECT_FALSE(event_receiver->Receive(&received));
}

View File

@ -5,14 +5,14 @@
extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
using namespace ftxui;
auto event_receiver = MakeReceiver<Event>();
auto event_receiver = MakeReceiver<Task>();
{
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.