FTXUI  0.9.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 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
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<std::function<void()>> 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
217std::function<void()> 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
233ScreenInteractive::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
245ScreenInteractive ScreenInteractive::FixedSize(int dimx, int dimy) {
246 return ScreenInteractive(dimx, dimy, Dimension::Fixed, false);
247}
248
249// static
250ScreenInteractive ScreenInteractive::Fullscreen() {
251 return ScreenInteractive(0, 0, Dimension::Fullscreen, true);
252}
253
254// static
255ScreenInteractive ScreenInteractive::TerminalOutput() {
256 return ScreenInteractive(0, 0, Dimension::TerminalOutput, false);
257}
258
259// static
260ScreenInteractive ScreenInteractive::FitComponent() {
261 return ScreenInteractive(0, 0, Dimension::FitComponent, false);
262}
263
264void ScreenInteractive::PostEvent(Event event) {
265 if (!quit_)
266 event_sender_->Send(event);
267}
268
269CapturedMouse 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
277void 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
314void 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
414void ScreenInteractive::Uninstall() {
415 ExitLoopClosure()();
416 event_listener_.join();
417
418 OnExit(0);
419}
420
421void 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
450void 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
527std::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.
std::unique_ptr< CapturedMouseInterface > CapturedMouse
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:34
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