15#include <initializer_list>
38#define DEFINE_CONSOLEV2_PROPERTIES
39#define WIN32_LEAN_AND_MEAN
45#error Must be compiled in UNICODE mode
49#include <sys/select.h>
56#if defined(__clang__) && defined(__APPLE__)
57#define quick_exit(a) exit(a)
62struct ScreenInteractive::Internal {
64 TerminalInputParser terminal_input_parser;
66 task::TaskRunner task_runner;
69 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
70 std::chrono::steady_clock::now();
72 explicit Internal(std::function<
void(Event)> out)
73 : terminal_input_parser(std::move(out)) {}
80 screen->RequestAnimationFrame();
87ScreenInteractive* g_active_screen =
nullptr;
91 std::cout <<
'\0' << std::flush;
94constexpr int timeout_milliseconds = 20;
95[[maybe_unused]]
constexpr int timeout_microseconds =
96 timeout_milliseconds * 1000;
99#elif defined(__EMSCRIPTEN__)
100#include <emscripten.h>
104void ftxui_on_resize(
int columns,
int rows) {
109 std::raise(SIGWINCH);
115int CheckStdinReady(
int fd) {
120 select(fd + 1, &fds,
nullptr,
nullptr, &tv);
121 return FD_ISSET(fd, &fds);
126std::stack<Closure> on_exit_functions;
128 while (!on_exit_functions.empty()) {
129 on_exit_functions.top()();
130 on_exit_functions.pop();
134std::atomic<int> g_signal_exit_count = 0;
136std::atomic<int> g_signal_stop_count = 0;
137std::atomic<int> g_signal_resize_count = 0;
141void RecordSignal(
int signal) {
149 g_signal_exit_count++;
154 g_signal_stop_count++;
158 g_signal_resize_count++;
167void ExecuteSignalHandlers() {
168 int signal_exit_count = g_signal_exit_count.exchange(0);
169 while (signal_exit_count--) {
174 int signal_stop_count = g_signal_stop_count.exchange(0);
175 while (signal_stop_count--) {
179 int signal_resize_count = g_signal_resize_count.exchange(0);
180 while (signal_resize_count--) {
186void InstallSignalHandler(
int sig) {
187 auto old_signal_handler = std::signal(sig, RecordSignal);
188 on_exit_functions.emplace(
189 [=] { std::ignore = std::signal(sig, old_signal_handler); });
193const std::string CSI =
"\x1b[";
196const std::string DCS =
"\x1bP";
198const std::string ST =
"\x1b\\";
202const std::string DECRQSS_DECSCUSR = DCS +
"$q q" + ST;
205enum class DECMode : std::uint16_t {
211 kMouseVt200Highlight = 1001,
213 kMouseBtnEventMouse = 1002,
214 kMouseAnyEvent = 1003,
217 kMouseSgrExtMode = 1006,
218 kMouseUrxvtMode = 1015,
219 kMouseSgrPixelsMode = 1016,
220 kAlternateScreen = 1049,
224enum class DSRMode : std::uint8_t {
228std::string Serialize(
const std::vector<DECMode>& parameters) {
231 for (
const DECMode parameter : parameters) {
235 out += std::to_string(
int(parameter));
242std::string Set(
const std::vector<DECMode>& parameters) {
243 return CSI +
"?" + Serialize(parameters) +
"h";
247std::string Reset(
const std::vector<DECMode>& parameters) {
248 return CSI +
"?" + Serialize(parameters) +
"l";
252std::string DeviceStatusReport(DSRMode ps) {
253 return CSI + std::to_string(
int(ps)) +
"n";
256class CapturedMouseImpl :
public CapturedMouseInterface {
258 explicit CapturedMouseImpl(std::function<
void(
void)> callback)
259 : callback_(std::move(callback)) {}
260 ~CapturedMouseImpl()
override { callback_(); }
261 CapturedMouseImpl(
const CapturedMouseImpl&) =
delete;
262 CapturedMouseImpl(CapturedMouseImpl&&) =
delete;
263 CapturedMouseImpl& operator=(
const CapturedMouseImpl&) =
delete;
264 CapturedMouseImpl& operator=(CapturedMouseImpl&&) =
delete;
267 std::function<void(
void)> callback_;
272ScreenInteractive::ScreenInteractive(
Dimension dimension,
275 bool use_alternative_screen)
276 : Screen(dimx, dimy),
277 dimension_(dimension),
278 use_alternative_screen_(use_alternative_screen) {
279 internal_ = std::make_unique<Internal>(
280 [&](Event event) { PostEvent(std::move(event)); });
307 Dimension::Fullscreen,
320 Dimension::Fullscreen,
333 Dimension::TerminalOutput,
347 Dimension::FitComponent,
369 track_mouse_ = enable;
381 handle_piped_input_ = enable;
387 internal_->task_runner.
PostTask([
this, task = std::move(task)]()
mutable {
388 HandleTask(component_, task);
400 if (animation_requested_) {
403 animation_requested_ =
true;
404 auto now = animation::Clock::now();
405 const auto time_histeresis = std::chrono::milliseconds(33);
406 if (now - previous_animation_time_ >= time_histeresis) {
407 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(); });
547 std::cout << DECRQSS_DECSCUSR;
548 on_exit_functions.emplace([
this] {
549 std::cout <<
"\033[?25h";
550 std::cout <<
"\033[" + std::to_string(cursor_reset_shape_) +
" q";
555 for (
const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
556 InstallSignalHandler(signal);
562 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
563 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
567 GetConsoleMode(stdout_handle, &out_mode);
568 GetConsoleMode(stdin_handle, &in_mode);
569 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
570 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
573 const int enable_virtual_terminal_processing = 0x0004;
574 const int disable_newline_auto_return = 0x0008;
575 out_mode |= enable_virtual_terminal_processing;
576 out_mode |= disable_newline_auto_return;
579 const int enable_line_input = 0x0002;
580 const int enable_echo_input = 0x0004;
581 const int enable_virtual_terminal_input = 0x0200;
582 const int enable_window_input = 0x0008;
583 in_mode &= ~enable_echo_input;
584 in_mode &= ~enable_line_input;
585 in_mode |= enable_virtual_terminal_input;
586 in_mode |= enable_window_input;
588 SetConsoleMode(stdin_handle, in_mode);
589 SetConsoleMode(stdout_handle, out_mode);
599 for (
const int signal : {SIGWINCH, SIGTSTP}) {
600 InstallSignalHandler(signal);
603 struct termios terminal;
604 tcgetattr(tty_fd_, &terminal);
605 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
606 tcsetattr(tty_fd_, TCSANOW, &terminal);
610 terminal.c_iflag &= ~IGNBRK;
611 terminal.c_iflag &= ~BRKINT;
613 terminal.c_iflag &= ~PARMRK;
614 terminal.c_iflag &= ~ISTRIP;
615 terminal.c_iflag &= ~INLCR;
616 terminal.c_iflag &= ~IGNCR;
617 terminal.c_iflag &= ~ICRNL;
618 terminal.c_iflag &= ~IXON;
620 terminal.c_lflag &= ~ECHO;
621 terminal.c_lflag &= ~ECHONL;
622 terminal.c_lflag &= ~ICANON;
623 terminal.c_lflag &= ~ISIG;
628 terminal.c_lflag &= ~IEXTEN;
629 terminal.c_cflag |= CS8;
631 terminal.c_cc[VMIN] = 0;
633 terminal.c_cc[VTIME] = 0;
635 tcsetattr(tty_fd_, TCSANOW, &terminal);
639 auto enable = [&](
const std::vector<DECMode>& parameters) {
640 std::cout << Set(parameters);
641 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
644 auto disable = [&](
const std::vector<DECMode>& parameters) {
645 std::cout << Reset(parameters);
646 on_exit_functions.emplace([=] { std::cout << 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 ScreenInteractive::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 ScreenInteractive::Uninstall() {
715void ScreenInteractive::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);
739void ScreenInteractive::RunOnce(
Component component) {
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_();
766void ScreenInteractive::HandleTask(
Component component,
Task& task) {
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 ScreenInteractive::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;
893void ScreenInteractive::Draw(
Component component) {
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 ResetCursorPosition();
927 if ((
dimx <
dimx_) && !use_alternative_screen_) {
928 std::cout <<
"\033[J";
929 std::cout <<
"\033[H";
936 pixels_ = std::vector<std::vector<Pixel>>(
dimy, std::vector<Pixel>(
dimx));
944#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
953 if (!use_alternative_screen_ && (i % 150 == 0)) {
954 std::cout << DeviceStatusReport(DSRMode::kCursor);
959 if (!use_alternative_screen_ &&
960 (previous_frame_resized_ || i % 40 == 0)) {
961 std::cout << DeviceStatusReport(DSRMode::kCursor);
964 previous_frame_resized_ = resized;
966 selection_ = selection_data_.empty
967 ? std::make_unique<Selection>()
968 : std::make_unique<Selection>(
969 selection_data_.start_x, selection_data_.start_y,
970 selection_data_.end_x, selection_data_.end_y);
971 Render(*
this, document.get(), *selection_);
978 set_cursor_position.clear();
979 reset_cursor_position.clear();
982 set_cursor_position +=
"\x1B[" + std::to_string(dy) +
"A";
983 reset_cursor_position +=
"\x1B[" + std::to_string(dy) +
"B";
987 set_cursor_position +=
"\x1B[" + std::to_string(dx) +
"D";
988 reset_cursor_position +=
"\x1B[" + std::to_string(dx) +
"C";
992 set_cursor_position +=
"\033[?25l";
994 set_cursor_position +=
"\033[?25h";
995 set_cursor_position +=
1000 std::cout <<
ToString() << set_cursor_position;
1003 frame_valid_ =
true;
1008void ScreenInteractive::ResetCursorPosition() {
1009 std::cout << reset_cursor_position;
1010 reset_cursor_position =
"";
1015 return [
this] {
Exit(); };
1020 Post([
this] { ExitNow(); });
1024void ScreenInteractive::ExitNow() {
1029void ScreenInteractive::Signal(
int signal) {
1030 if (signal == SIGABRT) {
1037 if (signal == SIGTSTP) {
1039 ResetCursorPosition();
1045 std::ignore = std::raise(SIGTSTP);
1051 if (signal == SIGWINCH) {
1058void ScreenInteractive::FetchTerminalEvents() {
1060 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1062 auto console = GetStdHandle(STD_INPUT_HANDLE);
1063 DWORD number_of_events = 0;
1064 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1065 return std::vector<INPUT_RECORD>();
1067 if (number_of_events <= 0) {
1069 return std::vector<INPUT_RECORD>();
1072 std::vector<INPUT_RECORD> records(number_of_events);
1073 DWORD number_of_events_read = 0;
1074 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1075 &number_of_events_read)) {
1076 return std::vector<INPUT_RECORD>();
1078 records.resize(number_of_events_read);
1082 auto records = get_input_records();
1083 if (records.size() == 0) {
1084 const auto timeout =
1085 std::chrono::steady_clock::now() - internal_->last_char_time;
1086 const size_t timeout_microseconds =
1087 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1088 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1091 internal_->last_char_time = std::chrono::steady_clock::now();
1096 for (
const auto& r : records) {
1097 switch (r.EventType) {
1099 auto key_event = r.Event.KeyEvent;
1101 if (key_event.bKeyDown == FALSE) {
1104 std::wstring wstring;
1105 wstring += key_event.uChar.UnicodeChar;
1107 internal_->terminal_input_parser.
Add(it);
1110 case WINDOW_BUFFER_SIZE_EVENT:
1120#elif defined(__EMSCRIPTEN__)
1123 std::array<char, 128> out{};
1124 size_t l = read(STDIN_FILENO, out.data(), out.size());
1126 const auto timeout =
1127 std::chrono::steady_clock::now() - internal_->last_char_time;
1128 const size_t timeout_microseconds =
1129 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1130 internal_->terminal_input_parser.
Timeout(timeout_microseconds);
1133 internal_->last_char_time = std::chrono::steady_clock::now();
1136 for (
size_t i = 0; i < l; ++i) {
1137 internal_->terminal_input_parser.
Add(out[i]);
1140 if (!CheckStdinReady(tty_fd_)) {
1141 const auto timeout =
1142 std::chrono::steady_clock::now() - internal_->last_char_time;
1143 const size_t timeout_ms =
1144 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1145 internal_->terminal_input_parser.
Timeout(timeout_ms);
1148 internal_->last_char_time = std::chrono::steady_clock::now();
1151 std::array<char, 128> out{};
1152 size_t l = read(tty_fd_, out.data(), out.size());
1155 for (
size_t i = 0; i < l; ++i) {
1156 internal_->terminal_input_parser.
Add(out[i]);
1161void ScreenInteractive::PostAnimationTask() {
1162 Post(AnimationTask());
1166 internal_->task_runner.
PostDelayedTask([
this] { PostAnimationTask(); },
1167 std::chrono::milliseconds(15));
1170bool ScreenInteractive::SelectionData::operator==(
1171 const ScreenInteractive::SelectionData& other)
const {
1172 if (empty && other.empty) {
1175 if (empty || other.empty) {
1178 return start_x == other.start_x && start_y == other.start_y &&
1179 end_x == other.end_x && end_y == other.end_y;
1182bool ScreenInteractive::SelectionData::operator!=(
1183 const ScreenInteractive::SelectionData& other)
const {
1184 return !(*
this == other);
static void Signal(ScreenInteractive &s, int signal)
auto PostTask(Task task) -> void
安排一个任务立即执行。
auto RunUntilIdle() -> std::optional< std::chrono::steady_clock::duration >
运行队列中的任务,返回下一个延迟任务可以执行的延迟时间。
auto PostDelayedTask(Task task, std::chrono::steady_clock::duration duration) -> void
安排一个任务在指定持续时间后执行。
size_t ExecutedTasks() const
static ScreenInteractive TerminalOutput()
void HandlePipedInput(bool enable=true)
启用或禁用自动管道输入处理。 启用后,FTXUI 将检测管道输入并将 stdin 从 /dev/tty 重定向 以进行键盘输入,从而允许应用程序读取管道数据,同时仍 接收交互式键盘事件。
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
向主循环添加一个事件。 它将在所有其他计划事件之后执行。
void Post(Task task)
向主循环添加一个任务。 它将在所有其他计划任务之后执行。
static ScreenInteractive FitComponent()
创建一个 ScreenInteractive,其宽度和高度与正在绘制的组件匹配。
static ScreenInteractive Fullscreen()
static const Event Custom
static ScreenInteractive FullscreenPrimaryScreen()
static ScreenInteractive * Active()
返回当前活动屏幕,如果没有则返回空。
CapturedMouse CaptureMouse()
尝试获取能够捕获鼠标的唯一锁。
std::string GetSelection()
返回当前选择的内容
static ScreenInteractive FullscreenAlternateScreen()
void TrackMouse(bool enable=true)
设置是否跟踪鼠标并报告事件。 在主循环之外调用。例如 ScreenInteractive::Loop(...)。
void SelectionChange(std::function< void()> callback)
void RequestAnimationFrame()
添加一个任务以再次绘制屏幕,直到所有动画完成。
Closure ExitLoopClosure()
返回一个退出主循环的函数。
void ForceHandleCtrlC(bool force)
强制 FTXUI 处理或不处理 Ctrl-C,即使组件 捕获了 Event::CtrlC。
~ScreenInteractive() override
void ForceHandleCtrlZ(bool force)
强制 FTXUI 处理或不处理 Ctrl-Z,即使组件 捕获了 Event::CtrlZ。
Closure WithRestoredIO(Closure)
装饰一个函数。它以相同的方式执行,但在执行期间, 当前活动屏幕终端的钩子会被暂时卸载。
static Event Special(std::string)
一个自定义事件,其含义由库用户定义。
ScreenInteractive 是一个可以处理事件、运行主循环和管理组件的 Screen。
void RequestAnimationFrame()
RequestAnimationFrame 是一个请求在下一个动画周期中绘制新帧的函数。
代表一个事件。它可以是按键事件、终端大小调整等等...
void Render(Screen &screen, const Element &element)
在 ftxui::Screen 上显示元素。
std::string ToString() const
std::string ResetPosition(bool clear=false) const
返回一个字符串,用于将光标位置重置到屏幕开头。
std::vector< std::vector< Pixel > > pixels_
FTXUI ftxui::Dimension:: 命名空间
FTXUI ftxui::animation:: 命名空间
void SetFallbackSize(const Dimensions &fallbackSize)
在自动检测失败时覆盖终端尺寸
std::chrono::duration< float > Duration
std::chrono::time_point< Clock > TimePoint
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
#include "ftxui/component/component_base.hpp" // 用于 ComponentBase
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(const std::wstring &s)
将 std::wstring 转换为 UTF8 std::string。
Element select(Element e)
将 child 设置为其同级元素中获得焦点的元素。
std::variant< Event, Closure, AnimationTask > Task
std::function< void()> Closure
std::shared_ptr< ComponentBase > Component