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 any_of, 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 <type_traits>
24#include <utility> // for move, swap
25#include <variant> // for visit, variant
26#include <vector> // for vector
27#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
28#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
29#include "ftxui/component/component_base.hpp" // for ComponentBase
30#include "ftxui/component/event.hpp" // for Event
31#include "ftxui/component/loop.hpp" // for Loop
34#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
35#include "ftxui/dom/node.hpp" // for Node, Render
36#include "ftxui/screen/cell.hpp" // for Cell
37#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
38#include "ftxui/screen/util.hpp" // for util::clamp
39#include "ftxui/util/autoreset.hpp" // for AutoReset
40
41#if defined(_WIN32)
42#define DEFINE_CONSOLEV2_PROPERTIES
43#define WIN32_LEAN_AND_MEAN
44#ifndef NOMINMAX
45#define NOMINMAX
46#endif
47#include <io.h>
48#include <windows.h>
49#ifndef UNICODE
50#error Must be compiled in UNICODE mode
51#endif
52#else
53#include <fcntl.h>
54#include <poll.h>
55#include <sys/poll.h>
56#include <sys/types.h>
57#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
58#include <unistd.h> // for STDIN_FILENO, STDOUT_FILENO, read
59#endif
60
61namespace ftxui {
62
63namespace animation {
65 auto* screen = App::Active();
66 if (screen) {
67 screen->RequestAnimationFrame();
68 }
69}
70} // namespace animation
71
72class ThrottledRequest {
73 public:
74 ThrottledRequest(App* app,
75 std::function<void()> send,
76 task::TaskRunner& task_runner)
77 : app_(app), send_(std::move(send)), task_runner_(&task_runner) {}
78
79 void Request(bool force = false) {
80 if (!app_->is_stdin_a_tty_) {
81 return;
82 }
83
84 if (force) {
85 Send();
86 return;
87 }
88
89 // Allow only one pending request at a time. This is to avoid flooding the
90 // terminal with requests.
91 if (HasPending()) {
92 return;
93 }
94
95 const auto now = std::chrono::steady_clock::now();
96 const auto delta = now - last_request_time_;
97 const auto delay = std::chrono::milliseconds(500) - delta;
98
99 if (delay <= std::chrono::milliseconds(0)) {
100 Send();
101 return;
102 }
103
104 request_queued_ = true;
105 task_runner_->PostDelayedTask(
106 [this] {
107 request_queued_ = false;
108 Request();
109 },
110 delay);
111 }
112
113 void OnReply() { pending_request_ = false; }
114
115 bool HasPending() const {
116 if (pending_request_) {
117 const auto now = std::chrono::steady_clock::now();
118 if (now - last_sent_time_ < std::chrono::seconds(5)) {
119 return true;
120 }
121 }
122 return request_queued_;
123 }
124
125 private:
126 void Send() {
127 last_sent_time_ = std::chrono::steady_clock::now();
128 pending_request_ = true;
129 send_();
130 }
131
132 App* app_;
133 std::function<void()> send_;
134 task::TaskRunner* task_runner_;
135 bool pending_request_ = false;
136 std::chrono::steady_clock::time_point last_request_time_ =
137 std::chrono::steady_clock::now() - std::chrono::hours(1);
138 std::chrono::steady_clock::time_point last_sent_time_ =
139 std::chrono::steady_clock::now() - std::chrono::hours(1);
140 bool request_queued_ = false;
141};
142
143struct App::Internal {
144 // Convert char to Event.
145 TerminalInputParser terminal_input_parser;
146
147 task::TaskRunner task_runner;
148
149 // The last time a character was received.
150 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
151 std::chrono::steady_clock::now();
152
153 // The buffer used to output the screen to the terminal.
154 // Unlike for std::vector::clear, the C++ standard does not explicitly require
155 // that capacity is unchanged by this function, but existing implementations
156 // do not change capacity. This means that they do not release the allocated
157 // memory (see also shrink_to_fit).
158 std::string output_buffer;
159
160 ThrottledRequest cursor_position_request;
161
162 MultiReceiverBuffer<Event> event_buffer;
163 std::unique_ptr<MultiReceiverBuffer<Event>::Receiver> setup_receiver;
164 std::unique_ptr<MultiReceiverBuffer<Event>::Receiver> main_loop_receiver;
165
166 explicit Internal(App* app, std::function<void(Event)> out);
167};
168
169namespace {
170
171App* g_active_screen = nullptr; // NOLINT
172
173std::stack<Closure> on_exit_functions; // NOLINT
174
175void OnExit() {
176 while (!on_exit_functions.empty()) {
177 on_exit_functions.top()();
178 on_exit_functions.pop();
179 }
180}
181
182#if defined(_WIN32)
183
184#elif defined(__EMSCRIPTEN__)
185#include <emscripten.h>
186
187extern "C" {
188EMSCRIPTEN_KEEPALIVE
189void ftxui_on_resize(int columns, int rows) {
191 columns,
192 rows,
193 });
194 std::raise(SIGWINCH);
195}
196}
197
198#else // POSIX (Linux & Mac)
199
200#endif
201
202std::atomic<int> g_signal_exit_count = 0; // NOLINT
203#if !defined(_WIN32)
204std::atomic<int> g_signal_stop_count = 0; // NOLINT
205std::atomic<int> g_signal_resize_count = 0; // NOLINT
206#endif
207
208// Async signal safe function
209void RecordSignal(int signal) {
210 switch (signal) {
211 case SIGABRT:
212 case SIGFPE:
213 case SIGILL:
214 case SIGINT:
215 case SIGSEGV:
216 case SIGTERM:
217 g_signal_exit_count++;
218 break;
219
220#if !defined(_WIN32)
221 case SIGTSTP: // NOLINT
222 g_signal_stop_count++;
223 break;
224
225 case SIGWINCH: // NOLINT
226 g_signal_resize_count++;
227 break;
228#endif
229
230 default:
231 break;
232 }
233}
234
235void ExecuteSignalHandlers() {
236 int signal_exit_count = g_signal_exit_count.exchange(0);
237 while (signal_exit_count--) {
238 App::Private::Signal(*g_active_screen, SIGABRT);
239 }
240
241#if !defined(_WIN32)
242 int signal_stop_count = g_signal_stop_count.exchange(0);
243 while (signal_stop_count--) {
244 App::Private::Signal(*g_active_screen, SIGTSTP);
245 }
246
247 int signal_resize_count = g_signal_resize_count.exchange(0);
248 while (signal_resize_count--) {
249 App::Private::Signal(*g_active_screen, SIGWINCH);
250 }
251#endif
252}
253
254void InstallSignalHandler(int sig) {
255 auto old_signal_handler = std::signal(sig, RecordSignal);
256 on_exit_functions.emplace(
257 [=] { std::ignore = std::signal(sig, old_signal_handler); });
258}
259
260// CSI: Control Sequence Introducer
261const std::string CSI = "\x1b["; // NOLINT
262 //
263// DCS: Device Control String
264const std::string DCS = "\x1bP"; // NOLINT
265
266// ST: String Terminator
267const std::string ST = "\x1b\\"; // NOLINT
268
269// DECRQSS: Request Status String
270// DECSCUSR: Set Cursor Style
271const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
272
273// DEC: Digital Equipment Corporation
274enum class DECMode : std::uint16_t {
275 kLineWrap = 7,
276 kCursor = 25,
277
278 kMouseX10 = 9,
279 kMouseVt200 = 1000,
280 kMouseVt200Highlight = 1001,
281
282 kMouseBtnEventMouse = 1002,
283 kMouseAnyEvent = 1003,
284
285 kMouseUtf8 = 1005,
286 kMouseSgrExtMode = 1006,
287 kMouseUrxvtMode = 1015,
288 kMouseSgrPixelsMode = 1016,
289 kAlternateScreen = 1049,
290};
291
292// Device Status Report (DSR) {
293enum class DSRMode : std::uint8_t {
294 kCursor = 6,
295};
296
297std::string Serialize(const std::vector<DECMode>& parameters) {
298 bool first = true;
299 std::string out;
300 for (const DECMode parameter : parameters) {
301 if (!first) {
302 out += ";";
303 }
304 out += std::to_string(int(parameter));
305 first = false;
306 }
307 return out;
308}
309
310// DEC Private Mode Set (DECSET)
311std::string Set(const std::vector<DECMode>& parameters) {
312 return CSI + "?" + Serialize(parameters) + "h";
313}
314
315// DEC Private Mode Reset (DECRST)
316std::string Reset(const std::vector<DECMode>& parameters) {
317 return CSI + "?" + Serialize(parameters) + "l";
318}
319
320// Device Status Report (DSR)
321std::string DeviceStatusReport(DSRMode ps) {
322 return CSI + std::to_string(int(ps)) + "n";
323}
324
325class CapturedMouseImpl : public CapturedMouseInterface {
326 public:
327 explicit CapturedMouseImpl(std::function<void(void)> callback)
328 : callback_(std::move(callback)) {}
329 ~CapturedMouseImpl() override { callback_(); }
330 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
331 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
332 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
333 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
334
335 private:
336 std::function<void(void)> callback_;
337};
338
339} // namespace
340
341App::Internal::Internal(App* app, std::function<void(Event)> out)
342 : terminal_input_parser(std::move(out)),
343 cursor_position_request(
344 app,
345 [app] { app->TerminalSend(DeviceStatusReport(DSRMode::kCursor)); },
346 task_runner) {
347 setup_receiver = event_buffer.CreateReceiver();
348 main_loop_receiver = event_buffer.CreateReceiver();
349}
350
351App::App(Dimension dimension, int dimx, int dimy, bool use_alternative_screen)
352 : Screen(dimx, dimy),
353 dimension_(dimension),
354 use_alternative_screen_(use_alternative_screen) {
355 internal_ = std::make_unique<Internal>(this, [&](Event event) {
356 internal_->event_buffer.Push(std::move(event));
358 });
359}
360
361// static
362App App::FixedSize(int dimx, int dimy) {
363 return {
364 Dimension::Fixed,
365 dimx,
366 dimy,
367 /*use_alternative_screen=*/false,
368 };
369}
370
371/// Create a App taking the full terminal size. This is using the
372/// alternate screen buffer to avoid messing with the terminal content.
373/// @note This is the same as `App::FullscreenAlternateScreen()`
374// static
378
379/// Create a App taking the full terminal size. The primary screen
380/// buffer is being used. It means if the terminal is resized, the previous
381/// content might mess up with the terminal content.
382// static
384 auto terminal = Terminal::Size();
385 return {
386 Dimension::Fullscreen,
387 terminal.dimx,
388 terminal.dimy,
389 /*use_alternative_screen=*/false,
390 };
391}
392
393/// Create a App taking the full terminal size. This is using the
394/// alternate screen buffer to avoid messing with the terminal content.
395// static
397 auto terminal = Terminal::Size();
398 return {
399 Dimension::Fullscreen,
400 terminal.dimx,
401 terminal.dimy,
402 /*use_alternative_screen=*/true,
403 };
404}
405
406/// Create a App whose width match the terminal output width and
407/// the height matches the component being drawn.
408// static
410 auto terminal = Terminal::Size();
411 return {
412 Dimension::TerminalOutput,
413 terminal.dimx,
414 terminal.dimy, // Best guess.
415 /*use_alternative_screen=*/false,
416 };
417}
418
419App::~App() = default;
420
421/// Create a App whose width and height match the component being
422/// drawn.
423// static
425 auto terminal = Terminal::Size();
426 return {
427 Dimension::FitComponent,
428 terminal.dimx, // Best guess.
429 terminal.dimy, // Best guess.
430 false,
431 };
432}
433
434/// @brief Set whether mouse is tracked and events reported.
435/// called outside of the main loop. E.g `App::Loop(...)`.
436/// @param enable Whether to enable mouse event tracking.
437/// @note This muse be called outside of the main loop. E.g. before calling
438/// `App::Loop`.
439/// @note Mouse tracking is enabled by default.
440/// @note Mouse tracking is only supported on terminals that supports it.
441///
442/// ### Example
443///
444/// ```cpp
445/// auto screen = App::TerminalOutput();
446/// screen.TrackMouse(false);
447/// screen.Loop(component);
448/// ```
449void App::TrackMouse(bool enable) {
450 track_mouse_ = enable;
451}
452
453/// @brief Enable or disable automatic piped input handling.
454/// When enabled, FTXUI will detect piped input and redirect stdin from /dev/tty
455/// for keyboard input, allowing applications to read piped data while still
456/// receiving interactive keyboard events.
457/// @param enable Whether to enable piped input handling. Default is true.
458/// @note This must be called before Loop().
459/// @note This feature is enabled by default.
460/// @note This feature is only available on POSIX systems (Linux/macOS).
461void App::HandlePipedInput(bool enable) {
462 handle_piped_input_ = enable;
463}
464
465/// @brief Add a task to the main loop.
466/// It will be executed later, after every other scheduled tasks.
467void App::Post(Task task) {
468 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
469 if (component_) {
470 HandleTask(component_, task);
471 return;
472 }
473
474 // If there is no component, we can still execute closures.
475 if (std::holds_alternative<Closure>(task)) {
476 std::get<Closure>(task)();
477 }
478 });
479}
480
481/// @brief Add an event to the main loop.
482/// It will be executed later, after every other scheduled events.
484 internal_->event_buffer.Push(std::move(event));
486}
487
488/// @brief Add a task to draw the screen one more time, until all the animations
489/// are done.
491 if (animation_requested_) {
492 return;
493 }
494 animation_requested_ = true;
495 auto now = animation::Clock::now();
496 const auto time_histeresis = std::chrono::milliseconds(33);
497 if (now - previous_animation_time_ >= time_histeresis) {
498 previous_animation_time_ = now;
499 }
500}
501
502/// @brief Try to get the unique lock about being able to capture the mouse.
503/// @return A unique lock if the mouse is not already captured, otherwise a
504/// null.
506 if (mouse_captured) {
507 return nullptr;
508 }
509 mouse_captured = true;
510 return std::make_unique<CapturedMouseImpl>(
511 [this] { mouse_captured = false; });
512}
513
514/// @brief Execute the main loop.
515/// @param component The component to draw.
516void App::Loop(Component component) { // NOLINT
517 class Loop loop(this, std::move(component));
518 loop.Run();
519}
520
521/// @brief Return whether the main loop has been quit.
522bool App::HasQuitted() {
523 return quit_;
524}
525
526// private
527void App::PreMain() {
528 // Suspend previously active screen:
529 if (g_active_screen) {
530 std::swap(suspended_screen_, g_active_screen);
531 // Reset cursor position to the top of the screen and clear the screen.
532 suspended_screen_->TerminalSend(suspended_screen_->ResetCursorPosition());
533 suspended_screen_->ResetPosition(
534 suspended_screen_->internal_->output_buffer,
535 /*clear=*/true);
536 suspended_screen_->dimx_ = 0;
537 suspended_screen_->dimy_ = 0;
538
539 // Reset dimensions to force drawing the screen again next time:
540 suspended_screen_->Uninstall();
541 }
542
543 // This screen is now active:
544 g_active_screen = this;
545 g_active_screen->Install();
546
547 previous_animation_time_ = animation::Clock::now();
548}
549
550// private
551void App::PostMain() {
552 // Put cursor position at the end of the drawing.
553 TerminalSend(ResetCursorPosition());
554
555 g_active_screen = nullptr;
556
557 // Restore suspended screen.
558 if (suspended_screen_) {
559 // Clear screen, and put the cursor at the beginning of the drawing.
560 ResetPosition(internal_->output_buffer, /*clear=*/true);
561 dimx_ = 0;
562 dimy_ = 0;
563 Uninstall();
564 std::swap(g_active_screen, suspended_screen_);
565 g_active_screen->Install();
566 } else {
567 Uninstall();
568
569 std::cout << "\r";
570 // On final exit, keep the current drawing and reset cursor position one
571 // line after it.
572 if (!use_alternative_screen_) {
573 std::cout << "\n";
574 }
575 std::cout << std::flush;
576 }
577}
578
579/// @brief Decorate a function. It executes the same way, but with the currently
580/// active screen terminal hooks temporarily uninstalled during its execution.
581/// @param fn The function to decorate.
583 return [this, fn] {
584 Uninstall();
585 fn();
586 Install();
587 };
588}
589
590/// @brief Force FTXUI to handle or not handle Ctrl-C, even if the component
591/// catches the Event::CtrlC.
592void App::ForceHandleCtrlC(bool force) {
593 force_handle_ctrl_c_ = force;
594}
595
596/// @brief Force FTXUI to handle or not handle Ctrl-Z, even if the component
597/// catches the Event::CtrlZ.
598void App::ForceHandleCtrlZ(bool force) {
599 force_handle_ctrl_z_ = force;
600}
601
602/// @brief Returns the content of the current selection
603std::string App::GetSelection() {
604 if (!selection_) {
605 return "";
606 }
607 return selection_->GetParts();
608}
609
610void App::SelectionChange(std::function<void()> callback) {
611 selection_on_change_ = std::move(callback);
612}
613
614/// @brief Return the currently active screen, or null if none.
615// static
617 return g_active_screen;
618}
619
620// private
621void App::Install() {
622 frame_valid_ = false;
623
624 // Flush the buffer for stdout to ensure whatever the user has printed before
625 // is fully applied before we start modifying the terminal configuration. This
626 // is important, because we are using two different channels (stdout vs
627 // termios/WinAPI) to communicate with the terminal emulator below. See
628 // https://github.com/ArthurSonzogni/FTXUI/issues/846
629 TerminalFlush();
630
631 InstallPipedInputHandling();
632
633 // After uninstalling the new configuration, flush it to the terminal to
634 // ensure it is fully applied:
635 on_exit_functions.emplace([this] { TerminalFlush(); });
636
637 // Install signal handlers to restore the terminal state on exit. The default
638 // signal handlers are restored on exit.
639 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
640 InstallSignalHandler(signal);
641 }
642
643// Save the old terminal configuration and restore it on exit.
644#if defined(_WIN32)
645 // Enable VT processing on stdout and stdin
646 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
647 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
648
649 DWORD out_mode = 0;
650 DWORD in_mode = 0;
651 GetConsoleMode(stdout_handle, &out_mode);
652 GetConsoleMode(stdin_handle, &in_mode);
653 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
654 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
655
656 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
657 const int enable_virtual_terminal_processing = 0x0004;
658 const int disable_newline_auto_return = 0x0008;
659 out_mode |= enable_virtual_terminal_processing;
660 out_mode |= disable_newline_auto_return;
661
662 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
663 const int enable_line_input = 0x0002;
664 const int enable_echo_input = 0x0004;
665 const int enable_virtual_terminal_input = 0x0200;
666 const int enable_window_input = 0x0008;
667 in_mode &= ~enable_echo_input;
668 in_mode &= ~enable_line_input;
669 in_mode |= enable_virtual_terminal_input;
670 in_mode |= enable_window_input;
671
672 SetConsoleMode(stdin_handle, in_mode);
673 SetConsoleMode(stdout_handle, out_mode);
674#else // POSIX (Linux & Mac)
675 // #if defined(__EMSCRIPTEN__)
676 //// Reading stdin isn't blocking.
677 // int flags = fcntl(0, F_GETFL, 0);
678 // fcntl(0, F_SETFL, flags | O_NONBLOCK);
679
680 //// Restore the terminal configuration on exit.
681 // on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
682 // #endif
683 for (const int signal : {SIGWINCH, SIGTSTP}) {
684 InstallSignalHandler(signal);
685 }
686
687 struct termios terminal; // NOLINT
688 tcgetattr(tty_fd_, &terminal);
689 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
690 tcsetattr(tty_fd_, TCSANOW, &terminal);
691 });
692
693 // Enabling raw terminal input mode
694 terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition
695 terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be
696 // flushed
697 terminal.c_iflag &= ~PARMRK; // Disable marking parity errors.
698 terminal.c_iflag &= ~ISTRIP; // Disable striping 8th bit off characters.
699 terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR.
700 terminal.c_iflag &= ~IGNCR; // Disable ignoring CR.
701 terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL.
702 terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output
703
704 terminal.c_lflag &= ~ECHO; // Disable echoing input characters.
705 terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters.
706 terminal.c_lflag &= ~ICANON; // Disable Canonical mode.
707 terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting:
708 // - => DSUSP
709 // - C-Z => SUSP
710 // - C-C => INTR
711 // - C-d => QUIT
712 terminal.c_lflag &= ~IEXTEN; // Disable extended input processing
713 terminal.c_cflag |= CS8; // 8 bits per byte
714
715 terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical
716 // read.
717 terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read.
718
719 tcsetattr(tty_fd_, TCSANOW, &terminal);
720
721#endif
722
723 auto enable = [&](const std::vector<DECMode>& parameters) {
724 TerminalSend(Set(parameters));
725 on_exit_functions.emplace(
726 [this, parameters] { TerminalSend(Reset(parameters)); });
727 };
728
729 auto disable = [&](const std::vector<DECMode>& parameters) {
730 TerminalSend(Reset(parameters));
731 on_exit_functions.emplace(
732 [this, parameters] { TerminalSend(Set(parameters)); });
733 };
734
735 if (use_alternative_screen_) {
736 enable({
737 DECMode::kAlternateScreen,
738 });
739 }
740
741 disable({
742 // DECMode::kCursor,
743 DECMode::kLineWrap,
744 });
745
746 if (track_mouse_) {
747 enable({DECMode::kMouseVt200});
748 enable({DECMode::kMouseAnyEvent});
749 enable({DECMode::kMouseUrxvtMode});
750 enable({DECMode::kMouseSgrExtMode});
751 }
752
753 // After installing the new configuration, flush it to the terminal to
754 // ensure it is fully applied:
755 TerminalFlush();
756
757 InstallTerminalInfo();
758
759 quit_ = false;
760
761 PostAnimationTask();
762
763 installed_ = true;
764}
765
766void App::InstallPipedInputHandling() {
767 is_stdin_a_tty_ = false;
768 is_stdout_a_tty_ = false;
769#if defined(__EMSCRIPTEN__)
770 is_stdin_a_tty_ = true;
771 is_stdout_a_tty_ = true;
772#elif defined(_WIN32)
773 is_stdin_a_tty_ = _isatty(_fileno(stdin));
774 is_stdout_a_tty_ = _isatty(_fileno(stdout));
775#else
776 tty_fd_ = STDIN_FILENO;
777 is_stdout_a_tty_ = isatty(STDOUT_FILENO);
778 // Handle piped input redirection if explicitly enabled by the application.
779 // This allows applications to read data from stdin while still receiving
780 // keyboard input from the terminal for interactive use.
781 if (!handle_piped_input_) {
782 is_stdin_a_tty_ = isatty(STDIN_FILENO);
783 } else if (isatty(STDIN_FILENO)) {
784 is_stdin_a_tty_ = true;
785 } else {
786 // Open /dev/tty for keyboard input.
787 tty_fd_ = open("/dev/tty", O_RDONLY); // NOLINT
788 if (tty_fd_ < 0) {
789 // Failed to open /dev/tty (containers, headless systems, etc.)
790 tty_fd_ = STDIN_FILENO; // Fallback to stdin.
791 is_stdin_a_tty_ = isatty(STDIN_FILENO);
792 } else {
793 is_stdin_a_tty_ = true;
794 // Close the /dev/tty file descriptor on exit.
795 on_exit_functions.emplace([this] {
796 close(tty_fd_);
797 tty_fd_ = -1;
798 });
799 }
800 }
801#endif
802}
803
804/// @brief Return the names of the terminal capabilities.
805std::vector<std::string> App::TerminalCapabilityNames() const {
806 return Event::TerminalCapabilities("", terminal_capabilities_)
807 .TerminalCapabilityNames();
808}
809
810/// @brief Return the terminal name.
811const std::string& App::TerminalName() const {
812 return terminal_name_;
813}
814
815/// @brief Return the terminal version.
817 return terminal_version_;
818}
819
820/// @brief Return the terminal emulator name.
821const std::string& App::TerminalEmulatorName() const {
822 return terminal_emulator_name_;
823}
824
825/// @brief Return the terminal emulator version.
826const std::string& App::TerminalEmulatorVersion() const {
827 return terminal_emulator_version_;
828}
829
830/// @brief Return the terminal capabilities.
831const std::vector<int>& App::TerminalCapabilities() const {
832 return terminal_capabilities_;
833}
834
835// NOLINTNEXTLINE
836void App::InstallTerminalInfo() {
837 // Request the terminal to report the current cursor shape. We will restore it
838 // on exit.
839 if (is_stdout_a_tty_) {
840 TerminalSend(DECRQSS_DECSCUSR);
841 TerminalSend("\033[c"); // DA1
842 TerminalSend("\033[>c"); // DA2
843 TerminalSend("\033[>q"); // XTVERSION
844 TerminalFlush();
845 }
846
847 int cursor_reset_shape = 1;
848
849 // Wait for the cursor shape reply using the setup head.
850 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
851 auto start = std::chrono::steady_clock::now();
852 bool cursor_shape_received = false;
853 bool da1_received = false;
854 bool da2_received = false;
855 bool xtversion_received = false;
856 // Wait for the cursor shape reply using the setup head.
857 while (true) {
858 FetchTerminalEvents();
859 while (internal_->setup_receiver->Has()) {
860 const auto event = internal_->setup_receiver->Pop();
861 if (event.is_cursor_shape()) {
862 cursor_reset_shape = event.cursor_shape();
863 cursor_shape_received = true;
864 }
865 if (event.IsTerminalCapabilities()) {
866 terminal_capabilities_ = event.TerminalCapabilities();
867 da1_received = true;
868 }
869 if (event.IsTerminalNameVersion()) {
870 terminal_name_ = event.TerminalName();
871 terminal_version_ = event.TerminalVersion();
872 da2_received = true;
873 }
874 if (event.IsTerminalEmulator()) {
875 terminal_emulator_name_ = event.TerminalEmulatorName();
876 terminal_emulator_version_ = event.TerminalEmulatorVersion();
877 xtversion_received = true;
878 }
879 }
880
881 if (cursor_shape_received && da1_received && da2_received) {
882 break;
883 }
884
885 if (std::chrono::steady_clock::now() - start >
886 std::chrono::milliseconds(1000)) {
887 break;
888 }
889 std::this_thread::sleep_for(std::chrono::milliseconds(10));
890 }
891
892 // If we received DA1, DA2 and cursor shape, but not XTVERSION yet,
893 // wait a tiny bit more for XTVERSION as it is usually sent last.
894 if (!xtversion_received && cursor_shape_received && da1_received &&
895 da2_received) {
896 for (int i = 0; i < 10; ++i) {
897 FetchTerminalEvents();
898 while (internal_->setup_receiver->Has()) {
899 const auto event = internal_->setup_receiver->Pop();
900 if (event.IsTerminalEmulator()) {
901 terminal_emulator_name_ = event.TerminalEmulatorName();
902 terminal_emulator_version_ = event.TerminalEmulatorVersion();
903 xtversion_received = true;
904 }
905 }
906 if (xtversion_received) {
907 break;
908 }
909 std::this_thread::sleep_for(std::chrono::milliseconds(10));
910 }
911 }
912 }
913
914 // Set quirks and color support based on terminal identification.
915 Terminal::Quirks quirks = Terminal::GetQuirks();
916
917 const bool is_modern_emulator = (TerminalEmulatorName() != "unknown");
918 const bool is_urxvt = (TerminalName() == "urxvt");
919 const bool is_vt220_plus =
920 (TerminalName() != "vt100" && TerminalName() != "unknown");
921 bool reports_utf8 = false;
922 for (const int x : terminal_capabilities_) {
923 if (x == 42) {
924 reports_utf8 = true;
925 break;
926 }
927 }
928 bool reports_color = false;
929 for (const int x : terminal_capabilities_) {
930 if (x == 22) {
931 reports_color = true;
932 break;
933 }
934 }
935
936 // Heuristic:
937 // 1. If it's a modern emulator, we can trust it supports TrueColor.
938 if (is_modern_emulator) {
939 quirks.color_support =
940 std::max(quirks.color_support, Terminal::Color::TrueColor);
941 quirks.block_characters = true;
942 quirks.cursor_hiding = true;
943 quirks.component_ascii = false;
944 } else if (is_vt220_plus || reports_utf8 || reports_color) {
945 // For VT220+ (including urxvt) or terminals reporting color support:
946 // - Enable modern quirks.
947 // - Ensure at least 256 colors.
948 // - If it's NOT urxvt, we can also upgrade to TrueColor as most modern
949 // terminals identifying as VT220 support it.
950 quirks.block_characters = true;
951 quirks.cursor_hiding = true;
952 quirks.component_ascii = false;
953 if (!is_urxvt && quirks.color_support < Terminal::Color::TrueColor) {
954 quirks.color_support = Terminal::Color::TrueColor;
955 } else if (quirks.color_support < Terminal::Color::Palette256) {
956 quirks.color_support = Terminal::Color::Palette256;
957 }
958 }
959
960 Terminal::SetQuirks(quirks);
961
962 on_exit_functions.emplace([this, cursor_reset_shape] {
963 TerminalSend("\033[?25h"); // Enable cursor.
964 if (is_stdout_a_tty_) {
965 TerminalSend("\033[" + std::to_string(cursor_reset_shape) + " q");
966 }
967 });
968}
969
970// private
971void App::Uninstall() {
972 installed_ = false;
973
974 // During shutdown, wait for all of the replies.
975 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
976 auto closing_receiver = internal_->event_buffer.CreateReceiverAt(
977 internal_->main_loop_receiver->index());
978 auto start = std::chrono::steady_clock::now();
979 while (internal_->cursor_position_request.HasPending()) {
980 FetchTerminalEvents();
981
982 while (closing_receiver->Has()) {
983 const auto event = closing_receiver->Pop();
984 if (event.is_cursor_position()) {
985 cursor_x_ = event.cursor_x();
986 cursor_y_ = event.cursor_y();
987 internal_->cursor_position_request.OnReply();
988 }
989 }
990
991 internal_->task_runner.RunUntilIdle();
992
993 if (std::chrono::steady_clock::now() - start >
994 std::chrono::milliseconds(400)) {
995 break;
996 }
997 std::this_thread::sleep_for(std::chrono::milliseconds(10));
998 }
999 }
1000
1001 OnExit();
1002}
1003
1004// private
1005// NOLINTNEXTLINE
1006void App::RunOnceBlocking(Component component) {
1007 // Set FPS to 60 at most.
1008 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
1009
1010 auto time = std::chrono::steady_clock::now();
1011 const size_t executed_task = internal_->task_runner.ExecutedTasks();
1012
1013 // Wait for at least one task to execute.
1014 while (executed_task == internal_->task_runner.ExecutedTasks() &&
1015 !HasQuitted()) {
1016 RunOnce(component);
1017
1018 const auto now = std::chrono::steady_clock::now();
1019 const auto delta = now - time;
1020 time = now;
1021
1022 if (delta < time_per_frame) {
1023 const auto sleep_duration = time_per_frame - delta;
1024 std::this_thread::sleep_for(sleep_duration);
1025 }
1026 }
1027}
1028
1029// private
1030void App::RunOnce(const Component& component) {
1031 const AutoReset set_component(&component_, component);
1032 ExecuteSignalHandlers();
1033 FetchTerminalEvents();
1034
1035 while (!quit_ && internal_->main_loop_receiver->Has()) {
1036 Post(internal_->main_loop_receiver->Pop());
1037 }
1038
1039 // Execute the pending tasks from the queue.
1040 const size_t executed_task = internal_->task_runner.ExecutedTasks();
1041 internal_->task_runner.RunUntilIdle();
1042 // If no executed task, we can return early without redrawing the screen.
1043 if (executed_task == internal_->task_runner.ExecutedTasks()) {
1044 return;
1045 }
1046
1047 ExecuteSignalHandlers();
1048 Draw(component);
1049
1050 if (selection_data_previous_ != selection_data_) {
1051 selection_data_previous_ = selection_data_;
1052 if (selection_on_change_) {
1053 selection_on_change_();
1055 }
1056 }
1057}
1058
1059// private
1060// NOLINTNEXTLINE
1061void App::HandleTask(Component component, Task& task) {
1062 std::visit(
1063 [&](auto&& arg) {
1064 using T = std::decay_t<decltype(arg)>;
1065 // clang-format off
1066
1067 // Handle Event.
1068 if constexpr (std::is_same_v<T, Event>) {
1069
1070 if (arg.is_cursor_position()) {
1071 cursor_x_ = arg.cursor_x();
1072 cursor_y_ = arg.cursor_y();
1073 internal_->cursor_position_request.OnReply();
1074 return;
1075 }
1076
1077
1078
1079 if (arg.is_mouse()) {
1080 arg.mouse().x -= cursor_x_;
1081 arg.mouse().y -= cursor_y_;
1082 }
1083
1084 arg.screen_ = this;
1085
1086 bool handled = component->OnEvent(arg);
1087 handled = HandleSelection(handled, arg);
1088
1089 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
1090 RecordSignal(SIGABRT);
1091 }
1092
1093#if !defined(_WIN32)
1094 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
1095 RecordSignal(SIGTSTP);
1096 }
1097#endif
1098
1099 frame_valid_ = false;
1100 return;
1101 }
1102
1103 // Handle callback
1104 if constexpr (std::is_same_v<T, Closure>) {
1105 arg();
1106 return;
1107 }
1108
1109 // Handle Animation
1110 if constexpr (std::is_same_v<T, AnimationTask>) {
1111 if (!animation_requested_) {
1112 return;
1113 }
1114
1115 animation_requested_ = false;
1116 const animation::TimePoint now = animation::Clock::now();
1117 const animation::Duration delta = now - previous_animation_time_;
1118 previous_animation_time_ = now;
1119
1120 animation::Params params(delta);
1121 component->OnAnimation(params);
1122 frame_valid_ = false;
1123 return;
1124 }
1125 },
1126 task);
1127 // clang-format on
1128}
1129
1130// private
1131bool App::HandleSelection(bool handled, Event event) {
1132 if (handled) {
1133 selection_pending_ = nullptr;
1134 selection_data_.empty = true;
1135 selection_ = nullptr;
1136 return true;
1137 }
1138
1139 if (!event.is_mouse()) {
1140 return false;
1141 }
1142
1143 auto& mouse = event.mouse();
1144 if (mouse.button != Mouse::Left) {
1145 return false;
1146 }
1147
1148 if (mouse.motion == Mouse::Pressed) {
1149 selection_pending_ = CaptureMouse();
1150 selection_data_.start_x = mouse.x;
1151 selection_data_.start_y = mouse.y;
1152 selection_data_.end_x = mouse.x;
1153 selection_data_.end_y = mouse.y;
1154 return false;
1155 }
1156
1157 if (!selection_pending_) {
1158 return false;
1159 }
1160
1161 if (mouse.motion == Mouse::Moved) {
1162 if ((mouse.x != selection_data_.end_x) ||
1163 (mouse.y != selection_data_.end_y)) {
1164 selection_data_.end_x = mouse.x;
1165 selection_data_.end_y = mouse.y;
1166 selection_data_.empty = false;
1167 }
1168
1169 return true;
1170 }
1171
1172 if (mouse.motion == Mouse::Released) {
1173 selection_pending_ = nullptr;
1174 selection_data_.end_x = mouse.x;
1175 selection_data_.end_y = mouse.y;
1176 selection_data_.empty = false;
1177 return true;
1178 }
1179
1180 return false;
1181}
1182
1183// private
1184// NOLINTNEXTLINE
1185void App::Draw(Component component) {
1186 if (frame_valid_) {
1187 return;
1188 }
1189 auto document = component->Render();
1190 int dimx = 0;
1191 int dimy = 0;
1192 auto terminal = Terminal::Size();
1193 document->ComputeRequirement();
1194 switch (dimension_) {
1195 case Dimension::Fixed:
1196 dimx = dimx_;
1197 dimy = dimy_;
1198 break;
1199 case Dimension::TerminalOutput:
1200 dimx = terminal.dimx;
1201 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1202 break;
1203 case Dimension::Fullscreen:
1204 dimx = terminal.dimx;
1205 dimy = terminal.dimy;
1206 break;
1207 case Dimension::FitComponent:
1208 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
1209 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1210 break;
1211 }
1212
1213 // Hide cursor to prevent flickering during reset.
1214 TerminalSend("\033[?25l");
1215
1216 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
1217 TerminalSend(ResetCursorPosition());
1218
1219 if (frame_count_ != 0) {
1220 // Reset the cursor position to the lower left corner to start drawing the
1221 // new frame.
1222 ResetPosition(internal_->output_buffer, resized);
1223
1224 // If the terminal width decrease, the terminal emulator will start wrapping
1225 // lines and make the display dirty. We should clear it completely.
1226 if ((dimx < dimx_) && !use_alternative_screen_) {
1227 TerminalSend("\033[J"); // clear terminal output
1228 TerminalSend("\033[H"); // move cursor to home position
1229 }
1230 }
1231
1232 // Resize the screen if needed.
1233 if (resized) {
1234 dimx_ = dimx;
1235 dimy_ = dimy;
1236 cells_ = std::vector<std::vector<Cell>>(dimy, std::vector<Cell>(dimx));
1237 cursor_.x = dimx_ - 1;
1238 cursor_.y = dimy_ - 1;
1239 }
1240
1241 // Periodically request the terminal emulator the frame position relative to
1242 // the screen. This is useful for converting mouse position reported in
1243 // screen's coordinates to frame's coordinates.
1244 if (!use_alternative_screen_ && is_stdout_a_tty_) {
1245 RequestCursorPosition(previous_frame_resized_);
1246 }
1247 previous_frame_resized_ = resized;
1248
1249 selection_ = selection_data_.empty
1250 ? std::make_unique<Selection>()
1251 : std::make_unique<Selection>(
1252 selection_data_.start_x, selection_data_.start_y, //
1253 selection_data_.end_x, selection_data_.end_y);
1254 Render(*this, document.get(), *selection_);
1255
1256 // Set cursor position for user using tools to insert CJK characters.
1257 {
1258 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
1259 const int dy = dimy_ - 1 - cursor_.y;
1260
1261 set_cursor_position_.clear();
1262 reset_cursor_position_.clear();
1263
1264 if (dy != 0) {
1265 set_cursor_position_ += "\x1B[" + std::to_string(dy) + "A";
1266 reset_cursor_position_ += "\x1B[" + std::to_string(dy) + "B";
1267 }
1268
1269 if (dx != 0) {
1270 set_cursor_position_ += "\x1B[" + std::to_string(dx) + "D";
1271 reset_cursor_position_ += "\x1B[" + std::to_string(dx) + "C";
1272 }
1273
1274 if (cursor_.shape != Cursor::Hidden) {
1275 set_cursor_position_ += "\033[?25h";
1276 set_cursor_position_ +=
1277 "\033[" + std::to_string(int(cursor_.shape)) + " q";
1278 }
1279 }
1280
1281 ToString(internal_->output_buffer);
1282 TerminalSend(set_cursor_position_);
1283 TerminalFlush();
1284
1285 Clear();
1286 frame_valid_ = true;
1287 frame_count_++;
1288}
1289
1290// private
1291std::string App::ResetCursorPosition() {
1292 std::string result = std::move(reset_cursor_position_);
1293 reset_cursor_position_ = "";
1294 return result;
1295}
1296
1297// private
1298void App::RequestCursorPosition(bool force) {
1299 internal_->cursor_position_request.Request(force);
1300}
1301
1302// private
1303
1304// private
1305void App::TerminalSend(std::string_view s) {
1306 internal_->output_buffer += s;
1307}
1308
1309// private
1310void App::TerminalFlush() {
1311 // Emscripten doesn't implement flush. We interpret zero as flush.
1312 internal_->output_buffer += '\0';
1313 std::cout << internal_->output_buffer << std::flush;
1314 internal_->output_buffer.clear();
1315}
1316
1317/// @brief Return a function to exit the main loop.
1319 return [this] { Exit(); };
1320}
1321
1322/// @brief Exit the main loop.
1324 Post([this] { ExitNow(); });
1325}
1326
1327// private:
1328void App::ExitNow() {
1329 quit_ = true;
1330}
1331
1332// private:
1333void App::Signal(int signal) {
1334 if (signal == SIGABRT) {
1335 Exit();
1336 return;
1337 }
1338
1339// Windows do no support SIGTSTP / SIGWINCH
1340#if !defined(_WIN32)
1341 if (signal == SIGTSTP) {
1342 Post([&] {
1343 TerminalSend(ResetCursorPosition());
1344 ResetPosition(internal_->output_buffer, /*clear*/ true);
1345 Uninstall();
1346 dimx_ = 0;
1347 dimy_ = 0;
1348 (void)std::raise(SIGTSTP);
1349 Install();
1350 });
1351 return;
1352 }
1353
1354 if (signal == SIGWINCH) {
1355 Post(Event::Special({0}));
1356 return;
1357 }
1358#endif
1359}
1360
1361size_t App::FetchTerminalEvents() {
1362#if defined(_WIN32)
1363 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1364 // Check if there is input in the console.
1365 auto console = GetStdHandle(STD_INPUT_HANDLE);
1366 DWORD number_of_events = 0;
1367 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1368 return std::vector<INPUT_RECORD>();
1369 }
1370 if (number_of_events <= 0) {
1371 // No input, return.
1372 return std::vector<INPUT_RECORD>();
1373 }
1374 // Read the input events.
1375 std::vector<INPUT_RECORD> records(number_of_events);
1376 DWORD number_of_events_read = 0;
1377 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1378 &number_of_events_read)) {
1379 return std::vector<INPUT_RECORD>();
1380 }
1381 records.resize(number_of_events_read);
1382 return records;
1383 };
1384
1385 auto records = get_input_records();
1386 if (records.size() == 0) {
1387 const auto timeout =
1388 std::chrono::steady_clock::now() - internal_->last_char_time;
1389 const size_t timeout_microseconds =
1390 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1391 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1392 return 0;
1393 }
1394 internal_->last_char_time = std::chrono::steady_clock::now();
1395
1396 // Convert the input events to FTXUI events.
1397 // For each event, we call the terminal input parser to convert it to
1398 // Event.
1399 std::wstring wstring;
1400 for (const auto& r : records) {
1401 switch (r.EventType) {
1402 case KEY_EVENT: {
1403 auto key_event = r.Event.KeyEvent;
1404 // ignore UP key events
1405 if (key_event.bKeyDown == FALSE) {
1406 continue;
1407 }
1408 const wchar_t wc = key_event.uChar.UnicodeChar;
1409 wstring += wc;
1410 if (wc >= 0xd800 && wc <= 0xdbff) {
1411 // Wait for the Low Surrogate to arrive in the next record.
1412 continue;
1413 }
1414 for (auto it : to_string(wstring)) {
1415 internal_->terminal_input_parser.Add(it);
1416 }
1417 wstring.clear();
1418 } break;
1419 case WINDOW_BUFFER_SIZE_EVENT:
1420 Post(Event::Special({0}));
1421 break;
1422 case MENU_EVENT:
1423 case FOCUS_EVENT:
1424 case MOUSE_EVENT:
1425 // TODO(mauve): Implement later.
1426 break;
1427 }
1428 }
1429 return records.size();
1430#elif defined(__EMSCRIPTEN__)
1431 // Read chars from the terminal.
1432 // We configured it to be non blocking.
1433 std::array<char, 128> out{};
1434 const ssize_t l = read(STDIN_FILENO, out.data(), out.size());
1435 if (l <= 0) {
1436 const auto timeout =
1437 std::chrono::steady_clock::now() - internal_->last_char_time;
1438 const size_t timeout_microseconds =
1439 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1440 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1441 return 0;
1442 }
1443 internal_->last_char_time = std::chrono::steady_clock::now();
1444
1445 // Convert the chars to events.
1446 for (ssize_t i = 0; i < l; ++i) {
1447 internal_->terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1448 }
1449 return (size_t)l;
1450#else // POSIX (Linux & Mac)
1451 struct pollfd pfd = {tty_fd_, POLLIN, 0};
1452 const int poll_result = poll(&pfd, 1, 0);
1453 if (poll_result <= 0) {
1454 const auto timeout =
1455 std::chrono::steady_clock::now() - internal_->last_char_time;
1456 const size_t timeout_ms =
1457 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1458 internal_->terminal_input_parser.Timeout(static_cast<int>(timeout_ms));
1459 return 0;
1460 }
1461 internal_->last_char_time = std::chrono::steady_clock::now();
1462
1463 // Read chars from the terminal.
1464 std::array<char, 128> out{};
1465 const ssize_t l = read(tty_fd_, out.data(), out.size());
1466 if (l <= 0) {
1467 return 0;
1468 }
1469
1470 // Convert the chars to events.
1471 for (ssize_t i = 0; i < l; ++i) {
1472 internal_->terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1473 }
1474 return (size_t)l;
1475#endif
1476}
1477
1478void App::PostAnimationTask() {
1479 Post(AnimationTask());
1480
1481 // Repeat the animation task every 15ms. This correspond to a frame rate
1482 // of around 66fps.
1483 internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1484 std::chrono::milliseconds(15));
1485}
1486
1487bool App::SelectionData::operator==(const App::SelectionData& other) const {
1488 if (empty && other.empty) {
1489 return true;
1490 }
1491 if (empty || other.empty) {
1492 return false;
1493 }
1494 return start_x == other.start_x && start_y == other.start_y &&
1495 end_x == other.end_x && end_y == other.end_y;
1496}
1497
1498bool App::SelectionData::operator!=(const App::SelectionData& other) const {
1499 return !(*this == other);
1500}
1501
1502} // namespace ftxui
static void Signal(App &s, int signal)
Definition app.hpp:204
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:83
void HandlePipedInput(bool enable=true)
Enable or disable automatic piped input handling. When enabled, FTXUI will detect piped input and red...
Definition app.cpp:461
void Exit()
Exit the main loop.
Definition app.cpp:1323
static const Event CtrlZ
Definition event.hpp:106
void PostEvent(Event event)
Add an event to the main loop. It will be executed later, after every other scheduled events.
Definition app.cpp:483
static App FitComponent()
Definition app.cpp:424
~App() override
static App * Active()
Return the currently active screen, or null if none.
Definition app.cpp:616
void Post(Task task)
Add a task to the main loop. It will be executed later, after every other scheduled tasks.
Definition app.cpp:467
int TerminalVersion() const
Return the terminal version.
Definition app.cpp:816
std::vector< std::string > TerminalCapabilityNames() const
Return the names of the terminal capabilities.
Definition app.cpp:805
static Event Special(std::string_view)
An custom event whose meaning is defined by the user of the library.
Definition event.cpp:240
static App FullscreenPrimaryScreen()
Definition app.cpp:383
static App Fullscreen()
Definition app.cpp:375
static const Event Custom
Definition event.hpp:109
friend class Loop
Definition app.hpp:197
const std::string & TerminalName() const
Return the terminal name.
Definition app.cpp:811
static App TerminalOutput()
Definition app.cpp:409
static App FullscreenAlternateScreen()
Definition app.cpp:396
CapturedMouse CaptureMouse()
Try to get the unique lock about being able to capture the mouse.
Definition app.cpp:505
const std::string & TerminalEmulatorVersion() const
Return the terminal emulator version.
Definition app.cpp:826
static App FixedSize(int dimx, int dimy)
Definition app.cpp:362
std::string GetSelection()
Returns the content of the current selection.
Definition app.cpp:603
const std::vector< int > & TerminalCapabilities() const
Return the terminal capabilities.
Definition event.cpp:218
void TrackMouse(bool enable=true)
Set whether mouse is tracked and events reported. called outside of the main loop....
Definition app.cpp:449
void SelectionChange(std::function< void()> callback)
Definition app.cpp:610
const std::string & TerminalEmulatorName() const
Return the terminal emulator name.
Definition app.cpp:821
void RequestAnimationFrame()
Add a task to draw the screen one more time, until all the animations are done.
Definition app.cpp:490
friend class ThrottledRequest
Definition app.hpp:104
Closure ExitLoopClosure()
Return a function to exit the main loop.
Definition app.cpp:1318
void ForceHandleCtrlC(bool force)
Force FTXUI to handle or not handle Ctrl-C, even if the component catches the Event::CtrlC.
Definition app.cpp:592
void ForceHandleCtrlZ(bool force)
Force FTXUI to handle or not handle Ctrl-Z, even if the component catches the Event::CtrlZ.
Definition app.cpp:598
Closure WithRestoredIO(Closure)
Decorate a function. It executes the same way, but with the currently active screen terminal hooks te...
Definition app.cpp:582
App is a Screen that can handle events, run a main loop, and manage components.
Definition app.hpp:36
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:64
Represent an event. It can be key press event, a terminal resize, or more ...
Definition event.hpp:32
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
Definition node.cpp:84
int dimy() const
Definition surface.hpp:43
std::string ToString() const
Definition screen.cpp:428
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:499
Cursor cursor_
Definition screen.hpp:81
void Clear()
Clear all the cells from the screen.
Definition screen.cpp:527
std::vector< std::vector< Cell > > cells_
Definition surface.hpp:53
int dimx() const
Definition surface.hpp:42
Quirks GetQuirks()
Get the terminal quirks.
Definition terminal.cpp:183
Dimensions Size()
Get the terminal size.
Definition terminal.cpp:130
void SetQuirks(const Quirks &quirks)
Override terminal quirks.
Definition terminal.cpp:193
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:160
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:1594
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