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