15#include <initializer_list>
40#define DEFINE_CONSOLEV2_PROPERTIES
41#define WIN32_LEAN_AND_MEAN
47#error Must be compiled in UNICODE mode
50#include <sys/select.h>
56#if defined(__clang__) && defined(__APPLE__)
57#define quick_exit(a) exit(a)
66 screen->RequestAnimationFrame();
73ScreenInteractive* g_active_screen =
nullptr;
77 std::cout <<
'\0' << std::flush;
80constexpr int timeout_milliseconds = 20;
81[[maybe_unused]]
constexpr int timeout_microseconds =
82 timeout_milliseconds * 1000;
85void EventListener(std::atomic<bool>* quit,
Sender<Task> out) {
86 auto console = GetStdHandle(STD_INPUT_HANDLE);
87 auto parser = TerminalInputParser(out->Clone());
91 auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
92 if (wait_result == WAIT_TIMEOUT) {
93 parser.Timeout(timeout_milliseconds);
97 DWORD number_of_events = 0;
98 if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
100 if (number_of_events <= 0)
103 std::vector<INPUT_RECORD> records{number_of_events};
104 DWORD number_of_events_read = 0;
105 ReadConsoleInput(console, records.data(), (DWORD)records.size(),
106 &number_of_events_read);
107 records.resize(number_of_events_read);
109 for (
const auto& r : records) {
110 switch (r.EventType) {
112 auto key_event = r.Event.KeyEvent;
114 if (key_event.bKeyDown == FALSE)
116 std::wstring wstring;
117 wstring += key_event.uChar.UnicodeChar;
122 case WINDOW_BUFFER_SIZE_EVENT:
135#elif defined(__EMSCRIPTEN__)
136#include <emscripten.h>
139void EventListener(std::atomic<bool>* quit,
Sender<Task> out) {
140 auto parser = TerminalInputParser(std::move(out));
144 while (read(STDIN_FILENO, &c, 1), c)
154void ftxui_on_resize(
int columns,
int rows) {
159 std::raise(SIGWINCH);
165int CheckStdinReady(
int usec_timeout) {
166 timeval tv = {0, usec_timeout};
169 FD_SET(STDIN_FILENO, &fds);
170 select(STDIN_FILENO + 1, &fds,
nullptr,
nullptr, &tv);
171 return FD_ISSET(STDIN_FILENO, &fds);
175void EventListener(std::atomic<bool>* quit,
Sender<Task> out) {
176 auto parser = TerminalInputParser(std::move(out));
179 if (!CheckStdinReady(timeout_microseconds)) {
180 parser.Timeout(timeout_milliseconds);
184 const size_t buffer_size = 100;
185 std::array<char, buffer_size> buffer;
186 size_t l = read(fileno(stdin), buffer.data(), buffer_size);
187 for (
size_t i = 0; i < l; ++i) {
188 parser.Add(buffer[i]);
194std::stack<Closure> on_exit_functions;
196 while (!on_exit_functions.empty()) {
197 on_exit_functions.top()();
198 on_exit_functions.pop();
202std::atomic<int> g_signal_exit_count = 0;
204std::atomic<int> g_signal_stop_count = 0;
205std::atomic<int> g_signal_resize_count = 0;
209void RecordSignal(
int signal) {
217 g_signal_exit_count++;
222 g_signal_stop_count++;
226 g_signal_resize_count++;
235void ExecuteSignalHandlers() {
236 int signal_exit_count = g_signal_exit_count.exchange(0);
237 while (signal_exit_count--) {
242 int signal_stop_count = g_signal_stop_count.exchange(0);
243 while (signal_stop_count--) {
247 int signal_resize_count = g_signal_resize_count.exchange(0);
248 while (signal_resize_count--) {
254void InstallSignalHandler(
int sig) {
255 auto old_signal_handler = std::signal(sig, RecordSignal);
256 on_exit_functions.emplace(
257 [=] { std::ignore = std::signal(sig, old_signal_handler); });
261const std::string CSI =
"\x1b[";
264const std::string DCS =
"\x1bP";
266const std::string ST =
"\x1b\\";
270const std::string DECRQSS_DECSCUSR = DCS +
"$q q" + ST;
273enum class DECMode : std::uint16_t {
279 kMouseVt200Highlight = 1001,
281 kMouseBtnEventMouse = 1002,
282 kMouseAnyEvent = 1003,
285 kMouseSgrExtMode = 1006,
286 kMouseUrxvtMode = 1015,
287 kMouseSgrPixelsMode = 1016,
288 kAlternateScreen = 1049,
292enum class DSRMode : std::uint8_t {
296std::string Serialize(
const std::vector<DECMode>& parameters) {
299 for (
const DECMode parameter : parameters) {
303 out += std::to_string(
int(parameter));
310std::string Set(
const std::vector<DECMode>& parameters) {
311 return CSI +
"?" + Serialize(parameters) +
"h";
315std::string Reset(
const std::vector<DECMode>& parameters) {
316 return CSI +
"?" + Serialize(parameters) +
"l";
320std::string DeviceStatusReport(DSRMode ps) {
321 return CSI + std::to_string(
int(ps)) +
"n";
324class CapturedMouseImpl :
public CapturedMouseInterface {
326 explicit CapturedMouseImpl(std::function<
void(
void)> callback)
327 : callback_(std::move(callback)) {}
328 ~CapturedMouseImpl()
override { callback_(); }
329 CapturedMouseImpl(
const CapturedMouseImpl&) =
delete;
330 CapturedMouseImpl(CapturedMouseImpl&&) =
delete;
331 CapturedMouseImpl& operator=(
const CapturedMouseImpl&) =
delete;
332 CapturedMouseImpl& operator=(CapturedMouseImpl&&) =
delete;
335 std::function<void(
void)> callback_;
338void AnimationListener(std::atomic<bool>* quit,
Sender<Task> out) {
340 const auto time_delta = std::chrono::milliseconds(15);
342 out->Send(AnimationTask());
343 std::this_thread::sleep_for(time_delta);
349ScreenInteractive::ScreenInteractive(Dimension dimension,
352 bool use_alternative_screen)
353 : Screen(dimx, dimy),
354 dimension_(dimension),
355 use_alternative_screen_(use_alternative_screen) {
384 Dimension::Fullscreen,
397 Dimension::Fullscreen,
410 Dimension::TerminalOutput,
423 Dimension::FitComponent,
446 track_mouse_ = enable;
458 task_sender_->Send(std::move(task));
470 if (animation_requested_) {
473 animation_requested_ =
true;
474 auto now = animation::Clock::now();
475 const auto time_histeresis = std::chrono::milliseconds(33);
476 if (now - previous_animation_time_ >= time_histeresis) {
477 previous_animation_time_ = now;
485 if (mouse_captured) {
488 mouse_captured =
true;
489 return std::make_unique<CapturedMouseImpl>(
490 [
this] { mouse_captured =
false; });
496 class Loop loop(this, std::move(component));
501bool ScreenInteractive::HasQuitted() {
506void ScreenInteractive::PreMain() {
508 if (g_active_screen) {
509 std::swap(suspended_screen_, g_active_screen);
511 suspended_screen_->ResetCursorPosition();
513 suspended_screen_->
dimx_ = 0;
514 suspended_screen_->
dimy_ = 0;
517 suspended_screen_->Uninstall();
521 g_active_screen =
this;
522 g_active_screen->Install();
524 previous_animation_time_ = animation::Clock::now();
528void ScreenInteractive::PostMain() {
530 ResetCursorPosition();
532 g_active_screen =
nullptr;
535 if (suspended_screen_) {
541 std::swap(g_active_screen, suspended_screen_);
542 g_active_screen->Install();
549 if (!use_alternative_screen_) {
551 std::cout << std::flush;
570 force_handle_ctrl_c_ = force;
576 force_handle_ctrl_z_ = force;
584 return selection_->GetParts();
588 selection_on_change_ = std::move(callback);
594 return g_active_screen;
598void ScreenInteractive::Install() {
599 frame_valid_ =
false;
610 on_exit_functions.emplace([] { Flush(); });
616 std::cout << DECRQSS_DECSCUSR;
617 on_exit_functions.emplace([
this] {
618 std::cout <<
"\033[?25h";
619 std::cout <<
"\033[" + std::to_string(cursor_reset_shape_) +
" q";
624 for (
const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
625 InstallSignalHandler(signal);
631 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
632 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
636 GetConsoleMode(stdout_handle, &out_mode);
637 GetConsoleMode(stdin_handle, &in_mode);
638 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
639 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
642 const int enable_virtual_terminal_processing = 0x0004;
643 const int disable_newline_auto_return = 0x0008;
644 out_mode |= enable_virtual_terminal_processing;
645 out_mode |= disable_newline_auto_return;
648 const int enable_line_input = 0x0002;
649 const int enable_echo_input = 0x0004;
650 const int enable_virtual_terminal_input = 0x0200;
651 const int enable_window_input = 0x0008;
652 in_mode &= ~enable_echo_input;
653 in_mode &= ~enable_line_input;
654 in_mode |= enable_virtual_terminal_input;
655 in_mode |= enable_window_input;
657 SetConsoleMode(stdin_handle, in_mode);
658 SetConsoleMode(stdout_handle, out_mode);
660 for (
const int signal : {SIGWINCH, SIGTSTP}) {
661 InstallSignalHandler(signal);
664 struct termios terminal;
665 tcgetattr(STDIN_FILENO, &terminal);
666 on_exit_functions.emplace(
667 [=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
670 terminal.c_iflag &= ~IGNBRK;
671 terminal.c_iflag &= ~BRKINT;
673 terminal.c_iflag &= ~PARMRK;
674 terminal.c_iflag &= ~ISTRIP;
675 terminal.c_iflag &= ~INLCR;
676 terminal.c_iflag &= ~IGNCR;
677 terminal.c_iflag &= ~ICRNL;
678 terminal.c_iflag &= ~IXON;
680 terminal.c_lflag &= ~ECHO;
681 terminal.c_lflag &= ~ECHONL;
682 terminal.c_lflag &= ~ICANON;
683 terminal.c_lflag &= ~ISIG;
688 terminal.c_lflag &= ~IEXTEN;
689 terminal.c_cflag |= CS8;
691 terminal.c_cc[VMIN] = 0;
693 terminal.c_cc[VTIME] = 0;
695 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
699 auto enable = [&](
const std::vector<DECMode>& parameters) {
700 std::cout << Set(parameters);
701 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
704 auto disable = [&](
const std::vector<DECMode>& parameters) {
705 std::cout << Reset(parameters);
706 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
709 if (use_alternative_screen_) {
711 DECMode::kAlternateScreen,
721 enable({DECMode::kMouseVt200});
722 enable({DECMode::kMouseAnyEvent});
723 enable({DECMode::kMouseUrxvtMode});
724 enable({DECMode::kMouseSgrExtMode});
732 task_sender_ = task_receiver_->MakeSender();
734 std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
735 animation_listener_ =
736 std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
740void ScreenInteractive::Uninstall() {
742 event_listener_.join();
743 animation_listener_.join();
749void ScreenInteractive::RunOnceBlocking(
Component component) {
750 ExecuteSignalHandlers();
752 if (task_receiver_->Receive(&task)) {
753 HandleTask(component, task);
759void ScreenInteractive::RunOnce(
Component component) {
761 while (task_receiver_->ReceiveNonBlocking(&task)) {
762 HandleTask(component, task);
763 ExecuteSignalHandlers();
765 Draw(std::move(component));
767 if (selection_data_previous_ != selection_data_) {
768 selection_data_previous_ = selection_data_;
769 if (selection_on_change_) {
770 selection_on_change_();
778void ScreenInteractive::HandleTask(
Component component,
Task& task) {
781 using T = std::decay_t<
decltype(arg)>;
785 if constexpr (std::is_same_v<T, Event>) {
786 if (arg.is_cursor_position()) {
787 cursor_x_ = arg.cursor_x();
788 cursor_y_ = arg.cursor_y();
792 if (arg.is_cursor_shape()) {
793 cursor_reset_shape_= arg.cursor_shape();
797 if (arg.is_mouse()) {
798 arg.mouse().x -= cursor_x_;
799 arg.mouse().y -= cursor_y_;
804 bool handled = component->OnEvent(arg);
806 handled = HandleSelection(handled, arg);
808 if (arg ==
Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
809 RecordSignal(SIGABRT);
813 if (arg ==
Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
814 RecordSignal(SIGTSTP);
818 frame_valid_ =
false;
823 if constexpr (std::is_same_v<T, Closure>) {
829 if constexpr (std::is_same_v<T, AnimationTask>) {
830 if (!animation_requested_) {
834 animation_requested_ =
false;
837 previous_animation_time_ = now;
839 animation::Params params(delta);
840 component->OnAnimation(params);
841 frame_valid_ =
false;
850bool ScreenInteractive::HandleSelection(
bool handled, Event event) {
852 selection_pending_ =
nullptr;
853 selection_data_.empty =
true;
854 selection_ =
nullptr;
858 if (!event.is_mouse()) {
862 auto& mouse =
event.mouse();
869 selection_data_.start_x = mouse.x;
870 selection_data_.start_y = mouse.y;
871 selection_data_.end_x = mouse.x;
872 selection_data_.end_y = mouse.y;
876 if (!selection_pending_) {
881 if ((mouse.x != selection_data_.end_x) ||
882 (mouse.y != selection_data_.end_y)) {
883 selection_data_.end_x = mouse.x;
884 selection_data_.end_y = mouse.y;
885 selection_data_.empty =
false;
892 selection_pending_ =
nullptr;
893 selection_data_.end_x = mouse.x;
894 selection_data_.end_y = mouse.y;
895 selection_data_.empty =
false;
904void ScreenInteractive::Draw(
Component component) {
908 auto document = component->Render();
912 document->ComputeRequirement();
913 switch (dimension_) {
914 case Dimension::Fixed:
918 case Dimension::TerminalOutput:
919 dimx = terminal.dimx;
922 case Dimension::Fullscreen:
923 dimx = terminal.dimx;
924 dimy = terminal.dimy;
926 case Dimension::FitComponent:
933 ResetCursorPosition();
938 if ((
dimx <
dimx_) && !use_alternative_screen_) {
939 std::cout <<
"\033[J";
940 std::cout <<
"\033[H";
947 pixels_ = std::vector<std::vector<Pixel>>(
dimy, std::vector<Pixel>(
dimx));
955#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
964 if (!use_alternative_screen_ && (i % 150 == 0)) {
965 std::cout << DeviceStatusReport(DSRMode::kCursor);
970 if (!use_alternative_screen_ &&
971 (previous_frame_resized_ || i % 40 == 0)) {
972 std::cout << DeviceStatusReport(DSRMode::kCursor);
975 previous_frame_resized_ = resized;
977 selection_ = selection_data_.empty
978 ? std::make_unique<Selection>()
979 : std::make_unique<Selection>(
980 selection_data_.start_x, selection_data_.start_y,
981 selection_data_.end_x, selection_data_.end_y);
982 Render(*
this, document.get(), *selection_);
989 set_cursor_position.clear();
990 reset_cursor_position.clear();
993 set_cursor_position +=
"\x1B[" + std::to_string(dy) +
"A";
994 reset_cursor_position +=
"\x1B[" + std::to_string(dy) +
"B";
998 set_cursor_position +=
"\x1B[" + std::to_string(dx) +
"D";
999 reset_cursor_position +=
"\x1B[" + std::to_string(dx) +
"C";
1003 set_cursor_position +=
"\033[?25l";
1005 set_cursor_position +=
"\033[?25h";
1006 set_cursor_position +=
1011 std::cout <<
ToString() << set_cursor_position;
1014 frame_valid_ =
true;
1019void ScreenInteractive::ResetCursorPosition() {
1020 std::cout << reset_cursor_position;
1021 reset_cursor_position =
"";
1026 return [
this] {
Exit(); };
1031 Post([
this] { ExitNow(); });
1035void ScreenInteractive::ExitNow() {
1037 task_sender_.reset();
1041void ScreenInteractive::Signal(
int signal) {
1042 if (signal == SIGABRT) {
1049 if (signal == SIGTSTP) {
1051 ResetCursorPosition();
1057 std::ignore = std::raise(SIGTSTP);
1063 if (signal == SIGWINCH) {
1070bool ScreenInteractive::SelectionData::operator==(
1071 const ScreenInteractive::SelectionData& other)
const {
1072 if (empty && other.empty) {
1075 if (empty || other.empty) {
1078 return start_x == other.start_x && start_y == other.start_y &&
1079 end_x == other.end_x && end_y == other.end_y;
1082bool ScreenInteractive::SelectionData::operator!=(
1083 const ScreenInteractive::SelectionData& other)
const {
1084 return !(*
this == other);
static void Signal(ScreenInteractive &s, int signal)
static ScreenInteractive TerminalOutput()
bool HasQuitted()
Whether the loop has quitted.
void Exit()
Exit the main loop.
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
Add an event to the main loop. It will be executed later, after every other scheduled events.
void Post(Task task)
Add a task to the main loop. It will be executed later, after every other scheduled tasks.
static ScreenInteractive FitComponent()
static ScreenInteractive Fullscreen()
static const Event Custom
static ScreenInteractive FullscreenPrimaryScreen()
static ScreenInteractive * Active()
Return the currently active screen, or null if none.
CapturedMouse CaptureMouse()
Try to get the unique lock about behing able to capture the mouse.
std::string GetSelection()
Returns the content of the current selection.
static ScreenInteractive FullscreenAlternateScreen()
void TrackMouse(bool enable=true)
Set whether mouse is tracked and events reported. called outside of the main loop....
void SelectionChange(std::function< void()> callback)
void RequestAnimationFrame()
Add a task to draw the screen one more time, until all the animations are done.
Closure ExitLoopClosure()
Return a function to exit the main loop.
void ForceHandleCtrlC(bool force)
Force FTXUI to handle or not handle Ctrl-C, even if the component catches the Event::CtrlC.
void ForceHandleCtrlZ(bool force)
Force FTXUI to handle or not handle Ctrl-Z, even if the component catches the Event::CtrlZ.
Closure WithRestoredIO(Closure)
Decorate a function. It executes the same way, but with the currently active screen terminal hooks te...
static Event Special(std::string)
An custom event whose meaning is defined by the user of the library.
Loop is a class that manages the event loop for a component.
ScreenInteractive is a Screen that can handle events, run a main loop, and manage components.
void RequestAnimationFrame()
RequestAnimationFrame is a function that requests a new frame to be drawn in the next animation cycle...
Represent an event. It can be key press event, a terminal resize, or more ...
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
std::string ToString() const
std::string ResetPosition(bool clear=false) const
Return a string to be printed in order to reset the cursor position to the beginning of the screen.
void Clear()
Clear all the pixel from the screen.
std::vector< std::vector< Pixel > > pixels_
Dimensions Size()
Get the terminal size.
void SetFallbackSize(const Dimensions &fallbackSize)
Override terminal size in case auto-detection fails.
std::chrono::duration< float > Duration
std::chrono::time_point< Clock > TimePoint
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
The FTXUI ftxui:: namespace.
std::unique_ptr< CapturedMouseInterface > CapturedMouse
Receiver< T > MakeReceiver()
std::string to_string(const std::wstring &s)
Convert a std::wstring into a UTF8 std::string.
std::unique_ptr< SenderImpl< T > > Sender
Element select(Element e)
Set the child to be the one focused among its siblings.
std::variant< Event, Closure, AnimationTask > Task
std::function< void()> Closure
std::shared_ptr< ComponentBase > Component