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);
88 TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
92 auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
93 if (wait_result == WAIT_TIMEOUT) {
94 parser.Timeout(timeout_milliseconds);
98 DWORD number_of_events = 0;
99 if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
101 if (number_of_events <= 0)
104 std::vector<INPUT_RECORD> records{number_of_events};
105 DWORD number_of_events_read = 0;
106 ReadConsoleInput(console, records.data(), (DWORD)records.size(),
107 &number_of_events_read);
108 records.resize(number_of_events_read);
110 for (
const auto& r : records) {
111 switch (r.EventType) {
113 auto key_event = r.Event.KeyEvent;
115 if (key_event.bKeyDown == FALSE)
117 std::wstring wstring;
118 wstring += key_event.uChar.UnicodeChar;
123 case WINDOW_BUFFER_SIZE_EVENT:
136#elif defined(__EMSCRIPTEN__)
137#include <emscripten.h>
140void EventListener(std::atomic<bool>* quit,
Sender<Task> out) {
142 TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
146 while (read(STDIN_FILENO, &c, 1), c)
156void ftxui_on_resize(
int columns,
int rows) {
161 std::raise(SIGWINCH);
167int CheckStdinReady(
int usec_timeout) {
168 timeval tv = {0, usec_timeout};
171 FD_SET(STDIN_FILENO, &fds);
172 select(STDIN_FILENO + 1, &fds,
nullptr,
nullptr, &tv);
173 return FD_ISSET(STDIN_FILENO, &fds);
177void EventListener(std::atomic<bool>* quit,
Sender<Task> out) {
179 TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
182 if (!CheckStdinReady(timeout_microseconds)) {
183 parser.Timeout(timeout_milliseconds);
187 const size_t buffer_size = 100;
188 std::array<char, buffer_size> buffer;
189 size_t l = read(fileno(stdin), buffer.data(), buffer_size);
190 for (
size_t i = 0; i < l; ++i) {
191 parser.Add(buffer[i]);
197std::stack<Closure> on_exit_functions;
199 while (!on_exit_functions.empty()) {
200 on_exit_functions.top()();
201 on_exit_functions.pop();
205std::atomic<int> g_signal_exit_count = 0;
207std::atomic<int> g_signal_stop_count = 0;
208std::atomic<int> g_signal_resize_count = 0;
212void RecordSignal(
int signal) {
220 g_signal_exit_count++;
225 g_signal_stop_count++;
229 g_signal_resize_count++;
238void ExecuteSignalHandlers() {
239 int signal_exit_count = g_signal_exit_count.exchange(0);
240 while (signal_exit_count--) {
245 int signal_stop_count = g_signal_stop_count.exchange(0);
246 while (signal_stop_count--) {
250 int signal_resize_count = g_signal_resize_count.exchange(0);
251 while (signal_resize_count--) {
257void InstallSignalHandler(
int sig) {
258 auto old_signal_handler = std::signal(sig, RecordSignal);
259 on_exit_functions.emplace(
260 [=] { std::ignore = std::signal(sig, old_signal_handler); });
264const std::string CSI =
"\x1b[";
267const std::string DCS =
"\x1bP";
269const std::string ST =
"\x1b\\";
273const std::string DECRQSS_DECSCUSR = DCS +
"$q q" + ST;
276enum class DECMode : std::uint16_t {
282 kMouseVt200Highlight = 1001,
284 kMouseBtnEventMouse = 1002,
285 kMouseAnyEvent = 1003,
288 kMouseSgrExtMode = 1006,
289 kMouseUrxvtMode = 1015,
290 kMouseSgrPixelsMode = 1016,
291 kAlternateScreen = 1049,
295enum class DSRMode : std::uint8_t {
299std::string Serialize(
const std::vector<DECMode>& parameters) {
302 for (
const DECMode parameter : parameters) {
306 out += std::to_string(
int(parameter));
313std::string Set(
const std::vector<DECMode>& parameters) {
314 return CSI +
"?" + Serialize(parameters) +
"h";
318std::string Reset(
const std::vector<DECMode>& parameters) {
319 return CSI +
"?" + Serialize(parameters) +
"l";
323std::string DeviceStatusReport(DSRMode ps) {
324 return CSI + std::to_string(
int(ps)) +
"n";
327class CapturedMouseImpl :
public CapturedMouseInterface {
329 explicit CapturedMouseImpl(std::function<
void(
void)> callback)
330 : callback_(std::move(callback)) {}
331 ~CapturedMouseImpl()
override { callback_(); }
332 CapturedMouseImpl(
const CapturedMouseImpl&) =
delete;
333 CapturedMouseImpl(CapturedMouseImpl&&) =
delete;
334 CapturedMouseImpl& operator=(
const CapturedMouseImpl&) =
delete;
335 CapturedMouseImpl& operator=(CapturedMouseImpl&&) =
delete;
338 std::function<void(
void)> callback_;
341void AnimationListener(std::atomic<bool>* quit,
Sender<Task> out) {
343 const auto time_delta = std::chrono::milliseconds(15);
345 out->Send(AnimationTask());
346 std::this_thread::sleep_for(time_delta);
352ScreenInteractive::ScreenInteractive(Dimension dimension,
355 bool use_alternative_screen)
356 : Screen(dimx, dimy),
357 dimension_(dimension),
358 use_alternative_screen_(use_alternative_screen) {
387 Dimension::Fullscreen,
400 Dimension::Fullscreen,
413 Dimension::TerminalOutput,
426 Dimension::FitComponent,
449 track_mouse_ = enable;
461 task_sender_->Send(std::move(task));
473 if (animation_requested_) {
476 animation_requested_ =
true;
477 auto now = animation::Clock::now();
478 const auto time_histeresis = std::chrono::milliseconds(33);
479 if (now - previous_animation_time_ >= time_histeresis) {
480 previous_animation_time_ = now;
488 if (mouse_captured) {
491 mouse_captured =
true;
492 return std::make_unique<CapturedMouseImpl>(
493 [
this] { mouse_captured =
false; });
499 class Loop loop(this, std::move(component));
504bool ScreenInteractive::HasQuitted() {
509void ScreenInteractive::PreMain() {
511 if (g_active_screen) {
512 std::swap(suspended_screen_, g_active_screen);
514 suspended_screen_->ResetCursorPosition();
516 suspended_screen_->
dimx_ = 0;
517 suspended_screen_->
dimy_ = 0;
520 suspended_screen_->Uninstall();
524 g_active_screen =
this;
525 g_active_screen->Install();
527 previous_animation_time_ = animation::Clock::now();
531void ScreenInteractive::PostMain() {
533 ResetCursorPosition();
535 g_active_screen =
nullptr;
538 if (suspended_screen_) {
544 std::swap(g_active_screen, suspended_screen_);
545 g_active_screen->Install();
552 if (!use_alternative_screen_) {
554 std::cout << std::flush;
573 force_handle_ctrl_c_ = force;
579 force_handle_ctrl_z_ = force;
587 return selection_->GetParts();
591 selection_on_change_ = std::move(callback);
597 return g_active_screen;
601void ScreenInteractive::Install() {
602 frame_valid_ =
false;
613 on_exit_functions.emplace([] { Flush(); });
619 std::cout << DECRQSS_DECSCUSR;
620 on_exit_functions.emplace([
this] {
621 std::cout <<
"\033[?25h";
622 std::cout <<
"\033[" + std::to_string(cursor_reset_shape_) +
" q";
627 for (
const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
628 InstallSignalHandler(signal);
634 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
635 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
639 GetConsoleMode(stdout_handle, &out_mode);
640 GetConsoleMode(stdin_handle, &in_mode);
641 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
642 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
645 const int enable_virtual_terminal_processing = 0x0004;
646 const int disable_newline_auto_return = 0x0008;
647 out_mode |= enable_virtual_terminal_processing;
648 out_mode |= disable_newline_auto_return;
651 const int enable_line_input = 0x0002;
652 const int enable_echo_input = 0x0004;
653 const int enable_virtual_terminal_input = 0x0200;
654 const int enable_window_input = 0x0008;
655 in_mode &= ~enable_echo_input;
656 in_mode &= ~enable_line_input;
657 in_mode |= enable_virtual_terminal_input;
658 in_mode |= enable_window_input;
660 SetConsoleMode(stdin_handle, in_mode);
661 SetConsoleMode(stdout_handle, out_mode);
663 for (
const int signal : {SIGWINCH, SIGTSTP}) {
664 InstallSignalHandler(signal);
667 struct termios terminal;
668 tcgetattr(STDIN_FILENO, &terminal);
669 on_exit_functions.emplace(
670 [=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
673 terminal.c_iflag &= ~IGNBRK;
674 terminal.c_iflag &= ~BRKINT;
676 terminal.c_iflag &= ~PARMRK;
677 terminal.c_iflag &= ~ISTRIP;
678 terminal.c_iflag &= ~INLCR;
679 terminal.c_iflag &= ~IGNCR;
680 terminal.c_iflag &= ~ICRNL;
681 terminal.c_iflag &= ~IXON;
683 terminal.c_lflag &= ~ECHO;
684 terminal.c_lflag &= ~ECHONL;
685 terminal.c_lflag &= ~ICANON;
686 terminal.c_lflag &= ~ISIG;
691 terminal.c_lflag &= ~IEXTEN;
692 terminal.c_cflag |= CS8;
694 terminal.c_cc[VMIN] = 0;
696 terminal.c_cc[VTIME] = 0;
698 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
702 auto enable = [&](
const std::vector<DECMode>& parameters) {
703 std::cout << Set(parameters);
704 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
707 auto disable = [&](
const std::vector<DECMode>& parameters) {
708 std::cout << Reset(parameters);
709 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
712 if (use_alternative_screen_) {
714 DECMode::kAlternateScreen,
724 enable({DECMode::kMouseVt200});
725 enable({DECMode::kMouseAnyEvent});
726 enable({DECMode::kMouseUrxvtMode});
727 enable({DECMode::kMouseSgrExtMode});
735 task_sender_ = task_receiver_->MakeSender();
737 std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
738 animation_listener_ =
739 std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
743void ScreenInteractive::Uninstall() {
745 event_listener_.join();
746 animation_listener_.join();
752void ScreenInteractive::RunOnceBlocking(
Component component) {
753 ExecuteSignalHandlers();
755 if (task_receiver_->Receive(&task)) {
756 HandleTask(component, task);
762void ScreenInteractive::RunOnce(
Component component) {
764 while (task_receiver_->ReceiveNonBlocking(&task)) {
765 HandleTask(component, task);
766 ExecuteSignalHandlers();
768 Draw(std::move(component));
770 if (selection_data_previous_ != selection_data_) {
771 selection_data_previous_ = selection_data_;
772 if (selection_on_change_) {
773 selection_on_change_();
781void ScreenInteractive::HandleTask(
Component component,
Task& task) {
784 using T = std::decay_t<
decltype(arg)>;
788 if constexpr (std::is_same_v<T, Event>) {
789 if (arg.is_cursor_position()) {
790 cursor_x_ = arg.cursor_x();
791 cursor_y_ = arg.cursor_y();
795 if (arg.is_cursor_shape()) {
796 cursor_reset_shape_= arg.cursor_shape();
800 if (arg.is_mouse()) {
801 arg.mouse().x -= cursor_x_;
802 arg.mouse().y -= cursor_y_;
807 bool handled = component->OnEvent(arg);
809 handled = HandleSelection(handled, arg);
811 if (arg ==
Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
812 RecordSignal(SIGABRT);
816 if (arg ==
Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
817 RecordSignal(SIGTSTP);
821 frame_valid_ =
false;
826 if constexpr (std::is_same_v<T, Closure>) {
832 if constexpr (std::is_same_v<T, AnimationTask>) {
833 if (!animation_requested_) {
837 animation_requested_ =
false;
840 previous_animation_time_ = now;
842 animation::Params params(delta);
843 component->OnAnimation(params);
844 frame_valid_ =
false;
853bool ScreenInteractive::HandleSelection(
bool handled, Event event) {
855 selection_pending_ =
nullptr;
856 selection_data_.empty =
true;
857 selection_ =
nullptr;
861 if (!event.is_mouse()) {
865 auto& mouse =
event.mouse();
872 selection_data_.start_x = mouse.x;
873 selection_data_.start_y = mouse.y;
874 selection_data_.end_x = mouse.x;
875 selection_data_.end_y = mouse.y;
879 if (!selection_pending_) {
884 if ((mouse.x != selection_data_.end_x) ||
885 (mouse.y != selection_data_.end_y)) {
886 selection_data_.end_x = mouse.x;
887 selection_data_.end_y = mouse.y;
888 selection_data_.empty =
false;
895 selection_pending_ =
nullptr;
896 selection_data_.end_x = mouse.x;
897 selection_data_.end_y = mouse.y;
898 selection_data_.empty =
false;
907void ScreenInteractive::Draw(
Component component) {
911 auto document = component->Render();
915 document->ComputeRequirement();
916 switch (dimension_) {
917 case Dimension::Fixed:
921 case Dimension::TerminalOutput:
922 dimx = terminal.dimx;
925 case Dimension::Fullscreen:
926 dimx = terminal.dimx;
927 dimy = terminal.dimy;
929 case Dimension::FitComponent:
936 ResetCursorPosition();
941 if ((
dimx <
dimx_) && !use_alternative_screen_) {
942 std::cout <<
"\033[J";
943 std::cout <<
"\033[H";
950 pixels_ = std::vector<std::vector<Pixel>>(
dimy, std::vector<Pixel>(
dimx));
958#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
967 if (!use_alternative_screen_ && (i % 150 == 0)) {
968 std::cout << DeviceStatusReport(DSRMode::kCursor);
973 if (!use_alternative_screen_ &&
974 (previous_frame_resized_ || i % 40 == 0)) {
975 std::cout << DeviceStatusReport(DSRMode::kCursor);
978 previous_frame_resized_ = resized;
980 selection_ = selection_data_.empty
981 ? std::make_unique<Selection>()
982 : std::make_unique<Selection>(
983 selection_data_.start_x, selection_data_.start_y,
984 selection_data_.end_x, selection_data_.end_y);
985 Render(*
this, document.get(), *selection_);
992 set_cursor_position.clear();
993 reset_cursor_position.clear();
996 set_cursor_position +=
"\x1B[" + std::to_string(dy) +
"A";
997 reset_cursor_position +=
"\x1B[" + std::to_string(dy) +
"B";
1001 set_cursor_position +=
"\x1B[" + std::to_string(dx) +
"D";
1002 reset_cursor_position +=
"\x1B[" + std::to_string(dx) +
"C";
1006 set_cursor_position +=
"\033[?25l";
1008 set_cursor_position +=
"\033[?25h";
1009 set_cursor_position +=
1014 std::cout <<
ToString() << set_cursor_position;
1017 frame_valid_ =
true;
1022void ScreenInteractive::ResetCursorPosition() {
1023 std::cout << reset_cursor_position;
1024 reset_cursor_position =
"";
1029 return [
this] {
Exit(); };
1034 Post([
this] { ExitNow(); });
1038void ScreenInteractive::ExitNow() {
1040 task_sender_.reset();
1044void ScreenInteractive::Signal(
int signal) {
1045 if (signal == SIGABRT) {
1052 if (signal == SIGTSTP) {
1054 ResetCursorPosition();
1060 std::ignore = std::raise(SIGTSTP);
1066 if (signal == SIGWINCH) {
1073bool ScreenInteractive::SelectionData::operator==(
1074 const ScreenInteractive::SelectionData& other)
const {
1075 if (empty && other.empty) {
1078 if (empty || other.empty) {
1081 return start_x == other.start_x && start_y == other.start_y &&
1082 end_x == other.end_x && end_y == other.end_y;
1085bool ScreenInteractive::SelectionData::operator!=(
1086 const ScreenInteractive::SelectionData& other)
const {
1087 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