FTXUI 6.1.9
C++ functional terminal UI.
Loading...
Searching...
No Matches
screen_interactive.cpp
Go to the documentation of this file.
1// Copyright 2020 Arthur Sonzogni. All rights reserved.
2// Use of this source code is governed by the MIT license that can be found in
3// the LICENSE file.
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/// Create a ScreenInteractive taking the full terminal size. This is using the
294/// alternate screen buffer to avoid messing with the terminal content.
295/// @note This is the same as `ScreenInteractive::FullscreenAlternateScreen()`
296// static
300
301/// Create a ScreenInteractive taking the full terminal size. The primary screen
302/// buffer is being used. It means if the terminal is resized, the previous
303/// content might mess up with the terminal content.
304// static
306 auto terminal = Terminal::Size();
307 return {
308 Dimension::Fullscreen,
309 terminal.dimx,
310 terminal.dimy,
311 /*use_alternative_screen=*/false,
312 };
313}
314
315/// Create a ScreenInteractive taking the full terminal size. This is using the
316/// alternate screen buffer to avoid messing with the terminal content.
317// static
319 auto terminal = Terminal::Size();
320 return {
321 Dimension::Fullscreen,
322 terminal.dimx,
323 terminal.dimy,
324 /*use_alternative_screen=*/true,
325 };
326}
327
328/// Create a ScreenInteractive whose width match the terminal output width and
329/// the height matches the component being drawn.
330// static
332 auto terminal = Terminal::Size();
333 return {
334 Dimension::TerminalOutput,
335 terminal.dimx,
336 terminal.dimy, // Best guess.
337 /*use_alternative_screen=*/false,
338 };
339}
340
342
343/// Create a ScreenInteractive whose width and height match the component being
344/// drawn.
345// static
347 auto terminal = Terminal::Size();
348 return {
349 Dimension::FitComponent,
350 terminal.dimx, // Best guess.
351 terminal.dimy, // Best guess.
352 false,
353 };
354}
355
356/// @brief Set whether mouse is tracked and events reported.
357/// called outside of the main loop. E.g `ScreenInteractive::Loop(...)`.
358/// @param enable Whether to enable mouse event tracking.
359/// @note This muse be called outside of the main loop. E.g. before calling
360/// `ScreenInteractive::Loop`.
361/// @note Mouse tracking is enabled by default.
362/// @note Mouse tracking is only supported on terminals that supports it.
363///
364/// ### Example
365///
366/// ```cpp
367/// auto screen = ScreenInteractive::TerminalOutput();
368/// screen.TrackMouse(false);
369/// screen.Loop(component);
370/// ```
372 track_mouse_ = enable;
373}
374
375/// @brief Enable or disable automatic piped input handling.
376/// When enabled, FTXUI will detect piped input and redirect stdin from /dev/tty
377/// for keyboard input, allowing applications to read piped data while still
378/// receiving interactive keyboard events.
379/// @param enable Whether to enable piped input handling. Default is true.
380/// @note This must be called before Loop().
381/// @note This feature is enabled by default.
382/// @note This feature is only available on POSIX systems (Linux/macOS).
384 handle_piped_input_ = enable;
385}
386
387/// @brief Add a task to the main loop.
388/// It will be executed later, after every other scheduled tasks.
390 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
391 HandleTask(component_, task);
392 });
393}
394
395/// @brief Add an event to the main loop.
396/// It will be executed later, after every other scheduled events.
398 Post(event);
399}
400
401/// @brief Add a task to draw the screen one more time, until all the animations
402/// are done.
404 if (animation_requested_) {
405 return;
406 }
407 animation_requested_ = true;
408 auto now = animation::Clock::now();
409 const auto time_histeresis = std::chrono::milliseconds(33);
410 if (now - previous_animation_time_ >= time_histeresis) {
411 previous_animation_time_ = now;
412 }
413}
414
415/// @brief Try to get the unique lock about behing able to capture the mouse.
416/// @return A unique lock if the mouse is not already captured, otherwise a
417/// null.
419 if (mouse_captured) {
420 return nullptr;
421 }
422 mouse_captured = true;
423 return std::make_unique<CapturedMouseImpl>(
424 [this] { mouse_captured = false; });
425}
426
427/// @brief Execute the main loop.
428/// @param component The component to draw.
429void ScreenInteractive::Loop(Component component) { // NOLINT
430 class Loop loop(this, std::move(component));
431 loop.Run();
432}
433
434/// @brief Return whether the main loop has been quit.
435bool ScreenInteractive::HasQuitted() {
436 return quit_;
437}
438
439// private
440void ScreenInteractive::PreMain() {
441 // Suspend previously active screen:
442 if (g_active_screen) {
443 std::swap(suspended_screen_, g_active_screen);
444 // Reset cursor position to the top of the screen and clear the screen.
445 suspended_screen_->ResetCursorPosition();
446 std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
447 suspended_screen_->dimx_ = 0;
448 suspended_screen_->dimy_ = 0;
449
450 // Reset dimensions to force drawing the screen again next time:
451 suspended_screen_->Uninstall();
452 }
453
454 // This screen is now active:
455 g_active_screen = this;
456 g_active_screen->Install();
457
458 previous_animation_time_ = animation::Clock::now();
459}
460
461// private
462void ScreenInteractive::PostMain() {
463 // Put cursor position at the end of the drawing.
464 ResetCursorPosition();
465
466 g_active_screen = nullptr;
467
468 // Restore suspended screen.
469 if (suspended_screen_) {
470 // Clear screen, and put the cursor at the beginning of the drawing.
471 std::cout << ResetPosition(/*clear=*/true);
472 dimx_ = 0;
473 dimy_ = 0;
474 Uninstall();
475 std::swap(g_active_screen, suspended_screen_);
476 g_active_screen->Install();
477 } else {
478 Uninstall();
479
480 std::cout << '\r';
481 // On final exit, keep the current drawing and reset cursor position one
482 // line after it.
483 if (!use_alternative_screen_) {
484 std::cout << '\n';
485 std::cout << std::flush;
486 }
487 }
488}
489
490/// @brief Decorate a function. It executes the same way, but with the currently
491/// active screen terminal hooks temporarilly uninstalled during its execution.
492/// @param fn The function to decorate.
494 return [this, fn] {
495 Uninstall();
496 fn();
497 Install();
498 };
499}
500
501/// @brief Force FTXUI to handle or not handle Ctrl-C, even if the component
502/// catches the Event::CtrlC.
504 force_handle_ctrl_c_ = force;
505}
506
507/// @brief Force FTXUI to handle or not handle Ctrl-Z, even if the component
508/// catches the Event::CtrlZ.
510 force_handle_ctrl_z_ = force;
511}
512
513/// @brief Returns the content of the current selection
515 if (!selection_) {
516 return "";
517 }
518 return selection_->GetParts();
519}
520
521void ScreenInteractive::SelectionChange(std::function<void()> callback) {
522 selection_on_change_ = std::move(callback);
523}
524
525/// @brief Return the currently active screen, or null if none.
526// static
528 return g_active_screen;
529}
530
531// private
532void ScreenInteractive::Install() {
533 frame_valid_ = false;
534
535 // Flush the buffer for stdout to ensure whatever the user has printed before
536 // is fully applied before we start modifying the terminal configuration. This
537 // is important, because we are using two different channels (stdout vs
538 // termios/WinAPI) to communicate with the terminal emulator below. See
539 // https://github.com/ArthurSonzogni/FTXUI/issues/846
540 Flush();
541
542 InstallPipedInputHandling();
543
544 // After uninstalling the new configuration, flush it to the terminal to
545 // ensure it is fully applied:
546 on_exit_functions.emplace([] { Flush(); });
547
548 on_exit_functions.emplace([this] { ExitLoopClosure()(); });
549
550 // Request the terminal to report the current cursor shape. We will restore it
551 // on exit.
552 std::cout << DECRQSS_DECSCUSR;
553 on_exit_functions.emplace([this] {
554 std::cout << "\033[?25h"; // Enable cursor.
555 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
556 });
557
558 // Install signal handlers to restore the terminal state on exit. The default
559 // signal handlers are restored on exit.
560 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
561 InstallSignalHandler(signal);
562 }
563
564// Save the old terminal configuration and restore it on exit.
565#if defined(_WIN32)
566 // Enable VT processing on stdout and stdin
567 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
568 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
569
570 DWORD out_mode = 0;
571 DWORD in_mode = 0;
572 GetConsoleMode(stdout_handle, &out_mode);
573 GetConsoleMode(stdin_handle, &in_mode);
574 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
575 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
576
577 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
578 const int enable_virtual_terminal_processing = 0x0004;
579 const int disable_newline_auto_return = 0x0008;
580 out_mode |= enable_virtual_terminal_processing;
581 out_mode |= disable_newline_auto_return;
582
583 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
584 const int enable_line_input = 0x0002;
585 const int enable_echo_input = 0x0004;
586 const int enable_virtual_terminal_input = 0x0200;
587 const int enable_window_input = 0x0008;
588 in_mode &= ~enable_echo_input;
589 in_mode &= ~enable_line_input;
590 in_mode |= enable_virtual_terminal_input;
591 in_mode |= enable_window_input;
592
593 SetConsoleMode(stdin_handle, in_mode);
594 SetConsoleMode(stdout_handle, out_mode);
595#else // POSIX (Linux & Mac)
596 // #if defined(__EMSCRIPTEN__)
597 //// Reading stdin isn't blocking.
598 // int flags = fcntl(0, F_GETFL, 0);
599 // fcntl(0, F_SETFL, flags | O_NONBLOCK);
600
601 //// Restore the terminal configuration on exit.
602 // on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
603 // #endif
604 for (const int signal : {SIGWINCH, SIGTSTP}) {
605 InstallSignalHandler(signal);
606 }
607
608 struct termios terminal; // NOLINT
609 tcgetattr(tty_fd_, &terminal);
610 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
611 tcsetattr(tty_fd_, TCSANOW, &terminal);
612 });
613
614 // Enabling raw terminal input mode
615 terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition
616 terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be
617 // flushed
618 terminal.c_iflag &= ~PARMRK; // Disable marking parity errors.
619 terminal.c_iflag &= ~ISTRIP; // Disable striping 8th bit off characters.
620 terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR.
621 terminal.c_iflag &= ~IGNCR; // Disable ignoring CR.
622 terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL.
623 terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output
624
625 terminal.c_lflag &= ~ECHO; // Disable echoing input characters.
626 terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters.
627 terminal.c_lflag &= ~ICANON; // Disable Canonical mode.
628 terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting:
629 // - => DSUSP
630 // - C-Z => SUSP
631 // - C-C => INTR
632 // - C-d => QUIT
633 terminal.c_lflag &= ~IEXTEN; // Disable extended input processing
634 terminal.c_cflag |= CS8; // 8 bits per byte
635
636 terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical
637 // read.
638 terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read.
639
640 tcsetattr(tty_fd_, TCSANOW, &terminal);
641
642#endif
643
644 auto enable = [&](const std::vector<DECMode>& parameters) {
645 std::cout << Set(parameters);
646 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
647 };
648
649 auto disable = [&](const std::vector<DECMode>& parameters) {
650 std::cout << Reset(parameters);
651 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
652 };
653
654 if (use_alternative_screen_) {
655 enable({
656 DECMode::kAlternateScreen,
657 });
658 }
659
660 disable({
661 // DECMode::kCursor,
662 DECMode::kLineWrap,
663 });
664
665 if (track_mouse_) {
666 enable({DECMode::kMouseVt200});
667 enable({DECMode::kMouseAnyEvent});
668 enable({DECMode::kMouseUrxvtMode});
669 enable({DECMode::kMouseSgrExtMode});
670 }
671
672 // After installing the new configuration, flush it to the terminal to
673 // ensure it is fully applied:
674 Flush();
675
676 quit_ = false;
677
678 PostAnimationTask();
679}
680
681void ScreenInteractive::InstallPipedInputHandling() {
682#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
683 tty_fd_ = STDIN_FILENO;
684 // Handle piped input redirection if explicitly enabled by the application.
685 // This allows applications to read data from stdin while still receiving
686 // keyboard input from the terminal for interactive use.
687 if (!handle_piped_input_) {
688 return;
689 }
690
691 // If stdin is a terminal, we don't need to open /dev/tty.
692 if (isatty(STDIN_FILENO)) {
693 return;
694 }
695
696 // Open /dev/tty for keyboard input.
697 tty_fd_ = open("/dev/tty", O_RDONLY);
698 if (tty_fd_ < 0) {
699 // Failed to open /dev/tty (containers, headless systems, etc.)
700 tty_fd_ = STDIN_FILENO; // Fallback to stdin.
701 return;
702 }
703
704 // Close the /dev/tty file descriptor on exit.
705 on_exit_functions.emplace([this] {
706 close(tty_fd_);
707 tty_fd_ = -1;
708 });
709#endif
710}
711
712// private
713void ScreenInteractive::Uninstall() {
714 ExitNow();
715 OnExit();
716}
717
718// private
719// NOLINTNEXTLINE
720void ScreenInteractive::RunOnceBlocking(Component component) {
721 // Set FPS to 60 at most.
722 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
723
724 auto time = std::chrono::steady_clock::now();
725 size_t executed_task = internal_->task_runner.ExecutedTasks();
726
727 // Wait for at least one task to execute.
728 while (executed_task == internal_->task_runner.ExecutedTasks() &&
729 !HasQuitted()) {
730 RunOnce(component);
731
732 const auto now = std::chrono::steady_clock::now();
733 const auto delta = now - time;
734 time = now;
735
736 if (delta < time_per_frame) {
737 const auto sleep_duration = time_per_frame - delta;
738 std::this_thread::sleep_for(sleep_duration);
739 }
740 }
741}
742
743// private
744void ScreenInteractive::RunOnce(Component component) {
745 AutoReset set_component(&component_, component);
746 ExecuteSignalHandlers();
747 FetchTerminalEvents();
748
749 // Execute the pending tasks from the queue.
750 const size_t executed_task = internal_->task_runner.ExecutedTasks();
751 internal_->task_runner.RunUntilIdle();
752 // If no executed task, we can return early without redrawing the screen.
753 if (executed_task == internal_->task_runner.ExecutedTasks()) {
754 return;
755 }
756
757 ExecuteSignalHandlers();
758 Draw(component);
759
760 if (selection_data_previous_ != selection_data_) {
761 selection_data_previous_ = selection_data_;
762 if (selection_on_change_) {
763 selection_on_change_();
765 }
766 }
767}
768
769// private
770// NOLINTNEXTLINE
771void ScreenInteractive::HandleTask(Component component, Task& task) {
772 std::visit(
773 [&](auto&& arg) {
774 using T = std::decay_t<decltype(arg)>;
775
776 // clang-format off
777 // Handle Event.
778 if constexpr (std::is_same_v<T, Event>) {
779
780 if (arg.is_cursor_position()) {
781 cursor_x_ = arg.cursor_x();
782 cursor_y_ = arg.cursor_y();
783 return;
784 }
785
786 if (arg.is_cursor_shape()) {
787 cursor_reset_shape_= arg.cursor_shape();
788 return;
789 }
790
791 if (arg.is_mouse()) {
792 arg.mouse().x -= cursor_x_;
793 arg.mouse().y -= cursor_y_;
794 }
795
796 arg.screen_ = this;
797
798 bool handled = component->OnEvent(arg);
799
800 handled = HandleSelection(handled, arg);
801
802 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
803 RecordSignal(SIGABRT);
804 }
805
806#if !defined(_WIN32)
807 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
808 RecordSignal(SIGTSTP);
809 }
810#endif
811
812 frame_valid_ = false;
813 return;
814 }
815
816 // Handle callback
817 if constexpr (std::is_same_v<T, Closure>) {
818 arg();
819 return;
820 }
821
822 // Handle Animation
823 if constexpr (std::is_same_v<T, AnimationTask>) {
824 if (!animation_requested_) {
825 return;
826 }
827
828 animation_requested_ = false;
829 const animation::TimePoint now = animation::Clock::now();
830 const animation::Duration delta = now - previous_animation_time_;
831 previous_animation_time_ = now;
832
833 animation::Params params(delta);
834 component->OnAnimation(params);
835 frame_valid_ = false;
836 return;
837 }
838 },
839 task);
840 // clang-format on
841}
842
843// private
844bool ScreenInteractive::HandleSelection(bool handled, Event event) {
845 if (handled) {
846 selection_pending_ = nullptr;
847 selection_data_.empty = true;
848 selection_ = nullptr;
849 return true;
850 }
851
852 if (!event.is_mouse()) {
853 return false;
854 }
855
856 auto& mouse = event.mouse();
857 if (mouse.button != Mouse::Left) {
858 return false;
859 }
860
861 if (mouse.motion == Mouse::Pressed) {
862 selection_pending_ = CaptureMouse();
863 selection_data_.start_x = mouse.x;
864 selection_data_.start_y = mouse.y;
865 selection_data_.end_x = mouse.x;
866 selection_data_.end_y = mouse.y;
867 return false;
868 }
869
870 if (!selection_pending_) {
871 return false;
872 }
873
874 if (mouse.motion == Mouse::Moved) {
875 if ((mouse.x != selection_data_.end_x) ||
876 (mouse.y != selection_data_.end_y)) {
877 selection_data_.end_x = mouse.x;
878 selection_data_.end_y = mouse.y;
879 selection_data_.empty = false;
880 }
881
882 return true;
883 }
884
885 if (mouse.motion == Mouse::Released) {
886 selection_pending_ = nullptr;
887 selection_data_.end_x = mouse.x;
888 selection_data_.end_y = mouse.y;
889 selection_data_.empty = false;
890 return true;
891 }
892
893 return false;
894}
895
896// private
897// NOLINTNEXTLINE
898void ScreenInteractive::Draw(Component component) {
899 if (frame_valid_) {
900 return;
901 }
902 auto document = component->Render();
903 int dimx = 0;
904 int dimy = 0;
905 auto terminal = Terminal::Size();
906 document->ComputeRequirement();
907 switch (dimension_) {
908 case Dimension::Fixed:
909 dimx = dimx_;
910 dimy = dimy_;
911 break;
912 case Dimension::TerminalOutput:
913 dimx = terminal.dimx;
914 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
915 break;
916 case Dimension::Fullscreen:
917 dimx = terminal.dimx;
918 dimy = terminal.dimy;
919 break;
920 case Dimension::FitComponent:
921 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
922 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
923 break;
924 }
925
926 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
927 ResetCursorPosition();
928 std::cout << ResetPosition(/*clear=*/resized);
929
930 // If the terminal width decrease, the terminal emulator will start wrapping
931 // lines and make the display dirty. We should clear it completely.
932 if ((dimx < dimx_) && !use_alternative_screen_) {
933 std::cout << "\033[J"; // clear terminal output
934 std::cout << "\033[H"; // move cursor to home position
935 }
936
937 // Resize the screen if needed.
938 if (resized) {
939 dimx_ = dimx;
940 dimy_ = dimy;
941 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
942 cursor_.x = dimx_ - 1;
943 cursor_.y = dimy_ - 1;
944 }
945
946 // Periodically request the terminal emulator the frame position relative to
947 // the screen. This is useful for converting mouse position reported in
948 // screen's coordinates to frame's coordinates.
949#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
950 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
951 // position, several output sequences are mixed together into garbage.
952 // This causes FTXUI user to see some "1;1;R" sequences into the Input
953 // component. See [issue]. Solution is to request cursor position less
954 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
955 // https://github.com/ArthurSonzogni/FTXUI/issues/136
956 static int i = -3;
957 ++i;
958 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
959 std::cout << DeviceStatusReport(DSRMode::kCursor);
960 }
961#else
962 static int i = -3;
963 ++i;
964 if (!use_alternative_screen_ &&
965 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
966 std::cout << DeviceStatusReport(DSRMode::kCursor);
967 }
968#endif
969 previous_frame_resized_ = resized;
970
971 selection_ = selection_data_.empty
972 ? std::make_unique<Selection>()
973 : std::make_unique<Selection>(
974 selection_data_.start_x, selection_data_.start_y, //
975 selection_data_.end_x, selection_data_.end_y);
976 Render(*this, document.get(), *selection_);
977
978 // Set cursor position for user using tools to insert CJK characters.
979 {
980 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
981 const int dy = dimy_ - 1 - cursor_.y;
982
983 set_cursor_position.clear();
984 reset_cursor_position.clear();
985
986 if (dy != 0) {
987 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
988 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
989 }
990
991 if (dx != 0) {
992 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
993 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
994 }
995
996 if (cursor_.shape == Cursor::Hidden) {
997 set_cursor_position += "\033[?25l";
998 } else {
999 set_cursor_position += "\033[?25h";
1000 set_cursor_position +=
1001 "\033[" + std::to_string(int(cursor_.shape)) + " q";
1002 }
1003 }
1004
1005 std::cout << ToString() << set_cursor_position;
1006 Flush();
1007 Clear();
1008 frame_valid_ = true;
1009 frame_count_++;
1010}
1011
1012// private
1013void ScreenInteractive::ResetCursorPosition() {
1014 std::cout << reset_cursor_position;
1015 reset_cursor_position = "";
1016}
1017
1018/// @brief Return a function to exit the main loop.
1020 return [this] { Exit(); };
1021}
1022
1023/// @brief Exit the main loop.
1025 Post([this] { ExitNow(); });
1026}
1027
1028// private:
1029void ScreenInteractive::ExitNow() {
1030 quit_ = true;
1031}
1032
1033// private:
1034void ScreenInteractive::Signal(int signal) {
1035 if (signal == SIGABRT) {
1036 Exit();
1037 return;
1038 }
1039
1040// Windows do no support SIGTSTP / SIGWINCH
1041#if !defined(_WIN32)
1042 if (signal == SIGTSTP) {
1043 Post([&] {
1044 ResetCursorPosition();
1045 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
1046 Uninstall();
1047 dimx_ = 0;
1048 dimy_ = 0;
1049 Flush();
1050 std::ignore = std::raise(SIGTSTP);
1051 Install();
1052 });
1053 return;
1054 }
1055
1056 if (signal == SIGWINCH) {
1057 Post(Event::Special({0}));
1058 return;
1059 }
1060#endif
1061}
1062
1063void ScreenInteractive::FetchTerminalEvents() {
1064#if defined(_WIN32)
1065 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1066 // Check if there is input in the console.
1067 auto console = GetStdHandle(STD_INPUT_HANDLE);
1068 DWORD number_of_events = 0;
1069 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1070 return std::vector<INPUT_RECORD>();
1071 }
1072 if (number_of_events <= 0) {
1073 // No input, return.
1074 return std::vector<INPUT_RECORD>();
1075 }
1076 // Read the input events.
1077 std::vector<INPUT_RECORD> records(number_of_events);
1078 DWORD number_of_events_read = 0;
1079 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1080 &number_of_events_read)) {
1081 return std::vector<INPUT_RECORD>();
1082 }
1083 records.resize(number_of_events_read);
1084 return records;
1085 };
1086
1087 auto records = get_input_records();
1088 if (records.size() == 0) {
1089 const auto timeout =
1090 std::chrono::steady_clock::now() - internal_->last_char_time;
1091 const size_t timeout_microseconds =
1092 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1093 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1094 return;
1095 }
1096 internal_->last_char_time = std::chrono::steady_clock::now();
1097
1098 // Convert the input events to FTXUI events.
1099 // For each event, we call the terminal input parser to convert it to
1100 // Event.
1101 for (const auto& r : records) {
1102 switch (r.EventType) {
1103 case KEY_EVENT: {
1104 auto key_event = r.Event.KeyEvent;
1105 // ignore UP key events
1106 if (key_event.bKeyDown == FALSE) {
1107 continue;
1108 }
1109 std::wstring wstring;
1110 wstring += key_event.uChar.UnicodeChar;
1111 for (auto it : to_string(wstring)) {
1112 internal_->terminal_input_parser.Add(it);
1113 }
1114 } break;
1115 case WINDOW_BUFFER_SIZE_EVENT:
1116 Post(Event::Special({0}));
1117 break;
1118 case MENU_EVENT:
1119 case FOCUS_EVENT:
1120 case MOUSE_EVENT:
1121 // TODO(mauve): Implement later.
1122 break;
1123 }
1124 }
1125#elif defined(__EMSCRIPTEN__)
1126 // Read chars from the terminal.
1127 // We configured it to be non blocking.
1128 std::array<char, 128> out{};
1129 size_t l = read(STDIN_FILENO, out.data(), out.size());
1130 if (l == 0) {
1131 const auto timeout =
1132 std::chrono::steady_clock::now() - internal_->last_char_time;
1133 const size_t timeout_microseconds =
1134 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1135 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1136 return;
1137 }
1138 internal_->last_char_time = std::chrono::steady_clock::now();
1139
1140 // Convert the chars to events.
1141 for (size_t i = 0; i < l; ++i) {
1142 internal_->terminal_input_parser.Add(out[i]);
1143 }
1144#else // POSIX (Linux & Mac)
1145 if (!CheckStdinReady(tty_fd_)) {
1146 const auto timeout =
1147 std::chrono::steady_clock::now() - internal_->last_char_time;
1148 const size_t timeout_ms =
1149 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1150 internal_->terminal_input_parser.Timeout(timeout_ms);
1151 return;
1152 }
1153 internal_->last_char_time = std::chrono::steady_clock::now();
1154
1155 // Read chars from the terminal.
1156 std::array<char, 128> out{};
1157 size_t l = read(tty_fd_, out.data(), out.size());
1158
1159 // Convert the chars to events.
1160 for (size_t i = 0; i < l; ++i) {
1161 internal_->terminal_input_parser.Add(out[i]);
1162 }
1163#endif
1164}
1165
1166void ScreenInteractive::PostAnimationTask() {
1167 Post(AnimationTask());
1168
1169 // Repeat the animation task every 15ms. This correspond to a frame rate
1170 // of around 66fps.
1171 internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1172 std::chrono::milliseconds(15));
1173}
1174
1175bool ScreenInteractive::SelectionData::operator==(
1176 const ScreenInteractive::SelectionData& other) const {
1177 if (empty && other.empty) {
1178 return true;
1179 }
1180 if (empty || other.empty) {
1181 return false;
1182 }
1183 return start_x == other.start_x && start_y == other.start_y &&
1184 end_x == other.end_x && end_y == other.end_y;
1185}
1186
1187bool ScreenInteractive::SelectionData::operator!=(
1188 const ScreenInteractive::SelectionData& other) const {
1189 return !(*this == other);
1190}
1191
1192} // namespace ftxui.
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 const Event CtrlC
Definition event.hpp:71
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 const Event CtrlZ
Definition event.hpp:94
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
Add an event to the main loop. It will be executed later, after every other scheduled events.
void Post(Task task)
Add a task to the main loop. It will be executed later, after every other scheduled tasks.
static ScreenInteractive FitComponent()
static ScreenInteractive Fullscreen()
static const Event Custom
Definition event.hpp:97
static ScreenInteractive FullscreenPrimaryScreen()
static ScreenInteractive * Active()
Return the currently active screen, or null if none.
CapturedMouse CaptureMouse()
Try to get the unique lock about behing able to capture the mouse.
std::string GetSelection()
Returns the content of the current selection.
static ScreenInteractive FullscreenAlternateScreen()
void TrackMouse(bool enable=true)
Set whether mouse is tracked and events reported. called outside of the main loop....
void SelectionChange(std::function< void()> callback)
void RequestAnimationFrame()
Add a task to draw the screen one more time, until all the animations are done.
Closure ExitLoopClosure()
Return a function to exit the main loop.
void ForceHandleCtrlC(bool force)
Force FTXUI to handle or not handle Ctrl-C, even if the component catches the Event::CtrlC.
void ForceHandleCtrlZ(bool force)
Force FTXUI to handle or not handle Ctrl-Z, even if the component catches the Event::CtrlZ.
Closure WithRestoredIO(Closure)
Decorate a function. It executes the same way, but with the currently active screen terminal hooks te...
static Event Special(std::string)
An custom event whose meaning is defined by the user of the library.
Definition event.cpp:74
Loop is a class that manages the event loop for a component.
Definition loop.hpp:56
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 ...
Definition event.hpp:29
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
Definition node.cpp:84
int dimy() const
Definition image.hpp:36
std::string ToString() const
Definition screen.cpp:416
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.
Definition screen.cpp:476
Cursor cursor_
Definition screen.hpp:79
void Clear()
Clear all the pixel from the screen.
Definition screen.cpp:495
int dimx() const
Definition image.hpp:35
std::vector< std::vector< Pixel > > pixels_
Definition image.hpp:46
Dimensions Size()
Get the terminal size.
Definition terminal.cpp:94
The FTXUI ftxui::Dimension:: namespace.
The FTXUI ftxui::animation:: namespace.
void SetFallbackSize(const Dimensions &fallbackSize)
Override terminal size in case auto-detection fails.
Definition terminal.cpp:124
std::chrono::duration< float > Duration
Definition animation.hpp:30
std::chrono::time_point< Clock > TimePoint
Definition animation.hpp:29
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
Definition util.hpp:11
The FTXUI ftxui:: namespace.
Definition animation.hpp:10
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(const std::wstring &s)
Convert a std::wstring into a UTF8 std::string.
Definition string.cpp:1566
Element select(Element e)
Set the child to be the one focused among its siblings.
Definition frame.cpp:108
std::variant< Event, Closure, AnimationTask > Task
Definition task.hpp:14
std::function< void()> Closure
Definition task.hpp:13
std::shared_ptr< ComponentBase > Component