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(
int dimx,
352 bool use_alternative_screen)
353 : Screen(dimx, dimy),
354 dimension_(dimension),
355 use_alternative_screen_(use_alternative_screen) {
387 Dimension::Fullscreen,
400 Dimension::Fullscreen,
410 Dimension::TerminalOutput,
420 Dimension::FitComponent,
442 track_mouse_ = enable;
455 task_sender_->Send(std::move(task));
468 if (animation_requested_) {
471 animation_requested_ =
true;
472 auto now = animation::Clock::now();
473 const auto time_histeresis = std::chrono::milliseconds(33);
474 if (now - previous_animation_time_ >= time_histeresis) {
475 previous_animation_time_ = now;
484 if (mouse_captured) {
487 mouse_captured =
true;
488 return std::make_unique<CapturedMouseImpl>(
489 [
this] { mouse_captured =
false; });
496 class Loop loop(this, std::move(component));
502bool ScreenInteractive::HasQuitted() {
507void ScreenInteractive::PreMain() {
509 if (g_active_screen) {
510 std::swap(suspended_screen_, g_active_screen);
512 suspended_screen_->ResetCursorPosition();
514 suspended_screen_->
dimx_ = 0;
515 suspended_screen_->
dimy_ = 0;
518 suspended_screen_->Uninstall();
522 g_active_screen =
this;
523 g_active_screen->Install();
525 previous_animation_time_ = animation::Clock::now();
529void ScreenInteractive::PostMain() {
531 ResetCursorPosition();
533 g_active_screen =
nullptr;
536 if (suspended_screen_) {
542 std::swap(g_active_screen, suspended_screen_);
543 g_active_screen->Install();
550 if (!use_alternative_screen_) {
552 std::cout << std::flush;
571 force_handle_ctrl_c_ = force;
577 force_handle_ctrl_z_ = force;
585 return selection_->GetParts();
589 selection_on_change_ = std::move(callback);
595 return g_active_screen;
599void ScreenInteractive::Install() {
600 frame_valid_ =
false;
611 on_exit_functions.emplace([] { Flush(); });
617 std::cout << DECRQSS_DECSCUSR;
618 on_exit_functions.emplace([
this] {
619 std::cout <<
"\033[?25h";
620 std::cout <<
"\033[" + std::to_string(cursor_reset_shape_) +
" q";
625 for (
const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
626 InstallSignalHandler(signal);
632 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
633 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
637 GetConsoleMode(stdout_handle, &out_mode);
638 GetConsoleMode(stdin_handle, &in_mode);
639 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
640 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
643 const int enable_virtual_terminal_processing = 0x0004;
644 const int disable_newline_auto_return = 0x0008;
645 out_mode |= enable_virtual_terminal_processing;
646 out_mode |= disable_newline_auto_return;
649 const int enable_line_input = 0x0002;
650 const int enable_echo_input = 0x0004;
651 const int enable_virtual_terminal_input = 0x0200;
652 const int enable_window_input = 0x0008;
653 in_mode &= ~enable_echo_input;
654 in_mode &= ~enable_line_input;
655 in_mode |= enable_virtual_terminal_input;
656 in_mode |= enable_window_input;
658 SetConsoleMode(stdin_handle, in_mode);
659 SetConsoleMode(stdout_handle, out_mode);
661 for (
const int signal : {SIGWINCH, SIGTSTP}) {
662 InstallSignalHandler(signal);
665 struct termios terminal;
666 tcgetattr(STDIN_FILENO, &terminal);
667 on_exit_functions.emplace(
668 [=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
671 terminal.c_iflag &= ~IGNBRK;
672 terminal.c_iflag &= ~BRKINT;
674 terminal.c_iflag &= ~PARMRK;
675 terminal.c_iflag &= ~ISTRIP;
676 terminal.c_iflag &= ~INLCR;
677 terminal.c_iflag &= ~IGNCR;
678 terminal.c_iflag &= ~ICRNL;
679 terminal.c_iflag &= ~IXON;
681 terminal.c_lflag &= ~ECHO;
682 terminal.c_lflag &= ~ECHONL;
683 terminal.c_lflag &= ~ICANON;
684 terminal.c_lflag &= ~ISIG;
689 terminal.c_lflag &= ~IEXTEN;
690 terminal.c_cflag |= CS8;
692 terminal.c_cc[VMIN] = 0;
694 terminal.c_cc[VTIME] = 0;
696 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
700 auto enable = [&](
const std::vector<DECMode>& parameters) {
701 std::cout << Set(parameters);
702 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
705 auto disable = [&](
const std::vector<DECMode>& parameters) {
706 std::cout << Reset(parameters);
707 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
710 if (use_alternative_screen_) {
712 DECMode::kAlternateScreen,
722 enable({DECMode::kMouseVt200});
723 enable({DECMode::kMouseAnyEvent});
724 enable({DECMode::kMouseUrxvtMode});
725 enable({DECMode::kMouseSgrExtMode});
733 task_sender_ = task_receiver_->MakeSender();
735 std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
736 animation_listener_ =
737 std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
741void ScreenInteractive::Uninstall() {
743 event_listener_.join();
744 animation_listener_.join();
750void ScreenInteractive::RunOnceBlocking(
Component component) {
751 ExecuteSignalHandlers();
753 if (task_receiver_->Receive(&task)) {
754 HandleTask(component, task);
760void ScreenInteractive::RunOnce(
Component component) {
762 while (task_receiver_->ReceiveNonBlocking(&task)) {
763 HandleTask(component, task);
764 ExecuteSignalHandlers();
766 Draw(std::move(component));
768 if (selection_data_previous_ != selection_data_) {
769 selection_data_previous_ = selection_data_;
770 if (selection_on_change_) {
771 selection_on_change_();
779void ScreenInteractive::HandleTask(
Component component,
Task& task) {
782 using T = std::decay_t<
decltype(arg)>;
786 if constexpr (std::is_same_v<T, Event>) {
787 if (arg.is_cursor_position()) {
788 cursor_x_ = arg.cursor_x();
789 cursor_y_ = arg.cursor_y();
793 if (arg.is_cursor_shape()) {
794 cursor_reset_shape_= arg.cursor_shape();
798 if (arg.is_mouse()) {
799 arg.mouse().x -= cursor_x_;
800 arg.mouse().y -= cursor_y_;
805 bool handled = component->OnEvent(arg);
807 handled = HandleSelection(handled, arg);
809 if (arg ==
Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
810 RecordSignal(SIGABRT);
814 if (arg ==
Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
815 RecordSignal(SIGTSTP);
819 frame_valid_ =
false;
824 if constexpr (std::is_same_v<T, Closure>) {
830 if constexpr (std::is_same_v<T, AnimationTask>) {
831 if (!animation_requested_) {
835 animation_requested_ =
false;
838 previous_animation_time_ = now;
840 animation::Params params(delta);
841 component->OnAnimation(params);
842 frame_valid_ =
false;
851bool ScreenInteractive::HandleSelection(
bool handled, Event event) {
853 selection_pending_ =
nullptr;
854 selection_data_.empty =
true;
855 selection_ =
nullptr;
859 if (!event.is_mouse()) {
863 auto& mouse =
event.mouse();
870 selection_data_.start_x = mouse.x;
871 selection_data_.start_y = mouse.y;
872 selection_data_.end_x = mouse.x;
873 selection_data_.end_y = mouse.y;
877 if (!selection_pending_) {
882 if ((mouse.x != selection_data_.end_x) ||
883 (mouse.y != selection_data_.end_y)) {
884 selection_data_.end_x = mouse.x;
885 selection_data_.end_y = mouse.y;
886 selection_data_.empty =
false;
893 selection_pending_ =
nullptr;
894 selection_data_.end_x = mouse.x;
895 selection_data_.end_y = mouse.y;
896 selection_data_.empty =
false;
905void ScreenInteractive::Draw(
Component component) {
909 auto document = component->Render();
913 document->ComputeRequirement();
914 switch (dimension_) {
915 case Dimension::Fixed:
919 case Dimension::TerminalOutput:
920 dimx = terminal.dimx;
923 case Dimension::Fullscreen:
924 dimx = terminal.dimx;
925 dimy = terminal.dimy;
927 case Dimension::FitComponent:
934 ResetCursorPosition();
939 if ((
dimx <
dimx_) && !use_alternative_screen_) {
940 std::cout <<
"\033[J";
941 std::cout <<
"\033[H";
948 pixels_ = std::vector<std::vector<Pixel>>(
dimy, std::vector<Pixel>(
dimx));
956#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
965 if (!use_alternative_screen_ && (i % 150 == 0)) {
966 std::cout << DeviceStatusReport(DSRMode::kCursor);
971 if (!use_alternative_screen_ &&
972 (previous_frame_resized_ || i % 40 == 0)) {
973 std::cout << DeviceStatusReport(DSRMode::kCursor);
976 previous_frame_resized_ = resized;
978 selection_ = selection_data_.empty
979 ? std::make_unique<Selection>()
980 : std::make_unique<Selection>(
981 selection_data_.start_x, selection_data_.start_y,
982 selection_data_.end_x, selection_data_.end_y);
983 Render(*
this, document.get(), *selection_);
990 set_cursor_position.clear();
991 reset_cursor_position.clear();
994 set_cursor_position +=
"\x1B[" + std::to_string(dy) +
"A";
995 reset_cursor_position +=
"\x1B[" + std::to_string(dy) +
"B";
999 set_cursor_position +=
"\x1B[" + std::to_string(dx) +
"D";
1000 reset_cursor_position +=
"\x1B[" + std::to_string(dx) +
"C";
1004 set_cursor_position +=
"\033[?25l";
1006 set_cursor_position +=
"\033[?25h";
1007 set_cursor_position +=
1012 std::cout <<
ToString() << set_cursor_position;
1015 frame_valid_ =
true;
1019void ScreenInteractive::ResetCursorPosition() {
1020 std::cout << reset_cursor_position;
1021 reset_cursor_position =
"";
1027 return [
this] {
Exit(); };
1033 Post([
this] { ExitNow(); });
1037void ScreenInteractive::ExitNow() {
1039 task_sender_.reset();
1043void ScreenInteractive::Signal(
int signal) {
1044 if (signal == SIGABRT) {
1051 if (signal == SIGTSTP) {
1053 ResetCursorPosition();
1059 std::ignore = std::raise(SIGTSTP);
1065 if (signal == SIGWINCH) {
1072bool ScreenInteractive::SelectionData::operator==(
1073 const ScreenInteractive::SelectionData& other)
const {
1074 if (empty && other.empty) {
1077 if (empty || other.empty) {
1080 return start_x == other.start_x && start_y == other.start_y &&
1081 end_x == other.end_x && end_y == other.end_y;
1084bool ScreenInteractive::SelectionData::operator!=(
1085 const ScreenInteractive::SelectionData& other)
const {
1086 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()
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
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.