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