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