FTXUI 7.0.3
C++ functional terminal UI.
Loading...
Searching...
No Matches
app.cpp
Go to the documentation of this file.
1// Copyright 2020 Arthur Sonzogni. All rights reserved.
2// Use of this source code is governed by the MIT license that can be found in
3// the LICENSE file.
4#include <algorithm> // for any_of, copy, max, min
5#include <array> // for array
6#include <atomic>
7#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
8#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
9#include <cstdint>
10#include <cstdio> // for fileno, stdin
12#include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
13#include <ftxui/screen/screen.hpp> // for Cell, 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 <map>
18#include <memory>
19#include <stack> // for stack
20#include <string>
21#include <string_view>
22#include <thread> // for thread, sleep_for
23#include <tuple> // for _Swallow_assign, ignore
24#include <type_traits>
25#include <utility> // for move, swap
26#include <variant> // for visit, variant
27#include <vector> // for vector
28
29#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
30#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
31#include "ftxui/component/component_base.hpp" // for ComponentBase
32#include "ftxui/component/event.hpp" // for Event
33#include "ftxui/component/loop.hpp" // for Loop
36#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
37#include "ftxui/dom/node.hpp" // for Node, Render
38#include "ftxui/screen/cell.hpp" // for Cell
39#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
40#include "ftxui/screen/util.hpp" // for util::clamp
41#include "ftxui/util/autoreset.hpp" // for AutoReset
42
43#if defined(_WIN32)
44#define DEFINE_CONSOLEV2_PROPERTIES
45#define WIN32_LEAN_AND_MEAN
46#ifndef NOMINMAX
47#define NOMINMAX
48#endif
49#include <io.h>
50#include <windows.h>
51#else
52#include <fcntl.h>
53#include <poll.h>
54#include <sys/poll.h>
55#include <sys/types.h>
56#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
57#include <unistd.h> // for STDIN_FILENO, STDOUT_FILENO, read
58#endif
59
60#if defined(__EMSCRIPTEN__)
61#include <emscripten.h>
62#endif
63
64namespace ftxui {
65
66enum class AppDimension {
68 Fixed,
71};
72
73namespace animation {
75 auto* screen = App::Active();
76 if (screen) {
77 screen->RequestAnimationFrame();
78 }
79}
80} // namespace animation
81
82#if defined(__EMSCRIPTEN__)
83extern "C" {
84EMSCRIPTEN_KEEPALIVE
85void ftxui_on_resize(int columns, int rows) {
87 columns,
88 rows,
89 });
90 std::raise(SIGWINCH);
91}
92}
93#endif
94
95struct App::Internal {
96 App* public_;
97
98 App* suspended_screen_ = nullptr;
99 const AppDimension dimension_;
100 const bool use_alternative_screen_;
101
102 bool track_mouse_ = true;
103
104 std::string set_cursor_position_;
105 std::string reset_cursor_position_;
106
107 std::atomic<bool> quit_{false};
108 bool installed_ = false;
109 bool animation_requested_ = false;
110 animation::TimePoint previous_animation_time_;
111
112 int cursor_x_ = 1;
113 int cursor_y_ = 1;
114
115 std::uint64_t frame_count_ = 0;
116 bool mouse_captured = false;
117 bool previous_frame_resized_ = false;
118
119 bool frame_valid_ = false;
120
121 bool force_handle_ctrl_c_ = true;
122 bool force_handle_ctrl_z_ = true;
123
124 int cursor_reset_shape_ = 1;
125
126 // État de la gestion de l'entrée redirigée (POSIX uniquement)
127 bool handle_piped_input_ = true;
128 bool is_stdin_a_tty_ = false;
129 bool is_stdout_a_tty_ = false;
130 // Descripteur de fichier pour /dev/tty, utilisé pour la gestion de l'entrée
131 // redirigée.
132 int tty_fd_ = -1;
133
134 std::string terminal_name_ = "unknown";
135 int terminal_version_ = 0;
136
137 std::string terminal_emulator_name_ = "unknown";
138 std::string terminal_emulator_version_ = "unknown";
139
140 std::vector<int> terminal_capabilities_;
141
142 // API de sélection :
143 CapturedMouse selection_pending_;
144 struct SelectionData {
145 int start_x = -1;
146 int start_y = -1;
147 int end_x = -2;
148 int end_y = -2;
149 bool empty = true;
150 bool operator==(const SelectionData& other) const {
151 if (empty && other.empty) {
152 return true;
153 }
154 if (empty || other.empty) {
155 return false;
156 }
157 return start_x == other.start_x && start_y == other.start_y &&
158 end_x == other.end_x && end_y == other.end_y;
159 }
160 bool operator!=(const SelectionData& other) const {
161 return !(*this == other);
162 }
163 };
164 SelectionData selection_data_;
165 SelectionData selection_data_previous_;
166 std::unique_ptr<Selection> selection_;
167 std::function<void()> selection_on_change_;
168
169 Component component_;
170
171 // Préexistant dans Internal :
172 TerminalInputParser terminal_input_parser;
173 task::TaskRunner task_runner;
174 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
175 std::chrono::steady_clock::now();
176 std::string output_buffer;
177
178 class ThrottledRequest {
179 public:
180 ThrottledRequest(App::Internal* internal, std::function<void()> send)
181 : internal_(internal), send_(std::move(send)) {}
182
183 void Request(bool force = false) {
184 if (!internal_->is_stdin_a_tty_) {
185 return;
186 }
187
188 if (force) {
189 Send();
190 return;
191 }
192
193 // N'autorise qu'une seule requête en attente à la fois. Ceci évite de
194 // saturer le terminal de requêtes.
195 if (HasPending()) {
196 return;
197 }
198
199 const auto now = std::chrono::steady_clock::now();
200 if (now - last_request_time_ < std::chrono::milliseconds(500)) {
201 // Trop tôt depuis la dernière requête. On l'ignore : la requête doit
202 // être envoyée de manière synchrone depuis Draw(), juste après que le
203 // curseur a été déplacé vers l'origine de la frame, afin que la
204 // réponse du terminal reflète cette position. Draw() appelle à
205 // nouveau Request() à la prochaine frame, donc la requête n'est pas
206 // perdue, seulement retardée.
207 return;
208 }
209
210 Send();
211 }
212
213 void OnReply() { pending_request_ = false; }
214
215 bool HasPending() const {
216 if (!pending_request_) {
217 return false;
218 }
219 const auto now = std::chrono::steady_clock::now();
220 return now - last_sent_time_ < std::chrono::seconds(5);
221 }
222
223 private:
224 void Send() {
225 last_sent_time_ = std::chrono::steady_clock::now();
226 last_request_time_ = last_sent_time_;
227 pending_request_ = true;
228 send_();
229 }
230
231 App::Internal* internal_;
232 std::function<void()> send_;
233 bool pending_request_ = false;
234 std::chrono::steady_clock::time_point last_request_time_ =
235 std::chrono::steady_clock::now() - std::chrono::hours(1);
236 std::chrono::steady_clock::time_point last_sent_time_ =
237 std::chrono::steady_clock::now() - std::chrono::hours(1);
238 };
239
240 ThrottledRequest cursor_position_request;
241
242 MultiReceiverBuffer<Event> event_buffer;
243 std::unique_ptr<MultiReceiverBuffer<Event>::Receiver> main_loop_receiver;
244
245 Internal(App* app, AppDimension dimension, bool use_alternative_screen);
246
247 void ExitNow();
248 void Install();
249 void Uninstall();
250 void PreMain();
251 void PostMain();
252 bool HasQuitted();
253 void RunOnce(const Component& component);
254 void RunOnceBlocking(Component component);
255 void HandleTask(Component component, Task& task);
256 bool HandleSelection(bool handled, Event event);
257 void Draw(Component component);
258 std::string ResetCursorPosition();
259 void RequestCursorPosition(bool force = false);
260 void TerminalSend(std::string_view);
261 void TerminalFlush();
262 void InstallPipedInputHandling();
263 void InstallTerminalInfo();
264 void Signal(int signal);
265 size_t FetchTerminalEvents();
266 void PostAnimationTask();
267};
268
269namespace {
270
271App* g_active_screen = nullptr; // NOLINT
272
273std::stack<Closure> on_exit_functions; // NOLINT
274
275void OnExit() {
276 while (!on_exit_functions.empty()) {
277 on_exit_functions.top()();
278 on_exit_functions.pop();
279 }
280}
281
282// CSI : Control Sequence Introducer
283const std::string CSI = "\x1b["; // NOLINT
284 //
285// DCS: Device Control String
286const std::string DCS = "\x1bP"; // NOLINT
287
288// ST : String Terminator
289const std::string ST = "\x1b\\"; // NOLINT
290
291// DECRQSS : Request Status String
292// DECSCUSR : Set Cursor Style
293const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
294
295// DEC : Digital Equipment Corporation
296enum class DECMode : std::uint16_t {
297 kLineWrap = 7,
298 kCursor = 25,
299
300 kMouseX10 = 9,
301 kMouseVt200 = 1000,
302 kMouseVt200Highlight = 1001,
303
304 kMouseBtnEventMouse = 1002,
305 kMouseAnyEvent = 1003,
306
307 kMouseUtf8 = 1005,
308 kMouseSgrExtMode = 1006,
309 kMouseUrxvtMode = 1015,
310 kMouseSgrPixelsMode = 1016,
311 kAlternateScreen = 1049,
312};
313
314// Device Status Report (DSR) {
315enum class DSRMode : std::uint8_t {
316 kCursor = 6,
317};
318
319std::string Serialize(const std::vector<DECMode>& parameters) {
320 bool first = true;
321 std::string out;
322 for (const DECMode parameter : parameters) {
323 if (!first) {
324 out += ";";
325 }
326 out += std::to_string(int(parameter));
327 first = false;
328 }
329 return out;
330}
331
332// DEC Private Mode Set (DECSET)
333std::string Set(const std::vector<DECMode>& parameters) {
334 return CSI + "?" + Serialize(parameters) + "h";
335}
336
337// DEC Private Mode Reset (DECRST)
338std::string Reset(const std::vector<DECMode>& parameters) {
339 return CSI + "?" + Serialize(parameters) + "l";
340}
341
342// Device Status Report (DSR)
343std::string DeviceStatusReport(DSRMode ps) {
344 return CSI + std::to_string(int(ps)) + "n";
345}
346
347class CapturedMouseImpl : public CapturedMouseInterface {
348 public:
349 explicit CapturedMouseImpl(std::function<void(void)> callback)
350 : callback_(std::move(callback)) {}
351 ~CapturedMouseImpl() override { callback_(); }
352 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
353 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
354 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
355 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
356
357 private:
358 std::function<void(void)> callback_;
359};
360
361#if !defined(_WIN32)
362std::atomic<int> g_signal_exit_count = 0; // NOLINT
363std::atomic<int> g_signal_stop_count = 0; // NOLINT
364std::atomic<int> g_signal_resize_count = 0; // NOLINT
365#else
366std::atomic<int> g_signal_exit_count = 0; // NOLINT
367#endif
368
369// Indique si le terminal est actuellement configuré en mode brut (raw).
370// Utilisé pour éviter une double restauration lors des sorties d'urgence et
371// normales.
372std::atomic<bool> g_terminal_is_raw{false};
373
374// Stocke le dernier signal différé reçu (par ex. SIGINT, SIGTERM) pour le
375// relever pendant la désinstallation/sortie.
376std::atomic<int> g_last_signal{0}; // NOLINT
377
378#if defined(_WIN32)
379using SignalHandler = void (*)(int);
380// Stocke les gestionnaires de signaux d'origine avant que FTXUI n'installe
381// les siens.
382std::map<int, SignalHandler> g_old_signal_handlers;
383
384// Stocke les modes de console d'origine pour les restaurer à la sortie.
385DWORD g_original_stdout_mode = 0;
386DWORD g_original_stdin_mode = 0;
387bool g_has_original_console_mode = false;
388#else
389// Stocke les structures sigaction d'origine avant que FTXUI n'installe les
390// siennes.
391std::map<int, struct sigaction> g_old_sigactions;
392
393// Stocke les réglages termios du terminal d'origine pour les restaurer à la
394// sortie.
395struct termios g_original_termios;
396bool g_has_original_termios = false;
397int g_tty_fd = -1;
398#endif
399
400// Restaure le gestionnaire de signal d'origine pour le signal donné et le
401// relève.
402// Fonction sûre pour un signal asynchrone (async-signal-safe).
403void RestoreSignalHandlerAndRaise(int signal) {
404#if defined(_WIN32)
405 auto it = g_old_signal_handlers.find(signal);
406 auto old_handler = (it != g_old_signal_handlers.end()) ? it->second : SIG_DFL;
407 std::signal(signal, old_handler);
408#else
409 auto it = g_old_sigactions.find(signal);
410 if (it != g_old_sigactions.end()) {
411 sigaction(signal, &it->second, nullptr);
412 } else {
413 struct sigaction sa;
414 sa.sa_handler = SIG_DFL;
415 sigemptyset(&sa.sa_mask);
416 sa.sa_flags = 0;
417 sigaction(signal, &sa, nullptr);
418 }
419#endif
420 std::raise(signal);
421}
422
423// Restauration d'urgence de l'état du terminal.
424// Fonction sûre pour un signal asynchrone (async-signal-safe).
425void RestoreTerminalEmergency() {
426 if (!g_terminal_is_raw.exchange(false)) {
427 return;
428 }
429#if defined(_WIN32)
430 if (g_has_original_console_mode) {
431 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
432 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
433 SetConsoleMode(stdout_handle, g_original_stdout_mode);
434 SetConsoleMode(stdin_handle, g_original_stdin_mode);
435 }
436#else
437 if (g_has_original_termios && g_tty_fd >= 0) {
438 const char restore_seq[] =
439 "\x1b[?25h" // Affiche le curseur.
440 "\x1b[?1049l" // Bascule vers le tampon d'écran normal.
441 "\x1b[?1000l" // Désactive le suivi normal de la souris.
442 "\x1b[?1002l" // Désactive le suivi des événements de bouton souris.
443 "\x1b[?1003l" // Désactive le suivi de tous les mouvements souris.
444 "\x1b[?1006l" // Désactive le suivi souris SGR.
445 "\x1b[?1015l" // Désactive le suivi souris Urxvt.
446 "\x1b[?7h"; // Active le retour à la ligne automatique.
447 std::ignore = write(STDOUT_FILENO, restore_seq, sizeof(restore_seq) - 1);
448 tcsetattr(g_tty_fd, TCSANOW, &g_original_termios);
449 }
450#endif
451}
452
453// Fonction sûre pour un signal asynchrone (async signal safe)
454void RecordSignal(int signal) {
455 switch (signal) {
456 // Terminaison anormale (par ex. abort() ou échec d'assertion).
457 case SIGABRT:
458 // Opération arithmétique erronée (par ex. division par zéro).
459 case SIGFPE:
460 // Instruction illégale.
461 case SIGILL:
462 // Référence mémoire invalide (erreur de segmentation).
463 case SIGSEGV:
464#if !defined(_WIN32)
465 // Erreur de bus (par ex. mauvais alignement d'accès mémoire).
466 case SIGBUS:
467 // Appel système invalide.
468 case SIGSYS:
469#endif
470 {
471 RestoreTerminalEmergency();
472 RestoreSignalHandlerAndRaise(signal);
473 break;
474 }
475
476 // Interruption terminal (par ex. Ctrl-C).
477 case SIGINT:
478 // Demande de terminaison.
479 case SIGTERM:
480#if !defined(_WIN32)
481 // Fin du terminal (par ex. Ctrl-\, produit un core dump).
482 case SIGQUIT:
483 // Raccroché détecté sur le terminal de contrôle ou mort du processus de
484 // contrôle.
485 case SIGHUP:
486#endif
487 g_last_signal.store(signal);
488 g_signal_exit_count++;
489 break;
490
491#if !defined(_WIN32)
492 // Signal d'arrêt du terminal (par ex. Ctrl-Z).
493 case SIGTSTP: // NOLINT
494 g_signal_stop_count++;
495 break;
496
497 // Changement de taille de la fenêtre du terminal.
498 case SIGWINCH: // NOLINT
499 g_signal_resize_count++;
500 break;
501#endif
502
503 default:
504 break;
505 }
506}
507
508void ExecuteSignalHandlers() {
509 if (g_last_signal.load() != 0) {
510 App::Private::Signal(*g_active_screen, SIGABRT);
511 }
512
513 int signal_exit_count = g_signal_exit_count.exchange(0);
514 while (signal_exit_count--) {
515 App::Private::Signal(*g_active_screen, SIGABRT);
516 }
517
518#if !defined(_WIN32)
519 int signal_stop_count = g_signal_stop_count.exchange(0);
520 while (signal_stop_count--) {
521 App::Private::Signal(*g_active_screen, SIGTSTP);
522 }
523
524 int signal_resize_count = g_signal_resize_count.exchange(0);
525 while (signal_resize_count--) {
526 App::Private::Signal(*g_active_screen, SIGWINCH);
527 }
528#endif
529}
530
531void InstallSignalHandler(int sig) {
532#if defined(_WIN32)
533 auto old_signal_handler = std::signal(sig, RecordSignal);
534 g_old_signal_handlers[sig] = old_signal_handler;
535 on_exit_functions.emplace(
536 [=] { std::ignore = std::signal(sig, old_signal_handler); });
537#else
538 struct sigaction sa;
539 sa.sa_handler = RecordSignal;
540 sigemptyset(&sa.sa_mask);
541 sa.sa_flags = SA_RESTART;
542 struct sigaction old_sa;
543 sigaction(sig, &sa, &old_sa);
544 g_old_sigactions[sig] = old_sa;
545 on_exit_functions.emplace([=] { sigaction(sig, &old_sa, nullptr); });
546#endif
547}
548
549} // namespace
550
551App::Internal::Internal(App* app,
552 AppDimension dimension,
553 bool use_alternative_screen)
554 : public_(app),
555 dimension_(dimension),
556 use_alternative_screen_(use_alternative_screen),
557 terminal_input_parser([&](Event event) {
558 event_buffer.Push(std::move(event));
559 }),
560 cursor_position_request(this, [this] {
561 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
562 }) {
563 main_loop_receiver = event_buffer.CreateReceiver();
564}
565
566void App::Internal::ExitNow() {
567 quit_ = true;
568}
569
570void App::Internal::Install() {
571 frame_valid_ = false;
572
573 // Vide le tampon de stdout pour s'assurer que tout ce que l'utilisateur a
574 // affiché avant est pleinement appliqué avant que l'on commence à modifier
575 // la configuration du terminal. C'est important, car on utilise deux
576 // canaux différents (stdout vs termios/WinAPI) pour communiquer avec
577 // l'émulateur de terminal ci-dessous. Voir
578 // https://github.com/ArthurSonzogni/FTXUI/issues/846
579 TerminalFlush();
580
581 InstallPipedInputHandling();
582
583 // Après avoir désinstallé la nouvelle configuration, la vider vers le
584 // terminal pour s'assurer qu'elle est pleinement appliquée :
585 on_exit_functions.emplace([this] { TerminalFlush(); });
586
587 // Installe des gestionnaires de signaux pour restaurer l'état du terminal
588 // à la sortie. Les gestionnaires de signaux par défaut sont restaurés à la
589 // sortie.
590 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
591 InstallSignalHandler(signal);
592 }
593
594// Sauvegarde l'ancienne configuration du terminal et la restaure à la sortie.
595#if defined(_WIN32)
596 // Active le traitement VT sur stdout et stdin
597 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
598 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
599
600 DWORD out_mode = 0;
601 DWORD in_mode = 0;
602 GetConsoleMode(stdout_handle, &out_mode);
603 GetConsoleMode(stdin_handle, &in_mode);
604 g_original_stdout_mode = out_mode;
605 g_original_stdin_mode = in_mode;
606 g_has_original_console_mode = true;
607 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
608 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
609
610 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
611 const int enable_virtual_terminal_processing = 0x0004;
612 const int disable_newline_auto_return = 0x0008;
613 out_mode |= enable_virtual_terminal_processing;
614 out_mode |= disable_newline_auto_return;
615
616 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
617 const int enable_line_input = 0x0002;
618 const int enable_echo_input = 0x0004;
619 const int enable_virtual_terminal_input = 0x0200;
620 const int enable_window_input = 0x0008;
621 in_mode &= ~enable_echo_input;
622 in_mode &= ~enable_line_input;
623 in_mode |= enable_virtual_terminal_input;
624 in_mode |= enable_window_input;
625
626 SetConsoleMode(stdin_handle, in_mode);
627 SetConsoleMode(stdout_handle, out_mode);
628#else // POSIX (Linux & Mac)
629 for (const int signal :
630 {SIGWINCH, SIGTSTP, SIGBUS, SIGSYS, SIGQUIT, SIGHUP}) {
631 InstallSignalHandler(signal);
632 }
633
634 struct termios terminal; // NOLINT
635 tcgetattr(tty_fd_, &terminal);
636 g_original_termios = terminal;
637 g_tty_fd = tty_fd_;
638 g_has_original_termios = true;
639 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
640 tcsetattr(tty_fd_, TCSANOW, &terminal);
641 });
642
643 // Activation du mode d'entrée brut (raw) du terminal
644 terminal.c_iflag &= ~IGNBRK; // Désactive l'ignorance de la condition break
645 terminal.c_iflag &= ~BRKINT; // Désactive le fait qu'un break vide l'entrée
646 // et la sortie
647 terminal.c_iflag &= ~PARMRK; // Désactive le marquage des erreurs de parité.
648 terminal.c_iflag &= ~ISTRIP; // Désactive la suppression du 8e bit des
649 // caractères.
650 terminal.c_iflag &= ~INLCR; // Désactive la conversion NL en CR.
651 terminal.c_iflag &= ~IGNCR; // Désactive l'ignorance de CR.
652 terminal.c_iflag &= ~ICRNL; // Désactive la conversion CR en NL.
653 terminal.c_iflag &= ~IXON; // Désactive le contrôle de flux XON/XOFF en
654 // sortie
655
656 terminal.c_lflag &= ~ECHO; // Désactive l'écho des caractères saisis.
657 terminal.c_lflag &= ~ECHONL; // Désactive l'écho des retours à la ligne.
658 terminal.c_lflag &= ~ICANON; // Désactive le mode canonique.
659 terminal.c_lflag &= ~ISIG; // Désactive l'envoi de signal lors de la
660 // frappe de :
661 // - => DSUSP
662 // - C-Z => SUSP
663 // - C-C => INTR
664 // - C-d => QUIT
665 terminal.c_lflag &= ~IEXTEN; // Désactive le traitement étendu des entrées
666 terminal.c_cflag |= CS8; // 8 bits par octet
667
668 terminal.c_cc[VMIN] = 0; // Nombre minimum de caractères pour une lecture
669 // non canonique.
670 terminal.c_cc[VTIME] = 0; // Délai en dixièmes de seconde pour une lecture
671 // non canonique.
672
673 tcsetattr(tty_fd_, TCSANOW, &terminal);
674
675#endif
676
677 auto enable = [&](const std::vector<DECMode>& parameters) {
678 TerminalSend(Set(parameters));
679 on_exit_functions.emplace(
680 [this, parameters] { TerminalSend(Reset(parameters)); });
681 };
682
683 auto disable = [&](const std::vector<DECMode>& parameters) {
684 TerminalSend(Reset(parameters));
685 on_exit_functions.emplace(
686 [this, parameters] { TerminalSend(Set(parameters)); });
687 };
688
689 if (use_alternative_screen_) {
690 enable({
691 DECMode::kAlternateScreen,
692 });
693 }
694
695 disable({
696 DECMode::kLineWrap,
697 });
698
699 if (track_mouse_) {
700 enable({DECMode::kMouseVt200});
701 enable({DECMode::kMouseAnyEvent});
702 enable({DECMode::kMouseUrxvtMode});
703 enable({DECMode::kMouseSgrExtMode});
704 }
705
706 // Après avoir installé la nouvelle configuration, la vider vers le
707 // terminal pour s'assurer qu'elle est pleinement appliquée :
708 TerminalFlush();
709
710 InstallTerminalInfo();
711
712 quit_ = false;
713
714 PostAnimationTask();
715
716 installed_ = true;
717 g_terminal_is_raw = true;
718}
719
720void App::Internal::Uninstall() {
721 g_terminal_is_raw = false;
722 installed_ = false;
723
724 // Pendant l'arrêt, attend toutes les réponses.
725 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
726 auto closing_receiver =
727 event_buffer.CreateReceiverAt(main_loop_receiver->index());
728 auto start = std::chrono::steady_clock::now();
729 while (cursor_position_request.HasPending()) {
730 FetchTerminalEvents();
731
732 while (closing_receiver->Has()) {
733 const auto event = closing_receiver->Pop();
734 if (event.is_cursor_position()) {
735 cursor_x_ = event.cursor_x();
736 cursor_y_ = event.cursor_y();
737 cursor_position_request.OnReply();
738 }
739 }
740
741 task_runner.RunUntilIdle();
742
743 if (std::chrono::steady_clock::now() - start >
744 std::chrono::milliseconds(400)) {
745 break;
746 }
747 std::this_thread::sleep_for(std::chrono::milliseconds(10));
748 }
749 }
750
751 OnExit();
752}
753
754void App::Internal::PreMain() {
755 // Suspend l'écran précédemment actif :
756 if (g_active_screen) {
757 std::swap(suspended_screen_, g_active_screen);
758 // Replace la position du curseur en haut de l'écran et efface l'écran.
759 suspended_screen_->internal_->TerminalSend(
760 suspended_screen_->internal_->ResetCursorPosition());
761 suspended_screen_->ResetPosition(
762 suspended_screen_->internal_->output_buffer,
763 /*clear=*/true);
764 suspended_screen_->dimx_ = 0;
765 suspended_screen_->dimy_ = 0;
766
767 // Réinitialise les dimensions pour forcer le redessin de l'écran la
768 // prochaine fois :
769 suspended_screen_->internal_->Uninstall();
770 }
771
772 // Cet écran est maintenant actif :
773 g_active_screen = public_;
774 g_active_screen->internal_->Install();
775
776 previous_animation_time_ = animation::Clock::now();
777}
778
779void App::Internal::PostMain() {
780 // Place la position du curseur à la fin du dessin.
781 TerminalSend(ResetCursorPosition());
782
783 g_active_screen = nullptr;
784
785 // Restaure l'écran suspendu.
786 if (suspended_screen_) {
787 // Efface l'écran, et place le curseur au début du dessin.
788 public_->ResetPosition(output_buffer, /*clear=*/true);
789 public_->dimx_ = 0;
790 public_->dimy_ = 0;
791 Uninstall();
792 std::swap(g_active_screen, suspended_screen_);
793 g_active_screen->internal_->Install();
794 } else {
795 Uninstall();
796
797 std::cout << "\r";
798 // À la sortie finale, conserve le dessin actuel et replace le curseur
799 // une ligne après celui-ci.
800 if (!use_alternative_screen_) {
801 std::cout << "\n";
802 }
803 std::cout << std::flush;
804 }
805
806 int sig = g_last_signal.exchange(0);
807 if (sig != 0) {
808 RestoreSignalHandlerAndRaise(sig);
809 }
810}
811
812bool App::Internal::HasQuitted() {
813 return quit_;
814}
815
816void App::Internal::RunOnce(const Component& component) {
817 const AutoReset set_component(&component_, component);
818 ExecuteSignalHandlers();
819 FetchTerminalEvents();
820
821 while (!quit_ && main_loop_receiver->Has()) {
822 public_->Post(main_loop_receiver->Pop());
823 }
824
825 // Exécute les tâches en attente dans la file.
826 const size_t executed_task = task_runner.ExecutedTasks();
827 task_runner.RunUntilIdle();
828 // Si aucune tâche n'a été exécutée, on peut retourner tôt sans redessiner
829 // l'écran.
830 if (executed_task == task_runner.ExecutedTasks()) {
831 return;
832 }
833
834 ExecuteSignalHandlers();
835 Draw(component);
836
837 if (selection_data_previous_ != selection_data_) {
838 selection_data_previous_ = selection_data_;
839 if (selection_on_change_) {
840 selection_on_change_();
841 public_->Post(Event::Custom);
842 }
843 }
844}
845
846void App::Internal::RunOnceBlocking(Component component) {
847 // Limite le FPS à 60 au maximum.
848 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
849
850 auto time = std::chrono::steady_clock::now();
851 const size_t executed_task = task_runner.ExecutedTasks();
852
853 // Attend qu'au moins une tâche s'exécute.
854 while (executed_task == task_runner.ExecutedTasks() && !HasQuitted()) {
855 RunOnce(component);
856
857 const auto now = std::chrono::steady_clock::now();
858 const auto delta = now - time;
859 time = now;
860
861 if (delta < time_per_frame) {
862 const auto sleep_duration = time_per_frame - delta;
863 std::this_thread::sleep_for(sleep_duration);
864 }
865 }
866}
867
868void App::Internal::HandleTask(Component component, Task& task) {
869 std::visit(
870 [&](auto&& arg) {
871 using T = std::decay_t<decltype(arg)>;
872 // clang-format off
873
874 // Gère les événements.
875 if constexpr (std::is_same_v<T, Event>) {
876
877 if (arg.is_cursor_position()) {
878 cursor_x_ = arg.cursor_x();
879 cursor_y_ = arg.cursor_y();
880 cursor_position_request.OnReply();
881 return;
882 }
883
884 if (arg.is_cursor_shape()) {
885 cursor_reset_shape_ = arg.cursor_shape();
886 return;
887 }
888
889 if (arg.IsTerminalCapabilities()) {
890 terminal_capabilities_ = arg.TerminalCapabilities();
891 return;
892 }
893
894 if (arg.IsTerminalNameVersion()) {
895 terminal_name_ = arg.TerminalName();
896 terminal_version_ = arg.TerminalVersion();
897 return;
898 }
899
900 if (arg.IsTerminalEmulator()) {
901 terminal_emulator_name_ = arg.TerminalEmulatorName();
902 terminal_emulator_version_ = arg.TerminalEmulatorVersion();
903 return;
904 }
905
906 if (arg.is_mouse()) {
907 arg.mouse().x -= cursor_x_;
908 arg.mouse().y -= cursor_y_;
909 }
910
911 arg.screen_ = public_;
912
913 bool handled = component->OnEvent(arg);
914 handled = HandleSelection(handled, arg);
915
916 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
917 RecordSignal(SIGINT);
918 }
919
920#if !defined(_WIN32)
921 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
922 RecordSignal(SIGTSTP);
923 }
924#endif
925
926 frame_valid_ = false;
927 return;
928 }
929
930 // Gère le callback
931 if constexpr (std::is_same_v<T, Closure>) {
932 arg();
933 return;
934 }
935
936 // Gère l'animation
937 if constexpr (std::is_same_v<T, AnimationTask>) {
938 if (!animation_requested_) {
939 return;
940 }
941
942 animation_requested_ = false;
943 const animation::TimePoint now = animation::Clock::now();
944 const animation::Duration delta = now - previous_animation_time_;
945 previous_animation_time_ = now;
946
947 animation::Params params(delta);
948 component->OnAnimation(params);
949 frame_valid_ = false;
950 return;
951 }
952 },
953 task);
954 // clang-format on
955}
956
957bool App::Internal::HandleSelection(bool handled, Event event) {
958 if (handled) {
959 selection_pending_ = nullptr;
960 selection_data_.empty = true;
961 selection_ = nullptr;
962 return true;
963 }
964
965 if (!event.is_mouse()) {
966 return false;
967 }
968
969 auto& mouse = event.mouse();
970 if (mouse.button != Mouse::Left) {
971 return false;
972 }
973
974 if (mouse.motion == Mouse::Pressed) {
975 selection_pending_ = public_->CaptureMouse();
976 selection_data_.start_x = mouse.x;
977 selection_data_.start_y = mouse.y;
978 selection_data_.end_x = mouse.x;
979 selection_data_.end_y = mouse.y;
980 return false;
981 }
982
983 if (!selection_pending_) {
984 return false;
985 }
986
987 if (mouse.motion == Mouse::Moved) {
988 if ((mouse.x != selection_data_.end_x) ||
989 (mouse.y != selection_data_.end_y)) {
990 selection_data_.end_x = mouse.x;
991 selection_data_.end_y = mouse.y;
992 selection_data_.empty = false;
993 }
994
995 return true;
996 }
997
998 if (mouse.motion == Mouse::Released) {
999 selection_pending_ = nullptr;
1000 selection_data_.end_x = mouse.x;
1001 selection_data_.end_y = mouse.y;
1002 selection_data_.empty = false;
1003 return true;
1004 }
1005
1006 return false;
1007}
1008
1009void App::Internal::Draw(Component component) {
1010 if (frame_valid_) {
1011 return;
1012 }
1013 auto document = component->Render();
1014 int dimx = 0;
1015 int dimy = 0;
1016 auto terminal = Terminal::Size();
1017 document->ComputeRequirement();
1018 switch (dimension_) {
1020 dimx = public_->dimx_;
1021 dimy = public_->dimy_;
1022 break;
1024 dimx = terminal.dimx;
1025 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1026 break;
1028 dimx = terminal.dimx;
1029 dimy = terminal.dimy;
1030 break;
1032 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
1033 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1034 break;
1035 }
1036
1037 // Cache le curseur pour éviter un scintillement pendant la réinitialisation.
1038 TerminalSend("\033[?25l");
1039
1040 const bool resized =
1041 frame_count_ == 0 || (dimx != public_->dimx_) || (dimy != public_->dimy_);
1042 TerminalSend(ResetCursorPosition());
1043
1044 if (frame_count_ != 0) {
1045 // Replace la position du curseur dans le coin inférieur gauche pour
1046 // commencer à dessiner la nouvelle frame.
1047 public_->ResetPosition(output_buffer, resized);
1048
1049 // Si la largeur du terminal diminue, l'émulateur de terminal commencera à
1050 // faire un retour à la ligne automatique et rendra l'affichage sale. Il
1051 // faut alors le nettoyer entièrement.
1052 if ((dimx < public_->dimx_) && !use_alternative_screen_) {
1053 TerminalSend("\033[J"); // efface la sortie du terminal
1054 TerminalSend("\033[H"); // déplace le curseur à la position d'origine
1055 }
1056 }
1057
1058 // Redimensionne l'écran si nécessaire.
1059 if (resized) {
1060 public_->dimx_ = dimx;
1061 public_->dimy_ = dimy;
1062 public_->cells_ = std::vector<Cell>(static_cast<size_t>(dimx) *
1063 static_cast<size_t>(dimy));
1064 Cursor cursor = public_->cursor_;
1065 cursor.x = dimx - 1;
1066 cursor.y = dimy - 1;
1067 public_->SetCursor(cursor);
1068 }
1069
1070 // Demande périodiquement à l'émulateur de terminal la position de la frame
1071 // relative à l'écran. Ceci est utile pour convertir la position de la
1072 // souris rapportée dans les coordonnées de l'écran vers les coordonnées de
1073 // la frame.
1074 if (!use_alternative_screen_ && is_stdout_a_tty_) {
1075 RequestCursorPosition(previous_frame_resized_);
1076 }
1077 previous_frame_resized_ = resized;
1078
1079 selection_ = selection_data_.empty
1080 ? std::make_unique<Selection>()
1081 : std::make_unique<Selection>(
1082 selection_data_.start_x, selection_data_.start_y, //
1083 selection_data_.end_x, selection_data_.end_y);
1084 Render(*public_, document.get(), *selection_);
1085
1086 // Définit la position du curseur pour les utilisateurs utilisant des outils
1087 // pour insérer des caractères CJK.
1088 {
1089 const int dx = public_->dimx_ - 1 - public_->cursor_.x +
1090 int(public_->dimx_ != terminal.dimx);
1091 const int dy = public_->dimy_ - 1 - public_->cursor_.y;
1092
1093 set_cursor_position_.clear();
1094 reset_cursor_position_.clear();
1095
1096 if (dy != 0) {
1097 set_cursor_position_ += "\x1B[" + std::to_string(dy) + "A";
1098 reset_cursor_position_ += "\x1B[" + std::to_string(dy) + "B";
1099 }
1100
1101 if (dx != 0) {
1102 set_cursor_position_ += "\x1B[" + std::to_string(dx) + "D";
1103 reset_cursor_position_ += "\x1B[" + std::to_string(dx) + "C";
1104 }
1105
1106 if (public_->cursor_.shape != Screen::Cursor::Hidden) {
1107 set_cursor_position_ += "\033[?25h";
1108 set_cursor_position_ +=
1109 "\033[" + std::to_string(int(public_->cursor_.shape)) + " q";
1110 }
1111 }
1112
1113 public_->ToString(output_buffer);
1114 TerminalSend(set_cursor_position_);
1115 TerminalFlush();
1116
1117 public_->Clear();
1118 frame_valid_ = true;
1119 frame_count_++;
1120}
1121
1122std::string App::Internal::ResetCursorPosition() {
1123 std::string result = std::move(reset_cursor_position_);
1124 reset_cursor_position_ = "";
1125 return result;
1126}
1127
1128void App::Internal::RequestCursorPosition(bool force) {
1129 cursor_position_request.Request(force);
1130}
1131
1132void App::Internal::TerminalSend(std::string_view s) {
1133 output_buffer += s;
1134}
1135
1136void App::Internal::TerminalFlush() {
1137 // Emscripten n'implémente pas flush. On interprète zéro comme un flush.
1138 output_buffer += '\0';
1139 std::cout << output_buffer << std::flush;
1140 output_buffer.clear();
1141}
1142
1143void App::Internal::InstallPipedInputHandling() {
1144 is_stdin_a_tty_ = false;
1145 is_stdout_a_tty_ = false;
1146#if defined(__EMSCRIPTEN__)
1147 is_stdin_a_tty_ = true;
1148 is_stdout_a_tty_ = true;
1149#elif defined(_WIN32)
1150 is_stdin_a_tty_ = _isatty(_fileno(stdin));
1151 is_stdout_a_tty_ = _isatty(_fileno(stdout));
1152#else
1153 tty_fd_ = STDIN_FILENO;
1154 is_stdout_a_tty_ = isatty(STDOUT_FILENO);
1155 // Gère la redirection de l'entrée piped si explicitement activée par
1156 // l'application. Cela permet aux applications de lire les données depuis
1157 // stdin tout en continuant à recevoir les entrées clavier du terminal pour
1158 // un usage interactif.
1159 if (!handle_piped_input_) {
1160 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1161 } else if (isatty(STDIN_FILENO)) {
1162 is_stdin_a_tty_ = true;
1163 } else {
1164 // Ouvre /dev/tty pour les entrées clavier.
1165 tty_fd_ = open("/dev/tty", O_RDONLY); // NOLINT
1166 if (tty_fd_ < 0) {
1167 // Échec de l'ouverture de /dev/tty (conteneurs, systèmes sans tête,
1168 // etc.)
1169 tty_fd_ = STDIN_FILENO; // Se rabat sur stdin.
1170 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1171 } else {
1172 is_stdin_a_tty_ = true;
1173 // Ferme le descripteur de fichier /dev/tty à la sortie.
1174 on_exit_functions.emplace([this] {
1175 close(tty_fd_);
1176 tty_fd_ = -1;
1177 });
1178 }
1179 }
1180#endif
1181}
1182
1183void App::Internal::InstallTerminalInfo() {
1184 // Demande au terminal de rapporter la forme actuelle du curseur. On la
1185 // restaurera à la sortie.
1186 if (is_stdout_a_tty_) {
1187 TerminalSend(DECRQSS_DECSCUSR);
1188 TerminalSend("\033[>q"); // XTVERSION
1189 TerminalSend("\033[>c"); // DA2
1190 TerminalSend("\033[c"); // DA1
1191 TerminalFlush();
1192 }
1193
1194 // Attend la réponse de la forme du curseur en utilisant la tête de
1195 // configuration.
1196 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
1197 // Un récepteur limité à la configuration : en garder un vivant après la
1198 // configuration épinglerait chaque événement suivant dans le tampon, le
1199 // faisant grossir pendant toute la durée de vie de l'application.
1200 auto setup_receiver = event_buffer.CreateReceiver();
1201 auto start = std::chrono::steady_clock::now();
1202 bool terminal_capabilities_received = false;
1203 // Attend la réponse de la forme du curseur en utilisant la tête de
1204 // configuration.
1205 while (true) {
1206 FetchTerminalEvents();
1207 while (setup_receiver->Has()) {
1208 const auto event = setup_receiver->Pop();
1209 if (event.is_cursor_shape()) {
1210 cursor_reset_shape_ = event.cursor_shape();
1211 }
1212
1213 if (event.IsTerminalCapabilities()) {
1214 terminal_capabilities_ = event.TerminalCapabilities();
1215 terminal_capabilities_received = true;
1216 }
1217
1218 if (event.IsTerminalNameVersion()) {
1219 terminal_name_ = event.TerminalName();
1220 terminal_version_ = event.TerminalVersion();
1221 }
1222
1223 if (event.IsTerminalEmulator()) {
1224 terminal_emulator_name_ = event.TerminalEmulatorName();
1225 terminal_emulator_version_ = event.TerminalEmulatorVersion();
1226 }
1227 }
1228
1229 // Les réponses sont censées être reçues dans l'ordre, on peut donc
1230 // sortir de la boucle quand la dernière (XTVERSION) est reçue. On fixe
1231 // aussi un délai limite pour éviter d'attendre indéfiniment si le
1232 // terminal ne supporte pas ces requêtes.
1233 if (terminal_capabilities_received) {
1234 break;
1235 }
1236
1237 if (std::chrono::steady_clock::now() - start >
1238 std::chrono::milliseconds(500)) {
1239 break;
1240 }
1241 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1242 }
1243 }
1244
1245 // Définit les quirks et le support des couleurs en fonction de
1246 // l'identification du terminal.
1247 Terminal::Quirks quirks = Terminal::GetQuirks();
1248
1249 auto color_support = Terminal::ComputeColorSupport(
1250 util::GetEnv("TERM"), util::GetEnv("COLORTERM"),
1251 util::GetEnv("TERM_PROGRAM"), terminal_name_, terminal_emulator_name_,
1252 terminal_capabilities_);
1253
1254 quirks.SetColorSupport(color_support);
1255
1256 const bool is_modern_emulator = (terminal_emulator_name_ != "unknown");
1257 const bool is_vt220_plus =
1258 (terminal_name_ != "vt100" && terminal_name_ != "unknown");
1259 bool reports_utf8 = false;
1260 for (const int x : terminal_capabilities_) {
1261 if (x == 52) {
1262 reports_utf8 = true;
1263 break;
1264 }
1265 }
1266
1267 // Heuristique : Si l'émulateur de terminal est moderne, ou s'il rapporte
1268 // supporter l'UTF-8 ou la couleur, on peut supposer qu'il supporte les
1269 // caractères de blocs et le masquage du curseur, qui sont essentiels pour
1270 // une bonne expérience. C'est une heuristique, mais elle nous permet de
1271 // contourner certains émulateurs de terminal plus anciens qui ne
1272 // supportent pas ces fonctionnalités, tout en continuant à offrir une bonne
1273 // expérience sur les émulateurs de terminal modernes qui les supportent.
1274 bool modern = is_modern_emulator || is_vt220_plus || reports_utf8;
1275 if (modern) {
1276 quirks.SetBlockCharacters(true);
1277 quirks.SetCursorHiding(true);
1278 quirks.SetComponentAscii(false);
1279 }
1280
1281 Terminal::SetQuirks(quirks);
1282
1283 on_exit_functions.emplace([this] {
1284 TerminalSend("\033[?25h"); // Active le curseur.
1285 if (is_stdout_a_tty_) {
1286 TerminalSend("\033[" + std::to_string(cursor_reset_shape_) + " q");
1287 }
1288 });
1289}
1290
1291void App::Internal::Signal(int signal) {
1292 if (signal == SIGABRT) {
1293 public_->Exit();
1294 return;
1295 }
1296
1297// Windows ne supporte pas SIGTSTP / SIGWINCH
1298#if !defined(_WIN32)
1299 if (signal == SIGTSTP) {
1300 public_->Post([&] {
1301 TerminalSend(ResetCursorPosition());
1302 public_->ResetPosition(output_buffer, /*clear*/ true);
1303 Uninstall();
1304 public_->dimx_ = 0;
1305 public_->dimy_ = 0;
1306 (void)std::raise(SIGTSTP);
1307 Install();
1308 });
1309 return;
1310 }
1311
1312 if (signal == SIGWINCH) {
1313 public_->Post(Event::Special({0}));
1314 return;
1315 }
1316#endif
1317}
1318
1319size_t App::Internal::FetchTerminalEvents() {
1320#if defined(_WIN32)
1321 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1322 // Vérifie s'il y a une entrée dans la console.
1323 auto console = GetStdHandle(STD_INPUT_HANDLE);
1324 DWORD number_of_events = 0;
1325 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1326 return std::vector<INPUT_RECORD>();
1327 }
1328 if (number_of_events <= 0) {
1329 // Aucune entrée, on retourne.
1330 return std::vector<INPUT_RECORD>();
1331 }
1332 // Lit les événements d'entrée.
1333 std::vector<INPUT_RECORD> records(number_of_events);
1334 DWORD number_of_events_read = 0;
1335 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1336 &number_of_events_read)) {
1337 return std::vector<INPUT_RECORD>();
1338 }
1339 records.resize(number_of_events_read);
1340 return records;
1341 };
1342
1343 auto records = get_input_records();
1344 if (records.size() == 0) {
1345 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1346 const size_t timeout_microseconds =
1347 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1348 terminal_input_parser.Timeout(timeout_microseconds);
1349 return 0;
1350 }
1351 last_char_time = std::chrono::steady_clock::now();
1352
1353 // Convertit les événements d'entrée en événements FTXUI.
1354 // Pour chaque événement, on appelle l'analyseur d'entrée du terminal pour
1355 // le convertir en Event.
1356 std::wstring wstring;
1357 for (const auto& r : records) {
1358 switch (r.EventType) {
1359 case KEY_EVENT: {
1360 auto key_event = r.Event.KeyEvent;
1361 // ignore les événements de relâchement de touche
1362 if (key_event.bKeyDown == FALSE) {
1363 continue;
1364 }
1365 const wchar_t wc = key_event.uChar.UnicodeChar;
1366 wstring += wc;
1367 if (wc >= 0xd800 && wc <= 0xdbff) {
1368 // Attend l'arrivée du Low Surrogate dans le prochain enregistrement.
1369 continue;
1370 }
1371 for (auto it : to_string(wstring)) {
1372 terminal_input_parser.Add(it);
1373 }
1374 wstring.clear();
1375 } break;
1376 case WINDOW_BUFFER_SIZE_EVENT:
1377 public_->Post(Event::Special({0}));
1378 break;
1379 case MENU_EVENT:
1380 case FOCUS_EVENT:
1381 case MOUSE_EVENT:
1382 // TODO(mauve): Implémenter plus tard.
1383 break;
1384 }
1385 }
1386 return records.size();
1387#elif defined(__EMSCRIPTEN__)
1388 // Lit les caractères depuis le terminal.
1389 // On l'a configuré pour être non bloquant.
1390 std::array<char, 4096> out{};
1391 const ssize_t l = read(STDIN_FILENO, out.data(), out.size());
1392 if (l <= 0) {
1393 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1394 const size_t timeout_microseconds =
1395 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1396 terminal_input_parser.Timeout(timeout_microseconds);
1397 return 0;
1398 }
1399 last_char_time = std::chrono::steady_clock::now();
1400
1401 // Convertit les caractères en événements.
1402 for (ssize_t i = 0; i < l; ++i) {
1403 terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1404 }
1405 return (size_t)l;
1406#else // POSIX (Linux & Mac)
1407 struct pollfd pfd = {tty_fd_, POLLIN, 0};
1408 const int poll_result = poll(&pfd, 1, 0);
1409 if (poll_result <= 0) {
1410 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1411 const size_t timeout_ms =
1412 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1413 terminal_input_parser.Timeout(static_cast<int>(timeout_ms));
1414 return 0;
1415 }
1416 last_char_time = std::chrono::steady_clock::now();
1417
1418 // Draine l'entrée disponible, afin que les rafales (par ex. un défilement
1419 // rapide de la molette souris) ne s'accumulent pas d'une frame à l'autre.
1420 // Le total est borné pour maintenir la frame réactive en cas d'afflux
1421 // continu d'entrées. Voir #1348.
1422 constexpr size_t kMaxBytesPerFetch = 64 * 1024;
1423 std::array<char, 4096> out{};
1424 size_t total = 0;
1425 while (total < kMaxBytesPerFetch) {
1426 const ssize_t l = read(tty_fd_, out.data(), out.size());
1427 if (l <= 0) {
1428 break;
1429 }
1430
1431 // Convertit les caractères en événements.
1432 for (ssize_t i = 0; i < l; ++i) {
1433 terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1434 }
1435 total += static_cast<size_t>(l);
1436
1437 pfd.revents = 0;
1438 if (poll(&pfd, 1, 0) <= 0) {
1439 break;
1440 }
1441 }
1442 return total;
1443#endif
1444}
1445
1446void App::Internal::PostAnimationTask() {
1447 public_->Post(AnimationTask());
1448
1449 // Répète la tâche d'animation toutes les 15ms. Cela correspond à un taux
1450 // de rafraîchissement d'environ 66fps.
1451 task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1452 std::chrono::milliseconds(15));
1453}
1454
1455App::App(std::unique_ptr<Internal> internal, int dimx, int dimy)
1456 : Screen(dimx, dimy), internal_(std::move(internal)) {
1457 internal_->public_ = this;
1458}
1459
1460App::App(App&& other) noexcept : Screen(std::move(other)) {
1461 internal_ = std::move(other.internal_);
1462 if (internal_) {
1463 internal_->public_ = this;
1464 }
1465}
1466
1467App& App::operator=(App&& other) noexcept {
1468 Screen::operator=(std::move(other));
1469 internal_ = std::move(other.internal_);
1470 if (internal_) {
1471 internal_->public_ = this;
1472 }
1473 return *this;
1474}
1475
1476App::~App() = default;
1477
1478// static
1479App App::FixedSize(int dimx, int dimy) {
1480 auto internal =
1481 std::make_unique<Internal>(nullptr, AppDimension::Fixed, false);
1482 return App(std::move(internal), dimx, dimy);
1483}
1484
1485// static
1486App App::Fullscreen() {
1487 return FullscreenAlternateScreen();
1488}
1489
1490// static
1491App App::FullscreenPrimaryScreen() {
1492 auto terminal = Terminal::Size();
1493 auto internal =
1494 std::make_unique<Internal>(nullptr, AppDimension::Fullscreen, false);
1495 return App(std::move(internal), terminal.dimx, terminal.dimy);
1496}
1497
1498// static
1499App App::FullscreenAlternateScreen() {
1500 auto terminal = Terminal::Size();
1501 auto internal =
1502 std::make_unique<Internal>(nullptr, AppDimension::Fullscreen, true);
1503 return App(std::move(internal), terminal.dimx, terminal.dimy);
1504}
1505
1506// static
1507App App::FitComponent() {
1508 auto terminal = Terminal::Size();
1509 auto internal =
1510 std::make_unique<Internal>(nullptr, AppDimension::FitComponent, false);
1511 return App(std::move(internal), terminal.dimx, terminal.dimy);
1512}
1513
1514// static
1515App App::TerminalOutput() {
1516 auto terminal = Terminal::Size();
1517 auto internal =
1518 std::make_unique<Internal>(nullptr, AppDimension::TerminalOutput, false);
1519 return App(std::move(internal), terminal.dimx, terminal.dimy);
1520}
1521
1522void App::TrackMouse(bool enable) {
1523 internal_->track_mouse_ = enable;
1524}
1525
1526void App::HandlePipedInput(bool enable) {
1527 internal_->handle_piped_input_ = enable;
1528}
1529
1530// static
1531App* App::Active() {
1532 return g_active_screen;
1533}
1534
1535void App::Loop(Component component) {
1536 class Loop loop(this, std::move(component));
1537 loop.Run();
1538}
1539
1540void App::Exit() {
1541 Post([this] { internal_->ExitNow(); });
1542}
1543
1544Closure App::ExitLoopClosure() {
1545 return [this] { Exit(); };
1546}
1547
1548void App::Post(Task task) {
1549 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
1550 if (internal_->component_) {
1551 internal_->HandleTask(internal_->component_, task);
1552 return;
1553 }
1554
1555 // S'il n'y a pas de composant, on peut quand même exécuter les closures.
1556 if (std::holds_alternative<Closure>(task)) {
1557 std::get<Closure>(task)();
1558 }
1559 });
1560}
1561
1562void App::PostEvent(Event event) {
1563 // PostEvent est documenté comme étant thread-safe : on passe par la file de
1564 // tâches protégée par un mutex. L'event_buffer n'est sûr à utiliser que
1565 // depuis le thread principal.
1566 Post(Task(std::move(event)));
1567}
1568
1569// static
1570void App::PostEventOrExecute(Closure closure) {
1571 if (!closure) {
1572 return;
1573 }
1574 if (auto* app = App::Active()) {
1575 app->Post(std::move(closure));
1576 } else {
1577 closure();
1578 }
1579}
1580
1581void App::RequestAnimationFrame() {
1582 if (internal_->animation_requested_) {
1583 return;
1584 }
1585 internal_->animation_requested_ = true;
1586 auto now = animation::Clock::now();
1587 const auto time_histeresis = std::chrono::milliseconds(33);
1588 if (now - internal_->previous_animation_time_ >= time_histeresis) {
1589 internal_->previous_animation_time_ = now;
1590 }
1591}
1592
1593CapturedMouse App::CaptureMouse() {
1594 if (internal_->mouse_captured) {
1595 return nullptr;
1596 }
1597 internal_->mouse_captured = true;
1598 return std::make_unique<CapturedMouseImpl>(
1599 [this] { internal_->mouse_captured = false; });
1600}
1601
1602Closure App::WithRestoredIO(Closure fn) {
1603 return [this, fn] {
1604 internal_->Uninstall();
1605 fn();
1606 internal_->Install();
1607 };
1608}
1609
1610void App::ForceHandleCtrlC(bool force) {
1611 internal_->force_handle_ctrl_c_ = force;
1612}
1613
1614void App::ForceHandleCtrlZ(bool force) {
1615 internal_->force_handle_ctrl_z_ = force;
1616}
1617
1618std::string App::GetSelection() {
1619 if (!internal_->selection_) {
1620 return "";
1621 }
1622 return internal_->selection_->GetParts();
1623}
1624
1625void App::SelectionChange(std::function<void()> callback) {
1626 internal_->selection_on_change_ = std::move(callback);
1627}
1628
1629const std::string& App::TerminalName() const {
1630 return internal_->terminal_name_;
1631}
1632
1633int App::TerminalVersion() const {
1634 return internal_->terminal_version_;
1635}
1636
1637const std::string& App::TerminalEmulatorName() const {
1638 return internal_->terminal_emulator_name_;
1639}
1640
1641const std::string& App::TerminalEmulatorVersion() const {
1642 return internal_->terminal_emulator_version_;
1643}
1644
1645const std::vector<int>& App::TerminalCapabilities() const {
1646 return internal_->terminal_capabilities_;
1647}
1648
1649std::vector<std::string> App::TerminalCapabilityNames() const {
1650 return Event::TerminalCapabilities("", internal_->terminal_capabilities_)
1651 .TerminalCapabilityNames();
1652}
1653
1654// Loop appelle ceci :
1655
1656void App::ExitNow() {
1657 internal_->ExitNow();
1658}
1659void App::Install() {
1660 internal_->Install();
1661}
1662void App::Uninstall() {
1663 internal_->Uninstall();
1664}
1665void App::PreMain() {
1666 internal_->PreMain();
1667}
1668void App::PostMain() {
1669 internal_->PostMain();
1670}
1671bool App::HasQuitted() {
1672 return internal_->HasQuitted();
1673}
1674void App::RunOnce(const Component& component) {
1675 internal_->RunOnce(component);
1676}
1677void App::RunOnceBlocking(Component component) {
1678 internal_->RunOnceBlocking(component);
1679}
1680void App::HandleTask(Component component, Task& task) {
1681 internal_->HandleTask(component, task);
1682}
1683bool App::HandleSelection(bool handled, Event event) {
1684 return internal_->HandleSelection(handled, event);
1685}
1686void App::Draw(Component component) {
1687 internal_->Draw(component);
1688}
1689std::string App::ResetCursorPosition() {
1690 return internal_->ResetCursorPosition();
1691}
1692void App::RequestCursorPosition(bool force) {
1693 internal_->RequestCursorPosition(force);
1694}
1695void App::TerminalSend(std::string_view s) {
1696 internal_->TerminalSend(s);
1697}
1698void App::TerminalFlush() {
1699 internal_->TerminalFlush();
1700}
1701void App::InstallPipedInputHandling() {
1702 internal_->InstallPipedInputHandling();
1703}
1704void App::InstallTerminalInfo() {
1705 internal_->InstallTerminalInfo();
1706}
1707void App::Signal(int signal) {
1708 internal_->Signal(signal);
1709}
1710size_t App::FetchTerminalEvents() {
1711 return internal_->FetchTerminalEvents();
1712}
1713void App::PostAnimationTask() {
1714 internal_->PostAnimationTask();
1715}
1716
1717Loop::Loop(App* screen, Component component)
1718 : screen_(screen), component_(std::move(component)) {
1719 screen_->PreMain();
1720}
1721
1722Loop::~Loop() {
1723 screen_->PostMain();
1724}
1725
1726bool Loop::HasQuitted() {
1727 return screen_->HasQuitted();
1728}
1729
1730void Loop::RunOnce() {
1731 screen_->RunOnce(component_);
1732}
1733
1734void Loop::RunOnceBlocking() {
1735 screen_->RunOnceBlocking(component_);
1736}
1737
1738void Loop::Run() {
1739 while (!HasQuitted()) {
1740 RunOnceBlocking();
1741 }
1742}
1743
1744} // namespace ftxui
Quirks GetQuirks()
Récupère les particularités du terminal.
Definition terminal.cpp:355
Dimensions Size()
Obtenir la taille du terminal.
Definition terminal.cpp:303
void SetQuirks(const Quirks &quirks)
Redéfinit les particularités du terminal.
Definition terminal.cpp:365
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:332
Color ComputeColorSupport(std::string_view term, std::string_view colorterm, std::string_view term_program, std::string_view terminal_name, std::string_view terminal_emulator_name, const std::vector< int > &capabilities)
Calcule la prise en charge des couleurs en fonction des variables d'environnement et de l'identificat...
Definition terminal.cpp:196
std::chrono::duration< float > Duration
Definition animation.hpp:31
std::chrono::time_point< Clock > TimePoint
Definition animation.hpp:30
void RequestAnimationFrame()
Definition app.cpp:74
const char * GetEnv(const char *name)
Definition util.hpp:18
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
Definition util.hpp:13
L'espace de noms FTXUI ftxui::
Definition animation.hpp:11
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(std::wstring_view s)
Convertit un std::wstring en std::string UTF8.
Definition string.cpp:563
AppDimension
Definition app.cpp:66
std::variant< Event, Closure, AnimationTask > Task
Definition task.hpp:14
void Render(Screen &screen, Node *node, Selection &selection)
Definition node.cpp:105
std::function< void()> Closure
Definition task.hpp:13
std::shared_ptr< ComponentBase > Component
Definition app.hpp:23