15#include <initializer_list>
38#define DEFINE_CONSOLEV2_PROPERTIES
39#define WIN32_LEAN_AND_MEAN
45#error Must be compiled in UNICODE mode
49#include <sys/select.h>
56#if defined(__clang__) && defined(__APPLE__)
57#define quick_exit(a) exit(a)
62struct ScreenInteractive::Internal {
64 TerminalInputParser terminal_input_parser;
66 task::TaskRunner task_runner;
69 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
70 std::chrono::steady_clock::now();
72 explicit Internal(std::function<
void(Event)> out)
73 : terminal_input_parser(std::move(out)) {}
80 screen->RequestAnimationFrame();
87ScreenInteractive* g_active_screen =
nullptr;
91 std::cout <<
'\0' << std::flush;
94constexpr int timeout_milliseconds = 20;
95[[maybe_unused]]
constexpr int timeout_microseconds =
96 timeout_milliseconds * 1000;
99#elif defined(__EMSCRIPTEN__)
100#include <emscripten.h>
104void ftxui_on_resize(
int columns,
int rows) {
109 std::raise(SIGWINCH);
115int CheckStdinReady(
int fd) {
120 select(fd + 1, &fds,
nullptr,
nullptr, &tv);
121 return FD_ISSET(fd, &fds);
126std::stack<Closure> on_exit_functions;
128 while (!on_exit_functions.empty()) {
129 on_exit_functions.top()();
130 on_exit_functions.pop();
134std::atomic<int> g_signal_exit_count = 0;
136std::atomic<int> g_signal_stop_count = 0;
137std::atomic<int> g_signal_resize_count = 0;
141void RecordSignal(
int signal) {
149 g_signal_exit_count++;
154 g_signal_stop_count++;
158 g_signal_resize_count++;
167void ExecuteSignalHandlers() {
168 int signal_exit_count = g_signal_exit_count.exchange(0);
169 while (signal_exit_count--) {
174 int signal_stop_count = g_signal_stop_count.exchange(0);
175 while (signal_stop_count--) {
179 int signal_resize_count = g_signal_resize_count.exchange(0);
180 while (signal_resize_count--) {
186void InstallSignalHandler(
int sig) {
187 auto old_signal_handler = std::signal(sig, RecordSignal);
188 on_exit_functions.emplace(
189 [=] { std::ignore = std::signal(sig, old_signal_handler); });
193const std::string CSI =
"\x1b[";
196const std::string DCS =
"\x1bP";
198const std::string ST =
"\x1b\\";
202const std::string DECRQSS_DECSCUSR = DCS +
"$q q" + ST;
205enum class DECMode : std::uint16_t {
211 kMouseVt200Highlight = 1001,
213 kMouseBtnEventMouse = 1002,
214 kMouseAnyEvent = 1003,
217 kMouseSgrExtMode = 1006,
218 kMouseUrxvtMode = 1015,
219 kMouseSgrPixelsMode = 1016,
220 kAlternateScreen = 1049,
224enum class DSRMode : std::uint8_t {
228std::string Serialize(
const std::vector<DECMode>& parameters) {
231 for (
const DECMode parameter : parameters) {
235 out += std::to_string(
int(parameter));
242std::string Set(
const std::vector<DECMode>& parameters) {
243 return CSI +
"?" + Serialize(parameters) +
"h";
247std::string Reset(
const std::vector<DECMode>& parameters) {
248 return CSI +
"?" + Serialize(parameters) +
"l";
252std::string DeviceStatusReport(DSRMode ps) {
253 return CSI + std::to_string(
int(ps)) +
"n";
256class CapturedMouseImpl :
public CapturedMouseInterface {
258 explicit CapturedMouseImpl(std::function<
void(
void)> callback)
259 : callback_(std::move(callback)) {}
260 ~CapturedMouseImpl()
override { callback_(); }
261 CapturedMouseImpl(
const CapturedMouseImpl&) =
delete;
262 CapturedMouseImpl(CapturedMouseImpl&&) =
delete;
263 CapturedMouseImpl& operator=(
const CapturedMouseImpl&) =
delete;
264 CapturedMouseImpl& operator=(CapturedMouseImpl&&) =
delete;
267 std::function<void(
void)> callback_;
272ScreenInteractive::ScreenInteractive(
Dimension dimension,
275 bool use_alternative_screen)
276 : Screen(dimx, dimy),
277 dimension_(dimension),
278 use_alternative_screen_(use_alternative_screen) {
279 internal_ = std::make_unique<Internal>(
280 [&](Event event) { PostEvent(std::move(event)); });
308 Dimension::Fullscreen,
321 Dimension::Fullscreen,
334 Dimension::TerminalOutput,
349 Dimension::FitComponent,
372 track_mouse_ = enable;
384 handle_piped_input_ = enable;
390 internal_->task_runner.
PostTask([
this, task = std::move(task)]()
mutable {
391 HandleTask(component_, task);
404 if (animation_requested_) {
407 animation_requested_ =
true;
408 auto now = animation::Clock::now();
409 const auto time_histeresis = std::chrono::milliseconds(33);
410 if (now - previous_animation_time_ >= time_histeresis) {
411 previous_animation_time_ = now;
419 if (mouse_captured) {
422 mouse_captured =
true;
423 return std::make_unique<CapturedMouseImpl>(
424 [
this] { mouse_captured =
false; });
430 class Loop loop(this, std::move(component));
435bool ScreenInteractive::HasQuitted() {
440void ScreenInteractive::PreMain() {
442 if (g_active_screen) {
443 std::swap(suspended_screen_, g_active_screen);
445 suspended_screen_->ResetCursorPosition();
447 suspended_screen_->
dimx_ = 0;
448 suspended_screen_->
dimy_ = 0;
451 suspended_screen_->Uninstall();
455 g_active_screen =
this;
456 g_active_screen->Install();
458 previous_animation_time_ = animation::Clock::now();
462void ScreenInteractive::PostMain() {
464 ResetCursorPosition();
466 g_active_screen =
nullptr;
469 if (suspended_screen_) {
475 std::swap(g_active_screen, suspended_screen_);
476 g_active_screen->Install();
483 if (!use_alternative_screen_) {
485 std::cout << std::flush;
504 force_handle_ctrl_c_ = force;
510 force_handle_ctrl_z_ = force;
518 return selection_->GetParts();
522 selection_on_change_ = std::move(callback);
528 return g_active_screen;
532void ScreenInteractive::Install() {
533 frame_valid_ =
false;
542 InstallPipedInputHandling();
546 on_exit_functions.emplace([] { Flush(); });
552 std::cout << DECRQSS_DECSCUSR;
553 on_exit_functions.emplace([
this] {
554 std::cout <<
"\033[?25h";
555 std::cout <<
"\033[" + std::to_string(cursor_reset_shape_) +
" q";
560 for (
const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
561 InstallSignalHandler(signal);
567 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
568 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
572 GetConsoleMode(stdout_handle, &out_mode);
573 GetConsoleMode(stdin_handle, &in_mode);
574 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
575 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
578 const int enable_virtual_terminal_processing = 0x0004;
579 const int disable_newline_auto_return = 0x0008;
580 out_mode |= enable_virtual_terminal_processing;
581 out_mode |= disable_newline_auto_return;
584 const int enable_line_input = 0x0002;
585 const int enable_echo_input = 0x0004;
586 const int enable_virtual_terminal_input = 0x0200;
587 const int enable_window_input = 0x0008;
588 in_mode &= ~enable_echo_input;
589 in_mode &= ~enable_line_input;
590 in_mode |= enable_virtual_terminal_input;
591 in_mode |= enable_window_input;
593 SetConsoleMode(stdin_handle, in_mode);
594 SetConsoleMode(stdout_handle, out_mode);
604 for (
const int signal : {SIGWINCH, SIGTSTP}) {
605 InstallSignalHandler(signal);
608 struct termios terminal;
609 tcgetattr(tty_fd_, &terminal);
610 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
611 tcsetattr(tty_fd_, TCSANOW, &terminal);
615 terminal.c_iflag &= ~IGNBRK;
616 terminal.c_iflag &= ~BRKINT;
618 terminal.c_iflag &= ~PARMRK;
619 terminal.c_iflag &= ~ISTRIP;
620 terminal.c_iflag &= ~INLCR;
621 terminal.c_iflag &= ~IGNCR;
622 terminal.c_iflag &= ~ICRNL;
623 terminal.c_iflag &= ~IXON;
625 terminal.c_lflag &= ~ECHO;
626 terminal.c_lflag &= ~ECHONL;
627 terminal.c_lflag &= ~ICANON;
628 terminal.c_lflag &= ~ISIG;
633 terminal.c_lflag &= ~IEXTEN;
634 terminal.c_cflag |= CS8;
636 terminal.c_cc[VMIN] = 0;
638 terminal.c_cc[VTIME] = 0;
640 tcsetattr(tty_fd_, TCSANOW, &terminal);
644 auto enable = [&](
const std::vector<DECMode>& parameters) {
645 std::cout << Set(parameters);
646 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
649 auto disable = [&](
const std::vector<DECMode>& parameters) {
650 std::cout << Reset(parameters);
651 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
654 if (use_alternative_screen_) {
656 DECMode::kAlternateScreen,
666 enable({DECMode::kMouseVt200});
667 enable({DECMode::kMouseAnyEvent});
668 enable({DECMode::kMouseUrxvtMode});
669 enable({DECMode::kMouseSgrExtMode});
681void ScreenInteractive::InstallPipedInputHandling() {
682#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
683 tty_fd_ = STDIN_FILENO;
687 if (!handle_piped_input_) {
692 if (isatty(STDIN_FILENO)) {
697 tty_fd_ = open(
"/dev/tty", O_RDONLY);
700 tty_fd_ = STDIN_FILENO;
705 on_exit_functions.emplace([
this] {
713void ScreenInteractive::Uninstall() {
720void ScreenInteractive::RunOnceBlocking(
Component component) {
722 const auto time_per_frame = std::chrono::microseconds(16666);
724 auto time = std::chrono::steady_clock::now();
725 size_t executed_task = internal_->task_runner.
ExecutedTasks();
728 while (executed_task == internal_->task_runner.
ExecutedTasks() &&
732 const auto now = std::chrono::steady_clock::now();
733 const auto delta = now - time;
736 if (delta < time_per_frame) {
737 const auto sleep_duration = time_per_frame - delta;
738 std::this_thread::sleep_for(sleep_duration);
744void ScreenInteractive::RunOnce(
Component component) {
745 AutoReset set_component(&component_, component);
746 ExecuteSignalHandlers();
747 FetchTerminalEvents();
750 const size_t executed_task = internal_->task_runner.
ExecutedTasks();
753 if (executed_task == internal_->task_runner.
ExecutedTasks()) {
757 ExecuteSignalHandlers();
760 if (selection_data_previous_ != selection_data_) {
761 selection_data_previous_ = selection_data_;
762 if (selection_on_change_) {
763 selection_on_change_();
771void ScreenInteractive::HandleTask(
Component component,
Task& task) {
774 using T = std::decay_t<
decltype(arg)>;
778 if constexpr (std::is_same_v<T, Event>) {
780 if (arg.is_cursor_position()) {
781 cursor_x_ = arg.cursor_x();
782 cursor_y_ = arg.cursor_y();
786 if (arg.is_cursor_shape()) {
787 cursor_reset_shape_= arg.cursor_shape();
791 if (arg.is_mouse()) {
792 arg.mouse().x -= cursor_x_;
793 arg.mouse().y -= cursor_y_;
798 bool handled = component->OnEvent(arg);
800 handled = HandleSelection(handled, arg);
802 if (arg ==
Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
803 RecordSignal(SIGABRT);
807 if (arg ==
Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
808 RecordSignal(SIGTSTP);
812 frame_valid_ =
false;
817 if constexpr (std::is_same_v<T, Closure>) {
823 if constexpr (std::is_same_v<T, AnimationTask>) {
824 if (!animation_requested_) {
828 animation_requested_ =
false;
831 previous_animation_time_ = now;
833 animation::Params params(delta);
834 component->OnAnimation(params);
835 frame_valid_ =
false;
844bool ScreenInteractive::HandleSelection(
bool handled, Event event) {
846 selection_pending_ =
nullptr;
847 selection_data_.empty =
true;
848 selection_ =
nullptr;
852 if (!event.is_mouse()) {
856 auto& mouse =
event.mouse();
863 selection_data_.start_x = mouse.x;
864 selection_data_.start_y = mouse.y;
865 selection_data_.end_x = mouse.x;
866 selection_data_.end_y = mouse.y;
870 if (!selection_pending_) {
875 if ((mouse.x != selection_data_.end_x) ||
876 (mouse.y != selection_data_.end_y)) {
877 selection_data_.end_x = mouse.x;
878 selection_data_.end_y = mouse.y;
879 selection_data_.empty =
false;
886 selection_pending_ =
nullptr;
887 selection_data_.end_x = mouse.x;
888 selection_data_.end_y = mouse.y;
889 selection_data_.empty =
false;
898void ScreenInteractive::Draw(
Component component) {
902 auto document = component->Render();
906 document->ComputeRequirement();
907 switch (dimension_) {
908 case Dimension::Fixed:
912 case Dimension::TerminalOutput:
913 dimx = terminal.dimx;
916 case Dimension::Fullscreen:
917 dimx = terminal.dimx;
918 dimy = terminal.dimy;
920 case Dimension::FitComponent:
927 ResetCursorPosition();
932 if ((
dimx <
dimx_) && !use_alternative_screen_) {
933 std::cout <<
"\033[J";
934 std::cout <<
"\033[H";
941 pixels_ = std::vector<std::vector<Pixel>>(
dimy, std::vector<Pixel>(
dimx));
949#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
958 if (!use_alternative_screen_ && (i % 150 == 0)) {
959 std::cout << DeviceStatusReport(DSRMode::kCursor);
964 if (!use_alternative_screen_ &&
965 (previous_frame_resized_ || i % 40 == 0)) {
966 std::cout << DeviceStatusReport(DSRMode::kCursor);
969 previous_frame_resized_ = resized;
971 selection_ = selection_data_.empty
972 ? std::make_unique<Selection>()
973 : std::make_unique<Selection>(
974 selection_data_.start_x, selection_data_.start_y,
975 selection_data_.end_x, selection_data_.end_y);
976 Render(*
this, document.get(), *selection_);
983 set_cursor_position.clear();
984 reset_cursor_position.clear();
987 set_cursor_position +=
"\x1B[" + std::to_string(dy) +
"A";
988 reset_cursor_position +=
"\x1B[" + std::to_string(dy) +
"B";
992 set_cursor_position +=
"\x1B[" + std::to_string(dx) +
"D";
993 reset_cursor_position +=
"\x1B[" + std::to_string(dx) +
"C";
997 set_cursor_position +=
"\033[?25l";
999 set_cursor_position +=
"\033[?25h";
1000 set_cursor_position +=
1005 std::cout <<
ToString() << set_cursor_position;
1008 frame_valid_ =
true;
1013void ScreenInteractive::ResetCursorPosition() {
1014 std::cout << reset_cursor_position;
1015 reset_cursor_position =
"";
1020 return [
this] {
Exit(); };
1025 Post([
this] { ExitNow(); });
1029void ScreenInteractive::ExitNow() {
1034void ScreenInteractive::Signal(
int signal) {
1035 if (signal == SIGABRT) {
1042 if (signal == SIGTSTP) {
1044 ResetCursorPosition();
1050 std::ignore = std::raise(SIGTSTP);
1056 if (signal == SIGWINCH) {
1063void ScreenInteractive::FetchTerminalEvents() {
1065 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1067 auto console = GetStdHandle(STD_INPUT_HANDLE);
1068 DWORD number_of_events = 0;
1069 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1070 return std::vector<INPUT_RECORD>();
1072 if (number_of_events <= 0) {
1074 return std::vector<INPUT_RECORD>();
1077 std::vector<INPUT_RECORD> records(number_of_events);
1078 DWORD number_of_events_read = 0;
1079 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1080 &number_of_events_read)) {
1081 return std::vector<INPUT_RECORD>();
1083 records.resize(number_of_events_read);
1087 auto records = get_input_records();
1088 if (records.size() == 0) {
1089 const auto timeout =
1090 std::chrono::steady_clock::now() - internal_->last_char_time;
1091 const size_t timeout_microseconds =
1092 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1093 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1096 internal_->last_char_time = std::chrono::steady_clock::now();
1101 for (
const auto& r : records) {
1102 switch (r.EventType) {
1104 auto key_event = r.Event.KeyEvent;
1106 if (key_event.bKeyDown == FALSE) {
1109 std::wstring wstring;
1110 wstring += key_event.uChar.UnicodeChar;
1112 internal_->terminal_input_parser.
Add(it);
1115 case WINDOW_BUFFER_SIZE_EVENT:
1125#elif defined(__EMSCRIPTEN__)
1128 std::array<char, 128> out{};
1129 size_t l = read(STDIN_FILENO, out.data(), out.size());
1131 const auto timeout =
1132 std::chrono::steady_clock::now() - internal_->last_char_time;
1133 const size_t timeout_microseconds =
1134 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1135 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1138 internal_->last_char_time = std::chrono::steady_clock::now();
1141 for (
size_t i = 0; i < l; ++i) {
1142 internal_->terminal_input_parser.
Add(out[i]);
1145 if (!CheckStdinReady(tty_fd_)) {
1146 const auto timeout =
1147 std::chrono::steady_clock::now() - internal_->last_char_time;
1148 const size_t timeout_ms =
1149 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1150 internal_->terminal_input_parser.
Timeout(timeout_ms);
1153 internal_->last_char_time = std::chrono::steady_clock::now();
1156 std::array<char, 128> out{};
1157 size_t l = read(tty_fd_, out.data(), out.size());
1160 for (
size_t i = 0; i < l; ++i) {
1161 internal_->terminal_input_parser.
Add(out[i]);
1166void ScreenInteractive::PostAnimationTask() {
1167 Post(AnimationTask());
1171 internal_->task_runner.
PostDelayedTask([
this] { PostAnimationTask(); },
1172 std::chrono::milliseconds(15));
1175bool ScreenInteractive::SelectionData::operator==(
1176 const ScreenInteractive::SelectionData& other)
const {
1177 if (empty && other.empty) {
1180 if (empty || other.empty) {
1183 return start_x == other.start_x && start_y == other.start_y &&
1184 end_x == other.end_x && end_y == other.end_y;
1187bool ScreenInteractive::SelectionData::operator!=(
1188 const ScreenInteractive::SelectionData& other)
const {
1189 return !(*
this == other);
static void Signal(ScreenInteractive &s, int signal)
auto PostTask(Task task) -> void
Schedules a task to be executed immediately.
auto RunUntilIdle() -> std::optional< std::chrono::steady_clock::duration >
Runs the tasks in the queue.
auto PostDelayedTask(Task task, std::chrono::steady_clock::duration duration) -> void
Schedules a task to be executed after a certain duration.
size_t ExecutedTasks() const
static ScreenInteractive TerminalOutput()
void HandlePipedInput(bool enable=true)
Enable or disable automatic piped input handling. When enabled, FTXUI will detect piped input and red...
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.
~ScreenInteractive() override
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.
The FTXUI ftxui::Dimension:: namespace.
The FTXUI ftxui::animation:: namespace.
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
std::string to_string(const std::wstring &s)
Convert a std::wstring into a UTF8 std::string.
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