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
55namespace ftxui {
56
57struct ScreenInteractive::Internal {
58 // Convert char to Event.
59 TerminalInputParser terminal_input_parser;
60
61 task::TaskRunner task_runner;
62
63 // The last time a character was received.
64 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
65 std::chrono::steady_clock::now();
66
67 explicit Internal(std::function<void(Event)> out)
68 : terminal_input_parser(std::move(out)) {}
69};
70
71namespace animation {
73 auto* screen = ScreenInteractive::Active();
74 if (screen) {
75 screen->RequestAnimationFrame();
76 }
77}
78} // namespace animation
79
80namespace {
81
82ScreenInteractive* g_active_screen = nullptr; // NOLINT
83
84void Flush() {
85 // Emscripten doesn't implement flush. We interpret zero as flush.
86 std::cout << '\0' << std::flush;
87}
88
89constexpr int timeout_milliseconds = 20;
90[[maybe_unused]] constexpr int timeout_microseconds =
91 timeout_milliseconds * 1000;
92#if defined(_WIN32)
93
94#elif defined(__EMSCRIPTEN__)
95#include <emscripten.h>
96
97extern "C" {
98EMSCRIPTEN_KEEPALIVE
99void ftxui_on_resize(int columns, int rows) {
101 columns,
102 rows,
103 });
104 std::raise(SIGWINCH);
105}
106}
107
108#else // POSIX (Linux & Mac)
109
110int CheckStdinReady(int fd) {
111 timeval tv = {0, 0}; // NOLINT
112 fd_set fds;
113 FD_ZERO(&fds); // NOLINT
114 FD_SET(fd, &fds); // NOLINT
115 select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT
116 return FD_ISSET(fd, &fds); // NOLINT
117}
118
119#endif
120
121std::stack<Closure> on_exit_functions; // NOLINT
122void OnExit() {
123 while (!on_exit_functions.empty()) {
124 on_exit_functions.top()();
125 on_exit_functions.pop();
126 }
127}
128
129std::atomic<int> g_signal_exit_count = 0; // NOLINT
130#if !defined(_WIN32)
131std::atomic<int> g_signal_stop_count = 0; // NOLINT
132std::atomic<int> g_signal_resize_count = 0; // NOLINT
133#endif
134
135// Async signal safe function
136void RecordSignal(int signal) {
137 switch (signal) {
138 case SIGABRT:
139 case SIGFPE:
140 case SIGILL:
141 case SIGINT:
142 case SIGSEGV:
143 case SIGTERM:
144 g_signal_exit_count++;
145 break;
146
147#if !defined(_WIN32)
148 case SIGTSTP: // NOLINT
149 g_signal_stop_count++;
150 break;
151
152 case SIGWINCH: // NOLINT
153 g_signal_resize_count++;
154 break;
155#endif
156
157 default:
158 break;
159 }
160}
161
162void ExecuteSignalHandlers() {
163 int signal_exit_count = g_signal_exit_count.exchange(0);
164 while (signal_exit_count--) {
165 ScreenInteractive::Private::Signal(*g_active_screen, SIGABRT);
166 }
167
168#if !defined(_WIN32)
169 int signal_stop_count = g_signal_stop_count.exchange(0);
170 while (signal_stop_count--) {
171 ScreenInteractive::Private::Signal(*g_active_screen, SIGTSTP);
172 }
173
174 int signal_resize_count = g_signal_resize_count.exchange(0);
175 while (signal_resize_count--) {
176 ScreenInteractive::Private::Signal(*g_active_screen, SIGWINCH);
177 }
178#endif
179}
180
181void InstallSignalHandler(int sig) {
182 auto old_signal_handler = std::signal(sig, RecordSignal);
183 on_exit_functions.emplace(
184 [=] { std::ignore = std::signal(sig, old_signal_handler); });
185}
186
187// CSI: Control Sequence Introducer
188const std::string CSI = "\x1b["; // NOLINT
189 //
190// DCS: Device Control String
191const std::string DCS = "\x1bP"; // NOLINT
192// ST: String Terminator
193const std::string ST = "\x1b\\"; // NOLINT
194
195// DECRQSS: Request Status String
196// DECSCUSR: Set Cursor Style
197const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
198
199// DEC: Digital Equipment Corporation
200enum class DECMode : std::uint16_t {
201 kLineWrap = 7,
202 kCursor = 25,
203
204 kMouseX10 = 9,
205 kMouseVt200 = 1000,
206 kMouseVt200Highlight = 1001,
207
208 kMouseBtnEventMouse = 1002,
209 kMouseAnyEvent = 1003,
210
211 kMouseUtf8 = 1005,
212 kMouseSgrExtMode = 1006,
213 kMouseUrxvtMode = 1015,
214 kMouseSgrPixelsMode = 1016,
215 kAlternateScreen = 1049,
216};
217
218// Device Status Report (DSR) {
219enum class DSRMode : std::uint8_t {
220 kCursor = 6,
221};
222
223std::string Serialize(const std::vector<DECMode>& parameters) {
224 bool first = true;
225 std::string out;
226 for (const DECMode parameter : parameters) {
227 if (!first) {
228 out += ";";
229 }
230 out += std::to_string(int(parameter));
231 first = false;
232 }
233 return out;
234}
235
236// DEC Private Mode Set (DECSET)
237std::string Set(const std::vector<DECMode>& parameters) {
238 return CSI + "?" + Serialize(parameters) + "h";
239}
240
241// DEC Private Mode Reset (DECRST)
242std::string Reset(const std::vector<DECMode>& parameters) {
243 return CSI + "?" + Serialize(parameters) + "l";
244}
245
246// Device Status Report (DSR)
247std::string DeviceStatusReport(DSRMode ps) {
248 return CSI + std::to_string(int(ps)) + "n";
249}
250
251class CapturedMouseImpl : public CapturedMouseInterface {
252 public:
253 explicit CapturedMouseImpl(std::function<void(void)> callback)
254 : callback_(std::move(callback)) {}
255 ~CapturedMouseImpl() override { callback_(); }
256 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
257 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
258 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
259 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
260
261 private:
262 std::function<void(void)> callback_;
263};
264
265} // namespace
266
267ScreenInteractive::ScreenInteractive(Dimension dimension,
268 int dimx,
269 int dimy,
270 bool use_alternative_screen)
271 : Screen(dimx, dimy),
272 dimension_(dimension),
273 use_alternative_screen_(use_alternative_screen) {
274 internal_ = std::make_unique<Internal>(
275 [&](Event event) { PostEvent(std::move(event)); });
276}
277
278// static
280 return {
281 Dimension::Fixed,
282 dimx,
283 dimy,
284 /*use_alternative_screen=*/false,
285 };
286}
287
288/// Create a ScreenInteractive taking the full terminal size. This is using the
289/// alternate screen buffer to avoid messing with the terminal content.
290/// @note This is the same as `ScreenInteractive::FullscreenAlternateScreen()`
291// static
295
296/// Create a ScreenInteractive taking the full terminal size. The primary screen
297/// buffer is being used. It means if the terminal is resized, the previous
298/// content might mess up with the terminal content.
299// static
301 auto terminal = Terminal::Size();
302 return {
303 Dimension::Fullscreen,
304 terminal.dimx,
305 terminal.dimy,
306 /*use_alternative_screen=*/false,
307 };
308}
309
310/// Create a ScreenInteractive taking the full terminal size. This is using the
311/// alternate screen buffer to avoid messing with the terminal content.
312// static
314 auto terminal = Terminal::Size();
315 return {
316 Dimension::Fullscreen,
317 terminal.dimx,
318 terminal.dimy,
319 /*use_alternative_screen=*/true,
320 };
321}
322
323/// Create a ScreenInteractive whose width match the terminal output width and
324/// the height matches the component being drawn.
325// static
327 auto terminal = Terminal::Size();
328 return {
329 Dimension::TerminalOutput,
330 terminal.dimx,
331 terminal.dimy, // Best guess.
332 /*use_alternative_screen=*/false,
333 };
334}
335
337
338/// Create a ScreenInteractive whose width and height match the component being
339/// drawn.
340// static
342 auto terminal = Terminal::Size();
343 return {
344 Dimension::FitComponent,
345 terminal.dimx, // Best guess.
346 terminal.dimy, // Best guess.
347 false,
348 };
349}
350
351/// @brief Set whether mouse is tracked and events reported.
352/// called outside of the main loop. E.g `ScreenInteractive::Loop(...)`.
353/// @param enable Whether to enable mouse event tracking.
354/// @note This muse be called outside of the main loop. E.g. before calling
355/// `ScreenInteractive::Loop`.
356/// @note Mouse tracking is enabled by default.
357/// @note Mouse tracking is only supported on terminals that supports it.
358///
359/// ### Example
360///
361/// ```cpp
362/// auto screen = ScreenInteractive::TerminalOutput();
363/// screen.TrackMouse(false);
364/// screen.Loop(component);
365/// ```
367 track_mouse_ = enable;
368}
369
370/// @brief Enable or disable automatic piped input handling.
371/// When enabled, FTXUI will detect piped input and redirect stdin from /dev/tty
372/// for keyboard input, allowing applications to read piped data while still
373/// receiving interactive keyboard events.
374/// @param enable Whether to enable piped input handling. Default is true.
375/// @note This must be called before Loop().
376/// @note This feature is enabled by default.
377/// @note This feature is only available on POSIX systems (Linux/macOS).
379 handle_piped_input_ = enable;
380}
381
382/// @brief Add a task to the main loop.
383/// It will be executed later, after every other scheduled tasks.
385 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
386 HandleTask(component_, task);
387 });
388}
389
390/// @brief Add an event to the main loop.
391/// It will be executed later, after every other scheduled events.
393 Post(event);
394}
395
396/// @brief Add a task to draw the screen one more time, until all the animations
397/// are done.
399 if (animation_requested_) {
400 return;
401 }
402 animation_requested_ = true;
403 auto now = animation::Clock::now();
404 const auto time_histeresis = std::chrono::milliseconds(33);
405 if (now - previous_animation_time_ >= time_histeresis) {
406 previous_animation_time_ = now;
407 }
408}
409
410/// @brief Try to get the unique lock about behing able to capture the mouse.
411/// @return A unique lock if the mouse is not already captured, otherwise a
412/// null.
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 Execute the main loop.
423/// @param component The component to draw.
424void ScreenInteractive::Loop(Component component) { // NOLINT
425 class Loop loop(this, std::move(component));
426 loop.Run();
427}
428
429/// @brief Return whether the main loop has been quit.
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 Decorate a function. It executes the same way, but with the currently
486/// active screen terminal hooks temporarilly uninstalled during its execution.
487/// @param fn The function to decorate.
489 return [this, fn] {
490 Uninstall();
491 fn();
492 Install();
493 };
494}
495
496/// @brief Force FTXUI to handle or not handle Ctrl-C, even if the component
497/// catches the Event::CtrlC.
499 force_handle_ctrl_c_ = force;
500}
501
502/// @brief Force FTXUI to handle or not handle Ctrl-Z, even if the component
503/// catches the Event::CtrlZ.
505 force_handle_ctrl_z_ = force;
506}
507
508/// @brief Returns the content of the current selection
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 Return the currently active screen, or null if none.
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 // Request the terminal to report the current cursor shape. We will restore it
544 // on exit.
545 std::cout << DECRQSS_DECSCUSR;
546 on_exit_functions.emplace([this] {
547 std::cout << "\033[?25h"; // Enable cursor.
548 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
549 });
550
551 // Install signal handlers to restore the terminal state on exit. The default
552 // signal handlers are restored on exit.
553 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
554 InstallSignalHandler(signal);
555 }
556
557// Save the old terminal configuration and restore it on exit.
558#if defined(_WIN32)
559 // Enable VT processing on stdout and stdin
560 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
561 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
562
563 DWORD out_mode = 0;
564 DWORD in_mode = 0;
565 GetConsoleMode(stdout_handle, &out_mode);
566 GetConsoleMode(stdin_handle, &in_mode);
567 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
568 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
569
570 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
571 const int enable_virtual_terminal_processing = 0x0004;
572 const int disable_newline_auto_return = 0x0008;
573 out_mode |= enable_virtual_terminal_processing;
574 out_mode |= disable_newline_auto_return;
575
576 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
577 const int enable_line_input = 0x0002;
578 const int enable_echo_input = 0x0004;
579 const int enable_virtual_terminal_input = 0x0200;
580 const int enable_window_input = 0x0008;
581 in_mode &= ~enable_echo_input;
582 in_mode &= ~enable_line_input;
583 in_mode |= enable_virtual_terminal_input;
584 in_mode |= enable_window_input;
585
586 SetConsoleMode(stdin_handle, in_mode);
587 SetConsoleMode(stdout_handle, out_mode);
588#else // POSIX (Linux & Mac)
589 // #if defined(__EMSCRIPTEN__)
590 //// Reading stdin isn't blocking.
591 // int flags = fcntl(0, F_GETFL, 0);
592 // fcntl(0, F_SETFL, flags | O_NONBLOCK);
593
594 //// Restore the terminal configuration on exit.
595 // on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
596 // #endif
597 for (const int signal : {SIGWINCH, SIGTSTP}) {
598 InstallSignalHandler(signal);
599 }
600
601 struct termios terminal; // NOLINT
602 tcgetattr(tty_fd_, &terminal);
603 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
604 tcsetattr(tty_fd_, TCSANOW, &terminal);
605 });
606
607 // Enabling raw terminal input mode
608 terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition
609 terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be
610 // flushed
611 terminal.c_iflag &= ~PARMRK; // Disable marking parity errors.
612 terminal.c_iflag &= ~ISTRIP; // Disable striping 8th bit off characters.
613 terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR.
614 terminal.c_iflag &= ~IGNCR; // Disable ignoring CR.
615 terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL.
616 terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output
617
618 terminal.c_lflag &= ~ECHO; // Disable echoing input characters.
619 terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters.
620 terminal.c_lflag &= ~ICANON; // Disable Canonical mode.
621 terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting:
622 // - => DSUSP
623 // - C-Z => SUSP
624 // - C-C => INTR
625 // - C-d => QUIT
626 terminal.c_lflag &= ~IEXTEN; // Disable extended input processing
627 terminal.c_cflag |= CS8; // 8 bits per byte
628
629 terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical
630 // read.
631 terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read.
632
633 tcsetattr(tty_fd_, TCSANOW, &terminal);
634
635#endif
636
637 auto enable = [&](const std::vector<DECMode>& parameters) {
638 std::cout << Set(parameters);
639 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
640 };
641
642 auto disable = [&](const std::vector<DECMode>& parameters) {
643 std::cout << Reset(parameters);
644 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
645 };
646
647 if (use_alternative_screen_) {
648 enable({
649 DECMode::kAlternateScreen,
650 });
651 }
652
653 disable({
654 // DECMode::kCursor,
655 DECMode::kLineWrap,
656 });
657
658 if (track_mouse_) {
659 enable({DECMode::kMouseVt200});
660 enable({DECMode::kMouseAnyEvent});
661 enable({DECMode::kMouseUrxvtMode});
662 enable({DECMode::kMouseSgrExtMode});
663 }
664
665 // After installing the new configuration, flush it to the terminal to
666 // ensure it is fully applied:
667 Flush();
668
669 quit_ = false;
670
671 PostAnimationTask();
672}
673
674void ScreenInteractive::InstallPipedInputHandling() {
675#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
676 tty_fd_ = STDIN_FILENO;
677 // Handle piped input redirection if explicitly enabled by the application.
678 // This allows applications to read data from stdin while still receiving
679 // keyboard input from the terminal for interactive use.
680 if (!handle_piped_input_) {
681 return;
682 }
683
684 // If stdin is a terminal, we don't need to open /dev/tty.
685 if (isatty(STDIN_FILENO)) {
686 return;
687 }
688
689 // Open /dev/tty for keyboard input.
690 tty_fd_ = open("/dev/tty", O_RDONLY);
691 if (tty_fd_ < 0) {
692 // Failed to open /dev/tty (containers, headless systems, etc.)
693 tty_fd_ = STDIN_FILENO; // Fallback to stdin.
694 return;
695 }
696
697 // Close the /dev/tty file descriptor on exit.
698 on_exit_functions.emplace([this] {
699 close(tty_fd_);
700 tty_fd_ = -1;
701 });
702#endif
703}
704
705// private
706void ScreenInteractive::Uninstall() {
707 ExitNow();
708 OnExit();
709}
710
711// private
712// NOLINTNEXTLINE
713void ScreenInteractive::RunOnceBlocking(Component component) {
714 // Set FPS to 60 at most.
715 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
716
717 auto time = std::chrono::steady_clock::now();
718 size_t executed_task = internal_->task_runner.ExecutedTasks();
719
720 // Wait for at least one task to execute.
721 while (executed_task == internal_->task_runner.ExecutedTasks() &&
722 !HasQuitted()) {
723 RunOnce(component);
724
725 const auto now = std::chrono::steady_clock::now();
726 const auto delta = now - time;
727 time = now;
728
729 if (delta < time_per_frame) {
730 const auto sleep_duration = time_per_frame - delta;
731 std::this_thread::sleep_for(sleep_duration);
732 }
733 }
734}
735
736// private
737void ScreenInteractive::RunOnce(Component component) {
738 AutoReset set_component(&component_, component);
739 ExecuteSignalHandlers();
740 FetchTerminalEvents();
741
742 // Execute the pending tasks from the queue.
743 const size_t executed_task = internal_->task_runner.ExecutedTasks();
744 internal_->task_runner.RunUntilIdle();
745 // If no executed task, we can return early without redrawing the screen.
746 if (executed_task == internal_->task_runner.ExecutedTasks()) {
747 return;
748 }
749
750 ExecuteSignalHandlers();
751 Draw(component);
752
753 if (selection_data_previous_ != selection_data_) {
754 selection_data_previous_ = selection_data_;
755 if (selection_on_change_) {
756 selection_on_change_();
758 }
759 }
760}
761
762// private
763// NOLINTNEXTLINE
764void ScreenInteractive::HandleTask(Component component, Task& task) {
765 std::visit(
766 [&](auto&& arg) {
767 using T = std::decay_t<decltype(arg)>;
768
769 // clang-format off
770 // Handle Event.
771 if constexpr (std::is_same_v<T, Event>) {
772
773 if (arg.is_cursor_position()) {
774 cursor_x_ = arg.cursor_x();
775 cursor_y_ = arg.cursor_y();
776 return;
777 }
778
779 if (arg.is_cursor_shape()) {
780 cursor_reset_shape_= arg.cursor_shape();
781 return;
782 }
783
784 if (arg.is_mouse()) {
785 arg.mouse().x -= cursor_x_;
786 arg.mouse().y -= cursor_y_;
787 }
788
789 arg.screen_ = this;
790
791 bool handled = component->OnEvent(arg);
792
793 handled = HandleSelection(handled, arg);
794
795 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
796 RecordSignal(SIGABRT);
797 }
798
799#if !defined(_WIN32)
800 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
801 RecordSignal(SIGTSTP);
802 }
803#endif
804
805 frame_valid_ = false;
806 return;
807 }
808
809 // Handle callback
810 if constexpr (std::is_same_v<T, Closure>) {
811 arg();
812 return;
813 }
814
815 // Handle Animation
816 if constexpr (std::is_same_v<T, AnimationTask>) {
817 if (!animation_requested_) {
818 return;
819 }
820
821 animation_requested_ = false;
822 const animation::TimePoint now = animation::Clock::now();
823 const animation::Duration delta = now - previous_animation_time_;
824 previous_animation_time_ = now;
825
826 animation::Params params(delta);
827 component->OnAnimation(params);
828 frame_valid_ = false;
829 return;
830 }
831 },
832 task);
833 // clang-format on
834}
835
836// private
837bool ScreenInteractive::HandleSelection(bool handled, Event event) {
838 if (handled) {
839 selection_pending_ = nullptr;
840 selection_data_.empty = true;
841 selection_ = nullptr;
842 return true;
843 }
844
845 if (!event.is_mouse()) {
846 return false;
847 }
848
849 auto& mouse = event.mouse();
850 if (mouse.button != Mouse::Left) {
851 return false;
852 }
853
854 if (mouse.motion == Mouse::Pressed) {
855 selection_pending_ = CaptureMouse();
856 selection_data_.start_x = mouse.x;
857 selection_data_.start_y = mouse.y;
858 selection_data_.end_x = mouse.x;
859 selection_data_.end_y = mouse.y;
860 return false;
861 }
862
863 if (!selection_pending_) {
864 return false;
865 }
866
867 if (mouse.motion == Mouse::Moved) {
868 if ((mouse.x != selection_data_.end_x) ||
869 (mouse.y != selection_data_.end_y)) {
870 selection_data_.end_x = mouse.x;
871 selection_data_.end_y = mouse.y;
872 selection_data_.empty = false;
873 }
874
875 return true;
876 }
877
878 if (mouse.motion == Mouse::Released) {
879 selection_pending_ = nullptr;
880 selection_data_.end_x = mouse.x;
881 selection_data_.end_y = mouse.y;
882 selection_data_.empty = false;
883 return true;
884 }
885
886 return false;
887}
888
889// private
890// NOLINTNEXTLINE
891void ScreenInteractive::Draw(Component component) {
892 if (frame_valid_) {
893 return;
894 }
895 auto document = component->Render();
896 int dimx = 0;
897 int dimy = 0;
898 auto terminal = Terminal::Size();
899 document->ComputeRequirement();
900 switch (dimension_) {
901 case Dimension::Fixed:
902 dimx = dimx_;
903 dimy = dimy_;
904 break;
905 case Dimension::TerminalOutput:
906 dimx = terminal.dimx;
907 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
908 break;
909 case Dimension::Fullscreen:
910 dimx = terminal.dimx;
911 dimy = terminal.dimy;
912 break;
913 case Dimension::FitComponent:
914 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
915 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
916 break;
917 }
918
919 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
920 ResetCursorPosition();
921 std::cout << ResetPosition(/*clear=*/resized);
922
923 // If the terminal width decrease, the terminal emulator will start wrapping
924 // lines and make the display dirty. We should clear it completely.
925 if ((dimx < dimx_) && !use_alternative_screen_) {
926 std::cout << "\033[J"; // clear terminal output
927 std::cout << "\033[H"; // move cursor to home position
928 }
929
930 // Resize the screen if needed.
931 if (resized) {
932 dimx_ = dimx;
933 dimy_ = dimy;
934 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
935 cursor_.x = dimx_ - 1;
936 cursor_.y = dimy_ - 1;
937 }
938
939 // Periodically request the terminal emulator the frame position relative to
940 // the screen. This is useful for converting mouse position reported in
941 // screen's coordinates to frame's coordinates.
942#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
943 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
944 // position, several output sequences are mixed together into garbage.
945 // This causes FTXUI user to see some "1;1;R" sequences into the Input
946 // component. See [issue]. Solution is to request cursor position less
947 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
948 // https://github.com/ArthurSonzogni/FTXUI/issues/136
949 static int i = -3;
950 ++i;
951 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
952 std::cout << DeviceStatusReport(DSRMode::kCursor);
953 }
954#else
955 static int i = -3;
956 ++i;
957 if (!use_alternative_screen_ &&
958 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
959 std::cout << DeviceStatusReport(DSRMode::kCursor);
960 }
961#endif
962 previous_frame_resized_ = resized;
963
964 selection_ = selection_data_.empty
965 ? std::make_unique<Selection>()
966 : std::make_unique<Selection>(
967 selection_data_.start_x, selection_data_.start_y, //
968 selection_data_.end_x, selection_data_.end_y);
969 Render(*this, document.get(), *selection_);
970
971 // Set cursor position for user using tools to insert CJK characters.
972 {
973 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
974 const int dy = dimy_ - 1 - cursor_.y;
975
976 set_cursor_position.clear();
977 reset_cursor_position.clear();
978
979 if (dy != 0) {
980 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
981 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
982 }
983
984 if (dx != 0) {
985 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
986 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
987 }
988
989 if (cursor_.shape == Cursor::Hidden) {
990 set_cursor_position += "\033[?25l";
991 } else {
992 set_cursor_position += "\033[?25h";
993 set_cursor_position +=
994 "\033[" + std::to_string(int(cursor_.shape)) + " q";
995 }
996 }
997
998 std::cout << ToString() << set_cursor_position;
999 Flush();
1000 Clear();
1001 frame_valid_ = true;
1002 frame_count_++;
1003}
1004
1005// private
1006void ScreenInteractive::ResetCursorPosition() {
1007 std::cout << reset_cursor_position;
1008 reset_cursor_position = "";
1009}
1010
1011/// @brief Return a function to exit the main loop.
1013 return [this] { Exit(); };
1014}
1015
1016/// @brief Exit the main loop.
1018 Post([this] { ExitNow(); });
1019}
1020
1021// private:
1022void ScreenInteractive::ExitNow() {
1023 quit_ = true;
1024}
1025
1026// private:
1027void ScreenInteractive::Signal(int signal) {
1028 if (signal == SIGABRT) {
1029 Exit();
1030 return;
1031 }
1032
1033// Windows do no support SIGTSTP / SIGWINCH
1034#if !defined(_WIN32)
1035 if (signal == SIGTSTP) {
1036 Post([&] {
1037 ResetCursorPosition();
1038 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
1039 Uninstall();
1040 dimx_ = 0;
1041 dimy_ = 0;
1042 Flush();
1043 std::ignore = std::raise(SIGTSTP);
1044 Install();
1045 });
1046 return;
1047 }
1048
1049 if (signal == SIGWINCH) {
1050 Post(Event::Special({0}));
1051 return;
1052 }
1053#endif
1054}
1055
1056void ScreenInteractive::FetchTerminalEvents() {
1057#if defined(_WIN32)
1058 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1059 // Check if there is input in the console.
1060 auto console = GetStdHandle(STD_INPUT_HANDLE);
1061 DWORD number_of_events = 0;
1062 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1063 return std::vector<INPUT_RECORD>();
1064 }
1065 if (number_of_events <= 0) {
1066 // No input, return.
1067 return std::vector<INPUT_RECORD>();
1068 }
1069 // Read the input events.
1070 std::vector<INPUT_RECORD> records(number_of_events);
1071 DWORD number_of_events_read = 0;
1072 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1073 &number_of_events_read)) {
1074 return std::vector<INPUT_RECORD>();
1075 }
1076 records.resize(number_of_events_read);
1077 return records;
1078 };
1079
1080 auto records = get_input_records();
1081 if (records.size() == 0) {
1082 const auto timeout =
1083 std::chrono::steady_clock::now() - internal_->last_char_time;
1084 const size_t timeout_microseconds =
1085 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1086 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1087 return;
1088 }
1089 internal_->last_char_time = std::chrono::steady_clock::now();
1090
1091 // Convert the input events to FTXUI events.
1092 // For each event, we call the terminal input parser to convert it to
1093 // Event.
1094 std::wstring wstring;
1095 for (const auto& r : records) {
1096 switch (r.EventType) {
1097 case KEY_EVENT: {
1098 auto key_event = r.Event.KeyEvent;
1099 // ignore UP key events
1100 if (key_event.bKeyDown == FALSE) {
1101 continue;
1102 }
1103 const wchar_t wc = key_event.uChar.UnicodeChar;
1104 wstring += wc;
1105 if (wc >= 0xd800 && wc <= 0xdbff) {
1106 // Wait for the Low Surrogate to arrive in the next record.
1107 continue;
1108 }
1109 for (auto it : to_string(wstring)) {
1110 internal_->terminal_input_parser.Add(it);
1111 }
1112 wstring.clear();
1113 } break;
1114 case WINDOW_BUFFER_SIZE_EVENT:
1115 Post(Event::Special({0}));
1116 break;
1117 case MENU_EVENT:
1118 case FOCUS_EVENT:
1119 case MOUSE_EVENT:
1120 // TODO(mauve): Implement later.
1121 break;
1122 }
1123 }
1124#elif defined(__EMSCRIPTEN__)
1125 // Read chars from the terminal.
1126 // We configured it to be non blocking.
1127 std::array<char, 128> out{};
1128 size_t l = read(STDIN_FILENO, out.data(), out.size());
1129 if (l == 0) {
1130 const auto timeout =
1131 std::chrono::steady_clock::now() - internal_->last_char_time;
1132 const size_t timeout_microseconds =
1133 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1134 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1135 return;
1136 }
1137 internal_->last_char_time = std::chrono::steady_clock::now();
1138
1139 // Convert the chars to events.
1140 for (size_t i = 0; i < l; ++i) {
1141 internal_->terminal_input_parser.Add(out[i]);
1142 }
1143#else // POSIX (Linux & Mac)
1144 if (!CheckStdinReady(tty_fd_)) {
1145 const auto timeout =
1146 std::chrono::steady_clock::now() - internal_->last_char_time;
1147 const size_t timeout_ms =
1148 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1149 internal_->terminal_input_parser.Timeout(timeout_ms);
1150 return;
1151 }
1152 internal_->last_char_time = std::chrono::steady_clock::now();
1153
1154 // Read chars from the terminal.
1155 std::array<char, 128> out{};
1156 size_t l = read(tty_fd_, out.data(), out.size());
1157
1158 // Convert the chars to events.
1159 for (size_t i = 0; i < l; ++i) {
1160 internal_->terminal_input_parser.Add(out[i]);
1161 }
1162#endif
1163}
1164
1165void ScreenInteractive::PostAnimationTask() {
1166 Post(AnimationTask());
1167
1168 // Repeat the animation task every 15ms. This correspond to a frame rate
1169 // of around 66fps.
1170 internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1171 std::chrono::milliseconds(15));
1172}
1173
1174bool ScreenInteractive::SelectionData::operator==(
1175 const ScreenInteractive::SelectionData& other) const {
1176 if (empty && other.empty) {
1177 return true;
1178 }
1179 if (empty || other.empty) {
1180 return false;
1181 }
1182 return start_x == other.start_x && start_y == other.start_y &&
1183 end_x == other.end_x && end_y == other.end_y;
1184}
1185
1186bool ScreenInteractive::SelectionData::operator!=(
1187 const ScreenInteractive::SelectionData& other) const {
1188 return !(*this == other);
1189}
1190
1191} // 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:73
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:96
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
Add an event to the main loop. It will be executed later, after every other scheduled events.
void Post(Task task)
Add a task to the main loop. It will be executed later, after every other scheduled tasks.
static ScreenInteractive FitComponent()
static ScreenInteractive Fullscreen()
static Event Special(std::string_view)
An custom event whose meaning is defined by the user of the library.
Definition event.cpp:74
static const Event Custom
Definition event.hpp:99
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...
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:30
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(std::wstring_view s)
Convert a std::wstring into a UTF8 std::string.
Definition string.cpp:1565
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