FTXUI 6.1.9
C++ functional terminal UI.
Loading...
Searching...
No Matches
screen_interactive.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 Pixel, 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 <thread> // for thread, sleep_for
21#include <tuple> // for _Swallow_assign, ignore
22#include <type_traits> // for decay_t
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
31#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver
32#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
33#include "ftxui/dom/node.hpp" // for Node, Render
34#include "ftxui/dom/requirement.hpp" // for Requirement
35#include "ftxui/screen/pixel.hpp" // for Pixel
36#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
37#include "ftxui/screen/util.hpp" // for util::clamp
38
39#if defined(_WIN32)
40#define DEFINE_CONSOLEV2_PROPERTIES
41#define WIN32_LEAN_AND_MEAN
42#ifndef NOMINMAX
43#define NOMINMAX
44#endif
45#include <windows.h>
46#ifndef UNICODE
47#error Must be compiled in UNICODE mode
48#endif
49#else
50#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
51#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
52#include <unistd.h> // for STDIN_FILENO, read
53#endif
54
55// Quick exit is missing in standard CLang headers
56#if defined(__clang__) && defined(__APPLE__)
57#define quick_exit(a) exit(a)
58#endif
59
60namespace ftxui {
61
62namespace animation {
64 auto* screen = ScreenInteractive::Active();
65 if (screen) {
66 screen->RequestAnimationFrame();
67 }
68}
69} // namespace animation
70
71namespace {
72
73ScreenInteractive* g_active_screen = nullptr; // NOLINT
74
75void Flush() {
76 // Emscripten doesn't implement flush. We interpret zero as flush.
77 std::cout << '\0' << std::flush;
78}
79
80constexpr int timeout_milliseconds = 20;
81[[maybe_unused]] constexpr int timeout_microseconds =
82 timeout_milliseconds * 1000;
83#if defined(_WIN32)
84
85void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
86 auto console = GetStdHandle(STD_INPUT_HANDLE);
87 auto parser =
88 TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
89 while (!*quit) {
90 // Throttle ReadConsoleInput by waiting 250ms, this wait function will
91 // return if there is input in the console.
92 auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
93 if (wait_result == WAIT_TIMEOUT) {
94 parser.Timeout(timeout_milliseconds);
95 continue;
96 }
97
98 DWORD number_of_events = 0;
99 if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
100 continue;
101 if (number_of_events <= 0)
102 continue;
103
104 std::vector<INPUT_RECORD> records{number_of_events};
105 DWORD number_of_events_read = 0;
106 ReadConsoleInput(console, records.data(), (DWORD)records.size(),
107 &number_of_events_read);
108 records.resize(number_of_events_read);
109
110 for (const auto& r : records) {
111 switch (r.EventType) {
112 case KEY_EVENT: {
113 auto key_event = r.Event.KeyEvent;
114 // ignore UP key events
115 if (key_event.bKeyDown == FALSE)
116 continue;
117 std::wstring wstring;
118 wstring += key_event.uChar.UnicodeChar;
119 for (auto it : to_string(wstring)) {
120 parser.Add(it);
121 }
122 } break;
123 case WINDOW_BUFFER_SIZE_EVENT:
124 out->Send(Event::Special({0}));
125 break;
126 case MENU_EVENT:
127 case FOCUS_EVENT:
128 case MOUSE_EVENT:
129 // TODO(mauve): Implement later.
130 break;
131 }
132 }
133 }
134}
135
136#elif defined(__EMSCRIPTEN__)
137#include <emscripten.h>
138
139// Read char from the terminal.
140void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
141 auto parser =
142 TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
143
144 char c;
145 while (!*quit) {
146 while (read(STDIN_FILENO, &c, 1), c)
147 parser.Add(c);
148
149 emscripten_sleep(1);
150 parser.Timeout(1);
151 }
152}
153
154extern "C" {
155EMSCRIPTEN_KEEPALIVE
156void ftxui_on_resize(int columns, int rows) {
158 columns,
159 rows,
160 });
161 std::raise(SIGWINCH);
162}
163}
164
165#else // POSIX (Linux & Mac)
166
167int CheckStdinReady(int usec_timeout) {
168 timeval tv = {0, usec_timeout}; // NOLINT
169 fd_set fds;
170 FD_ZERO(&fds); // NOLINT
171 FD_SET(STDIN_FILENO, &fds); // NOLINT
172 select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv); // NOLINT
173 return FD_ISSET(STDIN_FILENO, &fds); // NOLINT
174}
175
176// Read char from the terminal.
177void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
178 auto parser =
179 TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
180
181 while (!*quit) {
182 if (!CheckStdinReady(timeout_microseconds)) {
183 parser.Timeout(timeout_milliseconds);
184 continue;
185 }
186
187 const size_t buffer_size = 100;
188 std::array<char, buffer_size> buffer; // NOLINT;
189 size_t l = read(fileno(stdin), buffer.data(), buffer_size); // NOLINT
190 for (size_t i = 0; i < l; ++i) {
191 parser.Add(buffer[i]); // NOLINT
192 }
193 }
194}
195#endif
196
197std::stack<Closure> on_exit_functions; // NOLINT
198void OnExit() {
199 while (!on_exit_functions.empty()) {
200 on_exit_functions.top()();
201 on_exit_functions.pop();
202 }
203}
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 ScreenInteractive::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 ScreenInteractive::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 ScreenInteractive::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// ST: String Terminator
269const std::string ST = "\x1b\\"; // NOLINT
270
271// DECRQSS: Request Status String
272// DECSCUSR: Set Cursor Style
273const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
274
275// DEC: Digital Equipment Corporation
276enum class DECMode : std::uint16_t {
277 kLineWrap = 7,
278 kCursor = 25,
279
280 kMouseX10 = 9,
281 kMouseVt200 = 1000,
282 kMouseVt200Highlight = 1001,
283
284 kMouseBtnEventMouse = 1002,
285 kMouseAnyEvent = 1003,
286
287 kMouseUtf8 = 1005,
288 kMouseSgrExtMode = 1006,
289 kMouseUrxvtMode = 1015,
290 kMouseSgrPixelsMode = 1016,
291 kAlternateScreen = 1049,
292};
293
294// Device Status Report (DSR) {
295enum class DSRMode : std::uint8_t {
296 kCursor = 6,
297};
298
299std::string Serialize(const std::vector<DECMode>& parameters) {
300 bool first = true;
301 std::string out;
302 for (const DECMode parameter : parameters) {
303 if (!first) {
304 out += ";";
305 }
306 out += std::to_string(int(parameter));
307 first = false;
308 }
309 return out;
310}
311
312// DEC Private Mode Set (DECSET)
313std::string Set(const std::vector<DECMode>& parameters) {
314 return CSI + "?" + Serialize(parameters) + "h";
315}
316
317// DEC Private Mode Reset (DECRST)
318std::string Reset(const std::vector<DECMode>& parameters) {
319 return CSI + "?" + Serialize(parameters) + "l";
320}
321
322// Device Status Report (DSR)
323std::string DeviceStatusReport(DSRMode ps) {
324 return CSI + std::to_string(int(ps)) + "n";
325}
326
327class CapturedMouseImpl : public CapturedMouseInterface {
328 public:
329 explicit CapturedMouseImpl(std::function<void(void)> callback)
330 : callback_(std::move(callback)) {}
331 ~CapturedMouseImpl() override { callback_(); }
332 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
333 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
334 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
335 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
336
337 private:
338 std::function<void(void)> callback_;
339};
340
341void AnimationListener(std::atomic<bool>* quit, Sender<Task> out) {
342 // Animation at around 60fps.
343 const auto time_delta = std::chrono::milliseconds(15);
344 while (!*quit) {
345 out->Send(AnimationTask());
346 std::this_thread::sleep_for(time_delta);
347 }
348}
349
350} // namespace
351
352ScreenInteractive::ScreenInteractive(Dimension dimension,
353 int dimx,
354 int dimy,
355 bool use_alternative_screen)
356 : Screen(dimx, dimy),
357 dimension_(dimension),
358 use_alternative_screen_(use_alternative_screen) {
359 task_receiver_ = MakeReceiver<Task>();
360}
361
362// static
364 return {
365 Dimension::Fixed,
366 dimx,
367 dimy,
368 /*use_alternative_screen=*/false,
369 };
370}
371
372/// Create a ScreenInteractive taking the full terminal size. This is using the
373/// alternate screen buffer to avoid messing with the terminal content.
374/// @note This is the same as `ScreenInteractive::FullscreenAlternateScreen()`
375// static
379
380/// Create a ScreenInteractive taking the full terminal size. The primary screen
381/// buffer is being used. It means if the terminal is resized, the previous
382/// content might mess up with the terminal content.
383// static
385 auto terminal = Terminal::Size();
386 return {
387 Dimension::Fullscreen,
388 terminal.dimx,
389 terminal.dimy,
390 /*use_alternative_screen=*/false,
391 };
392}
393
394/// Create a ScreenInteractive taking the full terminal size. This is using the
395/// alternate screen buffer to avoid messing with the terminal content.
396// static
398 auto terminal = Terminal::Size();
399 return {
400 Dimension::Fullscreen,
401 terminal.dimx,
402 terminal.dimy,
403 /*use_alternative_screen=*/true,
404 };
405}
406
407/// Create a ScreenInteractive whose width match the terminal output width and
408/// the height matches the component being drawn.
409// static
411 auto terminal = Terminal::Size();
412 return {
413 Dimension::TerminalOutput,
414 terminal.dimx,
415 terminal.dimy, // Best guess.
416 /*use_alternative_screen=*/false,
417 };
418}
419
420/// Create a ScreenInteractive whose width and height match the component being
421/// drawn.
422// static
424 auto terminal = Terminal::Size();
425 return {
426 Dimension::FitComponent,
427 terminal.dimx, // Best guess.
428 terminal.dimy, // Best guess.
429 false,
430 };
431}
432
433/// @brief Set whether mouse is tracked and events reported.
434/// called outside of the main loop. E.g `ScreenInteractive::Loop(...)`.
435/// @param enable Whether to enable mouse event tracking.
436/// @note This muse be called outside of the main loop. E.g. before calling
437/// `ScreenInteractive::Loop`.
438/// @note Mouse tracking is enabled by default.
439/// @note Mouse tracking is only supported on terminals that supports it.
440///
441/// ### Example
442///
443/// ```cpp
444/// auto screen = ScreenInteractive::TerminalOutput();
445/// screen.TrackMouse(false);
446/// screen.Loop(component);
447/// ```
449 track_mouse_ = enable;
450}
451
452/// @brief Add a task to the main loop.
453/// It will be executed later, after every other scheduled tasks.
455 // Task/Events sent toward inactive screen or screen waiting to become
456 // inactive are dropped.
457 if (!task_sender_) {
458 return;
459 }
460
461 task_sender_->Send(std::move(task));
462}
463
464/// @brief Add an event to the main loop.
465/// It will be executed later, after every other scheduled events.
467 Post(event);
468}
469
470/// @brief Add a task to draw the screen one more time, until all the animations
471/// are done.
473 if (animation_requested_) {
474 return;
475 }
476 animation_requested_ = true;
477 auto now = animation::Clock::now();
478 const auto time_histeresis = std::chrono::milliseconds(33);
479 if (now - previous_animation_time_ >= time_histeresis) {
480 previous_animation_time_ = now;
481 }
482}
483
484/// @brief Try to get the unique lock about behing able to capture the mouse.
485/// @return A unique lock if the mouse is not already captured, otherwise a
486/// null.
488 if (mouse_captured) {
489 return nullptr;
490 }
491 mouse_captured = true;
492 return std::make_unique<CapturedMouseImpl>(
493 [this] { mouse_captured = false; });
494}
495
496/// @brief Execute the main loop.
497/// @param component The component to draw.
498void ScreenInteractive::Loop(Component component) { // NOLINT
499 class Loop loop(this, std::move(component));
500 loop.Run();
501}
502
503/// @brief Return whether the main loop has been quit.
504bool ScreenInteractive::HasQuitted() {
505 return task_receiver_->HasQuitted();
506}
507
508// private
509void ScreenInteractive::PreMain() {
510 // Suspend previously active screen:
511 if (g_active_screen) {
512 std::swap(suspended_screen_, g_active_screen);
513 // Reset cursor position to the top of the screen and clear the screen.
514 suspended_screen_->ResetCursorPosition();
515 std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
516 suspended_screen_->dimx_ = 0;
517 suspended_screen_->dimy_ = 0;
518
519 // Reset dimensions to force drawing the screen again next time:
520 suspended_screen_->Uninstall();
521 }
522
523 // This screen is now active:
524 g_active_screen = this;
525 g_active_screen->Install();
526
527 previous_animation_time_ = animation::Clock::now();
528}
529
530// private
531void ScreenInteractive::PostMain() {
532 // Put cursor position at the end of the drawing.
533 ResetCursorPosition();
534
535 g_active_screen = nullptr;
536
537 // Restore suspended screen.
538 if (suspended_screen_) {
539 // Clear screen, and put the cursor at the beginning of the drawing.
540 std::cout << ResetPosition(/*clear=*/true);
541 dimx_ = 0;
542 dimy_ = 0;
543 Uninstall();
544 std::swap(g_active_screen, suspended_screen_);
545 g_active_screen->Install();
546 } else {
547 Uninstall();
548
549 std::cout << '\r';
550 // On final exit, keep the current drawing and reset cursor position one
551 // line after it.
552 if (!use_alternative_screen_) {
553 std::cout << '\n';
554 std::cout << std::flush;
555 }
556 }
557}
558
559/// @brief Decorate a function. It executes the same way, but with the currently
560/// active screen terminal hooks temporarilly uninstalled during its execution.
561/// @param fn The function to decorate.
563 return [this, fn] {
564 Uninstall();
565 fn();
566 Install();
567 };
568}
569
570/// @brief Force FTXUI to handle or not handle Ctrl-C, even if the component
571/// catches the Event::CtrlC.
573 force_handle_ctrl_c_ = force;
574}
575
576/// @brief Force FTXUI to handle or not handle Ctrl-Z, even if the component
577/// catches the Event::CtrlZ.
579 force_handle_ctrl_z_ = force;
580}
581
582/// @brief Returns the content of the current selection
584 if (!selection_) {
585 return "";
586 }
587 return selection_->GetParts();
588}
589
590void ScreenInteractive::SelectionChange(std::function<void()> callback) {
591 selection_on_change_ = std::move(callback);
592}
593
594/// @brief Return the currently active screen, or null if none.
595// static
597 return g_active_screen;
598}
599
600// private
601void ScreenInteractive::Install() {
602 frame_valid_ = false;
603
604 // Flush the buffer for stdout to ensure whatever the user has printed before
605 // is fully applied before we start modifying the terminal configuration. This
606 // is important, because we are using two different channels (stdout vs
607 // termios/WinAPI) to communicate with the terminal emulator below. See
608 // https://github.com/ArthurSonzogni/FTXUI/issues/846
609 Flush();
610
611 // After uninstalling the new configuration, flush it to the terminal to
612 // ensure it is fully applied:
613 on_exit_functions.emplace([] { Flush(); });
614
615 on_exit_functions.emplace([this] { ExitLoopClosure()(); });
616
617 // Request the terminal to report the current cursor shape. We will restore it
618 // on exit.
619 std::cout << DECRQSS_DECSCUSR;
620 on_exit_functions.emplace([this] {
621 std::cout << "\033[?25h"; // Enable cursor.
622 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
623 });
624
625 // Install signal handlers to restore the terminal state on exit. The default
626 // signal handlers are restored on exit.
627 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
628 InstallSignalHandler(signal);
629 }
630
631// Save the old terminal configuration and restore it on exit.
632#if defined(_WIN32)
633 // Enable VT processing on stdout and stdin
634 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
635 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
636
637 DWORD out_mode = 0;
638 DWORD in_mode = 0;
639 GetConsoleMode(stdout_handle, &out_mode);
640 GetConsoleMode(stdin_handle, &in_mode);
641 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
642 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
643
644 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
645 const int enable_virtual_terminal_processing = 0x0004;
646 const int disable_newline_auto_return = 0x0008;
647 out_mode |= enable_virtual_terminal_processing;
648 out_mode |= disable_newline_auto_return;
649
650 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
651 const int enable_line_input = 0x0002;
652 const int enable_echo_input = 0x0004;
653 const int enable_virtual_terminal_input = 0x0200;
654 const int enable_window_input = 0x0008;
655 in_mode &= ~enable_echo_input;
656 in_mode &= ~enable_line_input;
657 in_mode |= enable_virtual_terminal_input;
658 in_mode |= enable_window_input;
659
660 SetConsoleMode(stdin_handle, in_mode);
661 SetConsoleMode(stdout_handle, out_mode);
662#else
663 for (const int signal : {SIGWINCH, SIGTSTP}) {
664 InstallSignalHandler(signal);
665 }
666
667 struct termios terminal; // NOLINT
668 tcgetattr(STDIN_FILENO, &terminal);
669 on_exit_functions.emplace(
670 [=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
671
672 // Enabling raw terminal input mode
673 terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition
674 terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be
675 // flushed
676 terminal.c_iflag &= ~PARMRK; // Disable marking parity errors.
677 terminal.c_iflag &= ~ISTRIP; // Disable striping 8th bit off characters.
678 terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR.
679 terminal.c_iflag &= ~IGNCR; // Disable ignoring CR.
680 terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL.
681 terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output
682
683 terminal.c_lflag &= ~ECHO; // Disable echoing input characters.
684 terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters.
685 terminal.c_lflag &= ~ICANON; // Disable Canonical mode.
686 terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting:
687 // - => DSUSP
688 // - C-Z => SUSP
689 // - C-C => INTR
690 // - C-d => QUIT
691 terminal.c_lflag &= ~IEXTEN; // Disable extended input processing
692 terminal.c_cflag |= CS8; // 8 bits per byte
693
694 terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical
695 // read.
696 terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read.
697
698 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
699
700#endif
701
702 auto enable = [&](const std::vector<DECMode>& parameters) {
703 std::cout << Set(parameters);
704 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
705 };
706
707 auto disable = [&](const std::vector<DECMode>& parameters) {
708 std::cout << Reset(parameters);
709 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
710 };
711
712 if (use_alternative_screen_) {
713 enable({
714 DECMode::kAlternateScreen,
715 });
716 }
717
718 disable({
719 // DECMode::kCursor,
720 DECMode::kLineWrap,
721 });
722
723 if (track_mouse_) {
724 enable({DECMode::kMouseVt200});
725 enable({DECMode::kMouseAnyEvent});
726 enable({DECMode::kMouseUrxvtMode});
727 enable({DECMode::kMouseSgrExtMode});
728 }
729
730 // After installing the new configuration, flush it to the terminal to
731 // ensure it is fully applied:
732 Flush();
733
734 quit_ = false;
735 task_sender_ = task_receiver_->MakeSender();
736 event_listener_ =
737 std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
738 animation_listener_ =
739 std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
740}
741
742// private
743void ScreenInteractive::Uninstall() {
744 ExitNow();
745 event_listener_.join();
746 animation_listener_.join();
747 OnExit();
748}
749
750// private
751// NOLINTNEXTLINE
752void ScreenInteractive::RunOnceBlocking(Component component) {
753 ExecuteSignalHandlers();
754 Task task;
755 if (task_receiver_->Receive(&task)) {
756 HandleTask(component, task);
757 }
758 RunOnce(component);
759}
760
761// private
762void ScreenInteractive::RunOnce(Component component) {
763 Task task;
764 while (task_receiver_->ReceiveNonBlocking(&task)) {
765 HandleTask(component, task);
766 ExecuteSignalHandlers();
767 }
768 Draw(std::move(component));
769
770 if (selection_data_previous_ != selection_data_) {
771 selection_data_previous_ = selection_data_;
772 if (selection_on_change_) {
773 selection_on_change_();
775 }
776 }
777}
778
779// private
780// NOLINTNEXTLINE
781void ScreenInteractive::HandleTask(Component component, Task& task) {
782 std::visit(
783 [&](auto&& arg) {
784 using T = std::decay_t<decltype(arg)>;
785
786 // clang-format off
787 // Handle Event.
788 if constexpr (std::is_same_v<T, Event>) {
789 if (arg.is_cursor_position()) {
790 cursor_x_ = arg.cursor_x();
791 cursor_y_ = arg.cursor_y();
792 return;
793 }
794
795 if (arg.is_cursor_shape()) {
796 cursor_reset_shape_= arg.cursor_shape();
797 return;
798 }
799
800 if (arg.is_mouse()) {
801 arg.mouse().x -= cursor_x_;
802 arg.mouse().y -= cursor_y_;
803 }
804
805 arg.screen_ = this;
806
807 bool handled = component->OnEvent(arg);
808
809 handled = HandleSelection(handled, arg);
810
811 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
812 RecordSignal(SIGABRT);
813 }
814
815#if !defined(_WIN32)
816 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
817 RecordSignal(SIGTSTP);
818 }
819#endif
820
821 frame_valid_ = false;
822 return;
823 }
824
825 // Handle callback
826 if constexpr (std::is_same_v<T, Closure>) {
827 arg();
828 return;
829 }
830
831 // Handle Animation
832 if constexpr (std::is_same_v<T, AnimationTask>) {
833 if (!animation_requested_) {
834 return;
835 }
836
837 animation_requested_ = false;
838 const animation::TimePoint now = animation::Clock::now();
839 const animation::Duration delta = now - previous_animation_time_;
840 previous_animation_time_ = now;
841
842 animation::Params params(delta);
843 component->OnAnimation(params);
844 frame_valid_ = false;
845 return;
846 }
847 },
848 task);
849 // clang-format on
850}
851
852// private
853bool ScreenInteractive::HandleSelection(bool handled, Event event) {
854 if (handled) {
855 selection_pending_ = nullptr;
856 selection_data_.empty = true;
857 selection_ = nullptr;
858 return true;
859 }
860
861 if (!event.is_mouse()) {
862 return false;
863 }
864
865 auto& mouse = event.mouse();
866 if (mouse.button != Mouse::Left) {
867 return false;
868 }
869
870 if (mouse.motion == Mouse::Pressed) {
871 selection_pending_ = CaptureMouse();
872 selection_data_.start_x = mouse.x;
873 selection_data_.start_y = mouse.y;
874 selection_data_.end_x = mouse.x;
875 selection_data_.end_y = mouse.y;
876 return false;
877 }
878
879 if (!selection_pending_) {
880 return false;
881 }
882
883 if (mouse.motion == Mouse::Moved) {
884 if ((mouse.x != selection_data_.end_x) ||
885 (mouse.y != selection_data_.end_y)) {
886 selection_data_.end_x = mouse.x;
887 selection_data_.end_y = mouse.y;
888 selection_data_.empty = false;
889 }
890
891 return true;
892 }
893
894 if (mouse.motion == Mouse::Released) {
895 selection_pending_ = nullptr;
896 selection_data_.end_x = mouse.x;
897 selection_data_.end_y = mouse.y;
898 selection_data_.empty = false;
899 return true;
900 }
901
902 return false;
903}
904
905// private
906// NOLINTNEXTLINE
907void ScreenInteractive::Draw(Component component) {
908 if (frame_valid_) {
909 return;
910 }
911 auto document = component->Render();
912 int dimx = 0;
913 int dimy = 0;
914 auto terminal = Terminal::Size();
915 document->ComputeRequirement();
916 switch (dimension_) {
917 case Dimension::Fixed:
918 dimx = dimx_;
919 dimy = dimy_;
920 break;
921 case Dimension::TerminalOutput:
922 dimx = terminal.dimx;
923 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
924 break;
925 case Dimension::Fullscreen:
926 dimx = terminal.dimx;
927 dimy = terminal.dimy;
928 break;
929 case Dimension::FitComponent:
930 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
931 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
932 break;
933 }
934
935 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
936 ResetCursorPosition();
937 std::cout << ResetPosition(/*clear=*/resized);
938
939 // If the terminal width decrease, the terminal emulator will start wrapping
940 // lines and make the display dirty. We should clear it completely.
941 if ((dimx < dimx_) && !use_alternative_screen_) {
942 std::cout << "\033[J"; // clear terminal output
943 std::cout << "\033[H"; // move cursor to home position
944 }
945
946 // Resize the screen if needed.
947 if (resized) {
948 dimx_ = dimx;
949 dimy_ = dimy;
950 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
951 cursor_.x = dimx_ - 1;
952 cursor_.y = dimy_ - 1;
953 }
954
955 // Periodically request the terminal emulator the frame position relative to
956 // the screen. This is useful for converting mouse position reported in
957 // screen's coordinates to frame's coordinates.
958#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
959 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
960 // position, several output sequences are mixed together into garbage.
961 // This causes FTXUI user to see some "1;1;R" sequences into the Input
962 // component. See [issue]. Solution is to request cursor position less
963 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
964 // https://github.com/ArthurSonzogni/FTXUI/issues/136
965 static int i = -3;
966 ++i;
967 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
968 std::cout << DeviceStatusReport(DSRMode::kCursor);
969 }
970#else
971 static int i = -3;
972 ++i;
973 if (!use_alternative_screen_ &&
974 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
975 std::cout << DeviceStatusReport(DSRMode::kCursor);
976 }
977#endif
978 previous_frame_resized_ = resized;
979
980 selection_ = selection_data_.empty
981 ? std::make_unique<Selection>()
982 : std::make_unique<Selection>(
983 selection_data_.start_x, selection_data_.start_y, //
984 selection_data_.end_x, selection_data_.end_y);
985 Render(*this, document.get(), *selection_);
986
987 // Set cursor position for user using tools to insert CJK characters.
988 {
989 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
990 const int dy = dimy_ - 1 - cursor_.y;
991
992 set_cursor_position.clear();
993 reset_cursor_position.clear();
994
995 if (dy != 0) {
996 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
997 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
998 }
999
1000 if (dx != 0) {
1001 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
1002 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
1003 }
1004
1005 if (cursor_.shape == Cursor::Hidden) {
1006 set_cursor_position += "\033[?25l";
1007 } else {
1008 set_cursor_position += "\033[?25h";
1009 set_cursor_position +=
1010 "\033[" + std::to_string(int(cursor_.shape)) + " q";
1011 }
1012 }
1013
1014 std::cout << ToString() << set_cursor_position;
1015 Flush();
1016 Clear();
1017 frame_valid_ = true;
1018 frame_count_++;
1019}
1020
1021// private
1022void ScreenInteractive::ResetCursorPosition() {
1023 std::cout << reset_cursor_position;
1024 reset_cursor_position = "";
1025}
1026
1027/// @brief Return a function to exit the main loop.
1029 return [this] { Exit(); };
1030}
1031
1032/// @brief Exit the main loop.
1034 Post([this] { ExitNow(); });
1035}
1036
1037// private:
1038void ScreenInteractive::ExitNow() {
1039 quit_ = true;
1040 task_sender_.reset();
1041}
1042
1043// private:
1044void ScreenInteractive::Signal(int signal) {
1045 if (signal == SIGABRT) {
1046 Exit();
1047 return;
1048 }
1049
1050// Windows do no support SIGTSTP / SIGWINCH
1051#if !defined(_WIN32)
1052 if (signal == SIGTSTP) {
1053 Post([&] {
1054 ResetCursorPosition();
1055 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
1056 Uninstall();
1057 dimx_ = 0;
1058 dimy_ = 0;
1059 Flush();
1060 std::ignore = std::raise(SIGTSTP);
1061 Install();
1062 });
1063 return;
1064 }
1065
1066 if (signal == SIGWINCH) {
1067 Post(Event::Special({0}));
1068 return;
1069 }
1070#endif
1071}
1072
1073bool ScreenInteractive::SelectionData::operator==(
1074 const ScreenInteractive::SelectionData& other) const {
1075 if (empty && other.empty) {
1076 return true;
1077 }
1078 if (empty || other.empty) {
1079 return false;
1080 }
1081 return start_x == other.start_x && start_y == other.start_y &&
1082 end_x == other.end_x && end_y == other.end_y;
1083}
1084
1085bool ScreenInteractive::SelectionData::operator!=(
1086 const ScreenInteractive::SelectionData& other) const {
1087 return !(*this == other);
1088}
1089
1090} // namespace ftxui.
static void Signal(ScreenInteractive &s, int signal)
static const Event CtrlC
Definition event.hpp:71
static ScreenInteractive TerminalOutput()
bool HasQuitted()
Whether the loop has quitted.
Definition loop.cpp:30
void Exit()
Exit the main loop.
static const Event CtrlZ
Definition event.hpp:94
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
Add an event to the main loop. It will be executed later, after every other scheduled events.
void Post(Task task)
Add a task to the main loop. It will be executed later, after every other scheduled tasks.
static ScreenInteractive FitComponent()
static ScreenInteractive Fullscreen()
static const Event Custom
Definition event.hpp:97
static ScreenInteractive FullscreenPrimaryScreen()
static ScreenInteractive * Active()
Return the currently active screen, or null if none.
CapturedMouse CaptureMouse()
Try to get the unique lock about behing able to capture the mouse.
std::string GetSelection()
Returns the content of the current selection.
static ScreenInteractive FullscreenAlternateScreen()
void TrackMouse(bool enable=true)
Set whether mouse is tracked and events reported. called outside of the main loop....
void SelectionChange(std::function< void()> callback)
void RequestAnimationFrame()
Add a task to draw the screen one more time, until all the animations are done.
Closure ExitLoopClosure()
Return a function to exit the main loop.
void ForceHandleCtrlC(bool force)
Force FTXUI to handle or not handle Ctrl-C, even if the component catches the Event::CtrlC.
void ForceHandleCtrlZ(bool force)
Force FTXUI to handle or not handle Ctrl-Z, even if the component catches the Event::CtrlZ.
Closure WithRestoredIO(Closure)
Decorate a function. It executes the same way, but with the currently active screen terminal hooks te...
static Event Special(std::string)
An custom event whose meaning is defined by the user of the library.
Definition event.cpp:74
Loop is a class that manages the event loop for a component.
Definition loop.hpp:56
ScreenInteractive is a Screen that can handle events, run a main loop, and manage components.
void RequestAnimationFrame()
RequestAnimationFrame is a function that requests a new frame to be drawn in the next animation cycle...
Represent an event. It can be key press event, a terminal resize, or more ...
Definition event.hpp:29
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
Definition node.cpp:84
int dimy() const
Definition image.hpp:33
std::string ToString() const
Definition screen.cpp:415
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:475
Cursor cursor_
Definition screen.hpp:77
void Clear()
Clear all the pixel from the screen.
Definition screen.cpp:494
int dimx() const
Definition image.hpp:32
std::vector< std::vector< Pixel > > pixels_
Definition image.hpp:43
Dimensions Size()
Get the terminal size.
Definition terminal.cpp:94
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
Receiver< T > MakeReceiver()
Definition receiver.hpp:139
std::string to_string(const std::wstring &s)
Convert a std::wstring into a UTF8 std::string.
Definition string.cpp:1565
std::unique_ptr< SenderImpl< T > > Sender
Definition receiver.hpp:45
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