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