15#include <initializer_list>
39#define DEFINE_CONSOLEV2_PROPERTIES
40#define WIN32_LEAN_AND_MEAN
46#error Must be compiled in UNICODE mode
50#include <sys/select.h>
62 screen->RequestAnimationFrame();
69 TerminalInputParser terminal_input_parser;
71 task::TaskRunner task_runner;
74 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
75 std::chrono::steady_clock::now();
82 std::string output_buffer;
84 explicit Internal(std::function<
void(Event)> out)
85 : terminal_input_parser(std::move(out)) {}
90App* g_active_screen =
nullptr;
92std::stack<Closure> on_exit_functions;
95 while (!on_exit_functions.empty()) {
96 on_exit_functions.top()();
97 on_exit_functions.pop();
103#elif defined(__EMSCRIPTEN__)
104#include <emscripten.h>
108void ftxui_on_resize(
int columns,
int rows) {
113 std::raise(SIGWINCH);
119int CheckStdinReady(
int fd) {
124 select(fd + 1, &fds,
nullptr,
nullptr, &tv);
125 return FD_ISSET(fd, &fds);
130std::atomic<int> g_signal_exit_count = 0;
132std::atomic<int> g_signal_stop_count = 0;
133std::atomic<int> g_signal_resize_count = 0;
137void RecordSignal(
int signal) {
145 g_signal_exit_count++;
150 g_signal_stop_count++;
154 g_signal_resize_count++;
163void ExecuteSignalHandlers() {
164 int signal_exit_count = g_signal_exit_count.exchange(0);
165 while (signal_exit_count--) {
170 int signal_stop_count = g_signal_stop_count.exchange(0);
171 while (signal_stop_count--) {
175 int signal_resize_count = g_signal_resize_count.exchange(0);
176 while (signal_resize_count--) {
182void InstallSignalHandler(
int sig) {
183 auto old_signal_handler = std::signal(sig, RecordSignal);
184 on_exit_functions.emplace(
185 [=] { std::ignore = std::signal(sig, old_signal_handler); });
189const std::string CSI =
"\x1b[";
192const std::string DCS =
"\x1bP";
195const std::string ST =
"\x1b\\";
199const std::string DECRQSS_DECSCUSR = DCS +
"$q q" + ST;
202enum class DECMode : std::uint16_t {
208 kMouseVt200Highlight = 1001,
210 kMouseBtnEventMouse = 1002,
211 kMouseAnyEvent = 1003,
214 kMouseSgrExtMode = 1006,
215 kMouseUrxvtMode = 1015,
216 kMouseSgrPixelsMode = 1016,
217 kAlternateScreen = 1049,
221enum class DSRMode : std::uint8_t {
225std::string Serialize(
const std::vector<DECMode>& parameters) {
228 for (
const DECMode parameter : parameters) {
232 out += std::to_string(
int(parameter));
239std::string Set(
const std::vector<DECMode>& parameters) {
240 return CSI +
"?" + Serialize(parameters) +
"h";
244std::string Reset(
const std::vector<DECMode>& parameters) {
245 return CSI +
"?" + Serialize(parameters) +
"l";
249std::string DeviceStatusReport(DSRMode ps) {
250 return CSI + std::to_string(
int(ps)) +
"n";
253class CapturedMouseImpl :
public CapturedMouseInterface {
255 explicit CapturedMouseImpl(std::function<
void(
void)> callback)
256 : callback_(std::move(callback)) {}
257 ~CapturedMouseImpl()
override { callback_(); }
258 CapturedMouseImpl(
const CapturedMouseImpl&) =
delete;
259 CapturedMouseImpl(CapturedMouseImpl&&) =
delete;
260 CapturedMouseImpl& operator=(
const CapturedMouseImpl&) =
delete;
261 CapturedMouseImpl& operator=(CapturedMouseImpl&&) =
delete;
264 std::function<void(
void)> callback_;
269App::App(
Dimension dimension,
int dimx,
int dimy,
bool use_alternative_screen)
270 : Screen(dimx, dimy),
271 dimension_(dimension),
272 use_alternative_screen_(use_alternative_screen) {
273 internal_ = std::make_unique<Internal>(
274 [&](Event event) { PostEvent(std::move(event)); });
302 Dimension::Fullscreen,
315 Dimension::Fullscreen,
328 Dimension::TerminalOutput,
343 Dimension::FitComponent,
366 track_mouse_ = enable;
378 handle_piped_input_ = enable;
384 internal_->task_runner.
PostTask([
this, task = std::move(task)]()
mutable {
385 HandleTask(component_, task);
398 if (animation_requested_) {
401 animation_requested_ =
true;
402 auto now = animation::Clock::now();
403 const auto time_histeresis = std::chrono::milliseconds(33);
404 if (now - previous_animation_time_ >= time_histeresis) {
405 previous_animation_time_ = now;
413 if (mouse_captured) {
416 mouse_captured =
true;
417 return std::make_unique<CapturedMouseImpl>(
418 [
this] { mouse_captured =
false; });
424 class Loop loop(this, std::move(component));
429bool App::HasQuitted() {
436 if (g_active_screen) {
437 std::swap(suspended_screen_, g_active_screen);
439 suspended_screen_->TerminalSend(suspended_screen_->ResetCursorPosition());
440 suspended_screen_->
ResetPosition(suspended_screen_->internal_->output_buffer,
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 App::PostMain() {
459 TerminalSend(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_) {
481 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;
528 frame_valid_ =
false;
537 InstallPipedInputHandling();
541 on_exit_functions.emplace([
this] { TerminalFlush(); });
545 TerminalSend(DECRQSS_DECSCUSR);
546 on_exit_functions.emplace([
this] {
547 TerminalSend(
"\033[?25h");
548 TerminalSend(
"\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 TerminalSend(Set(parameters));
639 on_exit_functions.emplace(
640 [
this, parameters] { TerminalSend(Reset(parameters)); });
643 auto disable = [&](
const std::vector<DECMode>& parameters) {
644 TerminalSend(Reset(parameters));
645 on_exit_functions.emplace(
646 [
this, parameters] { TerminalSend(Set(parameters)); });
649 if (use_alternative_screen_) {
651 DECMode::kAlternateScreen,
661 enable({DECMode::kMouseVt200});
662 enable({DECMode::kMouseAnyEvent});
663 enable({DECMode::kMouseUrxvtMode});
664 enable({DECMode::kMouseSgrExtMode});
676void App::InstallPipedInputHandling() {
677#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
678 tty_fd_ = STDIN_FILENO;
682 if (!handle_piped_input_) {
687 if (isatty(STDIN_FILENO)) {
692 tty_fd_ = open(
"/dev/tty", O_RDONLY);
695 tty_fd_ = STDIN_FILENO;
700 on_exit_functions.emplace([
this] {
708void App::Uninstall() {
715void App::RunOnceBlocking(
Component component) {
717 const auto time_per_frame = std::chrono::microseconds(16666);
719 auto time = std::chrono::steady_clock::now();
720 size_t executed_task = internal_->task_runner.
ExecutedTasks();
723 while (executed_task == internal_->task_runner.
ExecutedTasks() &&
727 const auto now = std::chrono::steady_clock::now();
728 const auto delta = now - time;
731 if (delta < time_per_frame) {
732 const auto sleep_duration = time_per_frame - delta;
733 std::this_thread::sleep_for(sleep_duration);
740 AutoReset set_component(&component_, component);
741 ExecuteSignalHandlers();
742 FetchTerminalEvents();
745 const size_t executed_task = internal_->task_runner.
ExecutedTasks();
748 if (executed_task == internal_->task_runner.
ExecutedTasks()) {
752 ExecuteSignalHandlers();
755 if (selection_data_previous_ != selection_data_) {
756 selection_data_previous_ = selection_data_;
757 if (selection_on_change_) {
758 selection_on_change_();
769 using T = std::decay_t<
decltype(arg)>;
773 if constexpr (std::is_same_v<T, Event>) {
775 if (arg.is_cursor_position()) {
776 cursor_x_ = arg.cursor_x();
777 cursor_y_ = arg.cursor_y();
781 if (arg.is_cursor_shape()) {
782 cursor_reset_shape_= arg.cursor_shape();
786 if (arg.is_mouse()) {
787 arg.mouse().x -= cursor_x_;
788 arg.mouse().y -= cursor_y_;
793 bool handled = component->OnEvent(arg);
795 handled = HandleSelection(handled, arg);
797 if (arg ==
Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
798 RecordSignal(SIGABRT);
802 if (arg ==
Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
803 RecordSignal(SIGTSTP);
807 frame_valid_ =
false;
812 if constexpr (std::is_same_v<T, Closure>) {
818 if constexpr (std::is_same_v<T, AnimationTask>) {
819 if (!animation_requested_) {
823 animation_requested_ =
false;
826 previous_animation_time_ = now;
828 animation::Params params(delta);
829 component->OnAnimation(params);
830 frame_valid_ =
false;
839bool App::HandleSelection(
bool handled, Event event) {
841 selection_pending_ =
nullptr;
842 selection_data_.empty =
true;
843 selection_ =
nullptr;
847 if (!event.is_mouse()) {
851 auto& mouse =
event.mouse();
858 selection_data_.start_x = mouse.x;
859 selection_data_.start_y = mouse.y;
860 selection_data_.end_x = mouse.x;
861 selection_data_.end_y = mouse.y;
865 if (!selection_pending_) {
870 if ((mouse.x != selection_data_.end_x) ||
871 (mouse.y != selection_data_.end_y)) {
872 selection_data_.end_x = mouse.x;
873 selection_data_.end_y = mouse.y;
874 selection_data_.empty =
false;
881 selection_pending_ =
nullptr;
882 selection_data_.end_x = mouse.x;
883 selection_data_.end_y = mouse.y;
884 selection_data_.empty =
false;
897 auto document = component->Render();
901 document->ComputeRequirement();
902 switch (dimension_) {
903 case Dimension::Fixed:
907 case Dimension::TerminalOutput:
908 dimx = terminal.dimx;
911 case Dimension::Fullscreen:
912 dimx = terminal.dimx;
913 dimy = terminal.dimy;
915 case Dimension::FitComponent:
922 TerminalSend(
"\033[?25l");
925 TerminalSend(ResetCursorPosition());
927 if (frame_count_ != 0) {
934 if ((
dimx <
dimx_) && !use_alternative_screen_) {
935 TerminalSend(
"\033[J");
936 TerminalSend(
"\033[H");
944 cells_ = std::vector<std::vector<Cell>>(
dimy, std::vector<Cell>(
dimx));
952#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
961 if (!use_alternative_screen_ && (i % 150 == 0)) {
962 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
967 if (!use_alternative_screen_ &&
968 (previous_frame_resized_ || i % 40 == 0)) {
969 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
972 previous_frame_resized_ = resized;
974 selection_ = selection_data_.empty
975 ? std::make_unique<Selection>()
976 : std::make_unique<Selection>(
977 selection_data_.start_x, selection_data_.start_y,
978 selection_data_.end_x, selection_data_.end_y);
979 Render(*
this, document.get(), *selection_);
986 set_cursor_position_.clear();
987 reset_cursor_position_.clear();
990 set_cursor_position_ +=
"\x1B[" + std::to_string(dy) +
"A";
991 reset_cursor_position_ +=
"\x1B[" + std::to_string(dy) +
"B";
995 set_cursor_position_ +=
"\x1B[" + std::to_string(dx) +
"D";
996 reset_cursor_position_ +=
"\x1B[" + std::to_string(dx) +
"C";
1000 set_cursor_position_ +=
"\033[?25h";
1001 set_cursor_position_ +=
1006 ToString(internal_->output_buffer);
1007 TerminalSend(set_cursor_position_);
1011 frame_valid_ =
true;
1016std::string App::ResetCursorPosition() {
1017 std::string result = std::move(reset_cursor_position_);
1018 reset_cursor_position_=
"";
1023void App::TerminalSend(std::string_view s) {
1024 internal_->output_buffer += s;
1028void App::TerminalFlush() {
1030 internal_->output_buffer +=
'\0';
1031 std::cout << internal_->output_buffer << std::flush;
1032 internal_->output_buffer.clear();
1037 return [
this] {
Exit(); };
1042 Post([
this] { ExitNow(); });
1046void App::ExitNow() {
1051void App::Signal(
int signal) {
1052 if (signal == SIGABRT) {
1059 if (signal == SIGTSTP) {
1061 TerminalSend(ResetCursorPosition());
1066 std::raise(SIGTSTP);
1072 if (signal == SIGWINCH) {
1079void App::FetchTerminalEvents() {
1081 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1083 auto console = GetStdHandle(STD_INPUT_HANDLE);
1084 DWORD number_of_events = 0;
1085 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1086 return std::vector<INPUT_RECORD>();
1088 if (number_of_events <= 0) {
1090 return std::vector<INPUT_RECORD>();
1093 std::vector<INPUT_RECORD> records(number_of_events);
1094 DWORD number_of_events_read = 0;
1095 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1096 &number_of_events_read)) {
1097 return std::vector<INPUT_RECORD>();
1099 records.resize(number_of_events_read);
1103 auto records = get_input_records();
1104 if (records.size() == 0) {
1105 const auto timeout =
1106 std::chrono::steady_clock::now() - internal_->last_char_time;
1107 const size_t timeout_microseconds =
1108 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1109 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1112 internal_->last_char_time = std::chrono::steady_clock::now();
1117 std::wstring wstring;
1118 for (
const auto& r : records) {
1119 switch (r.EventType) {
1121 auto key_event = r.Event.KeyEvent;
1123 if (key_event.bKeyDown == FALSE) {
1126 const wchar_t wc = key_event.uChar.UnicodeChar;
1128 if (wc >= 0xd800 && wc <= 0xdbff) {
1133 internal_->terminal_input_parser.
Add(it);
1137 case WINDOW_BUFFER_SIZE_EVENT:
1147#elif defined(__EMSCRIPTEN__)
1150 std::array<char, 128> out{};
1151 size_t l = read(STDIN_FILENO, out.data(), out.size());
1153 const auto timeout =
1154 std::chrono::steady_clock::now() - internal_->last_char_time;
1155 const size_t timeout_microseconds =
1156 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1157 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1160 internal_->last_char_time = std::chrono::steady_clock::now();
1163 for (
size_t i = 0; i < l; ++i) {
1164 internal_->terminal_input_parser.
Add(out[i]);
1167 if (!CheckStdinReady(tty_fd_)) {
1168 const auto timeout =
1169 std::chrono::steady_clock::now() - internal_->last_char_time;
1170 const size_t timeout_ms =
1171 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1172 internal_->terminal_input_parser.
Timeout(timeout_ms);
1175 internal_->last_char_time = std::chrono::steady_clock::now();
1178 std::array<char, 128> out{};
1179 size_t l = read(tty_fd_, out.data(), out.size());
1182 for (
size_t i = 0; i < l; ++i) {
1183 internal_->terminal_input_parser.
Add(out[i]);
1188void App::PostAnimationTask() {
1189 Post(AnimationTask());
1193 internal_->task_runner.
PostDelayedTask([
this] { PostAnimationTask(); },
1194 std::chrono::milliseconds(15));
1197bool App::SelectionData::operator==(
const App::SelectionData& other)
const {
1198 if (empty && other.empty) {
1201 if (empty || other.empty) {
1204 return start_x == other.start_x && start_y == other.start_y &&
1205 end_x == other.end_x && end_y == other.end_y;
1208bool App::SelectionData::operator!=(
const App::SelectionData& other)
const {
1209 return !(*
this == other);
static void Signal(App &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
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.
void PostEvent(Event event)
Add an event to the main loop. It will be executed later, after every other scheduled events.
static App FitComponent()
static App * Active()
Return the currently active screen, or null if none.
void Post(Task task)
Add a task to the main loop. It will be executed later, after every other scheduled tasks.
static Event Special(std::string_view)
An custom event whose meaning is defined by the user of the library.
static App FullscreenPrimaryScreen()
static const Event Custom
static App TerminalOutput()
static App FullscreenAlternateScreen()
CapturedMouse CaptureMouse()
Try to get the unique lock about behing able to capture the mouse.
static App FixedSize(int dimx, int dimy)
std::string GetSelection()
Returns the content of the current selection.
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...
App is a Screen that can handle events, run a main loop, and manage components.
Loop is a class that manages the event loop for a component.
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 cells from the screen.
std::vector< std::vector< Cell > > cells_
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