15#include <initializer_list>
44#define DEFINE_CONSOLEV2_PROPERTIES
45#define WIN32_LEAN_AND_MEAN
60#if defined(__EMSCRIPTEN__)
61#include <emscripten.h>
75 auto* screen = App::Active();
77 screen->RequestAnimationFrame();
82#if defined(__EMSCRIPTEN__)
85void ftxui_on_resize(
int columns,
int rows) {
98 App* suspended_screen_ =
nullptr;
100 const bool use_alternative_screen_;
102 bool track_mouse_ =
true;
104 std::string set_cursor_position_;
105 std::string reset_cursor_position_;
107 std::atomic<bool> quit_{
false};
108 bool installed_ =
false;
109 bool animation_requested_ =
false;
115 std::uint64_t frame_count_ = 0;
116 bool mouse_captured =
false;
117 bool previous_frame_resized_ =
false;
119 bool frame_valid_ =
false;
121 bool force_handle_ctrl_c_ =
true;
122 bool force_handle_ctrl_z_ =
true;
124 int cursor_reset_shape_ = 1;
127 bool handle_piped_input_ =
true;
128 bool is_stdin_a_tty_ =
false;
129 bool is_stdout_a_tty_ =
false;
133 std::string terminal_name_ =
"unknown";
134 int terminal_version_ = 0;
136 std::string terminal_emulator_name_ =
"unknown";
137 std::string terminal_emulator_version_ =
"unknown";
139 std::vector<int> terminal_capabilities_;
143 struct SelectionData {
149 bool operator==(
const SelectionData& other)
const {
150 if (empty && other.empty) {
153 if (empty || other.empty) {
156 return start_x == other.start_x && start_y == other.start_y &&
157 end_x == other.end_x && end_y == other.end_y;
159 bool operator!=(
const SelectionData& other)
const {
160 return !(*
this == other);
163 SelectionData selection_data_;
164 SelectionData selection_data_previous_;
165 std::unique_ptr<Selection> selection_;
166 std::function<void()> selection_on_change_;
171 TerminalInputParser terminal_input_parser;
172 task::TaskRunner task_runner;
173 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
174 std::chrono::steady_clock::now();
175 std::string output_buffer;
177 class ThrottledRequest {
179 ThrottledRequest(App::Internal* internal, std::function<
void()> send)
180 : internal_(internal), send_(std::move(send)) {}
182 void Request(
bool force =
false) {
183 if (!internal_->is_stdin_a_tty_) {
198 const auto now = std::chrono::steady_clock::now();
199 if (now - last_request_time_ < std::chrono::milliseconds(500)) {
211 void OnReply() { pending_request_ =
false; }
213 bool HasPending()
const {
214 if (!pending_request_) {
217 const auto now = std::chrono::steady_clock::now();
218 return now - last_sent_time_ < std::chrono::seconds(5);
223 last_sent_time_ = std::chrono::steady_clock::now();
224 last_request_time_ = last_sent_time_;
225 pending_request_ =
true;
229 App::Internal* internal_;
230 std::function<void()> send_;
231 bool pending_request_ =
false;
232 std::chrono::steady_clock::time_point last_request_time_ =
233 std::chrono::steady_clock::now() - std::chrono::hours(1);
234 std::chrono::steady_clock::time_point last_sent_time_ =
235 std::chrono::steady_clock::now() - std::chrono::hours(1);
238 ThrottledRequest cursor_position_request;
240 MultiReceiverBuffer<Event> event_buffer;
241 std::unique_ptr<MultiReceiverBuffer<Event>::Receiver> main_loop_receiver;
243 Internal(App* app,
AppDimension dimension,
bool use_alternative_screen);
251 void RunOnce(
const Component& component);
252 void RunOnceBlocking(
Component component);
254 bool HandleSelection(
bool handled, Event event);
256 std::string ResetCursorPosition();
257 void RequestCursorPosition(
bool force =
false);
258 void TerminalSend(std::string_view);
259 void TerminalFlush();
260 void InstallPipedInputHandling();
261 void InstallTerminalInfo();
262 void Signal(
int signal);
263 size_t FetchTerminalEvents();
264 void PostAnimationTask();
269App* g_active_screen =
nullptr;
271std::stack<Closure> on_exit_functions;
274 while (!on_exit_functions.empty()) {
275 on_exit_functions.top()();
276 on_exit_functions.pop();
281const std::string CSI =
"\x1b[";
284const std::string DCS =
"\x1bP";
287const std::string ST =
"\x1b\\";
291const std::string DECRQSS_DECSCUSR = DCS +
"$q q" + ST;
294enum class DECMode : std::uint16_t {
300 kMouseVt200Highlight = 1001,
302 kMouseBtnEventMouse = 1002,
303 kMouseAnyEvent = 1003,
306 kMouseSgrExtMode = 1006,
307 kMouseUrxvtMode = 1015,
308 kMouseSgrPixelsMode = 1016,
309 kAlternateScreen = 1049,
313enum class DSRMode : std::uint8_t {
317std::string Serialize(
const std::vector<DECMode>& parameters) {
320 for (
const DECMode parameter : parameters) {
324 out += std::to_string(
int(parameter));
331std::string Set(
const std::vector<DECMode>& parameters) {
332 return CSI +
"?" + Serialize(parameters) +
"h";
336std::string Reset(
const std::vector<DECMode>& parameters) {
337 return CSI +
"?" + Serialize(parameters) +
"l";
341std::string DeviceStatusReport(DSRMode ps) {
342 return CSI + std::to_string(
int(ps)) +
"n";
345class CapturedMouseImpl :
public CapturedMouseInterface {
347 explicit CapturedMouseImpl(std::function<
void(
void)> callback)
348 : callback_(std::move(callback)) {}
349 ~CapturedMouseImpl()
override { callback_(); }
350 CapturedMouseImpl(
const CapturedMouseImpl&) =
delete;
351 CapturedMouseImpl(CapturedMouseImpl&&) =
delete;
352 CapturedMouseImpl& operator=(
const CapturedMouseImpl&) =
delete;
353 CapturedMouseImpl& operator=(CapturedMouseImpl&&) =
delete;
356 std::function<void(
void)> callback_;
360std::atomic<int> g_signal_exit_count = 0;
361std::atomic<int> g_signal_stop_count = 0;
362std::atomic<int> g_signal_resize_count = 0;
364std::atomic<int> g_signal_exit_count = 0;
369std::atomic<bool> g_terminal_is_raw{
false};
373std::atomic<int> g_last_signal{0};
376using SignalHandler = void (*)(int);
378std::map<int, SignalHandler> g_old_signal_handlers;
381DWORD g_original_stdout_mode = 0;
382DWORD g_original_stdin_mode = 0;
383bool g_has_original_console_mode =
false;
386std::map<int, struct sigaction> g_old_sigactions;
389struct termios g_original_termios;
390bool g_has_original_termios =
false;
396void RestoreSignalHandlerAndRaise(
int signal) {
398 auto it = g_old_signal_handlers.find(signal);
399 auto old_handler = (it != g_old_signal_handlers.end()) ? it->second : SIG_DFL;
400 std::signal(signal, old_handler);
402 auto it = g_old_sigactions.find(signal);
403 if (it != g_old_sigactions.end()) {
404 sigaction(signal, &it->second,
nullptr);
407 sa.sa_handler = SIG_DFL;
408 sigemptyset(&sa.sa_mask);
410 sigaction(signal, &sa,
nullptr);
418void RestoreTerminalEmergency() {
419 if (!g_terminal_is_raw.exchange(
false)) {
423 if (g_has_original_console_mode) {
424 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
425 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
426 SetConsoleMode(stdout_handle, g_original_stdout_mode);
427 SetConsoleMode(stdin_handle, g_original_stdin_mode);
430 if (g_has_original_termios && g_tty_fd >= 0) {
431 const char restore_seq[] =
440 std::ignore = write(STDOUT_FILENO, restore_seq,
sizeof(restore_seq) - 1);
441 tcsetattr(g_tty_fd, TCSANOW, &g_original_termios);
447void RecordSignal(
int signal) {
464 RestoreTerminalEmergency();
465 RestoreSignalHandlerAndRaise(signal);
479 g_last_signal.store(signal);
480 g_signal_exit_count++;
486 g_signal_stop_count++;
491 g_signal_resize_count++;
500void ExecuteSignalHandlers() {
501 if (g_last_signal.load() != 0) {
502 App::Private::Signal(*g_active_screen, SIGABRT);
505 int signal_exit_count = g_signal_exit_count.exchange(0);
506 while (signal_exit_count--) {
507 App::Private::Signal(*g_active_screen, SIGABRT);
511 int signal_stop_count = g_signal_stop_count.exchange(0);
512 while (signal_stop_count--) {
513 App::Private::Signal(*g_active_screen, SIGTSTP);
516 int signal_resize_count = g_signal_resize_count.exchange(0);
517 while (signal_resize_count--) {
518 App::Private::Signal(*g_active_screen, SIGWINCH);
523void InstallSignalHandler(
int sig) {
525 auto old_signal_handler = std::signal(sig, RecordSignal);
526 g_old_signal_handlers[sig] = old_signal_handler;
527 on_exit_functions.emplace(
528 [=] { std::ignore = std::signal(sig, old_signal_handler); });
531 sa.sa_handler = RecordSignal;
532 sigemptyset(&sa.sa_mask);
533 sa.sa_flags = SA_RESTART;
534 struct sigaction old_sa;
535 sigaction(sig, &sa, &old_sa);
536 g_old_sigactions[sig] = old_sa;
537 on_exit_functions.emplace([=] { sigaction(sig, &old_sa,
nullptr); });
543App::Internal::Internal(App* app,
545 bool use_alternative_screen)
547 dimension_(dimension),
548 use_alternative_screen_(use_alternative_screen),
549 terminal_input_parser([&](Event event) {
550 event_buffer.Push(std::move(event));
552 cursor_position_request(
this, [
this] {
553 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
555 main_loop_receiver = event_buffer.CreateReceiver();
558void App::Internal::ExitNow() {
562void App::Internal::Install() {
563 frame_valid_ =
false;
572 InstallPipedInputHandling();
576 on_exit_functions.emplace([
this] { TerminalFlush(); });
580 for (
const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
581 InstallSignalHandler(signal);
587 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
588 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
592 GetConsoleMode(stdout_handle, &out_mode);
593 GetConsoleMode(stdin_handle, &in_mode);
594 g_original_stdout_mode = out_mode;
595 g_original_stdin_mode = in_mode;
596 g_has_original_console_mode =
true;
597 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
598 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
601 const int enable_virtual_terminal_processing = 0x0004;
602 const int disable_newline_auto_return = 0x0008;
603 out_mode |= enable_virtual_terminal_processing;
604 out_mode |= disable_newline_auto_return;
607 const int enable_line_input = 0x0002;
608 const int enable_echo_input = 0x0004;
609 const int enable_virtual_terminal_input = 0x0200;
610 const int enable_window_input = 0x0008;
611 in_mode &= ~enable_echo_input;
612 in_mode &= ~enable_line_input;
613 in_mode |= enable_virtual_terminal_input;
614 in_mode |= enable_window_input;
616 SetConsoleMode(stdin_handle, in_mode);
617 SetConsoleMode(stdout_handle, out_mode);
619 for (
const int signal :
620 {SIGWINCH, SIGTSTP, SIGBUS, SIGSYS, SIGQUIT, SIGHUP}) {
621 InstallSignalHandler(signal);
624 struct termios terminal;
625 tcgetattr(tty_fd_, &terminal);
626 g_original_termios = terminal;
628 g_has_original_termios =
true;
629 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
630 tcsetattr(tty_fd_, TCSANOW, &terminal);
634 terminal.c_iflag &= ~IGNBRK;
635 terminal.c_iflag &= ~BRKINT;
637 terminal.c_iflag &= ~PARMRK;
638 terminal.c_iflag &= ~ISTRIP;
639 terminal.c_iflag &= ~INLCR;
640 terminal.c_iflag &= ~IGNCR;
641 terminal.c_iflag &= ~ICRNL;
642 terminal.c_iflag &= ~IXON;
644 terminal.c_lflag &= ~ECHO;
645 terminal.c_lflag &= ~ECHONL;
646 terminal.c_lflag &= ~ICANON;
647 terminal.c_lflag &= ~ISIG;
652 terminal.c_lflag &= ~IEXTEN;
653 terminal.c_cflag |= CS8;
655 terminal.c_cc[VMIN] = 0;
657 terminal.c_cc[VTIME] = 0;
659 tcsetattr(tty_fd_, TCSANOW, &terminal);
663 auto enable = [&](
const std::vector<DECMode>& parameters) {
664 TerminalSend(Set(parameters));
665 on_exit_functions.emplace(
666 [
this, parameters] { TerminalSend(Reset(parameters)); });
669 auto disable = [&](
const std::vector<DECMode>& parameters) {
670 TerminalSend(Reset(parameters));
671 on_exit_functions.emplace(
672 [
this, parameters] { TerminalSend(Set(parameters)); });
675 if (use_alternative_screen_) {
677 DECMode::kAlternateScreen,
686 enable({DECMode::kMouseVt200});
687 enable({DECMode::kMouseAnyEvent});
688 enable({DECMode::kMouseUrxvtMode});
689 enable({DECMode::kMouseSgrExtMode});
696 InstallTerminalInfo();
703 g_terminal_is_raw =
true;
706void App::Internal::Uninstall() {
707 g_terminal_is_raw =
false;
711 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
712 auto closing_receiver =
713 event_buffer.CreateReceiverAt(main_loop_receiver->index());
714 auto start = std::chrono::steady_clock::now();
715 while (cursor_position_request.HasPending()) {
716 FetchTerminalEvents();
718 while (closing_receiver->Has()) {
719 const auto event = closing_receiver->Pop();
720 if (event.is_cursor_position()) {
721 cursor_x_ =
event.cursor_x();
722 cursor_y_ =
event.cursor_y();
723 cursor_position_request.OnReply();
727 task_runner.RunUntilIdle();
729 if (std::chrono::steady_clock::now() - start >
730 std::chrono::milliseconds(400)) {
733 std::this_thread::sleep_for(std::chrono::milliseconds(10));
740void App::Internal::PreMain() {
742 if (g_active_screen) {
743 std::swap(suspended_screen_, g_active_screen);
745 suspended_screen_->internal_->TerminalSend(
746 suspended_screen_->internal_->ResetCursorPosition());
747 suspended_screen_->ResetPosition(
748 suspended_screen_->internal_->output_buffer,
750 suspended_screen_->dimx_ = 0;
751 suspended_screen_->dimy_ = 0;
754 suspended_screen_->internal_->Uninstall();
758 g_active_screen = public_;
759 g_active_screen->internal_->Install();
761 previous_animation_time_ = animation::Clock::now();
764void App::Internal::PostMain() {
766 TerminalSend(ResetCursorPosition());
768 g_active_screen =
nullptr;
771 if (suspended_screen_) {
773 public_->ResetPosition(output_buffer,
true);
777 std::swap(g_active_screen, suspended_screen_);
778 g_active_screen->internal_->Install();
785 if (!use_alternative_screen_) {
788 std::cout << std::flush;
791 int sig = g_last_signal.exchange(0);
793 RestoreSignalHandlerAndRaise(sig);
797bool App::Internal::HasQuitted() {
801void App::Internal::RunOnce(
const Component& component) {
802 const AutoReset set_component(&component_, component);
803 ExecuteSignalHandlers();
804 FetchTerminalEvents();
806 while (!quit_ && main_loop_receiver->Has()) {
807 public_->Post(main_loop_receiver->Pop());
811 const size_t executed_task = task_runner.ExecutedTasks();
812 task_runner.RunUntilIdle();
814 if (executed_task == task_runner.ExecutedTasks()) {
818 ExecuteSignalHandlers();
821 if (selection_data_previous_ != selection_data_) {
822 selection_data_previous_ = selection_data_;
823 if (selection_on_change_) {
824 selection_on_change_();
825 public_->Post(Event::Custom);
830void App::Internal::RunOnceBlocking(
Component component) {
832 const auto time_per_frame = std::chrono::microseconds(16666);
834 auto time = std::chrono::steady_clock::now();
835 const size_t executed_task = task_runner.ExecutedTasks();
838 while (executed_task == task_runner.ExecutedTasks() && !HasQuitted()) {
841 const auto now = std::chrono::steady_clock::now();
842 const auto delta = now - time;
845 if (delta < time_per_frame) {
846 const auto sleep_duration = time_per_frame - delta;
847 std::this_thread::sleep_for(sleep_duration);
852void App::Internal::HandleTask(
Component component,
Task& task) {
855 using T = std::decay_t<
decltype(arg)>;
859 if constexpr (std::is_same_v<T, Event>) {
861 if (arg.is_cursor_position()) {
862 cursor_x_ = arg.cursor_x();
863 cursor_y_ = arg.cursor_y();
864 cursor_position_request.OnReply();
868 if (arg.is_cursor_shape()) {
869 cursor_reset_shape_ = arg.cursor_shape();
873 if (arg.IsTerminalCapabilities()) {
874 terminal_capabilities_ = arg.TerminalCapabilities();
878 if (arg.IsTerminalNameVersion()) {
879 terminal_name_ = arg.TerminalName();
880 terminal_version_ = arg.TerminalVersion();
884 if (arg.IsTerminalEmulator()) {
885 terminal_emulator_name_ = arg.TerminalEmulatorName();
886 terminal_emulator_version_ = arg.TerminalEmulatorVersion();
890 if (arg.is_mouse()) {
891 arg.mouse().x -= cursor_x_;
892 arg.mouse().y -= cursor_y_;
895 arg.screen_ = public_;
897 bool handled = component->OnEvent(arg);
898 handled = HandleSelection(handled, arg);
900 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
901 RecordSignal(SIGINT);
905 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
906 RecordSignal(SIGTSTP);
910 frame_valid_ =
false;
915 if constexpr (std::is_same_v<T, Closure>) {
921 if constexpr (std::is_same_v<T, AnimationTask>) {
922 if (!animation_requested_) {
926 animation_requested_ =
false;
929 previous_animation_time_ = now;
931 animation::Params params(delta);
932 component->OnAnimation(params);
933 frame_valid_ =
false;
941bool App::Internal::HandleSelection(
bool handled, Event event) {
943 selection_pending_ =
nullptr;
944 selection_data_.empty =
true;
945 selection_ =
nullptr;
949 if (!event.is_mouse()) {
953 auto& mouse =
event.mouse();
954 if (mouse.button != Mouse::Left) {
958 if (mouse.motion == Mouse::Pressed) {
959 selection_pending_ = public_->CaptureMouse();
960 selection_data_.start_x = mouse.x;
961 selection_data_.start_y = mouse.y;
962 selection_data_.end_x = mouse.x;
963 selection_data_.end_y = mouse.y;
967 if (!selection_pending_) {
971 if (mouse.motion == Mouse::Moved) {
972 if ((mouse.x != selection_data_.end_x) ||
973 (mouse.y != selection_data_.end_y)) {
974 selection_data_.end_x = mouse.x;
975 selection_data_.end_y = mouse.y;
976 selection_data_.empty =
false;
982 if (mouse.motion == Mouse::Released) {
983 selection_pending_ =
nullptr;
984 selection_data_.end_x = mouse.x;
985 selection_data_.end_y = mouse.y;
986 selection_data_.empty =
false;
993void App::Internal::Draw(
Component component) {
997 auto document = component->Render();
1001 document->ComputeRequirement();
1002 switch (dimension_) {
1004 dimx = public_->dimx_;
1005 dimy = public_->dimy_;
1008 dimx = terminal.dimx;
1009 dimy =
util::clamp(document->requirement().min_y, 0, terminal.dimy);
1012 dimx = terminal.dimx;
1013 dimy = terminal.dimy;
1016 dimx =
util::clamp(document->requirement().min_x, 0, terminal.dimx);
1017 dimy =
util::clamp(document->requirement().min_y, 0, terminal.dimy);
1022 TerminalSend(
"\033[?25l");
1024 const bool resized =
1025 frame_count_ == 0 || (dimx != public_->dimx_) || (dimy != public_->dimy_);
1026 TerminalSend(ResetCursorPosition());
1028 if (frame_count_ != 0) {
1031 public_->ResetPosition(output_buffer, resized);
1035 if ((dimx < public_->dimx_) && !use_alternative_screen_) {
1036 TerminalSend(
"\033[J");
1037 TerminalSend(
"\033[H");
1043 public_->dimx_ = dimx;
1044 public_->dimy_ = dimy;
1045 public_->cells_ = std::vector<Cell>(
static_cast<size_t>(dimx) *
1046 static_cast<size_t>(dimy));
1047 Cursor cursor = public_->cursor_;
1048 cursor.x = dimx - 1;
1049 cursor.y = dimy - 1;
1050 public_->SetCursor(cursor);
1056 if (!use_alternative_screen_ && is_stdout_a_tty_) {
1057 RequestCursorPosition(previous_frame_resized_);
1059 previous_frame_resized_ = resized;
1061 selection_ = selection_data_.empty
1062 ? std::make_unique<Selection>()
1063 : std::make_unique<Selection>(
1064 selection_data_.start_x, selection_data_.start_y,
1065 selection_data_.end_x, selection_data_.end_y);
1066 Render(*public_, document.get(), *selection_);
1070 const int dx = public_->dimx_ - 1 - public_->cursor_.x +
1071 int(public_->dimx_ != terminal.dimx);
1072 const int dy = public_->dimy_ - 1 - public_->cursor_.y;
1074 set_cursor_position_.clear();
1075 reset_cursor_position_.clear();
1078 set_cursor_position_ +=
"\x1B[" + std::to_string(dy) +
"A";
1079 reset_cursor_position_ +=
"\x1B[" + std::to_string(dy) +
"B";
1083 set_cursor_position_ +=
"\x1B[" + std::to_string(dx) +
"D";
1084 reset_cursor_position_ +=
"\x1B[" + std::to_string(dx) +
"C";
1087 if (public_->cursor_.shape != Screen::Cursor::Hidden) {
1088 set_cursor_position_ +=
"\033[?25h";
1089 set_cursor_position_ +=
1090 "\033[" + std::to_string(
int(public_->cursor_.shape)) +
" q";
1094 public_->ToString(output_buffer);
1095 TerminalSend(set_cursor_position_);
1099 frame_valid_ =
true;
1103std::string App::Internal::ResetCursorPosition() {
1104 std::string result = std::move(reset_cursor_position_);
1105 reset_cursor_position_ =
"";
1109void App::Internal::RequestCursorPosition(
bool force) {
1110 cursor_position_request.Request(force);
1113void App::Internal::TerminalSend(std::string_view s) {
1117void App::Internal::TerminalFlush() {
1119 output_buffer +=
'\0';
1120 std::cout << output_buffer << std::flush;
1121 output_buffer.clear();
1124void App::Internal::InstallPipedInputHandling() {
1125 is_stdin_a_tty_ =
false;
1126 is_stdout_a_tty_ =
false;
1127#if defined(__EMSCRIPTEN__)
1128 is_stdin_a_tty_ =
true;
1129 is_stdout_a_tty_ =
true;
1130#elif defined(_WIN32)
1131 is_stdin_a_tty_ = _isatty(_fileno(stdin));
1132 is_stdout_a_tty_ = _isatty(_fileno(stdout));
1134 tty_fd_ = STDIN_FILENO;
1135 is_stdout_a_tty_ = isatty(STDOUT_FILENO);
1139 if (!handle_piped_input_) {
1140 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1141 }
else if (isatty(STDIN_FILENO)) {
1142 is_stdin_a_tty_ =
true;
1145 tty_fd_ = open(
"/dev/tty", O_RDONLY);
1148 tty_fd_ = STDIN_FILENO;
1149 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1151 is_stdin_a_tty_ =
true;
1153 on_exit_functions.emplace([
this] {
1162void App::Internal::InstallTerminalInfo() {
1165 if (is_stdout_a_tty_) {
1166 TerminalSend(DECRQSS_DECSCUSR);
1167 TerminalSend(
"\033[>q");
1168 TerminalSend(
"\033[>c");
1169 TerminalSend(
"\033[c");
1174 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
1178 auto setup_receiver = event_buffer.CreateReceiver();
1179 auto start = std::chrono::steady_clock::now();
1180 bool terminal_capabilities_received =
false;
1183 FetchTerminalEvents();
1184 while (setup_receiver->Has()) {
1185 const auto event = setup_receiver->Pop();
1186 if (event.is_cursor_shape()) {
1187 cursor_reset_shape_ =
event.cursor_shape();
1190 if (event.IsTerminalCapabilities()) {
1191 terminal_capabilities_ =
event.TerminalCapabilities();
1192 terminal_capabilities_received =
true;
1195 if (event.IsTerminalNameVersion()) {
1196 terminal_name_ =
event.TerminalName();
1197 terminal_version_ =
event.TerminalVersion();
1200 if (event.IsTerminalEmulator()) {
1201 terminal_emulator_name_ =
event.TerminalEmulatorName();
1202 terminal_emulator_version_ =
event.TerminalEmulatorVersion();
1209 if (terminal_capabilities_received) {
1213 if (std::chrono::steady_clock::now() - start >
1214 std::chrono::milliseconds(500)) {
1217 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1224 auto safe_getenv = [](
const char* name) -> std::string_view {
1225 const char*
value = std::getenv(name);
1230 safe_getenv(
"TERM"), safe_getenv(
"COLORTERM"),
1231 safe_getenv(
"TERM_PROGRAM"), terminal_name_, terminal_emulator_name_,
1232 terminal_capabilities_);
1234 quirks.SetColorSupport(color_support);
1236 const bool is_modern_emulator = (terminal_emulator_name_ !=
"unknown");
1237 const bool is_vt220_plus =
1238 (terminal_name_ !=
"vt100" && terminal_name_ !=
"unknown");
1239 bool reports_utf8 =
false;
1240 for (
const int x : terminal_capabilities_) {
1242 reports_utf8 =
true;
1253 bool modern = is_modern_emulator || is_vt220_plus || reports_utf8;
1255 quirks.SetBlockCharacters(
true);
1256 quirks.SetCursorHiding(
true);
1257 quirks.SetComponentAscii(
false);
1262 on_exit_functions.emplace([
this] {
1263 TerminalSend(
"\033[?25h");
1264 if (is_stdout_a_tty_) {
1265 TerminalSend(
"\033[" + std::to_string(cursor_reset_shape_) +
" q");
1270void App::Internal::Signal(
int signal) {
1271 if (signal == SIGABRT) {
1278 if (signal == SIGTSTP) {
1280 TerminalSend(ResetCursorPosition());
1281 public_->ResetPosition(output_buffer,
true);
1285 (void)std::raise(SIGTSTP);
1291 if (signal == SIGWINCH) {
1292 public_->Post(Event::Special({0}));
1298size_t App::Internal::FetchTerminalEvents() {
1300 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1302 auto console = GetStdHandle(STD_INPUT_HANDLE);
1303 DWORD number_of_events = 0;
1304 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1305 return std::vector<INPUT_RECORD>();
1307 if (number_of_events <= 0) {
1309 return std::vector<INPUT_RECORD>();
1312 std::vector<INPUT_RECORD> records(number_of_events);
1313 DWORD number_of_events_read = 0;
1314 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1315 &number_of_events_read)) {
1316 return std::vector<INPUT_RECORD>();
1318 records.resize(number_of_events_read);
1322 auto records = get_input_records();
1323 if (records.size() == 0) {
1324 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1325 const size_t timeout_microseconds =
1326 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1327 terminal_input_parser.Timeout(timeout_microseconds);
1330 last_char_time = std::chrono::steady_clock::now();
1335 std::wstring wstring;
1336 for (
const auto& r : records) {
1337 switch (r.EventType) {
1339 auto key_event = r.Event.KeyEvent;
1341 if (key_event.bKeyDown == FALSE) {
1344 const wchar_t wc = key_event.uChar.UnicodeChar;
1346 if (wc >= 0xd800 && wc <= 0xdbff) {
1351 terminal_input_parser.Add(it);
1355 case WINDOW_BUFFER_SIZE_EVENT:
1356 public_->Post(Event::Special({0}));
1365 return records.size();
1366#elif defined(__EMSCRIPTEN__)
1369 std::array<char, 128> out{};
1370 const ssize_t l = read(STDIN_FILENO, out.data(), out.size());
1372 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1373 const size_t timeout_microseconds =
1374 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1375 terminal_input_parser.Timeout(timeout_microseconds);
1378 last_char_time = std::chrono::steady_clock::now();
1381 for (ssize_t i = 0; i < l; ++i) {
1382 terminal_input_parser.Add(out.at(
static_cast<size_t>(i)));
1386 struct pollfd pfd = {tty_fd_, POLLIN, 0};
1387 const int poll_result = poll(&pfd, 1, 0);
1388 if (poll_result <= 0) {
1389 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1390 const size_t timeout_ms =
1391 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1392 terminal_input_parser.Timeout(
static_cast<int>(timeout_ms));
1395 last_char_time = std::chrono::steady_clock::now();
1398 std::array<char, 128> out{};
1399 const ssize_t l = read(tty_fd_, out.data(), out.size());
1405 for (ssize_t i = 0; i < l; ++i) {
1406 terminal_input_parser.Add(out.at(
static_cast<size_t>(i)));
1412void App::Internal::PostAnimationTask() {
1413 public_->Post(AnimationTask());
1417 task_runner.PostDelayedTask([
this] { PostAnimationTask(); },
1418 std::chrono::milliseconds(15));
1421App::App(std::unique_ptr<Internal> internal,
int dimx,
int dimy)
1422 : Screen(dimx, dimy), internal_(std::move(internal)) {
1423 internal_->public_ =
this;
1426App::App(App&& other) noexcept : Screen(std::move(other)) {
1427 internal_ = std::move(other.internal_);
1429 internal_->public_ =
this;
1433App& App::operator=(App&& other)
noexcept {
1434 Screen::operator=(std::move(other));
1435 internal_ = std::move(other.internal_);
1437 internal_->public_ =
this;
1442App::~App() =
default;
1445App App::FixedSize(
int dimx,
int dimy) {
1448 return App(std::move(internal), dimx, dimy);
1452App App::Fullscreen() {
1453 return FullscreenAlternateScreen();
1457App App::FullscreenPrimaryScreen() {
1461 return App(std::move(internal), terminal.dimx, terminal.dimy);
1465App App::FullscreenAlternateScreen() {
1469 return App(std::move(internal), terminal.dimx, terminal.dimy);
1473App App::FitComponent() {
1477 return App(std::move(internal), terminal.dimx, terminal.dimy);
1481App App::TerminalOutput() {
1485 return App(std::move(internal), terminal.dimx, terminal.dimy);
1488void App::TrackMouse(
bool enable) {
1489 internal_->track_mouse_ = enable;
1492void App::HandlePipedInput(
bool enable) {
1493 internal_->handle_piped_input_ = enable;
1498 return g_active_screen;
1502 class Loop loop(this, std::move(component));
1507 Post([
this] { internal_->ExitNow(); });
1510Closure App::ExitLoopClosure() {
1511 return [
this] { Exit(); };
1514void App::Post(
Task task) {
1515 internal_->task_runner.PostTask([
this, task = std::move(task)]()
mutable {
1516 if (internal_->component_) {
1517 internal_->HandleTask(internal_->component_, task);
1522 if (std::holds_alternative<Closure>(task)) {
1523 std::get<Closure>(task)();
1528void App::PostEvent(Event event) {
1531 Post(
Task(std::move(event)));
1535void App::PostEventOrExecute(
Closure closure) {
1539 if (
auto* app = App::Active()) {
1540 app->Post(std::move(closure));
1546void App::RequestAnimationFrame() {
1547 if (internal_->animation_requested_) {
1550 internal_->animation_requested_ =
true;
1551 auto now = animation::Clock::now();
1552 const auto time_histeresis = std::chrono::milliseconds(33);
1553 if (now - internal_->previous_animation_time_ >= time_histeresis) {
1554 internal_->previous_animation_time_ = now;
1559 if (internal_->mouse_captured) {
1562 internal_->mouse_captured =
true;
1563 return std::make_unique<CapturedMouseImpl>(
1564 [
this] { internal_->mouse_captured =
false; });
1569 internal_->Uninstall();
1571 internal_->Install();
1575void App::ForceHandleCtrlC(
bool force) {
1576 internal_->force_handle_ctrl_c_ = force;
1579void App::ForceHandleCtrlZ(
bool force) {
1580 internal_->force_handle_ctrl_z_ = force;
1583std::string App::GetSelection() {
1584 if (!internal_->selection_) {
1587 return internal_->selection_->GetParts();
1590void App::SelectionChange(std::function<
void()> callback) {
1591 internal_->selection_on_change_ = std::move(callback);
1594const std::string& App::TerminalName()
const {
1595 return internal_->terminal_name_;
1598int App::TerminalVersion()
const {
1599 return internal_->terminal_version_;
1602const std::string& App::TerminalEmulatorName()
const {
1603 return internal_->terminal_emulator_name_;
1606const std::string& App::TerminalEmulatorVersion()
const {
1607 return internal_->terminal_emulator_version_;
1610const std::vector<int>& App::TerminalCapabilities()
const {
1611 return internal_->terminal_capabilities_;
1614std::vector<std::string> App::TerminalCapabilityNames()
const {
1615 return Event::TerminalCapabilities(
"", internal_->terminal_capabilities_)
1616 .TerminalCapabilityNames();
1621void App::ExitNow() {
1622 internal_->ExitNow();
1624void App::Install() {
1625 internal_->Install();
1627void App::Uninstall() {
1628 internal_->Uninstall();
1630void App::PreMain() {
1631 internal_->PreMain();
1633void App::PostMain() {
1634 internal_->PostMain();
1636bool App::HasQuitted() {
1637 return internal_->HasQuitted();
1639void App::RunOnce(
const Component& component) {
1640 internal_->RunOnce(component);
1642void App::RunOnceBlocking(
Component component) {
1643 internal_->RunOnceBlocking(component);
1646 internal_->HandleTask(component, task);
1648bool App::HandleSelection(
bool handled, Event event) {
1649 return internal_->HandleSelection(handled, event);
1652 internal_->Draw(component);
1654std::string App::ResetCursorPosition() {
1655 return internal_->ResetCursorPosition();
1657void App::RequestCursorPosition(
bool force) {
1658 internal_->RequestCursorPosition(force);
1660void App::TerminalSend(std::string_view s) {
1661 internal_->TerminalSend(s);
1663void App::TerminalFlush() {
1664 internal_->TerminalFlush();
1666void App::InstallPipedInputHandling() {
1667 internal_->InstallPipedInputHandling();
1669void App::InstallTerminalInfo() {
1670 internal_->InstallTerminalInfo();
1672void App::Signal(
int signal) {
1673 internal_->Signal(signal);
1675size_t App::FetchTerminalEvents() {
1676 return internal_->FetchTerminalEvents();
1678void App::PostAnimationTask() {
1679 internal_->PostAnimationTask();
1682Loop::Loop(App* screen,
Component component)
1683 : screen_(screen), component_(std::move(component)) {
1688 screen_->PostMain();
1691bool Loop::HasQuitted() {
1692 return screen_->HasQuitted();
1695void Loop::RunOnce() {
1696 screen_->RunOnce(component_);
1699void Loop::RunOnceBlocking() {
1700 screen_->RunOnceBlocking(component_);
1704 while (!HasQuitted()) {
Quirks GetQuirks()
Get the terminal quirks.
Dimensions Size()
Get the terminal size.
void SetQuirks(const Quirks &quirks)
Override terminal quirks.
The FTXUI ftxui::animation:: namespace.
void SetFallbackSize(const Dimensions &fallbackSize)
Override terminal size in case auto-detection fails.
Color ComputeColorSupport(std::string_view term, std::string_view colorterm, std::string_view term_program, std::string_view terminal_name, std::string_view terminal_emulator_name, const std::vector< int > &capabilities)
Compute the color support based on environment variables and terminal identification.
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)
The FTXUI ftxui:: namespace.
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(std::wstring_view s)
Convert a std::wstring into a UTF8 std::string.
std::variant< Event, Closure, AnimationTask > Task
void Render(Screen &screen, Node *node, Selection &selection)
std::function< void()> Closure
std::shared_ptr< ComponentBase > Component