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 // Estado del manejo de entrada por tubería (solo POSIX)
127 bool handle_piped_input_ = true;
128 bool is_stdin_a_tty_ = false;
129 bool is_stdout_a_tty_ = false;
130 // Descriptor de archivo para /dev/tty, usado para el manejo de entrada por tubería.
131 int tty_fd_ = -1;
132
133 std::string terminal_name_ = "unknown";
134 int terminal_version_ = 0;
135
136 std::string terminal_emulator_name_ = "unknown";
137 std::string terminal_emulator_version_ = "unknown";
138
139 std::vector<int> terminal_capabilities_;
140
141 // API de selección:
142 CapturedMouse selection_pending_;
143 struct SelectionData {
144 int start_x = -1;
145 int start_y = -1;
146 int end_x = -2;
147 int end_y = -2;
148 bool empty = true;
149 bool operator==(const SelectionData& other) const {
150 if (empty && other.empty) {
151 return true;
152 }
153 if (empty || other.empty) {
154 return false;
155 }
156 return start_x == other.start_x && start_y == other.start_y &&
157 end_x == other.end_x && end_y == other.end_y;
158 }
159 bool operator!=(const SelectionData& other) const {
160 return !(*this == other);
161 }
162 };
163 SelectionData selection_data_;
164 SelectionData selection_data_previous_;
165 std::unique_ptr<Selection> selection_;
166 std::function<void()> selection_on_change_;
167
168 Component component_;
169
170 // Preexistente en Internal:
171 TerminalInputParser terminal_input_parser;
172 task::TaskRunner task_runner;
173 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
174 std::chrono::steady_clock::now();
175 std::string output_buffer;
176
177 class ThrottledRequest {
178 public:
179 ThrottledRequest(App::Internal* internal, std::function<void()> send)
180 : internal_(internal), send_(std::move(send)) {}
181
182 void Request(bool force = false) {
183 if (!internal_->is_stdin_a_tty_) {
184 return;
185 }
186
187 if (force) {
188 Send();
189 return;
190 }
191
192 // Permitir solo una solicitud pendiente a la vez. Esto es para evitar saturar
193 // el terminal con solicitudes.
194 if (HasPending()) {
195 return;
196 }
197
198 const auto now = std::chrono::steady_clock::now();
199 if (now - last_request_time_ < std::chrono::milliseconds(500)) {
200 // Demasiado pronto desde la última solicitud. Omitirla: la solicitud debe enviarse
201 // de forma síncrona desde Draw(), justo después de que el cursor se mueve al
202 // origen del fotograma, para que la respuesta del terminal refleje esa
203 // posición. Draw() vuelve a llamar a Request() en el siguiente fotograma, así que la
204 // solicitud no se pierde, solo se retrasa.
205 return;
206 }
207
208 Send();
209 }
210
211 void OnReply() { pending_request_ = false; }
212
213 bool HasPending() const {
214 if (!pending_request_) {
215 return false;
216 }
217 const auto now = std::chrono::steady_clock::now();
218 return now - last_sent_time_ < std::chrono::seconds(5);
219 }
220
221 private:
222 void Send() {
223 last_sent_time_ = std::chrono::steady_clock::now();
224 last_request_time_ = last_sent_time_;
225 pending_request_ = true;
226 send_();
227 }
228
229 App::Internal* internal_;
230 std::function<void()> send_;
231 bool pending_request_ = false;
232 std::chrono::steady_clock::time_point last_request_time_ =
233 std::chrono::steady_clock::now() - std::chrono::hours(1);
234 std::chrono::steady_clock::time_point last_sent_time_ =
235 std::chrono::steady_clock::now() - std::chrono::hours(1);
236 };
237
238 ThrottledRequest cursor_position_request;
239
240 MultiReceiverBuffer<Event> event_buffer;
241 std::unique_ptr<MultiReceiverBuffer<Event>::Receiver> main_loop_receiver;
242
243 Internal(App* app, AppDimension dimension, bool use_alternative_screen);
244
245 void ExitNow();
246 void Install();
247 void Uninstall();
248 void PreMain();
249 void PostMain();
250 bool HasQuitted();
251 void RunOnce(const Component& component);
252 void RunOnceBlocking(Component component);
253 void HandleTask(Component component, Task& task);
254 bool HandleSelection(bool handled, Event event);
255 void Draw(Component component);
256 std::string ResetCursorPosition();
257 void RequestCursorPosition(bool force = false);
258 void TerminalSend(std::string_view);
259 void TerminalFlush();
260 void InstallPipedInputHandling();
261 void InstallTerminalInfo();
262 void Signal(int signal);
263 size_t FetchTerminalEvents();
264 void PostAnimationTask();
265};
266
267namespace {
268
269App* g_active_screen = nullptr; // NOLINT
270
271std::stack<Closure> on_exit_functions; // NOLINT
272
273void OnExit() {
274 while (!on_exit_functions.empty()) {
275 on_exit_functions.top()();
276 on_exit_functions.pop();
277 }
278}
279
280// CSI: Introductor de Secuencia de Control
281const std::string CSI = "\x1b["; // NOLINT
282 //
283// DCS: Device Control String
284const std::string DCS = "\x1bP"; // NOLINT
285
286// ST: Terminador de cadena (String Terminator)
287const std::string ST = "\x1b\\"; // NOLINT
288
289// DECRQSS: Solicitud de Cadena de Estado
290// DECSCUSR: Establecer Estilo de Cursor
291const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
292
293// DEC: Corporación de Equipos Digitales
294enum class DECMode : std::uint16_t {
295 kLineWrap = 7,
296 kCursor = 25,
297
298 kMouseX10 = 9,
299 kMouseVt200 = 1000,
300 kMouseVt200Highlight = 1001,
301
302 kMouseBtnEventMouse = 1002,
303 kMouseAnyEvent = 1003,
304
305 kMouseUtf8 = 1005,
306 kMouseSgrExtMode = 1006,
307 kMouseUrxvtMode = 1015,
308 kMouseSgrPixelsMode = 1016,
309 kAlternateScreen = 1049,
310};
311
312// Informe de estado del dispositivo (DSR) {
313enum class DSRMode : std::uint8_t {
314 kCursor = 6,
315};
316
317std::string Serialize(const std::vector<DECMode>& parameters) {
318 bool first = true;
319 std::string out;
320 for (const DECMode parameter : parameters) {
321 if (!first) {
322 out += ";";
323 }
324 out += std::to_string(int(parameter));
325 first = false;
326 }
327 return out;
328}
329
330// Establecer Modo Privado DEC (DECSET)
331std::string Set(const std::vector<DECMode>& parameters) {
332 return CSI + "?" + Serialize(parameters) + "h";
333}
334
335// Restablecer Modo Privado DEC (DECRST)
336std::string Reset(const std::vector<DECMode>& parameters) {
337 return CSI + "?" + Serialize(parameters) + "l";
338}
339
340// Informe de Estado del Dispositivo (DSR)
341std::string DeviceStatusReport(DSRMode ps) {
342 return CSI + std::to_string(int(ps)) + "n";
343}
344
345class CapturedMouseImpl : public CapturedMouseInterface {
346 public:
347 explicit CapturedMouseImpl(std::function<void(void)> callback)
348 : callback_(std::move(callback)) {}
349 ~CapturedMouseImpl() override { callback_(); }
350 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
351 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
352 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
353 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
354
355 private:
356 std::function<void(void)> callback_;
357};
358
359#if !defined(_WIN32)
360std::atomic<int> g_signal_exit_count = 0; // NOLINT
361std::atomic<int> g_signal_stop_count = 0; // NOLINT
362std::atomic<int> g_signal_resize_count = 0; // NOLINT
363#else
364std::atomic<int> g_signal_exit_count = 0; // NOLINT
365#endif
366
367// Rastrea si el terminal está actualmente configurado en modo raw.
368// Se usa para evitar la doble restauración en salidas de emergencia y normales.
369std::atomic<bool> g_terminal_is_raw{false};
370
371// Almacena la última señal diferida recibida (p. ej. SIGINT, SIGTERM) para
372// volver a emitirla durante la desinstalación/salida.
373std::atomic<int> g_last_signal{0}; // NOLINT
374
375#if defined(_WIN32)
376using SignalHandler = void (*)(int);
377// Almacena los manejadores de señal originales antes de que FTXUI instalara los suyos.
378std::map<int, SignalHandler> g_old_signal_handlers;
379
380// Almacena los modos de consola originales para restaurarlos durante la salida.
381DWORD g_original_stdout_mode = 0;
382DWORD g_original_stdin_mode = 0;
383bool g_has_original_console_mode = false;
384#else
385// Almacena las estructuras sigaction originales antes de que FTXUI instalara las suyas.
386std::map<int, struct sigaction> g_old_sigactions;
387
388// Almacena la configuración termios original del terminal para restaurarla durante la salida.
389struct termios g_original_termios;
390bool g_has_original_termios = false;
391int g_tty_fd = -1;
392#endif
393
394// Restaura el manejador de señal original para la señal dada y vuelve a emitirla.
395// Función segura para señales asíncronas.
396void RestoreSignalHandlerAndRaise(int signal) {
397#if defined(_WIN32)
398 auto it = g_old_signal_handlers.find(signal);
399 auto old_handler = (it != g_old_signal_handlers.end()) ? it->second : SIG_DFL;
400 std::signal(signal, old_handler);
401#else
402 auto it = g_old_sigactions.find(signal);
403 if (it != g_old_sigactions.end()) {
404 sigaction(signal, &it->second, nullptr);
405 } else {
406 struct sigaction sa;
407 sa.sa_handler = SIG_DFL;
408 sigemptyset(&sa.sa_mask);
409 sa.sa_flags = 0;
410 sigaction(signal, &sa, nullptr);
411 }
412#endif
413 std::raise(signal);
414}
415
416// Restauración de emergencia del estado del terminal.
417// Función segura para señales asíncronas.
418void RestoreTerminalEmergency() {
419 if (!g_terminal_is_raw.exchange(false)) {
420 return;
421 }
422#if defined(_WIN32)
423 if (g_has_original_console_mode) {
424 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
425 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
426 SetConsoleMode(stdout_handle, g_original_stdout_mode);
427 SetConsoleMode(stdin_handle, g_original_stdin_mode);
428 }
429#else
430 if (g_has_original_termios && g_tty_fd >= 0) {
431 const char restore_seq[] =
432 "\x1b[?25h" // Mostrar cursor.
433 "\x1b[?1049l" // Cambiar al búfer de pantalla normal.
434 "\x1b[?1000l" // Deshabilitar el seguimiento normal del ratón.
435 "\x1b[?1002l" // Deshabilitar el seguimiento de eventos de botón del ratón.
436 "\x1b[?1003l" // Deshabilitar todo el seguimiento de movimiento del ratón.
437 "\x1b[?1006l" // Deshabilitar el seguimiento SGR del ratón.
438 "\x1b[?1015l" // Deshabilitar el seguimiento Urxvt del ratón.
439 "\x1b[?7h"; // Habilitar el ajuste de línea.
440 std::ignore = write(STDOUT_FILENO, restore_seq, sizeof(restore_seq) - 1);
441 tcsetattr(g_tty_fd, TCSANOW, &g_original_termios);
442 }
443#endif
444}
445
446// Función asíncrona segura para señales
447void RecordSignal(int signal) {
448 switch (signal) {
449 // Terminación anormal (p. ej. abort() o fallo de aserción).
450 case SIGABRT:
451 // Operación aritmética errónea (p. ej. división por cero).
452 case SIGFPE:
453 // Instrucción ilegal.
454 case SIGILL:
455 // Referencia de memoria inválida (fallo de segmentación).
456 case SIGSEGV:
457#if !defined(_WIN32)
458 // Error de bus (p. ej. mala alineación de acceso a memoria).
459 case SIGBUS:
460 // Llamada al sistema incorrecta.
461 case SIGSYS:
462#endif
463 {
464 RestoreTerminalEmergency();
465 RestoreSignalHandlerAndRaise(signal);
466 break;
467 }
468
469 // Interrupción del terminal (p. ej. Ctrl-C).
470 case SIGINT:
471 // Solicitud de terminación.
472 case SIGTERM:
473#if !defined(_WIN32)
474 // Salida del terminal (p. ej. Ctrl-\, produce volcado de núcleo).
475 case SIGQUIT:
476 // Colgado detectado en el terminal de control o muerte del proceso de control.
477 case SIGHUP:
478#endif
479 g_last_signal.store(signal);
480 g_signal_exit_count++;
481 break;
482
483#if !defined(_WIN32)
484 // Señal de parada del terminal (p. ej. Ctrl-Z).
485 case SIGTSTP: // NOLINT
486 g_signal_stop_count++;
487 break;
488
489 // Cambio de tamaño de la ventana del terminal.
490 case SIGWINCH: // NOLINT
491 g_signal_resize_count++;
492 break;
493#endif
494
495 default:
496 break;
497 }
498}
499
500void ExecuteSignalHandlers() {
501 if (g_last_signal.load() != 0) {
502 App::Private::Signal(*g_active_screen, SIGABRT);
503 }
504
505 int signal_exit_count = g_signal_exit_count.exchange(0);
506 while (signal_exit_count--) {
507 App::Private::Signal(*g_active_screen, SIGABRT);
508 }
509
510#if !defined(_WIN32)
511 int signal_stop_count = g_signal_stop_count.exchange(0);
512 while (signal_stop_count--) {
513 App::Private::Signal(*g_active_screen, SIGTSTP);
514 }
515
516 int signal_resize_count = g_signal_resize_count.exchange(0);
517 while (signal_resize_count--) {
518 App::Private::Signal(*g_active_screen, SIGWINCH);
519 }
520#endif
521}
522
523void InstallSignalHandler(int sig) {
524#if defined(_WIN32)
525 auto old_signal_handler = std::signal(sig, RecordSignal);
526 g_old_signal_handlers[sig] = old_signal_handler;
527 on_exit_functions.emplace(
528 [=] { std::ignore = std::signal(sig, old_signal_handler); });
529#else
530 struct sigaction sa;
531 sa.sa_handler = RecordSignal;
532 sigemptyset(&sa.sa_mask);
533 sa.sa_flags = SA_RESTART;
534 struct sigaction old_sa;
535 sigaction(sig, &sa, &old_sa);
536 g_old_sigactions[sig] = old_sa;
537 on_exit_functions.emplace([=] { sigaction(sig, &old_sa, nullptr); });
538#endif
539}
540
541} // namespace
542
543App::Internal::Internal(App* app,
544 AppDimension dimension,
545 bool use_alternative_screen)
546 : public_(app),
547 dimension_(dimension),
548 use_alternative_screen_(use_alternative_screen),
549 terminal_input_parser([&](Event event) {
550 event_buffer.Push(std::move(event));
551 }),
552 cursor_position_request(this, [this] {
553 TerminalSend(DeviceStatusReport(DSRMode::kCursor));
554 }) {
555 main_loop_receiver = event_buffer.CreateReceiver();
556}
557
558void App::Internal::ExitNow() {
559 quit_ = true;
560}
561
562void App::Internal::Install() {
563 frame_valid_ = false;
564
565 // Vaciar el búfer de stdout para asegurar que todo lo que el usuario haya impreso antes
566 // se aplique completamente antes de que comencemos a modificar la configuración del terminal. Esto
567 // es importante porque estamos usando dos canales diferentes (stdout vs
568 // termios/WinAPI) para comunicarnos con el emulador de terminal a continuación. Ver
569 // https://github.com/ArthurSonzogni/FTXUI/issues/846
570 TerminalFlush();
571
572 InstallPipedInputHandling();
573
574 // Después de desinstalar la nueva configuración, vaciarla al terminal para
575 // asegurar que se aplique completamente:
576 on_exit_functions.emplace([this] { TerminalFlush(); });
577
578 // Instalar manejadores de señal para restaurar el estado del terminal al salir. Los manejadores
579 // de señal predeterminados se restauran al salir.
580 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
581 InstallSignalHandler(signal);
582 }
583
584// Guardar la antigua configuración del terminal y restaurarla al salir.
585#if defined(_WIN32)
586 // Habilitar el procesamiento VT en stdout y stdin
587 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
588 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
589
590 DWORD out_mode = 0;
591 DWORD in_mode = 0;
592 GetConsoleMode(stdout_handle, &out_mode);
593 GetConsoleMode(stdin_handle, &in_mode);
594 g_original_stdout_mode = out_mode;
595 g_original_stdin_mode = in_mode;
596 g_has_original_console_mode = true;
597 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
598 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
599
600 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
601 const int enable_virtual_terminal_processing = 0x0004;
602 const int disable_newline_auto_return = 0x0008;
603 out_mode |= enable_virtual_terminal_processing;
604 out_mode |= disable_newline_auto_return;
605
606 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
607 const int enable_line_input = 0x0002;
608 const int enable_echo_input = 0x0004;
609 const int enable_virtual_terminal_input = 0x0200;
610 const int enable_window_input = 0x0008;
611 in_mode &= ~enable_echo_input;
612 in_mode &= ~enable_line_input;
613 in_mode |= enable_virtual_terminal_input;
614 in_mode |= enable_window_input;
615
616 SetConsoleMode(stdin_handle, in_mode);
617 SetConsoleMode(stdout_handle, out_mode);
618#else // POSIX (Linux y Mac)
619 for (const int signal :
620 {SIGWINCH, SIGTSTP, SIGBUS, SIGSYS, SIGQUIT, SIGHUP}) {
621 InstallSignalHandler(signal);
622 }
623
624 struct termios terminal; // NOLINT
625 tcgetattr(tty_fd_, &terminal);
626 g_original_termios = terminal;
627 g_tty_fd = tty_fd_;
628 g_has_original_termios = true;
629 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
630 tcsetattr(tty_fd_, TCSANOW, &terminal);
631 });
632
633 // Habilitar el modo de entrada de terminal en crudo
634 terminal.c_iflag &= ~IGNBRK; // Deshabilitar la ignorancia de la condición de interrupción
635 terminal.c_iflag &= ~BRKINT; // Deshabilitar que la interrupción cause el
636 // vaciado de entrada y salida
637 terminal.c_iflag &= ~PARMRK; // Deshabilitar la marcación de errores de paridad.
638 terminal.c_iflag &= ~ISTRIP; // Deshabilitar el recorte del octavo bit de los caracteres.
639 terminal.c_iflag &= ~INLCR; // Deshabilitar el mapeo de NL a CR.
640 terminal.c_iflag &= ~IGNCR; // Deshabilitar la ignorancia de CR.
641 terminal.c_iflag &= ~ICRNL; // Deshabilitar el mapeo de CR a NL.
642 terminal.c_iflag &= ~IXON; // Deshabilitar el control de flujo XON/XOFF en la salida
643
644 terminal.c_lflag &= ~ECHO; // Deshabilitar el eco de los caracteres de entrada.
645 terminal.c_lflag &= ~ECHONL; // Deshabilitar el eco de los caracteres de nueva línea.
646 terminal.c_lflag &= ~ICANON; // Deshabilitar el modo canónico.
647 terminal.c_lflag &= ~ISIG; // Deshabilitar el envío de señal al pulsar:
648 // - => DSUSP
649 // - C-Z => SUSP
650 // - C-C => INTR
651 // - C-d => QUIT
652 terminal.c_lflag &= ~IEXTEN; // Deshabilitar el procesamiento de entrada extendido
653 terminal.c_cflag |= CS8; // 8 bits por byte
654
655 terminal.c_cc[VMIN] = 0; // Número mínimo de caracteres para lectura no canónica.
656 terminal.c_cc[VTIME] = 0; // Tiempo de espera en deciseGUNDOS para lectura no canónica.
657
658 tcsetattr(tty_fd_, TCSANOW, &terminal);
659
660#endif
661
662 auto enable = [&](const std::vector<DECMode>& parameters) {
663 TerminalSend(Set(parameters));
664 on_exit_functions.emplace(
665 [this, parameters] { TerminalSend(Reset(parameters)); });
666 };
667
668 auto disable = [&](const std::vector<DECMode>& parameters) {
669 TerminalSend(Reset(parameters));
670 on_exit_functions.emplace(
671 [this, parameters] { TerminalSend(Set(parameters)); });
672 };
673
674 if (use_alternative_screen_) {
675 enable({
676 DECMode::kAlternateScreen,
677 });
678 }
679
680 disable({
681 DECMode::kLineWrap,
682 });
683
684 if (track_mouse_) {
685 enable({DECMode::kMouseVt200});
686 enable({DECMode::kMouseAnyEvent});
687 enable({DECMode::kMouseUrxvtMode});
688 enable({DECMode::kMouseSgrExtMode});
689 }
690
691 // Después de instalar la nueva configuración, vaciarla al terminal para
692 // asegurar que se aplique completamente:
693 TerminalFlush();
694
695 InstallTerminalInfo();
696
697 quit_ = false;
698
699 PostAnimationTask();
700
701 installed_ = true;
702 g_terminal_is_raw = true;
703}
704
705void App::Internal::Uninstall() {
706 g_terminal_is_raw = false;
707 installed_ = false;
708
709 // Durante el apagado, esperar todas las respuestas.
710 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
711 auto closing_receiver =
712 event_buffer.CreateReceiverAt(main_loop_receiver->index());
713 auto start = std::chrono::steady_clock::now();
714 while (cursor_position_request.HasPending()) {
715 FetchTerminalEvents();
716
717 while (closing_receiver->Has()) {
718 const auto event = closing_receiver->Pop();
719 if (event.is_cursor_position()) {
720 cursor_x_ = event.cursor_x();
721 cursor_y_ = event.cursor_y();
722 cursor_position_request.OnReply();
723 }
724 }
725
726 task_runner.RunUntilIdle();
727
728 if (std::chrono::steady_clock::now() - start >
729 std::chrono::milliseconds(400)) {
730 break;
731 }
732 std::this_thread::sleep_for(std::chrono::milliseconds(10));
733 }
734 }
735
736 OnExit();
737}
738
739void App::Internal::PreMain() {
740 // Suspender la pantalla previamente activa:
741 if (g_active_screen) {
742 std::swap(suspended_screen_, g_active_screen);
743 // Restablecer la posición del cursor a la parte superior de la pantalla y borrar la pantalla.
744 suspended_screen_->internal_->TerminalSend(
745 suspended_screen_->internal_->ResetCursorPosition());
746 suspended_screen_->ResetPosition(
747 suspended_screen_->internal_->output_buffer,
748 /*clear=*/true);
749 suspended_screen_->dimx_ = 0;
750 suspended_screen_->dimy_ = 0;
751
752 // Restablecer las dimensiones para forzar el dibujo de la pantalla de nuevo la próxima vez:
753 suspended_screen_->internal_->Uninstall();
754 }
755
756 // Esta pantalla está ahora activa:
757 g_active_screen = public_;
758 g_active_screen->internal_->Install();
759
760 previous_animation_time_ = animation::Clock::now();
761}
762
763void App::Internal::PostMain() {
764 // Colocar la posición del cursor al final del dibujo.
765 TerminalSend(ResetCursorPosition());
766
767 g_active_screen = nullptr;
768
769 // Restaurar pantalla suspendida.
770 if (suspended_screen_) {
771 // Borrar pantalla y colocar el cursor al principio del dibujo.
772 public_->ResetPosition(output_buffer, /*clear=*/true);
773 public_->dimx_ = 0;
774 public_->dimy_ = 0;
775 Uninstall();
776 std::swap(g_active_screen, suspended_screen_);
777 g_active_screen->internal_->Install();
778 } else {
779 Uninstall();
780
781 std::cout << "\r";
782 // Al salir finalmente, mantener el dibujo actual y restablecer la posición del cursor una
783 // línea después.
784 if (!use_alternative_screen_) {
785 std::cout << "\n";
786 }
787 std::cout << std::flush;
788 }
789
790 int sig = g_last_signal.exchange(0);
791 if (sig != 0) {
792 RestoreSignalHandlerAndRaise(sig);
793 }
794}
795
796bool App::Internal::HasQuitted() {
797 return quit_;
798}
799
800void App::Internal::RunOnce(const Component& component) {
801 const AutoReset set_component(&component_, component);
802 ExecuteSignalHandlers();
803 FetchTerminalEvents();
804
805 while (!quit_ && main_loop_receiver->Has()) {
806 public_->Post(main_loop_receiver->Pop());
807 }
808
809 // Ejecutar las tareas pendientes de la cola.
810 const size_t executed_task = task_runner.ExecutedTasks();
811 task_runner.RunUntilIdle();
812 // Si no se ejecutó ninguna tarea, podemos regresar temprano sin volver a dibujar la pantalla.
813 if (executed_task == task_runner.ExecutedTasks()) {
814 return;
815 }
816
817 ExecuteSignalHandlers();
818 Draw(component);
819
820 if (selection_data_previous_ != selection_data_) {
821 selection_data_previous_ = selection_data_;
822 if (selection_on_change_) {
823 selection_on_change_();
824 public_->Post(Event::Custom);
825 }
826 }
827}
828
829void App::Internal::RunOnceBlocking(Component component) {
830 // Establecer FPS a 60 como máximo.
831 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
832
833 auto time = std::chrono::steady_clock::now();
834 const size_t executed_task = task_runner.ExecutedTasks();
835
836 // Esperar a que se ejecute al menos una tarea.
837 while (executed_task == task_runner.ExecutedTasks() && !HasQuitted()) {
838 RunOnce(component);
839
840 const auto now = std::chrono::steady_clock::now();
841 const auto delta = now - time;
842 time = now;
843
844 if (delta < time_per_frame) {
845 const auto sleep_duration = time_per_frame - delta;
846 std::this_thread::sleep_for(sleep_duration);
847 }
848 }
849}
850
851void App::Internal::HandleTask(Component component, Task& task) {
852 std::visit(
853 [&](auto&& arg) {
854 using T = std::decay_t<decltype(arg)>;
855 // clang-format off
856
857 // Manejar Evento.
858 if constexpr (std::is_same_v<T, Event>) {
859
860 if (arg.is_cursor_position()) {
861 cursor_x_ = arg.cursor_x();
862 cursor_y_ = arg.cursor_y();
863 cursor_position_request.OnReply();
864 return;
865 }
866
867 if (arg.is_cursor_shape()) {
868 cursor_reset_shape_ = arg.cursor_shape();
869 return;
870 }
871
872 if (arg.IsTerminalCapabilities()) {
873 terminal_capabilities_ = arg.TerminalCapabilities();
874 return;
875 }
876
877 if (arg.IsTerminalNameVersion()) {
878 terminal_name_ = arg.TerminalName();
879 terminal_version_ = arg.TerminalVersion();
880 return;
881 }
882
883 if (arg.IsTerminalEmulator()) {
884 terminal_emulator_name_ = arg.TerminalEmulatorName();
885 terminal_emulator_version_ = arg.TerminalEmulatorVersion();
886 return;
887 }
888
889 if (arg.is_mouse()) {
890 arg.mouse().x -= cursor_x_;
891 arg.mouse().y -= cursor_y_;
892 }
893
894 arg.screen_ = public_;
895
896 bool handled = component->OnEvent(arg);
897 handled = HandleSelection(handled, arg);
898
899 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
900 RecordSignal(SIGINT);
901 }
902
903#if !defined(_WIN32)
904 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
905 RecordSignal(SIGTSTP);
906 }
907#endif
908
909 frame_valid_ = false;
910 return;
911 }
912
913 // Manejar callback
914 if constexpr (std::is_same_v<T, Closure>) {
915 arg();
916 return;
917 }
918
919 // Manejar Animación
920 if constexpr (std::is_same_v<T, AnimationTask>) {
921 if (!animation_requested_) {
922 return;
923 }
924
925 animation_requested_ = false;
926 const animation::TimePoint now = animation::Clock::now();
927 const animation::Duration delta = now - previous_animation_time_;
928 previous_animation_time_ = now;
929
930 animation::Params params(delta);
931 component->OnAnimation(params);
932 frame_valid_ = false;
933 return;
934 }
935 },
936 task);
937 // clang-format on
938}
939
940bool App::Internal::HandleSelection(bool handled, Event event) {
941 if (handled) {
942 selection_pending_ = nullptr;
943 selection_data_.empty = true;
944 selection_ = nullptr;
945 return true;
946 }
947
948 if (!event.is_mouse()) {
949 return false;
950 }
951
952 auto& mouse = event.mouse();
953 if (mouse.button != Mouse::Left) {
954 return false;
955 }
956
957 if (mouse.motion == Mouse::Pressed) {
958 selection_pending_ = public_->CaptureMouse();
959 selection_data_.start_x = mouse.x;
960 selection_data_.start_y = mouse.y;
961 selection_data_.end_x = mouse.x;
962 selection_data_.end_y = mouse.y;
963 return false;
964 }
965
966 if (!selection_pending_) {
967 return false;
968 }
969
970 if (mouse.motion == Mouse::Moved) {
971 if ((mouse.x != selection_data_.end_x) ||
972 (mouse.y != selection_data_.end_y)) {
973 selection_data_.end_x = mouse.x;
974 selection_data_.end_y = mouse.y;
975 selection_data_.empty = false;
976 }
977
978 return true;
979 }
980
981 if (mouse.motion == Mouse::Released) {
982 selection_pending_ = nullptr;
983 selection_data_.end_x = mouse.x;
984 selection_data_.end_y = mouse.y;
985 selection_data_.empty = false;
986 return true;
987 }
988
989 return false;
990}
991
992void App::Internal::Draw(Component component) {
993 if (frame_valid_) {
994 return;
995 }
996 auto document = component->Render();
997 int dimx = 0;
998 int dimy = 0;
999 auto terminal = Terminal::Size();
1000 document->ComputeRequirement();
1001 switch (dimension_) {
1003 dimx = public_->dimx_;
1004 dimy = public_->dimy_;
1005 break;
1007 dimx = terminal.dimx;
1008 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1009 break;
1011 dimx = terminal.dimx;
1012 dimy = terminal.dimy;
1013 break;
1015 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimx);
1016 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
1017 break;
1018 }
1019
1020 // Ocultar el cursor para evitar parpadeos durante el reinicio.
1021 TerminalSend("\033[?25l");
1022
1023 const bool resized =
1024 frame_count_ == 0 || (dimx != public_->dimx_) || (dimy != public_->dimy_);
1025 TerminalSend(ResetCursorPosition());
1026
1027 if (frame_count_ != 0) {
1028 // Restablecer la posición del cursor a la esquina inferior izquierda para comenzar a dibujar el
1029 // nuevo fotograma.
1030 public_->ResetPosition(output_buffer, resized);
1031
1032 // Si el ancho del terminal disminuye, el emulador de terminal comenzará a ajustar
1033 // las líneas y ensuciará la pantalla. Debemos limpiarla completamente.
1034 if ((dimx < public_->dimx_) && !use_alternative_screen_) {
1035 TerminalSend("\033[J"); // limpiar la salida del terminal
1036 TerminalSend("\033[H"); // mover el cursor a la posición inicial
1037 }
1038 }
1039
1040 // Redimensionar la pantalla si es necesario.
1041 if (resized) {
1042 public_->dimx_ = dimx;
1043 public_->dimy_ = dimy;
1044 public_->cells_ = std::vector<Cell>(static_cast<size_t>(dimx) *
1045 static_cast<size_t>(dimy));
1046 Cursor cursor = public_->cursor_;
1047 cursor.x = dimx - 1;
1048 cursor.y = dimy - 1;
1049 public_->SetCursor(cursor);
1050 }
1051
1052 // Solicitar periódicamente al emulador de terminal la posición del marco en relación con
1053 // la pantalla. Esto es útil para convertir la posición del ratón informada en
1054 // coordenadas de la pantalla a coordenadas del marco.
1055 if (!use_alternative_screen_ && is_stdout_a_tty_) {
1056 RequestCursorPosition(previous_frame_resized_);
1057 }
1058 previous_frame_resized_ = resized;
1059
1060 selection_ = selection_data_.empty
1061 ? std::make_unique<Selection>()
1062 : std::make_unique<Selection>(
1063 selection_data_.start_x, selection_data_.start_y, //
1064 selection_data_.end_x, selection_data_.end_y);
1065 Render(*public_, document.get(), *selection_);
1066
1067 // Establecer la posición del cursor para el usuario que utiliza herramientas para insertar caracteres CJK.
1068 {
1069 const int dx = public_->dimx_ - 1 - public_->cursor_.x +
1070 int(public_->dimx_ != terminal.dimx);
1071 const int dy = public_->dimy_ - 1 - public_->cursor_.y;
1072
1073 set_cursor_position_.clear();
1074 reset_cursor_position_.clear();
1075
1076 if (dy != 0) {
1077 set_cursor_position_ += "\x1B[" + std::to_string(dy) + "A";
1078 reset_cursor_position_ += "\x1B[" + std::to_string(dy) + "B";
1079 }
1080
1081 if (dx != 0) {
1082 set_cursor_position_ += "\x1B[" + std::to_string(dx) + "D";
1083 reset_cursor_position_ += "\x1B[" + std::to_string(dx) + "C";
1084 }
1085
1086 if (public_->cursor_.shape != Screen::Cursor::Hidden) {
1087 set_cursor_position_ += "\033[?25h";
1088 set_cursor_position_ +=
1089 "\033[" + std::to_string(int(public_->cursor_.shape)) + " q";
1090 }
1091 }
1092
1093 public_->ToString(output_buffer);
1094 TerminalSend(set_cursor_position_);
1095 TerminalFlush();
1096
1097 public_->Clear();
1098 frame_valid_ = true;
1099 frame_count_++;
1100}
1101
1102std::string App::Internal::ResetCursorPosition() {
1103 std::string result = std::move(reset_cursor_position_);
1104 reset_cursor_position_ = "";
1105 return result;
1106}
1107
1108void App::Internal::RequestCursorPosition(bool force) {
1109 cursor_position_request.Request(force);
1110}
1111
1112void App::Internal::TerminalSend(std::string_view s) {
1113 output_buffer += s;
1114}
1115
1116void App::Internal::TerminalFlush() {
1117 // Emscripten no implementa flush. Interpretamos cero como flush.
1118 output_buffer += '\0';
1119 std::cout << output_buffer << std::flush;
1120 output_buffer.clear();
1121}
1122
1123void App::Internal::InstallPipedInputHandling() {
1124 is_stdin_a_tty_ = false;
1125 is_stdout_a_tty_ = false;
1126#if defined(__EMSCRIPTEN__)
1127 is_stdin_a_tty_ = true;
1128 is_stdout_a_tty_ = true;
1129#elif defined(_WIN32)
1130 is_stdin_a_tty_ = _isatty(_fileno(stdin));
1131 is_stdout_a_tty_ = _isatty(_fileno(stdout));
1132#else
1133 tty_fd_ = STDIN_FILENO;
1134 is_stdout_a_tty_ = isatty(STDOUT_FILENO);
1135 // Manejar la redirección de entrada canalizada si la aplicación la habilita explícitamente.
1136 // Esto permite que las aplicaciones lean datos de stdin mientras siguen recibiendo
1137 // entrada de teclado del terminal para uso interactivo.
1138 if (!handle_piped_input_) {
1139 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1140 } else if (isatty(STDIN_FILENO)) {
1141 is_stdin_a_tty_ = true;
1142 } else {
1143 // Abrir /dev/tty para la entrada del teclado.
1144 tty_fd_ = open("/dev/tty", O_RDONLY); // NOLINT
1145 if (tty_fd_ < 0) {
1146 // Falló al abrir /dev/tty (contenedores, sistemas sin cabeza, etc.)
1147 tty_fd_ = STDIN_FILENO; // Volver a stdin.
1148 is_stdin_a_tty_ = isatty(STDIN_FILENO);
1149 } else {
1150 is_stdin_a_tty_ = true;
1151 // Cerrar el descriptor de archivo /dev/tty al salir.
1152 on_exit_functions.emplace([this] {
1153 close(tty_fd_);
1154 tty_fd_ = -1;
1155 });
1156 }
1157 }
1158#endif
1159}
1160
1161void App::Internal::InstallTerminalInfo() {
1162 // Solicitar al terminal que informe la forma actual del cursor. Lo restauraremos
1163 // al salir.
1164 if (is_stdout_a_tty_) {
1165 TerminalSend(DECRQSS_DECSCUSR);
1166 TerminalSend("\033[>q"); // XTVERSION
1167 TerminalSend("\033[>c"); // DA2
1168 TerminalSend("\033[c"); // DA1
1169 TerminalFlush();
1170 }
1171
1172 // Esperar la respuesta de forma del cursor usando la cabecera de configuración.
1173 if (is_stdin_a_tty_ && is_stdout_a_tty_) {
1174 // Un receptor con alcance limitado a la configuración: mantener uno vivo después de la configuración fijaría
1175 // cada evento subsecuente en el búfer, haciéndolo crecer durante toda la vida
1176 // de la aplicación.
1177 auto setup_receiver = event_buffer.CreateReceiver();
1178 auto start = std::chrono::steady_clock::now();
1179 bool terminal_capabilities_received = false;
1180 // Esperar la respuesta de forma del cursor usando la cabecera de configuración.
1181 while (true) {
1182 FetchTerminalEvents();
1183 while (setup_receiver->Has()) {
1184 const auto event = setup_receiver->Pop();
1185 if (event.is_cursor_shape()) {
1186 cursor_reset_shape_ = event.cursor_shape();
1187 }
1188
1189 if (event.IsTerminalCapabilities()) {
1190 terminal_capabilities_ = event.TerminalCapabilities();
1191 terminal_capabilities_received = true;
1192 }
1193
1194 if (event.IsTerminalNameVersion()) {
1195 terminal_name_ = event.TerminalName();
1196 terminal_version_ = event.TerminalVersion();
1197 }
1198
1199 if (event.IsTerminalEmulator()) {
1200 terminal_emulator_name_ = event.TerminalEmulatorName();
1201 terminal_emulator_version_ = event.TerminalEmulatorVersion();
1202 }
1203 }
1204
1205 // Se espera que las respuestas se reciban en orden, así que podemos salir cuando
1206 // se reciba la última (XTVERSION). También establecemos un tiempo de espera para evitar
1207 // esperar para siempre en caso de que el terminal no admita estas consultas.
1208 if (terminal_capabilities_received) {
1209 break;
1210 }
1211
1212 if (std::chrono::steady_clock::now() - start >
1213 std::chrono::milliseconds(500)) {
1214 break;
1215 }
1216 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1217 }
1218 }
1219
1220 // Establecer peculiaridades y soporte de color según la identificación del terminal.
1221 Terminal::Quirks quirks = Terminal::GetQuirks();
1222
1223 auto color_support = Terminal::ComputeColorSupport(
1224 util::GetEnv("TERM"), util::GetEnv("COLORTERM"),
1225 util::GetEnv("TERM_PROGRAM"), terminal_name_, terminal_emulator_name_,
1226 terminal_capabilities_);
1227
1228 quirks.SetColorSupport(color_support);
1229
1230 const bool is_modern_emulator = (terminal_emulator_name_ != "unknown");
1231 const bool is_vt220_plus =
1232 (terminal_name_ != "vt100" && terminal_name_ != "unknown");
1233 bool reports_utf8 = false;
1234 for (const int x : terminal_capabilities_) {
1235 if (x == 52) {
1236 reports_utf8 = true;
1237 break;
1238 }
1239 }
1240
1241 // Heurística: Si el emulador de terminal es moderno, o reporta soporte de
1242 // UTF-8 o color, podemos asumir que admite caracteres de bloque y ocultación
1243 // de cursor, que son esenciales para una buena experiencia. Esto es una heurística, pero
1244 // nos permite evitar algunos emuladores de terminal antiguos que no
1245 // admiten estas funciones, mientras seguimos proporcionando una buena experiencia en
1246 // emuladores de terminal modernos que sí las admiten.
1247 bool modern = is_modern_emulator || is_vt220_plus || reports_utf8;
1248 if (modern) {
1249 quirks.SetBlockCharacters(true);
1250 quirks.SetCursorHiding(true);
1251 quirks.SetComponentAscii(false);
1252 }
1253
1254 Terminal::SetQuirks(quirks);
1255
1256 on_exit_functions.emplace([this] {
1257 TerminalSend("\033[?25h"); // Habilitar cursor.
1258 if (is_stdout_a_tty_) {
1259 TerminalSend("\033[" + std::to_string(cursor_reset_shape_) + " q");
1260 }
1261 });
1262}
1263
1264void App::Internal::Signal(int signal) {
1265 if (signal == SIGABRT) {
1266 public_->Exit();
1267 return;
1268 }
1269
1270// Windows no soporta SIGTSTP / SIGWINCH
1271#if !defined(_WIN32)
1272 if (signal == SIGTSTP) {
1273 public_->Post([&] {
1274 TerminalSend(ResetCursorPosition());
1275 public_->ResetPosition(output_buffer, /*clear*/ true);
1276 Uninstall();
1277 public_->dimx_ = 0;
1278 public_->dimy_ = 0;
1279 (void)std::raise(SIGTSTP);
1280 Install();
1281 });
1282 return;
1283 }
1284
1285 if (signal == SIGWINCH) {
1286 public_->Post(Event::Special({0}));
1287 return;
1288 }
1289#endif
1290}
1291
1292size_t App::Internal::FetchTerminalEvents() {
1293#if defined(_WIN32)
1294 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1295 // Comprobar si hay entrada en la consola.
1296 auto console = GetStdHandle(STD_INPUT_HANDLE);
1297 DWORD number_of_events = 0;
1298 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1299 return std::vector<INPUT_RECORD>();
1300 }
1301 if (number_of_events <= 0) {
1302 // No hay entrada, regresar.
1303 return std::vector<INPUT_RECORD>();
1304 }
1305 // Leer los eventos de entrada.
1306 std::vector<INPUT_RECORD> records(number_of_events);
1307 DWORD number_of_events_read = 0;
1308 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1309 &number_of_events_read)) {
1310 return std::vector<INPUT_RECORD>();
1311 }
1312 records.resize(number_of_events_read);
1313 return records;
1314 };
1315
1316 auto records = get_input_records();
1317 if (records.size() == 0) {
1318 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1319 const size_t timeout_microseconds =
1320 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1321 terminal_input_parser.Timeout(timeout_microseconds);
1322 return 0;
1323 }
1324 last_char_time = std::chrono::steady_clock::now();
1325
1326 // Convertir los eventos de entrada a eventos FTXUI.
1327 // Para cada evento, llamamos al analizador de entrada del terminal para convertirlo a
1328 // Evento.
1329 std::wstring wstring;
1330 for (const auto& r : records) {
1331 switch (r.EventType) {
1332 case KEY_EVENT: {
1333 auto key_event = r.Event.KeyEvent;
1334 // ignorar eventos de tecla UP
1335 if (key_event.bKeyDown == FALSE) {
1336 continue;
1337 }
1338 const wchar_t wc = key_event.uChar.UnicodeChar;
1339 wstring += wc;
1340 if (wc >= 0xd800 && wc <= 0xdbff) {
1341 // Esperar a que llegue el Low Surrogate en el siguiente registro.
1342 continue;
1343 }
1344 for (auto it : to_string(wstring)) {
1345 terminal_input_parser.Add(it);
1346 }
1347 wstring.clear();
1348 } break;
1349 case WINDOW_BUFFER_SIZE_EVENT:
1350 public_->Post(Event::Special({0}));
1351 break;
1352 case MENU_EVENT:
1353 case FOCUS_EVENT:
1354 case MOUSE_EVENT:
1355 // TODO(mauve): Implementar más tarde.
1356 break;
1357 }
1358 }
1359 return records.size();
1360#elif defined(__EMSCRIPTEN__)
1361 // Leer caracteres del terminal.
1362 // Lo configuramos para que no sea bloqueante.
1363 std::array<char, 4096> out{};
1364 const ssize_t l = read(STDIN_FILENO, out.data(), out.size());
1365 if (l <= 0) {
1366 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1367 const size_t timeout_microseconds =
1368 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1369 terminal_input_parser.Timeout(timeout_microseconds);
1370 return 0;
1371 }
1372 last_char_time = std::chrono::steady_clock::now();
1373
1374 // Convertir los caracteres a eventos.
1375 for (ssize_t i = 0; i < l; ++i) {
1376 terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1377 }
1378 return (size_t)l;
1379#else // POSIX (Linux y Mac)
1380 struct pollfd pfd = {tty_fd_, POLLIN, 0};
1381 const int poll_result = poll(&pfd, 1, 0);
1382 if (poll_result <= 0) {
1383 const auto timeout = std::chrono::steady_clock::now() - last_char_time;
1384 const size_t timeout_ms =
1385 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1386 terminal_input_parser.Timeout(static_cast<int>(timeout_ms));
1387 return 0;
1388 }
1389 last_char_time = std::chrono::steady_clock::now();
1390
1391 // Drenar la entrada disponible, para que las ráfagas (p. ej. desplazamiento rápido
1392 // de la rueda del ratón) no se acumulen entre fotogramas. El total está limitado para mantener
1393 // la respuesta del fotograma bajo una inundación continua de entrada. Ver #1348.
1394 constexpr size_t kMaxBytesPerFetch = 64 * 1024;
1395 std::array<char, 4096> out{};
1396 size_t total = 0;
1397 while (total < kMaxBytesPerFetch) {
1398 const ssize_t l = read(tty_fd_, out.data(), out.size());
1399 if (l <= 0) {
1400 break;
1401 }
1402
1403 // Convertir los caracteres a eventos.
1404 for (ssize_t i = 0; i < l; ++i) {
1405 terminal_input_parser.Add(out.at(static_cast<size_t>(i)));
1406 }
1407 total += static_cast<size_t>(l);
1408
1409 pfd.revents = 0;
1410 if (poll(&pfd, 1, 0) <= 0) {
1411 break;
1412 }
1413 }
1414 return total;
1415#endif
1416}
1417
1418void App::Internal::PostAnimationTask() {
1419 public_->Post(AnimationTask());
1420
1421 // Repetir la tarea de animación cada 15ms. Esto corresponde a una velocidad de fotogramas
1422 // de alrededor de 66fps.
1423 task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1424 std::chrono::milliseconds(15));
1425}
1426
1427App::App(std::unique_ptr<Internal> internal, int dimx, int dimy)
1428 : Screen(dimx, dimy), internal_(std::move(internal)) {
1429 internal_->public_ = this;
1430}
1431
1432App::App(App&& other) noexcept : Screen(std::move(other)) {
1433 internal_ = std::move(other.internal_);
1434 if (internal_) {
1435 internal_->public_ = this;
1436 }
1437}
1438
1439App& App::operator=(App&& other) noexcept {
1440 Screen::operator=(std::move(other));
1441 internal_ = std::move(other.internal_);
1442 if (internal_) {
1443 internal_->public_ = this;
1444 }
1445 return *this;
1446}
1447
1448App::~App() = default;
1449
1450// static
1451App App::FixedSize(int dimx, int dimy) {
1452 auto internal =
1453 std::make_unique<Internal>(nullptr, AppDimension::Fixed, false);
1454 return App(std::move(internal), dimx, dimy);
1455}
1456
1457// static
1458App App::Fullscreen() {
1459 return FullscreenAlternateScreen();
1460}
1461
1462// static
1463App App::FullscreenPrimaryScreen() {
1464 auto terminal = Terminal::Size();
1465 auto internal =
1466 std::make_unique<Internal>(nullptr, AppDimension::Fullscreen, false);
1467 return App(std::move(internal), terminal.dimx, terminal.dimy);
1468}
1469
1470// static
1471App App::FullscreenAlternateScreen() {
1472 auto terminal = Terminal::Size();
1473 auto internal =
1474 std::make_unique<Internal>(nullptr, AppDimension::Fullscreen, true);
1475 return App(std::move(internal), terminal.dimx, terminal.dimy);
1476}
1477
1478// static
1479App App::FitComponent() {
1480 auto terminal = Terminal::Size();
1481 auto internal =
1482 std::make_unique<Internal>(nullptr, AppDimension::FitComponent, false);
1483 return App(std::move(internal), terminal.dimx, terminal.dimy);
1484}
1485
1486// static
1487App App::TerminalOutput() {
1488 auto terminal = Terminal::Size();
1489 auto internal =
1490 std::make_unique<Internal>(nullptr, AppDimension::TerminalOutput, false);
1491 return App(std::move(internal), terminal.dimx, terminal.dimy);
1492}
1493
1494void App::TrackMouse(bool enable) {
1495 internal_->track_mouse_ = enable;
1496}
1497
1498void App::HandlePipedInput(bool enable) {
1499 internal_->handle_piped_input_ = enable;
1500}
1501
1502// static
1503App* App::Active() {
1504 return g_active_screen;
1505}
1506
1507void App::Loop(Component component) {
1508 class Loop loop(this, std::move(component));
1509 loop.Run();
1510}
1511
1512void App::Exit() {
1513 Post([this] { internal_->ExitNow(); });
1514}
1515
1516Closure App::ExitLoopClosure() {
1517 return [this] { Exit(); };
1518}
1519
1520void App::Post(Task task) {
1521 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
1522 if (internal_->component_) {
1523 internal_->HandleTask(internal_->component_, task);
1524 return;
1525 }
1526
1527 // Si no hay ningún componente, aún podemos ejecutar cierres (closures).
1528 if (std::holds_alternative<Closure>(task)) {
1529 std::get<Closure>(task)();
1530 }
1531 });
1532}
1533
1534void App::PostEvent(Event event) {
1535 // PostEvent está documentado como seguro para hilos: pasa por la cola de tareas
1536 // protegida por mutex. event_buffer solo es seguro de usar desde el hilo principal.
1537 Post(Task(std::move(event)));
1538}
1539
1540// static
1541void App::PostEventOrExecute(Closure closure) {
1542 if (!closure) {
1543 return;
1544 }
1545 if (auto* app = App::Active()) {
1546 app->Post(std::move(closure));
1547 } else {
1548 closure();
1549 }
1550}
1551
1552void App::RequestAnimationFrame() {
1553 if (internal_->animation_requested_) {
1554 return;
1555 }
1556 internal_->animation_requested_ = true;
1557 auto now = animation::Clock::now();
1558 const auto time_histeresis = std::chrono::milliseconds(33);
1559 if (now - internal_->previous_animation_time_ >= time_histeresis) {
1560 internal_->previous_animation_time_ = now;
1561 }
1562}
1563
1564CapturedMouse App::CaptureMouse() {
1565 if (internal_->mouse_captured) {
1566 return nullptr;
1567 }
1568 internal_->mouse_captured = true;
1569 return std::make_unique<CapturedMouseImpl>(
1570 [this] { internal_->mouse_captured = false; });
1571}
1572
1573Closure App::WithRestoredIO(Closure fn) {
1574 return [this, fn] {
1575 internal_->Uninstall();
1576 fn();
1577 internal_->Install();
1578 };
1579}
1580
1581void App::ForceHandleCtrlC(bool force) {
1582 internal_->force_handle_ctrl_c_ = force;
1583}
1584
1585void App::ForceHandleCtrlZ(bool force) {
1586 internal_->force_handle_ctrl_z_ = force;
1587}
1588
1589std::string App::GetSelection() {
1590 if (!internal_->selection_) {
1591 return "";
1592 }
1593 return internal_->selection_->GetParts();
1594}
1595
1596void App::SelectionChange(std::function<void()> callback) {
1597 internal_->selection_on_change_ = std::move(callback);
1598}
1599
1600const std::string& App::TerminalName() const {
1601 return internal_->terminal_name_;
1602}
1603
1604int App::TerminalVersion() const {
1605 return internal_->terminal_version_;
1606}
1607
1608const std::string& App::TerminalEmulatorName() const {
1609 return internal_->terminal_emulator_name_;
1610}
1611
1612const std::string& App::TerminalEmulatorVersion() const {
1613 return internal_->terminal_emulator_version_;
1614}
1615
1616const std::vector<int>& App::TerminalCapabilities() const {
1617 return internal_->terminal_capabilities_;
1618}
1619
1620std::vector<std::string> App::TerminalCapabilityNames() const {
1621 return Event::TerminalCapabilities("", internal_->terminal_capabilities_)
1622 .TerminalCapabilityNames();
1623}
1624
1625// El bucle llama a esto:
1626
1627void App::ExitNow() {
1628 internal_->ExitNow();
1629}
1630void App::Install() {
1631 internal_->Install();
1632}
1633void App::Uninstall() {
1634 internal_->Uninstall();
1635}
1636void App::PreMain() {
1637 internal_->PreMain();
1638}
1639void App::PostMain() {
1640 internal_->PostMain();
1641}
1642bool App::HasQuitted() {
1643 return internal_->HasQuitted();
1644}
1645void App::RunOnce(const Component& component) {
1646 internal_->RunOnce(component);
1647}
1648void App::RunOnceBlocking(Component component) {
1649 internal_->RunOnceBlocking(component);
1650}
1651void App::HandleTask(Component component, Task& task) {
1652 internal_->HandleTask(component, task);
1653}
1654bool App::HandleSelection(bool handled, Event event) {
1655 return internal_->HandleSelection(handled, event);
1656}
1657void App::Draw(Component component) {
1658 internal_->Draw(component);
1659}
1660std::string App::ResetCursorPosition() {
1661 return internal_->ResetCursorPosition();
1662}
1663void App::RequestCursorPosition(bool force) {
1664 internal_->RequestCursorPosition(force);
1665}
1666void App::TerminalSend(std::string_view s) {
1667 internal_->TerminalSend(s);
1668}
1669void App::TerminalFlush() {
1670 internal_->TerminalFlush();
1671}
1672void App::InstallPipedInputHandling() {
1673 internal_->InstallPipedInputHandling();
1674}
1675void App::InstallTerminalInfo() {
1676 internal_->InstallTerminalInfo();
1677}
1678void App::Signal(int signal) {
1679 internal_->Signal(signal);
1680}
1681size_t App::FetchTerminalEvents() {
1682 return internal_->FetchTerminalEvents();
1683}
1684void App::PostAnimationTask() {
1685 internal_->PostAnimationTask();
1686}
1687
1688Loop::Loop(App* screen, Component component)
1689 : screen_(screen), component_(std::move(component)) {
1690 screen_->PreMain();
1691}
1692
1693Loop::~Loop() {
1694 screen_->PostMain();
1695}
1696
1697bool Loop::HasQuitted() {
1698 return screen_->HasQuitted();
1699}
1700
1701void Loop::RunOnce() {
1702 screen_->RunOnce(component_);
1703}
1704
1705void Loop::RunOnceBlocking() {
1706 screen_->RunOnceBlocking(component_);
1707}
1708
1709void Loop::Run() {
1710 while (!HasQuitted()) {
1711 RunOnceBlocking();
1712 }
1713}
1714
1715} // namespace ftxui
Quirks GetQuirks()
Obtiene las peculiaridades de la terminal.
Definition terminal.cpp:355
Dimensions Size()
Obtiene el tamaño del terminal.
Definition terminal.cpp:303
void SetQuirks(const Quirks &quirks)
Anula las peculiaridades de la terminal.
Definition terminal.cpp:365
El espacio de nombres ftxui::animation:: de FTXUI.
void SetFallbackSize(const Dimensions &fallbackSize)
Anula el tamaño del terminal en caso de que la autodetección falle.
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)
Calcula el soporte de color según variables de entorno e identificación de la terminal.
Definition terminal.cpp:196
std::chrono::duration< float > Duration
Definition animation.hpp:30
std::chrono::time_point< Clock > TimePoint
Definition animation.hpp:29
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
El espacio de nombres ftxui:: de FTXUI.
Definition animation.hpp:11
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(std::wstring_view s)
Definition string.cpp:576
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