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