FTXUI  5.0.0
C++ functional terminal UI.
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.
4#include <algorithm> // for copy, max, min
5#include <array> // for array
6#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
7#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
8#include <cstdio> // for fileno, stdin
9#include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
10#include <ftxui/screen/screen.hpp> // for Pixel, Screen::Cursor, Screen, Screen::Cursor::Hidden
11#include <functional> // for function
12#include <initializer_list> // for initializer_list
13#include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
14#include <stack> // for stack
15#include <thread> // for thread, sleep_for
16#include <tuple> // for _Swallow_assign, ignore
17#include <type_traits> // for decay_t
18#include <utility> // for move, swap
19#include <variant> // for visit, variant
20#include <vector> // for vector
21
22#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
23#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
24#include "ftxui/component/component_base.hpp" // for ComponentBase
25#include "ftxui/component/event.hpp" // for Event
26#include "ftxui/component/loop.hpp" // for Loop
27#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver
29#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
30#include "ftxui/dom/node.hpp" // for Node, Render
31#include "ftxui/dom/requirement.hpp" // for Requirement
32#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
33
34#if defined(_WIN32)
35#define DEFINE_CONSOLEV2_PROPERTIES
36#define WIN32_LEAN_AND_MEAN
37#ifndef NOMINMAX
38#define NOMINMAX
39#endif
40#include <windows.h>
41#ifndef UNICODE
42#error Must be compiled in UNICODE mode
43#endif
44#else
45#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
46#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
47#include <unistd.h> // for STDIN_FILENO, read
48#endif
49
50// Quick exit is missing in standard CLang headers
51#if defined(__clang__) && defined(__APPLE__)
52#define quick_exit(a) exit(a)
53#endif
54
55namespace ftxui {
56
57namespace animation {
59 auto* screen = ScreenInteractive::Active();
60 if (screen) {
61 screen->RequestAnimationFrame();
62 }
63}
64} // namespace animation
65
66namespace {
67
68ScreenInteractive* g_active_screen = nullptr; // NOLINT
69
70void Flush() {
71 // Emscripten doesn't implement flush. We interpret zero as flush.
72 std::cout << '\0' << std::flush;
73}
74
75constexpr int timeout_milliseconds = 20;
76[[maybe_unused]] constexpr int timeout_microseconds =
77 timeout_milliseconds * 1000;
78#if defined(_WIN32)
79
80void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
81 auto console = GetStdHandle(STD_INPUT_HANDLE);
82 auto parser = TerminalInputParser(out->Clone());
83 while (!*quit) {
84 // Throttle ReadConsoleInput by waiting 250ms, this wait function will
85 // return if there is input in the console.
86 auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
87 if (wait_result == WAIT_TIMEOUT) {
88 parser.Timeout(timeout_milliseconds);
89 continue;
90 }
91
92 DWORD number_of_events = 0;
93 if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
94 continue;
95 if (number_of_events <= 0)
96 continue;
97
98 std::vector<INPUT_RECORD> records{number_of_events};
99 DWORD number_of_events_read = 0;
100 ReadConsoleInput(console, records.data(), (DWORD)records.size(),
101 &number_of_events_read);
102 records.resize(number_of_events_read);
103
104 for (const auto& r : records) {
105 switch (r.EventType) {
106 case KEY_EVENT: {
107 auto key_event = r.Event.KeyEvent;
108 // ignore UP key events
109 if (key_event.bKeyDown == FALSE)
110 continue;
111 std::wstring wstring;
112 wstring += key_event.uChar.UnicodeChar;
113 for (auto it : to_string(wstring)) {
114 parser.Add(it);
115 }
116 } break;
117 case WINDOW_BUFFER_SIZE_EVENT:
118 out->Send(Event::Special({0}));
119 break;
120 case MENU_EVENT:
121 case FOCUS_EVENT:
122 case MOUSE_EVENT:
123 // TODO(mauve): Implement later.
124 break;
125 }
126 }
127 }
128}
129
130#elif defined(__EMSCRIPTEN__)
131#include <emscripten.h>
132
133// Read char from the terminal.
134void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
135 (void)timeout_microseconds;
136 auto parser = TerminalInputParser(std::move(out));
137
138 char c;
139 while (!*quit) {
140 while (read(STDIN_FILENO, &c, 1), c)
141 parser.Add(c);
142
143 emscripten_sleep(1);
144 parser.Timeout(1);
145 }
146}
147
148extern "C" {
149EMSCRIPTEN_KEEPALIVE
150void ftxui_on_resize(int columns, int rows) {
152 columns,
153 rows,
154 });
155 std::raise(SIGWINCH);
156}
157}
158
159#else // POSIX (Linux & Mac)
160
161int CheckStdinReady(int usec_timeout) {
162 timeval tv = {0, usec_timeout};
163 fd_set fds;
164 FD_ZERO(&fds); // NOLINT
165 FD_SET(STDIN_FILENO, &fds); // NOLINT
166 select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv); // NOLINT
167 return FD_ISSET(STDIN_FILENO, &fds); // NOLINT
168}
169
170// Read char from the terminal.
171void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
172 auto parser = TerminalInputParser(std::move(out));
173
174 while (!*quit) {
175 if (!CheckStdinReady(timeout_microseconds)) {
176 parser.Timeout(timeout_milliseconds);
177 continue;
178 }
179
180 const size_t buffer_size = 100;
181 std::array<char, buffer_size> buffer; // NOLINT;
182 size_t l = read(fileno(stdin), buffer.data(), buffer_size); // NOLINT
183 for (size_t i = 0; i < l; ++i) {
184 parser.Add(buffer[i]); // NOLINT
185 }
186 }
187}
188#endif
189
190std::stack<Closure> on_exit_functions; // NOLINT
191void OnExit() {
192 while (!on_exit_functions.empty()) {
193 on_exit_functions.top()();
194 on_exit_functions.pop();
195 }
196}
197
198std::atomic<int> g_signal_exit_count = 0; // NOLINT
199#if !defined(_WIN32)
200std::atomic<int> g_signal_stop_count = 0; // NOLINT
201std::atomic<int> g_signal_resize_count = 0; // NOLINT
202#endif
203
204// Async signal safe function
205void RecordSignal(int signal) {
206 switch (signal) {
207 case SIGABRT:
208 case SIGFPE:
209 case SIGILL:
210 case SIGINT:
211 case SIGSEGV:
212 case SIGTERM:
213 g_signal_exit_count++;
214 break;
215
216#if !defined(_WIN32)
217 case SIGTSTP:
218 g_signal_stop_count++;
219 break;
220
221 case SIGWINCH:
222 g_signal_resize_count++;
223 break;
224#endif
225
226 default:
227 break;
228 }
229}
230
231void ExecuteSignalHandlers() {
232 int signal_exit_count = g_signal_exit_count.exchange(0);
233 while (signal_exit_count--) {
234 ScreenInteractive::Private::Signal(*g_active_screen, SIGABRT);
235 }
236
237#if !defined(_WIN32)
238 int signal_stop_count = g_signal_stop_count.exchange(0);
239 while (signal_stop_count--) {
240 ScreenInteractive::Private::Signal(*g_active_screen, SIGTSTP);
241 }
242
243 int signal_resize_count = g_signal_resize_count.exchange(0);
244 while (signal_resize_count--) {
245 ScreenInteractive::Private::Signal(*g_active_screen, SIGWINCH);
246 }
247#endif
248}
249
250void InstallSignalHandler(int sig) {
251 auto old_signal_handler = std::signal(sig, RecordSignal);
252 on_exit_functions.push(
253 [=] { std::ignore = std::signal(sig, old_signal_handler); });
254}
255
256// CSI: Control Sequence Introducer
257const std::string CSI = "\x1b["; // NOLINT
258 //
259// DCS: Device Control String
260const std::string DCS = "\x1bP"; // NOLINT
261// ST: String Terminator
262const std::string ST = "\x1b\\"; // NOLINT
263
264// DECRQSS: Request Status String
265// DECSCUSR: Set Cursor Style
266const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
267
268// DEC: Digital Equipment Corporation
269enum class DECMode {
270 kLineWrap = 7,
271 kCursor = 25,
272
273 kMouseX10 = 9,
274 kMouseVt200 = 1000,
275 kMouseVt200Highlight = 1001,
276
277 kMouseBtnEventMouse = 1002,
278 kMouseAnyEvent = 1003,
279
280 kMouseUtf8 = 1005,
281 kMouseSgrExtMode = 1006,
282 kMouseUrxvtMode = 1015,
283 kMouseSgrPixelsMode = 1016,
284 kAlternateScreen = 1049,
285};
286
287// Device Status Report (DSR) {
288enum class DSRMode {
289 kCursor = 6,
290};
291
292std::string Serialize(const std::vector<DECMode>& parameters) {
293 bool first = true;
294 std::string out;
295 for (const DECMode parameter : parameters) {
296 if (!first) {
297 out += ";";
298 }
299 out += std::to_string(int(parameter));
300 first = false;
301 }
302 return out;
303}
304
305// DEC Private Mode Set (DECSET)
306std::string Set(const std::vector<DECMode>& parameters) {
307 return CSI + "?" + Serialize(parameters) + "h";
308}
309
310// DEC Private Mode Reset (DECRST)
311std::string Reset(const std::vector<DECMode>& parameters) {
312 return CSI + "?" + Serialize(parameters) + "l";
313}
314
315// Device Status Report (DSR)
316std::string DeviceStatusReport(DSRMode ps) {
317 return CSI + std::to_string(int(ps)) + "n";
318}
319
320class CapturedMouseImpl : public CapturedMouseInterface {
321 public:
322 explicit CapturedMouseImpl(std::function<void(void)> callback)
323 : callback_(std::move(callback)) {}
324 ~CapturedMouseImpl() override { callback_(); }
325 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
326 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
327 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
328 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
329
330 private:
331 std::function<void(void)> callback_;
332};
333
334void AnimationListener(std::atomic<bool>* quit, Sender<Task> out) {
335 // Animation at around 60fps.
336 const auto time_delta = std::chrono::milliseconds(15);
337 while (!*quit) {
338 out->Send(AnimationTask());
339 std::this_thread::sleep_for(time_delta);
340 }
341}
342
343} // namespace
344
345ScreenInteractive::ScreenInteractive(int dimx,
346 int dimy,
347 Dimension dimension,
348 bool use_alternative_screen)
349 : Screen(dimx, dimy),
350 dimension_(dimension),
351 use_alternative_screen_(use_alternative_screen) {
352 task_receiver_ = MakeReceiver<Task>();
353}
354
355// static
357 return {
358 dimx,
359 dimy,
360 Dimension::Fixed,
361 false,
362 };
363}
364
365/// @ingroup component
366/// Create a ScreenInteractive taking the full terminal size. This is using the
367/// alternate screen buffer to avoid messing with the terminal content.
368/// @note This is the same as `ScreenInteractive::FullscreenAlternateScreen()`
369// static
372}
373
374/// @ingroup component
375/// Create a ScreenInteractive taking the full terminal size. The primary screen
376/// buffer is being used. It means if the terminal is resized, the previous
377/// content might mess up with the terminal content.
378// static
380 return {
381 0,
382 0,
383 Dimension::Fullscreen,
384 false,
385 };
386}
387
388/// @ingroup component
389/// Create a ScreenInteractive taking the full terminal size. This is using the
390/// alternate screen buffer to avoid messing with the terminal content.
391// static
393 return {
394 0,
395 0,
396 Dimension::Fullscreen,
397 true,
398 };
399}
400
401// static
403 return {
404 0,
405 0,
406 Dimension::TerminalOutput,
407 false,
408 };
409}
410
411// static
413 return {
414 0,
415 0,
416 Dimension::FitComponent,
417 false,
418 };
419}
420
421/// @ingroup component
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.
443/// @ingroup component
445 // Task/Events sent toward inactive screen or screen waiting to become
446 // inactive are dropped.
447 if (!task_sender_) {
448 return;
449 }
450
451 task_sender_->Send(std::move(task));
452}
453
454/// @brief Add an event to the main loop.
455/// It will be executed later, after every other scheduled events.
456/// @ingroup component
458 Post(event);
459}
460
461/// @brief Add a task to draw the screen one more time, until all the animations
462/// are done.
464 if (animation_requested_) {
465 return;
466 }
467 animation_requested_ = true;
468 auto now = animation::Clock::now();
469 const auto time_histeresis = std::chrono::milliseconds(33);
470 if (now - previous_animation_time_ >= time_histeresis) {
471 previous_animation_time_ = now;
472 }
473}
474
475/// @brief Try to get the unique lock about behing able to capture the mouse.
476/// @return A unique lock if the mouse is not already captured, otherwise a
477/// null.
478/// @ingroup component
480 if (mouse_captured) {
481 return nullptr;
482 }
483 mouse_captured = true;
484 return std::make_unique<CapturedMouseImpl>(
485 [this] { mouse_captured = false; });
486}
487
488/// @brief Execute the main loop.
489/// @param component The component to draw.
490/// @ingroup component
491void ScreenInteractive::Loop(Component component) { // NOLINT
492 class Loop loop(this, std::move(component));
493 loop.Run();
494}
495
496/// @brief Return whether the main loop has been quit.
497/// @ingroup component
498bool ScreenInteractive::HasQuitted() {
499 return task_receiver_->HasQuitted();
500}
501
502// private
503void ScreenInteractive::PreMain() {
504 // Suspend previously active screen:
505 if (g_active_screen) {
506 std::swap(suspended_screen_, g_active_screen);
507 // Reset cursor position to the top of the screen and clear the screen.
508 suspended_screen_->ResetCursorPosition();
509 std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
510 suspended_screen_->dimx_ = 0;
511 suspended_screen_->dimy_ = 0;
512
513 // Reset dimensions to force drawing the screen again next time:
514 suspended_screen_->Uninstall();
515 }
516
517 // This screen is now active:
518 g_active_screen = this;
519 g_active_screen->Install();
520
521 previous_animation_time_ = animation::Clock::now();
522}
523
524// private
525void ScreenInteractive::PostMain() {
526 // Put cursor position at the end of the drawing.
527 ResetCursorPosition();
528
529 g_active_screen = nullptr;
530
531 // Restore suspended screen.
532 if (suspended_screen_) {
533 // Clear screen, and put the cursor at the beginning of the drawing.
534 std::cout << ResetPosition(/*clear=*/true);
535 dimx_ = 0;
536 dimy_ = 0;
537 Uninstall();
538 std::swap(g_active_screen, suspended_screen_);
539 g_active_screen->Install();
540 } else {
541 Uninstall();
542
543 std::cout << '\r';
544 // On final exit, keep the current drawing and reset cursor position one
545 // line after it.
546 if (!use_alternative_screen_) {
547 std::cout << std::endl;
548 }
549 }
550}
551
552/// @brief Decorate a function. It executes the same way, but with the currently
553/// active screen terminal hooks temporarilly uninstalled during its execution.
554/// @param fn The function to decorate.
556 return [this, fn] {
557 Uninstall();
558 fn();
559 Install();
560 };
561}
562
563/// @brief Return the currently active screen, or null if none.
564// static
566 return g_active_screen;
567}
568
569// private
570void ScreenInteractive::Install() {
571 frame_valid_ = false;
572
573 // After uninstalling the new configuration, flush it to the terminal to
574 // ensure it is fully applied:
575 on_exit_functions.push([] { Flush(); });
576
577 on_exit_functions.push([this] { ExitLoopClosure()(); });
578
579 // Request the terminal to report the current cursor shape. We will restore it
580 // on exit.
581 std::cout << DECRQSS_DECSCUSR;
582 on_exit_functions.push([=] {
583 std::cout << "\033[?25h"; // Enable cursor.
584 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
585 });
586
587 // Install signal handlers to restore the terminal state on exit. The default
588 // signal handlers are restored on exit.
589 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
590 InstallSignalHandler(signal);
591 }
592
593// Save the old terminal configuration and restore it on exit.
594#if defined(_WIN32)
595 // Enable VT processing on stdout and stdin
596 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
597 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
598
599 DWORD out_mode = 0;
600 DWORD in_mode = 0;
601 GetConsoleMode(stdout_handle, &out_mode);
602 GetConsoleMode(stdin_handle, &in_mode);
603 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
604 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
605
606 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
607 const int enable_virtual_terminal_processing = 0x0004;
608 const int disable_newline_auto_return = 0x0008;
609 out_mode |= enable_virtual_terminal_processing;
610 out_mode |= disable_newline_auto_return;
611
612 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
613 const int enable_line_input = 0x0002;
614 const int enable_echo_input = 0x0004;
615 const int enable_virtual_terminal_input = 0x0200;
616 const int enable_window_input = 0x0008;
617 in_mode &= ~enable_echo_input;
618 in_mode &= ~enable_line_input;
619 in_mode |= enable_virtual_terminal_input;
620 in_mode |= enable_window_input;
621
622 SetConsoleMode(stdin_handle, in_mode);
623 SetConsoleMode(stdout_handle, out_mode);
624#else
625 for (const int signal : {SIGWINCH, SIGTSTP}) {
626 InstallSignalHandler(signal);
627 }
628
629 struct termios terminal; // NOLINT
630 tcgetattr(STDIN_FILENO, &terminal);
631 on_exit_functions.push([=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
632
633 terminal.c_lflag &= ~ICANON; // NOLINT Non canonique terminal.
634 terminal.c_lflag &= ~ECHO; // NOLINT Do not print after a key press.
635 terminal.c_cc[VMIN] = 0;
636 terminal.c_cc[VTIME] = 0;
637 // auto oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
638 // fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
639 // on_exit_functions.push([=] { fcntl(STDIN_FILENO, F_GETFL, oldf); });
640
641 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
642
643#endif
644
645 auto enable = [&](const std::vector<DECMode>& parameters) {
646 std::cout << Set(parameters);
647 on_exit_functions.push([=] { std::cout << Reset(parameters); });
648 };
649
650 auto disable = [&](const std::vector<DECMode>& parameters) {
651 std::cout << Reset(parameters);
652 on_exit_functions.push([=] { std::cout << Set(parameters); });
653 };
654
655 if (use_alternative_screen_) {
656 enable({
657 DECMode::kAlternateScreen,
658 });
659 }
660
661 disable({
662 // DECMode::kCursor,
663 DECMode::kLineWrap,
664 });
665
666 if (track_mouse_) {
667 enable({DECMode::kMouseVt200});
668 enable({DECMode::kMouseAnyEvent});
669 enable({DECMode::kMouseUrxvtMode});
670 enable({DECMode::kMouseSgrExtMode});
671 }
672
673 // After installing the new configuration, flush it to the terminal to
674 // ensure it is fully applied:
675 Flush();
676
677 quit_ = false;
678 task_sender_ = task_receiver_->MakeSender();
679 event_listener_ =
680 std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
681 animation_listener_ =
682 std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
683}
684
685// private
686void ScreenInteractive::Uninstall() {
687 ExitNow();
688 event_listener_.join();
689 animation_listener_.join();
690 OnExit();
691}
692
693// private
694// NOLINTNEXTLINE
695void ScreenInteractive::RunOnceBlocking(Component component) {
696 ExecuteSignalHandlers();
697 Task task;
698 if (task_receiver_->Receive(&task)) {
699 HandleTask(component, task);
700 }
701 RunOnce(component);
702}
703
704// private
705void ScreenInteractive::RunOnce(Component component) {
706 Task task;
707 while (task_receiver_->ReceiveNonBlocking(&task)) {
708 HandleTask(component, task);
709 ExecuteSignalHandlers();
710 }
711 Draw(std::move(component));
712}
713
714// private
715void ScreenInteractive::HandleTask(Component component, Task& task) {
716 std::visit(
717 [&](auto&& arg) {
718 using T = std::decay_t<decltype(arg)>;
719
720 // clang-format off
721 // Handle Event.
722 if constexpr (std::is_same_v<T, Event>) {
723 if (arg.is_cursor_position()) {
724 cursor_x_ = arg.cursor_x();
725 cursor_y_ = arg.cursor_y();
726 return;
727 }
728
729 if (arg.is_cursor_shape()) {
730 cursor_reset_shape_= arg.cursor_shape();
731 return;
732 }
733
734 if (arg.is_mouse()) {
735 arg.mouse().x -= cursor_x_;
736 arg.mouse().y -= cursor_y_;
737 arg.mouse().previous = &latest_mouse_event_;
738 }
739
740 arg.screen_ = this;
741 component->OnEvent(arg);
742 frame_valid_ = false;
743
744 if (arg.is_mouse()) {
745 latest_mouse_event_ = arg.mouse();
746 latest_mouse_event_.previous = nullptr;
747 }
748 return;
749 }
750
751 // Handle callback
752 if constexpr (std::is_same_v<T, Closure>) {
753 arg();
754 return;
755 }
756
757 // Handle Animation
758 if constexpr (std::is_same_v<T, AnimationTask>) {
759 if (!animation_requested_) {
760 return;
761 }
762
763 animation_requested_ = false;
764 const animation::TimePoint now = animation::Clock::now();
765 const animation::Duration delta = now - previous_animation_time_;
766 previous_animation_time_ = now;
767
768 animation::Params params(delta);
769 component->OnAnimation(params);
770 frame_valid_ = false;
771 return;
772 }
773 },
774 task);
775 // clang-format on
776}
777
778// private
779// NOLINTNEXTLINE
780void ScreenInteractive::Draw(Component component) {
781 if (frame_valid_) {
782 return;
783 }
784 auto document = component->Render();
785 int dimx = 0;
786 int dimy = 0;
787 auto terminal = Terminal::Size();
788 document->ComputeRequirement();
789 switch (dimension_) {
790 case Dimension::Fixed:
791 dimx = dimx_;
792 dimy = dimy_;
793 break;
794 case Dimension::TerminalOutput:
795 dimx = terminal.dimx;
796 dimy = document->requirement().min_y;
797 break;
798 case Dimension::Fullscreen:
799 dimx = terminal.dimx;
800 dimy = terminal.dimy;
801 break;
802 case Dimension::FitComponent:
803 dimx = std::min(document->requirement().min_x, terminal.dimx);
804 dimy = std::min(document->requirement().min_y, terminal.dimy);
805 break;
806 }
807
808 const bool resized = (dimx != dimx_) || (dimy != dimy_);
809 ResetCursorPosition();
810 std::cout << ResetPosition(/*clear=*/resized);
811
812 // Resize the screen if needed.
813 if (resized) {
814 dimx_ = dimx;
815 dimy_ = dimy;
816 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
817 cursor_.x = dimx_ - 1;
818 cursor_.y = dimy_ - 1;
819 }
820
821 // Periodically request the terminal emulator the frame position relative to
822 // the screen. This is useful for converting mouse position reported in
823 // screen's coordinates to frame's coordinates.
824#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
825 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
826 // position, several output sequences are mixed together into garbage.
827 // This causes FTXUI user to see some "1;1;R" sequences into the Input
828 // component. See [issue]. Solution is to request cursor position less
829 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
830 // https://github.com/ArthurSonzogni/FTXUI/issues/136
831 static int i = -3;
832 ++i;
833 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
834 std::cout << DeviceStatusReport(DSRMode::kCursor);
835 }
836#else
837 static int i = -3;
838 ++i;
839 if (!use_alternative_screen_ &&
840 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
841 std::cout << DeviceStatusReport(DSRMode::kCursor);
842 }
843#endif
844 previous_frame_resized_ = resized;
845
846 Render(*this, document);
847
848 // Set cursor position for user using tools to insert CJK characters.
849 {
850 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
851 const int dy = dimy_ - 1 - cursor_.y;
852
853 set_cursor_position = "\x1B[" + std::to_string(dy) + "A" + //
854 "\x1B[" + std::to_string(dx) + "D";
855 reset_cursor_position = "\x1B[" + std::to_string(dy) + "B" + //
856 "\x1B[" + std::to_string(dx) + "C";
857
859 set_cursor_position += "\033[?25l";
860 } else {
861 set_cursor_position += "\033[?25h";
862 set_cursor_position +=
863 "\033[" + std::to_string(int(cursor_.shape)) + " q";
864 }
865 }
866
867 std::cout << ToString() << set_cursor_position;
868 Flush();
869 Clear();
870 frame_valid_ = true;
871}
872
873// private
874void ScreenInteractive::ResetCursorPosition() {
875 std::cout << reset_cursor_position;
876 reset_cursor_position = "";
877}
878
879/// @brief Return a function to exit the main loop.
880/// @ingroup component
882 return [this] { Exit(); };
883}
884
885/// @brief Exit the main loop.
886/// @ingroup component
888 Post([this] { ExitNow(); });
889}
890
891// private:
892void ScreenInteractive::ExitNow() {
893 quit_ = true;
894 task_sender_.reset();
895}
896
897// private:
898void ScreenInteractive::Signal(int signal) {
899 if (signal == SIGABRT) {
900 OnExit();
901 return;
902 }
903
904// Windows do no support SIGTSTP / SIGWINCH
905#if !defined(_WIN32)
906 if (signal == SIGTSTP) {
907 Post([&] {
908 ResetCursorPosition();
909 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
910 Uninstall();
911 dimx_ = 0;
912 dimy_ = 0;
913 Flush();
914 std::ignore = std::raise(SIGTSTP);
915 Install();
916 });
917 return;
918 }
919
920 if (signal == SIGWINCH) {
921 Post(Event::Special({0}));
922 return;
923 }
924#endif
925}
926
927} // namespace ftxui.
bool HasQuitted()
Whether the loop has quitted.
Definition: loop.cpp:32
static void Signal(ScreenInteractive &s, int signal)
static ScreenInteractive TerminalOutput()
void Exit()
Exit the main loop.
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 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.
static ScreenInteractive FullscreenAlternateScreen()
void TrackMouse(bool enable=true)
Set whether mouse is tracked and events reported. called outside of the main loop....
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.
Closure WithRestoredIO(Closure)
Decorate a function. It executes the same way, but with the currently active screen terminal hooks te...
int dimy() const
Definition: screen.hpp:85
std::string ToString() const
Definition: screen.cpp:416
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:500
Cursor cursor_
Definition: screen.hpp:124
void Clear()
Clear all the pixel from the screen.
Definition: screen.cpp:519
int dimx() const
Definition: screen.hpp:84
std::vector< std::vector< Pixel > > pixels_
Definition: screen.hpp:123
Dimensions Fixed(int)
Definition: screen.cpp:372
void SetFallbackSize(const Dimensions &fallbackSize)
Override terminal size in case auto-detection fails.
Definition: terminal.cpp:124
Dimensions Size()
Get the terminal size.
Definition: terminal.cpp:94
std::chrono::duration< float > Duration
Definition: animation.hpp:24
std::chrono::time_point< Clock > TimePoint
Definition: animation.hpp:23
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::shared_ptr< ComponentBase > Component
std::string to_string(const std::wstring &s)
Convert a UTF8 std::string into a std::wstring.
Definition: string.cpp:1565
Element select(Element)
Set the child to be the one selected among its siblings.
Definition: frame.cpp:150
std::variant< Event, Closure, AnimationTask > Task
Definition: task.hpp:14
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
Definition: node.cpp:47
std::function< void()> Closure
Definition: task.hpp:13
Represent an event. It can be key press event, a terminal resize, or more ...
Definition: event.hpp:29
static Event Special(std::string)
An custom event whose meaning is defined by the user of the library.
Definition: event.cpp:66
Mouse * previous
Definition: mouse.hpp:47