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