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