FTXUI 6.1.9
C++ functional terminal UI.
载入中...
搜索中...
未找到
screen_interactive.cpp
浏览该文件的文档.
1// 版权所有 2020 Arthur Sonzogni. 保留所有权利。
2// 本源代码的使用受 MIT 许可证的约束,该许可证可在
3// LICENSE 文件中找到。
5#include <algorithm> // for copy, max, min
6#include <array> // for array
7#include <atomic>
8#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
9#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
10#include <cstdint>
11#include <cstdio> // for fileno, stdin
12#include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
13#include <ftxui/screen/screen.hpp> // for Pixel, Screen::Cursor, Screen, Screen::Cursor::Hidden
14#include <functional> // for function
15#include <initializer_list> // for initializer_list
16#include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
17#include <memory>
18#include <stack> // for stack
19#include <string>
20#include <thread> // for thread, sleep_for
21#include <tuple> // for _Swallow_assign, ignore
22#include <utility> // for move, swap
23#include <variant> // for visit, variant
24#include <vector> // for vector
25#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
26#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
27#include "ftxui/component/component_base.hpp" // for ComponentBase
28#include "ftxui/component/event.hpp" // for Event
29#include "ftxui/component/loop.hpp" // for Loop
31#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
32#include "ftxui/dom/node.hpp" // for Node, Render
33#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
34#include "ftxui/screen/util.hpp" // for util::clamp
35#include "ftxui/util/autoreset.hpp" // for AutoReset
36
37#if defined(_WIN32)
38#define DEFINE_CONSOLEV2_PROPERTIES
39#define WIN32_LEAN_AND_MEAN
40#ifndef NOMINMAX
41#define NOMINMAX
42#endif
43#include <windows.h>
44#ifndef UNICODE
45#error Must be compiled in UNICODE mode
46#endif
47#else
48#include <fcntl.h>
49#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
50#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
51#include <unistd.h> // for STDIN_FILENO, read
52#include <cerrno>
53#endif
54
55// Quick exit is missing in standard CLang headers
56#if defined(__clang__) && defined(__APPLE__)
57#define quick_exit(a) exit(a)
58#endif
59
60namespace ftxui {
61
62struct ScreenInteractive::Internal {
63 // Convert char to Event.
64 TerminalInputParser terminal_input_parser;
65
66 task::TaskRunner task_runner;
67
68 // The last time a character was received.
69 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
70 std::chrono::steady_clock::now();
71
72 explicit Internal(std::function<void(Event)> out)
73 : terminal_input_parser(std::move(out)) {}
74};
75
76namespace animation {
78 auto* screen = ScreenInteractive::Active();
79 if (screen) {
80 screen->RequestAnimationFrame();
81 }
82}
83} // namespace animation
84
85namespace {
86
87ScreenInteractive* g_active_screen = nullptr; // NOLINT
88
89void Flush() {
90 // Emscripten doesn't implement flush. We interpret zero as flush.
91 std::cout << '\0' << std::flush;
92}
93
94constexpr int timeout_milliseconds = 20;
95[[maybe_unused]] constexpr int timeout_microseconds =
96 timeout_milliseconds * 1000;
97#if defined(_WIN32)
98
99#elif defined(__EMSCRIPTEN__)
100#include <emscripten.h>
101
102extern "C" {
103EMSCRIPTEN_KEEPALIVE
104void ftxui_on_resize(int columns, int rows) {
106 columns,
107 rows,
108 });
109 std::raise(SIGWINCH);
110}
111}
112
113#else // POSIX (Linux & Mac)
114
115int CheckStdinReady(int fd) {
116 timeval tv = {0, 0}; // NOLINT
117 fd_set fds;
118 FD_ZERO(&fds); // NOLINT
119 FD_SET(fd, &fds); // NOLINT
120 select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT
121 return FD_ISSET(fd, &fds); // NOLINT
122}
123
124#endif
125
126std::stack<Closure> on_exit_functions; // NOLINT
127void OnExit() {
128 while (!on_exit_functions.empty()) {
129 on_exit_functions.top()();
130 on_exit_functions.pop();
131 }
132}
133
134std::atomic<int> g_signal_exit_count = 0; // NOLINT
135#if !defined(_WIN32)
136std::atomic<int> g_signal_stop_count = 0; // NOLINT
137std::atomic<int> g_signal_resize_count = 0; // NOLINT
138#endif
139
140// Async signal safe function
141void RecordSignal(int signal) {
142 switch (signal) {
143 case SIGABRT:
144 case SIGFPE:
145 case SIGILL:
146 case SIGINT:
147 case SIGSEGV:
148 case SIGTERM:
149 g_signal_exit_count++;
150 break;
151
152#if !defined(_WIN32)
153 case SIGTSTP: // NOLINT
154 g_signal_stop_count++;
155 break;
156
157 case SIGWINCH: // NOLINT
158 g_signal_resize_count++;
159 break;
160#endif
161
162 default:
163 break;
164 }
165}
166
167void ExecuteSignalHandlers() {
168 int signal_exit_count = g_signal_exit_count.exchange(0);
169 while (signal_exit_count--) {
170 ScreenInteractive::Private::Signal(*g_active_screen, SIGABRT);
171 }
172
173#if !defined(_WIN32)
174 int signal_stop_count = g_signal_stop_count.exchange(0);
175 while (signal_stop_count--) {
176 ScreenInteractive::Private::Signal(*g_active_screen, SIGTSTP);
177 }
178
179 int signal_resize_count = g_signal_resize_count.exchange(0);
180 while (signal_resize_count--) {
181 ScreenInteractive::Private::Signal(*g_active_screen, SIGWINCH);
182 }
183#endif
184}
185
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); });
190}
191
192// CSI: Control Sequence Introducer
193const std::string CSI = "\x1b["; // NOLINT
194 //
195// DCS: Device Control String
196const std::string DCS = "\x1bP"; // NOLINT
197// ST: String Terminator
198const std::string ST = "\x1b\\"; // NOLINT
199
200// DECRQSS: Request Status String
201// DECSCUSR: Set Cursor Style
202const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
203
204// DEC: Digital Equipment Corporation
205enum class DECMode : std::uint16_t {
206 kLineWrap = 7,
207 kCursor = 25,
208
209 kMouseX10 = 9,
210 kMouseVt200 = 1000,
211 kMouseVt200Highlight = 1001,
212
213 kMouseBtnEventMouse = 1002,
214 kMouseAnyEvent = 1003,
215
216 kMouseUtf8 = 1005,
217 kMouseSgrExtMode = 1006,
218 kMouseUrxvtMode = 1015,
219 kMouseSgrPixelsMode = 1016,
220 kAlternateScreen = 1049,
221};
222
223// Device Status Report (DSR) {
224enum class DSRMode : std::uint8_t {
225 kCursor = 6,
226};
227
228std::string Serialize(const std::vector<DECMode>& parameters) {
229 bool first = true;
230 std::string out;
231 for (const DECMode parameter : parameters) {
232 if (!first) {
233 out += ";";
234 }
235 out += std::to_string(int(parameter));
236 first = false;
237 }
238 return out;
239}
240
241// DEC Private Mode Set (DECSET)
242std::string Set(const std::vector<DECMode>& parameters) {
243 return CSI + "?" + Serialize(parameters) + "h";
244}
245
246// DEC Private Mode Reset (DECRST)
247std::string Reset(const std::vector<DECMode>& parameters) {
248 return CSI + "?" + Serialize(parameters) + "l";
249}
250
251// Device Status Report (DSR)
252std::string DeviceStatusReport(DSRMode ps) {
253 return CSI + std::to_string(int(ps)) + "n";
254}
255
256class CapturedMouseImpl : public CapturedMouseInterface {
257 public:
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;
265
266 private:
267 std::function<void(void)> callback_;
268};
269
270} // namespace
271
272ScreenInteractive::ScreenInteractive(Dimension dimension,
273 int dimx,
274 int dimy,
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)); });
281}
282
283// static
285 return {
286 Dimension::Fixed,
287 dimx,
288 dimy,
289 /*use_alternative_screen=*/false,
290 };
291}
292
293/// 创建一个占据整个终端大小的 ScreenInteractive。这使用备用屏幕缓冲区,
294/// 以避免弄乱终端内容。
295/// @note 这与 `ScreenInteractive::FullscreenAlternateScreen()` 相同
296// static
300
301/// 创建一个占据整个终端大小的 ScreenInteractive。正在使用主屏幕缓冲区。
302/// 这意味着如果终端被调整大小,之前的内容可能会与终端内容混淆。
303// static
305 auto terminal = Terminal::Size();
306 return {
307 Dimension::Fullscreen,
308 terminal.dimx,
309 terminal.dimy,
310 /*use_alternative_screen=*/false,
311 };
312}
313
314/// 创建一个占据整个终端大小的 ScreenInteractive。这使用备用屏幕缓冲区,
315/// 以避免弄乱终端内容。
316// static
318 auto terminal = Terminal::Size();
319 return {
320 Dimension::Fullscreen,
321 terminal.dimx,
322 terminal.dimy,
323 /*use_alternative_screen=*/true,
324 };
325}
326
327/// 创建一个 ScreenInteractive,其宽度与终端输出宽度匹配,
328/// 高度与正在绘制的组件匹配。
329// static
331 auto terminal = Terminal::Size();
332 return {
333 Dimension::TerminalOutput,
334 terminal.dimx,
335 terminal.dimy, // Best guess.
336 /*use_alternative_screen=*/false,
337 };
338}
339
341
342/// 创建一个 ScreenInteractive,其宽度和高度与正在绘制的组件匹配。
343// static
345 auto terminal = Terminal::Size();
346 return {
347 Dimension::FitComponent,
348 terminal.dimx, // Best guess.
349 terminal.dimy, // Best guess.
350 false,
351 };
352}
353
354/// @brief 设置是否跟踪鼠标并报告事件。
355/// 在主循环之外调用。例如 `ScreenInteractive::Loop(...)`。
356/// @param enable 是否启用鼠标事件跟踪。
357/// @note 这必须在主循环之外调用。例如在调用 `ScreenInteractive::Loop` 之前。
358/// @note 鼠标跟踪默认启用。
359/// @note 鼠标跟踪仅在支持的终端上受支持。
360///
361/// ### 示例
362///
363/// ```cpp
364/// auto screen = ScreenInteractive::TerminalOutput();
365/// screen.TrackMouse(false);
366/// screen.Loop(component);
367/// ```
369 track_mouse_ = enable;
370}
371
372/// @brief 启用或禁用自动管道输入处理。
373/// 启用后,FTXUI 将检测管道输入并将 stdin 从 /dev/tty 重定向
374/// 以进行键盘输入,从而允许应用程序读取管道数据,同时仍
375/// 接收交互式键盘事件。
376/// @param enable 是否启用管道输入处理。默认为 true。
377/// @note 这必须在 Loop() 之前调用。
378/// @note 此功能默认启用。
379/// @note 此功能仅在 POSIX 系统(Linux/macOS)上可用。
381 handle_piped_input_ = enable;
382}
383
384/// @brief 向主循环添加一个任务。
385/// 它将在所有其他计划任务之后执行。
387 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
388 HandleTask(component_, task);
389 });
390}
391
392/// @brief 向主循环添加一个事件。
393/// 它将在所有其他计划事件之后执行。
395 Post(event);
396}
397
398/// @brief 添加一个任务以再次绘制屏幕,直到所有动画完成。
400 if (animation_requested_) {
401 return;
402 }
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;
408 }
409}
410
411/// @brief 尝试获取能够捕获鼠标的唯一锁。
412/// @return 如果鼠标尚未被捕获,则返回一个唯一锁,否则返回空。
414 if (mouse_captured) {
415 return nullptr;
416 }
417 mouse_captured = true;
418 return std::make_unique<CapturedMouseImpl>(
419 [this] { mouse_captured = false; });
420}
421
422/// @brief 执行主循环。
423/// @param component 要绘制的组件。
424void ScreenInteractive::Loop(Component component) { // NOLINT
425 class Loop loop(this, std::move(component));
426 loop.Run();
427}
428
429/// @brief 返回主循环是否已退出。
430bool ScreenInteractive::HasQuitted() {
431 return quit_;
432}
433
434// private
435void ScreenInteractive::PreMain() {
436 // Suspend previously active screen:
437 if (g_active_screen) {
438 std::swap(suspended_screen_, g_active_screen);
439 // Reset cursor position to the top of the screen and clear the screen.
440 suspended_screen_->ResetCursorPosition();
441 std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
442 suspended_screen_->dimx_ = 0;
443 suspended_screen_->dimy_ = 0;
444
445 // Reset dimensions to force drawing the screen again next time:
446 suspended_screen_->Uninstall();
447 }
448
449 // This screen is now active:
450 g_active_screen = this;
451 g_active_screen->Install();
452
453 previous_animation_time_ = animation::Clock::now();
454}
455
456// private
457void ScreenInteractive::PostMain() {
458 // Put cursor position at the end of the drawing.
459 ResetCursorPosition();
460
461 g_active_screen = nullptr;
462
463 // Restore suspended screen.
464 if (suspended_screen_) {
465 // Clear screen, and put the cursor at the beginning of the drawing.
466 std::cout << ResetPosition(/*clear=*/true);
467 dimx_ = 0;
468 dimy_ = 0;
469 Uninstall();
470 std::swap(g_active_screen, suspended_screen_);
471 g_active_screen->Install();
472 } else {
473 Uninstall();
474
475 std::cout << '\r';
476 // On final exit, keep the current drawing and reset cursor position one
477 // line after it.
478 if (!use_alternative_screen_) {
479 std::cout << '\n';
480 std::cout << std::flush;
481 }
482 }
483}
484
485/// @brief 装饰一个函数。它以相同的方式执行,但在执行期间,
486/// 当前活动屏幕终端的钩子会被暂时卸载。
487/// @param fn 要装饰的函数。
489 return [this, fn] {
490 Uninstall();
491 fn();
492 Install();
493 };
494}
495
496/// @brief 强制 FTXUI 处理或不处理 Ctrl-C,即使组件
497/// 捕获了 Event::CtrlC。
499 force_handle_ctrl_c_ = force;
500}
501
502/// @brief 强制 FTXUI 处理或不处理 Ctrl-Z,即使组件
503/// 捕获了 Event::CtrlZ。
505 force_handle_ctrl_z_ = force;
506}
507
508/// @brief 返回当前选择的内容
510 if (!selection_) {
511 return "";
512 }
513 return selection_->GetParts();
514}
515
516void ScreenInteractive::SelectionChange(std::function<void()> callback) {
517 selection_on_change_ = std::move(callback);
518}
519
520/// @brief 返回当前活动屏幕,如果没有则返回空。
521// static
523 return g_active_screen;
524}
525
526// private
527void ScreenInteractive::Install() {
528 frame_valid_ = false;
529
530 // Flush the buffer for stdout to ensure whatever the user has printed before
531 // is fully applied before we start modifying the terminal configuration. This
532 // is important, because we are using two different channels (stdout vs
533 // termios/WinAPI) to communicate with the terminal emulator below. See
534 // https://github.com/ArthurSonzogni/FTXUI/issues/846
535 Flush();
536
537 InstallPipedInputHandling();
538
539 // After uninstalling the new configuration, flush it to the terminal to
540 // ensure it is fully applied:
541 on_exit_functions.emplace([] { Flush(); });
542
543 on_exit_functions.emplace([this] { ExitLoopClosure()(); });
544
545 // Request the terminal to report the current cursor shape. We will restore it
546 // on exit.
547 std::cout << DECRQSS_DECSCUSR;
548 on_exit_functions.emplace([this] {
549 std::cout << "\033[?25h"; // Enable cursor.
550 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
551 });
552
553 // Install signal handlers to restore the terminal state on exit. The default
554 // signal handlers are restored on exit.
555 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
556 InstallSignalHandler(signal);
557 }
558
559// Save the old terminal configuration and restore it on exit.
560#if defined(_WIN32)
561 // Enable VT processing on stdout and stdin
562 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
563 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
564
565 DWORD out_mode = 0;
566 DWORD in_mode = 0;
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); });
571
572 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
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;
577
578 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
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;
587
588 SetConsoleMode(stdin_handle, in_mode);
589 SetConsoleMode(stdout_handle, out_mode);
590#else // POSIX (Linux & Mac)
591 // #if defined(__EMSCRIPTEN__)
592 //// Reading stdin isn't blocking.
593 // int flags = fcntl(0, F_GETFL, 0);
594 // fcntl(0, F_SETFL, flags | O_NONBLOCK);
595
596 //// Restore the terminal configuration on exit.
597 // on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
598 // #endif
599 for (const int signal : {SIGWINCH, SIGTSTP}) {
600 InstallSignalHandler(signal);
601 }
602
603 struct termios terminal; // NOLINT
604 tcgetattr(tty_fd_, &terminal);
605 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
606 tcsetattr(tty_fd_, TCSANOW, &terminal);
607 });
608
609 // Enabling raw terminal input mode
610 terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition
611 terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be
612 // flushed
613 terminal.c_iflag &= ~PARMRK; // Disable marking parity errors.
614 terminal.c_iflag &= ~ISTRIP; // Disable striping 8th bit off characters.
615 terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR.
616 terminal.c_iflag &= ~IGNCR; // Disable ignoring CR.
617 terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL.
618 terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output
619
620 terminal.c_lflag &= ~ECHO; // Disable echoing input characters.
621 terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters.
622 terminal.c_lflag &= ~ICANON; // Disable Canonical mode.
623 terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting:
624 // - => DSUSP
625 // - C-Z => SUSP
626 // - C-C => INTR
627 // - C-d => QUIT
628 terminal.c_lflag &= ~IEXTEN; // Disable extended input processing
629 terminal.c_cflag |= CS8; // 8 bits per byte
630
631 terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical
632 // read.
633 terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read.
634
635 tcsetattr(tty_fd_, TCSANOW, &terminal);
636
637#endif
638
639 auto enable = [&](const std::vector<DECMode>& parameters) {
640 std::cout << Set(parameters);
641 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
642 };
643
644 auto disable = [&](const std::vector<DECMode>& parameters) {
645 std::cout << Reset(parameters);
646 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
647 };
648
649 if (use_alternative_screen_) {
650 enable({
651 DECMode::kAlternateScreen,
652 });
653 }
654
655 disable({
656 // DECMode::kCursor,
657 DECMode::kLineWrap,
658 });
659
660 if (track_mouse_) {
661 enable({DECMode::kMouseVt200});
662 enable({DECMode::kMouseAnyEvent});
663 enable({DECMode::kMouseUrxvtMode});
664 enable({DECMode::kMouseSgrExtMode});
665 }
666
667 // After installing the new configuration, flush it to the terminal to
668 // ensure it is fully applied:
669 Flush();
670
671 quit_ = false;
672
673 PostAnimationTask();
674}
675
676void ScreenInteractive::InstallPipedInputHandling() {
677#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
678 tty_fd_ = STDIN_FILENO;
679 // Handle piped input redirection if explicitly enabled by the application.
680 // This allows applications to read data from stdin while still receiving
681 // keyboard input from the terminal for interactive use.
682 if (!handle_piped_input_) {
683 return;
684 }
685
686 // If stdin is a terminal, we don't need to open /dev/tty.
687 if (isatty(STDIN_FILENO)) {
688 return;
689 }
690
691 // Open /dev/tty for keyboard input.
692 tty_fd_ = open("/dev/tty", O_RDONLY);
693 if (tty_fd_ < 0) {
694 // Failed to open /dev/tty (containers, headless systems, etc.)
695 tty_fd_ = STDIN_FILENO; // Fallback to stdin.
696 return;
697 }
698
699 // Close the /dev/tty file descriptor on exit.
700 on_exit_functions.emplace([this] {
701 close(tty_fd_);
702 tty_fd_ = -1;
703 });
704#endif
705}
706
707// private
708void ScreenInteractive::Uninstall() {
709 ExitNow();
710 OnExit();
711}
712
713// private
714// NOLINTNEXTLINE
715void ScreenInteractive::RunOnceBlocking(Component component) {
716 // Set FPS to 60 at most.
717 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
718
719 auto time = std::chrono::steady_clock::now();
720 size_t executed_task = internal_->task_runner.ExecutedTasks();
721
722 // Wait for at least one task to execute.
723 while (executed_task == internal_->task_runner.ExecutedTasks() &&
724 !HasQuitted()) {
725 RunOnce(component);
726
727 const auto now = std::chrono::steady_clock::now();
728 const auto delta = now - time;
729 time = now;
730
731 if (delta < time_per_frame) {
732 const auto sleep_duration = time_per_frame - delta;
733 std::this_thread::sleep_for(sleep_duration);
734 }
735 }
736}
737
738// private
739void ScreenInteractive::RunOnce(Component component) {
740 AutoReset set_component(&component_, component);
741 ExecuteSignalHandlers();
742 FetchTerminalEvents();
743
744 // Execute the pending tasks from the queue.
745 const size_t executed_task = internal_->task_runner.ExecutedTasks();
746 internal_->task_runner.RunUntilIdle();
747 // If no executed task, we can return early without redrawing the screen.
748 if (executed_task == internal_->task_runner.ExecutedTasks()) {
749 return;
750 }
751
752 ExecuteSignalHandlers();
753 Draw(component);
754
755 if (selection_data_previous_ != selection_data_) {
756 selection_data_previous_ = selection_data_;
757 if (selection_on_change_) {
758 selection_on_change_();
760 }
761 }
762}
763
764// private
765// NOLINTNEXTLINE
766void ScreenInteractive::HandleTask(Component component, Task& task) {
767 std::visit(
768 [&](auto&& arg) {
769 using T = std::decay_t<decltype(arg)>;
770
771 // clang-format off
772 // Handle Event.
773 if constexpr (std::is_same_v<T, Event>) {
774
775 if (arg.is_cursor_position()) {
776 cursor_x_ = arg.cursor_x();
777 cursor_y_ = arg.cursor_y();
778 return;
779 }
780
781 if (arg.is_cursor_shape()) {
782 cursor_reset_shape_= arg.cursor_shape();
783 return;
784 }
785
786 if (arg.is_mouse()) {
787 arg.mouse().x -= cursor_x_;
788 arg.mouse().y -= cursor_y_;
789 }
790
791 arg.screen_ = this;
792
793 bool handled = component->OnEvent(arg);
794
795 handled = HandleSelection(handled, arg);
796
797 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
798 RecordSignal(SIGABRT);
799 }
800
801#if !defined(_WIN32)
802 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
803 RecordSignal(SIGTSTP);
804 }
805#endif
806
807 frame_valid_ = false;
808 return;
809 }
810
811 // Handle callback
812 if constexpr (std::is_same_v<T, Closure>) {
813 arg();
814 return;
815 }
816
817 // Handle Animation
818 if constexpr (std::is_same_v<T, AnimationTask>) {
819 if (!animation_requested_) {
820 return;
821 }
822
823 animation_requested_ = false;
824 const animation::TimePoint now = animation::Clock::now();
825 const animation::Duration delta = now - previous_animation_time_;
826 previous_animation_time_ = now;
827
828 animation::Params params(delta);
829 component->OnAnimation(params);
830 frame_valid_ = false;
831 return;
832 }
833 },
834 task);
835 // clang-format on
836}
837
838// private
839bool ScreenInteractive::HandleSelection(bool handled, Event event) {
840 if (handled) {
841 selection_pending_ = nullptr;
842 selection_data_.empty = true;
843 selection_ = nullptr;
844 return true;
845 }
846
847 if (!event.is_mouse()) {
848 return false;
849 }
850
851 auto& mouse = event.mouse();
852 if (mouse.button != Mouse::Left) {
853 return false;
854 }
855
856 if (mouse.motion == Mouse::Pressed) {
857 selection_pending_ = CaptureMouse();
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;
862 return false;
863 }
864
865 if (!selection_pending_) {
866 return false;
867 }
868
869 if (mouse.motion == Mouse::Moved) {
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;
875 }
876
877 return true;
878 }
879
880 if (mouse.motion == Mouse::Released) {
881 selection_pending_ = nullptr;
882 selection_data_.end_x = mouse.x;
883 selection_data_.end_y = mouse.y;
884 selection_data_.empty = false;
885 return true;
886 }
887
888 return false;
889}
890
891// private
892// NOLINTNEXTLINE
893void ScreenInteractive::Draw(Component component) {
894 if (frame_valid_) {
895 return;
896 }
897 auto document = component->Render();
898 int dimx = 0;
899 int dimy = 0;
900 auto terminal = Terminal::Size();
901 document->ComputeRequirement();
902 switch (dimension_) {
903 case Dimension::Fixed:
904 dimx = dimx_;
905 dimy = dimy_;
906 break;
907 case Dimension::TerminalOutput:
908 dimx = terminal.dimx;
909 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
910 break;
911 case Dimension::Fullscreen:
912 dimx = terminal.dimx;
913 dimy = terminal.dimy;
914 break;
915 case Dimension::FitComponent:
916 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
917 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
918 break;
919 }
920
921 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
922 ResetCursorPosition();
923 std::cout << ResetPosition(/*clear=*/resized);
924
925 // If the terminal width decrease, the terminal emulator will start wrapping
926 // lines and make the display dirty. We should clear it completely.
927 if ((dimx < dimx_) && !use_alternative_screen_) {
928 std::cout << "\033[J"; // clear terminal output
929 std::cout << "\033[H"; // move cursor to home position
930 }
931
932 // Resize the screen if needed.
933 if (resized) {
934 dimx_ = dimx;
935 dimy_ = dimy;
936 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
937 cursor_.x = dimx_ - 1;
938 cursor_.y = dimy_ - 1;
939 }
940
941 // Periodically request the terminal emulator the frame position relative to
942 // the screen. This is useful for converting mouse position reported in
943 // screen's coordinates to frame's coordinates.
944#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
945 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
946 // position, several output sequences are mixed together into garbage.
947 // This causes FTXUI user to see some "1;1;R" sequences into the Input
948 // component. See [issue]. Solution is to request cursor position less
949 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
950 // https://github.com/ArthurSonzogni/FTXUI/issues/136
951 static int i = -3;
952 ++i;
953 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
954 std::cout << DeviceStatusReport(DSRMode::kCursor);
955 }
956#else
957 static int i = -3;
958 ++i;
959 if (!use_alternative_screen_ &&
960 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
961 std::cout << DeviceStatusReport(DSRMode::kCursor);
962 }
963#endif
964 previous_frame_resized_ = resized;
965
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_);
972
973 // Set cursor position for user using tools to insert CJK characters.
974 {
975 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
976 const int dy = dimy_ - 1 - cursor_.y;
977
978 set_cursor_position.clear();
979 reset_cursor_position.clear();
980
981 if (dy != 0) {
982 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
983 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
984 }
985
986 if (dx != 0) {
987 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
988 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
989 }
990
991 if (cursor_.shape == Cursor::Hidden) {
992 set_cursor_position += "\033[?25l";
993 } else {
994 set_cursor_position += "\033[?25h";
995 set_cursor_position +=
996 "\033[" + std::to_string(int(cursor_.shape)) + " q";
997 }
998 }
999
1000 std::cout << ToString() << set_cursor_position;
1001 Flush();
1002 Clear();
1003 frame_valid_ = true;
1004 frame_count_++;
1005}
1006
1007// private
1008void ScreenInteractive::ResetCursorPosition() {
1009 std::cout << reset_cursor_position;
1010 reset_cursor_position = "";
1011}
1012
1013/// @brief 返回一个退出主循环的函数。
1015 return [this] { Exit(); };
1016}
1017
1018/// @brief 退出主循环。
1020 Post([this] { ExitNow(); });
1021}
1022
1023// private:
1024void ScreenInteractive::ExitNow() {
1025 quit_ = true;
1026}
1027
1028// private:
1029void ScreenInteractive::Signal(int signal) {
1030 if (signal == SIGABRT) {
1031 Exit();
1032 return;
1033 }
1034
1035// Windows do no support SIGTSTP / SIGWINCH
1036#if !defined(_WIN32)
1037 if (signal == SIGTSTP) {
1038 Post([&] {
1039 ResetCursorPosition();
1040 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
1041 Uninstall();
1042 dimx_ = 0;
1043 dimy_ = 0;
1044 Flush();
1045 std::ignore = std::raise(SIGTSTP);
1046 Install();
1047 });
1048 return;
1049 }
1050
1051 if (signal == SIGWINCH) {
1052 Post(Event::Special({0}));
1053 return;
1054 }
1055#endif
1056}
1057
1058void ScreenInteractive::FetchTerminalEvents() {
1059#if defined(_WIN32)
1060 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1061 // Check if there is input in the console.
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>();
1066 }
1067 if (number_of_events <= 0) {
1068 // No input, return.
1069 return std::vector<INPUT_RECORD>();
1070 }
1071 // Read the input events.
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>();
1077 }
1078 records.resize(number_of_events_read);
1079 return records;
1080 };
1081
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);
1089 return;
1090 }
1091 internal_->last_char_time = std::chrono::steady_clock::now();
1092
1093 // Convert the input events to FTXUI events.
1094 // For each event, we call the terminal input parser to convert it to
1095 // Event.
1096 for (const auto& r : records) {
1097 switch (r.EventType) {
1098 case KEY_EVENT: {
1099 auto key_event = r.Event.KeyEvent;
1100 // ignore UP key events
1101 if (key_event.bKeyDown == FALSE) {
1102 continue;
1103 }
1104 std::wstring wstring;
1105 wstring += key_event.uChar.UnicodeChar;
1106 for (auto it : to_string(wstring)) {
1107 internal_->terminal_input_parser.Add(it);
1108 }
1109 } break;
1110 case WINDOW_BUFFER_SIZE_EVENT:
1111 Post(Event::Special({0}));
1112 break;
1113 case MENU_EVENT:
1114 case FOCUS_EVENT:
1115 case MOUSE_EVENT:
1116 // TODO(mauve): Implement later.
1117 break;
1118 }
1119 }
1120#elif defined(__EMSCRIPTEN__)
1121 // Read chars from the terminal.
1122 // We configured it to be non blocking.
1123 std::array<char, 128> out{};
1124 size_t l = read(STDIN_FILENO, out.data(), out.size());
1125 if (l == 0) {
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);
1131 return;
1132 }
1133 internal_->last_char_time = std::chrono::steady_clock::now();
1134
1135 // Convert the chars to events.
1136 for (size_t i = 0; i < l; ++i) {
1137 internal_->terminal_input_parser.Add(out[i]);
1138 }
1139#else // POSIX (Linux & Mac)
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);
1146 return;
1147 }
1148 internal_->last_char_time = std::chrono::steady_clock::now();
1149
1150 // Read chars from the terminal.
1151 std::array<char, 128> out{};
1152 size_t l = read(tty_fd_, out.data(), out.size());
1153
1154 // Convert the chars to events.
1155 for (size_t i = 0; i < l; ++i) {
1156 internal_->terminal_input_parser.Add(out[i]);
1157 }
1158#endif
1159}
1160
1161void ScreenInteractive::PostAnimationTask() {
1162 Post(AnimationTask());
1163
1164 // Repeat the animation task every 15ms. This correspond to a frame rate
1165 // of around 66fps.
1166 internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1167 std::chrono::milliseconds(15));
1168}
1169
1170bool ScreenInteractive::SelectionData::operator==(
1171 const ScreenInteractive::SelectionData& other) const {
1172 if (empty && other.empty) {
1173 return true;
1174 }
1175 if (empty || other.empty) {
1176 return false;
1177 }
1178 return start_x == other.start_x && start_y == other.start_y &&
1179 end_x == other.end_x && end_y == other.end_y;
1180}
1181
1182bool ScreenInteractive::SelectionData::operator!=(
1183 const ScreenInteractive::SelectionData& other) const {
1184 return !(*this == other);
1185}
1186
1187} // namespace ftxui.
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
安排一个任务在指定持续时间后执行。
static const Event CtrlC
static ScreenInteractive TerminalOutput()
void HandlePipedInput(bool enable=true)
启用或禁用自动管道输入处理。 启用后,FTXUI 将检测管道输入并将 stdin 从 /dev/tty 重定向 以进行键盘输入,从而允许应用程序读取管道数据,同时仍 接收交互式键盘事件。
static const Event CtrlZ
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。
void ForceHandleCtrlZ(bool force)
强制 FTXUI 处理或不处理 Ctrl-Z,即使组件 捕获了 Event::CtrlZ。
Closure WithRestoredIO(Closure)
装饰一个函数。它以相同的方式执行,但在执行期间, 当前活动屏幕终端的钩子会被暂时卸载。
static Event Special(std::string)
一个自定义事件,其含义由库用户定义。
Loop 是一个管理组件事件循环的类。
ScreenInteractive 是一个可以处理事件、运行主循环和管理组件的 Screen。
void RequestAnimationFrame()
RequestAnimationFrame 是一个请求在下一个动画周期中绘制新帧的函数。
代表一个事件。它可以是按键事件、终端大小调整等等...
void Render(Screen &screen, const Element &element)
在 ftxui::Screen 上显示元素。
int dimy() const
std::string ToString() const
std::string ResetPosition(bool clear=false) const
返回一个字符串,用于将光标位置重置到屏幕开头。
void Clear()
清除屏幕上的所有像素。
int dimx() const
std::vector< std::vector< Pixel > > pixels_
Dimensions Size()
获取终端尺寸。
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