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(int dimx,
350 int dimy,
351 Dimension dimension,
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 dimx,
363 dimy,
364 Dimension::Fixed,
365 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 return {
383 0,
384 0,
385 Dimension::Fullscreen,
386 false,
387 };
388}
389
390/// Create a ScreenInteractive taking the full terminal size. This is using the
391/// alternate screen buffer to avoid messing with the terminal content.
392// static
394 return {
395 0,
396 0,
397 Dimension::Fullscreen,
398 true,
399 };
400}
401
402// static
404 return {
405 0,
406 0,
407 Dimension::TerminalOutput,
408 false,
409 };
410}
411
412// static
414 return {
415 0,
416 0,
417 Dimension::FitComponent,
418 false,
419 };
420}
421
422/// @brief Set whether mouse is tracked and events reported.
423/// called outside of the main loop. E.g `ScreenInteractive::Loop(...)`.
424/// @param enable Whether to enable mouse event tracking.
425/// @note This muse be called outside of the main loop. E.g. before calling
426/// `ScreenInteractive::Loop`.
427/// @note Mouse tracking is enabled by default.
428/// @note Mouse tracking is only supported on terminals that supports it.
429///
430/// ### Example
431///
432/// ```cpp
433/// auto screen = ScreenInteractive::TerminalOutput();
434/// screen.TrackMouse(false);
435/// screen.Loop(component);
436/// ```
438 track_mouse_ = enable;
439}
440
441/// @brief Add a task to the main loop.
442/// It will be executed later, after every other scheduled tasks.
444 // Task/Events sent toward inactive screen or screen waiting to become
445 // inactive are dropped.
446 if (!task_sender_) {
447 return;
448 }
449
450 task_sender_->Send(std::move(task));
451}
452
453/// @brief Add an event to the main loop.
454/// It will be executed later, after every other scheduled events.
456 Post(event);
457}
458
459/// @brief Add a task to draw the screen one more time, until all the animations
460/// are done.
462 if (animation_requested_) {
463 return;
464 }
465 animation_requested_ = true;
466 auto now = animation::Clock::now();
467 const auto time_histeresis = std::chrono::milliseconds(33);
468 if (now - previous_animation_time_ >= time_histeresis) {
469 previous_animation_time_ = now;
470 }
471}
472
473/// @brief Try to get the unique lock about behing able to capture the mouse.
474/// @return A unique lock if the mouse is not already captured, otherwise a
475/// null.
477 if (mouse_captured) {
478 return nullptr;
479 }
480 mouse_captured = true;
481 return std::make_unique<CapturedMouseImpl>(
482 [this] { mouse_captured = false; });
483}
484
485/// @brief Execute the main loop.
486/// @param component The component to draw.
487void ScreenInteractive::Loop(Component component) { // NOLINT
488 class Loop loop(this, std::move(component));
489 loop.Run();
490}
491
492/// @brief Return whether the main loop has been quit.
493bool ScreenInteractive::HasQuitted() {
494 return task_receiver_->HasQuitted();
495}
496
497// private
498void ScreenInteractive::PreMain() {
499 // Suspend previously active screen:
500 if (g_active_screen) {
501 std::swap(suspended_screen_, g_active_screen);
502 // Reset cursor position to the top of the screen and clear the screen.
503 suspended_screen_->ResetCursorPosition();
504 std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
505 suspended_screen_->dimx_ = 0;
506 suspended_screen_->dimy_ = 0;
507
508 // Reset dimensions to force drawing the screen again next time:
509 suspended_screen_->Uninstall();
510 }
511
512 // This screen is now active:
513 g_active_screen = this;
514 g_active_screen->Install();
515
516 previous_animation_time_ = animation::Clock::now();
517}
518
519// private
520void ScreenInteractive::PostMain() {
521 // Put cursor position at the end of the drawing.
522 ResetCursorPosition();
523
524 g_active_screen = nullptr;
525
526 // Restore suspended screen.
527 if (suspended_screen_) {
528 // Clear screen, and put the cursor at the beginning of the drawing.
529 std::cout << ResetPosition(/*clear=*/true);
530 dimx_ = 0;
531 dimy_ = 0;
532 Uninstall();
533 std::swap(g_active_screen, suspended_screen_);
534 g_active_screen->Install();
535 } else {
536 Uninstall();
537
538 std::cout << '\r';
539 // On final exit, keep the current drawing and reset cursor position one
540 // line after it.
541 if (!use_alternative_screen_) {
542 std::cout << '\n';
543 std::cout << std::flush;
544 }
545 }
546}
547
548/// @brief Decorate a function. It executes the same way, but with the currently
549/// active screen terminal hooks temporarilly uninstalled during its execution.
550/// @param fn The function to decorate.
552 return [this, fn] {
553 Uninstall();
554 fn();
555 Install();
556 };
557}
558
559/// @brief Force FTXUI to handle or not handle Ctrl-C, even if the component
560/// catches the Event::CtrlC.
562 force_handle_ctrl_c_ = force;
563}
564
565/// @brief Force FTXUI to handle or not handle Ctrl-Z, even if the component
566/// catches the Event::CtrlZ.
568 force_handle_ctrl_z_ = force;
569}
570
571/// @brief Returns the content of the current selection
573 if (!selection_) {
574 return "";
575 }
576 return selection_->GetParts();
577}
578
579void ScreenInteractive::SelectionChange(std::function<void()> callback) {
580 selection_on_change_ = std::move(callback);
581}
582
583/// @brief Return the currently active screen, or null if none.
584// static
586 return g_active_screen;
587}
588
589// private
590void ScreenInteractive::Install() {
591 frame_valid_ = false;
592
593 // Flush the buffer for stdout to ensure whatever the user has printed before
594 // is fully applied before we start modifying the terminal configuration. This
595 // is important, because we are using two different channels (stdout vs
596 // termios/WinAPI) to communicate with the terminal emulator below. See
597 // https://github.com/ArthurSonzogni/FTXUI/issues/846
598 Flush();
599
600 // After uninstalling the new configuration, flush it to the terminal to
601 // ensure it is fully applied:
602 on_exit_functions.emplace([] { Flush(); });
603
604 on_exit_functions.emplace([this] { ExitLoopClosure()(); });
605
606 // Request the terminal to report the current cursor shape. We will restore it
607 // on exit.
608 std::cout << DECRQSS_DECSCUSR;
609 on_exit_functions.emplace([this] {
610 std::cout << "\033[?25h"; // Enable cursor.
611 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
612 });
613
614 // Install signal handlers to restore the terminal state on exit. The default
615 // signal handlers are restored on exit.
616 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
617 InstallSignalHandler(signal);
618 }
619
620// Save the old terminal configuration and restore it on exit.
621#if defined(_WIN32)
622 // Enable VT processing on stdout and stdin
623 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
624 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
625
626 DWORD out_mode = 0;
627 DWORD in_mode = 0;
628 GetConsoleMode(stdout_handle, &out_mode);
629 GetConsoleMode(stdin_handle, &in_mode);
630 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
631 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
632
633 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
634 const int enable_virtual_terminal_processing = 0x0004;
635 const int disable_newline_auto_return = 0x0008;
636 out_mode |= enable_virtual_terminal_processing;
637 out_mode |= disable_newline_auto_return;
638
639 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
640 const int enable_line_input = 0x0002;
641 const int enable_echo_input = 0x0004;
642 const int enable_virtual_terminal_input = 0x0200;
643 const int enable_window_input = 0x0008;
644 in_mode &= ~enable_echo_input;
645 in_mode &= ~enable_line_input;
646 in_mode |= enable_virtual_terminal_input;
647 in_mode |= enable_window_input;
648
649 SetConsoleMode(stdin_handle, in_mode);
650 SetConsoleMode(stdout_handle, out_mode);
651#else
652 for (const int signal : {SIGWINCH, SIGTSTP}) {
653 InstallSignalHandler(signal);
654 }
655
656 struct termios terminal; // NOLINT
657 tcgetattr(STDIN_FILENO, &terminal);
658 on_exit_functions.emplace(
659 [=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
660
661 // Enabling raw terminal input mode
662 terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition
663 terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be
664 // flushed
665 terminal.c_iflag &= ~PARMRK; // Disable marking parity errors.
666 terminal.c_iflag &= ~ISTRIP; // Disable striping 8th bit off characters.
667 terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR.
668 terminal.c_iflag &= ~IGNCR; // Disable ignoring CR.
669 terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL.
670 terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output
671
672 terminal.c_lflag &= ~ECHO; // Disable echoing input characters.
673 terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters.
674 terminal.c_lflag &= ~ICANON; // Disable Canonical mode.
675 terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting:
676 // - => DSUSP
677 // - C-Z => SUSP
678 // - C-C => INTR
679 // - C-d => QUIT
680 terminal.c_lflag &= ~IEXTEN; // Disable extended input processing
681 terminal.c_cflag |= CS8; // 8 bits per byte
682
683 terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical
684 // read.
685 terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read.
686
687 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
688
689#endif
690
691 auto enable = [&](const std::vector<DECMode>& parameters) {
692 std::cout << Set(parameters);
693 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
694 };
695
696 auto disable = [&](const std::vector<DECMode>& parameters) {
697 std::cout << Reset(parameters);
698 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
699 };
700
701 if (use_alternative_screen_) {
702 enable({
703 DECMode::kAlternateScreen,
704 });
705 }
706
707 disable({
708 // DECMode::kCursor,
709 DECMode::kLineWrap,
710 });
711
712 if (track_mouse_) {
713 enable({DECMode::kMouseVt200});
714 enable({DECMode::kMouseAnyEvent});
715 enable({DECMode::kMouseUrxvtMode});
716 enable({DECMode::kMouseSgrExtMode});
717 }
718
719 // After installing the new configuration, flush it to the terminal to
720 // ensure it is fully applied:
721 Flush();
722
723 quit_ = false;
724 task_sender_ = task_receiver_->MakeSender();
725 event_listener_ =
726 std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
727 animation_listener_ =
728 std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
729}
730
731// private
732void ScreenInteractive::Uninstall() {
733 ExitNow();
734 event_listener_.join();
735 animation_listener_.join();
736 OnExit();
737}
738
739// private
740// NOLINTNEXTLINE
741void ScreenInteractive::RunOnceBlocking(Component component) {
742 ExecuteSignalHandlers();
743 Task task;
744 if (task_receiver_->Receive(&task)) {
745 HandleTask(component, task);
746 }
747 RunOnce(component);
748}
749
750// private
751void ScreenInteractive::RunOnce(Component component) {
752 Task task;
753 while (task_receiver_->ReceiveNonBlocking(&task)) {
754 HandleTask(component, task);
755 ExecuteSignalHandlers();
756 }
757 Draw(std::move(component));
758
759 if (selection_data_previous_ != selection_data_) {
760 selection_data_previous_ = selection_data_;
761 if (selection_on_change_) {
762 selection_on_change_();
764 }
765 }
766}
767
768// private
769// NOLINTNEXTLINE
770void ScreenInteractive::HandleTask(Component component, Task& task) {
771 std::visit(
772 [&](auto&& arg) {
773 using T = std::decay_t<decltype(arg)>;
774
775 // clang-format off
776 // Handle Event.
777 if constexpr (std::is_same_v<T, Event>) {
778 if (arg.is_cursor_position()) {
779 cursor_x_ = arg.cursor_x();
780 cursor_y_ = arg.cursor_y();
781 return;
782 }
783
784 if (arg.is_cursor_shape()) {
785 cursor_reset_shape_= arg.cursor_shape();
786 return;
787 }
788
789 if (arg.is_mouse()) {
790 arg.mouse().x -= cursor_x_;
791 arg.mouse().y -= cursor_y_;
792 }
793
794 arg.screen_ = this;
795
796 bool handled = component->OnEvent(arg);
797
798 handled = HandleSelection(handled, arg);
799
800 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
801 RecordSignal(SIGABRT);
802 }
803
804#if !defined(_WIN32)
805 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
806 RecordSignal(SIGTSTP);
807 }
808#endif
809
810 frame_valid_ = false;
811 return;
812 }
813
814 // Handle callback
815 if constexpr (std::is_same_v<T, Closure>) {
816 arg();
817 return;
818 }
819
820 // Handle Animation
821 if constexpr (std::is_same_v<T, AnimationTask>) {
822 if (!animation_requested_) {
823 return;
824 }
825
826 animation_requested_ = false;
827 const animation::TimePoint now = animation::Clock::now();
828 const animation::Duration delta = now - previous_animation_time_;
829 previous_animation_time_ = now;
830
831 animation::Params params(delta);
832 component->OnAnimation(params);
833 frame_valid_ = false;
834 return;
835 }
836 },
837 task);
838 // clang-format on
839}
840
841// private
842bool ScreenInteractive::HandleSelection(bool handled, Event event) {
843 if (handled) {
844 selection_pending_ = nullptr;
845 selection_data_.empty = true;
846 selection_ = nullptr;
847 return true;
848 }
849
850 if (!event.is_mouse()) {
851 return false;
852 }
853
854 auto& mouse = event.mouse();
855 if (mouse.button != Mouse::Left) {
856 return false;
857 }
858
859 if (mouse.motion == Mouse::Pressed) {
860 selection_pending_ = CaptureMouse();
861 selection_data_.start_x = mouse.x;
862 selection_data_.start_y = mouse.y;
863 selection_data_.end_x = mouse.x;
864 selection_data_.end_y = mouse.y;
865 return false;
866 }
867
868 if (!selection_pending_) {
869 return false;
870 }
871
872 if (mouse.motion == Mouse::Moved) {
873 if ((mouse.x != selection_data_.end_x) ||
874 (mouse.y != selection_data_.end_y)) {
875 selection_data_.end_x = mouse.x;
876 selection_data_.end_y = mouse.y;
877 selection_data_.empty = false;
878 }
879
880 return true;
881 }
882
883 if (mouse.motion == Mouse::Released) {
884 selection_pending_ = nullptr;
885 selection_data_.end_x = mouse.x;
886 selection_data_.end_y = mouse.y;
887 selection_data_.empty = false;
888 return true;
889 }
890
891 return false;
892}
893
894// private
895// NOLINTNEXTLINE
896void ScreenInteractive::Draw(Component component) {
897 if (frame_valid_) {
898 return;
899 }
900 auto document = component->Render();
901 int dimx = 0;
902 int dimy = 0;
903 auto terminal = Terminal::Size();
904 document->ComputeRequirement();
905 switch (dimension_) {
906 case Dimension::Fixed:
907 dimx = dimx_;
908 dimy = dimy_;
909 break;
910 case Dimension::TerminalOutput:
911 dimx = terminal.dimx;
912 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
913 break;
914 case Dimension::Fullscreen:
915 dimx = terminal.dimx;
916 dimy = terminal.dimy;
917 break;
918 case Dimension::FitComponent:
919 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
920 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
921 break;
922 }
923
924 const bool resized = (dimx != dimx_) || (dimy != dimy_);
925 ResetCursorPosition();
926 std::cout << ResetPosition(/*clear=*/resized);
927
928 // If the terminal width decrease, the terminal emulator will start wrapping
929 // lines and make the display dirty. We should clear it completely.
930 if ((dimx < dimx_) && !use_alternative_screen_) {
931 std::cout << "\033[J"; // clear terminal output
932 std::cout << "\033[H"; // move cursor to home position
933 }
934
935 // Resize the screen if needed.
936 if (resized) {
937 dimx_ = dimx;
938 dimy_ = dimy;
939 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
940 cursor_.x = dimx_ - 1;
941 cursor_.y = dimy_ - 1;
942 }
943
944 // Periodically request the terminal emulator the frame position relative to
945 // the screen. This is useful for converting mouse position reported in
946 // screen's coordinates to frame's coordinates.
947#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
948 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
949 // position, several output sequences are mixed together into garbage.
950 // This causes FTXUI user to see some "1;1;R" sequences into the Input
951 // component. See [issue]. Solution is to request cursor position less
952 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
953 // https://github.com/ArthurSonzogni/FTXUI/issues/136
954 static int i = -3;
955 ++i;
956 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
957 std::cout << DeviceStatusReport(DSRMode::kCursor);
958 }
959#else
960 static int i = -3;
961 ++i;
962 if (!use_alternative_screen_ &&
963 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
964 std::cout << DeviceStatusReport(DSRMode::kCursor);
965 }
966#endif
967 previous_frame_resized_ = resized;
968
969 selection_ = selection_data_.empty
970 ? std::make_unique<Selection>()
971 : std::make_unique<Selection>(
972 selection_data_.start_x, selection_data_.start_y, //
973 selection_data_.end_x, selection_data_.end_y);
974 Render(*this, document.get(), *selection_);
975
976 // Set cursor position for user using tools to insert CJK characters.
977 {
978 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
979 const int dy = dimy_ - 1 - cursor_.y;
980
981 set_cursor_position.clear();
982 reset_cursor_position.clear();
983
984 if (dy != 0) {
985 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
986 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
987 }
988
989 if (dx != 0) {
990 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
991 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
992 }
993
994 if (cursor_.shape == Cursor::Hidden) {
995 set_cursor_position += "\033[?25l";
996 } else {
997 set_cursor_position += "\033[?25h";
998 set_cursor_position +=
999 "\033[" + std::to_string(int(cursor_.shape)) + " q";
1000 }
1001 }
1002
1003 std::cout << ToString() << set_cursor_position;
1004 Flush();
1005 Clear();
1006 frame_valid_ = true;
1007}
1008
1009// private
1010void ScreenInteractive::ResetCursorPosition() {
1011 std::cout << reset_cursor_position;
1012 reset_cursor_position = "";
1013}
1014
1015/// @brief Return a function to exit the main loop.
1017 return [this] { Exit(); };
1018}
1019
1020/// @brief Exit the main loop.
1022 Post([this] { ExitNow(); });
1023}
1024
1025// private:
1026void ScreenInteractive::ExitNow() {
1027 quit_ = true;
1028 task_sender_.reset();
1029}
1030
1031// private:
1032void ScreenInteractive::Signal(int signal) {
1033 if (signal == SIGABRT) {
1034 Exit();
1035 return;
1036 }
1037
1038// Windows do no support SIGTSTP / SIGWINCH
1039#if !defined(_WIN32)
1040 if (signal == SIGTSTP) {
1041 Post([&] {
1042 ResetCursorPosition();
1043 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
1044 Uninstall();
1045 dimx_ = 0;
1046 dimy_ = 0;
1047 Flush();
1048 std::ignore = std::raise(SIGTSTP);
1049 Install();
1050 });
1051 return;
1052 }
1053
1054 if (signal == SIGWINCH) {
1055 Post(Event::Special({0}));
1056 return;
1057 }
1058#endif
1059}
1060
1061bool ScreenInteractive::SelectionData::operator==(
1062 const ScreenInteractive::SelectionData& other) const {
1063 if (empty && other.empty) {
1064 return true;
1065 }
1066 if (empty || other.empty) {
1067 return false;
1068 }
1069 return start_x == other.start_x && start_y == other.start_y &&
1070 end_x == other.end_x && end_y == other.end_y;
1071}
1072
1073bool ScreenInteractive::SelectionData::operator!=(
1074 const ScreenInteractive::SelectionData& other) const {
1075 return !(*this == other);
1076}
1077
1078} // 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