FTXUI  3.0.0
C++ functional terminal UI.
Loading...
Searching...
No Matches
screen_interactive.cpp
Go to the documentation of this file.
1#include <algorithm> // for copy, max, min
2#include <array> // for array
3#include <chrono> // for operator-, milliseconds, duration, operator>=, time_point, common_type<>::type
4#include <csignal> // for signal, raise, SIGTSTP, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGWINCH
5#include <cstdio> // for fileno, size_t, stdin
6#include <functional> // for function
7#include <initializer_list> // for initializer_list
8#include <iostream> // for cout, ostream, basic_ostream, operator<<, endl, flush
9#include <stack> // for stack
10#include <thread> // for thread, sleep_for
11#include <type_traits> // for decay_t
12#include <utility> // for move, swap
13#include <variant> // for visit
14#include <vector> // for vector
15
16#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
17#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
18#include "ftxui/component/component_base.hpp" // for ComponentBase
19#include "ftxui/component/event.hpp" // for Event
20#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver
22#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
23#include "ftxui/dom/node.hpp" // for Node, Render
24#include "ftxui/dom/requirement.hpp" // for Requirement
25#include "ftxui/screen/terminal.hpp" // for Size, Dimensions
26
27#if defined(_WIN32)
28#define DEFINE_CONSOLEV2_PROPERTIES
29#define WIN32_LEAN_AND_MEAN
30#ifndef NOMINMAX
31#define NOMINMAX
32#endif
33#include <Windows.h>
34#ifndef UNICODE
35#error Must be compiled in UNICODE mode
36#endif
37#else
38#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set
39#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
40#include <unistd.h> // for STDIN_FILENO, read
41#endif
42
43// Quick exit is missing in standard CLang headers
44#if defined(__clang__) && defined(__APPLE__)
45#define quick_exit(a) exit(a)
46#endif
47
48namespace ftxui {
49
50namespace animation {
52 auto* screen = ScreenInteractive::Active();
53 if (screen) {
54 screen->RequestAnimationFrame();
55 }
56}
57} // namespace animation
58
59namespace {
60
61ScreenInteractive* g_active_screen = nullptr; // NOLINT
62
63void Flush() {
64 // Emscripten doesn't implement flush. We interpret zero as flush.
65 std::cout << '\0' << std::flush;
66}
67
68constexpr int timeout_milliseconds = 20;
69constexpr int timeout_microseconds = timeout_milliseconds * 1000;
70#if defined(_WIN32)
71
72void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
73 auto console = GetStdHandle(STD_INPUT_HANDLE);
74 auto parser = TerminalInputParser(out->Clone());
75 while (!*quit) {
76 // Throttle ReadConsoleInput by waiting 250ms, this wait function will
77 // return if there is input in the console.
78 auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
79 if (wait_result == WAIT_TIMEOUT) {
80 parser.Timeout(timeout_milliseconds);
81 continue;
82 }
83
84 DWORD number_of_events = 0;
85 if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
86 continue;
87 if (number_of_events <= 0)
88 continue;
89
90 std::vector<INPUT_RECORD> records{number_of_events};
91 DWORD number_of_events_read = 0;
92 ReadConsoleInput(console, records.data(), (DWORD)records.size(),
93 &number_of_events_read);
94 records.resize(number_of_events_read);
95
96 for (const auto& r : records) {
97 switch (r.EventType) {
98 case KEY_EVENT: {
99 auto key_event = r.Event.KeyEvent;
100 // ignore UP key events
101 if (key_event.bKeyDown == FALSE)
102 continue;
103 parser.Add((char)key_event.uChar.UnicodeChar);
104 } break;
105 case WINDOW_BUFFER_SIZE_EVENT:
106 out->Send(Event::Special({0}));
107 break;
108 case MENU_EVENT:
109 case FOCUS_EVENT:
110 case MOUSE_EVENT:
111 // TODO(mauve): Implement later.
112 break;
113 }
114 }
115 }
116}
117
118#elif defined(__EMSCRIPTEN__)
119#include <emscripten.h>
120
121// Read char from the terminal.
122void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
123 (void)timeout_microseconds;
124 auto parser = TerminalInputParser(std::move(out));
125
126 char c;
127 while (!*quit) {
128 while (read(STDIN_FILENO, &c, 1), c)
129 parser.Add(c);
130
131 emscripten_sleep(1);
132 parser.Timeout(1);
133 }
134}
135
136#else
137#include <sys/time.h> // for timeval
138
139int CheckStdinReady(int usec_timeout) {
140 timeval tv = {0, usec_timeout};
141 fd_set fds;
142 FD_ZERO(&fds); // NOLINT
143 FD_SET(STDIN_FILENO, &fds); // NOLINT
144 select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv); // NOLINT
145 return FD_ISSET(STDIN_FILENO, &fds); // NOLINT
146}
147
148// Read char from the terminal.
149void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
150 auto parser = TerminalInputParser(std::move(out));
151
152 while (!*quit) {
153 if (!CheckStdinReady(timeout_microseconds)) {
154 parser.Timeout(timeout_milliseconds);
155 continue;
156 }
157
158 const size_t buffer_size = 100;
159 std::array<char, buffer_size> buffer; // NOLINT;
160 int l = read(fileno(stdin), buffer.data(), buffer_size); // NOLINT
161 for (int i = 0; i < l; ++i) {
162 parser.Add(buffer[i]); // NOLINT
163 }
164 }
165}
166
167#endif
168
169const std::string CSI = "\x1b["; // NOLINT
170
171// DEC: Digital Equipment Corporation
172enum class DECMode {
173 kLineWrap = 7,
174 kMouseX10 = 9,
175 kCursor = 25,
176 kMouseVt200 = 1000,
177 kMouseAnyEvent = 1003,
178 kMouseUtf8 = 1005,
179 kMouseSgrExtMode = 1006,
180 kMouseUrxvtMode = 1015,
181 kMouseSgrPixelsMode = 1016,
182 kAlternateScreen = 1049,
183};
184
185// Device Status Report (DSR) {
186enum class DSRMode {
187 kCursor = 6,
188};
189
190std::string Serialize(const std::vector<DECMode>& parameters) {
191 bool first = true;
192 std::string out;
193 for (DECMode parameter : parameters) {
194 if (!first) {
195 out += ";";
196 }
197 out += std::to_string(int(parameter));
198 first = false;
199 }
200 return out;
201}
202
203// DEC Private Mode Set (DECSET)
204std::string Set(const std::vector<DECMode>& parameters) {
205 return CSI + "?" + Serialize(parameters) + "h";
206}
207
208// DEC Private Mode Reset (DECRST)
209std::string Reset(const std::vector<DECMode>& parameters) {
210 return CSI + "?" + Serialize(parameters) + "l";
211}
212
213// Device Status Report (DSR)
214std::string DeviceStatusReport(DSRMode ps) {
215 return CSI + std::to_string(int(ps)) + "n";
216}
217
218using SignalHandler = void(int);
219std::stack<Closure> on_exit_functions; // NOLINT
220void OnExit(int signal) {
221 (void)signal;
222 while (!on_exit_functions.empty()) {
223 on_exit_functions.top()();
224 on_exit_functions.pop();
225 }
226}
227
228const auto install_signal_handler = [](int sig, SignalHandler handler) {
229 auto old_signal_handler = std::signal(sig, handler);
230 on_exit_functions.push([=] { std::signal(sig, old_signal_handler); });
231};
232
233Closure g_on_resize = [] {}; // NOLINT
234void OnResize(int /* signal */) {
235 g_on_resize();
236}
237
238void OnSigStop(int /*signal*/) {
239 ScreenInteractive::Private::SigStop(*g_active_screen);
240}
241
242class CapturedMouseImpl : public CapturedMouseInterface {
243 public:
244 explicit CapturedMouseImpl(std::function<void(void)> callback)
245 : callback_(std::move(callback)) {}
246 ~CapturedMouseImpl() override { callback_(); }
247 CapturedMouseImpl(const CapturedMouseImpl&) = delete;
248 CapturedMouseImpl(CapturedMouseImpl&&) = delete;
249 CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
250 CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
251
252 private:
253 std::function<void(void)> callback_;
254};
255
256void AnimationListener(std::atomic<bool>* quit, Sender<Task> out) {
257 // Animation at around 60fps.
258 const auto time_delta = std::chrono::milliseconds(15);
259 while (!*quit) {
260 out->Send(AnimationTask());
261 std::this_thread::sleep_for(time_delta);
262 }
263}
264
265} // namespace
266
267ScreenInteractive::ScreenInteractive(int dimx,
268 int dimy,
269 Dimension dimension,
270 bool use_alternative_screen)
271 : Screen(dimx, dimy),
272 dimension_(dimension),
273 use_alternative_screen_(use_alternative_screen) {
274 task_receiver_ = MakeReceiver<Task>();
275}
276
277// static
279 return ScreenInteractive(dimx, dimy, Dimension::Fixed, false);
280}
281
282// static
284 return ScreenInteractive(0, 0, Dimension::Fullscreen, true);
285}
286
287// static
289 return ScreenInteractive(0, 0, Dimension::TerminalOutput, false);
290}
291
292// static
294 return ScreenInteractive(0, 0, Dimension::FitComponent, false);
295}
296
298 if (!quit_) {
299 task_sender_->Send(std::move(task));
300 }
301}
303 Post(event);
304}
305
307 if (animation_requested_) {
308 return;
309 }
310 animation_requested_ = true;
311 auto now = animation::Clock::now();
312 const auto time_histeresis = std::chrono::milliseconds(33);
313 if (now - previous_animation_time >= time_histeresis) {
314 previous_animation_time = now;
315 }
316}
317
319 if (mouse_captured) {
320 return nullptr;
321 }
322 mouse_captured = true;
323 return std::make_unique<CapturedMouseImpl>(
324 [this] { mouse_captured = false; });
325}
326
327void ScreenInteractive::Loop(Component component) { // NOLINT
328 // Suspend previously active screen:
329 if (g_active_screen) {
330 std::swap(suspended_screen_, g_active_screen);
331 std::cout << suspended_screen_->reset_cursor_position
332 << suspended_screen_->ResetPosition(/*clear=*/true);
333 suspended_screen_->dimx_ = 0;
334 suspended_screen_->dimy_ = 0;
335 suspended_screen_->Uninstall();
336 }
337
338 // This screen is now active:
339 g_active_screen = this;
340 g_active_screen->Install();
341 g_active_screen->Main(std::move(component));
342 g_active_screen->Uninstall();
343 g_active_screen = nullptr;
344
345 // Put cursor position at the end of the drawing.
346 std::cout << reset_cursor_position;
347
348 // Restore suspended screen.
349 if (suspended_screen_) {
350 std::cout << ResetPosition(/*clear=*/true);
351 dimx_ = 0;
352 dimy_ = 0;
353 std::swap(g_active_screen, suspended_screen_);
354 g_active_screen->Install();
355 } else {
356 // On final exit, keep the current drawing and reset cursor position one
357 // line after it.
358 std::cout << std::endl;
359 }
360}
361
362/// @brief Decorate a function. It executes the same way, but with the currently
363/// active screen terminal hooks temporarilly uninstalled during its execution.
364/// @param fn The function to decorate.
366 return [this, fn] {
367 Uninstall();
368 fn();
369 Install();
370 };
371}
372
373// static
375 return g_active_screen;
376}
377
378void ScreenInteractive::Install() {
379 // After uninstalling the new configuration, flush it to the terminal to
380 // ensure it is fully applied:
381 on_exit_functions.push([] { Flush(); });
382
383 on_exit_functions.push([this] { ExitLoopClosure()(); });
384
385 // Install signal handlers to restore the terminal state on exit. The default
386 // signal handlers are restored on exit.
387 for (int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
388 install_signal_handler(signal, OnExit);
389 }
390
391 // Save the old terminal configuration and restore it on exit.
392#if defined(_WIN32)
393 // Enable VT processing on stdout and stdin
394 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
395 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
396
397 DWORD out_mode = 0;
398 DWORD in_mode = 0;
399 GetConsoleMode(stdout_handle, &out_mode);
400 GetConsoleMode(stdin_handle, &in_mode);
401 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
402 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
403
404 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
405 const int enable_virtual_terminal_processing = 0x0004;
406 const int disable_newline_auto_return = 0x0008;
407 out_mode |= enable_virtual_terminal_processing;
408 out_mode |= disable_newline_auto_return;
409
410 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
411 const int enable_line_input = 0x0002;
412 const int enable_echo_input = 0x0004;
413 const int enable_virtual_terminal_input = 0x0200;
414 const int enable_window_input = 0x0008;
415 in_mode &= ~enable_echo_input;
416 in_mode &= ~enable_line_input;
417 in_mode |= enable_virtual_terminal_input;
418 in_mode |= enable_window_input;
419
420 SetConsoleMode(stdin_handle, in_mode);
421 SetConsoleMode(stdout_handle, out_mode);
422#else
423 struct termios terminal; // NOLINT
424 tcgetattr(STDIN_FILENO, &terminal);
425 on_exit_functions.push([=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
426
427 terminal.c_lflag &= ~ICANON; // NOLINT Non canonique terminal.
428 terminal.c_lflag &= ~ECHO; // NOLINT Do not print after a key press.
429 terminal.c_cc[VMIN] = 0;
430 terminal.c_cc[VTIME] = 0;
431 // auto oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
432 // fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
433 // on_exit_functions.push([=] { fcntl(STDIN_FILENO, F_GETFL, oldf); });
434
435 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
436
437 // Handle resize.
438 g_on_resize = [&] { task_sender_->Send(Event::Special({0})); };
439 install_signal_handler(SIGWINCH, OnResize);
440
441 // Handle SIGTSTP/SIGCONT.
442 install_signal_handler(SIGTSTP, OnSigStop);
443#endif
444
445 auto enable = [&](const std::vector<DECMode>& parameters) {
446 std::cout << Set(parameters);
447 on_exit_functions.push([=] { std::cout << Reset(parameters); });
448 };
449
450 auto disable = [&](const std::vector<DECMode>& parameters) {
451 std::cout << Reset(parameters);
452 on_exit_functions.push([=] { std::cout << Set(parameters); });
453 };
454
455 if (use_alternative_screen_) {
456 enable({
457 DECMode::kAlternateScreen,
458 });
459 }
460
461 disable({
462 DECMode::kCursor,
463 DECMode::kLineWrap,
464 });
465
466 enable({
467 // DECMode::kMouseVt200,
468 DECMode::kMouseAnyEvent,
469 DECMode::kMouseUtf8,
470 DECMode::kMouseSgrExtMode,
471 });
472
473 // After installing the new configuration, flush it to the terminal to ensure
474 // it is fully applied:
475 Flush();
476
477 quit_ = false;
478 task_sender_ = task_receiver_->MakeSender();
479 event_listener_ =
480 std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
481 animation_listener_ =
482 std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
483}
484
485void ScreenInteractive::Uninstall() {
486 ExitLoopClosure()();
487 event_listener_.join();
488 animation_listener_.join();
489
490 OnExit(0);
491}
492
493// NOLINTNEXTLINE
494void ScreenInteractive::Main(Component component) {
495 previous_animation_time = animation::Clock::now();
496
497 auto draw = [&] {
498 Draw(component);
499 std::cout << ToString() << set_cursor_position;
500 Flush();
501 Clear();
502 };
503
504 bool attempt_draw = true;
505 while (!quit_) {
506 if (attempt_draw && !task_receiver_->HasPending()) {
507 draw();
508 attempt_draw = false;
509 }
510
511 Task task;
512 if (!task_receiver_->Receive(&task)) {
513 break;
514 }
515
516 // clang-format off
517 std::visit([&](auto&& arg) {
518 using T = std::decay_t<decltype(arg)>;
519
520 // Handle Event.
521 if constexpr (std::is_same_v<T, Event>) {
522 if (arg.is_cursor_reporting()) {
523 cursor_x_ = arg.cursor_x();
524 cursor_y_ = arg.cursor_y();
525 return;
526 }
527
528 if (arg.is_mouse()) {
529 arg.mouse().x -= cursor_x_;
530 arg.mouse().y -= cursor_y_;
531 }
532
533 arg.screen_ = this;
534 component->OnEvent(arg);
535 attempt_draw = true;
536 return;
537 }
538
539 // Handle callback
540 if constexpr (std::is_same_v<T, Closure>) {
541 arg();
542 return;
543 }
544
545 // Handle Animation
546 if constexpr (std::is_same_v<T, AnimationTask>) {
547 if (!animation_requested_) {
548 return;
549 }
550
551 animation_requested_ = false;
552 animation::TimePoint now = animation::Clock::now();
553 animation::Duration delta = now - previous_animation_time;
554 previous_animation_time = now;
555
556 animation::Params params(delta);
557 component->OnAnimation(params);
558 attempt_draw = true;
559 return;
560 }
561 },
562 task);
563 // clang-format on
564 }
565}
566
567// NOLINTNEXTLINE
568void ScreenInteractive::Draw(Component component) {
569 auto document = component->Render();
570 int dimx = 0;
571 int dimy = 0;
572 switch (dimension_) {
573 case Dimension::Fixed:
574 dimx = dimx_;
575 dimy = dimy_;
576 break;
577 case Dimension::TerminalOutput:
578 document->ComputeRequirement();
580 dimy = document->requirement().min_y;
581 break;
582 case Dimension::Fullscreen:
585 break;
586 case Dimension::FitComponent:
587 auto terminal = Terminal::Size();
588 document->ComputeRequirement();
589 dimx = std::min(document->requirement().min_x, terminal.dimx);
590 dimy = std::min(document->requirement().min_y, terminal.dimy);
591 break;
592 }
593
594 bool resized = (dimx != dimx_) || (dimy != dimy_);
595 std::cout << reset_cursor_position << ResetPosition(/*clear=*/resized);
596
597 // Resize the screen if needed.
598 if (resized) {
599 dimx_ = dimx;
600 dimy_ = dimy;
601 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
602 cursor_.x = dimx_ - 1;
603 cursor_.y = dimy_ - 1;
604 }
605
606 // Periodically request the terminal emulator the frame position relative to
607 // the screen. This is useful for converting mouse position reported in
608 // screen's coordinates to frame's coordinates.
609#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
610 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
611 // position, several output sequences are mixed together into garbage.
612 // This causes FTXUI user to see some "1;1;R" sequences into the Input
613 // component. See [issue]. Solution is to request cursor position less
614 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
615 // https://github.com/ArthurSonzogni/FTXUI/issues/136
616 static int i = -3;
617 ++i;
618 if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
619 std::cout << DeviceStatusReport(DSRMode::kCursor);
620 }
621#else
622 static int i = -3;
623 ++i;
624 if (!use_alternative_screen_ &&
625 (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
626 std::cout << DeviceStatusReport(DSRMode::kCursor);
627 }
628#endif
629 previous_frame_resized_ = resized;
630
631 Render(*this, document);
632
633 // Set cursor position for user using tools to insert CJK characters.
634 set_cursor_position = "";
635 reset_cursor_position = "";
636
637 int dx = dimx_ - 1 - cursor_.x;
638 int dy = dimy_ - 1 - cursor_.y;
639
640 if (dx != 0) {
641 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
642 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
643 }
644 if (dy != 0) {
645 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
646 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
647 }
648}
649
651 return [this] {
652 quit_ = true;
653 task_sender_.reset();
654 };
655}
656
657void ScreenInteractive::SigStop() {
658#if defined(_WIN32)
659 // Windows do no support SIGTSTP.
660#else
661 Post([&] {
662 Uninstall();
663 std::cout << reset_cursor_position;
664 reset_cursor_position = "";
665 std::cout << ResetPosition(/*clear=*/true);
666 dimx_ = 0;
667 dimy_ = 0;
668 Flush();
669 std::raise(SIGTSTP);
670 Install();
671 });
672#endif
673}
674
675} // namespace ftxui.
676
677// Copyright 2020 Arthur Sonzogni. All rights reserved.
678// Use of this source code is governed by the MIT license that can be found in
679// the LICENSE file.
static void SigStop(ScreenInteractive &s)
static ScreenInteractive TerminalOutput()
static ScreenInteractive FixedSize(int dimx, int dimy)
static ScreenInteractive FitComponent()
static ScreenInteractive Fullscreen()
static ScreenInteractive * Active()
Closure WithRestoredIO(Closure)
Decorate a function. It executes the same way, but with the currently active screen terminal hooks te...
int dimy() const
Definition screen.hpp:70
std::string ToString()
Definition screen.cpp:407
std::string ResetPosition(bool clear=false) const
Return a string to be printed in order to reset the cursor position to the beginning of the screen.
Definition screen.cpp:470
Cursor cursor_
Definition screen.hpp:93
void Clear()
Clear all the pixel from the screen.
Definition screen.cpp:489
int dimx() const
Definition screen.hpp:69
std::vector< std::vector< Pixel > > pixels_
Definition screen.hpp:92
Dimensions Size()
Definition terminal.cpp:84
std::chrono::duration< double > Duration
Definition animation.hpp:21
std::chrono::time_point< Clock > TimePoint
Definition animation.hpp:20
std::unique_ptr< CapturedMouseInterface > CapturedMouse
Receiver< T > MakeReceiver()
Definition receiver.hpp:117
std::unique_ptr< SenderImpl< T > > Sender
Definition receiver.hpp:44
std::variant< Event, Closure, AnimationTask > Task
Definition task.hpp:11
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
Definition node.cpp:43
std::function< void()> Closure
Definition task.hpp:10
Element select(Element)
Definition frame.cpp:38
std::shared_ptr< ComponentBase > Component
Represent an event. It can be key press event, a terminal resize, or more ...
Definition event.hpp:26
static Event Special(std::string)
Definition event.cpp:37