FTXUI/src/ftxui/component/screen_interactive.cpp

502 lines
14 KiB
C++
Raw Normal View History

2021-05-02 02:40:35 +08:00
#include <stdio.h> // for fileno, stdin
#include <algorithm> // for copy, max, min
#include <csignal> // for signal, SIGABRT, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, SIGWINCH
#include <cstdlib> // for NULL
#include <initializer_list> // for initializer_list
2021-05-10 02:32:27 +08:00
#include <iostream> // for cout, ostream, basic_ostream, operator<<, endl, flush
#include <stack> // for stack
#include <thread> // for thread
#include <utility> // for move
#include <vector> // for vector
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse, CapturedMouseInterface
#include "ftxui/component/component_base.hpp" // for ComponentBase
#include "ftxui/component/event.hpp" // for Event
#include "ftxui/component/mouse.hpp" // for Mouse
#include "ftxui/component/receiver.hpp" // for ReceiverImpl, MakeReceiver, Sender, SenderImpl, Receiver
2021-05-10 02:32:27 +08:00
#include "ftxui/component/screen_interactive.hpp"
#include "ftxui/component/terminal_input_parser.hpp" // for TerminalInputParser
2021-05-02 02:40:35 +08:00
#include "ftxui/dom/node.hpp" // for Node, Render
#include "ftxui/dom/requirement.hpp" // for Requirement
2021-05-10 02:32:27 +08:00
#include "ftxui/screen/terminal.hpp" // for Terminal::Dimensions, Terminal
#if defined(_WIN32)
#define DEFINE_CONSOLEV2_PROPERTIES
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#ifndef UNICODE
#error Must be compiled in UNICODE mode
#endif
#else
2021-05-10 02:32:27 +08:00
#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set
#include <termios.h> // for tcsetattr, termios, tcgetattr, TCSANOW, cc_t, ECHO, ICANON, VMIN, VTIME
#include <unistd.h> // for STDIN_FILENO, read
#endif
// Quick exit is missing in standard CLang headers
#if defined(__clang__) && defined(__APPLE__)
#define quick_exit(a) exit(a)
#endif
namespace ftxui {
2020-08-09 20:53:56 +08:00
namespace {
2020-03-25 08:15:46 +08:00
2021-03-22 05:54:39 +08:00
void Flush() {
// Emscripten doesn't implement flush. We interpret zero as flush.
std::cout << '\0' << std::flush;
2021-03-22 05:54:39 +08:00
}
constexpr int timeout_milliseconds = 20;
constexpr int timeout_microseconds = timeout_milliseconds * 1000;
#if defined(_WIN32)
2021-05-02 02:40:35 +08:00
void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
auto console = GetStdHandle(STD_INPUT_HANDLE);
auto parser = TerminalInputParser(out->Clone());
while (!*quit) {
// Throttle ReadConsoleInput by waiting 250ms, this wait function will
// return if there is input in the console.
auto wait_result = WaitForSingleObject(console, timeout_milliseconds);
if (wait_result == WAIT_TIMEOUT) {
parser.Timeout(timeout_milliseconds);
continue;
}
DWORD number_of_events = 0;
if (!GetNumberOfConsoleInputEvents(console, &number_of_events))
continue;
if (number_of_events <= 0)
continue;
std::vector<INPUT_RECORD> records{number_of_events};
DWORD number_of_events_read = 0;
2021-05-02 02:40:35 +08:00
ReadConsoleInput(console, records.data(), (DWORD)records.size(),
&number_of_events_read);
records.resize(number_of_events_read);
for (const auto& r : records) {
switch (r.EventType) {
case KEY_EVENT: {
auto key_event = r.Event.KeyEvent;
// ignore UP key events
if (key_event.bKeyDown == FALSE)
continue;
parser.Add((char)key_event.uChar.UnicodeChar);
} break;
case WINDOW_BUFFER_SIZE_EVENT:
out->Send(Event::Special({0}));
break;
case MENU_EVENT:
case FOCUS_EVENT:
case MOUSE_EVENT:
// TODO(mauve): Implement later.
break;
}
}
}
}
2021-03-22 05:54:39 +08:00
#elif defined(__EMSCRIPTEN__)
#include <emscripten.h>
// Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
(void)timeout_microseconds;
auto parser = TerminalInputParser(std::move(out));
2021-03-22 05:54:39 +08:00
char c;
while (!*quit) {
2021-05-10 02:32:27 +08:00
while (read(STDIN_FILENO, &c, 1), c)
2021-03-22 05:54:39 +08:00
parser.Add(c);
emscripten_sleep(1);
parser.Timeout(1);
}
}
#else
2021-05-02 02:40:35 +08:00
#include <sys/time.h> // for timeval
2021-03-21 05:45:21 +08:00
2020-05-02 08:02:04 +08:00
int CheckStdinReady(int usec_timeout) {
timeval tv = {0, usec_timeout};
fd_set fds;
FD_ZERO(&fds);
FD_SET(STDIN_FILENO, &fds);
select(STDIN_FILENO + 1, &fds, NULL, NULL, &tv);
return FD_ISSET(STDIN_FILENO, &fds);
}
2020-03-25 08:15:46 +08:00
// Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Event> out) {
2020-05-02 08:02:04 +08:00
const int buffer_size = 100;
auto parser = TerminalInputParser(std::move(out));
while (!*quit) {
if (!CheckStdinReady(timeout_microseconds)) {
parser.Timeout(timeout_milliseconds);
continue;
}
2020-05-02 08:02:04 +08:00
char buff[buffer_size];
int l = read(fileno(stdin), buff, buffer_size);
for (int i = 0; i < l; ++i)
parser.Add(buff[i]);
}
2020-03-25 08:15:46 +08:00
}
#endif
2021-04-25 21:22:38 +08:00
const std::string CSI = "\x1b[";
// DEC: Digital Equipment Corporation
enum class DECMode {
kLineWrap = 7,
kMouseX10 = 9,
kCursor = 25,
kMouseVt200 = 1000,
kMouseAnyEvent = 1003,
kMouseUtf8 = 1005,
kMouseSgrExtMode = 1006,
kMouseUrxvtMode = 1015,
kMouseSgrPixelsMode = 1016,
kAlternateScreen = 1049,
};
// Device Status Report (DSR) {
enum class DSRMode {
kCursor = 6,
};
2021-04-25 21:22:38 +08:00
const std::string Serialize(std::vector<DECMode> parameters) {
bool first = true;
std::string out;
for (DECMode parameter : parameters) {
if (!first)
out += ";";
out += std::to_string(int(parameter));
first = false;
}
return out;
}
2021-04-25 21:22:38 +08:00
// DEC Private Mode Set (DECSET)
const std::string Set(std::vector<DECMode> parameters) {
return CSI + "?" + Serialize(parameters) + "h";
}
2021-04-25 21:22:38 +08:00
// DEC Private Mode Reset (DECRST)
const std::string Reset(std::vector<DECMode> parameters) {
return CSI + "?" + Serialize(parameters) + "l";
}
2021-04-25 21:22:38 +08:00
// Device Status Report (DSR)
const std::string DeviceStatusReport(DSRMode ps) {
return CSI + std::to_string(int(ps)) + "n";
}
2020-03-23 16:23:57 +08:00
using SignalHandler = void(int);
std::stack<std::function<void()>> on_exit_functions;
void OnExit(int signal) {
(void)signal;
while (!on_exit_functions.empty()) {
on_exit_functions.top()();
on_exit_functions.pop();
}
}
2020-03-23 16:23:57 +08:00
auto install_signal_handler = [](int sig, SignalHandler handler) {
auto old_signal_handler = std::signal(sig, handler);
on_exit_functions.push([&]() { std::signal(sig, old_signal_handler); });
};
std::function<void()> on_resize = [] {};
2019-07-01 05:59:27 +08:00
void OnResize(int /* signal */) {
on_resize();
}
class CapturedMouseImpl : public CapturedMouseInterface {
public:
2021-05-02 02:40:35 +08:00
CapturedMouseImpl(std::function<void(void)> callback) : callback_(callback) {}
~CapturedMouseImpl() override { callback_(); }
private:
std::function<void(void)> callback_;
};
2020-08-09 20:53:56 +08:00
} // namespace
ScreenInteractive::ScreenInteractive(int dimx,
int dimy,
Dimension dimension,
bool use_alternative_screen)
: Screen(dimx, dimy),
dimension_(dimension),
use_alternative_screen_(use_alternative_screen) {
event_receiver_ = MakeReceiver<Event>();
event_sender_ = event_receiver_->MakeSender();
}
// static
2019-01-27 04:52:55 +08:00
ScreenInteractive ScreenInteractive::FixedSize(int dimx, int dimy) {
return ScreenInteractive(dimx, dimy, Dimension::Fixed, false);
}
// static
ScreenInteractive ScreenInteractive::Fullscreen() {
return ScreenInteractive(0, 0, Dimension::Fullscreen, true);
}
// static
ScreenInteractive ScreenInteractive::TerminalOutput() {
return ScreenInteractive(0, 0, Dimension::TerminalOutput, false);
}
2019-01-19 07:20:29 +08:00
// static
ScreenInteractive ScreenInteractive::FitComponent() {
return ScreenInteractive(0, 0, Dimension::FitComponent, false);
2019-01-19 07:20:29 +08:00
}
void ScreenInteractive::PostEvent(Event event) {
2020-03-25 08:15:46 +08:00
if (!quit_)
event_sender_->Send(event);
2019-01-27 09:33:06 +08:00
}
CapturedMouse ScreenInteractive::CaptureMouse() {
if (mouse_captured)
return nullptr;
mouse_captured = true;
return std::make_unique<CapturedMouseImpl>(
[this] { mouse_captured = false; });
}
2021-05-10 02:32:27 +08:00
void ScreenInteractive::Loop(Component component) {
on_exit_functions.push([this] { ExitLoopClosure()(); });
// Install signal handlers to restore the terminal state on exit. The default
// signal handlers are restored on exit.
for (int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE})
install_signal_handler(signal, OnExit);
// Save the old terminal configuration and restore it on exit.
#if defined(_WIN32)
// Enable VT processing on stdout and stdin
auto stdout_handle = GetStdHandle(STD_OUTPUT_HANDLE);
auto stdin_handle = GetStdHandle(STD_INPUT_HANDLE);
DWORD out_mode = 0;
DWORD in_mode = 0;
GetConsoleMode(stdout_handle, &out_mode);
GetConsoleMode(stdin_handle, &in_mode);
on_exit_functions.push([=] { SetConsoleMode(stdout_handle, out_mode); });
on_exit_functions.push([=] { SetConsoleMode(stdin_handle, in_mode); });
2020-07-17 02:58:58 +08:00
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
const int enable_virtual_terminal_processing = 0x0004;
const int disable_newline_auto_return = 0x0008;
out_mode |= enable_virtual_terminal_processing;
out_mode |= disable_newline_auto_return;
// https://docs.microsoft.com/en-us/windows/console/setconsolemode
const int enable_line_input = 0x0002;
const int enable_echo_input = 0x0004;
const int enable_virtual_terminal_input = 0x0200;
const int enable_window_input = 0x0008;
in_mode &= ~enable_echo_input;
in_mode &= ~enable_line_input;
in_mode |= enable_virtual_terminal_input;
in_mode |= enable_window_input;
SetConsoleMode(stdin_handle, in_mode);
SetConsoleMode(stdout_handle, out_mode);
#else
struct termios terminal;
tcgetattr(STDIN_FILENO, &terminal);
on_exit_functions.push([=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
terminal.c_lflag &= ~ICANON; // Non canonique terminal.
terminal.c_lflag &= ~ECHO; // Do not print after a key press.
2020-05-02 08:02:04 +08:00
terminal.c_cc[VMIN] = 0;
terminal.c_cc[VTIME] = 0;
2020-05-21 02:51:07 +08:00
// auto oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
// fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
// on_exit_functions.push([=] { fcntl(STDIN_FILENO, F_GETFL, oldf); });
2020-05-02 08:02:04 +08:00
tcsetattr(STDIN_FILENO, TCSANOW, &terminal);
// Handle resize.
on_resize = [&] { event_sender_->Send(Event::Special({0})); };
install_signal_handler(SIGWINCH, OnResize);
#endif
2021-04-25 21:22:38 +08:00
// Commit state:
auto flush = [&] {
Flush();
on_exit_functions.push([] { Flush(); });
};
auto enable = [&](std::vector<DECMode> parameters) {
std::cout << Set(parameters);
on_exit_functions.push([=] { std::cout << Reset(parameters); });
};
auto disable = [&](std::vector<DECMode> parameters) {
std::cout << Reset(parameters);
on_exit_functions.push([=] { std::cout << Set(parameters); });
};
flush();
if (use_alternative_screen_) {
2021-04-25 21:22:38 +08:00
enable({
DECMode::kAlternateScreen,
});
}
2021-04-25 21:22:38 +08:00
// On exit, reset cursor one line after the current drawing.
on_exit_functions.push(
2021-05-16 15:57:55 +08:00
[this] { std::cout << reset_cursor_position << std::endl; });
2021-04-25 21:22:38 +08:00
disable({
DECMode::kCursor,
DECMode::kLineWrap,
});
enable({
2021-05-02 02:40:35 +08:00
// DECMode::kMouseVt200,
2021-04-25 21:22:38 +08:00
DECMode::kMouseAnyEvent,
DECMode::kMouseUtf8,
DECMode::kMouseSgrExtMode,
});
flush();
auto event_listener =
std::thread(&EventListener, &quit_, event_receiver_->MakeSender());
2019-01-27 09:33:06 +08:00
2020-03-23 16:23:57 +08:00
// The main loop.
while (!quit_) {
if (!event_receiver_->HasPending()) {
Draw(component);
2021-03-22 05:54:39 +08:00
std::cout << ToString() << set_cursor_position;
Flush();
Clear();
}
Event event;
if (!event_receiver_->Receive(&event))
break;
if (event.is_cursor_reporting()) {
2021-04-25 21:22:38 +08:00
cursor_x_ = event.cursor_x();
cursor_y_ = event.cursor_y();
continue;
}
2021-04-25 21:22:38 +08:00
if (event.is_mouse()) {
event.mouse().x -= cursor_x_;
event.mouse().y -= cursor_y_;
}
2021-05-02 02:40:35 +08:00
event.screen_ = this;
component->OnEvent(event);
}
2020-03-25 08:15:46 +08:00
event_listener.join();
OnExit(0);
}
2021-05-10 02:32:27 +08:00
void ScreenInteractive::Draw(Component component) {
2019-01-13 01:24:46 +08:00
auto document = component->Render();
int dimx = 0;
int dimy = 0;
switch (dimension_) {
case Dimension::Fixed:
2019-01-13 01:24:46 +08:00
dimx = dimx_;
dimy = dimy_;
break;
case Dimension::TerminalOutput:
document->ComputeRequirement();
dimx = Terminal::Size().dimx;
2020-06-01 22:13:29 +08:00
dimy = document->requirement().min_y;
break;
case Dimension::Fullscreen:
dimx = Terminal::Size().dimx;
dimy = Terminal::Size().dimy;
break;
2019-01-19 07:20:29 +08:00
case Dimension::FitComponent:
auto terminal = Terminal::Size();
2019-01-19 07:20:29 +08:00
document->ComputeRequirement();
2020-06-01 22:13:29 +08:00
dimx = std::min(document->requirement().min_x, terminal.dimx);
dimy = std::min(document->requirement().min_y, terminal.dimy);
2019-01-19 07:20:29 +08:00
break;
}
2021-05-17 06:44:37 +08:00
bool resized = (dimx != dimx_) || (dimy != dimy_);
std::cout << reset_cursor_position << ResetPosition(/*clear=*/resized);
// Resize the screen if needed.
2021-05-17 06:44:37 +08:00
if (resized) {
dimx_ = dimx;
dimy_ = dimy;
pixels_ = std::vector<std::vector<Pixel>>(dimy, std::vector<Pixel>(dimx));
cursor_.x = dimx_ - 1;
cursor_.y = dimy_ - 1;
}
Implement Fallback for microsoft's terminals. (#138) I finally got access to a computer using the Microsoft's Windows OS. That's the opportunity to find and mitigate all the problems encountered. This patch: 1. Introduce an option and a C++ definition to enable fallback for Microsoft's terminal emulators. This allows me to see/test the Microsoft output from Linux. This also allows Windows users to remove the fallback and target non Microsoft terminals on Windows if needed. 2. Microsoft's terminal suffer from a race condition bug when reporting the cursor position: https://github.com/microsoft/terminal/pull/7583. The mitigation is not to ask for the cursor position in fullscreen mode where it isn't really needed and request it less often. This fixes: https://github.com/ArthurSonzogni/FTXUI/issues/136 3. Microsoft's terminal do not handle properly hidding the cursor. Instead the character under the cursor is hidden, which is a big problem. As a result, we don't enable setting the cursor to the best position for [input method editors](https://en.wikipedia.org/wiki/Input_method), It will be displayed at the bottom right corner. See: - https://github.com/microsoft/terminal/issues/1203 - https://github.com/microsoft/terminal/issues/3093 4. Microsoft's terminals do not provide a way to query if they support colors. As a fallback, assume true colors is supported. See issue: - https://github.com/microsoft/terminal/issues/1040 This mitigates: - https://github.com/ArthurSonzogni/FTXUI/issues/135 5. The "cmd" on Windows do not properly report its dimension. Powershell works correctly. As a fallback, use a 80x80 size instead of 0x0. 6. There are several dom elements and component displayed incorrectly, because the font used is missing several unicode glyph. Use alternatives or less detailled one as a fallback.
2021-07-04 23:38:31 +08:00
// Periodically request the terminal emulator the frame position relative to
// the screen. This is useful for converting mouse position reported in
// screen's coordinates to frame's coordinates.
static constexpr int cursor_refresh_rate =
#if defined(FTXUI_MICROSOFT_TERMINAL_FALLBACK)
// Microsoft's terminal suffers from a [bug]. When reporting the cursor
// position, several output sequences are mixed together into garbage.
// This causes FTXUI user to see some "1;1;R" sequences into the Input
// component. See [issue]. Solution is to request cursor position less
// often. [bug]: https://github.com/microsoft/terminal/pull/7583 [issue]:
// https://github.com/ArthurSonzogni/FTXUI/issues/136
150;
#else
20;
#endif
static int i = -3;
2021-05-17 06:44:37 +08:00
++i;
Implement Fallback for microsoft's terminals. (#138) I finally got access to a computer using the Microsoft's Windows OS. That's the opportunity to find and mitigate all the problems encountered. This patch: 1. Introduce an option and a C++ definition to enable fallback for Microsoft's terminal emulators. This allows me to see/test the Microsoft output from Linux. This also allows Windows users to remove the fallback and target non Microsoft terminals on Windows if needed. 2. Microsoft's terminal suffer from a race condition bug when reporting the cursor position: https://github.com/microsoft/terminal/pull/7583. The mitigation is not to ask for the cursor position in fullscreen mode where it isn't really needed and request it less often. This fixes: https://github.com/ArthurSonzogni/FTXUI/issues/136 3. Microsoft's terminal do not handle properly hidding the cursor. Instead the character under the cursor is hidden, which is a big problem. As a result, we don't enable setting the cursor to the best position for [input method editors](https://en.wikipedia.org/wiki/Input_method), It will be displayed at the bottom right corner. See: - https://github.com/microsoft/terminal/issues/1203 - https://github.com/microsoft/terminal/issues/3093 4. Microsoft's terminals do not provide a way to query if they support colors. As a fallback, assume true colors is supported. See issue: - https://github.com/microsoft/terminal/issues/1040 This mitigates: - https://github.com/ArthurSonzogni/FTXUI/issues/135 5. The "cmd" on Windows do not properly report its dimension. Powershell works correctly. As a fallback, use a 80x80 size instead of 0x0. 6. There are several dom elements and component displayed incorrectly, because the font used is missing several unicode glyph. Use alternatives or less detailled one as a fallback.
2021-07-04 23:38:31 +08:00
if (!use_alternative_screen_ && (i % cursor_refresh_rate == 0))
std::cout << DeviceStatusReport(DSRMode::kCursor);
2021-05-17 06:44:37 +08:00
Render(*this, document);
// Set cursor position for user using tools to insert CJK characters.
set_cursor_position = "";
reset_cursor_position = "";
int dx = dimx_ - 1 - cursor_.x;
int dy = dimy_ - 1 - cursor_.y;
if (dx != 0) {
2020-02-12 04:44:55 +08:00
set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
}
if (dy != 0) {
set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
2020-02-12 04:44:55 +08:00
reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
}
}
std::function<void()> ScreenInteractive::ExitLoopClosure() {
2020-03-25 08:15:46 +08:00
return [this]() {
quit_ = true;
event_sender_.reset();
};
}
} // namespace ftxui.
// Copyright 2020 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.