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() {
119 FD_SET(STDIN_FILENO, &fds);
120 select(STDIN_FILENO + 1, &fds,
nullptr,
nullptr, &tv);
121 return FD_ISSET(STDIN_FILENO, &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;
378 internal_->task_runner.
PostTask([
this, task = std::move(task)]()
mutable {
379 HandleTask(component_, task);
392 if (animation_requested_) {
395 animation_requested_ =
true;
396 auto now = animation::Clock::now();
397 const auto time_histeresis = std::chrono::milliseconds(33);
398 if (now - previous_animation_time_ >= time_histeresis) {
399 previous_animation_time_ = now;
407 if (mouse_captured) {
410 mouse_captured =
true;
411 return std::make_unique<CapturedMouseImpl>(
412 [
this] { mouse_captured =
false; });
418 class Loop loop(this, std::move(component));
423bool ScreenInteractive::HasQuitted() {
428void ScreenInteractive::PreMain() {
430 if (g_active_screen) {
431 std::swap(suspended_screen_, g_active_screen);
433 suspended_screen_->ResetCursorPosition();
435 suspended_screen_->
dimx_ = 0;
436 suspended_screen_->
dimy_ = 0;
439 suspended_screen_->Uninstall();
443 g_active_screen =
this;
444 g_active_screen->Install();
446 previous_animation_time_ = animation::Clock::now();
450void ScreenInteractive::PostMain() {
452 ResetCursorPosition();
454 g_active_screen =
nullptr;
457 if (suspended_screen_) {
463 std::swap(g_active_screen, suspended_screen_);
464 g_active_screen->Install();
471 if (!use_alternative_screen_) {
473 std::cout << std::flush;
492 force_handle_ctrl_c_ = force;
498 force_handle_ctrl_z_ = force;
506 return selection_->GetParts();
510 selection_on_change_ = std::move(callback);
516 return g_active_screen;
520void ScreenInteractive::Install() {
521 frame_valid_ =
false;
532 on_exit_functions.emplace([] { Flush(); });
538 std::cout << DECRQSS_DECSCUSR;
539 on_exit_functions.emplace([
this] {
540 std::cout <<
"\033[?25h";
541 std::cout <<
"\033[" + std::to_string(cursor_reset_shape_) +
" q";
546 for (
const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
547 InstallSignalHandler(signal);
553 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
554 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
558 GetConsoleMode(stdout_handle, &out_mode);
559 GetConsoleMode(stdin_handle, &in_mode);
560 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
561 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
564 const int enable_virtual_terminal_processing = 0x0004;
565 const int disable_newline_auto_return = 0x0008;
566 out_mode |= enable_virtual_terminal_processing;
567 out_mode |= disable_newline_auto_return;
570 const int enable_line_input = 0x0002;
571 const int enable_echo_input = 0x0004;
572 const int enable_virtual_terminal_input = 0x0200;
573 const int enable_window_input = 0x0008;
574 in_mode &= ~enable_echo_input;
575 in_mode &= ~enable_line_input;
576 in_mode |= enable_virtual_terminal_input;
577 in_mode |= enable_window_input;
579 SetConsoleMode(stdin_handle, in_mode);
580 SetConsoleMode(stdout_handle, out_mode);
590 for (
const int signal : {SIGWINCH, SIGTSTP}) {
591 InstallSignalHandler(signal);
594 struct termios terminal;
595 tcgetattr(STDIN_FILENO, &terminal);
596 on_exit_functions.emplace(
597 [=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
600 terminal.c_iflag &= ~IGNBRK;
601 terminal.c_iflag &= ~BRKINT;
603 terminal.c_iflag &= ~PARMRK;
604 terminal.c_iflag &= ~ISTRIP;
605 terminal.c_iflag &= ~INLCR;
606 terminal.c_iflag &= ~IGNCR;
607 terminal.c_iflag &= ~ICRNL;
608 terminal.c_iflag &= ~IXON;
610 terminal.c_lflag &= ~ECHO;
611 terminal.c_lflag &= ~ECHONL;
612 terminal.c_lflag &= ~ICANON;
613 terminal.c_lflag &= ~ISIG;
618 terminal.c_lflag &= ~IEXTEN;
619 terminal.c_cflag |= CS8;
621 terminal.c_cc[VMIN] = 0;
623 terminal.c_cc[VTIME] = 0;
625 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
629 auto enable = [&](
const std::vector<DECMode>& parameters) {
630 std::cout << Set(parameters);
631 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
634 auto disable = [&](
const std::vector<DECMode>& parameters) {
635 std::cout << Reset(parameters);
636 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
639 if (use_alternative_screen_) {
641 DECMode::kAlternateScreen,
651 enable({DECMode::kMouseVt200});
652 enable({DECMode::kMouseAnyEvent});
653 enable({DECMode::kMouseUrxvtMode});
654 enable({DECMode::kMouseSgrExtMode});
667void ScreenInteractive::Uninstall() {
674void ScreenInteractive::RunOnceBlocking(
Component component) {
676 const auto time_per_frame = std::chrono::microseconds(16666);
678 auto time = std::chrono::steady_clock::now();
679 size_t executed_task = internal_->task_runner.
ExecutedTasks();
682 while (executed_task == internal_->task_runner.
ExecutedTasks() &&
686 const auto now = std::chrono::steady_clock::now();
687 const auto delta = now - time;
690 if (delta < time_per_frame) {
691 const auto sleep_duration = time_per_frame - delta;
692 std::this_thread::sleep_for(sleep_duration);
698void ScreenInteractive::RunOnce(
Component component) {
699 AutoReset set_component(&component_, component);
700 ExecuteSignalHandlers();
701 FetchTerminalEvents();
704 const size_t executed_task = internal_->task_runner.
ExecutedTasks();
707 if (executed_task == internal_->task_runner.
ExecutedTasks()) {
711 ExecuteSignalHandlers();
714 if (selection_data_previous_ != selection_data_) {
715 selection_data_previous_ = selection_data_;
716 if (selection_on_change_) {
717 selection_on_change_();
725void ScreenInteractive::HandleTask(
Component component,
Task& task) {
728 using T = std::decay_t<
decltype(arg)>;
732 if constexpr (std::is_same_v<T, Event>) {
734 if (arg.is_cursor_position()) {
735 cursor_x_ = arg.cursor_x();
736 cursor_y_ = arg.cursor_y();
740 if (arg.is_cursor_shape()) {
741 cursor_reset_shape_= arg.cursor_shape();
745 if (arg.is_mouse()) {
746 arg.mouse().x -= cursor_x_;
747 arg.mouse().y -= cursor_y_;
752 bool handled = component->OnEvent(arg);
754 handled = HandleSelection(handled, arg);
756 if (arg ==
Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
757 RecordSignal(SIGABRT);
761 if (arg ==
Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
762 RecordSignal(SIGTSTP);
766 frame_valid_ =
false;
771 if constexpr (std::is_same_v<T, Closure>) {
777 if constexpr (std::is_same_v<T, AnimationTask>) {
778 if (!animation_requested_) {
782 animation_requested_ =
false;
785 previous_animation_time_ = now;
787 animation::Params params(delta);
788 component->OnAnimation(params);
789 frame_valid_ =
false;
798bool ScreenInteractive::HandleSelection(
bool handled, Event event) {
800 selection_pending_ =
nullptr;
801 selection_data_.empty =
true;
802 selection_ =
nullptr;
806 if (!event.is_mouse()) {
810 auto& mouse =
event.mouse();
817 selection_data_.start_x = mouse.x;
818 selection_data_.start_y = mouse.y;
819 selection_data_.end_x = mouse.x;
820 selection_data_.end_y = mouse.y;
824 if (!selection_pending_) {
829 if ((mouse.x != selection_data_.end_x) ||
830 (mouse.y != selection_data_.end_y)) {
831 selection_data_.end_x = mouse.x;
832 selection_data_.end_y = mouse.y;
833 selection_data_.empty =
false;
840 selection_pending_ =
nullptr;
841 selection_data_.end_x = mouse.x;
842 selection_data_.end_y = mouse.y;
843 selection_data_.empty =
false;
852void ScreenInteractive::Draw(
Component component) {
856 auto document = component->Render();
860 document->ComputeRequirement();
861 switch (dimension_) {
862 case Dimension::Fixed:
866 case Dimension::TerminalOutput:
867 dimx = terminal.dimx;
870 case Dimension::Fullscreen:
871 dimx = terminal.dimx;
872 dimy = terminal.dimy;
874 case Dimension::FitComponent:
881 ResetCursorPosition();
886 if ((
dimx <
dimx_) && !use_alternative_screen_) {
887 std::cout <<
"\033[J";
888 std::cout <<
"\033[H";
895 pixels_ = std::vector<std::vector<Pixel>>(
dimy, std::vector<Pixel>(
dimx));
903#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
912 if (!use_alternative_screen_ && (i % 150 == 0)) {
913 std::cout << DeviceStatusReport(DSRMode::kCursor);
918 if (!use_alternative_screen_ &&
919 (previous_frame_resized_ || i % 40 == 0)) {
920 std::cout << DeviceStatusReport(DSRMode::kCursor);
923 previous_frame_resized_ = resized;
925 selection_ = selection_data_.empty
926 ? std::make_unique<Selection>()
927 : std::make_unique<Selection>(
928 selection_data_.start_x, selection_data_.start_y,
929 selection_data_.end_x, selection_data_.end_y);
930 Render(*
this, document.get(), *selection_);
937 set_cursor_position.clear();
938 reset_cursor_position.clear();
941 set_cursor_position +=
"\x1B[" + std::to_string(dy) +
"A";
942 reset_cursor_position +=
"\x1B[" + std::to_string(dy) +
"B";
946 set_cursor_position +=
"\x1B[" + std::to_string(dx) +
"D";
947 reset_cursor_position +=
"\x1B[" + std::to_string(dx) +
"C";
951 set_cursor_position +=
"\033[?25l";
953 set_cursor_position +=
"\033[?25h";
954 set_cursor_position +=
959 std::cout <<
ToString() << set_cursor_position;
967void ScreenInteractive::ResetCursorPosition() {
968 std::cout << reset_cursor_position;
969 reset_cursor_position =
"";
974 return [
this] {
Exit(); };
979 Post([
this] { ExitNow(); });
983void ScreenInteractive::ExitNow() {
988void ScreenInteractive::Signal(
int signal) {
989 if (signal == SIGABRT) {
996 if (signal == SIGTSTP) {
998 ResetCursorPosition();
1004 std::ignore = std::raise(SIGTSTP);
1010 if (signal == SIGWINCH) {
1017void ScreenInteractive::FetchTerminalEvents() {
1019 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1021 auto console = GetStdHandle(STD_INPUT_HANDLE);
1022 DWORD number_of_events = 0;
1023 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1024 return std::vector<INPUT_RECORD>();
1026 if (number_of_events <= 0) {
1028 return std::vector<INPUT_RECORD>();
1031 std::vector<INPUT_RECORD> records(number_of_events);
1032 DWORD number_of_events_read = 0;
1033 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1034 &number_of_events_read)) {
1035 return std::vector<INPUT_RECORD>();
1037 records.resize(number_of_events_read);
1041 auto records = get_input_records();
1042 if (records.size() == 0) {
1043 const auto timeout =
1044 std::chrono::steady_clock::now() - internal_->last_char_time;
1045 const size_t timeout_microseconds =
1046 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1047 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1050 internal_->last_char_time = std::chrono::steady_clock::now();
1055 for (
const auto& r : records) {
1056 switch (r.EventType) {
1058 auto key_event = r.Event.KeyEvent;
1060 if (key_event.bKeyDown == FALSE) {
1063 std::wstring wstring;
1064 wstring += key_event.uChar.UnicodeChar;
1066 internal_->terminal_input_parser.
Add(it);
1069 case WINDOW_BUFFER_SIZE_EVENT:
1079#elif defined(__EMSCRIPTEN__)
1082 std::array<char, 128> out{};
1083 size_t l = read(STDIN_FILENO, out.data(), out.size());
1085 const auto timeout =
1086 std::chrono::steady_clock::now() - internal_->last_char_time;
1087 const size_t timeout_microseconds =
1088 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1089 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1092 internal_->last_char_time = std::chrono::steady_clock::now();
1095 for (
size_t i = 0; i < l; ++i) {
1096 internal_->terminal_input_parser.
Add(out[i]);
1099 if (!CheckStdinReady()) {
1100 const auto timeout =
1101 std::chrono::steady_clock::now() - internal_->last_char_time;
1102 const size_t timeout_ms =
1103 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1104 internal_->terminal_input_parser.
Timeout(timeout_ms);
1107 internal_->last_char_time = std::chrono::steady_clock::now();
1110 std::array<char, 128> out{};
1111 size_t l = read(fileno(stdin), out.data(), out.size());
1114 for (
size_t i = 0; i < l; ++i) {
1115 internal_->terminal_input_parser.
Add(out[i]);
1120void ScreenInteractive::PostAnimationTask() {
1121 Post(AnimationTask());
1125 internal_->task_runner.
PostDelayedTask([
this] { PostAnimationTask(); },
1126 std::chrono::milliseconds(15));
1129bool ScreenInteractive::SelectionData::operator==(
1130 const ScreenInteractive::SelectionData& other)
const {
1131 if (empty && other.empty) {
1134 if (empty || other.empty) {
1137 return start_x == other.start_x && start_y == other.start_y &&
1138 end_x == other.end_x && end_y == other.end_y;
1141bool ScreenInteractive::SelectionData::operator!=(
1142 const ScreenInteractive::SelectionData& other)
const {
1143 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 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
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