FTXUI 6.1.9
C++ functional terminal UI.
Chargement...
Recherche...
Aucune correspondance
screen_interactive.cpp
Aller à la documentation de ce fichier.
1// Copyright 2020 Arthur Sonzogni. Tous droits réservés.
2// L'utilisation de ce code source est régie par la licence MIT qui peut être trouvée dans
3// le fichier LICENSE.
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 {
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(int fd) {
116 timeval tv = {0, 0}; // NOLINT
117 fd_set fds;
118 FD_ZERO(&fds); // NOLINT
119 FD_SET(fd, &fds); // NOLINT
120 select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT
121 return FD_ISSET(fd, &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/// Crée un ScreenInteractive prenant toute la taille du terminal. Ceci utilise le
294/// tampon d'écran alternatif pour éviter de perturber le contenu du terminal.
295/// @note Ceci est identique à `ScreenInteractive::FullscreenAlternateScreen()`
296// static
300
301/// Crée un ScreenInteractive prenant toute la taille du terminal. Le tampon d'écran
302/// principal est utilisé. Cela signifie que si le terminal est redimensionné, le contenu
303/// précédent pourrait perturber le contenu du terminal.
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/// Crée un ScreenInteractive prenant toute la taille du terminal. Ceci utilise le
316/// tampon d'écran alternatif pour éviter de perturber le contenu du terminal.
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/// Crée un ScreenInteractive dont la largeur correspond à la largeur de sortie du terminal et
329/// dont la hauteur correspond au composant dessiné.
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/// Crée un ScreenInteractive dont la largeur et la hauteur correspondent au composant
344/// dessiné.
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 Définit si la souris est suivie et si les événements sont signalés.
357/// appelé en dehors de la boucle principale. Par exemple `ScreenInteractive::Loop(...)`.
358/// @param enable Indique s'il faut activer le suivi des événements de la souris.
359/// @note Ceci doit être appelé en dehors de la boucle principale. Par exemple, avant d'appeler
360/// `ScreenInteractive::Loop`.
361/// @note Le suivi de la souris est activé par défaut.
362/// @note Le suivi de la souris n'est pris en charge que sur les terminaux qui le supportent.
363///
364/// ### Exemple
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 Active ou désactive la gestion automatique des entrées pipées.
376/// Lorsqu'elle est activée, FTXUI détectera les entrées pipées et redirigera stdin depuis /dev/tty
377/// pour l'entrée clavier, permettant aux applications de lire des données pipées tout en
378/// recevant des événements clavier interactifs.
379/// @param enable Indique s'il faut activer la gestion des entrées pipées. La valeur par défaut est true.
380/// @note Ceci doit être appelé avant Loop().
381/// @note Cette fonctionnalité est activée par défaut.
382/// @note Cette fonctionnalité n'est disponible que sur les systèmes POSIX (Linux/macOS).
384 handle_piped_input_ = enable;
385}
386
387/// @brief Ajoute une tâche à la boucle principale.
388/// Elle sera exécutée plus tard, après toutes les autres tâches planifiées.
390 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
391 HandleTask(component_, task);
392 });
393}
394
395/// @brief Ajoute un événement à la boucle principale.
396/// Il sera exécuté plus tard, après tous les autres événements planifiés.
398 Post(event);
399}
400
401/// @brief Ajoute une tâche pour dessiner l'écran une fois de plus, jusqu'à ce que toutes les animations
402/// soient terminées.
404 if (animation_requested_) {
405 return;
406 }
407 animation_requested_ = true;
408 auto now = animation::Clock::now();
409 const auto time_histeresis = std::chrono::milliseconds(33);
410 if (now - previous_animation_time_ >= time_histeresis) {
411 previous_animation_time_ = now;
412 }
413}
414
415/// @brief Tente d'obtenir le verrou unique permettant de capturer la souris.
416/// @return Un verrou unique si la souris n'est pas déjà capturée, sinon
417/// null.
419 if (mouse_captured) {
420 return nullptr;
421 }
422 mouse_captured = true;
423 return std::make_unique<CapturedMouseImpl>(
424 [this] { mouse_captured = false; });
425}
426
427/// @brief Exécute la boucle principale.
428/// @param component Le composant à dessiner.
429void ScreenInteractive::Loop(Component component) { // NOLINT
430 class Loop loop(this, std::move(component));
431 loop.Run();
432}
433
434/// @brief Indique si la boucle principale a été quittée.
435bool ScreenInteractive::HasQuitted() {
436 return quit_;
437}
438
439// private
440void ScreenInteractive::PreMain() {
441 // Suspend previously active screen:
442 if (g_active_screen) {
443 std::swap(suspended_screen_, g_active_screen);
444 // Reset cursor position to the top of the screen and clear the screen.
445 suspended_screen_->ResetCursorPosition();
446 std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
447 suspended_screen_->dimx_ = 0;
448 suspended_screen_->dimy_ = 0;
449
450 // Reset dimensions to force drawing the screen again next time:
451 suspended_screen_->Uninstall();
452 }
453
454 // This screen is now active:
455 g_active_screen = this;
456 g_active_screen->Install();
457
458 previous_animation_time_ = animation::Clock::now();
459}
460
461// private
462void ScreenInteractive::PostMain() {
463 // Put cursor position at the end of the drawing.
464 ResetCursorPosition();
465
466 g_active_screen = nullptr;
467
468 // Restore suspended screen.
469 if (suspended_screen_) {
470 // Clear screen, and put the cursor at the beginning of the drawing.
471 std::cout << ResetPosition(/*clear=*/true);
472 dimx_ = 0;
473 dimy_ = 0;
474 Uninstall();
475 std::swap(g_active_screen, suspended_screen_);
476 g_active_screen->Install();
477 } else {
478 Uninstall();
479
480 std::cout << '\r';
481 // On final exit, keep the current drawing and reset cursor position one
482 // line after it.
483 if (!use_alternative_screen_) {
484 std::cout << '\n';
485 std::cout << std::flush;
486 }
487 }
488}
489
490/// @brief Décore une fonction. Elle s'exécute de la même manière, mais avec les
491/// hooks du terminal de l'écran actuellement actif temporairement désinstallés pendant son exécution.
492/// @param fn La fonction à décorer.
494 return [this, fn] {
495 Uninstall();
496 fn();
497 Install();
498 };
499}
500
501/// @brief Force FTXUI à gérer ou non le Ctrl-C, même si le composant
502/// intercepte l'événement Event::CtrlC.
504 force_handle_ctrl_c_ = force;
505}
506
507/// @brief Force FTXUI à gérer ou non le Ctrl-Z, même si le composant
508/// intercepte l'événement Event::CtrlZ.
510 force_handle_ctrl_z_ = force;
511}
512
513/// @brief Retourne le contenu de la sélection actuelle
515 if (!selection_) {
516 return "";
517 }
518 return selection_->GetParts();
519}
520
521void ScreenInteractive::SelectionChange(std::function<void()> callback) {
522 selection_on_change_ = std::move(callback);
523}
524
525/// @brief Retourne l'écran actuellement actif, ou null si aucun.
526// static
528 return g_active_screen;
529}
530
531// private
532void ScreenInteractive::Install() {
533 frame_valid_ = false;
534
535 // Flush the buffer for stdout to ensure whatever the user has printed before
536 // is fully applied before we start modifying the terminal configuration. This
537 // is important, because we are using two different channels (stdout vs
538 // termios/WinAPI) to communicate with the terminal emulator below. See
539 // https://github.com/ArthurSonzogni/FTXUI/issues/846
540 Flush();
541
542 InstallPipedInputHandling();
543
544 // After uninstalling the new configuration, flush it to the terminal to
545 // ensure it is fully applied:
546 on_exit_functions.emplace([] { Flush(); });
547
548 on_exit_functions.emplace([this] { ExitLoopClosure()(); });
549
550 // Request the terminal to report the current cursor shape. We will restore it
551 // on exit.
552 std::cout << DECRQSS_DECSCUSR;
553 on_exit_functions.emplace([this] {
554 std::cout << "\033[?25h"; // Enable cursor.
555 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
556 });
557
558 // Install signal handlers to restore the terminal state on exit. The default
559 // signal handlers are restored on exit.
560 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
561 InstallSignalHandler(signal);
562 }
563
564// Save the old terminal configuration and restore it on exit.
565#if defined(_WIN32)
566 // Enable VT processing on stdout and stdin
567 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
568 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
569
570 DWORD out_mode = 0;
571 DWORD in_mode = 0;
572 GetConsoleMode(stdout_handle, &out_mode);
573 GetConsoleMode(stdin_handle, &in_mode);
574 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
575 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
576
577 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
578 const int enable_virtual_terminal_processing = 0x0004;
579 const int disable_newline_auto_return = 0x0008;
580 out_mode |= enable_virtual_terminal_processing;
581 out_mode |= disable_newline_auto_return;
582
583 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
584 const int enable_line_input = 0x0002;
585 const int enable_echo_input = 0x0004;
586 const int enable_virtual_terminal_input = 0x0200;
587 const int enable_window_input = 0x0008;
588 in_mode &= ~enable_echo_input;
589 in_mode &= ~enable_line_input;
590 in_mode |= enable_virtual_terminal_input;
591 in_mode |= enable_window_input;
592
593 SetConsoleMode(stdin_handle, in_mode);
594 SetConsoleMode(stdout_handle, out_mode);
595#else // POSIX (Linux & Mac)
596 // #if defined(__EMSCRIPTEN__)
597 //// Reading stdin isn't blocking.
598 // int flags = fcntl(0, F_GETFL, 0);
599 // fcntl(0, F_SETFL, flags | O_NONBLOCK);
600
601 //// Restore the terminal configuration on exit.
602 // on_exit_functions.emplace([flags] { fcntl(0, F_SETFL, flags); });
603 // #endif
604 for (const int signal : {SIGWINCH, SIGTSTP}) {
605 InstallSignalHandler(signal);
606 }
607
608 struct termios terminal; // NOLINT
609 tcgetattr(tty_fd_, &terminal);
610 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
611 tcsetattr(tty_fd_, TCSANOW, &terminal);
612 });
613
614 // Enabling raw terminal input mode
615 terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition
616 terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be
617 // flushed
618 terminal.c_iflag &= ~PARMRK; // Disable marking parity errors.
619 terminal.c_iflag &= ~ISTRIP; // Disable striping 8th bit off characters.
620 terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR.
621 terminal.c_iflag &= ~IGNCR; // Disable ignoring CR.
622 terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL.
623 terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output
624
625 terminal.c_lflag &= ~ECHO; // Disable echoing input characters.
626 terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters.
627 terminal.c_lflag &= ~ICANON; // Disable Canonical mode.
628 terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting:
629 // - => DSUSP
630 // - C-Z => SUSP
631 // - C-C => INTR
632 // - C-d => QUIT
633 terminal.c_lflag &= ~IEXTEN; // Disable extended input processing
634 terminal.c_cflag |= CS8; // 8 bits per byte
635
636 terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical
637 // read.
638 terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read.
639
640 tcsetattr(tty_fd_, TCSANOW, &terminal);
641
642#endif
643
644 auto enable = [&](const std::vector<DECMode>& parameters) {
645 std::cout << Set(parameters);
646 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
647 };
648
649 auto disable = [&](const std::vector<DECMode>& parameters) {
650 std::cout << Reset(parameters);
651 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
652 };
653
654 if (use_alternative_screen_) {
655 enable({
656 DECMode::kAlternateScreen,
657 });
658 }
659
660 disable({
661 // DECMode::kCursor,
662 DECMode::kLineWrap,
663 });
664
665 if (track_mouse_) {
666 enable({DECMode::kMouseVt200});
667 enable({DECMode::kMouseAnyEvent});
668 enable({DECMode::kMouseUrxvtMode});
669 enable({DECMode::kMouseSgrExtMode});
670 }
671
672 // After installing the new configuration, flush it to the terminal to
673 // ensure it is fully applied:
674 Flush();
675
676 quit_ = false;
677
678 PostAnimationTask();
679}
680
681void ScreenInteractive::InstallPipedInputHandling() {
682#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
683 tty_fd_ = STDIN_FILENO;
684 // Handle piped input redirection if explicitly enabled by the application.
685 // This allows applications to read data from stdin while still receiving
686 // keyboard input from the terminal for interactive use.
687 if (!handle_piped_input_) {
688 return;
689 }
690
691 // If stdin is a terminal, we don't need to open /dev/tty.
692 if (isatty(STDIN_FILENO)) {
693 return;
694 }
695
696 // Open /dev/tty for keyboard input.
697 tty_fd_ = open("/dev/tty", O_RDONLY);
698 if (tty_fd_ < 0) {
699 // Failed to open /dev/tty (containers, headless systems, etc.)
700 tty_fd_ = STDIN_FILENO; // Fallback to stdin.
701 return;
702 }
703
704 // Close the /dev/tty file descriptor on exit.
705 on_exit_functions.emplace([this] {
706 close(tty_fd_);
707 tty_fd_ = -1;
708 });
709#endif
710}
711
712// private
713void ScreenInteractive::Uninstall() {
714 ExitNow();
715 OnExit();
716}
717
718// private
719// NOLINTNEXTLINE
720void ScreenInteractive::RunOnceBlocking(Component component) {
721 // Set FPS to 60 at most.
722 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
723
724 auto time = std::chrono::steady_clock::now();
725 size_t executed_task = internal_->task_runner.ExecutedTasks();
726
727 // Wait for at least one task to execute.
728 while (executed_task == internal_->task_runner.ExecutedTasks() &&
729 !HasQuitted()) {
730 RunOnce(component);
731
732 const auto now = std::chrono::steady_clock::now();
733 const auto delta = now - time;
734 time = now;
735
736 if (delta < time_per_frame) {
737 const auto sleep_duration = time_per_frame - delta;
738 std::this_thread::sleep_for(sleep_duration);
739 }
740 }
741}
742
743// private
744void ScreenInteractive::RunOnce(Component component) {
745 AutoReset set_component(&component_, component);
746 ExecuteSignalHandlers();
747 FetchTerminalEvents();
748
749 // Execute the pending tasks from the queue.
750 const size_t executed_task = internal_->task_runner.ExecutedTasks();
751 internal_->task_runner.RunUntilIdle();
752 // If no executed task, we can return early without redrawing the screen.
753 if (executed_task == internal_->task_runner.ExecutedTasks()) {
754 return;
755 }
756
757 ExecuteSignalHandlers();
758 Draw(component);
759
760 if (selection_data_previous_ != selection_data_) {
761 selection_data_previous_ = selection_data_;
762 if (selection_on_change_) {
763 selection_on_change_();
765 }
766 }
767}
768
769// private
770// NOLINTNEXTLINE
771void ScreenInteractive::HandleTask(Component component, Task& task) {
772 std::visit(
773 [&](auto&& arg) {
774 using T = std::decay_t<decltype(arg)>;
775
776 // clang-format off
777 // Handle Event.
778 if constexpr (std::is_same_v<T, Event>) {
779
780 if (arg.is_cursor_position()) {
781 cursor_x_ = arg.cursor_x();
782 cursor_y_ = arg.cursor_y();
783 return;
784 }
785
786 if (arg.is_cursor_shape()) {
787 cursor_reset_shape_= arg.cursor_shape();
788 return;
789 }
790
791 if (arg.is_mouse()) {
792 arg.mouse().x -= cursor_x_;
793 arg.mouse().y -= cursor_y_;
794 }
795
796 arg.screen_ = this;
797
798 bool handled = component->OnEvent(arg);
799
800 handled = HandleSelection(handled, arg);
801
802 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
803 RecordSignal(SIGABRT);
804 }
805
806#if !defined(_WIN32)
807 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
808 RecordSignal(SIGTSTP);
809 }
810#endif
811
812 frame_valid_ = false;
813 return;
814 }
815
816 // Handle callback
817 if constexpr (std::is_same_v<T, Closure>) {
818 arg();
819 return;
820 }
821
822 // Handle Animation
823 if constexpr (std::is_same_v<T, AnimationTask>) {
824 if (!animation_requested_) {
825 return;
826 }
827
828 animation_requested_ = false;
829 const animation::TimePoint now = animation::Clock::now();
830 const animation::Duration delta = now - previous_animation_time_;
831 previous_animation_time_ = now;
832
833 animation::Params params(delta);
834 component->OnAnimation(params);
835 frame_valid_ = false;
836 return;
837 }
838 },
839 task);
840 // clang-format on
841}
842
843// private
844bool ScreenInteractive::HandleSelection(bool handled, Event event) {
845 if (handled) {
846 selection_pending_ = nullptr;
847 selection_data_.empty = true;
848 selection_ = nullptr;
849 return true;
850 }
851
852 if (!event.is_mouse()) {
853 return false;
854 }
855
856 auto& mouse = event.mouse();
857 if (mouse.button != Mouse::Left) {
858 return false;
859 }
860
861 if (mouse.motion == Mouse::Pressed) {
862 selection_pending_ = CaptureMouse();
863 selection_data_.start_x = mouse.x;
864 selection_data_.start_y = mouse.y;
865 selection_data_.end_x = mouse.x;
866 selection_data_.end_y = mouse.y;
867 return false;
868 }
869
870 if (!selection_pending_) {
871 return false;
872 }
873
874 if (mouse.motion == Mouse::Moved) {
875 if ((mouse.x != selection_data_.end_x) ||
876 (mouse.y != selection_data_.end_y)) {
877 selection_data_.end_x = mouse.x;
878 selection_data_.end_y = mouse.y;
879 selection_data_.empty = false;
880 }
881
882 return true;
883 }
884
885 if (mouse.motion == Mouse::Released) {
886 selection_pending_ = nullptr;
887 selection_data_.end_x = mouse.x;
888 selection_data_.end_y = mouse.y;
889 selection_data_.empty = false;
890 return true;
891 }
892
893 return false;
894}
895
896// private
897// NOLINTNEXTLINE
898void ScreenInteractive::Draw(Component component) {
899 if (frame_valid_) {
900 return;
901 }
902 auto document = component->Render();
903 int dimx = 0;
904 int dimy = 0;
905 auto terminal = Terminal::Size();
906 document->ComputeRequirement();
907 switch (dimension_) {
908 case Dimension::Fixed:
909 dimx = dimx_;
910 dimy = dimy_;
911 break;
912 case Dimension::TerminalOutput:
913 dimx = terminal.dimx;
914 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
915 break;
916 case Dimension::Fullscreen:
917 dimx = terminal.dimx;
918 dimy = terminal.dimy;
919 break;
920 case Dimension::FitComponent:
921 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
922 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
923 break;
924 }
925
926 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
927 ResetCursorPosition();
928 std::cout << ResetPosition(/*clear=*/resized);
929
930 // If the terminal width decrease, the terminal emulator will start wrapping
931 // lines and make the display dirty. We should clear it completely.
932 if ((dimx < dimx_) && !use_alternative_screen_) {
933 std::cout << "\033[J"; // clear terminal output
934 std::cout << "\033[H"; // move cursor to home position
935 }
936
937 // Resize the screen if needed.
938 if (resized) {
939 dimx_ = dimx;
940 dimy_ = dimy;
941 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
942 cursor_.x = dimx_ - 1;
943 cursor_.y = dimy_ - 1;
944 }
945
946 // Periodically request the terminal emulator the frame position relative to
947 // the screen. This is useful for converting mouse position reported in
948 // screen's coordinates to frame's coordinates.
949#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
950 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
951 // position, several output sequences are mixed together into garbage.
952 // This causes FTXUI user to see some "1;1;R" sequences into the Input
953 // component. See [issue]. Solution is to request cursor position less
954 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
955 // https://github.com/ArthurSonzogni/FTXUI/issues/136
956 static int i = -3;
957 ++i;
958 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
959 std::cout << DeviceStatusReport(DSRMode::kCursor);
960 }
961#else
962 static int i = -3;
963 ++i;
964 if (!use_alternative_screen_ &&
965 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
966 std::cout << DeviceStatusReport(DSRMode::kCursor);
967 }
968#endif
969 previous_frame_resized_ = resized;
970
971 selection_ = selection_data_.empty
972 ? std::make_unique<Selection>()
973 : std::make_unique<Selection>(
974 selection_data_.start_x, selection_data_.start_y, //
975 selection_data_.end_x, selection_data_.end_y);
976 Render(*this, document.get(), *selection_);
977
978 // Set cursor position for user using tools to insert CJK characters.
979 {
980 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
981 const int dy = dimy_ - 1 - cursor_.y;
982
983 set_cursor_position.clear();
984 reset_cursor_position.clear();
985
986 if (dy != 0) {
987 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
988 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
989 }
990
991 if (dx != 0) {
992 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
993 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
994 }
995
996 if (cursor_.shape == Cursor::Hidden) {
997 set_cursor_position += "\033[?25l";
998 } else {
999 set_cursor_position += "\033[?25h";
1000 set_cursor_position +=
1001 "\033[" + std::to_string(int(cursor_.shape)) + " q";
1002 }
1003 }
1004
1005 std::cout << ToString() << set_cursor_position;
1006 Flush();
1007 Clear();
1008 frame_valid_ = true;
1009 frame_count_++;
1010}
1011
1012// private
1013void ScreenInteractive::ResetCursorPosition() {
1014 std::cout << reset_cursor_position;
1015 reset_cursor_position = "";
1016}
1017
1018/// @brief Retourne une fonction pour quitter la boucle principale.
1020 return [this] { Exit(); };
1021}
1022
1023/// @brief Quitte la boucle principale.
1025 Post([this] { ExitNow(); });
1026}
1027
1028// private:
1029void ScreenInteractive::ExitNow() {
1030 quit_ = true;
1031}
1032
1033// private:
1034void ScreenInteractive::Signal(int signal) {
1035 if (signal == SIGABRT) {
1036 Exit();
1037 return;
1038 }
1039
1040// Windows do no support SIGTSTP / SIGWINCH
1041#if !defined(_WIN32)
1042 if (signal == SIGTSTP) {
1043 Post([&] {
1044 ResetCursorPosition();
1045 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
1046 Uninstall();
1047 dimx_ = 0;
1048 dimy_ = 0;
1049 Flush();
1050 std::ignore = std::raise(SIGTSTP);
1051 Install();
1052 });
1053 return;
1054 }
1055
1056 if (signal == SIGWINCH) {
1057 Post(Event::Special({0}));
1058 return;
1059 }
1060#endif
1061}
1062
1063void ScreenInteractive::FetchTerminalEvents() {
1064#if defined(_WIN32)
1065 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1066 // Check if there is input in the console.
1067 auto console = GetStdHandle(STD_INPUT_HANDLE);
1068 DWORD number_of_events = 0;
1069 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1070 return std::vector<INPUT_RECORD>();
1071 }
1072 if (number_of_events <= 0) {
1073 // No input, return.
1074 return std::vector<INPUT_RECORD>();
1075 }
1076 // Read the input events.
1077 std::vector<INPUT_RECORD> records(number_of_events);
1078 DWORD number_of_events_read = 0;
1079 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1080 &number_of_events_read)) {
1081 return std::vector<INPUT_RECORD>();
1082 }
1083 records.resize(number_of_events_read);
1084 return records;
1085 };
1086
1087 auto records = get_input_records();
1088 if (records.size() == 0) {
1089 const auto timeout =
1090 std::chrono::steady_clock::now() - internal_->last_char_time;
1091 const size_t timeout_microseconds =
1092 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1093 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1094 return;
1095 }
1096 internal_->last_char_time = std::chrono::steady_clock::now();
1097
1098 // Convert the input events to FTXUI events.
1099 // For each event, we call the terminal input parser to convert it to
1100 // Event.
1101 for (const auto& r : records) {
1102 switch (r.EventType) {
1103 case KEY_EVENT: {
1104 auto key_event = r.Event.KeyEvent;
1105 // ignore UP key events
1106 if (key_event.bKeyDown == FALSE) {
1107 continue;
1108 }
1109 std::wstring wstring;
1110 wstring += key_event.uChar.UnicodeChar;
1111 for (auto it : to_string(wstring)) {
1112 internal_->terminal_input_parser.Add(it);
1113 }
1114 } break;
1115 case WINDOW_BUFFER_SIZE_EVENT:
1116 Post(Event::Special({0}));
1117 break;
1118 case MENU_EVENT:
1119 case FOCUS_EVENT:
1120 case MOUSE_EVENT:
1121 // TODO(mauve): Implement later.
1122 break;
1123 }
1124 }
1125#elif defined(__EMSCRIPTEN__)
1126 // Read chars from the terminal.
1127 // We configured it to be non blocking.
1128 std::array<char, 128> out{};
1129 size_t l = read(STDIN_FILENO, out.data(), out.size());
1130 if (l == 0) {
1131 const auto timeout =
1132 std::chrono::steady_clock::now() - internal_->last_char_time;
1133 const size_t timeout_microseconds =
1134 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1135 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1136 return;
1137 }
1138 internal_->last_char_time = std::chrono::steady_clock::now();
1139
1140 // Convert the chars to events.
1141 for (size_t i = 0; i < l; ++i) {
1142 internal_->terminal_input_parser.Add(out[i]);
1143 }
1144#else // POSIX (Linux & Mac)
1145 if (!CheckStdinReady(tty_fd_)) {
1146 const auto timeout =
1147 std::chrono::steady_clock::now() - internal_->last_char_time;
1148 const size_t timeout_ms =
1149 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1150 internal_->terminal_input_parser.Timeout(timeout_ms);
1151 return;
1152 }
1153 internal_->last_char_time = std::chrono::steady_clock::now();
1154
1155 // Read chars from the terminal.
1156 std::array<char, 128> out{};
1157 size_t l = read(tty_fd_, out.data(), out.size());
1158
1159 // Convert the chars to events.
1160 for (size_t i = 0; i < l; ++i) {
1161 internal_->terminal_input_parser.Add(out[i]);
1162 }
1163#endif
1164}
1165
1166void ScreenInteractive::PostAnimationTask() {
1167 Post(AnimationTask());
1168
1169 // Repeat the animation task every 15ms. This correspond to a frame rate
1170 // of around 66fps.
1171 internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1172 std::chrono::milliseconds(15));
1173}
1174
1175bool ScreenInteractive::SelectionData::operator==(
1176 const ScreenInteractive::SelectionData& other) const {
1177 if (empty && other.empty) {
1178 return true;
1179 }
1180 if (empty || other.empty) {
1181 return false;
1182 }
1183 return start_x == other.start_x && start_y == other.start_y &&
1184 end_x == other.end_x && end_y == other.end_y;
1185}
1186
1187bool ScreenInteractive::SelectionData::operator!=(
1188 const ScreenInteractive::SelectionData& other) const {
1189 return !(*this == other);
1190}
1191
1192} // namespace ftxui.
static void Signal(ScreenInteractive &s, int signal)
auto PostTask(Task task) -> void
Planifie une tâche à exécuter immédiatement.
auto RunUntilIdle() -> std::optional< std::chrono::steady_clock::duration >
Exécute les tâches dans la file d'attente.
auto PostDelayedTask(Task task, std::chrono::steady_clock::duration duration) -> void
Planifie une tâche à exécuter après une certaine durée.
size_t ExecutedTasks() const
auto screen
static const Event CtrlC
Definition event.hpp:70
static ScreenInteractive TerminalOutput()
void HandlePipedInput(bool enable=true)
Active ou désactive la gestion automatique des entrées pipées. Lorsqu'elle est activée,...
void Exit()
Quitte la boucle principale.
static const Event CtrlZ
Definition event.hpp:93
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
Ajoute un événement à la boucle principale. Il sera exécuté plus tard, après tous les autres événemen...
void Post(Task task)
Ajoute une tâche à la boucle principale. Elle sera exécutée plus tard, après toutes les autres tâches...
static ScreenInteractive FitComponent()
static ScreenInteractive Fullscreen()
static const Event Custom
Definition event.hpp:96
static ScreenInteractive FullscreenPrimaryScreen()
static ScreenInteractive * Active()
Retourne l'écran actuellement actif, ou null si aucun.
CapturedMouse CaptureMouse()
Tente d'obtenir le verrou unique permettant de capturer la souris.
std::string GetSelection()
Retourne le contenu de la sélection actuelle.
static ScreenInteractive FullscreenAlternateScreen()
void TrackMouse(bool enable=true)
Définit si la souris est suivie et si les événements sont signalés. appelé en dehors de la boucle pri...
void SelectionChange(std::function< void()> callback)
void RequestAnimationFrame()
Ajoute une tâche pour dessiner l'écran une fois de plus, jusqu'à ce que toutes les animations soient ...
Closure ExitLoopClosure()
Retourne une fonction pour quitter la boucle principale.
void ForceHandleCtrlC(bool force)
Force FTXUI à gérer ou non le Ctrl-C, même si le composant intercepte l'événement Event::CtrlC.
void ForceHandleCtrlZ(bool force)
Force FTXUI à gérer ou non le Ctrl-Z, même si le composant intercepte l'événement Event::CtrlZ.
Closure WithRestoredIO(Closure)
Décore une fonction. Elle s'exécute de la même manière, mais avec les hooks du terminal de l'écran ac...
static Event Special(std::string)
Un événement personnalisé dont la signification est définie par l'utilisateur de la bibliothèque.
Definition event.cpp:74
Loop est une classe qui gère la boucle d'événements pour un composant.
Definition loop.hpp:56
ScreenInteractive est un Screen qui peut gérer les événements, exécuter une boucle principale et gére...
void RequestAnimationFrame()
RequestAnimationFrame est une fonction qui demande à ce qu'une nouvelle trame soit dessinée lors du p...
Représente un événement. Il peut s'agir d'un événement de touche, d'un redimensionnement de terminal,...
Definition event.hpp:28
int dimy() const
Definition image.hpp:36
std::string ToString() const
std::string ResetPosition(bool clear=false) const
Cursor cursor_
Definition screen.hpp:79
int dimx() const
Definition image.hpp:35
std::vector< std::vector< Pixel > > pixels_
Definition image.hpp:46
Dimensions Size()
Obtenir la taille du terminal.
Definition terminal.cpp:92
L'espace de noms FTXUI ftxui::Dimension::
L'espace de noms FTXUI ftxui::animation::
void SetFallbackSize(const Dimensions &fallbackSize)
Outrepasser la taille du terminal en cas d'échec de la détection automatique.
Definition terminal.cpp:121
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
L'espace de noms FTXUI ftxui::
Definition animation.hpp:10
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(const std::wstring &s)
Convertit un std::wstring en un std::string UTF8.
Definition string.cpp:1566
Element select(Element e)
Définit l'élément child comme étant celui qui est focalisé parmi ses frères.
Definition frame.cpp:108
std::variant< Event, Closure, AnimationTask > Task
Definition task.hpp:14
void Render(Screen &screen, const Element &element)
std::function< void()> Closure
Definition task.hpp:13
std::shared_ptr< ComponentBase > Component