FTXUI  5.0.0
C++ functional terminal UI.
screen_interactive.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 copy, max, min
5 #include <array> // for array
6 #include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
7 #include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
8 #include <cstdio> // for fileno, stdin
9 #include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
10 #include <ftxui/screen/screen.hpp> // for Pixel, Screen::Cursor, Screen, Screen::Cursor::Hidden
11 #include <functional> // for function
12 #include <initializer_list> // for initializer_list
13 #include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
14 #include <stack> // for stack
15 #include <thread> // for thread, sleep_for
16 #include <tuple> // for _Swallow_assign, ignore
17 #include <type_traits> // for decay_t
18 #include <utility> // for move, swap
19 #include <variant> // for visit, variant
20 #include <vector> // for vector
21 
22 #include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
23 #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
24 #include "ftxui/component/component_base.hpp" // for ComponentBase
25 #include "ftxui/component/event.hpp" // for Event
26 #include "ftxui/component/loop.hpp" // for Loop
27 #include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, SenderImpl, Receiver
29 #include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
30 #include "ftxui/dom/node.hpp" // for Node, Render
31 #include "ftxui/dom/requirement.hpp" // for Requirement
32 #include "ftxui/screen/terminal.hpp" // for Dimensions, Size
33 
34 #if defined(_WIN32)
35 #define DEFINE_CONSOLEV2_PROPERTIES
36 #define WIN32_LEAN_AND_MEAN
37 #ifndef NOMINMAX
38 #define NOMINMAX
39 #endif
40 #include <windows.h>
41 #ifndef UNICODE
42 #error Must be compiled in UNICODE mode
43 #endif
44 #else
45 #include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
46 #include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
47 #include <unistd.h> // for STDIN_FILENO, read
48 #endif
49 
50 // Quick exit is missing in standard CLang headers
51 #if defined(__clang__) && defined(__APPLE__)
52 #define quick_exit(a) exit(a)
53 #endif
54 
55 namespace ftxui {
56 
57 namespace animation {
59  auto* screen = ScreenInteractive::Active();
60  if (screen) {
61  screen->RequestAnimationFrame();
62  }
63 }
64 } // namespace animation
65 
66 namespace {
67 
68 ScreenInteractive* g_active_screen = nullptr; // NOLINT
69 
70 void Flush() {
71  // Emscripten doesn't implement flush. We interpret zero as flush.
72  std::cout << '\0' << std::flush;
73 }
74 
75 constexpr int timeout_milliseconds = 20;
76 [[maybe_unused]] constexpr int timeout_microseconds =
77  timeout_milliseconds * 1000;
78 #if defined(_WIN32)
79 
80 void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
81  auto console = GetStdHandle(STD_INPUT_HANDLE);
82  auto parser = TerminalInputParser(out->Clone());
83  while (!*quit) {
84  // Throttle ReadConsoleInput by waiting 250ms, this wait function will
85  // return if there is input in the console.
86  auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
87  if (wait_result == WAIT_TIMEOUT) {
88  parser.Timeout(timeout_milliseconds);
89  continue;
90  }
91 
92  DWORD number_of_events = 0;
93  if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
94  continue;
95  if (number_of_events <= 0)
96  continue;
97 
98  std::vector<INPUT_RECORD> records{number_of_events};
99  DWORD number_of_events_read = 0;
100  ReadConsoleInput(console, records.data(), (DWORD)records.size(),
101  &number_of_events_read);
102  records.resize(number_of_events_read);
103 
104  for (const auto& r : records) {
105  switch (r.EventType) {
106  case KEY_EVENT: {
107  auto key_event = r.Event.KeyEvent;
108  // ignore UP key events
109  if (key_event.bKeyDown == FALSE)
110  continue;
111  std::wstring wstring;
112  wstring += key_event.uChar.UnicodeChar;
113  for (auto it : to_string(wstring)) {
114  parser.Add(it);
115  }
116  } break;
117  case WINDOW_BUFFER_SIZE_EVENT:
118  out->Send(Event::Special({0}));
119  break;
120  case MENU_EVENT:
121  case FOCUS_EVENT:
122  case MOUSE_EVENT:
123  // TODO(mauve): Implement later.
124  break;
125  }
126  }
127  }
128 }
129 
130 #elif defined(__EMSCRIPTEN__)
131 #include <emscripten.h>
132 
133 // Read char from the terminal.
134 void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
135  (void)timeout_microseconds;
136  auto parser = TerminalInputParser(std::move(out));
137 
138  char c;
139  while (!*quit) {
140  while (read(STDIN_FILENO, &c, 1), c)
141  parser.Add(c);
142 
143  emscripten_sleep(1);
144  parser.Timeout(1);
145  }
146 }
147 
148 extern "C" {
149 EMSCRIPTEN_KEEPALIVE
150 void ftxui_on_resize(int columns, int rows) {
152  columns,
153  rows,
154  });
155  std::raise(SIGWINCH);
156 }
157 }
158 
159 #else // POSIX (Linux & Mac)
160 
161 int CheckStdinReady(int usec_timeout) {
162  timeval tv = {0, usec_timeout};
163  fd_set fds;
164  FD_ZERO(&fds); // NOLINT
165  FD_SET(STDIN_FILENO, &fds); // NOLINT
166  select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv); // NOLINT
167  return FD_ISSET(STDIN_FILENO, &fds); // NOLINT
168 }
169 
170 // Read char from the terminal.
171 void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
172  auto parser = TerminalInputParser(std::move(out));
173 
174  while (!*quit) {
175  if (!CheckStdinReady(timeout_microseconds)) {
176  parser.Timeout(timeout_milliseconds);
177  continue;
178  }
179 
180  const size_t buffer_size = 100;
181  std::array<char, buffer_size> buffer; // NOLINT;
182  size_t l = read(fileno(stdin), buffer.data(), buffer_size); // NOLINT
183  for (size_t i = 0; i < l; ++i) {
184  parser.Add(buffer[i]); // NOLINT
185  }
186  }
187 }
188 #endif
189 
190 std::stack<Closure> on_exit_functions; // NOLINT
191 void OnExit() {
192  while (!on_exit_functions.empty()) {
193  on_exit_functions.top()();
194  on_exit_functions.pop();
195  }
196 }
197 
198 std::atomic<int> g_signal_exit_count = 0; // NOLINT
199 #if !defined(_WIN32)
200 std::atomic<int> g_signal_stop_count = 0; // NOLINT
201 std::atomic<int> g_signal_resize_count = 0; // NOLINT
202 #endif
203 
204 // Async signal safe function
205 void RecordSignal(int signal) {
206  switch (signal) {
207  case SIGABRT:
208  case SIGFPE:
209  case SIGILL:
210  case SIGINT:
211  case SIGSEGV:
212  case SIGTERM:
213  g_signal_exit_count++;
214  break;
215 
216 #if !defined(_WIN32)
217  case SIGTSTP:
218  g_signal_stop_count++;
219  break;
220 
221  case SIGWINCH:
222  g_signal_resize_count++;
223  break;
224 #endif
225 
226  default:
227  break;
228  }
229 }
230 
231 void ExecuteSignalHandlers() {
232  int signal_exit_count = g_signal_exit_count.exchange(0);
233  while (signal_exit_count--) {
234  ScreenInteractive::Private::Signal(*g_active_screen, SIGABRT);
235  }
236 
237 #if !defined(_WIN32)
238  int signal_stop_count = g_signal_stop_count.exchange(0);
239  while (signal_stop_count--) {
240  ScreenInteractive::Private::Signal(*g_active_screen, SIGTSTP);
241  }
242 
243  int signal_resize_count = g_signal_resize_count.exchange(0);
244  while (signal_resize_count--) {
245  ScreenInteractive::Private::Signal(*g_active_screen, SIGWINCH);
246  }
247 #endif
248 }
249 
250 void InstallSignalHandler(int sig) {
251  auto old_signal_handler = std::signal(sig, RecordSignal);
252  on_exit_functions.push(
253  [=] { std::ignore = std::signal(sig, old_signal_handler); });
254 }
255 
256 // CSI: Control Sequence Introducer
257 const std::string CSI = "\x1b["; // NOLINT
258  //
259 // DCS: Device Control String
260 const std::string DCS = "\x1bP"; // NOLINT
261 // ST: String Terminator
262 const std::string ST = "\x1b\\"; // NOLINT
263 
264 // DECRQSS: Request Status String
265 // DECSCUSR: Set Cursor Style
266 const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
267 
268 // DEC: Digital Equipment Corporation
269 enum class DECMode {
270  kLineWrap = 7,
271  kCursor = 25,
272 
273  kMouseX10 = 9,
274  kMouseVt200 = 1000,
275  kMouseVt200Highlight = 1001,
276 
277  kMouseBtnEventMouse = 1002,
278  kMouseAnyEvent = 1003,
279 
280  kMouseUtf8 = 1005,
281  kMouseSgrExtMode = 1006,
282  kMouseUrxvtMode = 1015,
283  kMouseSgrPixelsMode = 1016,
284  kAlternateScreen = 1049,
285 };
286 
287 // Device Status Report (DSR) {
288 enum class DSRMode {
289  kCursor = 6,
290 };
291 
292 std::string Serialize(const std::vector<DECMode>& parameters) {
293  bool first = true;
294  std::string out;
295  for (const DECMode parameter : parameters) {
296  if (!first) {
297  out += ";";
298  }
299  out += std::to_string(int(parameter));
300  first = false;
301  }
302  return out;
303 }
304 
305 // DEC Private Mode Set (DECSET)
306 std::string Set(const std::vector<DECMode>& parameters) {
307  return CSI + "?" + Serialize(parameters) + "h";
308 }
309 
310 // DEC Private Mode Reset (DECRST)
311 std::string Reset(const std::vector<DECMode>& parameters) {
312  return CSI + "?" + Serialize(parameters) + "l";
313 }
314 
315 // Device Status Report (DSR)
316 std::string DeviceStatusReport(DSRMode ps) {
317  return CSI + std::to_string(int(ps)) + "n";
318 }
319 
320 class CapturedMouseImpl : public CapturedMouseInterface {
321  public:
322  explicit CapturedMouseImpl(std::function<void(void)> callback)
323  : callback_(std::move(callback)) {}
324  ~CapturedMouseImpl() override { callback_(); }
325  CapturedMouseImpl(const CapturedMouseImpl&) = delete;
326  CapturedMouseImpl(CapturedMouseImpl&&) = delete;
327  CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
328  CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
329 
330  private:
331  std::function<void(void)> callback_;
332 };
333 
334 void AnimationListener(std::atomic<bool>* quit, Sender<Task> out) {
335  // Animation at around 60fps.
336  const auto time_delta = std::chrono::milliseconds(15);
337  while (!*quit) {
338  out->Send(AnimationTask());
339  std::this_thread::sleep_for(time_delta);
340  }
341 }
342 
343 } // namespace
344 
345 ScreenInteractive::ScreenInteractive(int dimx,
346  int dimy,
347  Dimension dimension,
348  bool use_alternative_screen)
349  : Screen(dimx, dimy),
350  dimension_(dimension),
351  use_alternative_screen_(use_alternative_screen) {
352  task_receiver_ = MakeReceiver<Task>();
353 }
354 
355 // static
357  return {
358  dimx,
359  dimy,
360  Dimension::Fixed,
361  false,
362  };
363 }
364 
365 /// @ingroup component
366 /// Create a ScreenInteractive taking the full terminal size. This is using the
367 /// alternate screen buffer to avoid messing with the terminal content.
368 /// @note This is the same as `ScreenInteractive::FullscreenAlternateScreen()`
369 // static
371  return FullscreenAlternateScreen();
372 }
373 
374 /// @ingroup component
375 /// Create a ScreenInteractive taking the full terminal size. The primary screen
376 /// buffer is being used. It means if the terminal is resized, the previous
377 /// content might mess up with the terminal content.
378 // static
380  return {
381  0,
382  0,
383  Dimension::Fullscreen,
384  false,
385  };
386 }
387 
388 /// @ingroup component
389 /// Create a ScreenInteractive taking the full terminal size. This is using the
390 /// alternate screen buffer to avoid messing with the terminal content.
391 // static
393  return {
394  0,
395  0,
396  Dimension::Fullscreen,
397  true,
398  };
399 }
400 
401 // static
403  return {
404  0,
405  0,
406  Dimension::TerminalOutput,
407  false,
408  };
409 }
410 
411 // static
413  return {
414  0,
415  0,
416  Dimension::FitComponent,
417  false,
418  };
419 }
420 
421 /// @ingroup component
422 /// @brief Set whether mouse is tracked and events reported.
423 /// called outside of the main loop. E.g `ScreenInteractive::Loop(...)`.
424 /// @param enable Whether to enable mouse event tracking.
425 /// @note This muse be called outside of the main loop. E.g. before calling
426 /// `ScreenInteractive::Loop`.
427 /// @note Mouse tracking is enabled by default.
428 /// @note Mouse tracking is only supported on terminals that supports it.
429 ///
430 /// ### Example
431 ///
432 /// ```cpp
433 /// auto screen = ScreenInteractive::TerminalOutput();
434 /// screen.TrackMouse(false);
435 /// screen.Loop(component);
436 /// ```
437 void ScreenInteractive::TrackMouse(bool enable) {
438  track_mouse_ = enable;
439 }
440 
441 /// @brief Add a task to the main loop.
442 /// It will be executed later, after every other scheduled tasks.
443 /// @ingroup component
445  // Task/Events sent toward inactive screen or screen waiting to become
446  // inactive are dropped.
447  if (!task_sender_) {
448  return;
449  }
450 
451  task_sender_->Send(std::move(task));
452 }
453 
454 /// @brief Add an event to the main loop.
455 /// It will be executed later, after every other scheduled events.
456 /// @ingroup component
458  Post(event);
459 }
460 
461 /// @brief Add a task to draw the screen one more time, until all the animations
462 /// are done.
464  if (animation_requested_) {
465  return;
466  }
467  animation_requested_ = true;
468  auto now = animation::Clock::now();
469  const auto time_histeresis = std::chrono::milliseconds(33);
470  if (now - previous_animation_time_ >= time_histeresis) {
471  previous_animation_time_ = now;
472  }
473 }
474 
475 /// @brief Try to get the unique lock about behing able to capture the mouse.
476 /// @return A unique lock if the mouse is not already captured, otherwise a
477 /// null.
478 /// @ingroup component
480  if (mouse_captured) {
481  return nullptr;
482  }
483  mouse_captured = true;
484  return std::make_unique<CapturedMouseImpl>(
485  [this] { mouse_captured = false; });
486 }
487 
488 /// @brief Execute the main loop.
489 /// @param component The component to draw.
490 /// @ingroup component
491 void ScreenInteractive::Loop(Component component) { // NOLINT
492  class Loop loop(this, std::move(component));
493  loop.Run();
494 }
495 
496 /// @brief Return whether the main loop has been quit.
497 /// @ingroup component
498 bool ScreenInteractive::HasQuitted() {
499  return task_receiver_->HasQuitted();
500 }
501 
502 // private
503 void ScreenInteractive::PreMain() {
504  // Suspend previously active screen:
505  if (g_active_screen) {
506  std::swap(suspended_screen_, g_active_screen);
507  // Reset cursor position to the top of the screen and clear the screen.
508  suspended_screen_->ResetCursorPosition();
509  std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
510  suspended_screen_->dimx_ = 0;
511  suspended_screen_->dimy_ = 0;
512 
513  // Reset dimensions to force drawing the screen again next time:
514  suspended_screen_->Uninstall();
515  }
516 
517  // This screen is now active:
518  g_active_screen = this;
519  g_active_screen->Install();
520 
521  previous_animation_time_ = animation::Clock::now();
522 }
523 
524 // private
525 void ScreenInteractive::PostMain() {
526  // Put cursor position at the end of the drawing.
527  ResetCursorPosition();
528 
529  g_active_screen = nullptr;
530 
531  // Restore suspended screen.
532  if (suspended_screen_) {
533  // Clear screen, and put the cursor at the beginning of the drawing.
534  std::cout << ResetPosition(/*clear=*/true);
535  dimx_ = 0;
536  dimy_ = 0;
537  Uninstall();
538  std::swap(g_active_screen, suspended_screen_);
539  g_active_screen->Install();
540  } else {
541  Uninstall();
542 
543  std::cout << '\r';
544  // On final exit, keep the current drawing and reset cursor position one
545  // line after it.
546  if (!use_alternative_screen_) {
547  std::cout << std::endl;
548  }
549  }
550 }
551 
552 /// @brief Decorate a function. It executes the same way, but with the currently
553 /// active screen terminal hooks temporarilly uninstalled during its execution.
554 /// @param fn The function to decorate.
556  return [this, fn] {
557  Uninstall();
558  fn();
559  Install();
560  };
561 }
562 
563 /// @brief Return the currently active screen, or null if none.
564 // static
566  return g_active_screen;
567 }
568 
569 // private
570 void ScreenInteractive::Install() {
571 
572  frame_valid_ = false;
573 
574  // Flush the buffer for stdout to ensure whatever the user has printed before
575  // is fully applied before we start modifying the terminal configuration. This
576  // is important, because we are using two different channels (stdout vs
577  // termios/WinAPI) to communicate with the terminal emulator below. See
578  // https://github.com/ArthurSonzogni/FTXUI/issues/846
579  Flush();
580 
581  // After uninstalling the new configuration, flush it to the terminal to
582  // ensure it is fully applied:
583  on_exit_functions.push([] { Flush(); });
584 
585  on_exit_functions.push([this] { ExitLoopClosure()(); });
586 
587  // Request the terminal to report the current cursor shape. We will restore it
588  // on exit.
589  std::cout << DECRQSS_DECSCUSR;
590  on_exit_functions.push([=] {
591  std::cout << "\033[?25h"; // Enable cursor.
592  std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
593  });
594 
595  // Install signal handlers to restore the terminal state on exit. The default
596  // signal handlers are restored on exit.
597  for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
598  InstallSignalHandler(signal);
599  }
600 
601 // Save the old terminal configuration and restore it on exit.
602 #if defined(_WIN32)
603  // Enable VT processing on stdout and stdin
604  auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
605  auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
606 
607  DWORD out_mode = 0;
608  DWORD in_mode = 0;
609  GetConsoleMode(stdout_handle, &out_mode);
610  GetConsoleMode(stdin_handle, &in_mode);
611  on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
612  on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
613 
614  // https://docs.microsoft.com/en-us/windows/console/setconsolemode
615  const int enable_virtual_terminal_processing = 0x0004;
616  const int disable_newline_auto_return = 0x0008;
617  out_mode |= enable_virtual_terminal_processing;
618  out_mode |= disable_newline_auto_return;
619 
620  // https://docs.microsoft.com/en-us/windows/console/setconsolemode
621  const int enable_line_input = 0x0002;
622  const int enable_echo_input = 0x0004;
623  const int enable_virtual_terminal_input = 0x0200;
624  const int enable_window_input = 0x0008;
625  in_mode &= ~enable_echo_input;
626  in_mode &= ~enable_line_input;
627  in_mode |= enable_virtual_terminal_input;
628  in_mode |= enable_window_input;
629 
630  SetConsoleMode(stdin_handle, in_mode);
631  SetConsoleMode(stdout_handle, out_mode);
632 #else
633  for (const int signal : {SIGWINCH, SIGTSTP}) {
634  InstallSignalHandler(signal);
635  }
636 
637  struct termios terminal; // NOLINT
638  tcgetattr(STDIN_FILENO, &terminal);
639  on_exit_functions.push([=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
640 
641  terminal.c_lflag &= ~ICANON; // NOLINT Non canonique terminal.
642  terminal.c_lflag &= ~ECHO; // NOLINT Do not print after a key press.
643  terminal.c_cc[VMIN] = 0;
644  terminal.c_cc[VTIME] = 0;
645  // auto oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
646  // fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
647  // on_exit_functions.push([=] { fcntl(STDIN_FILENO, F_GETFL, oldf); });
648 
649  tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
650 
651 #endif
652 
653  auto enable = [&](const std::vector<DECMode>& parameters) {
654  std::cout << Set(parameters);
655  on_exit_functions.push([=] { std::cout << Reset(parameters); });
656  };
657 
658  auto disable = [&](const std::vector<DECMode>& parameters) {
659  std::cout << Reset(parameters);
660  on_exit_functions.push([=] { std::cout << Set(parameters); });
661  };
662 
663  if (use_alternative_screen_) {
664  enable({
665  DECMode::kAlternateScreen,
666  });
667  }
668 
669  disable({
670  // DECMode::kCursor,
671  DECMode::kLineWrap,
672  });
673 
674  if (track_mouse_) {
675  enable({DECMode::kMouseVt200});
676  enable({DECMode::kMouseAnyEvent});
677  enable({DECMode::kMouseUrxvtMode});
678  enable({DECMode::kMouseSgrExtMode});
679  }
680 
681  // After installing the new configuration, flush it to the terminal to
682  // ensure it is fully applied:
683  Flush();
684 
685  quit_ = false;
686  task_sender_ = task_receiver_->MakeSender();
687  event_listener_ =
688  std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
689  animation_listener_ =
690  std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
691 }
692 
693 // private
694 void ScreenInteractive::Uninstall() {
695  ExitNow();
696  event_listener_.join();
697  animation_listener_.join();
698  OnExit();
699 }
700 
701 // private
702 // NOLINTNEXTLINE
703 void ScreenInteractive::RunOnceBlocking(Component component) {
704  ExecuteSignalHandlers();
705  Task task;
706  if (task_receiver_->Receive(&task)) {
707  HandleTask(component, task);
708  }
709  RunOnce(component);
710 }
711 
712 // private
713 void ScreenInteractive::RunOnce(Component component) {
714  Task task;
715  while (task_receiver_->ReceiveNonBlocking(&task)) {
716  HandleTask(component, task);
717  ExecuteSignalHandlers();
718  }
719  Draw(std::move(component));
720 }
721 
722 // private
723 void ScreenInteractive::HandleTask(Component component, Task& task) {
724  std::visit(
725  [&](auto&& arg) {
726  using T = std::decay_t<decltype(arg)>;
727 
728  // clang-format off
729  // Handle Event.
730  if constexpr (std::is_same_v<T, Event>) {
731  if (arg.is_cursor_position()) {
732  cursor_x_ = arg.cursor_x();
733  cursor_y_ = arg.cursor_y();
734  return;
735  }
736 
737  if (arg.is_cursor_shape()) {
738  cursor_reset_shape_= arg.cursor_shape();
739  return;
740  }
741 
742  if (arg.is_mouse()) {
743  arg.mouse().x -= cursor_x_;
744  arg.mouse().y -= cursor_y_;
745  }
746 
747  arg.screen_ = this;
748  component->OnEvent(arg);
749  frame_valid_ = false;
750  return;
751  }
752 
753  // Handle callback
754  if constexpr (std::is_same_v<T, Closure>) {
755  arg();
756  return;
757  }
758 
759  // Handle Animation
760  if constexpr (std::is_same_v<T, AnimationTask>) {
761  if (!animation_requested_) {
762  return;
763  }
764 
765  animation_requested_ = false;
766  const animation::TimePoint now = animation::Clock::now();
767  const animation::Duration delta = now - previous_animation_time_;
768  previous_animation_time_ = now;
769 
770  animation::Params params(delta);
771  component->OnAnimation(params);
772  frame_valid_ = false;
773  return;
774  }
775  },
776  task);
777  // clang-format on
778 }
779 
780 // private
781 // NOLINTNEXTLINE
782 void ScreenInteractive::Draw(Component component) {
783  if (frame_valid_) {
784  return;
785  }
786  auto document = component->Render();
787  int dimx = 0;
788  int dimy = 0;
789  auto terminal = Terminal::Size();
790  document->ComputeRequirement();
791  switch (dimension_) {
792  case Dimension::Fixed:
793  dimx = dimx_;
794  dimy = dimy_;
795  break;
796  case Dimension::TerminalOutput:
797  dimx = terminal.dimx;
798  dimy = document->requirement().min_y;
799  break;
800  case Dimension::Fullscreen:
801  dimx = terminal.dimx;
802  dimy = terminal.dimy;
803  break;
804  case Dimension::FitComponent:
805  dimx = std::min(document->requirement().min_x, terminal.dimx);
806  dimy = std::min(document->requirement().min_y, terminal.dimy);
807  break;
808  }
809 
810  const bool resized = (dimx != dimx_) || (dimy != dimy_);
811  ResetCursorPosition();
812  std::cout << ResetPosition(/*clear=*/resized);
813 
814  // Resize the screen if needed.
815  if (resized) {
816  dimx_ = dimx;
817  dimy_ = dimy;
818  pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
819  cursor_.x = dimx_ - 1;
820  cursor_.y = dimy_ - 1;
821  }
822 
823  // Periodically request the terminal emulator the frame position relative to
824  // the screen. This is useful for converting mouse position reported in
825  // screen's coordinates to frame's coordinates.
826 #if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
827  // Microsoft's terminal suffers from a [bug]. When reporting the cursor
828  // position, several output sequences are mixed together into garbage.
829  // This causes FTXUI user to see some "1;1;R" sequences into the Input
830  // component. See [issue]. Solution is to request cursor position less
831  // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
832  // https://github.com/ArthurSonzogni/FTXUI/issues/136
833  static int i = -3;
834  ++i;
835  if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
836  std::cout << DeviceStatusReport(DSRMode::kCursor);
837  }
838 #else
839  static int i = -3;
840  ++i;
841  if (!use_alternative_screen_ &&
842  (previous_frame_resized_ || i % 40 == 0)) { // NOLINT
843  std::cout << DeviceStatusReport(DSRMode::kCursor);
844  }
845 #endif
846  previous_frame_resized_ = resized;
847 
848  Render(*this, document);
849 
850  // Set cursor position for user using tools to insert CJK characters.
851  {
852  const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
853  const int dy = dimy_ - 1 - cursor_.y;
854 
855  set_cursor_position.clear();
856  reset_cursor_position.clear();
857 
858  if (dy != 0) {
859  set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
860  reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
861  }
862 
863  if (dx != 0) {
864  set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
865  reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
866  }
867 
868  if (cursor_.shape == Cursor::Hidden) {
869  set_cursor_position += "\033[?25l";
870  } else {
871  set_cursor_position += "\033[?25h";
872  set_cursor_position +=
873  "\033[" + std::to_string(int(cursor_.shape)) + " q";
874  }
875  }
876 
877  std::cout << ToString() << set_cursor_position;
878  Flush();
879  Clear();
880  frame_valid_ = true;
881 }
882 
883 // private
884 void ScreenInteractive::ResetCursorPosition() {
885  std::cout << reset_cursor_position;
886  reset_cursor_position = "";
887 }
888 
889 /// @brief Return a function to exit the main loop.
890 /// @ingroup component
892  return [this] { Exit(); };
893 }
894 
895 /// @brief Exit the main loop.
896 /// @ingroup component
898  Post([this] { ExitNow(); });
899 }
900 
901 // private:
902 void ScreenInteractive::ExitNow() {
903  quit_ = true;
904  task_sender_.reset();
905 }
906 
907 // private:
908 void ScreenInteractive::Signal(int signal) {
909  if (signal == SIGABRT) {
910  Exit();
911  return;
912  }
913 
914 // Windows do no support SIGTSTP / SIGWINCH
915 #if !defined(_WIN32)
916  if (signal == SIGTSTP) {
917  Post([&] {
918  ResetCursorPosition();
919  std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
920  Uninstall();
921  dimx_ = 0;
922  dimy_ = 0;
923  Flush();
924  std::ignore = std::raise(SIGTSTP);
925  Install();
926  });
927  return;
928  }
929 
930  if (signal == SIGWINCH) {
931  Post(Event::Special({0}));
932  return;
933  }
934 #endif
935 }
936 
937 } // namespace ftxui.
int dimy() const
Definition: image.hpp:35
int dimx_
Definition: image.hpp:43
int dimy_
Definition: image.hpp:44
int dimx() const
Definition: image.hpp:34
std::vector< std::vector< Pixel > > pixels_
Definition: image.hpp:45
bool HasQuitted()
Whether the loop has quitted.
Definition: loop.cpp:32
static void Signal(ScreenInteractive &s, int signal)
static ScreenInteractive TerminalOutput()
void Exit()
Exit the main loop.
static ScreenInteractive FixedSize(int dimx, int dimy)
void PostEvent(Event event)
Add an event to the main loop. It will be executed later, after every other scheduled events.
void Post(Task task)
Add a task to the main loop. It will be executed later, after every other scheduled tasks.
static ScreenInteractive FitComponent()
static ScreenInteractive Fullscreen()
static ScreenInteractive FullscreenPrimaryScreen()
static ScreenInteractive * Active()
Return the currently active screen, or null if none.
CapturedMouse CaptureMouse()
Try to get the unique lock about behing able to capture the mouse.
static ScreenInteractive FullscreenAlternateScreen()
void TrackMouse(bool enable=true)
Set whether mouse is tracked and events reported. called outside of the main loop....
void RequestAnimationFrame()
Add a task to draw the screen one more time, until all the animations are done.
Closure ExitLoopClosure()
Return a function to exit the main loop.
Closure WithRestoredIO(Closure)
Decorate a function. It executes the same way, but with the currently active screen terminal hooks te...
std::string ToString() const
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:463
Cursor cursor_
Definition: screen.hpp:72
void Clear()
Clear all the pixel from the screen.
Definition: screen.cpp:482
Dimensions Fixed(int)
Definition: screen.cpp:367
void SetFallbackSize(const Dimensions &fallbackSize)
Override terminal size in case auto-detection fails.
Definition: terminal.cpp:124
Dimensions Size()
Get the terminal size.
Definition: terminal.cpp:94
std::chrono::duration< float > Duration
Definition: animation.hpp:24
std::chrono::time_point< Clock > TimePoint
Definition: animation.hpp:23
std::unique_ptr< CapturedMouseInterface > CapturedMouse
std::shared_ptr< ComponentBase > Component
std::string to_string(const std::wstring &s)
Convert a UTF8 std::string into a std::wstring.
Definition: string.cpp:1565
Element select(Element)
Set the child to be the one selected among its siblings.
Definition: frame.cpp:150
std::variant< Event, Closure, AnimationTask > Task
Definition: task.hpp:14
void Render(Screen &screen, const Element &element)
Display an element on a ftxui::Screen.
Definition: node.cpp:47
std::function< void()> Closure
Definition: task.hpp:13
Represent an event. It can be key press event, a terminal resize, or more ...
Definition: event.hpp:29
static Event Special(std::string)
An custom event whose meaning is defined by the user of the library.
Definition: event.cpp:66