FTXUI  0.8.1
C++ functional terminal UI.
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Pages
screen_interactive.cpp
Go to the documentation of this file.
1 #include <stdio.h> // for fileno, stdin
2 #include <algorithm> // for copy, max, min
3 #include <csignal> // for signal, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGWINCH
4 #include <cstdlib> // for NULL
5 #include <initializer_list> // for initializer_list
6 #include <iostream> // for cout, ostream, basic_ostream, operator<<, endl, flush
7 #include <stack> // for stack
8 #include <thread> // for thread
9 #include <utility> // for move
10 #include <vector> // for vector
11 
12 #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
13 #include "ftxui/component/component_base.hpp" // for ComponentBase
14 #include "ftxui/component/event.hpp" // for Event
15 #include "ftxui/component/mouse.hpp" // for Mouse
16 #include "ftxui/component/receiver.hpp" // for ReceiverImpl, MakeReceiver, Sender, SenderImpl, Receiver
18 #include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
19 #include "ftxui/dom/node.hpp" // for Node, Render
20 #include "ftxui/dom/requirement.hpp" // for Requirement
21 #include "ftxui/screen/terminal.hpp" // for Terminal::Dimensions, Terminal
22 
23 #if defined(_WIN32)
24 #define DEFINE_CONSOLEV2_PROPERTIES
25 #define WIN32_LEAN_AND_MEAN
26 #ifndef NOMINMAX
27 #define NOMINMAX
28 #endif
29 #include <Windows.h>
30 #ifndef UNICODE
31 #error Must be compiled in UNICODE mode
32 #endif
33 #else
34 #include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set
35 #include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
36 #include <unistd.h> // for STDIN_FILENO, read
37 #endif
38 
39 // Quick exit is missing in standard CLang headers
40 #if defined(__clang__) && defined(__APPLE__)
41 #define quick_exit(a) exit(a)
42 #endif
43 
44 namespace ftxui {
45 
46 namespace {
47 
48 void Flush() {
49  // Emscripten doesn't implement flush. We interpret zero as flush.
50  std::cout << '\0' << std::flush;
51 }
52 
53 constexpr int timeout_milliseconds = 20;
54 constexpr int timeout_microseconds = timeout_milliseconds * 1000;
55 #if defined(_WIN32)
56 
57 void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
58  auto console = GetStdHandle(STD_INPUT_HANDLE);
59  auto parser = TerminalInputParser(out->Clone());
60  while (!*quit) {
61  // Throttle ReadConsoleInput by waiting 250ms, this wait function will
62  // return if there is input in the console.
63  auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
64  if (wait_result == WAIT_TIMEOUT) {
65  parser.Timeout(timeout_milliseconds);
66  continue;
67  }
68 
69  DWORD number_of_events = 0;
70  if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
71  continue;
72  if (number_of_events <= 0)
73  continue;
74 
75  std::vector<INPUT_RECORD> records{number_of_events};
76  DWORD number_of_events_read = 0;
77  ReadConsoleInput(console, records.data(), (DWORD)records.size(),
78  &number_of_events_read);
79  records.resize(number_of_events_read);
80 
81  for (const auto& r : records) {
82  switch (r.EventType) {
83  case KEY_EVENT: {
84  auto key_event = r.Event.KeyEvent;
85  // ignore UP key events
86  if (key_event.bKeyDown == FALSE)
87  continue;
88  parser.Add((char)key_event.uChar.UnicodeChar);
89  } break;
90  case WINDOW_BUFFER_SIZE_EVENT:
91  out->Send(Event::Special({0}));
92  break;
93  case MENU_EVENT:
94  case FOCUS_EVENT:
95  case MOUSE_EVENT:
96  // TODO(mauve): Implement later.
97  break;
98  }
99  }
100  }
101 }
102 
103 #elif defined(__EMSCRIPTEN__)
104 #include <emscripten.h>
105 
106 // Read char from the terminal.
107 void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
108  (void)timeout_microseconds;
109  auto parser = TerminalInputParser(std::move(out));
110 
111  char c;
112  while (!*quit) {
113  while (read(STDIN_FILENO, &c, 1), c)
114  parser.Add(c);
115 
116  emscripten_sleep(1);
117  parser.Timeout(1);
118  }
119 }
120 
121 #else
122 #include <sys/time.h> // for timeval
123 
124 int CheckStdinReady(int usec_timeout) {
125  timeval tv = {0, usec_timeout};
126  fd_set fds;
127  FD_ZERO(&fds);
128  FD_SET(STDIN_FILENO, &fds);
129  select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv);
130  return FD_ISSET(STDIN_FILENO, &fds);
131 }
132 
133 // Read char from the terminal.
134 void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
135  const int buffer_size = 100;
136 
137  auto parser = TerminalInputParser(std::move(out));
138 
139  while (!*quit) {
140  if (!CheckStdinReady(timeout_microseconds)) {
141  parser.Timeout(timeout_milliseconds);
142  continue;
143  }
144 
145  char buff[buffer_size];
146  int l = read(fileno(stdin), buff, buffer_size);
147  for (int i = 0; i < l; ++i)
148  parser.Add(buff[i]);
149  }
150 }
151 
152 #endif
153 
154 const std::string CSI = "\x1b[";
155 
156 // DEC: Digital Equipment Corporation
157 enum class DECMode {
158  kLineWrap = 7,
159  kMouseX10 = 9,
160  kCursor = 25,
161  kMouseVt200 = 1000,
162  kMouseAnyEvent = 1003,
163  kMouseUtf8 = 1005,
164  kMouseSgrExtMode = 1006,
165  kMouseUrxvtMode = 1015,
166  kMouseSgrPixelsMode = 1016,
167  kAlternateScreen = 1049,
168 };
169 
170 // Device Status Report (DSR) {
171 enum class DSRMode {
172  kCursor = 6,
173 };
174 
175 const std::string Serialize(std::vector<DECMode> parameters) {
176  bool first = true;
177  std::string out;
178  for (DECMode parameter : parameters) {
179  if (!first)
180  out += ";";
181  out += std::to_string(int(parameter));
182  first = false;
183  }
184  return out;
185 }
186 
187 // DEC Private Mode Set (DECSET)
188 const std::string Set(std::vector<DECMode> parameters) {
189  return CSI + "?" + Serialize(parameters) + "h";
190 }
191 
192 // DEC Private Mode Reset (DECRST)
193 const std::string Reset(std::vector<DECMode> parameters) {
194  return CSI + "?" + Serialize(parameters) + "l";
195 }
196 
197 // Device Status Report (DSR)
198 const std::string DeviceStatusReport(DSRMode ps) {
199  return CSI + std::to_string(int(ps)) + "n";
200 }
201 
202 using SignalHandler = void(int);
203 std::stack<std::function<void()>> on_exit_functions;
204 void OnExit(int signal) {
205  (void)signal;
206  while (!on_exit_functions.empty()) {
207  on_exit_functions.top()();
208  on_exit_functions.pop();
209  }
210 }
211 
212 auto install_signal_handler = [](int sig, SignalHandler handler) {
213  auto old_signal_handler = std::signal(sig, handler);
214  on_exit_functions.push([&]() { std::signal(sig, old_signal_handler); });
215 };
216 
217 std::function<void()> on_resize = [] {};
218 void OnResize(int /* signal */) {
219  on_resize();
220 }
221 
222 class CapturedMouseImpl : public CapturedMouseInterface {
223  public:
224  CapturedMouseImpl(std::function<void(void)> callback) : callback_(callback) {}
225  ~CapturedMouseImpl() override { callback_(); }
226 
227  private:
228  std::function<void(void)> callback_;
229 };
230 
231 } // namespace
232 
233 ScreenInteractive::ScreenInteractive(int dimx,
234  int dimy,
235  Dimension dimension,
236  bool use_alternative_screen)
237  : Screen(dimx, dimy),
238  dimension_(dimension),
239  use_alternative_screen_(use_alternative_screen) {
240  event_receiver_ = MakeReceiver<Event>();
241  event_sender_ = event_receiver_->MakeSender();
242 }
243 
244 // static
245 ScreenInteractive ScreenInteractive::FixedSize(int dimx, int dimy) {
246  return ScreenInteractive(dimx, dimy, Dimension::Fixed, false);
247 }
248 
249 // static
250 ScreenInteractive ScreenInteractive::Fullscreen() {
251  return ScreenInteractive(0, 0, Dimension::Fullscreen, true);
252 }
253 
254 // static
255 ScreenInteractive ScreenInteractive::TerminalOutput() {
256  return ScreenInteractive(0, 0, Dimension::TerminalOutput, false);
257 }
258 
259 // static
260 ScreenInteractive ScreenInteractive::FitComponent() {
261  return ScreenInteractive(0, 0, Dimension::FitComponent, false);
262 }
263 
264 void ScreenInteractive::PostEvent(Event event) {
265  if (!quit_)
266  event_sender_->Send(event);
267 }
268 
269 CapturedMouse ScreenInteractive::CaptureMouse() {
270  if (mouse_captured)
271  return nullptr;
272  mouse_captured = true;
273  return std::make_unique<CapturedMouseImpl>(
274  [this] { mouse_captured = false; });
275 }
276 
277 void ScreenInteractive::Loop(Component component) {
278  static ScreenInteractive* g_active_screen = nullptr;
279 
280  // Suspend previously active screen:
281  if (g_active_screen) {
282  std::swap(suspended_screen_, g_active_screen);
283  std::cout << suspended_screen_->reset_cursor_position
284  << suspended_screen_->ResetPosition(/*clear=*/true);
285  suspended_screen_->dimx_ = 0;
286  suspended_screen_->dimy_ = 0;
287  suspended_screen_->Uninstall();
288  }
289 
290  // This screen is now active:
291  g_active_screen = this;
292  g_active_screen->Install();
293  g_active_screen->Main(component);
294  g_active_screen->Uninstall();
295  g_active_screen = nullptr;
296 
297  // Put cursor position at the end of the drawing.
298  std::cout << reset_cursor_position;
299 
300  // Restore suspended screen.
301  if (suspended_screen_) {
302  std::cout << ResetPosition(/*clear=*/true);
303  dimx_ = 0;
304  dimy_ = 0;
305  std::swap(g_active_screen, suspended_screen_);
306  g_active_screen->Install();
307  } else {
308  // On final exit, keep the current drawing and reset cursor position one
309  // line after it.
310  std::cout << std::endl;
311  }
312 }
313 
314 void ScreenInteractive::Install() {
315  on_exit_functions.push([this] { ExitLoopClosure()(); });
316 
317  // Install signal handlers to restore the terminal state on exit. The default
318  // signal handlers are restored on exit.
319  for (int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE})
320  install_signal_handler(signal, OnExit);
321 
322  // Save the old terminal configuration and restore it on exit.
323 #if defined(_WIN32)
324  // Enable VT processing on stdout and stdin
325  auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
326  auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
327 
328  DWORD out_mode = 0;
329  DWORD in_mode = 0;
330  GetConsoleMode(stdout_handle, &out_mode);
331  GetConsoleMode(stdin_handle, &in_mode);
332  on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
333  on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
334 
335  // https://docs.microsoft.com/en-us/windows/console/setconsolemode
336  const int enable_virtual_terminal_processing = 0x0004;
337  const int disable_newline_auto_return = 0x0008;
338  out_mode |= enable_virtual_terminal_processing;
339  out_mode |= disable_newline_auto_return;
340 
341  // https://docs.microsoft.com/en-us/windows/console/setconsolemode
342  const int enable_line_input = 0x0002;
343  const int enable_echo_input = 0x0004;
344  const int enable_virtual_terminal_input = 0x0200;
345  const int enable_window_input = 0x0008;
346  in_mode &= ~enable_echo_input;
347  in_mode &= ~enable_line_input;
348  in_mode |= enable_virtual_terminal_input;
349  in_mode |= enable_window_input;
350 
351  SetConsoleMode(stdin_handle, in_mode);
352  SetConsoleMode(stdout_handle, out_mode);
353 #else
354  struct termios terminal;
355  tcgetattr(STDIN_FILENO, &terminal);
356  on_exit_functions.push([=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
357 
358  terminal.c_lflag &= ~ICANON; // Non canonique terminal.
359  terminal.c_lflag &= ~ECHO; // Do not print after a key press.
360  terminal.c_cc[VMIN] = 0;
361  terminal.c_cc[VTIME] = 0;
362  // auto oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
363  // fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
364  // on_exit_functions.push([=] { fcntl(STDIN_FILENO, F_GETFL, oldf); });
365 
366  tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
367 
368  // Handle resize.
369  on_resize = [&] { event_sender_->Send(Event::Special({0})); };
370  install_signal_handler(SIGWINCH, OnResize);
371 #endif
372 
373  // Commit state:
374  auto flush = [&] {
375  Flush();
376  on_exit_functions.push([] { Flush(); });
377  };
378 
379  auto enable = [&](std::vector<DECMode> parameters) {
380  std::cout << Set(parameters);
381  on_exit_functions.push([=] { std::cout << Reset(parameters); });
382  };
383 
384  auto disable = [&](std::vector<DECMode> parameters) {
385  std::cout << Reset(parameters);
386  on_exit_functions.push([=] { std::cout << Set(parameters); });
387  };
388 
389  if (use_alternative_screen_) {
390  enable({
391  DECMode::kAlternateScreen,
392  });
393  }
394 
395  disable({
396  DECMode::kCursor,
397  DECMode::kLineWrap,
398  });
399 
400  enable({
401  // DECMode::kMouseVt200,
402  DECMode::kMouseAnyEvent,
403  DECMode::kMouseUtf8,
404  DECMode::kMouseSgrExtMode,
405  });
406 
407  flush();
408 
409  quit_ = false;
410  event_listener_ =
411  std::thread(&EventListener, &quit_, event_receiver_->MakeSender());
412 }
413 
414 void ScreenInteractive::Uninstall() {
415  ExitLoopClosure()();
416  event_listener_.join();
417 
418  OnExit(0);
419 }
420 
421 void ScreenInteractive::Main(Component component) {
422  while (!quit_) {
423  if (!event_receiver_->HasPending()) {
424  Draw(component);
425  std::cout << ToString() << set_cursor_position;
426  Flush();
427  Clear();
428  }
429 
430  Event event;
431  if (!event_receiver_->Receive(&event))
432  break;
433 
434  if (event.is_cursor_reporting()) {
435  cursor_x_ = event.cursor_x();
436  cursor_y_ = event.cursor_y();
437  continue;
438  }
439 
440  if (event.is_mouse()) {
441  event.mouse().x -= cursor_x_;
442  event.mouse().y -= cursor_y_;
443  }
444 
445  event.screen_ = this;
446  component->OnEvent(event);
447  }
448 }
449 
450 void ScreenInteractive::Draw(Component component) {
451  auto document = component->Render();
452  int dimx = 0;
453  int dimy = 0;
454  switch (dimension_) {
455  case Dimension::Fixed:
456  dimx = dimx_;
457  dimy = dimy_;
458  break;
459  case Dimension::TerminalOutput:
460  document->ComputeRequirement();
461  dimx = Terminal::Size().dimx;
462  dimy = document->requirement().min_y;
463  break;
464  case Dimension::Fullscreen:
465  dimx = Terminal::Size().dimx;
466  dimy = Terminal::Size().dimy;
467  break;
468  case Dimension::FitComponent:
469  auto terminal = Terminal::Size();
470  document->ComputeRequirement();
471  dimx = std::min(document->requirement().min_x, terminal.dimx);
472  dimy = std::min(document->requirement().min_y, terminal.dimy);
473  break;
474  }
475 
476  bool resized = (dimx != dimx_) || (dimy != dimy_);
477  std::cout << reset_cursor_position << ResetPosition(/*clear=*/resized);
478 
479  // Resize the screen if needed.
480  if (resized) {
481  dimx_ = dimx;
482  dimy_ = dimy;
483  pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
484  cursor_.x = dimx_ - 1;
485  cursor_.y = dimy_ - 1;
486  }
487 
488  // Periodically request the terminal emulator the frame position relative to
489  // the screen. This is useful for converting mouse position reported in
490  // screen's coordinates to frame's coordinates.
491  static constexpr int cursor_refresh_rate =
492 #if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
493  // Microsoft's terminal suffers from a [bug]. When reporting the cursor
494  // position, several output sequences are mixed together into garbage.
495  // This causes FTXUI user to see some "1;1;R" sequences into the Input
496  // component. See [issue]. Solution is to request cursor position less
497  // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
498  // https://github.com/ArthurSonzogni/FTXUI/issues/136
499  150;
500 #else
501  20;
502 #endif
503  static int i = -3;
504  ++i;
505  if (!use_alternative_screen_ && (i % cursor_refresh_rate == 0))
506  std::cout << DeviceStatusReport(DSRMode::kCursor);
507 
508  Render(*this, document);
509 
510  // Set cursor position for user using tools to insert CJK characters.
511  set_cursor_position = "";
512  reset_cursor_position = "";
513 
514  int dx = dimx_ - 1 - cursor_.x;
515  int dy = dimy_ - 1 - cursor_.y;
516 
517  if (dx != 0) {
518  set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
519  reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
520  }
521  if (dy != 0) {
522  set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
523  reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
524  }
525 }
526 
527 std::function<void()> ScreenInteractive::ExitLoopClosure() {
528  return [this]() {
529  quit_ = true;
530  event_sender_.reset();
531  };
532 }
533 
534 } // namespace ftxui.
535 
536 // Copyright 2020 Arthur Sonzogni. All rights reserved.
537 // Use of this source code is governed by the MIT license that can be found in
538 // the LICENSE file.
terminal_input_parser.hpp
screen_interactive.hpp
ftxui
Definition: captured_mouse.hpp:6
node.hpp
ftxui::to_string
std::string to_string(const std::wstring &s)
Convert a UTF8 std::string into a std::wstring.
Definition: string.cpp:297
ftxui::Component
std::shared_ptr< ComponentBase > Component
Definition: component_base.hpp:17
event.hpp
requirement.hpp
ftxui::ScreenInteractive
Definition: screen_interactive.hpp:21
terminal.hpp
ftxui::select
Element select(Element)
Definition: frame.cpp:38
captured_mouse.hpp
receiver.hpp
component_base.hpp
ftxui::Dimensions::dimx
int dimx
Definition: terminal.hpp:6
ftxui::Dimension::Fixed
Dimensions Fixed(int)
Definition: screen.cpp:96
ftxui::Terminal::Size
Dimensions Size()
Definition: terminal.cpp:21
ftxui::CapturedMouse
std::unique_ptr< CapturedMouseInterface > CapturedMouse
Definition: captured_mouse.hpp:11
mouse.hpp
ftxui::Render
void Render(Screen &screen, const Element &node)
Display an element on a ftxui::Screen.
Definition: node.cpp:34
ftxui::Dimensions::dimy
int dimy
Definition: terminal.hpp:7
ftxui::Event
Represent an event. It can be key press event, a terminal resize, or more ...
Definition: event.hpp:25
ftxui::Event::Special
static Event Special(std::string)
Definition: event.cpp:37