FTXUI 6.1.9
C++ functional terminal UI.
Loading...
Searching...
No Matches
screen_interactive.cpp
Go to the documentation of this file.
1// Copyright 2020 Arthur Sonzogni. Todos los derechos reservados.
2// El uso de este código fuente se rige por la licencia MIT que se puede encontrar en
3// el archivo LICENSE.
5#include <algorithm> // for copy, max, min
6#include <array> // for array
7#include <atomic>
8#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
9#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
10#include <cstdint>
11#include <cstdio> // for fileno, stdin
12#include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
13#include <ftxui/screen/screen.hpp> // for Pixel, Screen::Cursor, Screen, Screen::Cursor::Hidden
14#include <functional> // for function
15#include <initializer_list> // for initializer_list
16#include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
17#include <memory>
18#include <stack> // for stack
19#include <string>
20#include <thread> // for thread, sleep_for
21#include <tuple> // for _Swallow_assign, ignore
22#include <utility> // for move, swap
23#include <variant> // for visit, variant
24#include <vector> // for vector
25#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
26#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
27#include "ftxui/component/component_base.hpp" // for ComponentBase
28#include "ftxui/component/event.hpp" // for Event
29#include "ftxui/component/loop.hpp" // for Loop
31#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
32#include "ftxui/dom/node.hpp" // for Node, Render
33#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
34#include "ftxui/screen/util.hpp" // for util::clamp
35#include "ftxui/util/autoreset.hpp" // for AutoReset
36
37#if defined(_WIN32)
38#define DEFINE_CONSOLEV2_PROPERTIES
39#define WIN32_LEAN_AND_MEAN
40#ifndef NOMINMAX
41#define NOMINMAX
42#endif
43#include <windows.h>
44#ifndef UNICODE
45#error Must be compiled in UNICODE mode
46#endif
47#else
48#include <fcntl.h>
49#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
50#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
51#include <unistd.h> // for STDIN_FILENO, read
52#include <cerrno>
53#endif
54
55// Quick exit is missing in standard CLang headers
56#if defined(__clang__) && defined(__APPLE__)
57#define quick_exit(a) exit(a)
58#endif
59
60namespace ftxui {
61
62struct ScreenInteractive::Internal {
63 // Convierte char a Event.
64 TerminalInputParser terminal_input_parser;
65
66 task::TaskRunner task_runner;
67
68 // La última vez que se recibió un carácter.
69 std::chrono::time_point<std::chrono::steady_clock> last_char_time =
70 std::chrono::steady_clock::now();
71
72 explicit Internal(std::function<void(Event)> out)
73 : terminal_input_parser(std::move(out)) {}
74};
75
76namespace animation {
79 if (screen) {
80 screen->RequestAnimationFrame();
81 }
82}
83} // namespace animation
84
85namespace {
86
87ScreenInteractive* g_active_screen = nullptr; // NOLINT
88
89void Flush() {
90 // Emscripten no implementa flush. Interpretamos cero como flush.
91 std::cout << '\0' << std::flush;
92}
93
94constexpr int timeout_milliseconds = 20;
95[[maybe_unused]] constexpr int timeout_microseconds =
96 timeout_milliseconds * 1000;
97#if defined(_WIN32)
98
99#elif defined(__EMSCRIPTEN__)
100#include <emscripten.h>
101
102extern "C" {
103EMSCRIPTEN_KEEPALIVE
104void ftxui_on_resize(int columns, int rows) {
106 columns,
107 rows,
108 });
109 std::raise(SIGWINCH);
110}
111}
112
113#else // POSIX (Linux & Mac)
114
115int CheckStdinReady(int fd) {
116 timeval tv = {0, 0}; // NOLINT
117 fd_set fds;
118 FD_ZERO(&fds); // NOLINT
119 FD_SET(fd, &fds); // NOLINT
120 select(fd + 1, &fds, nullptr, nullptr, &tv); // NOLINT
121 return FD_ISSET(fd, &fds); // NOLINT
122}
123
124#endif
125
126std::stack<Closure> on_exit_functions; // NOLINT
127void OnExit() {
128 while (!on_exit_functions.empty()) {
129 on_exit_functions.top()();
130 on_exit_functions.pop();
131 }
132}
133
134std::atomic<int> g_signal_exit_count = 0; // NOLINT
135#if !defined(_WIN32)
136std::atomic<int> g_signal_stop_count = 0; // NOLINT
137std::atomic<int> g_signal_resize_count = 0; // NOLINT
138#endif
139
140// Función asíncrona segura para señales
141void RecordSignal(int signal) {
142 switch (signal) {
143 case SIGABRT:
144 case SIGFPE:
145 case SIGILL:
146 case SIGINT:
147 case SIGSEGV:
148 case SIGTERM:
149 g_signal_exit_count++;
150 break;
151
152#if !defined(_WIN32)
153 case SIGTSTP: // NOLINT
154 g_signal_stop_count++;
155 break;
156
157 case SIGWINCH: // NOLINT
158 g_signal_resize_count++;
159 break;
160#endif
161
162 default:
163 break;
164 }
165}
166
167void ExecuteSignalHandlers() {
168 int signal_exit_count = g_signal_exit_count.exchange(0);
169 while (signal_exit_count--) {
170 ScreenInteractive::Private::Signal(*g_active_screen, SIGABRT);
171 }
172
173#if !defined(_WIN32)
174 int signal_stop_count = g_signal_stop_count.exchange(0);
175 while (signal_stop_count--) {
176 ScreenInteractive::Private::Signal(*g_active_screen, SIGTSTP);
177 }
178
179 int signal_resize_count = g_signal_resize_count.exchange(0);
180 while (signal_resize_count--) {
181 ScreenInteractive::Private::Signal(*g_active_screen, SIGWINCH);
182 }
183#endif
184}
185
186void InstallSignalHandler(int sig) {
187 auto old_signal_handler = std::signal(sig, RecordSignal);
188 on_exit_functions.emplace(
189 [=] { std::ignore = std::signal(sig, old_signal_handler); });
190}
191
192// CSI: Introductor de Secuencia de Control
193 //
194// DCS: Cadena de Control de Dispositivo
195const std::string ST = "\x1b\\"; // NOLINT
196
197// DECRQSS: Solicitud de Cadena de Estado
198// DECSCUSR: Establecer Estilo de Cursor
199
200// DEC: Corporación de Equipos Digitales
201enum class DECMode : std::uint16_t {
202 kLineWrap = 7,
203 kCursor = 25,
204
205 kMouseX10 = 9,
206 kMouseVt200 = 1000,
207 kMouseVt200Highlight = 1001,
208
209 kMouseBtnEventMouse = 1002,
210 kMouseAnyEvent = 1003,
211
212 kMouseUtf8 = 1005,
213 kMouseSgrExtMode = 1006,
214 kMouseUrxvtMode = 1015,
215 kMouseSgrPixelsMode = 1016,
216 kAlternateScreen = 1049,
217};
218
219// Informe de estado del dispositivo (DSR) {
220enum class DSRMode : std::uint8_t {
221 kCursor = 6,
222};
223
224std::string Serialize(const std::vector<DECMode>& parameters) {
225 bool first = true;
226 std::string out;
227 for (const DECMode parameter : parameters) {
228 if (!first) {
229 out += ";";
230 }
231 out += std::to_string(int(parameter));
232 first = false;
233 }
234 return out;
235}
236
237// Establecer Modo Privado DEC (DECSET)
238std::string Set(const std::vector<DECMode>& parameters) {
239 return CSI + "?" + Serialize(parameters) + "h";
240}
241
242// Restablecer Modo Privado DEC (DECRST)
243std::string Reset(const std::vector<DECMode>& parameters) {
244 return CSI + "?" + Serialize(parameters) + "l";
245}
246
247// Informe de Estado del Dispositivo (DSR)
248std::string DeviceStatusReport(DSRMode ps) {
249 return CSI + std::to_string(int(ps)) + "n";
250}
251
252class CapturedMouseImpl : public CapturedMouseInterface {
253 public:
254 explicit CapturedMouseImpl(std::function<void(void)> callback)
255 : callback_(std::move(callback)) {}
256 ~CapturedMouseImpl() override { callback_(); }
257 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
258 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
259 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
260 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
261
262 private:
263 std::function<void(void)> callback_;
264};
265
266} // namespace
267
268ScreenInteractive::ScreenInteractive(Dimension dimension,
269 int dimx,
270 int dimy,
271 bool use_alternative_screen)
272 : Screen(dimx, dimy),
273 dimension_(dimension),
274 use_alternative_screen_(use_alternative_screen) {
275 internal_ = std::make_unique<Internal>(
276 [&](Event event) { PostEvent(std::move(event)); });
277}
278
279// static
281 return {
282 Dimension::Fixed,
283 dimx,
284 dimy,
285 /*use_alternative_screen=*/false,
286 };
287}
288
289/// Crea una ScreenInteractive que ocupa todo el tamaño del terminal. Esto utiliza el
290/// búfer de pantalla alternativo para evitar interferir con el contenido del terminal.
291/// @note Es lo mismo que `ScreenInteractive::FullscreenAlternateScreen()`
292// static
296
297/// Crea una ScreenInteractive que ocupa todo el tamaño del terminal. Se utiliza el
298/// búfer de pantalla principal. Esto significa que si el terminal se redimensiona, el contenido
299/// anterior podría desordenarse con el contenido del terminal.
300// static
302 auto terminal = Terminal::Size();
303 return {
304 Dimension::Fullscreen,
305 terminal.dimx,
306 terminal.dimy,
307 /*use_alternative_screen=*/false,
308 };
309}
310
311/// Crea una ScreenInteractive que ocupa todo el tamaño del terminal. Esto utiliza el
312/// búfer de pantalla alternativo para evitar interferir con el contenido del terminal.
313// static
315 auto terminal = Terminal::Size();
316 return {
317 Dimension::Fullscreen,
318 terminal.dimx,
319 terminal.dimy,
320 /*use_alternative_screen=*/true,
321 };
322}
323
324/// Crea una ScreenInteractive cuyo ancho coincide con el ancho de salida del terminal y
325/// cuya altura coincide con el componente que se está dibujando.
326// static
328 auto terminal = Terminal::Size();
329 return {
330 Dimension::TerminalOutput,
331 terminal.dimx,
332 terminal.dimy, // Best guess.
333 /*use_alternative_screen=*/false,
334 };
335}
336
338
339/// Crea una ScreenInteractive cuyo ancho y altura coinciden con el componente
340/// que se está dibujando.
341// static
343 auto terminal = Terminal::Size();
344 return {
345 Dimension::FitComponent,
346 terminal.dimx, // Best guess.
347 terminal.dimy, // Best guess.
348 false,
349 };
350}
351
352/// @brief Establece si el ratón es rastreado y se informan los eventos.
353/// se llama fuera del bucle principal. Por ejemplo, `ScreenInteractive::Loop(...)`.
354/// @param enable Si se habilita el seguimiento de eventos del ratón.
355/// @note Esto debe llamarse fuera del bucle principal. Por ejemplo, antes de llamar a
356/// `ScreenInteractive::Loop`.
357/// @note El seguimiento del ratón está habilitado por defecto.
358/// @note El seguimiento del ratón solo es compatible con terminales que lo soporten.
359///
360/// ### Ejemplo
361///
362/// ```cpp
363/// auto screen = ScreenInteractive::TerminalOutput();
364/// screen.TrackMouse(false);
365/// screen.Loop(component);
366/// ```
368 track_mouse_ = enable;
369}
370
371/// @brief Habilita o deshabilita el manejo automático de entrada por tubería.
372/// Cuando está habilitado, FTXUI detectará la entrada por tubería y redirigirá stdin desde /dev/tty
373/// para la entrada de teclado, permitiendo que las aplicaciones lean datos canalizados mientras
374/// siguen recibiendo eventos interactivos de teclado.
375/// @param enable Si se habilita el manejo de entrada por tubería. El valor predeterminado es verdadero.
376/// @note Esto debe llamarse antes de Loop().
377/// @note Esta característica está habilitada por defecto.
378/// @note Esta característica solo está disponible en sistemas POSIX (Linux/macOS).
380 handle_piped_input_ = enable;
381}
382
383/// @brief Añade una tarea al bucle principal.
384/// Se ejecutará más tarde, después de todas las demás tareas programadas.
386 internal_->task_runner.PostTask([this, task = std::move(task)]() mutable {
387 HandleTask(component_, task);
388 });
389}
390
391/// @brief Añade un evento al bucle principal.
392/// Se ejecutará más tarde, después de todos los demás eventos programados.
394 Post(event);
395}
396
397/// @brief Añade una tarea para dibujar la pantalla una vez más, hasta que todas las animaciones
398/// hayan terminado.
400 if (animation_requested_) {
401 return;
402 }
403 animation_requested_ = true;
404 auto now = animation::Clock::now();
405 const auto time_histeresis = std::chrono::milliseconds(33);
406 if (now - previous_animation_time_ >= time_histeresis) {
407 previous_animation_time_ = now;
408 }
409}
410
411/// @brief Intenta obtener el bloqueo único para poder capturar el ratón.
412/// @return Un bloqueo único si el ratón no está ya capturado, de lo contrario un
413/// nulo.
415 if (mouse_captured) {
416 return nullptr;
417 }
418 mouse_captured = true;
419 return std::make_unique<CapturedMouseImpl>(
420 [this] { mouse_captured = false; });
421}
422
423/// @brief Ejecuta el bucle principal.
424/// @param component El componente a dibujar.
425void ScreenInteractive::Loop(Component component) { // NOLINT
426 class Loop loop(this, std::move(component));
427 loop.Run();
428}
429
430/// @brief Devuelve si el bucle principal ha terminado.
431bool ScreenInteractive::HasQuitted() {
432 return quit_;
433}
434
435// private
436void ScreenInteractive::PreMain() {
437 // Suspender la pantalla previamente activa:
438 if (g_active_screen) {
439 std::swap(suspended_screen_, g_active_screen);
440 // Restablecer la posición del cursor a la parte superior de la pantalla y borrar la pantalla.
441 suspended_screen_->ResetCursorPosition();
442 std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
443 suspended_screen_->dimx_ = 0;
444 suspended_screen_->dimy_ = 0;
445
446 // Restablecer las dimensiones para forzar el dibujo de la pantalla de nuevo la próxima vez:
447 suspended_screen_->Uninstall();
448 }
449
450 // Esta pantalla está ahora activa:
451 g_active_screen = this;
452}
453
454// private
455void ScreenInteractive::PostMain() {
456 // Colocar la posición del cursor al final del dibujo.
457 ResetCursorPosition();
458
459 g_active_screen = nullptr;
460
461 // Restaurar pantalla suspendida.
462 if (suspended_screen_) {
463 // Borrar pantalla y colocar el cursor al principio del dibujo.
464 std::cout << ResetPosition(/*clear=*/true);
465 dimx_ = 0;
466 dimy_ = 0;
467 Uninstall();
468 std::swap(g_active_screen, suspended_screen_);
469 g_active_screen->Install();
470 } else {
471 Uninstall();
472
473 std::cout << '\r';
474 // Al salir finalmente, mantener el dibujo actual y restablecer la posición del cursor una
475 // línea después.
476 if (!use_alternative_screen_) {
477 std::cout << '\n';
478 std::cout << std::flush;
479 }
480 }
481}
482
483/// @brief Decora una función. Se ejecuta de la misma manera, pero con los
484/// hooks del terminal de la pantalla activa temporalmente desinstalados durante su ejecución.
485/// @param fn La función a decorar.
487 return [this, fn] {
488 Uninstall();
489 fn();
490 Install();
491 };
492}
493
494/// @brief Fuerza a FTXUI a manejar o no Ctrl-C, incluso si el componente
495/// captura el Event::CtrlC.
497 force_handle_ctrl_c_ = force;
498}
499
500/// @brief Fuerza a FTXUI a manejar o no Ctrl-Z, incluso si el componente
501/// captura el Event::CtrlZ.
503 force_handle_ctrl_z_ = force;
504}
505
506/// @brief Devuelve el contenido de la selección actual
508 if (!selection_) {
509 return "";
510 }
511 return selection_->GetParts();
512}
513
514void ScreenInteractive::SelectionChange(std::function<void()> callback) {
515 selection_on_change_ = std::move(callback);
516}
517
518/// @brief Devuelve la pantalla actualmente activa, o nulo si no hay ninguna.
519// static
521 return g_active_screen;
522}
523
524// private
525void ScreenInteractive::Install() {
526 frame_valid_ = false;
527
528 // Vaciar el búfer de stdout para asegurar que todo lo que el usuario haya impreso antes
529 // se aplique completamente antes de que comencemos a modificar la configuración del terminal. Esto
530 // es importante porque estamos usando dos canales diferentes (stdout vs
531 // termios/WinAPI) para comunicarnos con el emulador de terminal a continuación. Ver
532 // https://github.com/ArthurSonzogni/FTXUI/issues/846
533 Flush();
534
535 InstallPipedInputHandling();
536
537 // Después de desinstalar la nueva configuración, vaciarla al terminal para
538 // asegurar que se aplique completamente:
539 on_exit_functions.emplace([] { Flush(); });
540
541 on_exit_functions.emplace([this] { ExitLoopClosure()(); });
542
543 // Solicitar al terminal que informe la forma actual del cursor. Lo restauraremos
544 // al salir.
545 std::cout << DECRQSS_DECSCUSR;
546 on_exit_functions.emplace([this] {
547 std::cout << "\033[?25h"; // Habilitar cursor.
548 std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
549 });
550
551 // Instalar manejadores de señal para restaurar el estado del terminal al salir. Los manejadores
552 // de señal predeterminados se restauran al salir.
553 for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
554 InstallSignalHandler(signal);
555 }
556
557// Guardar la antigua configuración del terminal y restaurarla al salir.
558#if defined(_WIN32)
559 // Habilitar el procesamiento VT en stdout y stdin
560 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
561 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
562
563 DWORD out_mode = 0;
564 DWORD in_mode = 0;
565 GetConsoleMode(stdout_handle, &out_mode);
566 GetConsoleMode(stdin_handle, &in_mode);
567 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
568 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
569
570 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
571 const int enable_virtual_terminal_processing = 0x0004;
572 const int disable_newline_auto_return = 0x0008;
573 out_mode |= enable_virtual_terminal_processing;
574 out_mode |= disable_newline_auto_return;
575
576 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
577 const int enable_line_input = 0x0002;
578 const int enable_echo_input = 0x0004;
579 const int enable_virtual_terminal_input = 0x0200;
580 const int enable_window_input = 0x0008;
581 in_mode &= ~enable_echo_input;
582 in_mode &= ~enable_line_input;
583 in_mode |= enable_virtual_terminal_input;
584 in_mode |= enable_window_input;
585
586 SetConsoleMode(stdin_handle, in_mode);
587 SetConsoleMode(stdout_handle, out_mode);
588#else // POSIX (Linux & Mac)
589 for (const int signal : {SIGWINCH, SIGTSTP}) {
590 InstallSignalHandler(signal);
591 }
592
593 struct termios terminal; // NOLINT
594 tcgetattr(tty_fd_, &terminal);
595 on_exit_functions.emplace([terminal = terminal, tty_fd_ = tty_fd_] {
596 tcsetattr(tty_fd_, TCSANOW, &terminal);
597 });
598
599 // Habilitar el modo de entrada de terminal en crudo
600 terminal.c_iflag &= ~IGNBRK; // Deshabilitar la ignorancia de la condición de interrupción
601 terminal.c_iflag &= ~BRKINT; // Deshabilitar que la interrupción cause el
602 // vaciado de entrada y salida
603 terminal.c_iflag &= ~PARMRK; // Deshabilitar la marcación de errores de paridad.
604 terminal.c_iflag &= ~ISTRIP; // Deshabilitar la eliminación del 8º bit de los caracteres.
605 terminal.c_iflag &= ~INLCR; // Deshabilitar el mapeo de NL a CR.
606 terminal.c_iflag &= ~IGNCR; // Deshabilitar la ignorancia de CR.
607 terminal.c_iflag &= ~ICRNL; // Deshabilitar el mapeo de CR a NL.
608 terminal.c_iflag &= ~IXON; // Deshabilitar el control de flujo XON/XOFF en la salida
609
610 terminal.c_lflag &= ~ECHO; // Deshabilitar el eco de los caracteres de entrada.
611 terminal.c_lflag &= ~ECHONL; // Deshabilitar el eco de los caracteres de nueva línea.
612 terminal.c_lflag &= ~ICANON; // Deshabilitar el modo canónico.
613 terminal.c_lflag &= ~ISIG; // Deshabilitar el envío de señal al pulsar:
614 // - => DSUSP
615 // - C-Z => SUSP
616 // - C-C => INTR
617 // - C-d => QUIT
618 terminal.c_lflag &= ~IEXTEN; // Deshabilitar el procesamiento de entrada extendido
619 terminal.c_cflag |= CS8; // 8 bits por byte
620
621 terminal.c_cc[VMIN] = 0; // Número mínimo de caracteres para lectura no canónica.
622 terminal.c_cc[VTIME] = 0; // Tiempo de espera en deciseGUNDOS para lectura no canónica.
623
624 tcsetattr(tty_fd_, TCSANOW, &terminal);
625
626#endif
627
628 auto enable = [&](const std::vector<DECMode>& parameters) {
629 std::cout << Set(parameters);
630 on_exit_functions.emplace([=] { std::cout << Reset(parameters); });
631 };
632
633 auto disable = [&](const std::vector<DECMode>& parameters) {
634 std::cout << Reset(parameters);
635 on_exit_functions.emplace([=] { std::cout << Set(parameters); });
636 };
637
638 if (use_alternative_screen_) {
639 enable({
640 DECMode::kAlternateScreen,
641 });
642 }
643
644 disable({
645 // DECMode::kCursor,
646 DECMode::kLineWrap,
647 });
648
649 if (track_mouse_) {
650 enable({DECMode::kMouseVt200});
651 enable({DECMode::kMouseAnyEvent});
652 enable({DECMode::kMouseUrxvtMode});
653 enable({DECMode::kMouseSgrExtMode});
654 }
655
656 // Después de instalar la nueva configuración, vaciarla al terminal para
657 // asegurar que se aplique completamente:
658 Flush();
659
660 quit_ = false;
661
662 PostAnimationTask();
663}
664
665void ScreenInteractive::InstallPipedInputHandling() {
666#if !defined(_WIN32) && !defined(__EMSCRIPTEN__)
667 tty_fd_ = STDIN_FILENO;
668 // Manejar la redirección de entrada canalizada si la aplicación la habilita explícitamente.
669 // Esto permite que las aplicaciones lean datos de stdin mientras siguen recibiendo
670 // entrada de teclado del terminal para uso interactivo.
671 if (!handle_piped_input_) {
672 return;
673 }
674
675 // Si stdin es un terminal, no necesitamos abrir /dev/tty.
676 if (isatty(STDIN_FILENO)) {
677 return;
678 }
679
680 // Abrir /dev/tty para la entrada del teclado.
681 tty_fd_ = open("/dev/tty", O_RDONLY);
682 if (tty_fd_ < 0) {
683 // Falló al abrir /dev/tty (contenedores, sistemas sin cabeza, etc.)
684 tty_fd_ = STDIN_FILENO; // Volver a stdin.
685 return;
686 }
687
688 // Cerrar el descriptor de archivo /dev/tty al salir.
689 on_exit_functions.emplace([this] {
690 close(tty_fd_);
691 tty_fd_ = -1;
692 });
693#endif
694}
695
696// private
697void ScreenInteractive::Uninstall() {
698 ExitNow();
699 OnExit();
700}
701
702// private
703// NOLINTNEXTLINE
704void ScreenInteractive::RunOnceBlocking(Component component) {
705 // Establecer FPS a 60 como máximo.
706 const auto time_per_frame = std::chrono::microseconds(16666); // 1s / 60fps
707
708 auto time = std::chrono::steady_clock::now();
709 size_t executed_task = internal_->task_runner.ExecutedTasks();
710
711 // Esperar a que se ejecute al menos una tarea.
712 while (executed_task == internal_->task_runner.ExecutedTasks() &&
713 !HasQuitted()) {
714 RunOnce(component);
715
716 const auto now = std::chrono::steady_clock::now();
717 const auto delta = now - time;
718 time = now;
719
720 if (delta < time_per_frame) {
721 const auto sleep_duration = time_per_frame - delta;
722 std::this_thread::sleep_for(sleep_duration);
723 }
724 }
725}
726
727// private
728void ScreenInteractive::RunOnce(Component component) {
729 AutoReset set_component(&component_, component);
730 ExecuteSignalHandlers();
731 FetchTerminalEvents();
732
733 // Ejecutar las tareas pendientes de la cola.
734 const size_t executed_task = internal_->task_runner.ExecutedTasks();
735 internal_->task_runner.RunUntilIdle();
736 // Si no se ejecutó ninguna tarea, podemos regresar temprano sin volver a dibujar la pantalla.
737 if (executed_task == internal_->task_runner.ExecutedTasks()) {
738 return;
739 }
740
741 ExecuteSignalHandlers();
742 Draw(component);
743
744 if (selection_data_previous_ != selection_data_) {
745 selection_data_previous_ = selection_data_;
746 if (selection_on_change_) {
747 selection_on_change_();
749 }
750 }
751}
752
753// private
754// NOLINTNEXTLINE
755void ScreenInteractive::HandleTask(Component component, Task& task) {
756 std::visit(
757 [&](auto&& arg) {
758 using T = std::decay_t<decltype(arg)>;
759
760 // clang-format off
761 // Manejar Evento.
762 if constexpr (std::is_same_v<T, Event>) {
763
764 if (arg.is_cursor_position()) {
765 cursor_x_ = arg.cursor_x();
766 cursor_y_ = arg.cursor_y();
767 return;
768 }
769
770 if (arg.is_cursor_shape()) {
771 cursor_reset_shape_= arg.cursor_shape();
772 return;
773 }
774
775 if (arg.is_mouse()) {
776 arg.mouse().x -= cursor_x_;
777 arg.mouse().y -= cursor_y_;
778 }
779
780 arg.screen_ = this;
781
782 bool handled = component->OnEvent(arg);
783
784 handled = HandleSelection(handled, arg);
785
786 if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) {
787 RecordSignal(SIGABRT);
788 }
789
790#if !defined(_WIN32)
791 if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) {
792 RecordSignal(SIGTSTP);
793 }
794#endif
795
796 frame_valid_ = false;
797 return;
798 }
799
800 // Manejar callback
801 if constexpr (std::is_same_v<T, Closure>) {
802 arg();
803 return;
804 }
805
806 // Manejar Animación
807 if constexpr (std::is_same_v<T, AnimationTask>) {
808 if (!animation_requested_) {
809 return;
810 }
811
812 animation_requested_ = false;
813 const animation::TimePoint now = animation::Clock::now();
814 const animation::Duration delta = now - previous_animation_time_;
815 previous_animation_time_ = now;
816
817 animation::Params params(delta);
818 component->OnAnimation(params);
819 frame_valid_ = false;
820 return;
821 }
822 },
823 task);
824 // clang-format on
825}
826
827// private
828bool ScreenInteractive::HandleSelection(bool handled, Event event) {
829 if (handled) {
830 selection_pending_ = nullptr;
831 selection_data_.empty = true;
832 selection_ = nullptr;
833 return true;
834 }
835
836 if (!event.is_mouse()) {
837 return false;
838 }
839
840 auto& mouse = event.mouse();
841 if (mouse.button != Mouse::Left) {
842 return false;
843 }
844
845 if (mouse.motion == Mouse::Pressed) {
846 selection_pending_ = CaptureMouse();
847 selection_data_.start_x = mouse.x;
848 selection_data_.start_y = mouse.y;
849 selection_data_.end_x = mouse.x;
850 selection_data_.end_y = mouse.y;
851 return false;
852 }
853
854 if (!selection_pending_) {
855 return false;
856 }
857
858 if (mouse.motion == Mouse::Moved) {
859 if ((mouse.x != selection_data_.end_x) ||
860 (mouse.y != selection_data_.end_y)) {
861 selection_data_.end_x = mouse.x;
862 selection_data_.end_y = mouse.y;
863 selection_data_.empty = false;
864 }
865
866 return true;
867 }
868
869 if (mouse.motion == Mouse::Released) {
870 selection_pending_ = nullptr;
871 selection_data_.end_x = mouse.x;
872 selection_data_.end_y = mouse.y;
873 selection_data_.empty = false;
874 return true;
875 }
876
877 return false;
878}
879
880// private
881// NOLINTNEXTLINE
882void ScreenInteractive::Draw(Component component) {
883 if (frame_valid_) {
884 return;
885 }
886 auto document = component->Render();
887 int dimx = 0;
888 int dimy = 0;
889 auto terminal = Terminal::Size();
890 document->ComputeRequirement();
891 switch (dimension_) {
892 case Dimension::Fixed:
893 dimx = dimx_;
894 dimy = dimy_;
895 break;
896 case Dimension::TerminalOutput:
897 dimx = terminal.dimx;
898 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
899 break;
900 case Dimension::Fullscreen:
901 dimx = terminal.dimx;
902 dimy = terminal.dimy;
903 break;
904 case Dimension::FitComponent:
905 dimx = util::clamp(document->requirement().min_x, 0, terminal.dimy);
906 dimy = util::clamp(document->requirement().min_y, 0, terminal.dimy);
907 break;
908 }
909
910 const bool resized = frame_count_ == 0 || (dimx != dimx_) || (dimy != dimy_);
911 ResetCursorPosition();
912 std::cout << ResetPosition(/*clear=*/resized);
913
914 // Si el ancho del terminal disminuye, el emulador de terminal comenzará a ajustar
915 // líneas y ensuciará la pantalla. Debemos limpiarla por completo.
916 if ((dimx < dimx_) && !use_alternative_screen_) {
917 std::cout << "\033[J"; // limpiar la salida del terminal
918 std::cout << "\033[H"; // mover el cursor a la posición inicial
919 }
920
921 // Redimensionar la pantalla si es necesario.
922 if (resized) {
923 dimx_ = dimx;
924 dimy_ = dimy;
925 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
926 cursor_.x = dimx_ - 1;
927 cursor_.y = dimy_ - 1;
928 }
929
930 // Solicitar periódicamente al emulador de terminal la posición del marco en relación con
931 // la pantalla. Esto es útil para convertir la posición del ratón informada en
932 // coordenadas de la pantalla a coordenadas del marco.
933#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
934 // El terminal de Microsoft sufre de un [bug]. Al informar la posición del cursor,
935 // varias secuencias de salida se mezclan generando basura.
936 // Esto hace que el usuario de FTXUI vea algunas secuencias "1;1;R" en el componente Input.
937 // Ver [issue]. La solución es solicitar la posición del cursor con menos
938 // frecuencia. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
939 // https://github.com/ArthurSonzogni/FTXUI/issues/136
940 static int i = -3;
941 ++i;
942 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
943 std::cout << DeviceStatusReport(DSRMode::kCursor);
944 }
945#else
946 static int i = -3;
947 ++i;
948 if (!use_alternative_screen_ &&
949 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
950 std::cout << DeviceStatusReport(DSRMode::kCursor);
951 }
952#endif
953 previous_frame_resized_ = resized;
954
955 selection_ = selection_data_.empty
956 ? std::make_unique<Selection>()
957 : std::make_unique<Selection>(
958 selection_data_.start_x, selection_data_.start_y, //
959 selection_data_.end_x, selection_data_.end_y);
960 Render(*this, document.get(), *selection_);
961
962 // Establecer la posición del cursor para el usuario que utiliza herramientas para insertar caracteres CJK.
963 {
964 const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
965 const int dy = dimy_ - 1 - cursor_.y;
966
967 set_cursor_position.clear();
968 reset_cursor_position.clear();
969
970 if (dy != 0) {
971 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
972 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
973 }
974
975 if (dx != 0) {
976 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
977 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
978 }
979
980 if (cursor_.shape == Cursor::Hidden) {
981 set_cursor_position += "\033[?25l";
982 } else {
983 set_cursor_position += "\033[?25h";
984 set_cursor_position +=
985 "\033[" + std::to_string(int(cursor_.shape)) + " q";
986 }
987 }
988
989 std::cout << ToString() << set_cursor_position;
990 Flush();
991 Clear();
992 frame_valid_ = true;
993 frame_count_++;
994}
995
996// private
997void ScreenInteractive::ResetCursorPosition() {
998 std::cout << reset_cursor_position;
999 reset_cursor_position = "";
1000}
1001
1002/// @brief Devuelve una función para salir del bucle principal.
1004 return [this] { Exit(); };
1005}
1006
1007/// @brief Sale del bucle principal.
1009 Post([this] { ExitNow(); });
1010}
1011
1012// private:
1013void ScreenInteractive::ExitNow() {
1014 quit_ = true;
1015}
1016
1017// private:
1018void ScreenInteractive::Signal(int signal) {
1019 if (signal == SIGABRT) {
1020 Exit();
1021 return;
1022 }
1023
1024// Windows no soporta SIGTSTP / SIGWINCH
1025#if !defined(_WIN32)
1026 if (signal == SIGTSTP) {
1027 Post([&] {
1028 ResetCursorPosition();
1029 std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
1030 Uninstall();
1031 dimx_ = 0;
1032 dimy_ = 0;
1033 Flush();
1034 std::ignore = std::raise(SIGTSTP);
1035 Install();
1036 });
1037 return;
1038 }
1039
1040 if (signal == SIGWINCH) {
1041 Post(Event::Special({0}));
1042 return;
1043 }
1044#endif
1045}
1046
1047void ScreenInteractive::FetchTerminalEvents() {
1048#if defined(_WIN32)
1049 auto get_input_records = [&]() -> std::vector<INPUT_RECORD> {
1050 // Comprobar si hay entrada en la consola.
1051 auto console = GetStdHandle(STD_INPUT_HANDLE);
1052 DWORD number_of_events = 0;
1053 if (!GetNumberOfConsoleInputEvents(console, &number_of_events)) {
1054 return std::vector<INPUT_RECORD>();
1055 }
1056 if (number_of_events <= 0) {
1057 // No hay entrada, regresar.
1058 return std::vector<INPUT_RECORD>();
1059 }
1060 // Leer los eventos de entrada.
1061 std::vector<INPUT_RECORD> records(number_of_events);
1062 DWORD number_of_events_read = 0;
1063 if (!ReadConsoleInput(console, records.data(), (DWORD)records.size(),
1064 &number_of_events_read)) {
1065 return std::vector<INPUT_RECORD>();
1066 }
1067 records.resize(number_of_events_read);
1068 return records;
1069 };
1070
1071 auto records = get_input_records();
1072 if (records.size() == 0) {
1073 const auto timeout =
1074 std::chrono::steady_clock::now() - internal_->last_char_time;
1075 const size_t timeout_microseconds =
1076 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1077 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1078 return;
1079 }
1080 internal_->last_char_time = std::chrono::steady_clock::now();
1081
1082 // Convertir los eventos de entrada a eventos FTXUI.
1083 // Para cada evento, llamamos al analizador de entrada del terminal para convertirlo a
1084 // Evento.
1085 for (const auto& r : records) {
1086 switch (r.EventType) {
1087 case KEY_EVENT: {
1088 auto key_event = r.Event.KeyEvent;
1089 // ignorar eventos de tecla UP
1090 if (key_event.bKeyDown == FALSE) {
1091 continue;
1092 }
1093 std::wstring wstring;
1094 wstring += key_event.uChar.UnicodeChar;
1095 for (auto it : to_string(wstring)) {
1096 internal_->terminal_input_parser.Add(it);
1097 }
1098 } break;
1099 case WINDOW_BUFFER_SIZE_EVENT:
1100 Post(Event::Special({0}));
1101 break;
1102 case MENU_EVENT:
1103 case FOCUS_EVENT:
1104 case MOUSE_EVENT:
1105 // TODO(mauve): Implementar más tarde.
1106 break;
1107 }
1108 }
1109#elif defined(__EMSCRIPTEN__)
1110 // Leer caracteres del terminal.
1111 // Lo configuramos para que no sea bloqueante.
1112 std::array<char, 128> out{};
1113 size_t l = read(STDIN_FILENO, out.data(), out.size());
1114 if (l == 0) {
1115 const auto timeout =
1116 std::chrono::steady_clock::now() - internal_->last_char_time;
1117 const size_t timeout_microseconds =
1118 std::chrono::duration_cast<std::chrono::microseconds>(timeout).count();
1119 internal_->terminal_input_parser.Timeout(timeout_microseconds);
1120 return;
1121 }
1122 internal_->last_char_time = std::chrono::steady_clock::now();
1123
1124 // Convertir los caracteres a eventos.
1125 for (size_t i = 0; i < l; ++i) {
1126 internal_->terminal_input_parser.Add(out[i]);
1127 }
1128#else // POSIX (Linux & Mac)
1129 if (!CheckStdinReady(tty_fd_)) {
1130 const auto timeout =
1131 std::chrono::steady_clock::now() - internal_->last_char_time;
1132 const size_t timeout_ms =
1133 std::chrono::duration_cast<std::chrono::milliseconds>(timeout).count();
1134 internal_->terminal_input_parser.Timeout(timeout_ms);
1135 return;
1136 }
1137 internal_->last_char_time = std::chrono::steady_clock::now();
1138
1139 // Leer caracteres del terminal.
1140 std::array<char, 128> out{};
1141 size_t l = read(tty_fd_, out.data(), out.size());
1142
1143 // Convertir los caracteres a eventos.
1144 for (size_t i = 0; i < l; ++i) {
1145 internal_->terminal_input_parser.Add(out[i]);
1146 }
1147#endif
1148}
1149
1150void ScreenInteractive::PostAnimationTask() {
1151 Post(AnimationTask());
1152
1153 // Repetir la tarea de animación cada 15ms. Esto corresponde a una velocidad de fotogramas
1154 // de alrededor de 66fps.
1155 internal_->task_runner.PostDelayedTask([this] { PostAnimationTask(); },
1156 std::chrono::milliseconds(15));
1157}
1158
1159bool ScreenInteractive::SelectionData::operator==(
1160 const ScreenInteractive::SelectionData& other) const {
1161 if (empty && other.empty) {
1162 return true;
1163 }
1164 if (empty || other.empty) {
1165 return false;
1166 }
1167 return start_x == other.start_x && start_y == other.start_y &&
1168 end_x == other.end_x && end_y == other.end_y;
1169}
1170
1171bool ScreenInteractive::SelectionData::operator!=(
1172 const ScreenInteractive::SelectionData& other) const {
1173 return !(*this == other);
1174}
1175
1176} // namespace ftxui.
static void Signal(ScreenInteractive &s, int signal)
auto PostTask(Task task) -> void
Programa una tarea para ser ejecutada inmediatamente.
auto RunUntilIdle() -> std::optional< std::chrono::steady_clock::duration >
Ejecuta las tareas en la cola.
auto PostDelayedTask(Task task, std::chrono::steady_clock::duration duration) -> void
Programa una tarea para ser ejecutada después de una cierta duración.
size_t ExecutedTasks() const
auto screen
static const Event CtrlC
Definition event.hpp:71
static ScreenInteractive TerminalOutput()
void HandlePipedInput(bool enable=true)
Habilita o deshabilita el manejo automático de entrada por tubería. Cuando está habilitado,...
void Exit()
Sale del bucle principal.
static const Event CtrlZ
Definition event.hpp:94
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
Añade un evento al bucle principal. Se ejecutará más tarde, después de todos los demás eventos progra...
void Post(Task task)
Añade una tarea al bucle principal. Se ejecutará más tarde, después de todas las demás tareas program...
static ScreenInteractive FitComponent()
static ScreenInteractive Fullscreen()
static const Event Custom
Definition event.hpp:97
static ScreenInteractive FullscreenPrimaryScreen()
static ScreenInteractive * Active()
Devuelve la pantalla actualmente activa, o nulo si no hay ninguna.
CapturedMouse CaptureMouse()
Intenta obtener el bloqueo único para poder capturar el ratón.
std::string GetSelection()
Devuelve el contenido de la selección actual.
static ScreenInteractive FullscreenAlternateScreen()
void TrackMouse(bool enable=true)
Establece si el ratón es rastreado y se informan los eventos. se llama fuera del bucle principal....
void SelectionChange(std::function< void()> callback)
void RequestAnimationFrame()
Añade una tarea para dibujar la pantalla una vez más, hasta que todas las animaciones hayan terminado...
Closure ExitLoopClosure()
Devuelve una función para salir del bucle principal.
void ForceHandleCtrlC(bool force)
Fuerza a FTXUI a manejar o no Ctrl-C, incluso si el componente captura el Event::CtrlC.
void ForceHandleCtrlZ(bool force)
Fuerza a FTXUI a manejar o no Ctrl-Z, incluso si el componente captura el Event::CtrlZ.
Closure WithRestoredIO(Closure)
Decora una función. Se ejecuta de la misma manera, pero con los hooks del terminal de la pantalla act...
static Event Special(std::string)
Un evento personalizado cuyo significado es definido por el usuario de la biblioteca.
Definition event.cpp:74
Loop es una clase que gestiona el bucle de eventos de un componente.
Definition loop.hpp:56
ScreenInteractive es una Screen que puede manejar eventos, ejecutar un bucle principal y administrar ...
void RequestAnimationFrame()
RequestAnimationFrame es una función que solicita que se dibuje un nuevo fotograma en el siguiente ci...
Representa un evento. Puede ser un evento de pulsación de tecla, un redimensionamiento de terminal,...
Definition event.hpp:29
void Render(Screen &screen, const Element &element)
Muestra un elemento en un ftxui::Screen.
Definition node.cpp:84
int dimy() const
Definition image.hpp:36
std::string ToString() const
Definition screen.cpp:416
std::string ResetPosition(bool clear=false) const
Devuelve una cadena que se puede imprimir para restablecer la posición del cursor al principio de la ...
Definition screen.cpp:476
Cursor cursor_
Definition screen.hpp:79
void Clear()
Borra todos los píxeles de la pantalla.
Definition screen.cpp:495
int dimx() const
Definition image.hpp:35
std::vector< std::vector< Pixel > > pixels_
Definition image.hpp:46
Dimensions Size()
Obtiene el tamaño del terminal.
Definition terminal.cpp:93
Elements columns
El espacio de nombres ftxui::Dimension:: de FTXUI.
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:123
std::chrono::duration< float > Duration
Definition animation.hpp:29
std::chrono::time_point< Clock > TimePoint
Definition animation.hpp:28
constexpr const T & clamp(const T &v, const T &lo, const T &hi)
Definition util.hpp:11
El espacio de nombres ftxui:: de FTXUI.
Definition animation.hpp:10
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::string to_string(const std::wstring &s)
Element select(Element e)
Establece que child sea el elemento enfocado entre sus hermanos.
Definition frame.cpp:108
std::variant< Event, Closure, AnimationTask > Task
Definition task.hpp:14
std::function< void()> Closure
Definition task.hpp:13
std::shared_ptr< ComponentBase > Component
return out
Definition string.cpp:1399