FTXUI 7.0.1
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.
4#include <algorithm> // for any_of, copy, max, min
5#include <array> // for array
6#include <atomic>
7#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
8#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
9#include <cstdint>
10#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 <map>
18#include <memory>
19#include <stack> // for stack
20#include <string>
21#include <string_view>
22#include <thread> // for thread, sleep_for
23#include <tuple> // for _Swallow_assign, ignore
24#include <type_traits>
25#include <utility> // for move, swap
26#include <variant> // for visit, variant
27#include <vector> // for vector
28
29#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
30#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
31#include "ftxui/component/component_base.hpp" // for ComponentBase
32#include "ftxui/component/event.hpp" // for Event
33#include "ftxui/component/loop.hpp" // for Loop
36#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
37#include "ftxui/dom/node.hpp" // for Node, Render
38#include "ftxui/screen/cell.hpp" // for Cell
39#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
40#include "ftxui/screen/util.hpp" // for util::clamp
41#include "ftxui/util/autoreset.hpp" // for AutoReset
42
43#if defined(_WIN32)
44#define DEFINE_CONSOLEV2_PROPERTIES
45#define WIN32_LEAN_AND_MEAN
46#ifndef NOMINMAX
47#define NOMINMAX
48#endif
49#include <io.h>
50#include <windows.h>
51#else
52#include <fcntl.h>
53#include <poll.h>
54#include <sys/poll.h>
55#include <sys/types.h>
56#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
57#include <unistd.h> // for STDIN_FILENO, STDOUT_FILENO, read
58#endif
59
60#if defined(__EMSCRIPTEN__)
61#include <emscripten.h>
62#endif
63
64namespace ftxui {
65
66enum class AppDimension {
68 Fixed,
71};
72
73namespace animation {
75 auto* screen = App::Active();
76 if (screen) {
77 screen->RequestAnimationFrame();
78 }
79}
80} // namespace animation
81
82#if defined(__EMSCRIPTEN__)
83extern "C" {
84EMSCRIPTEN_KEEPALIVE
85void ftxui_on_resize(int columns, int rows) {
87 columns,
88 rows,
89 });
90 std::raise(SIGWINCH);
91}
92}
93#endif
94
95struct App::Internal {
96 App* public_;
97
98 App* suspended_screen_ = nullptr;
99 const AppDimension dimension_;
100 const bool use_alternative_screen_;
101
102 bool track_mouse_ = true;
103
104 std::string set_cursor_position_;
105 std::string reset_cursor_position_;
106
107 std::atomic<bool> quit_{false};
108 bool installed_ = false;
109 bool animation_requested_ = false;
110 animation::TimePoint previous_animation_time_;
111
112 int cursor_x_ = 1;
113 int cursor_y_ = 1;
114
115 std::uint64_t frame_count_ = 0;
116 bool mouse_captured = false;
117 bool previous_frame_resized_ = false;
118
119 bool frame_valid_ = false;
120
121 bool force_handle_ctrl_c_ = true;
122 bool force_handle_ctrl_z_ = true;
123
124 int cursor_reset_shape_ = 1;
125
126 // Piped input handling state (POSIX only)
127 bool handle_piped_input_ = true;
128 bool is_stdin_a_tty_ = false;
129 bool is_stdout_a_tty_ = false;
130 // File descriptor for /dev/tty, used for piped input handling.
131 int tty_fd_ = -1;
132
133 std::string terminal_name_ = "unknown";
134 int terminal_version_ = 0;
135
136 std::string terminal_emulator_name_ = "unknown";
137 std::string terminal_emulator_version_ = "unknown";
138
139 std::vector<int> terminal_capabilities_;
140
141 // Selection API:
142 CapturedMouse selection_pending_;
143 struct SelectionData {
144 int start_x = -1;
145 int start_y = -1;
146 int end_x = -2;
147 int end_y = -2;
148 bool empty = true;
149 bool operator==(const SelectionData& other) const {
150 if (empty && other.empty) {
151 return true;
152 }
153 if (empty || other.empty) {
154 return false;
155 }
156 return start_x == other.start_x && start_y == other.start_y &&
157 end_x == other.end_x && end_y == other.end_y;
158 }
159 bool operator!=(const SelectionData& other) const {
160 return !(*this == other);
161 }
162 };
163 SelectionData selection_data_;
164 SelectionData selection_data_previous_;
165 std::unique_ptr<Selection> selection_;
166 std::function<void()> selection_on_change_;
167
168 Component component_;
169
170 // Pre-existing in Internal:
171 TerminalInputParser terminal_input_parser;
172 task::TaskRunner task_runner;
173 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
174 std::chrono::steady_clock::now();
175 std::string output_buffer;
176
177 class ThrottledRequest {
178 public:
179 ThrottledRequest(App::Internal* internal, std::function<void()> send)
180 : internal_(internal), send_(std::move(send)) {}
181
182 void Request(bool force = false) {
183 if (!internal_->is_stdin_a_tty_) {
184 return;
185 }
186
187 if (force) {
188 Send();
189 return;
190 }
191
192 // Allow only one pending request at a time. This is to avoid flooding the
193 // terminal with requests.
194 if (HasPending()) {
195 return;
196 }
197
198 const auto now = std::chrono::steady_clock::now();
199 const auto delta = now - last_request_time_;
200 const auto delay = std::chrono::milliseconds(500) - delta;
201
202 if (delay <= std::chrono::milliseconds(0)) {
203 Send();
204 return;
205 }
206
207 request_queued_ = true;
208 internal_->task_runner.PostDelayedTask(
209 [this] {
210 request_queued_ = false;
211 Request();
212 },
213 delay);
214 }
215
216 void OnReply() { pending_request_ = false; }
217
218 bool HasPending() const {
219 if (pending_request_) {
220 const auto now = std::chrono::steady_clock::now();
221 if (now - last_sent_time_ < std::chrono::seconds(5)) {
222 return true;
223 }
224 }
225 return request_queued_;
226 }
227
228 private:
229 void Send() {
230 last_sent_time_ = std::chrono::steady_clock::now();
231 last_request_time_ = last_sent_time_;
232 pending_request_ = true;
233 send_();
234 }
235
236 App::Internal* internal_;
237 std::function<void()> send_;
238 bool pending_request_ = false;
239 std::chrono::steady_clock::time_point last_request_time_ =
240 std::chrono::steady_clock::now() - std::chrono::hours(1);
241 std::chrono::steady_clock::time_point last_sent_time_ =
242 std::chrono::steady_clock::now() - std::chrono::hours(1);
243 bool request_queued_ = false;
244 };
245
246 ThrottledRequest cursor_position_request;
247
248 MultiReceiverBuffer<Event> event_buffer;
249 std::unique_ptr<MultiReceiverBuffer<Event>::Receiver> main_loop_receiver;
250
251 Internal(App* app, AppDimension dimension, bool use_alternative_screen);
252
253 void ExitNow();
254 void Install();
255 void Uninstall();
256 void PreMain();
257 void PostMain();
258 bool HasQuitted();
259 void RunOnce(const Component& component);
260 void RunOnceBlocking(Component component);
261 void HandleTask(Component component, Task& task);
262 bool HandleSelection(bool handled, Event event);
263 void Draw(Component component);
264 std::string ResetCursorPosition();
265 void RequestCursorPosition(bool force = false);
266 void TerminalSend(std::string_view);
267 void TerminalFlush();
268 void InstallPipedInputHandling();
269 void InstallTerminalInfo();
270 void Signal(int signal);
271 size_t FetchTerminalEvents();
272 void PostAnimationTask();
273};
274
275namespace {
276
277App* g_active_screen = nullptr; // NOLINT
278
279std::stack<Closure> on_exit_functions; // NOLINT
280
281void OnExit() {
282 while (!on_exit_functions.empty()) {
283 on_exit_functions.top()();
284 on_exit_functions.pop();
285 }
286}
287
288// CSI: Control Sequence Introducer
289const std::string CSI = "\x1b["; // NOLINT
290 //
291// DCS: Device Control String
292const std::string DCS = "\x1bP"; // NOLINT
293
294// ST: String Terminator
295const std::string ST = "\x1b\\"; // NOLINT
296
297// DECRQSS: Request Status String
298// DECSCUSR: Set Cursor Style
299const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
300
301// DEC: Digital Equipment Corporation
302enum class DECMode : std::uint16_t {
303 kLineWrap = 7,
304 kCursor = 25,
305
306 kMouseX10 = 9,
307 kMouseVt200 = 1000,
308 kMouseVt200Highlight = 1001,
309
310 kMouseBtnEventMouse = 1002,
311 kMouseAnyEvent = 1003,
312
313 kMouseUtf8 = 1005,
314 kMouseSgrExtMode = 1006,
315 kMouseUrxvtMode = 1015,
316 kMouseSgrPixelsMode = 1016,
317 kAlternateScreen = 1049,
318};
319
320// Device Status Report (DSR) {
321enum class DSRMode : std::uint8_t {
322 kCursor = 6,
323};
324
325std::string Serialize(const std::vector<DECMode>& parameters) {
326 bool first = true;
327 std::string out;
328 for (const DECMode parameter : parameters) {
329 if (!first) {
330 out += ";";
331 }
332 out += std::to_string(int(parameter));
333 first = false;
334 }
335 return out;
336}
337
338// DEC Private Mode Set (DECSET)
339std::string Set(const std::vector<DECMode>& parameters) {
340 return CSI + "?" + Serialize(parameters) + "h";
341}
342
343// DEC Private Mode Reset (DECRST)
344std::string Reset(const std::vector<DECMode>& parameters) {
345 return CSI + "?" + Serialize(parameters) + "l";
346}
347
348// Device Status Report (DSR)
349std::string DeviceStatusReport(DSRMode ps) {
350 return CSI + std::to_string(int(ps)) + "n";
351}
352
353class CapturedMouseImpl : public CapturedMouseInterface {
354 public:
355 explicit CapturedMouseImpl(std::function<void(void)> callback)
356 : callback_(std::move(callback)) {}
357 ~CapturedMouseImpl() override { callback_(); }
358 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
359 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
360 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
361 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
362
363 private:
364 std::function<void(void)> callback_;
365};
366
367#if !defined(_WIN32)
368std::atomic<int> g_signal_exit_count = 0; // NOLINT
369std::atomic<int> g_signal_stop_count = 0; // NOLINT
370std::atomic<int> g_signal_resize_count = 0; // NOLINT
371#else
372std::atomic<int> g_signal_exit_count = 0; // NOLINT
373#endif
374
375// Tracks whether the terminal is currently configured in raw mode.
376// Used to prevent double-restoration in emergency and normal exits.
377std::atomic<bool> g_terminal_is_raw{false};
378
379// Stores the last received deferred signal (e.g. SIGINT, SIGTERM) to be
380// re-raised during uninstallation/exit.
381std::atomic<int> g_last_signal{0}; // NOLINT
382
383#if defined(_WIN32)
384using SignalHandler = void (*)(int);
385// Stores the original signal handlers before FTXUI installed its own.
386std::map<int, SignalHandler> g_old_signal_handlers;
387
388// Stores the original console modes to restore them during exit.
389DWORD g_original_stdout_mode = 0;
390DWORD g_original_stdin_mode = 0;
391bool g_has_original_console_mode = false;
392#else
393// Stores the original sigaction structures before FTXUI installed its own.
394std::map<int, struct sigaction> g_old_sigactions;
395
396// Stores the original termios terminal settings to restore them during exit.
397struct termios g_original_termios;
398bool g_has_original_termios = false;
399int g_tty_fd = -1;
400#endif
401
402// Restores the original signal handler for the given signal and re-raises it.
403// Async-signal-safe function.
404void RestoreSignalHandlerAndRaise(int signal) {
405#if defined(_WIN32)
406 auto it = g_old_signal_handlers.find(signal);
407 auto old_handler = (it != g_old_signal_handlers.end()) ? it->second : SIG_DFL;
408 std::signal(signal, old_handler);
409#else
410 auto it = g_old_sigactions.find(signal);
411 if (it != g_old_sigactions.end()) {
412 sigaction(signal, &it->second, nullptr);
413 } else {
414 struct sigaction sa;
415 sa.sa_handler = SIG_DFL;
416 sigemptyset(&sa.sa_mask);
417 sa.sa_flags = 0;
418 sigaction(signal, &sa, nullptr);
419 }
420#endif
421 std::raise(signal);
422}
423
424// Emergency terminal state restoration.
425// Async-signal-safe function.
426void RestoreTerminalEmergency() {
427 if (!g_terminal_is_raw.exchange(false)) {
428 return;
429 }
430#if defined(_WIN32)
431 if (g_has_original_console_mode) {
432 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
433 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
434 SetConsoleMode(stdout_handle, g_original_stdout_mode);
435 SetConsoleMode(stdin_handle, g_original_stdin_mode);
436 }
437#else
438 if (g_has_original_termios && g_tty_fd >= 0) {
439 const char restore_seq[] =
440 "\x1b[?25h" // Show cursor.
441 "\x1b[?1049l" // Switch to normal screen buffer.
442 "\x1b[?1000l" // Disable normal mouse tracking.
443 "\x1b[?1002l" // Disable button event mouse tracking.
444 "\x1b[?1003l" // Disable all motion mouse tracking.
445 "\x1b[?1006l" // Disable SGR mouse tracking.
446 "\x1b[?1015l" // Disable Urxvt mouse tracking.
447 "\x1b[?7h"; // Enable line wrapping.
448 std::ignore = write(STDOUT_FILENO, restore_seq, sizeof(restore_seq) - 1);
449 tcsetattr(g_tty_fd, TCSANOW, &g_original_termios);
450 }
451#endif
452}
453
454// Async signal safe function
455void RecordSignal(int signal) {
456 switch (signal) {
457 // Abnormal termination (e.g. abort() or assertion failure).
458 case SIGABRT:
459 // Erroneous arithmetic operation (e.g. division by zero).
460 case SIGFPE:
461 // Illegal instruction.
462 case SIGILL:
463 // Invalid memory reference (segmentation fault).
464 case SIGSEGV:
465#if !defined(_WIN32)
466 // Bus error (e.g. bad memory access alignment).
467 case SIGBUS:
468 // Bad system call.
469 case SIGSYS:
470#endif
471 {
472 RestoreTerminalEmergency();
473 RestoreSignalHandlerAndRaise(signal);
474 break;
475 }
476
477 // Terminal interrupt (e.g. Ctrl-C).
478 case SIGINT:
479 // Termination request.
480 case SIGTERM:
481#if !defined(_WIN32)
482 // Terminal quit (e.g. Ctrl-\, produces core dump).
483 case SIGQUIT:
484 // Hangup detected on controlling terminal or death of controlling process.
485 case SIGHUP:
486#endif
487 g_last_signal.store(signal);
488 g_signal_exit_count++;
489 break;
490
491#if !defined(_WIN32)
492 // Terminal stop signal (e.g. Ctrl-Z).
493 case SIGTSTP: // NOLINT
494 g_signal_stop_count++;
495 break;
496
497 // Terminal window size change.
498 case SIGWINCH: // NOLINT
499 g_signal_resize_count++;
500 break;
501#endif
502
503 default:
504 break;
505 }
506}
507
508void ExecuteSignalHandlers() {
509 if (g_last_signal.load() != 0) {
510 App::Private::Signal(*g_active_screen, SIGABRT);
511 }
512
513 int signal_exit_count = g_signal_exit_count.exchange(0);
514 while (signal_exit_count--) {
515 App::Private::Signal(*g_active_screen, SIGABRT);
516 }
517
518#if !defined(_WIN32)
519 int signal_stop_count = g_signal_stop_count.exchange(0);
520 while (signal_stop_count--) {
521 App::Private::Signal(*g_active_screen, SIGTSTP);
522 }
523
524 int signal_resize_count = g_signal_resize_count.exchange(0);
525 while (signal_resize_count--) {
526 App::Private::Signal(*g_active_screen, SIGWINCH);
527 }
528#endif
529}
530
531void InstallSignalHandler(int sig) {
532#if defined(_WIN32)
533 auto old_signal_handler = std::signal(sig, RecordSignal);
534 g_old_signal_handlers[sig] = old_signal_handler;
535 on_exit_functions.emplace(
536 [=] { std::ignore = std::signal(sig, old_signal_handler); });
537#else
538 struct sigaction sa;
539 sa.sa_handler = RecordSignal;
540 sigemptyset(&sa.sa_mask);
541 sa.sa_flags = SA_RESTART;
542 struct sigaction old_sa;
543 sigaction(sig, &sa, &old_sa);
544 g_old_sigactions[sig] = old_sa;
545 on_exit_functions.emplace([=] { sigaction(sig, &old_sa, nullptr); });
546#endif
547}
548
549} // namespace
550
551App::Internal::Internal(App* app,
552 AppDimension dimension,
553 bool use_alternative_screen)
554 : public_(app),
555 dimension_(dimension),
556 use_alternative_screen_(use_alternative_screen),
557 terminal_input_parser([&](Event event) {
558 event_buffer.Push(std::move(event));
559 }),
560 cursor_position_request(this, [this] {
561 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
562 }) {
563 main_loop_receiver = event_buffer.CreateReceiver();
564}
565
566void App::Internal::ExitNow() {
567 quit_ = true;
568}
569
570void App::Internal::Install() {
571 frame_valid_ = false;
572
573 // Flush the buffer for stdout to ensure whatever the user has printed before
574 // is fully applied before we start modifying the terminal configuration. This
575 // is important, because we are using two different channels (stdout vs
576 // termios/WinAPI) to communicate with the terminal emulator below. See
577 // https://github.com/ArthurSonzogni/FTXUI/issues/846
578 TerminalFlush();
579
580 InstallPipedInputHandling();
581
582 // After uninstalling the new configuration, flush it to the terminal to
583 // ensure it is fully applied:
584 on_exit_functions.emplace([this] { TerminalFlush(); });
585
586 // Install signal handlers to restore the terminal state on exit. The default
587 // signal handlers are restored on exit.
588 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
589 InstallSignalHandler(signal);
590 }
591
592// Save the old terminal configuration and restore it on exit.
593#if defined(_WIN32)
594 // Enable VT processing on stdout and stdin
595 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
596 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
597
598 DWORD out_mode = 0;
599 DWORD in_mode = 0;
600 GetConsoleMode(stdout_handle, &out_mode);
601 GetConsoleMode(stdin_handle, &in_mode);
602 g_original_stdout_mode = out_mode;
603 g_original_stdin_mode = in_mode;
604 g_has_original_console_mode = true;
605 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
606 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
607
608 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
609 const int enable_virtual_terminal_processing = 0x0004;
610 const int disable_newline_auto_return = 0x0008;
611 out_mode |= enable_virtual_terminal_processing;
612 out_mode |= disable_newline_auto_return;
613
614 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
615 const int enable_line_input = 0x0002;
616 const int enable_echo_input = 0x0004;
617 const int enable_virtual_terminal_input = 0x0200;
618 const int enable_window_input = 0x0008;
619 in_mode &= ~enable_echo_input;
620 in_mode &= ~enable_line_input;
621 in_mode |= enable_virtual_terminal_input;
622 in_mode |= enable_window_input;
623
624 SetConsoleMode(stdin_handle, in_mode);
625 SetConsoleMode(stdout_handle, out_mode);
626#else // POSIX (Linux & Mac)
627 for (const int signal :
628 {SIGWINCH, SIGTSTP, SIGBUS, SIGSYS, SIGQUIT, SIGHUP}) {
629 InstallSignalHandler(signal);
630 }
631
632 struct termios terminal; // NOLINT
633 tcgetattr(tty_fd_, &terminal);
634 g_original_termios = terminal;
635 g_tty_fd = tty_fd_;
636 g_has_original_termios = true;
637 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
638 tcsetattr(tty_fd_, TCSANOW, &terminal);
639 });
640
641 // Enabling raw terminal input mode
642 terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition
643 terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be
644 // flushed
645 terminal.c_iflag &= ~PARMRK; // Disable marking parity errors.
646 terminal.c_iflag &= ~ISTRIP; // Disable stripping 8th bit off characters.
647 terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR.
648 terminal.c_iflag &= ~IGNCR; // Disable ignoring CR.
649 terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL.
650 terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output
651
652 terminal.c_lflag &= ~ECHO; // Disable echoing input characters.
653 terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters.
654 terminal.c_lflag &= ~ICANON; // Disable Canonical mode.
655 terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting:
656 // - => DSUSP
657 // - C-Z => SUSP
658 // - C-C => INTR
659 // - C-d => QUIT
660 terminal.c_lflag &= ~IEXTEN; // Disable extended input processing
661 terminal.c_cflag |= CS8; // 8 bits per byte
662
663 terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical
664 // read.
665 terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read.
666
667 tcsetattr(tty_fd_, TCSANOW, &terminal);
668
669#endif
670
671 auto enable = [&](const std::vector<DECMode>& parameters) {
672 TerminalSend(Set(parameters));
673 on_exit_functions.emplace(
674 [this, parameters] { TerminalSend(Reset(parameters)); });
675 };
676
677 auto disable = [&](const std::vector<DECMode>& parameters) {
678 TerminalSend(Reset(parameters));
679 on_exit_functions.emplace(
680 [this, parameters] { TerminalSend(Set(parameters)); });
681 };
682
683 if (use_alternative_screen_) {
684 enable({
685 DECMode::kAlternateScreen,
686 });
687 }
688
689 disable({
690 DECMode::kLineWrap,
691 });
692
693 if (track_mouse_) {
694 enable({DECMode::kMouseVt200});
695 enable({DECMode::kMouseAnyEvent});
696 enable({DECMode::kMouseUrxvtMode});
697 enable({DECMode::kMouseSgrExtMode});
698 }
699
700 // After installing the new configuration, flush it to the terminal to
701 // ensure it is fully applied:
702 TerminalFlush();
703
704 InstallTerminalInfo();
705
706 quit_ = false;
707
708 PostAnimationTask();
709
710 installed_ = true;
711 g_terminal_is_raw = true;
712}
713
714void App::Internal::Uninstall() {
715 g_terminal_is_raw = false;
716 installed_ = false;
717
718 // During shutdown, wait for all of the replies.
719 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
720 auto closing_receiver =
721 event_buffer.CreateReceiverAt(main_loop_receiver->index());
722 auto start = std::chrono::steady_clock::now();
723 while (cursor_position_request.HasPending()) {
724 FetchTerminalEvents();
725
726 while (closing_receiver->Has()) {
727 const auto event = closing_receiver->Pop();
728 if (event.is_cursor_position()) {
729 cursor_x_ = event.cursor_x();
730 cursor_y_ = event.cursor_y();
731 cursor_position_request.OnReply();
732 }
733 }
734
735 task_runner.RunUntilIdle();
736
737 if (std::chrono::steady_clock::now() - start >
738 std::chrono::milliseconds(400)) {
739 break;
740 }
741 std::this_thread::sleep_for(std::chrono::milliseconds(10));
742 }
743 }
744
745 OnExit();
746}
747
748void App::Internal::PreMain() {
749 // Suspend previously active screen:
750 if (g_active_screen) {
751 std::swap(suspended_screen_, g_active_screen);
752 // Reset cursor position to the top of the screen and clear the screen.
753 suspended_screen_->internal_->TerminalSend(
754 suspended_screen_->internal_->ResetCursorPosition());
755 suspended_screen_->ResetPosition(
756 suspended_screen_->internal_->output_buffer,
757 /*clear=*/true);
758 suspended_screen_->dimx_ = 0;
759 suspended_screen_->dimy_ = 0;
760
761 // Reset dimensions to force drawing the screen again next time:
762 suspended_screen_->internal_->Uninstall();
763 }
764
765 // This screen is now active:
766 g_active_screen = public_;
767 g_active_screen->internal_->Install();
768
769 previous_animation_time_ = animation::Clock::now();
770}
771
772void App::Internal::PostMain() {
773 // Put cursor position at the end of the drawing.
774 TerminalSend(ResetCursorPosition());
775
776 g_active_screen = nullptr;
777
778 // Restore suspended screen.
779 if (suspended_screen_) {
780 // Clear screen, and put the cursor at the beginning of the drawing.
781 public_->ResetPosition(output_buffer, /*clear=*/true);
782 public_->dimx_ = 0;
783 public_->dimy_ = 0;
784 Uninstall();
785 std::swap(g_active_screen, suspended_screen_);
786 g_active_screen->internal_->Install();
787 } else {
788 Uninstall();
789
790 std::cout << "\r";
791 // On final exit, keep the current drawing and reset cursor position one
792 // line after it.
793 if (!use_alternative_screen_) {
794 std::cout << "\n";
795 }
796 std::cout << std::flush;
797 }
798
799 int sig = g_last_signal.exchange(0);
800 if (sig != 0) {
801 RestoreSignalHandlerAndRaise(sig);
802 }
803}
804
805bool App::Internal::HasQuitted() {
806 return quit_;
807}
808
809void App::Internal::RunOnce(const Component& component) {
810 const AutoReset set_component(&component_, component);
811 ExecuteSignalHandlers();
812 FetchTerminalEvents();
813
814 while (!quit_ && main_loop_receiver->Has()) {
815 public_->Post(main_loop_receiver->Pop());
816 }
817
818 // Execute the pending tasks from the queue.
819 const size_t executed_task = task_runner.ExecutedTasks();
820 task_runner.RunUntilIdle();
821 // If no executed task, we can return early without redrawing the screen.
822 if (executed_task == task_runner.ExecutedTasks()) {
823 return;
824 }
825
826 ExecuteSignalHandlers();
827 Draw(component);
828
829 if (selection_data_previous_ != selection_data_) {
830 selection_data_previous_ = selection_data_;
831 if (selection_on_change_) {
832 selection_on_change_();
833 public_->Post(Event::Custom);
834 }
835 }
836}
837
838void App::Internal::RunOnceBlocking(Component component) {
839 // Set FPS to 60 at most.
840 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
841
842 auto time = std::chrono::steady_clock::now();
843 const size_t executed_task = task_runner.ExecutedTasks();
844
845 // Wait for at least one task to execute.
846 while (executed_task == task_runner.ExecutedTasks() && !HasQuitted()) {
847 RunOnce(component);
848
849 const auto now = std::chrono::steady_clock::now();
850 const auto delta = now - time;
851 time = now;
852
853 if (delta < time_per_frame) {
854 const auto sleep_duration = time_per_frame - delta;
855 std::this_thread::sleep_for(sleep_duration);
856 }
857 }
858}
859
860void App::Internal::HandleTask(Component component, Task& task) {
861 std::visit(
862 [&](auto&& arg) {
863 using T = std::decay_t<decltype(arg)>;
864 // clang-format off
865
866 // Handle Event.
867 if constexpr (std::is_same_v<T, Event>) {
868
869 if (arg.is_cursor_position()) {
870 cursor_x_ = arg.cursor_x();
871 cursor_y_ = arg.cursor_y();
872 cursor_position_request.OnReply();
873 return;
874 }
875
876 if (arg.is_cursor_shape()) {
877 cursor_reset_shape_ = arg.cursor_shape();
878 return;
879 }
880
881 if (arg.IsTerminalCapabilities()) {
882 terminal_capabilities_ = arg.TerminalCapabilities();
883 return;
884 }
885
886 if (arg.IsTerminalNameVersion()) {
887 terminal_name_ = arg.TerminalName();
888 terminal_version_ = arg.TerminalVersion();
889 return;
890 }
891
892 if (arg.IsTerminalEmulator()) {
893 terminal_emulator_name_ = arg.TerminalEmulatorName();
894 terminal_emulator_version_ = arg.TerminalEmulatorVersion();
895 return;
896 }
897
898 if (arg.is_mouse()) {
899 arg.mouse().x -= cursor_x_;
900 arg.mouse().y -= cursor_y_;
901 }
902
903 arg.screen_ = public_;
904
905 bool handled = component->OnEvent(arg);
906 handled = HandleSelection(handled, arg);
907
908 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
909 RecordSignal(SIGINT);
910 }
911
912#if !defined(_WIN32)
913 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
914 RecordSignal(SIGTSTP);
915 }
916#endif
917
918 frame_valid_ = false;
919 return;
920 }
921
922 // Handle callback
923 if constexpr (std::is_same_v<T, Closure>) {
924 arg();
925 return;
926 }
927
928 // Handle Animation
929 if constexpr (std::is_same_v<T, AnimationTask>) {
930 if (!animation_requested_) {
931 return;
932 }
933
934 animation_requested_ = false;
935 const animation::TimePoint now = animation::Clock::now();
936 const animation::Duration delta = now - previous_animation_time_;
937 previous_animation_time_ = now;
938
939 animation::Params params(delta);
940 component->OnAnimation(params);
941 frame_valid_ = false;
942 return;
943 }
944 },
945 task);
946 // clang-format on
947}
948
949bool App::Internal::HandleSelection(bool handled, Event event) {
950 if (handled) {
951 selection_pending_ = nullptr;
952 selection_data_.empty = true;
953 selection_ = nullptr;
954 return true;
955 }
956
957 if (!event.is_mouse()) {
958 return false;
959 }
960
961 auto& mouse = event.mouse();
962 if (mouse.button != Mouse::Left) {
963 return false;
964 }
965
966 if (mouse.motion == Mouse::Pressed) {
967 selection_pending_ = public_->CaptureMouse();
968 selection_data_.start_x = mouse.x;
969 selection_data_.start_y = mouse.y;
970 selection_data_.end_x = mouse.x;
971 selection_data_.end_y = mouse.y;
972 return false;
973 }
974
975 if (!selection_pending_) {
976 return false;
977 }
978
979 if (mouse.motion == Mouse::Moved) {
980 if ((mouse.x != selection_data_.end_x) ||
981 (mouse.y != selection_data_.end_y)) {
982 selection_data_.end_x = mouse.x;
983 selection_data_.end_y = mouse.y;
984 selection_data_.empty = false;
985 }
986
987 return true;
988 }
989
990 if (mouse.motion == Mouse::Released) {
991 selection_pending_ = nullptr;
992 selection_data_.end_x = mouse.x;
993 selection_data_.end_y = mouse.y;
994 selection_data_.empty = false;
995 return true;
996 }
997
998 return false;
999}
1000
1001void App::Internal::Draw(Component component) {
1002 if (frame_valid_) {
1003 return;
1004 }
1005 auto document = component->Render();
1006 int dimx = 0;
1007 int dimy = 0;
1008 auto terminal = Terminal::Size();
1009 document->ComputeRequirement();
1010 switch (dimension_) {
1012 dimx = public_->dimx_;
1013 dimy = public_->dimy_;
1014 break;
1016 dimx = terminal.dimx;
1017 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1018 break;
1020 dimx = terminal.dimx;
1021 dimy = terminal.dimy;
1022 break;
1024 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
1025 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1026 break;
1027 }
1028
1029 // Hide cursor to prevent flickering during reset.
1030 TerminalSend("\033[?25l");
1031
1032 const bool resized =
1033 frame_count_ == 0 || (dimx != public_->dimx_) || (dimy != public_->dimy_);
1034 TerminalSend(ResetCursorPosition());
1035
1036 if (frame_count_ != 0) {
1037 // Reset the cursor position to the lower left corner to start drawing the
1038 // new frame.
1039 public_->ResetPosition(output_buffer, resized);
1040
1041 // If the terminal width decrease, the terminal emulator will start wrapping
1042 // lines and make the display dirty. We should clear it completely.
1043 if ((dimx < public_->dimx_) && !use_alternative_screen_) {
1044 TerminalSend("\033[J"); // clear terminal output
1045 TerminalSend("\033[H"); // move cursor to home position
1046 }
1047 }
1048
1049 // Resize the screen if needed.
1050 if (resized) {
1051 public_->dimx_ = dimx;
1052 public_->dimy_ = dimy;
1053 public_->cells_ = std::vector<Cell>(static_cast<size_t>(dimx) *
1054 static_cast<size_t>(dimy));
1055 Cursor cursor = public_->cursor_;
1056 cursor.x = dimx - 1;
1057 cursor.y = dimy - 1;
1058 public_->SetCursor(cursor);
1059 }
1060
1061 // Periodically request the terminal emulator the frame position relative to
1062 // the screen. This is useful for converting mouse position reported in
1063 // screen's coordinates to frame's coordinates.
1064 if (!use_alternative_screen_ && is_stdout_a_tty_) {
1065 RequestCursorPosition(previous_frame_resized_);
1066 }
1067 previous_frame_resized_ = resized;
1068
1069 selection_ = selection_data_.empty
1070 ? std::make_unique<Selection>()
1071 : std::make_unique<Selection>(
1072 selection_data_.start_x, selection_data_.start_y, //
1073 selection_data_.end_x, selection_data_.end_y);
1074 Render(*public_, document.get(), *selection_);
1075
1076 // Set cursor position for user using tools to insert CJK characters.
1077 {
1078 const int dx = public_->dimx_ - 1 - public_->cursor_.x +
1079 int(public_->dimx_ != terminal.dimx);
1080 const int dy = public_->dimy_ - 1 - public_->cursor_.y;
1081
1082 set_cursor_position_.clear();
1083 reset_cursor_position_.clear();
1084
1085 if (dy != 0) {
1086 set_cursor_position_ += "\x1B[" + std::to_string(dy) + "A";
1087 reset_cursor_position_ += "\x1B[" + std::to_string(dy) + "B";
1088 }
1089
1090 if (dx != 0) {
1091 set_cursor_position_ += "\x1B[" + std::to_string(dx) + "D";
1092 reset_cursor_position_ += "\x1B[" + std::to_string(dx) + "C";
1093 }
1094
1095 if (public_->cursor_.shape != Screen::Cursor::Hidden) {
1096 set_cursor_position_ += "\033[?25h";
1097 set_cursor_position_ +=
1098 "\033[" + std::to_string(int(public_->cursor_.shape)) + " q";
1099 }
1100 }
1101
1102 public_->ToString(output_buffer);
1103 TerminalSend(set_cursor_position_);
1104 TerminalFlush();
1105
1106 public_->Clear();
1107 frame_valid_ = true;
1108 frame_count_++;
1109}
1110
1111std::string App::Internal::ResetCursorPosition() {
1112 std::string result = std::move(reset_cursor_position_);
1113 reset_cursor_position_ = "";
1114 return result;
1115}
1116
1117void App::Internal::RequestCursorPosition(bool force) {
1118 cursor_position_request.Request(force);
1119}
1120
1121void App::Internal::TerminalSend(std::string_view s) {
1122 output_buffer += s;
1123}
1124
1125void App::Internal::TerminalFlush() {
1126 // Emscripten doesn't implement flush. We interpret zero as flush.
1127 output_buffer += '\0';
1128 std::cout << output_buffer << std::flush;
1129 output_buffer.clear();
1130}
1131
1132void App::Internal::InstallPipedInputHandling() {
1133 is_stdin_a_tty_ = false;
1134 is_stdout_a_tty_ = false;
1135#if defined(__EMSCRIPTEN__)
1136 is_stdin_a_tty_ = true;
1137 is_stdout_a_tty_ = true;
1138#elif defined(_WIN32)
1139 is_stdin_a_tty_ = _isatty(_fileno(stdin));
1140 is_stdout_a_tty_ = _isatty(_fileno(stdout));
1141#else
1142 tty_fd_ = STDIN_FILENO;
1143 is_stdout_a_tty_ = isatty(STDOUT_FILENO);
1144 // Handle piped input redirection if explicitly enabled by the application.
1145 // This allows applications to read data from stdin while still receiving
1146 // keyboard input from the terminal for interactive use.
1147 if (!handle_piped_input_) {
1148 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1149 } else if (isatty(STDIN_FILENO)) {
1150 is_stdin_a_tty_ = true;
1151 } else {
1152 // Open /dev/tty for keyboard input.
1153 tty_fd_ = open("/dev/tty", O_RDONLY); // NOLINT
1154 if (tty_fd_ < 0) {
1155 // Failed to open /dev/tty (containers, headless systems, etc.)
1156 tty_fd_ = STDIN_FILENO; // Fallback to stdin.
1157 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1158 } else {
1159 is_stdin_a_tty_ = true;
1160 // Close the /dev/tty file descriptor on exit.
1161 on_exit_functions.emplace([this] {
1162 close(tty_fd_);
1163 tty_fd_ = -1;
1164 });
1165 }
1166 }
1167#endif
1168}
1169
1170void App::Internal::InstallTerminalInfo() {
1171 // Request the terminal to report the current cursor shape. We will restore it
1172 // on exit.
1173 if (is_stdout_a_tty_) {
1174 TerminalSend(DECRQSS_DECSCUSR);
1175 TerminalSend("\033[>q"); // XTVERSION
1176 TerminalSend("\033[>c"); // DA2
1177 TerminalSend("\033[c"); // DA1
1178 TerminalFlush();
1179 }
1180
1181 // Wait for the cursor shape reply using the setup head.
1182 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
1183 // A receiver scoped to the setup: keeping one alive after setup would pin
1184 // every subsequent event in the buffer, growing it for the whole app
1185 // lifetime.
1186 auto setup_receiver = event_buffer.CreateReceiver();
1187 auto start = std::chrono::steady_clock::now();
1188 bool terminal_capabilities_received = false;
1189 // Wait for the cursor shape reply using the setup head.
1190 while (true) {
1191 FetchTerminalEvents();
1192 while (setup_receiver->Has()) {
1193 const auto event = setup_receiver->Pop();
1194 if (event.is_cursor_shape()) {
1195 cursor_reset_shape_ = event.cursor_shape();
1196 }
1197
1198 if (event.IsTerminalCapabilities()) {
1199 terminal_capabilities_ = event.TerminalCapabilities();
1200 terminal_capabilities_received = true;
1201 }
1202
1203 if (event.IsTerminalNameVersion()) {
1204 terminal_name_ = event.TerminalName();
1205 terminal_version_ = event.TerminalVersion();
1206 }
1207
1208 if (event.IsTerminalEmulator()) {
1209 terminal_emulator_name_ = event.TerminalEmulatorName();
1210 terminal_emulator_version_ = event.TerminalEmulatorVersion();
1211 }
1212 }
1213
1214 // Response are expected to be received in order, so we can break when
1215 // the last one (XTVERSION) is received. We also set a timeout to prevent
1216 // waiting forever in case the terminal doesn't support these queries.
1217 if (terminal_capabilities_received) {
1218 break;
1219 }
1220
1221 if (std::chrono::steady_clock::now() - start >
1222 std::chrono::milliseconds(500)) {
1223 break;
1224 }
1225 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1226 }
1227 }
1228
1229 // Set quirks and color support based on terminal identification.
1230 Terminal::Quirks quirks = Terminal::GetQuirks();
1231
1232 auto safe_getenv = [](const char* name) -> std::string_view {
1233 const char* value = std::getenv(name);
1234 return value ? value : "";
1235 };
1236
1237 auto color_support = Terminal::ComputeColorSupport(
1238 safe_getenv("TERM"), safe_getenv("COLORTERM"),
1239 safe_getenv("TERM_PROGRAM"), terminal_name_, terminal_emulator_name_,
1240 terminal_capabilities_);
1241
1242 quirks.SetColorSupport(color_support);
1243
1244 const bool is_modern_emulator = (terminal_emulator_name_ != "unknown");
1245 const bool is_vt220_plus =
1246 (terminal_name_ != "vt100" && terminal_name_ != "unknown");
1247 bool reports_utf8 = false;
1248 for (const int x : terminal_capabilities_) {
1249 if (x == 52) {
1250 reports_utf8 = true;
1251 break;
1252 }
1253 }
1254
1255 // Heuristic: If the terminal emulator is modern, or it reports supporting
1256 // UTF-8 or color, we can assume it supports block characters and cursor
1257 // hiding, which are essential for a good experience. This is a heuristic, but
1258 // it allows us to work around some older terminal emulators that don't
1259 // support these features, while still providing a good experience on modern
1260 // terminal emulators that do support these features.
1261 bool modern = is_modern_emulator || is_vt220_plus || reports_utf8;
1262 if (modern) {
1263 quirks.SetBlockCharacters(true);
1264 quirks.SetCursorHiding(true);
1265 quirks.SetComponentAscii(false);
1266 }
1267
1268 Terminal::SetQuirks(quirks);
1269
1270 on_exit_functions.emplace([this] {
1271 TerminalSend("\033[?25h"); // Enable cursor.
1272 if (is_stdout_a_tty_) {
1273 TerminalSend("\033[" + std::to_string(cursor_reset_shape_) + " q");
1274 }
1275 });
1276}
1277
1278void App::Internal::Signal(int signal) {
1279 if (signal == SIGABRT) {
1280 public_->Exit();
1281 return;
1282 }
1283
1284// Windows do no support SIGTSTP / SIGWINCH
1285#if !defined(_WIN32)
1286 if (signal == SIGTSTP) {
1287 public_->Post([&] {
1288 TerminalSend(ResetCursorPosition());
1289 public_->ResetPosition(output_buffer, /*clear*/ true);
1290 Uninstall();
1291 public_->dimx_ = 0;
1292 public_->dimy_ = 0;
1293 (void)std::raise(SIGTSTP);
1294 Install();
1295 });
1296 return;
1297 }
1298
1299 if (signal == SIGWINCH) {
1300 public_->Post(Event::Special({0}));
1301 return;
1302 }
1303#endif
1304}
1305
1306size_t App::Internal::FetchTerminalEvents() {
1307#if defined(_WIN32)
1308 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1309 // Check if there is input in the console.
1310 auto console = GetStdHandle(STD_INPUT_HANDLE);
1311 DWORD number_of_events = 0;
1312 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1313 return std::vector<INPUT_RECORD>();
1314 }
1315 if (number_of_events <= 0) {
1316 // No input, return.
1317 return std::vector<INPUT_RECORD>();
1318 }
1319 // Read the input events.
1320 std::vector<INPUT_RECORD> records(number_of_events);
1321 DWORD number_of_events_read = 0;
1322 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1323 &number_of_events_read)) {
1324 return std::vector<INPUT_RECORD>();
1325 }
1326 records.resize(number_of_events_read);
1327 return records;
1328 };
1329
1330 auto records = get_input_records();
1331 if (records.size() == 0) {
1332 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1333 const size_t timeout_microseconds =
1334 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1335 terminal_input_parser.Timeout(timeout_microseconds);
1336 return 0;
1337 }
1338 last_char_time = std::chrono::steady_clock::now();
1339
1340 // Convert the input events to FTXUI events.
1341 // For each event, we call the terminal input parser to convert it to
1342 // Event.
1343 std::wstring wstring;
1344 for (const auto& r : records) {
1345 switch (r.EventType) {
1346 case KEY_EVENT: {
1347 auto key_event = r.Event.KeyEvent;
1348 // ignore UP key events
1349 if (key_event.bKeyDown == FALSE) {
1350 continue;
1351 }
1352 const wchar_t wc = key_event.uChar.UnicodeChar;
1353 wstring += wc;
1354 if (wc >= 0xd800 && wc <= 0xdbff) {
1355 // Wait for the Low Surrogate to arrive in the next record.
1356 continue;
1357 }
1358 for (auto it : to_string(wstring)) {
1359 terminal_input_parser.Add(it);
1360 }
1361 wstring.clear();
1362 } break;
1363 case WINDOW_BUFFER_SIZE_EVENT:
1364 public_->Post(Event::Special({0}));
1365 break;
1366 case MENU_EVENT:
1367 case FOCUS_EVENT:
1368 case MOUSE_EVENT:
1369 // TODO(mauve): Implement later.
1370 break;
1371 }
1372 }
1373 return records.size();
1374#elif defined(__EMSCRIPTEN__)
1375 // Read chars from the terminal.
1376 // We configured it to be non blocking.
1377 std::array<char, 128> out{};
1378 const ssize_t l = read(STDIN_FILENO, out.data(), out.size());
1379 if (l <= 0) {
1380 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1381 const size_t timeout_microseconds =
1382 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1383 terminal_input_parser.Timeout(timeout_microseconds);
1384 return 0;
1385 }
1386 last_char_time = std::chrono::steady_clock::now();
1387
1388 // Convert the chars to events.
1389 for (ssize_t i = 0; i < l; ++i) {
1390 terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1391 }
1392 return (size_t)l;
1393#else // POSIX (Linux & Mac)
1394 struct pollfd pfd = {tty_fd_, POLLIN, 0};
1395 const int poll_result = poll(&pfd, 1, 0);
1396 if (poll_result <= 0) {
1397 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1398 const size_t timeout_ms =
1399 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1400 terminal_input_parser.Timeout(static_cast<int>(timeout_ms));
1401 return 0;
1402 }
1403 last_char_time = std::chrono::steady_clock::now();
1404
1405 // Read chars from the terminal.
1406 std::array<char, 128> out{};
1407 const ssize_t l = read(tty_fd_, out.data(), out.size());
1408 if (l <= 0) {
1409 return 0;
1410 }
1411
1412 // Convert the chars to events.
1413 for (ssize_t i = 0; i < l; ++i) {
1414 terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1415 }
1416 return (size_t)l;
1417#endif
1418}
1419
1420void App::Internal::PostAnimationTask() {
1421 public_->Post(AnimationTask());
1422
1423 // Repeat the animation task every 15ms. This correspond to a frame rate
1424 // of around 66fps.
1425 task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1426 std::chrono::milliseconds(15));
1427}
1428
1429App::App(std::unique_ptr<Internal> internal, int dimx, int dimy)
1430 : Screen(dimx, dimy), internal_(std::move(internal)) {
1431 internal_->public_ = this;
1432}
1433
1434App::App(App&& other) noexcept : Screen(std::move(other)) {
1435 internal_ = std::move(other.internal_);
1436 if (internal_) {
1437 internal_->public_ = this;
1438 }
1439}
1440
1441App& App::operator=(App&& other) noexcept {
1442 Screen::operator=(std::move(other));
1443 internal_ = std::move(other.internal_);
1444 if (internal_) {
1445 internal_->public_ = this;
1446 }
1447 return *this;
1448}
1449
1450App::~App() = default;
1451
1452// static
1453App App::FixedSize(int dimx, int dimy) {
1454 auto internal =
1455 std::make_unique<Internal>(nullptr, AppDimension::Fixed, false);
1456 return App(std::move(internal), dimx, dimy);
1457}
1458
1459// static
1460App App::Fullscreen() {
1461 return FullscreenAlternateScreen();
1462}
1463
1464// static
1465App App::FullscreenPrimaryScreen() {
1466 auto terminal = Terminal::Size();
1467 auto internal =
1468 std::make_unique<Internal>(nullptr, AppDimension::Fullscreen, false);
1469 return App(std::move(internal), terminal.dimx, terminal.dimy);
1470}
1471
1472// static
1473App App::FullscreenAlternateScreen() {
1474 auto terminal = Terminal::Size();
1475 auto internal =
1476 std::make_unique<Internal>(nullptr, AppDimension::Fullscreen, true);
1477 return App(std::move(internal), terminal.dimx, terminal.dimy);
1478}
1479
1480// static
1481App App::FitComponent() {
1482 auto terminal = Terminal::Size();
1483 auto internal =
1484 std::make_unique<Internal>(nullptr, AppDimension::FitComponent, false);
1485 return App(std::move(internal), terminal.dimx, terminal.dimy);
1486}
1487
1488// static
1489App App::TerminalOutput() {
1490 auto terminal = Terminal::Size();
1491 auto internal =
1492 std::make_unique<Internal>(nullptr, AppDimension::TerminalOutput, false);
1493 return App(std::move(internal), terminal.dimx, terminal.dimy);
1494}
1495
1496void App::TrackMouse(bool enable) {
1497 internal_->track_mouse_ = enable;
1498}
1499
1500void App::HandlePipedInput(bool enable) {
1501 internal_->handle_piped_input_ = enable;
1502}
1503
1504// static
1505App* App::Active() {
1506 return g_active_screen;
1507}
1508
1509void App::Loop(Component component) {
1510 class Loop loop(this, std::move(component));
1511 loop.Run();
1512}
1513
1514void App::Exit() {
1515 Post([this] { internal_->ExitNow(); });
1516}
1517
1518Closure App::ExitLoopClosure() {
1519 return [this] { Exit(); };
1520}
1521
1522void App::Post(Task task) {
1523 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
1524 if (internal_->component_) {
1525 internal_->HandleTask(internal_->component_, task);
1526 return;
1527 }
1528
1529 // If there is no component, we can still execute closures.
1530 if (std::holds_alternative<Closure>(task)) {
1531 std::get<Closure>(task)();
1532 }
1533 });
1534}
1535
1536void App::PostEvent(Event event) {
1537 // PostEvent is documented as thread safe: go through the mutex-protected
1538 // task queue. The event_buffer is only safe to use from the main thread.
1539 Post(Task(std::move(event)));
1540}
1541
1542// static
1543void App::PostEventOrExecute(Closure closure) {
1544 if (!closure) {
1545 return;
1546 }
1547 if (auto* app = App::Active()) {
1548 app->Post(std::move(closure));
1549 } else {
1550 closure();
1551 }
1552}
1553
1554void App::RequestAnimationFrame() {
1555 if (internal_->animation_requested_) {
1556 return;
1557 }
1558 internal_->animation_requested_ = true;
1559 auto now = animation::Clock::now();
1560 const auto time_histeresis = std::chrono::milliseconds(33);
1561 if (now - internal_->previous_animation_time_ >= time_histeresis) {
1562 internal_->previous_animation_time_ = now;
1563 }
1564}
1565
1566CapturedMouse App::CaptureMouse() {
1567 if (internal_->mouse_captured) {
1568 return nullptr;
1569 }
1570 internal_->mouse_captured = true;
1571 return std::make_unique<CapturedMouseImpl>(
1572 [this] { internal_->mouse_captured = false; });
1573}
1574
1575Closure App::WithRestoredIO(Closure fn) {
1576 return [this, fn] {
1577 internal_->Uninstall();
1578 fn();
1579 internal_->Install();
1580 };
1581}
1582
1583void App::ForceHandleCtrlC(bool force) {
1584 internal_->force_handle_ctrl_c_ = force;
1585}
1586
1587void App::ForceHandleCtrlZ(bool force) {
1588 internal_->force_handle_ctrl_z_ = force;
1589}
1590
1591std::string App::GetSelection() {
1592 if (!internal_->selection_) {
1593 return "";
1594 }
1595 return internal_->selection_->GetParts();
1596}
1597
1598void App::SelectionChange(std::function<void()> callback) {
1599 internal_->selection_on_change_ = std::move(callback);
1600}
1601
1602const std::string& App::TerminalName() const {
1603 return internal_->terminal_name_;
1604}
1605
1606int App::TerminalVersion() const {
1607 return internal_->terminal_version_;
1608}
1609
1610const std::string& App::TerminalEmulatorName() const {
1611 return internal_->terminal_emulator_name_;
1612}
1613
1614const std::string& App::TerminalEmulatorVersion() const {
1615 return internal_->terminal_emulator_version_;
1616}
1617
1618const std::vector<int>& App::TerminalCapabilities() const {
1619 return internal_->terminal_capabilities_;
1620}
1621
1622std::vector<std::string> App::TerminalCapabilityNames() const {
1623 return Event::TerminalCapabilities("", internal_->terminal_capabilities_)
1624 .TerminalCapabilityNames();
1625}
1626
1627// Loop calls these:
1628
1629void App::ExitNow() {
1630 internal_->ExitNow();
1631}
1632void App::Install() {
1633 internal_->Install();
1634}
1635void App::Uninstall() {
1636 internal_->Uninstall();
1637}
1638void App::PreMain() {
1639 internal_->PreMain();
1640}
1641void App::PostMain() {
1642 internal_->PostMain();
1643}
1644bool App::HasQuitted() {
1645 return internal_->HasQuitted();
1646}
1647void App::RunOnce(const Component& component) {
1648 internal_->RunOnce(component);
1649}
1650void App::RunOnceBlocking(Component component) {
1651 internal_->RunOnceBlocking(component);
1652}
1653void App::HandleTask(Component component, Task& task) {
1654 internal_->HandleTask(component, task);
1655}
1656bool App::HandleSelection(bool handled, Event event) {
1657 return internal_->HandleSelection(handled, event);
1658}
1659void App::Draw(Component component) {
1660 internal_->Draw(component);
1661}
1662std::string App::ResetCursorPosition() {
1663 return internal_->ResetCursorPosition();
1664}
1665void App::RequestCursorPosition(bool force) {
1666 internal_->RequestCursorPosition(force);
1667}
1668void App::TerminalSend(std::string_view s) {
1669 internal_->TerminalSend(s);
1670}
1671void App::TerminalFlush() {
1672 internal_->TerminalFlush();
1673}
1674void App::InstallPipedInputHandling() {
1675 internal_->InstallPipedInputHandling();
1676}
1677void App::InstallTerminalInfo() {
1678 internal_->InstallTerminalInfo();
1679}
1680void App::Signal(int signal) {
1681 internal_->Signal(signal);
1682}
1683size_t App::FetchTerminalEvents() {
1684 return internal_->FetchTerminalEvents();
1685}
1686void App::PostAnimationTask() {
1687 internal_->PostAnimationTask();
1688}
1689
1690Loop::Loop(App* screen, Component component)
1691 : screen_(screen), component_(std::move(component)) {
1692 screen_->PreMain();
1693}
1694
1695Loop::~Loop() {
1696 screen_->PostMain();
1697}
1698
1699bool Loop::HasQuitted() {
1700 return screen_->HasQuitted();
1701}
1702
1703void Loop::RunOnce() {
1704 screen_->RunOnce(component_);
1705}
1706
1707void Loop::RunOnceBlocking() {
1708 screen_->RunOnceBlocking(component_);
1709}
1710
1711void Loop::Run() {
1712 while (!HasQuitted()) {
1713 RunOnceBlocking();
1714 }
1715}
1716
1717} // namespace ftxui
auto PostDelayedTask(Task task, std::chrono::steady_clock::duration duration) -> void
Schedules a task to be executed after a certain duration.
Quirks GetQuirks()
Get the terminal quirks.
Definition terminal.cpp:365
Dimensions Size()
Get the terminal size.
Definition terminal.cpp:312
void SetQuirks(const Quirks &quirks)
Override terminal quirks.
Definition terminal.cpp:375
The FTXUI ftxui::animation:: namespace.
void SetFallbackSize(const Dimensions &fallbackSize)
Override terminal size in case auto-detection fails.
Definition terminal.cpp:342
Color ComputeColorSupport(std::string_view term, std::string_view colorterm, std::string_view term_program, std::string_view terminal_name, std::string_view terminal_emulator_name, const std::vector< int > &capabilities)
Compute the color support based on environment variables and terminal identification.
Definition terminal.cpp:203
std::chrono::duration< float > Duration
Definition animation.hpp:31
std::chrono::time_point< Clock > TimePoint
Definition animation.hpp:30
void RequestAnimationFrame()
Definition app.cpp:74
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
Definition util.hpp:11
The FTXUI ftxui:: namespace.
Definition animation.hpp:11
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
AppDimension
Definition app.cpp:66
std::variant< Event, Closure, AnimationTask > Task
Definition task.hpp:14
void Render(Screen &screen, Node *node, Selection &selection)
Definition node.cpp:105
int value
Definition elements.hpp:188
std::function< void()> Closure
Definition task.hpp:13
std::shared_ptr< ComponentBase > Component
Definition app.hpp:23