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