FTXUI  5.0.0
C++ functional terminal UI.
screen.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 <cstdint> // for size_t
5 #include <iostream> // for operator<<, stringstream, basic_ostream, flush, cout, ostream
6 #include <limits>
7 #include <map> // for _Rb_tree_const_iterator, map, operator!=, operator==
8 #include <memory> // for allocator, allocator_traits<>::value_type
9 #include <sstream> // IWYU pragma: keep
10 #include <utility> // for pair
11 
12 #include "ftxui/screen/screen.hpp"
13 #include "ftxui/screen/string.hpp" // for string_width
14 #include "ftxui/screen/terminal.hpp" // for Dimensions, Size
15 
16 #if defined(_WIN32)
17 #define WIN32_LEAN_AND_MEAN
18 #ifndef NOMINMAX
19 #define NOMINMAX
20 #endif
21 #include <windows.h>
22 #endif
23 
24 // Macro for hinting that an expression is likely to be false.
25 #if !defined(FTXUI_UNLIKELY)
26 #if defined(COMPILER_GCC) || defined(__clang__)
27 #define FTXUI_UNLIKELY(x) __builtin_expect(!!(x), 0)
28 #else
29 #define FTXUI_UNLIKELY(x) (x)
30 #endif // defined(COMPILER_GCC)
31 #endif // !defined(FTXUI_UNLIKELY)
32 
33 #if !defined(FTXUI_LIKELY)
34 #if defined(COMPILER_GCC) || defined(__clang__)
35 #define FTXUI_LIKELY(x) __builtin_expect(!!(x), 1)
36 #else
37 #define FTXUI_LIKELY(x) (x)
38 #endif // defined(COMPILER_GCC)
39 #endif // !defined(FTXUI_LIKELY)
40 
41 namespace ftxui {
42 
43 namespace {
44 
45 Pixel& dev_null_pixel() {
46  static Pixel pixel;
47  return pixel;
48 }
49 
50 #if defined(_WIN32)
51 void WindowsEmulateVT100Terminal() {
52  static bool done = false;
53  if (done)
54  return;
55  done = true;
56 
57  // Enable VT processing on stdout and stdin
58  auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
59 
60  DWORD out_mode = 0;
61  GetConsoleMode(stdout_handle, &out_mode);
62 
63  // https://docs.microsoft.com/en-us/windows/console/setconsolemode
64  const int enable_virtual_terminal_processing = 0x0004;
65  const int disable_newline_auto_return = 0x0008;
66  out_mode |= enable_virtual_terminal_processing;
67  out_mode |= disable_newline_auto_return;
68 
69  SetConsoleMode(stdout_handle, out_mode);
70 }
71 #endif
72 
73 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
74 void UpdatePixelStyle(const Screen* screen,
75  std::stringstream& ss,
76  const Pixel& prev,
77  const Pixel& next) {
78  // See https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
79  if (FTXUI_UNLIKELY(next.hyperlink != prev.hyperlink)) {
80  ss << "\x1B]8;;" << screen->Hyperlink(next.hyperlink) << "\x1B\\";
81  }
82 
83  // Bold
84  if (FTXUI_UNLIKELY((next.bold ^ prev.bold) | (next.dim ^ prev.dim))) {
85  // BOLD_AND_DIM_RESET:
86  ss << ((prev.bold && !next.bold) || (prev.dim && !next.dim) ? "\x1B[22m"
87  : "");
88  ss << (next.bold ? "\x1B[1m" : ""); // BOLD_SET
89  ss << (next.dim ? "\x1B[2m" : ""); // DIM_SET
90  }
91 
92  // Underline
93  if (FTXUI_UNLIKELY(next.underlined != prev.underlined ||
94  next.underlined_double != prev.underlined_double)) {
95  ss << (next.underlined ? "\x1B[4m" // UNDERLINE
96  : next.underlined_double ? "\x1B[21m" // UNDERLINE_DOUBLE
97  : "\x1B[24m"); // UNDERLINE_RESET
98  }
99 
100  // Blink
101  if (FTXUI_UNLIKELY(next.blink != prev.blink)) {
102  ss << (next.blink ? "\x1B[5m" // BLINK_SET
103  : "\x1B[25m"); // BLINK_RESET
104  }
105 
106  // Inverted
107  if (FTXUI_UNLIKELY(next.inverted != prev.inverted)) {
108  ss << (next.inverted ? "\x1B[7m" // INVERTED_SET
109  : "\x1B[27m"); // INVERTED_RESET
110  }
111 
112  // StrikeThrough
113  if (FTXUI_UNLIKELY(next.strikethrough != prev.strikethrough)) {
114  ss << (next.strikethrough ? "\x1B[9m" // CROSSED_OUT
115  : "\x1B[29m"); // CROSSED_OUT_RESET
116  }
117 
118  if (FTXUI_UNLIKELY(next.foreground_color != prev.foreground_color ||
119  next.background_color != prev.background_color)) {
120  ss << "\x1B[" + next.foreground_color.Print(false) + "m";
121  ss << "\x1B[" + next.background_color.Print(true) + "m";
122  }
123 }
124 
125 struct TileEncoding {
126  uint8_t left : 2;
127  uint8_t top : 2;
128  uint8_t right : 2;
129  uint8_t down : 2;
130  uint8_t round : 1;
131 
132  // clang-format off
133  bool operator<(const TileEncoding& other) const {
134  if (left < other.left) { return true; }
135  if (left > other.left) { return false; }
136  if (top < other.top) { return true; }
137  if (top > other.top) { return false; }
138  if (right < other.right) { return true; }
139  if (right > other.right) { return false; }
140  if (down < other.down) { return true; }
141  if (down > other.down) { return false; }
142  if (round < other.round) { return true; }
143  if (round > other.round) { return false; }
144  return false;
145  }
146  // clang-format on
147 };
148 
149 // clang-format off
150 const std::map<std::string, TileEncoding> tile_encoding = { // NOLINT
151  {"─", {1, 0, 1, 0, 0}},
152  {"━", {2, 0, 2, 0, 0}},
153  {"╍", {2, 0, 2, 0, 0}},
154 
155  {"│", {0, 1, 0, 1, 0}},
156  {"┃", {0, 2, 0, 2, 0}},
157  {"╏", {0, 2, 0, 2, 0}},
158 
159  {"┌", {0, 0, 1, 1, 0}},
160  {"┍", {0, 0, 2, 1, 0}},
161  {"┎", {0, 0, 1, 2, 0}},
162  {"┏", {0, 0, 2, 2, 0}},
163 
164  {"┐", {1, 0, 0, 1, 0}},
165  {"┑", {2, 0, 0, 1, 0}},
166  {"┒", {1, 0, 0, 2, 0}},
167  {"┓", {2, 0, 0, 2, 0}},
168 
169  {"└", {0, 1, 1, 0, 0}},
170  {"┕", {0, 1, 2, 0, 0}},
171  {"┖", {0, 2, 1, 0, 0}},
172  {"┗", {0, 2, 2, 0, 0}},
173 
174  {"┘", {1, 1, 0, 0, 0}},
175  {"┙", {2, 1, 0, 0, 0}},
176  {"┚", {1, 2, 0, 0, 0}},
177  {"┛", {2, 2, 0, 0, 0}},
178 
179  {"├", {0, 1, 1, 1, 0}},
180  {"┝", {0, 1, 2, 1, 0}},
181  {"┞", {0, 2, 1, 1, 0}},
182  {"┟", {0, 1, 1, 2, 0}},
183  {"┠", {0, 2, 1, 2, 0}},
184  {"┡", {0, 2, 2, 1, 0}},
185  {"┢", {0, 1, 2, 2, 0}},
186  {"┣", {0, 2, 2, 2, 0}},
187 
188  {"┤", {1, 1, 0, 1, 0}},
189  {"┥", {2, 1, 0, 1, 0}},
190  {"┦", {1, 2, 0, 1, 0}},
191  {"┧", {1, 1, 0, 2, 0}},
192  {"┨", {1, 2, 0, 2, 0}},
193  {"┩", {2, 2, 0, 1, 0}},
194  {"┪", {2, 1, 0, 2, 0}},
195  {"┫", {2, 2, 0, 2, 0}},
196 
197  {"┬", {1, 0, 1, 1, 0}},
198  {"┭", {2, 0, 1, 1, 0}},
199  {"┮", {1, 0, 2, 1, 0}},
200  {"┯", {2, 0, 2, 1, 0}},
201  {"┰", {1, 0, 1, 2, 0}},
202  {"┱", {2, 0, 1, 2, 0}},
203  {"┲", {1, 0, 2, 2, 0}},
204  {"┳", {2, 0, 2, 2, 0}},
205 
206  {"┴", {1, 1, 1, 0, 0}},
207  {"┵", {2, 1, 1, 0, 0}},
208  {"┶", {1, 1, 2, 0, 0}},
209  {"┷", {2, 1, 2, 0, 0}},
210  {"┸", {1, 2, 1, 0, 0}},
211  {"┹", {2, 2, 1, 0, 0}},
212  {"┺", {1, 2, 2, 0, 0}},
213  {"┻", {2, 2, 2, 0, 0}},
214 
215  {"┼", {1, 1, 1, 1, 0}},
216  {"┽", {2, 1, 1, 1, 0}},
217  {"┾", {1, 1, 2, 1, 0}},
218  {"┿", {2, 1, 2, 1, 0}},
219  {"╀", {1, 2, 1, 1, 0}},
220  {"╁", {1, 1, 1, 2, 0}},
221  {"╂", {1, 2, 1, 2, 0}},
222  {"╃", {2, 2, 1, 1, 0}},
223  {"╄", {1, 2, 2, 1, 0}},
224  {"╅", {2, 1, 1, 2, 0}},
225  {"╆", {1, 1, 2, 2, 0}},
226  {"╇", {2, 2, 2, 1, 0}},
227  {"╈", {2, 1, 2, 2, 0}},
228  {"╉", {2, 2, 1, 2, 0}},
229  {"╊", {1, 2, 2, 2, 0}},
230  {"╋", {2, 2, 2, 2, 0}},
231 
232  {"═", {3, 0, 3, 0, 0}},
233  {"║", {0, 3, 0, 3, 0}},
234 
235  {"╒", {0, 0, 3, 1, 0}},
236  {"╓", {0, 0, 1, 3, 0}},
237  {"╔", {0, 0, 3, 3, 0}},
238 
239  {"╕", {3, 0, 0, 1, 0}},
240  {"╖", {1, 0, 0, 3, 0}},
241  {"╗", {3, 0, 0, 3, 0}},
242 
243  {"╘", {0, 1, 3, 0, 0}},
244  {"╙", {0, 3, 1, 0, 0}},
245  {"╚", {0, 3, 3, 0, 0}},
246 
247  {"╛", {3, 1, 0, 0, 0}},
248  {"╜", {1, 3, 0, 0, 0}},
249  {"╝", {3, 3, 0, 0, 0}},
250 
251  {"╞", {0, 1, 3, 1, 0}},
252  {"╟", {0, 3, 1, 3, 0}},
253  {"╠", {0, 3, 3, 3, 0}},
254 
255  {"╡", {3, 1, 0, 1, 0}},
256  {"╢", {1, 3, 0, 3, 0}},
257  {"╣", {3, 3, 0, 3, 0}},
258 
259  {"╤", {3, 0, 3, 1, 0}},
260  {"╥", {1, 0, 1, 3, 0}},
261  {"╦", {3, 0, 3, 3, 0}},
262 
263  {"╧", {3, 1, 3, 0, 0}},
264  {"╨", {1, 3, 1, 0, 0}},
265  {"╩", {3, 3, 3, 0, 0}},
266 
267  {"╪", {3, 1, 3, 1, 0}},
268  {"╫", {1, 3, 1, 3, 0}},
269  {"╬", {3, 3, 3, 3, 0}},
270 
271  {"╭", {0, 0, 1, 1, 1}},
272  {"╮", {1, 0, 0, 1, 1}},
273  {"╯", {1, 1, 0, 0, 1}},
274  {"╰", {0, 1, 1, 0, 1}},
275 
276  {"╴", {1, 0, 0, 0, 0}},
277  {"╵", {0, 1, 0, 0, 0}},
278  {"╶", {0, 0, 1, 0, 0}},
279  {"╷", {0, 0, 0, 1, 0}},
280 
281  {"╸", {2, 0, 0, 0, 0}},
282  {"╹", {0, 2, 0, 0, 0}},
283  {"╺", {0, 0, 2, 0, 0}},
284  {"╻", {0, 0, 0, 2, 0}},
285 
286  {"╼", {1, 0, 2, 0, 0}},
287  {"╽", {0, 1, 0, 2, 0}},
288  {"╾", {2, 0, 1, 0, 0}},
289  {"╿", {0, 2, 0, 1, 0}},
290 };
291 // clang-format on
292 
293 template <class A, class B>
294 std::map<B, A> InvertMap(const std::map<A, B> input) {
295  std::map<B, A> output;
296  for (const auto& it : input) {
297  output[it.second] = it.first;
298  }
299  return output;
300 }
301 
302 const std::map<TileEncoding, std::string> tile_encoding_inverse = // NOLINT
303  InvertMap(tile_encoding);
304 
305 void UpgradeLeftRight(std::string& left, std::string& right) {
306  const auto it_left = tile_encoding.find(left);
307  if (it_left == tile_encoding.end()) {
308  return;
309  }
310  const auto it_right = tile_encoding.find(right);
311  if (it_right == tile_encoding.end()) {
312  return;
313  }
314 
315  if (it_left->second.right == 0 && it_right->second.left != 0) {
316  TileEncoding encoding_left = it_left->second;
317  encoding_left.right = it_right->second.left;
318  const auto it_left_upgrade = tile_encoding_inverse.find(encoding_left);
319  if (it_left_upgrade != tile_encoding_inverse.end()) {
320  left = it_left_upgrade->second;
321  }
322  }
323 
324  if (it_right->second.left == 0 && it_left->second.right != 0) {
325  TileEncoding encoding_right = it_right->second;
326  encoding_right.left = it_left->second.right;
327  const auto it_right_upgrade = tile_encoding_inverse.find(encoding_right);
328  if (it_right_upgrade != tile_encoding_inverse.end()) {
329  right = it_right_upgrade->second;
330  }
331  }
332 }
333 
334 void UpgradeTopDown(std::string& top, std::string& down) {
335  const auto it_top = tile_encoding.find(top);
336  if (it_top == tile_encoding.end()) {
337  return;
338  }
339  const auto it_down = tile_encoding.find(down);
340  if (it_down == tile_encoding.end()) {
341  return;
342  }
343 
344  if (it_top->second.down == 0 && it_down->second.top != 0) {
345  TileEncoding encoding_top = it_top->second;
346  encoding_top.down = it_down->second.top;
347  const auto it_top_down = tile_encoding_inverse.find(encoding_top);
348  if (it_top_down != tile_encoding_inverse.end()) {
349  top = it_top_down->second;
350  }
351  }
352 
353  if (it_down->second.top == 0 && it_top->second.down != 0) {
354  TileEncoding encoding_down = it_down->second;
355  encoding_down.top = it_top->second.down;
356  const auto it_down_top = tile_encoding_inverse.find(encoding_down);
357  if (it_down_top != tile_encoding_inverse.end()) {
358  down = it_down_top->second;
359  }
360  }
361 }
362 
363 bool ShouldAttemptAutoMerge(Pixel& pixel) {
364  return pixel.automerge && pixel.character.size() == 3;
365 }
366 
367 } // namespace
368 
369 /// A fixed dimension.
370 /// @see Fit
371 /// @see Full
373  return {v, v};
374 }
375 
376 /// Use the terminal dimensions.
377 /// @see Fixed
378 /// @see Fit
380  return Terminal::Size();
381 }
382 
383 // static
384 /// Create a screen with the given dimension along the x-axis and y-axis.
386  return {width.dimx, height.dimy};
387 }
388 
389 // static
390 /// Create a screen with the given dimension.
392  return {dimension.dimx, dimension.dimy};
393 }
394 
395 Screen::Screen(int dimx, int dimy)
396  : stencil{0, dimx - 1, 0, dimy - 1},
397  dimx_(dimx),
398  dimy_(dimy),
399  pixels_(dimy, std::vector<Pixel>(dimx)) {
400 #if defined(_WIN32)
401  // The placement of this call is a bit weird, however we can assume that
402  // anybody who instantiates a Screen object eventually wants to output
403  // something to the console.
404  // As we require UTF8 for all input/output operations we will just switch to
405  // UTF8 encoding here
406  SetConsoleOutputCP(CP_UTF8);
407  SetConsoleCP(CP_UTF8);
408  WindowsEmulateVT100Terminal();
409 #endif
410 }
411 
412 /// Produce a std::string that can be used to print the Screen on the
413 /// terminal.
414 /// @note Don't forget to flush stdout. Alternatively, you can use
415 /// Screen::Print();
416 std::string Screen::ToString() const {
417  std::stringstream ss;
418 
419  const Pixel default_pixel;
420  const Pixel* previous_pixel_ref = &default_pixel;
421 
422  for (int y = 0; y < dimy_; ++y) {
423  // New line in between two lines.
424  if (y != 0) {
425  UpdatePixelStyle(this, ss, *previous_pixel_ref, default_pixel);
426  previous_pixel_ref = &default_pixel;
427  ss << "\r\n";
428  }
429 
430  // After printing a fullwith character, we need to skip the next cell.
431  bool previous_fullwidth = false;
432  for (const auto& pixel : pixels_[y]) {
433  if (!previous_fullwidth) {
434  UpdatePixelStyle(this, ss, *previous_pixel_ref, pixel);
435  previous_pixel_ref = &pixel;
436  ss << pixel.character;
437  }
438  previous_fullwidth = (string_width(pixel.character) == 2);
439  }
440  }
441 
442  // Reset the style to default:
443  UpdatePixelStyle(this, ss, *previous_pixel_ref, default_pixel);
444 
445  return ss.str();
446 }
447 
448 // Print the Screen to the terminal.
449 void Screen::Print() const {
450  std::cout << ToString() << '\0' << std::flush;
451 }
452 
453 /// @brief Access a character in a cell at a given position.
454 /// @param x The cell position along the x-axis.
455 /// @param y The cell position along the y-axis.
456 std::string& Screen::at(int x, int y) {
457  return PixelAt(x, y).character;
458 }
459 
460 /// @brief Access a character in a cell at a given position.
461 /// @param x The cell position along the x-axis.
462 /// @param y The cell position along the y-axis.
463 const std::string& Screen::at(int x, int y) const {
464  return PixelAt(x, y).character;
465 }
466 
467 /// @brief Access a cell (Pixel) at a given position.
468 /// @param x The cell position along the x-axis.
469 /// @param y The cell position along the y-axis.
470 Pixel& Screen::PixelAt(int x, int y) {
471  return stencil.Contain(x, y) ? pixels_[y][x] : dev_null_pixel();
472 }
473 
474 /// @brief Access a cell (Pixel) at a given position.
475 /// @param x The cell position along the x-axis.
476 /// @param y The cell position along the y-axis.
477 const Pixel& Screen::PixelAt(int x, int y) const {
478  return stencil.Contain(x, y) ? pixels_[y][x] : dev_null_pixel();
479 }
480 
481 /// @brief Return a string to be printed in order to reset the cursor position
482 /// to the beginning of the screen.
483 ///
484 /// ```cpp
485 /// std::string reset_position;
486 /// while(true) {
487 /// auto document = render();
488 /// auto screen = Screen::Create(Dimension::Full(), Dimension::Fit(document));
489 /// Render(screen, document);
490 /// std::cout << reset_position << screen.ToString() << std::flush;
491 /// reset_position = screen.ResetPosition();
492 ///
493 /// using namespace std::chrono_literals;
494 /// std::this_thread::sleep_for(0.01s);
495 /// }
496 /// ```
497 ///
498 /// @return The string to print in order to reset the cursor position to the
499 /// beginning.
500 std::string Screen::ResetPosition(bool clear) const {
501  std::stringstream ss;
502  if (clear) {
503  ss << "\r"; // MOVE_LEFT;
504  ss << "\x1b[2K"; // CLEAR_SCREEN;
505  for (int y = 1; y < dimy_; ++y) {
506  ss << "\x1B[1A"; // MOVE_UP;
507  ss << "\x1B[2K"; // CLEAR_LINE;
508  }
509  } else {
510  ss << "\r"; // MOVE_LEFT;
511  for (int y = 1; y < dimy_; ++y) {
512  ss << "\x1B[1A"; // MOVE_UP;
513  }
514  }
515  return ss.str();
516 }
517 
518 /// @brief Clear all the pixel from the screen.
520  for (auto& line : pixels_) {
521  for (auto& cell : line) {
522  cell = Pixel();
523  }
524  }
525  cursor_.x = dimx_ - 1;
526  cursor_.y = dimy_ - 1;
527 
528  hyperlinks_ = {
529  "",
530  };
531 }
532 
533 // clang-format off
535  // Merge box characters togethers.
536  for (int y = 0; y < dimy_; ++y) {
537  for (int x = 0; x < dimx_; ++x) {
538  // Box drawing character uses exactly 3 byte.
539  Pixel& cur = pixels_[y][x];
540  if (!ShouldAttemptAutoMerge(cur)) {
541  continue;
542  }
543 
544  if (x > 0) {
545  Pixel& left = pixels_[y][x-1];
546  if (ShouldAttemptAutoMerge(left)) {
547  UpgradeLeftRight(left.character, cur.character);
548  }
549  }
550  if (y > 0) {
551  Pixel& top = pixels_[y-1][x];
552  if (ShouldAttemptAutoMerge(top)) {
553  UpgradeTopDown(top.character, cur.character);
554  }
555  }
556  }
557  }
558 }
559 // clang-format on
560 
561 uint8_t Screen::RegisterHyperlink(const std::string& link) {
562  for (size_t i = 0; i < hyperlinks_.size(); ++i) {
563  if (hyperlinks_[i] == link) {
564  return i;
565  }
566  }
567  if (hyperlinks_.size() == std::numeric_limits<uint8_t>::max()) {
568  return 0;
569  }
570  hyperlinks_.push_back(link);
571  return hyperlinks_.size() - 1;
572 }
573 
574 const std::string& Screen::Hyperlink(uint8_t id) const {
575  if (id >= hyperlinks_.size()) {
576  return hyperlinks_[0];
577  }
578  return hyperlinks_[id];
579 }
580 
581 } // namespace ftxui
A rectangular grid of Pixel.
Definition: screen.hpp:63
void ApplyShader()
Definition: screen.cpp:534
const std::string & Hyperlink(uint8_t id) const
Definition: screen.cpp:574
std::string ToString() const
Definition: screen.cpp:416
static Screen Create(Dimensions dimension)
Create a screen with the given dimension.
Definition: screen.cpp:391
Pixel & PixelAt(int x, int y)
Access a cell (Pixel) at a given position.
Definition: screen.cpp:470
std::string & at(int x, int y)
Access a character in a cell at a given position.
Definition: screen.cpp:456
Screen(int dimx, int dimy)
Definition: screen.cpp:395
uint8_t RegisterHyperlink(const std::string &link)
Definition: screen.cpp:561
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:500
Cursor cursor_
Definition: screen.hpp:124
void Clear()
Clear all the pixel from the screen.
Definition: screen.cpp:519
std::vector< std::string > hyperlinks_
Definition: screen.hpp:125
void Print() const
Definition: screen.cpp:449
std::vector< std::vector< Pixel > > pixels_
Definition: screen.hpp:123
Dimensions Fixed(int)
Definition: screen.cpp:372
Dimensions Full()
Definition: screen.cpp:379
Dimensions Size()
Get the terminal size.
Definition: terminal.cpp:94
int string_width(const std::string &)
Definition: string.cpp:1329
#define FTXUI_UNLIKELY(x)
Definition: screen.cpp:29
bool Contain(int x, int y) const
Definition: box.cpp:35
A unicode character and its associated style.
Definition: screen.hpp:20
std::string character
Definition: screen.hpp:47