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>
57struct ScreenInteractive::Internal {
59 TerminalInputParser terminal_input_parser;
61 task::TaskRunner task_runner;
64 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
65 std::chrono::steady_clock::now();
67 explicit Internal(std::function<
void(Event)> out)
68 : terminal_input_parser(std::move(out)) {}
75 screen->RequestAnimationFrame();
82ScreenInteractive* g_active_screen =
nullptr;
86 std::cout <<
'\0' << std::flush;
89constexpr int timeout_milliseconds = 20;
90[[maybe_unused]]
constexpr int timeout_microseconds =
91 timeout_milliseconds * 1000;
94#elif defined(__EMSCRIPTEN__)
95#include <emscripten.h>
99void ftxui_on_resize(
int columns,
int rows) {
104 std::raise(SIGWINCH);
110int CheckStdinReady(
int fd) {
115 select(fd + 1, &fds,
nullptr,
nullptr, &tv);
116 return FD_ISSET(fd, &fds);
121std::stack<Closure> on_exit_functions;
123 while (!on_exit_functions.empty()) {
124 on_exit_functions.top()();
125 on_exit_functions.pop();
129std::atomic<int> g_signal_exit_count = 0;
131std::atomic<int> g_signal_stop_count = 0;
132std::atomic<int> g_signal_resize_count = 0;
136void RecordSignal(
int signal) {
144 g_signal_exit_count++;
149 g_signal_stop_count++;
153 g_signal_resize_count++;
162void ExecuteSignalHandlers() {
163 int signal_exit_count = g_signal_exit_count.exchange(0);
164 while (signal_exit_count--) {
169 int signal_stop_count = g_signal_stop_count.exchange(0);
170 while (signal_stop_count--) {
174 int signal_resize_count = g_signal_resize_count.exchange(0);
175 while (signal_resize_count--) {
181void InstallSignalHandler(
int sig) {
182 auto old_signal_handler = std::signal(sig, RecordSignal);
183 on_exit_functions.emplace(
184 [=] { std::ignore = std::signal(sig, old_signal_handler); });
188const std::string CSI =
"\x1b[";
191const std::string DCS =
"\x1bP";
193const std::string ST =
"\x1b\\";
197const std::string DECRQSS_DECSCUSR = DCS +
"$q q" + ST;
200enum class DECMode : std::uint16_t {
206 kMouseVt200Highlight = 1001,
208 kMouseBtnEventMouse = 1002,
209 kMouseAnyEvent = 1003,
212 kMouseSgrExtMode = 1006,
213 kMouseUrxvtMode = 1015,
214 kMouseSgrPixelsMode = 1016,
215 kAlternateScreen = 1049,
219enum class DSRMode : std::uint8_t {
223std::string Serialize(
const std::vector<DECMode>& parameters) {
226 for (
const DECMode parameter : parameters) {
230 out += std::to_string(
int(parameter));
237std::string Set(
const std::vector<DECMode>& parameters) {
238 return CSI +
"?" + Serialize(parameters) +
"h";
242std::string Reset(
const std::vector<DECMode>& parameters) {
243 return CSI +
"?" + Serialize(parameters) +
"l";
247std::string DeviceStatusReport(DSRMode ps) {
248 return CSI + std::to_string(
int(ps)) +
"n";
251class CapturedMouseImpl :
public CapturedMouseInterface {
253 explicit CapturedMouseImpl(std::function<
void(
void)> callback)
254 : callback_(std::move(callback)) {}
255 ~CapturedMouseImpl()
override { callback_(); }
256 CapturedMouseImpl(
const CapturedMouseImpl&) =
delete;
257 CapturedMouseImpl(CapturedMouseImpl&&) =
delete;
258 CapturedMouseImpl& operator=(
const CapturedMouseImpl&) =
delete;
259 CapturedMouseImpl& operator=(CapturedMouseImpl&&) =
delete;
262 std::function<void(
void)> callback_;
267ScreenInteractive::ScreenInteractive(
Dimension dimension,
270 bool use_alternative_screen)
271 : Screen(dimx, dimy),
272 dimension_(dimension),
273 use_alternative_screen_(use_alternative_screen) {
274 internal_ = std::make_unique<Internal>(
275 [&](Event event) { PostEvent(std::move(event)); });
303 Dimension::Fullscreen,
316 Dimension::Fullscreen,
329 Dimension::TerminalOutput,
344 Dimension::FitComponent,
367 track_mouse_ = enable;
379 handle_piped_input_ = enable;
385 internal_->task_runner.
PostTask([
this, task = std::move(task)]()
mutable {
386 HandleTask(component_, task);
399 if (animation_requested_) {
402 animation_requested_ =
true;
403 auto now = animation::Clock::now();
404 const auto time_histeresis = std::chrono::milliseconds(33);
405 if (now - previous_animation_time_ >= time_histeresis) {
406 previous_animation_time_ = now;
414 if (mouse_captured) {
417 mouse_captured =
true;
418 return std::make_unique<CapturedMouseImpl>(
419 [
this] { mouse_captured =
false; });
425 class Loop loop(this, std::move(component));
430bool ScreenInteractive::HasQuitted() {
435void ScreenInteractive::PreMain() {
437 if (g_active_screen) {
438 std::swap(suspended_screen_, g_active_screen);
440 suspended_screen_->ResetCursorPosition();
442 suspended_screen_->
dimx_ = 0;
443 suspended_screen_->
dimy_ = 0;
446 suspended_screen_->Uninstall();
450 g_active_screen =
this;
451 g_active_screen->Install();
453 previous_animation_time_ = animation::Clock::now();
457void ScreenInteractive::PostMain() {
459 ResetCursorPosition();
461 g_active_screen =
nullptr;
464 if (suspended_screen_) {
470 std::swap(g_active_screen, suspended_screen_);
471 g_active_screen->Install();
478 if (!use_alternative_screen_) {
480 std::cout << std::flush;
499 force_handle_ctrl_c_ = force;
505 force_handle_ctrl_z_ = force;
513 return selection_->GetParts();
517 selection_on_change_ = std::move(callback);
523 return g_active_screen;
527void ScreenInteractive::Install() {
528 frame_valid_ =
false;
537 InstallPipedInputHandling();
541 on_exit_functions.emplace([] { Flush(); });
545 std::cout << DECRQSS_DECSCUSR;
546 on_exit_functions.emplace([
this] {
547 std::cout <<
"\033[?25h";
548 std::cout <<
"\033[" + std::to_string(cursor_reset_shape_) +
" q";
553 for (
const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
554 InstallSignalHandler(signal);
560 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
561 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
565 GetConsoleMode(stdout_handle, &out_mode);
566 GetConsoleMode(stdin_handle, &in_mode);
567 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
568 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
571 const int enable_virtual_terminal_processing = 0x0004;
572 const int disable_newline_auto_return = 0x0008;
573 out_mode |= enable_virtual_terminal_processing;
574 out_mode |= disable_newline_auto_return;
577 const int enable_line_input = 0x0002;
578 const int enable_echo_input = 0x0004;
579 const int enable_virtual_terminal_input = 0x0200;
580 const int enable_window_input = 0x0008;
581 in_mode &= ~enable_echo_input;
582 in_mode &= ~enable_line_input;
583 in_mode |= enable_virtual_terminal_input;
584 in_mode |= enable_window_input;
586 SetConsoleMode(stdin_handle, in_mode);
587 SetConsoleMode(stdout_handle, out_mode);
597 for (
const int signal : {SIGWINCH, SIGTSTP}) {
598 InstallSignalHandler(signal);
601 struct termios terminal;
602 tcgetattr(tty_fd_, &terminal);
603 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
604 tcsetattr(tty_fd_, TCSANOW, &terminal);
608 terminal.c_iflag &= ~IGNBRK;
609 terminal.c_iflag &= ~BRKINT;
611 terminal.c_iflag &= ~PARMRK;
612 terminal.c_iflag &= ~ISTRIP;
613 terminal.c_iflag &= ~INLCR;
614 terminal.c_iflag &= ~IGNCR;
615 terminal.c_iflag &= ~ICRNL;
616 terminal.c_iflag &= ~IXON;
618 terminal.c_lflag &= ~ECHO;
619 terminal.c_lflag &= ~ECHONL;
620 terminal.c_lflag &= ~ICANON;
621 terminal.c_lflag &= ~ISIG;
626 terminal.c_lflag &= ~IEXTEN;
627 terminal.c_cflag |= CS8;
629 terminal.c_cc[VMIN] = 0;
631 terminal.c_cc[VTIME] = 0;
633 tcsetattr(tty_fd_, TCSANOW, &terminal);
637 auto enable = [&](
const std::vector<DECMode>& parameters) {
638 std::cout << Set(parameters);
639 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
642 auto disable = [&](
const std::vector<DECMode>& parameters) {
643 std::cout << Reset(parameters);
644 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
647 if (use_alternative_screen_) {
649 DECMode::kAlternateScreen,
659 enable({DECMode::kMouseVt200});
660 enable({DECMode::kMouseAnyEvent});
661 enable({DECMode::kMouseUrxvtMode});
662 enable({DECMode::kMouseSgrExtMode});
674void ScreenInteractive::InstallPipedInputHandling() {
675#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
676 tty_fd_ = STDIN_FILENO;
680 if (!handle_piped_input_) {
685 if (isatty(STDIN_FILENO)) {
690 tty_fd_ = open(
"/dev/tty", O_RDONLY);
693 tty_fd_ = STDIN_FILENO;
698 on_exit_functions.emplace([
this] {
706void ScreenInteractive::Uninstall() {
713void ScreenInteractive::RunOnceBlocking(
Component component) {
715 const auto time_per_frame = std::chrono::microseconds(16666);
717 auto time = std::chrono::steady_clock::now();
718 size_t executed_task = internal_->task_runner.
ExecutedTasks();
721 while (executed_task == internal_->task_runner.
ExecutedTasks() &&
725 const auto now = std::chrono::steady_clock::now();
726 const auto delta = now - time;
729 if (delta < time_per_frame) {
730 const auto sleep_duration = time_per_frame - delta;
731 std::this_thread::sleep_for(sleep_duration);
737void ScreenInteractive::RunOnce(
Component component) {
738 AutoReset set_component(&component_, component);
739 ExecuteSignalHandlers();
740 FetchTerminalEvents();
743 const size_t executed_task = internal_->task_runner.
ExecutedTasks();
746 if (executed_task == internal_->task_runner.
ExecutedTasks()) {
750 ExecuteSignalHandlers();
753 if (selection_data_previous_ != selection_data_) {
754 selection_data_previous_ = selection_data_;
755 if (selection_on_change_) {
756 selection_on_change_();
764void ScreenInteractive::HandleTask(
Component component,
Task& task) {
767 using T = std::decay_t<
decltype(arg)>;
771 if constexpr (std::is_same_v<T, Event>) {
773 if (arg.is_cursor_position()) {
774 cursor_x_ = arg.cursor_x();
775 cursor_y_ = arg.cursor_y();
779 if (arg.is_cursor_shape()) {
780 cursor_reset_shape_= arg.cursor_shape();
784 if (arg.is_mouse()) {
785 arg.mouse().x -= cursor_x_;
786 arg.mouse().y -= cursor_y_;
791 bool handled = component->OnEvent(arg);
793 handled = HandleSelection(handled, arg);
795 if (arg ==
Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
796 RecordSignal(SIGABRT);
800 if (arg ==
Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
801 RecordSignal(SIGTSTP);
805 frame_valid_ =
false;
810 if constexpr (std::is_same_v<T, Closure>) {
816 if constexpr (std::is_same_v<T, AnimationTask>) {
817 if (!animation_requested_) {
821 animation_requested_ =
false;
824 previous_animation_time_ = now;
826 animation::Params params(delta);
827 component->OnAnimation(params);
828 frame_valid_ =
false;
837bool ScreenInteractive::HandleSelection(
bool handled, Event event) {
839 selection_pending_ =
nullptr;
840 selection_data_.empty =
true;
841 selection_ =
nullptr;
845 if (!event.is_mouse()) {
849 auto& mouse =
event.mouse();
856 selection_data_.start_x = mouse.x;
857 selection_data_.start_y = mouse.y;
858 selection_data_.end_x = mouse.x;
859 selection_data_.end_y = mouse.y;
863 if (!selection_pending_) {
868 if ((mouse.x != selection_data_.end_x) ||
869 (mouse.y != selection_data_.end_y)) {
870 selection_data_.end_x = mouse.x;
871 selection_data_.end_y = mouse.y;
872 selection_data_.empty =
false;
879 selection_pending_ =
nullptr;
880 selection_data_.end_x = mouse.x;
881 selection_data_.end_y = mouse.y;
882 selection_data_.empty =
false;
891void ScreenInteractive::Draw(
Component component) {
895 auto document = component->Render();
899 document->ComputeRequirement();
900 switch (dimension_) {
901 case Dimension::Fixed:
905 case Dimension::TerminalOutput:
906 dimx = terminal.dimx;
909 case Dimension::Fullscreen:
910 dimx = terminal.dimx;
911 dimy = terminal.dimy;
913 case Dimension::FitComponent:
920 ResetCursorPosition();
925 if ((
dimx <
dimx_) && !use_alternative_screen_) {
926 std::cout <<
"\033[J";
927 std::cout <<
"\033[H";
934 pixels_ = std::vector<std::vector<Pixel>>(
dimy, std::vector<Pixel>(
dimx));
942#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
951 if (!use_alternative_screen_ && (i % 150 == 0)) {
952 std::cout << DeviceStatusReport(DSRMode::kCursor);
957 if (!use_alternative_screen_ &&
958 (previous_frame_resized_ || i % 40 == 0)) {
959 std::cout << DeviceStatusReport(DSRMode::kCursor);
962 previous_frame_resized_ = resized;
964 selection_ = selection_data_.empty
965 ? std::make_unique<Selection>()
966 : std::make_unique<Selection>(
967 selection_data_.start_x, selection_data_.start_y,
968 selection_data_.end_x, selection_data_.end_y);
969 Render(*
this, document.get(), *selection_);
976 set_cursor_position.clear();
977 reset_cursor_position.clear();
980 set_cursor_position +=
"\x1B[" + std::to_string(dy) +
"A";
981 reset_cursor_position +=
"\x1B[" + std::to_string(dy) +
"B";
985 set_cursor_position +=
"\x1B[" + std::to_string(dx) +
"D";
986 reset_cursor_position +=
"\x1B[" + std::to_string(dx) +
"C";
990 set_cursor_position +=
"\033[?25l";
992 set_cursor_position +=
"\033[?25h";
993 set_cursor_position +=
998 std::cout <<
ToString() << set_cursor_position;
1001 frame_valid_ =
true;
1006void ScreenInteractive::ResetCursorPosition() {
1007 std::cout << reset_cursor_position;
1008 reset_cursor_position =
"";
1013 return [
this] {
Exit(); };
1018 Post([
this] { ExitNow(); });
1022void ScreenInteractive::ExitNow() {
1027void ScreenInteractive::Signal(
int signal) {
1028 if (signal == SIGABRT) {
1035 if (signal == SIGTSTP) {
1037 ResetCursorPosition();
1043 std::ignore = std::raise(SIGTSTP);
1049 if (signal == SIGWINCH) {
1056void ScreenInteractive::FetchTerminalEvents() {
1058 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1060 auto console = GetStdHandle(STD_INPUT_HANDLE);
1061 DWORD number_of_events = 0;
1062 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1063 return std::vector<INPUT_RECORD>();
1065 if (number_of_events <= 0) {
1067 return std::vector<INPUT_RECORD>();
1070 std::vector<INPUT_RECORD> records(number_of_events);
1071 DWORD number_of_events_read = 0;
1072 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1073 &number_of_events_read)) {
1074 return std::vector<INPUT_RECORD>();
1076 records.resize(number_of_events_read);
1080 auto records = get_input_records();
1081 if (records.size() == 0) {
1082 const auto timeout =
1083 std::chrono::steady_clock::now() - internal_->last_char_time;
1084 const size_t timeout_microseconds =
1085 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1086 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1089 internal_->last_char_time = std::chrono::steady_clock::now();
1094 std::wstring wstring;
1095 for (
const auto& r : records) {
1096 switch (r.EventType) {
1098 auto key_event = r.Event.KeyEvent;
1100 if (key_event.bKeyDown == FALSE) {
1103 const wchar_t wc = key_event.uChar.UnicodeChar;
1105 if (wc >= 0xd800 && wc <= 0xdbff) {
1110 internal_->terminal_input_parser.
Add(it);
1114 case WINDOW_BUFFER_SIZE_EVENT:
1124#elif defined(__EMSCRIPTEN__)
1127 std::array<char, 128> out{};
1128 size_t l = read(STDIN_FILENO, out.data(), out.size());
1130 const auto timeout =
1131 std::chrono::steady_clock::now() - internal_->last_char_time;
1132 const size_t timeout_microseconds =
1133 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1134 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1137 internal_->last_char_time = std::chrono::steady_clock::now();
1140 for (
size_t i = 0; i < l; ++i) {
1141 internal_->terminal_input_parser.
Add(out[i]);
1144 if (!CheckStdinReady(tty_fd_)) {
1145 const auto timeout =
1146 std::chrono::steady_clock::now() - internal_->last_char_time;
1147 const size_t timeout_ms =
1148 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1149 internal_->terminal_input_parser.
Timeout(timeout_ms);
1152 internal_->last_char_time = std::chrono::steady_clock::now();
1155 std::array<char, 128> out{};
1156 size_t l = read(tty_fd_, out.data(), out.size());
1159 for (
size_t i = 0; i < l; ++i) {
1160 internal_->terminal_input_parser.
Add(out[i]);
1165void ScreenInteractive::PostAnimationTask() {
1166 Post(AnimationTask());
1170 internal_->task_runner.
PostDelayedTask([
this] { PostAnimationTask(); },
1171 std::chrono::milliseconds(15));
1174bool ScreenInteractive::SelectionData::operator==(
1175 const ScreenInteractive::SelectionData& other)
const {
1176 if (empty && other.empty) {
1179 if (empty || other.empty) {
1182 return start_x == other.start_x && start_y == other.start_y &&
1183 end_x == other.end_x && end_y == other.end_y;
1186bool ScreenInteractive::SelectionData::operator!=(
1187 const ScreenInteractive::SelectionData& other)
const {
1188 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 Event Special(std::string_view)
An custom event whose meaning is defined by the user of the library.
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...
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(std::wstring_view 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