15#include <initializer_list>
39#define DEFINE_CONSOLEV2_PROPERTIES
40#define WIN32_LEAN_AND_MEAN
46#error Must be compiled in UNICODE mode
49#include <sys/select.h>
55#if defined(__clang__) && defined(__APPLE__)
56#define quick_exit(a) exit(a)
65 screen->RequestAnimationFrame();
72ScreenInteractive* g_active_screen =
nullptr;
76 std::cout <<
'\0' << std::flush;
79constexpr int timeout_milliseconds = 20;
80[[maybe_unused]]
constexpr int timeout_microseconds =
81 timeout_milliseconds * 1000;
84void EventListener(std::atomic<bool>* quit,
Sender<Task> out) {
85 auto console = GetStdHandle(STD_INPUT_HANDLE);
86 auto parser = TerminalInputParser(out->Clone());
90 auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
91 if (wait_result == WAIT_TIMEOUT) {
92 parser.Timeout(timeout_milliseconds);
96 DWORD number_of_events = 0;
97 if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
99 if (number_of_events <= 0)
102 std::vector<INPUT_RECORD> records{number_of_events};
103 DWORD number_of_events_read = 0;
104 ReadConsoleInput(console, records.data(), (DWORD)records.size(),
105 &number_of_events_read);
106 records.resize(number_of_events_read);
108 for (
const auto& r : records) {
109 switch (r.EventType) {
111 auto key_event = r.Event.KeyEvent;
113 if (key_event.bKeyDown == FALSE)
115 std::wstring wstring;
116 wstring += key_event.uChar.UnicodeChar;
121 case WINDOW_BUFFER_SIZE_EVENT:
134#elif defined(__EMSCRIPTEN__)
135#include <emscripten.h>
138void EventListener(std::atomic<bool>* quit,
Sender<Task> out) {
139 auto parser = TerminalInputParser(std::move(out));
143 while (read(STDIN_FILENO, &c, 1), c)
153void ftxui_on_resize(
int columns,
int rows) {
158 std::raise(SIGWINCH);
164int CheckStdinReady(
int usec_timeout) {
165 timeval tv = {0, usec_timeout};
168 FD_SET(STDIN_FILENO, &fds);
169 select(STDIN_FILENO + 1, &fds,
nullptr,
nullptr, &tv);
170 return FD_ISSET(STDIN_FILENO, &fds);
174void EventListener(std::atomic<bool>* quit,
Sender<Task> out) {
175 auto parser = TerminalInputParser(std::move(out));
178 if (!CheckStdinReady(timeout_microseconds)) {
179 parser.Timeout(timeout_milliseconds);
183 const size_t buffer_size = 100;
184 std::array<char, buffer_size> buffer;
185 size_t l = read(fileno(stdin), buffer.data(), buffer_size);
186 for (
size_t i = 0; i < l; ++i) {
187 parser.Add(buffer[i]);
193std::stack<Closure> on_exit_functions;
195 while (!on_exit_functions.empty()) {
196 on_exit_functions.top()();
197 on_exit_functions.pop();
201std::atomic<int> g_signal_exit_count = 0;
203std::atomic<int> g_signal_stop_count = 0;
204std::atomic<int> g_signal_resize_count = 0;
208void RecordSignal(
int signal) {
216 g_signal_exit_count++;
221 g_signal_stop_count++;
225 g_signal_resize_count++;
234void ExecuteSignalHandlers() {
235 int signal_exit_count = g_signal_exit_count.exchange(0);
236 while (signal_exit_count--) {
241 int signal_stop_count = g_signal_stop_count.exchange(0);
242 while (signal_stop_count--) {
246 int signal_resize_count = g_signal_resize_count.exchange(0);
247 while (signal_resize_count--) {
253void InstallSignalHandler(
int sig) {
254 auto old_signal_handler = std::signal(sig, RecordSignal);
255 on_exit_functions.emplace(
256 [=] { std::ignore = std::signal(sig, old_signal_handler); });
260const std::string CSI =
"\x1b[";
263const std::string DCS =
"\x1bP";
265const std::string ST =
"\x1b\\";
269const std::string DECRQSS_DECSCUSR = DCS +
"$q q" + ST;
272enum class DECMode : std::uint16_t {
278 kMouseVt200Highlight = 1001,
280 kMouseBtnEventMouse = 1002,
281 kMouseAnyEvent = 1003,
284 kMouseSgrExtMode = 1006,
285 kMouseUrxvtMode = 1015,
286 kMouseSgrPixelsMode = 1016,
287 kAlternateScreen = 1049,
291enum class DSRMode : std::uint8_t {
295std::string Serialize(
const std::vector<DECMode>& parameters) {
298 for (
const DECMode parameter : parameters) {
302 out += std::to_string(
int(parameter));
309std::string Set(
const std::vector<DECMode>& parameters) {
310 return CSI +
"?" + Serialize(parameters) +
"h";
314std::string Reset(
const std::vector<DECMode>& parameters) {
315 return CSI +
"?" + Serialize(parameters) +
"l";
319std::string DeviceStatusReport(DSRMode ps) {
320 return CSI + std::to_string(
int(ps)) +
"n";
323class CapturedMouseImpl :
public CapturedMouseInterface {
325 explicit CapturedMouseImpl(std::function<
void(
void)> callback)
326 : callback_(std::move(callback)) {}
327 ~CapturedMouseImpl()
override { callback_(); }
328 CapturedMouseImpl(
const CapturedMouseImpl&) =
delete;
329 CapturedMouseImpl(CapturedMouseImpl&&) =
delete;
330 CapturedMouseImpl& operator=(
const CapturedMouseImpl&) =
delete;
331 CapturedMouseImpl& operator=(CapturedMouseImpl&&) =
delete;
334 std::function<void(
void)> callback_;
337void AnimationListener(std::atomic<bool>* quit,
Sender<Task> out) {
339 const auto time_delta = std::chrono::milliseconds(15);
341 out->Send(AnimationTask());
342 std::this_thread::sleep_for(time_delta);
348ScreenInteractive::ScreenInteractive(
int dimx,
351 bool use_alternative_screen)
352 : Screen(dimx, dimy),
353 dimension_(dimension),
354 use_alternative_screen_(use_alternative_screen) {
386 Dimension::Fullscreen,
399 Dimension::Fullscreen,
409 Dimension::TerminalOutput,
419 Dimension::FitComponent,
441 track_mouse_ = enable;
454 task_sender_->Send(std::move(task));
467 if (animation_requested_) {
470 animation_requested_ =
true;
471 auto now = animation::Clock::now();
472 const auto time_histeresis = std::chrono::milliseconds(33);
473 if (now - previous_animation_time_ >= time_histeresis) {
474 previous_animation_time_ = now;
483 if (mouse_captured) {
486 mouse_captured =
true;
487 return std::make_unique<CapturedMouseImpl>(
488 [
this] { mouse_captured =
false; });
495 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;
920 dimy = document->requirement().min_y;
922 case Dimension::Fullscreen:
923 dimx = terminal.dimx;
924 dimy = terminal.dimy;
926 case Dimension::FitComponent:
927 dimx = std::min(document->requirement().min_x, terminal.dimx);
928 dimy = std::min(document->requirement().min_y, terminal.dimy);
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;
1018void ScreenInteractive::ResetCursorPosition() {
1019 std::cout << reset_cursor_position;
1020 reset_cursor_position =
"";
1026 return [
this] {
Exit(); };
1032 Post([
this] { ExitNow(); });
1036void ScreenInteractive::ExitNow() {
1038 task_sender_.reset();
1042void ScreenInteractive::Signal(
int signal) {
1043 if (signal == SIGABRT) {
1050 if (signal == SIGTSTP) {
1052 ResetCursorPosition();
1058 std::ignore = std::raise(SIGTSTP);
1064 if (signal == SIGWINCH) {
1071bool ScreenInteractive::SelectionData::operator==(
1072 const ScreenInteractive::SelectionData& other)
const {
1073 if (empty && other.empty) {
1076 if (empty || other.empty) {
1079 return start_x == other.start_x && start_y == other.start_y &&
1080 end_x == other.end_x && end_y == other.end_y;
1083bool ScreenInteractive::SelectionData::operator!=(
1084 const ScreenInteractive::SelectionData& other)
const {
1085 return !(*
this == other);
std::vector< std::vector< Pixel > > pixels_
bool HasQuitted()
Whether the loop has quitted.
static void Signal(ScreenInteractive &s, int signal)
static ScreenInteractive TerminalOutput()
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 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...
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.
void SetFallbackSize(const Dimensions &fallbackSize)
Override terminal size in case auto-detection fails.
Dimensions Size()
Get the terminal size.
std::chrono::duration< float > Duration
std::chrono::time_point< Clock > TimePoint
void RequestAnimationFrame()
std::unique_ptr< CapturedMouseInterface > CapturedMouse
Receiver< T > MakeReceiver()
std::string to_string(const std::wstring &s)
Convert a UTF8 std::string into a std::wstring.
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
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
std::function< void()> Closure
std::shared_ptr< ComponentBase > Component
Represent an event. It can be key press event, a terminal resize, or more ...
static const Event Custom
static Event Special(std::string)
An custom event whose meaning is defined by the user of the library.