FTXUI  2.0.0
C++ functional terminal UI.
Loading...
Searching...
No Matches
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 swap, 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 Size, Dimensions
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, tcgetattr, cc_t
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
44namespace ftxui {
45
46namespace {
47
48void Flush() {
49 // Emscripten doesn't implement flush. We interpret zero as flush.
50 std::cout << '\0' << std::flush;
51}
52
53constexpr int timeout_milliseconds = 20;
54constexpr int timeout_microseconds = timeout_milliseconds * 1000;
55#if defined(_WIN32)
56
57void 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.
107void 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
124int 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.
134void 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
154const std::string CSI = "\x1b[";
155
156// DEC: Digital Equipment Corporation
157enum 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) {
171enum class DSRMode {
172 kCursor = 6,
173};
174
175const 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)
188const std::string Set(std::vector<DECMode> parameters) {
189 return CSI + "?" + Serialize(parameters) + "h";
190}
191
192// DEC Private Mode Reset (DECRST)
193const std::string Reset(std::vector<DECMode> parameters) {
194 return CSI + "?" + Serialize(parameters) + "l";
195}
196
197// Device Status Report (DSR)
198const std::string DeviceStatusReport(DSRMode ps) {
199 return CSI + std::to_string(int(ps)) + "n";
200}
201
202using SignalHandler = void(int);
203std::stack<ScreenInteractive::Callback> on_exit_functions;
204void 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
212auto 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
217ScreenInteractive::Callback on_resize = [] {};
218void OnResize(int /* signal */) {
219 on_resize();
220}
221
222class 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
234
235ScreenInteractive::ScreenInteractive(int dimx,
236 int dimy,
237 Dimension dimension,
238 bool use_alternative_screen)
239 : Screen(dimx, dimy),
240 dimension_(dimension),
241 use_alternative_screen_(use_alternative_screen) {
242 event_receiver_ = MakeReceiver<Event>();
243 event_sender_ = event_receiver_->MakeSender();
244}
245
246// static
247ScreenInteractive ScreenInteractive::FixedSize(int dimx, int dimy) {
248 return ScreenInteractive(dimx, dimy, Dimension::Fixed, false);
249}
250
251// static
252ScreenInteractive ScreenInteractive::Fullscreen() {
253 return ScreenInteractive(0, 0, Dimension::Fullscreen, true);
254}
255
256// static
257ScreenInteractive ScreenInteractive::TerminalOutput() {
258 return ScreenInteractive(0, 0, Dimension::TerminalOutput, false);
259}
260
261// static
262ScreenInteractive ScreenInteractive::FitComponent() {
263 return ScreenInteractive(0, 0, Dimension::FitComponent, false);
264}
265
266void ScreenInteractive::PostEvent(Event event) {
267 if (!quit_)
268 event_sender_->Send(event);
269}
270
271CapturedMouse ScreenInteractive::CaptureMouse() {
272 if (mouse_captured)
273 return nullptr;
274 mouse_captured = true;
275 return std::make_unique<CapturedMouseImpl>(
276 [this] { mouse_captured = false; });
277}
278
279void ScreenInteractive::Loop(Component component) {
280
281 // Suspend previously active screen:
282 if (g_active_screen) {
283 std::swap(suspended_screen_, g_active_screen);
284 std::cout << suspended_screen_->reset_cursor_position
285 << suspended_screen_->ResetPosition(/*clear=*/true);
286 suspended_screen_->dimx_ = 0;
287 suspended_screen_->dimy_ = 0;
288 suspended_screen_->Uninstall();
289 }
290
291 // This screen is now active:
292 g_active_screen = this;
293 g_active_screen->Install();
294 g_active_screen->Main(component);
295 g_active_screen->Uninstall();
296 g_active_screen = nullptr;
297
298 // Put cursor position at the end of the drawing.
299 std::cout << reset_cursor_position;
300
301 // Restore suspended screen.
302 if (suspended_screen_) {
303 std::cout << ResetPosition(/*clear=*/true);
304 dimx_ = 0;
305 dimy_ = 0;
306 std::swap(g_active_screen, suspended_screen_);
307 g_active_screen->Install();
308 } else {
309 // On final exit, keep the current drawing and reset cursor position one
310 // line after it.
311 std::cout << std::endl;
312 }
313}
314
315/// @brief Decorate a function. It executes the same way, but with the currently
316/// active screen terminal hooks temporarilly uninstalled during its execution.
317/// @param fn The function to decorate.
318ScreenInteractive::Callback ScreenInteractive::WithRestoredIO(Callback fn) {
319 return [this, fn] {
320 Uninstall();
321 fn();
322 Install();
323 };
324}
325
326void ScreenInteractive::Install() {
327 // After uninstalling the new configuration, flush it to the terminal to
328 // ensure it is fully applied:
329 on_exit_functions.push([] { Flush(); });
330
331 on_exit_functions.push([this] { ExitLoopClosure()(); });
332
333 // Install signal handlers to restore the terminal state on exit. The default
334 // signal handlers are restored on exit.
335 for (int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE})
336 install_signal_handler(signal, OnExit);
337
338 // Save the old terminal configuration and restore it on exit.
339#if defined(_WIN32)
340 // Enable VT processing on stdout and stdin
341 auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
342 auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
343
344 DWORD out_mode = 0;
345 DWORD in_mode = 0;
346 GetConsoleMode(stdout_handle, &out_mode);
347 GetConsoleMode(stdin_handle, &in_mode);
348 on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
349 on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
350
351 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
352 const int enable_virtual_terminal_processing = 0x0004;
353 const int disable_newline_auto_return = 0x0008;
354 out_mode |= enable_virtual_terminal_processing;
355 out_mode |= disable_newline_auto_return;
356
357 // https://docs.microsoft.com/en-us/windows/console/setconsolemode
358 const int enable_line_input = 0x0002;
359 const int enable_echo_input = 0x0004;
360 const int enable_virtual_terminal_input = 0x0200;
361 const int enable_window_input = 0x0008;
362 in_mode &= ~enable_echo_input;
363 in_mode &= ~enable_line_input;
364 in_mode |= enable_virtual_terminal_input;
365 in_mode |= enable_window_input;
366
367 SetConsoleMode(stdin_handle, in_mode);
368 SetConsoleMode(stdout_handle, out_mode);
369#else
370 struct termios terminal;
371 tcgetattr(STDIN_FILENO, &terminal);
372 on_exit_functions.push([=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
373
374 terminal.c_lflag &= ~ICANON; // Non canonique terminal.
375 terminal.c_lflag &= ~ECHO; // Do not print after a key press.
376 terminal.c_cc[VMIN] = 0;
377 terminal.c_cc[VTIME] = 0;
378 // auto oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
379 // fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
380 // on_exit_functions.push([=] { fcntl(STDIN_FILENO, F_GETFL, oldf); });
381
382 tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
383
384 // Handle resize.
385 on_resize = [&] { event_sender_->Send(Event::Special({0})); };
386 install_signal_handler(SIGWINCH, OnResize);
387#endif
388
389 auto enable = [&](std::vector<DECMode> parameters) {
390 std::cout << Set(parameters);
391 on_exit_functions.push([=] { std::cout << Reset(parameters); });
392 };
393
394 auto disable = [&](std::vector<DECMode> parameters) {
395 std::cout << Reset(parameters);
396 on_exit_functions.push([=] { std::cout << Set(parameters); });
397 };
398
399 if (use_alternative_screen_) {
400 enable({
401 DECMode::kAlternateScreen,
402 });
403 }
404
405 disable({
406 DECMode::kCursor,
407 DECMode::kLineWrap,
408 });
409
410 enable({
411 // DECMode::kMouseVt200,
412 DECMode::kMouseAnyEvent,
413 DECMode::kMouseUtf8,
414 DECMode::kMouseSgrExtMode,
415 });
416
417 // After installing the new configuration, flush it to the terminal to ensure
418 // it is fully applied:
419 Flush();
420
421 quit_ = false;
422 event_listener_ =
423 std::thread(&EventListener, &quit_, event_receiver_->MakeSender());
424}
425
426void ScreenInteractive::Uninstall() {
427 ExitLoopClosure()();
428 event_listener_.join();
429
430 OnExit(0);
431}
432
433void ScreenInteractive::Main(Component component) {
434 while (!quit_) {
435 if (!event_receiver_->HasPending()) {
436 Draw(component);
437 std::cout << ToString() << set_cursor_position;
438 Flush();
439 Clear();
440 }
441
442 Event event;
443 if (!event_receiver_->Receive(&event))
444 break;
445
446 if (event.is_cursor_reporting()) {
447 cursor_x_ = event.cursor_x();
448 cursor_y_ = event.cursor_y();
449 continue;
450 }
451
452 if (event.is_mouse()) {
453 event.mouse().x -= cursor_x_;
454 event.mouse().y -= cursor_y_;
455 }
456
457 event.screen_ = this;
458 component->OnEvent(event);
459 }
460}
461
462void ScreenInteractive::Draw(Component component) {
463 auto document = component->Render();
464 int dimx = 0;
465 int dimy = 0;
466 switch (dimension_) {
467 case Dimension::Fixed:
468 dimx = dimx_;
469 dimy = dimy_;
470 break;
471 case Dimension::TerminalOutput:
472 document->ComputeRequirement();
473 dimx = Terminal::Size().dimx;
474 dimy = document->requirement().min_y;
475 break;
476 case Dimension::Fullscreen:
477 dimx = Terminal::Size().dimx;
478 dimy = Terminal::Size().dimy;
479 break;
480 case Dimension::FitComponent:
481 auto terminal = Terminal::Size();
482 document->ComputeRequirement();
483 dimx = std::min(document->requirement().min_x, terminal.dimx);
484 dimy = std::min(document->requirement().min_y, terminal.dimy);
485 break;
486 }
487
488 bool resized = (dimx != dimx_) || (dimy != dimy_);
489 std::cout << reset_cursor_position << ResetPosition(/*clear=*/resized);
490
491 // Resize the screen if needed.
492 if (resized) {
493 dimx_ = dimx;
494 dimy_ = dimy;
495 pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
496 cursor_.x = dimx_ - 1;
497 cursor_.y = dimy_ - 1;
498 }
499
500 // Periodically request the terminal emulator the frame position relative to
501 // the screen. This is useful for converting mouse position reported in
502 // screen's coordinates to frame's coordinates.
503#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
504 // Microsoft's terminal suffers from a [bug]. When reporting the cursor
505 // position, several output sequences are mixed together into garbage.
506 // This causes FTXUI user to see some "1;1;R" sequences into the Input
507 // component. See [issue]. Solution is to request cursor position less
508 // often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
509 // https://github.com/ArthurSonzogni/FTXUI/issues/136
510 static int i = -3;
511 ++i;
512 if (!use_alternative_screen_ && (i % 150 == 0))
513 std::cout << DeviceStatusReport(DSRMode::kCursor);
514#else
515 static int i = -3;
516 ++i;
517 if (!use_alternative_screen_ && (previous_frame_resized_ || i % 40 == 0))
518 std::cout << DeviceStatusReport(DSRMode::kCursor);
519#endif
520 previous_frame_resized_ = resized;
521
522 Render(*this, document);
523
524 // Set cursor position for user using tools to insert CJK characters.
525 set_cursor_position = "";
526 reset_cursor_position = "";
527
528 int dx = dimx_ - 1 - cursor_.x;
529 int dy = dimy_ - 1 - cursor_.y;
530
531 if (dx != 0) {
532 set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
533 reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
534 }
535 if (dy != 0) {
536 set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
537 reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
538 }
539}
540
541ScreenInteractive::Callback ScreenInteractive::ExitLoopClosure() {
542 return [this] {
543 quit_ = true;
544 event_sender_.reset();
545 };
546}
547
548} // namespace ftxui.
549
550// Copyright 2020 Arthur Sonzogni. All rights reserved.
551// Use of this source code is governed by the MIT license that can be found in
552// the LICENSE file.
std::function< void()> Callback
A rectangular grid of Pixel.
Definition screen.hpp:51
std::unique_ptr< CapturedMouseInterface > CapturedMouse
Receiver< T > MakeReceiver()
Definition receiver.hpp:117
ScreenInteractive * g_active_screen
std::unique_ptr< SenderImpl< T > > Sender
Definition receiver.hpp:44
void Render(Screen &screen, const Element &node)
Display an element on a ftxui::Screen.
Definition node.cpp:40
Element select(Element)
Definition frame.cpp:38
std::shared_ptr< ComponentBase > Component
Represent an event. It can be key press event, a terminal resize, or more ...
Definition event.hpp:25
static Event Special(std::string)
Definition event.cpp:37