FTXUI 6.1.9
C++ functional terminal UI.
Loading...
Searching...
No Matches
app.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 Cell, 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 <string_view>
21#include <thread> // for thread, sleep_for
22#include <tuple> // for _Swallow_assign, ignore
23#include <utility> // for move, swap
24#include <variant> // for visit, variant
25#include <vector> // for vector
26#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
27#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
28#include "ftxui/component/component_base.hpp" // for ComponentBase
29#include "ftxui/component/event.hpp" // for Event
30#include "ftxui/component/loop.hpp" // for Loop
32#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
33#include "ftxui/dom/node.hpp" // for Node, Render
34#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
35#include "ftxui/screen/util.hpp" // for util::clamp
36#include "ftxui/util/autoreset.hpp" // for AutoReset
37
38#if defined(_WIN32)
39#define DEFINE_CONSOLEV2_PROPERTIES
40#define WIN32_LEAN_AND_MEAN
41#ifndef NOMINMAX
42#define NOMINMAX
43#endif
44#include <windows.h>
45#ifndef UNICODE
46#error Must be compiled in UNICODE mode
47#endif
48#else
49#include <fcntl.h>
50#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
51#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
52#include <unistd.h> // for STDIN_FILENO, read
53#include <cerrno>
54#endif
55
56namespace ftxui {
57
58namespace animation {
60 auto* screen = App::Active();
61 if (screen) {
62 screen->RequestAnimationFrame();
63 }
64}
65} // namespace animation
66
67struct App::Internal {
68 // Convert char to Event.
69 TerminalInputParser terminal_input_parser;
70
71 task::TaskRunner task_runner;
72
73 // The last time a character was received.
74 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
75 std::chrono::steady_clock::now();
76
77 // The buffer used to output the screen to the terminal.
78 // Unlike for std::vector::clear, the C++ standard does not explicitly require
79 // that capacity is unchanged by this function, but existing implementations
80 // do not change capacity. This means that they do not release the allocated
81 // memory (see also shrink_to_fit).
82 std::string output_buffer;
83
84 explicit Internal(std::function<void(Event)> out)
85 : terminal_input_parser(std::move(out)) {}
86};
87
88namespace {
89
90App* g_active_screen = nullptr; // NOLINT
91
92std::stack<Closure> on_exit_functions; // NOLINT
93
94void OnExit() {
95 while (!on_exit_functions.empty()) {
96 on_exit_functions.top()();
97 on_exit_functions.pop();
98 }
99}
100
101#if defined(_WIN32)
102
103#elif defined(__EMSCRIPTEN__)
104#include <emscripten.h>
105
106extern "C" {
107EMSCRIPTEN_KEEPALIVE
108void ftxui_on_resize(int columns, int rows) {
110 columns,
111 rows,
112 });
113 std::raise(SIGWINCH);
114}
115}
116
117#else // POSIX (Linux & Mac)
118
119int CheckStdinReady(int fd) {
120 timeval tv = {0, 0}; // NOLINT
121 fd_set fds;
122 FD_ZERO(&fds); // NOLINT
123 FD_SET(fd, &fds); // NOLINT
124 select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT
125 return FD_ISSET(fd, &fds); // NOLINT
126}
127
128#endif
129
130std::atomic<int> g_signal_exit_count = 0; // NOLINT
131#if !defined(_WIN32)
132std::atomic<int> g_signal_stop_count = 0; // NOLINT
133std::atomic<int> g_signal_resize_count = 0; // NOLINT
134#endif
135
136// Async signal safe function
137void RecordSignal(int signal) {
138 switch (signal) {
139 case SIGABRT:
140 case SIGFPE:
141 case SIGILL:
142 case SIGINT:
143 case SIGSEGV:
144 case SIGTERM:
145 g_signal_exit_count++;
146 break;
147
148#if !defined(_WIN32)
149 case SIGTSTP: // NOLINT
150 g_signal_stop_count++;
151 break;
152
153 case SIGWINCH: // NOLINT
154 g_signal_resize_count++;
155 break;
156#endif
157
158 default:
159 break;
160 }
161}
162
163void ExecuteSignalHandlers() {
164 int signal_exit_count = g_signal_exit_count.exchange(0);
165 while (signal_exit_count--) {
166 App::Private::Signal(*g_active_screen, SIGABRT);
167 }
168
169#if !defined(_WIN32)
170 int signal_stop_count = g_signal_stop_count.exchange(0);
171 while (signal_stop_count--) {
172 App::Private::Signal(*g_active_screen, SIGTSTP);
173 }
174
175 int signal_resize_count = g_signal_resize_count.exchange(0);
176 while (signal_resize_count--) {
177 App::Private::Signal(*g_active_screen, SIGWINCH);
178 }
179#endif
180}
181
182void InstallSignalHandler(int sig) {
183 auto old_signal_handler = std::signal(sig, RecordSignal);
184 on_exit_functions.emplace(
185 [=] { std::ignore = std::signal(sig, old_signal_handler); });
186}
187
188// CSI: Control Sequence Introducer
189const std::string CSI = "\x1b["; // NOLINT
190 //
191// DCS: Device Control String
192const std::string DCS = "\x1bP"; // NOLINT
193
194// ST: String Terminator
195const std::string ST = "\x1b\\"; // NOLINT
196
197// DECRQSS: Request Status String
198// DECSCUSR: Set Cursor Style
199const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
200
201// DEC: Digital Equipment Corporation
202enum class DECMode : std::uint16_t {
203 kLineWrap = 7,
204 kCursor = 25,
205
206 kMouseX10 = 9,
207 kMouseVt200 = 1000,
208 kMouseVt200Highlight = 1001,
209
210 kMouseBtnEventMouse = 1002,
211 kMouseAnyEvent = 1003,
212
213 kMouseUtf8 = 1005,
214 kMouseSgrExtMode = 1006,
215 kMouseUrxvtMode = 1015,
216 kMouseSgrPixelsMode = 1016,
217 kAlternateScreen = 1049,
218};
219
220// Device Status Report (DSR) {
221enum class DSRMode : std::uint8_t {
222 kCursor = 6,
223};
224
225std::string Serialize(const std::vector<DECMode>& parameters) {
226 bool first = true;
227 std::string out;
228 for (const DECMode parameter : parameters) {
229 if (!first) {
230 out += ";";
231 }
232 out += std::to_string(int(parameter));
233 first = false;
234 }
235 return out;
236}
237
238// DEC Private Mode Set (DECSET)
239std::string Set(const std::vector<DECMode>& parameters) {
240 return CSI + "?" + Serialize(parameters) + "h";
241}
242
243// DEC Private Mode Reset (DECRST)
244std::string Reset(const std::vector<DECMode>& parameters) {
245 return CSI + "?" + Serialize(parameters) + "l";
246}
247
248// Device Status Report (DSR)
249std::string DeviceStatusReport(DSRMode ps) {
250 return CSI + std::to_string(int(ps)) + "n";
251}
252
253class CapturedMouseImpl : public CapturedMouseInterface {
254 public:
255 explicit CapturedMouseImpl(std::function<void(void)> callback)
256 : callback_(std::move(callback)) {}
257 ~CapturedMouseImpl() override { callback_(); }
258 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
259 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
260 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
261 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
262
263 private:
264 std::function<void(void)> callback_;
265};
266
267} // namespace
268
269App::App(Dimension dimension, int dimx, int dimy, bool use_alternative_screen)
270 : Screen(dimx, dimy),
271 dimension_(dimension),
272 use_alternative_screen_(use_alternative_screen) {
273 internal_ = std::make_unique<Internal>(
274 [&](Event event) { PostEvent(std::move(event)); });
275}
276
277// static
278App App::FixedSize(int dimx, int dimy) {
279 return {
280 Dimension::Fixed,
281 dimx,
282 dimy,
283 /*use_alternative_screen=*/false,
284 };
285}
286
287/// Create a App taking the full terminal size. This is using the
288/// alternate screen buffer to avoid messing with the terminal content.
289/// @note This is the same as `App::FullscreenAlternateScreen()`
290// static
294
295/// Create a App taking the full terminal size. The primary screen
296/// buffer is being used. It means if the terminal is resized, the previous
297/// content might mess up with the terminal content.
298// static
300 auto terminal = Terminal::Size();
301 return {
302 Dimension::Fullscreen,
303 terminal.dimx,
304 terminal.dimy,
305 /*use_alternative_screen=*/false,
306 };
307}
308
309/// Create a App taking the full terminal size. This is using the
310/// alternate screen buffer to avoid messing with the terminal content.
311// static
313 auto terminal = Terminal::Size();
314 return {
315 Dimension::Fullscreen,
316 terminal.dimx,
317 terminal.dimy,
318 /*use_alternative_screen=*/true,
319 };
320}
321
322/// Create a App whose width match the terminal output width and
323/// the height matches the component being drawn.
324// static
326 auto terminal = Terminal::Size();
327 return {
328 Dimension::TerminalOutput,
329 terminal.dimx,
330 terminal.dimy, // Best guess.
331 /*use_alternative_screen=*/false,
332 };
333}
334
335App::~App() = default;
336
337/// Create a App whose width and height match the component being
338/// drawn.
339// static
341 auto terminal = Terminal::Size();
342 return {
343 Dimension::FitComponent,
344 terminal.dimx, // Best guess.
345 terminal.dimy, // Best guess.
346 false,
347 };
348}
349
350/// @brief Set whether mouse is tracked and events reported.
351/// called outside of the main loop. E.g `App::Loop(...)`.
352/// @param enable Whether to enable mouse event tracking.
353/// @note This muse be called outside of the main loop. E.g. before calling
354/// `App::Loop`.
355/// @note Mouse tracking is enabled by default.
356/// @note Mouse tracking is only supported on terminals that supports it.
357///
358/// ### Example
359///
360/// ```cpp
361/// auto screen = App::TerminalOutput();
362/// screen.TrackMouse(false);
363/// screen.Loop(component);
364/// ```
365void App::TrackMouse(bool enable) {
366 track_mouse_ = enable;
367}
368
369/// @brief Enable or disable automatic piped input handling.
370/// When enabled, FTXUI will detect piped input and redirect stdin from /dev/tty
371/// for keyboard input, allowing applications to read piped data while still
372/// receiving interactive keyboard events.
373/// @param enable Whether to enable piped input handling. Default is true.
374/// @note This must be called before Loop().
375/// @note This feature is enabled by default.
376/// @note This feature is only available on POSIX systems (Linux/macOS).
377void App::HandlePipedInput(bool enable) {
378 handle_piped_input_ = enable;
379}
380
381/// @brief Add a task to the main loop.
382/// It will be executed later, after every other scheduled tasks.
383void App::Post(Task task) {
384 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
385 HandleTask(component_, task);
386 });
387}
388
389/// @brief Add an event to the main loop.
390/// It will be executed later, after every other scheduled events.
392 Post(event);
393}
394
395/// @brief Add a task to draw the screen one more time, until all the animations
396/// are done.
398 if (animation_requested_) {
399 return;
400 }
401 animation_requested_ = true;
402 auto now = animation::Clock::now();
403 const auto time_histeresis = std::chrono::milliseconds(33);
404 if (now - previous_animation_time_ >= time_histeresis) {
405 previous_animation_time_ = now;
406 }
407}
408
409/// @brief Try to get the unique lock about behing able to capture the mouse.
410/// @return A unique lock if the mouse is not already captured, otherwise a
411/// null.
413 if (mouse_captured) {
414 return nullptr;
415 }
416 mouse_captured = true;
417 return std::make_unique<CapturedMouseImpl>(
418 [this] { mouse_captured = false; });
419}
420
421/// @brief Execute the main loop.
422/// @param component The component to draw.
423void App::Loop(Component component) { // NOLINT
424 class Loop loop(this, std::move(component));
425 loop.Run();
426}
427
428/// @brief Return whether the main loop has been quit.
429bool App::HasQuitted() {
430 return quit_;
431}
432
433// private
434void App::PreMain() {
435 // Suspend previously active screen:
436 if (g_active_screen) {
437 std::swap(suspended_screen_, g_active_screen);
438 // Reset cursor position to the top of the screen and clear the screen.
439 suspended_screen_->TerminalSend(suspended_screen_->ResetCursorPosition());
440 suspended_screen_->ResetPosition(suspended_screen_->internal_->output_buffer,
441 /*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 App::PostMain() {
458 // Put cursor position at the end of the drawing.
459 TerminalSend(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 ResetPosition(internal_->output_buffer, /*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 }
481 std::cout << std::flush;
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.
498void App::ForceHandleCtrlC(bool force) {
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.
504void App::ForceHandleCtrlZ(bool force) {
505 force_handle_ctrl_z_ = force;
506}
507
508/// @brief Returns the content of the current selection
509std::string App::GetSelection() {
510 if (!selection_) {
511 return "";
512 }
513 return selection_->GetParts();
514}
515
516void App::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 App::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 TerminalFlush();
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([this] { TerminalFlush(); });
542
543 // Request the terminal to report the current cursor shape. We will restore it
544 // on exit.
545 TerminalSend(DECRQSS_DECSCUSR);
546 on_exit_functions.emplace([this] {
547 TerminalSend("\033[?25h"); // Enable cursor.
548 TerminalSend("\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 TerminalSend(Set(parameters));
639 on_exit_functions.emplace(
640 [this, parameters] { TerminalSend(Reset(parameters)); });
641 };
642
643 auto disable = [&](const std::vector<DECMode>& parameters) {
644 TerminalSend(Reset(parameters));
645 on_exit_functions.emplace(
646 [this, parameters] { TerminalSend(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 TerminalFlush();
670
671 quit_ = false;
672
673 PostAnimationTask();
674}
675
676void App::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 App::Uninstall() {
709 ExitNow();
710 OnExit();
711}
712
713// private
714// NOLINTNEXTLINE
715void App::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 App::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 App::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 App::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 App::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 // Hide cursor to prevent flickering during reset.
922 TerminalSend("\033[?25l");
923
924 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
925 TerminalSend(ResetCursorPosition());
926
927 if (frame_count_ != 0) {
928 // Reset the cursor position to the lower left corner to start drawing the
929 // new frame.
930 ResetPosition(internal_->output_buffer, resized);
931
932 // If the terminal width decrease, the terminal emulator will start wrapping
933 // lines and make the display dirty. We should clear it completely.
934 if ((dimx < dimx_) && !use_alternative_screen_) {
935 TerminalSend("\033[J"); // clear terminal output
936 TerminalSend("\033[H"); // move cursor to home position
937 }
938 }
939
940 // Resize the screen if needed.
941 if (resized) {
942 dimx_ = dimx;
943 dimy_ = dimy;
944 cells_ = std::vector<std::vector<Cell>>(dimy, std::vector<Cell>(dimx));
945 cursor_.x = dimx_ - 1;
946 cursor_.y = dimy_ - 1;
947 }
948
949 // Periodically request the terminal emulator the frame position relative to
950 // the screen. This is useful for converting mouse position reported in
951 // screen's coordinates to frame's coordinates.
952#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
953 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
954 // position, several output sequences are mixed together into garbage.
955 // This causes FTXUI user to see some "1;1;R" sequences into the Input
956 // component. See [issue]. Solution is to request cursor position less
957 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
958 // https://github.com/ArthurSonzogni/FTXUI/issues/136
959 static int i = -3;
960 ++i;
961 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
962 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
963 }
964#else
965 static int i = -3;
966 ++i;
967 if (!use_alternative_screen_ &&
968 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
969 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
970 }
971#endif
972 previous_frame_resized_ = resized;
973
974 selection_ = selection_data_.empty
975 ? std::make_unique<Selection>()
976 : std::make_unique<Selection>(
977 selection_data_.start_x, selection_data_.start_y, //
978 selection_data_.end_x, selection_data_.end_y);
979 Render(*this, document.get(), *selection_);
980
981 // Set cursor position for user using tools to insert CJK characters.
982 {
983 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
984 const int dy = dimy_ - 1 - cursor_.y;
985
986 set_cursor_position_.clear();
987 reset_cursor_position_.clear();
988
989 if (dy != 0) {
990 set_cursor_position_ += "\x1B[" + std::to_string(dy) + "A";
991 reset_cursor_position_ += "\x1B[" + std::to_string(dy) + "B";
992 }
993
994 if (dx != 0) {
995 set_cursor_position_ += "\x1B[" + std::to_string(dx) + "D";
996 reset_cursor_position_ += "\x1B[" + std::to_string(dx) + "C";
997 }
998
999 if (cursor_.shape != Cursor::Hidden) {
1000 set_cursor_position_ += "\033[?25h";
1001 set_cursor_position_ +=
1002 "\033[" + std::to_string(int(cursor_.shape)) + " q";
1003 }
1004 }
1005
1006 ToString(internal_->output_buffer);
1007 TerminalSend(set_cursor_position_);
1008 TerminalFlush();
1009
1010 Clear();
1011 frame_valid_ = true;
1012 frame_count_++;
1013}
1014
1015// private
1016std::string App::ResetCursorPosition() {
1017 std::string result = std::move(reset_cursor_position_);
1018 reset_cursor_position_= "";
1019 return result;
1020}
1021
1022// private
1023void App::TerminalSend(std::string_view s) {
1024 internal_->output_buffer += s;
1025}
1026
1027// private
1028void App::TerminalFlush() {
1029 // Emscripten doesn't implement flush. We interpret zero as flush.
1030 internal_->output_buffer += '\0';
1031 std::cout << internal_->output_buffer << std::flush;
1032 internal_->output_buffer.clear();
1033}
1034
1035/// @brief Return a function to exit the main loop.
1037 return [this] { Exit(); };
1038}
1039
1040/// @brief Exit the main loop.
1042 Post([this] { ExitNow(); });
1043}
1044
1045// private:
1046void App::ExitNow() {
1047 quit_ = true;
1048}
1049
1050// private:
1051void App::Signal(int signal) {
1052 if (signal == SIGABRT) {
1053 Exit();
1054 return;
1055 }
1056
1057// Windows do no support SIGTSTP / SIGWINCH
1058#if !defined(_WIN32)
1059 if (signal == SIGTSTP) {
1060 Post([&] {
1061 TerminalSend(ResetCursorPosition());
1062 ResetPosition(internal_->output_buffer, /*clear*/ true);
1063 Uninstall();
1064 dimx_ = 0;
1065 dimy_ = 0;
1066 std::raise(SIGTSTP);
1067 Install();
1068 });
1069 return;
1070 }
1071
1072 if (signal == SIGWINCH) {
1073 Post(Event::Special({0}));
1074 return;
1075 }
1076#endif
1077}
1078
1079void App::FetchTerminalEvents() {
1080#if defined(_WIN32)
1081 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1082 // Check if there is input in the console.
1083 auto console = GetStdHandle(STD_INPUT_HANDLE);
1084 DWORD number_of_events = 0;
1085 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1086 return std::vector<INPUT_RECORD>();
1087 }
1088 if (number_of_events <= 0) {
1089 // No input, return.
1090 return std::vector<INPUT_RECORD>();
1091 }
1092 // Read the input events.
1093 std::vector<INPUT_RECORD> records(number_of_events);
1094 DWORD number_of_events_read = 0;
1095 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1096 &number_of_events_read)) {
1097 return std::vector<INPUT_RECORD>();
1098 }
1099 records.resize(number_of_events_read);
1100 return records;
1101 };
1102
1103 auto records = get_input_records();
1104 if (records.size() == 0) {
1105 const auto timeout =
1106 std::chrono::steady_clock::now() - internal_->last_char_time;
1107 const size_t timeout_microseconds =
1108 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1109 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1110 return;
1111 }
1112 internal_->last_char_time = std::chrono::steady_clock::now();
1113
1114 // Convert the input events to FTXUI events.
1115 // For each event, we call the terminal input parser to convert it to
1116 // Event.
1117 std::wstring wstring;
1118 for (const auto& r : records) {
1119 switch (r.EventType) {
1120 case KEY_EVENT: {
1121 auto key_event = r.Event.KeyEvent;
1122 // ignore UP key events
1123 if (key_event.bKeyDown == FALSE) {
1124 continue;
1125 }
1126 const wchar_t wc = key_event.uChar.UnicodeChar;
1127 wstring += wc;
1128 if (wc >= 0xd800 && wc <= 0xdbff) {
1129 // Wait for the Low Surrogate to arrive in the next record.
1130 continue;
1131 }
1132 for (auto it : to_string(wstring)) {
1133 internal_->terminal_input_parser.Add(it);
1134 }
1135 wstring.clear();
1136 } break;
1137 case WINDOW_BUFFER_SIZE_EVENT:
1138 Post(Event::Special({0}));
1139 break;
1140 case MENU_EVENT:
1141 case FOCUS_EVENT:
1142 case MOUSE_EVENT:
1143 // TODO(mauve): Implement later.
1144 break;
1145 }
1146 }
1147#elif defined(__EMSCRIPTEN__)
1148 // Read chars from the terminal.
1149 // We configured it to be non blocking.
1150 std::array<char, 128> out{};
1151 size_t l = read(STDIN_FILENO, out.data(), out.size());
1152 if (l == 0) {
1153 const auto timeout =
1154 std::chrono::steady_clock::now() - internal_->last_char_time;
1155 const size_t timeout_microseconds =
1156 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1157 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1158 return;
1159 }
1160 internal_->last_char_time = std::chrono::steady_clock::now();
1161
1162 // Convert the chars to events.
1163 for (size_t i = 0; i < l; ++i) {
1164 internal_->terminal_input_parser.Add(out[i]);
1165 }
1166#else // POSIX (Linux & Mac)
1167 if (!CheckStdinReady(tty_fd_)) {
1168 const auto timeout =
1169 std::chrono::steady_clock::now() - internal_->last_char_time;
1170 const size_t timeout_ms =
1171 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1172 internal_->terminal_input_parser.Timeout(timeout_ms);
1173 return;
1174 }
1175 internal_->last_char_time = std::chrono::steady_clock::now();
1176
1177 // Read chars from the terminal.
1178 std::array<char, 128> out{};
1179 size_t l = read(tty_fd_, out.data(), out.size());
1180
1181 // Convert the chars to events.
1182 for (size_t i = 0; i < l; ++i) {
1183 internal_->terminal_input_parser.Add(out[i]);
1184 }
1185#endif
1186}
1187
1188void App::PostAnimationTask() {
1189 Post(AnimationTask());
1190
1191 // Repeat the animation task every 15ms. This correspond to a frame rate
1192 // of around 66fps.
1193 internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1194 std::chrono::milliseconds(15));
1195}
1196
1197bool App::SelectionData::operator==(const App::SelectionData& other) const {
1198 if (empty && other.empty) {
1199 return true;
1200 }
1201 if (empty || other.empty) {
1202 return false;
1203 }
1204 return start_x == other.start_x && start_y == other.start_y &&
1205 end_x == other.end_x && end_y == other.end_y;
1206}
1207
1208bool App::SelectionData::operator!=(const App::SelectionData& other) const {
1209 return !(*this == other);
1210}
1211
1212} // namespace ftxui
static void Signal(App &s, int signal)
Definition app.hpp:182
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
void HandlePipedInput(bool enable=true)
Enable or disable automatic piped input handling. When enabled, FTXUI will detect piped input and red...
Definition app.cpp:377
void Exit()
Exit the main loop.
Definition app.cpp:1041
static const Event CtrlZ
Definition event.hpp:96
void PostEvent(Event event)
Add an event to the main loop. It will be executed later, after every other scheduled events.
Definition app.cpp:391
static App FitComponent()
Definition app.cpp:340
~App() override
static App * Active()
Return the currently active screen, or null if none.
Definition app.cpp:522
void Post(Task task)
Add a task to the main loop. It will be executed later, after every other scheduled tasks.
Definition app.cpp:383
static Event Special(std::string_view)
An custom event whose meaning is defined by the user of the library.
Definition event.cpp:74
static App FullscreenPrimaryScreen()
Definition app.cpp:299
static App Fullscreen()
Definition app.cpp:291
static const Event Custom
Definition event.hpp:99
friend class Loop
Definition app.hpp:175
static App TerminalOutput()
Definition app.cpp:325
static App FullscreenAlternateScreen()
Definition app.cpp:312
CapturedMouse CaptureMouse()
Try to get the unique lock about behing able to capture the mouse.
Definition app.cpp:412
static App FixedSize(int dimx, int dimy)
Definition app.cpp:278
std::string GetSelection()
Returns the content of the current selection.
Definition app.cpp:509
void TrackMouse(bool enable=true)
Set whether mouse is tracked and events reported. called outside of the main loop....
Definition app.cpp:365
void SelectionChange(std::function< void()> callback)
Definition app.cpp:516
void RequestAnimationFrame()
Add a task to draw the screen one more time, until all the animations are done.
Definition app.cpp:397
Closure ExitLoopClosure()
Return a function to exit the main loop.
Definition app.cpp:1036
void ForceHandleCtrlC(bool force)
Force FTXUI to handle or not handle Ctrl-C, even if the component catches the Event::CtrlC.
Definition app.cpp:498
void ForceHandleCtrlZ(bool force)
Force FTXUI to handle or not handle Ctrl-Z, even if the component catches the Event::CtrlZ.
Definition app.cpp:504
Closure WithRestoredIO(Closure)
Decorate a function. It executes the same way, but with the currently active screen terminal hooks te...
Definition app.cpp:488
App is a Screen that can handle events, run a main loop, and manage components.
Definition app.hpp:34
Loop is a class that manages the event loop for a component.
Definition loop.hpp:56
void RequestAnimationFrame()
RequestAnimationFrame is a function that requests a new frame to be drawn in the next animation cycle...
Definition app.cpp:59
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 surface.hpp:40
std::string ToString() const
Definition screen.cpp:426
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:497
Cursor cursor_
Definition screen.hpp:81
void Clear()
Clear all the cells from the screen.
Definition screen.cpp:525
std::vector< std::vector< Cell > > cells_
Definition surface.hpp:50
int dimx() const
Definition surface.hpp:39
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:1593
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
Definition app.hpp:24