FTXUI/src/ftxui/component/screen_interactive.cpp

938 lines
26 KiB
C++
Raw Normal View History

2023-08-19 19:56:36 +08:00
// 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.
#include <algorithm> // for copy, max, min
2022-03-31 08:17:43 +08:00
#include <array> // for array
2023-02-12 21:09:47 +08:00
#include <chrono> // for operator-, milliseconds, operator>=, duration, common_type<>::type, time_point
2022-12-20 01:51:25 +08:00
#include <csignal> // for signal, SIGTSTP, SIGABRT, SIGWINCH, raise, SIGFPE, SIGILL, SIGINT, SIGSEGV, SIGTERM, __sighandler_t, size_t
#include <cstdio> // for fileno, stdin
2022-06-12 23:08:22 +08:00
#include <ftxui/component/task.hpp> // for Task, Closure, AnimationTask
2022-12-20 01:51:25 +08:00
#include <ftxui/screen/screen.hpp> // for Pixel, Screen::Cursor, Screen, Screen::Cursor::Hidden
#include <functional> // for function
#include <initializer_list> // for initializer_list
#include <iostream> // for cout, ostream, operator<<, basic_ostream, endl, flush
2021-05-10 02:32:27 +08:00
#include <stack> // for stack
2022-03-14 01:51:46 +08:00
#include <thread> // for thread, sleep_for
2022-12-20 01:51:25 +08:00
#include <tuple> // for _Swallow_assign, ignore
#include <type_traits> // for decay_t
2022-03-31 08:17:43 +08:00
#include <utility> // for move, swap
2022-12-20 01:51:25 +08:00
#include <variant> // for visit, variant
#include <vector> // for vector
2021-05-10 02:32:27 +08:00
2022-03-14 01:51:46 +08:00
#include "ftxui/component/animation.hpp" // for TimePoint, Clock, Duration, Params, RequestAnimationFrame
2021-05-10 02:32:27 +08:00
#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/loop.hpp" // for Loop
2022-12-20 01:51:25 +08:00
#include "ftxui/component/receiver.hpp" // for ReceiverImpl, Sender, MakeReceiver, 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
2023-02-12 21:07:28 +08:00
#include "ftxui/screen/terminal.hpp" // for Dimensions, Size
#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
2022-12-20 01:51:25 +08:00
#include <sys/select.h> // for select, FD_ISSET, FD_SET, FD_ZERO, fd_set, timeval
#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 {
2022-03-14 01:51:46 +08:00
namespace animation {
void RequestAnimationFrame() {
auto* screen = ScreenInteractive::Active();
2022-03-31 08:17:43 +08:00
if (screen) {
2022-03-14 01:51:46 +08:00
screen->RequestAnimationFrame();
2022-03-31 08:17:43 +08:00
}
2022-03-14 01:51:46 +08:00
}
} // namespace animation
2020-08-09 20:53:56 +08:00
namespace {
2020-03-25 08:15:46 +08:00
2022-03-31 08:17:43 +08:00
ScreenInteractive* g_active_screen = nullptr; // NOLINT
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;
[[maybe_unused]] constexpr int timeout_microseconds =
timeout_milliseconds * 1000;
#if defined(_WIN32)
void EventListener(std::atomic<bool>* quit, Sender<Task> 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;
std::wstring wstring;
wstring += key_event.uChar.UnicodeChar;
2023-02-12 21:07:28 +08:00
for (auto it : to_string(wstring)) {
parser.Add(it);
}
} 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<Task> out) {
2021-03-22 05:54:39 +08:00
(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);
}
}
extern "C" {
EMSCRIPTEN_KEEPALIVE
void ftxui_on_resize(int columns, int rows) {
Terminal::SetFallbackSize({
columns,
rows,
});
std::raise(SIGWINCH);
}
}
#else // POSIX (Linux & Mac)
2020-05-02 08:02:04 +08:00
int CheckStdinReady(int usec_timeout) {
timeval tv = {0, usec_timeout};
fd_set fds;
2022-03-31 08:17:43 +08:00
FD_ZERO(&fds); // NOLINT
FD_SET(STDIN_FILENO, &fds); // NOLINT
select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv); // NOLINT
return FD_ISSET(STDIN_FILENO, &fds); // NOLINT
2020-05-02 08:02:04 +08:00
}
2020-03-25 08:15:46 +08:00
// Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto parser = TerminalInputParser(std::move(out));
while (!*quit) {
if (!CheckStdinReady(timeout_microseconds)) {
parser.Timeout(timeout_milliseconds);
continue;
}
2022-03-31 08:17:43 +08:00
const size_t buffer_size = 100;
std::array<char, buffer_size> buffer; // NOLINT;
size_t l = read(fileno(stdin), buffer.data(), buffer_size); // NOLINT
for (size_t i = 0; i < l; ++i) {
2022-03-31 08:17:43 +08:00
parser.Add(buffer[i]); // NOLINT
}
}
2020-03-25 08:15:46 +08:00
}
#endif
std::stack<Closure> on_exit_functions; // NOLINT
void OnExit() {
while (!on_exit_functions.empty()) {
on_exit_functions.top()();
on_exit_functions.pop();
}
}
2022-12-20 01:51:25 +08:00
std::atomic<int> g_signal_exit_count = 0; // NOLINT
#if !defined(_WIN32)
2022-12-20 01:51:25 +08:00
std::atomic<int> g_signal_stop_count = 0; // NOLINT
std::atomic<int> g_signal_resize_count = 0; // NOLINT
#endif
// Async signal safe function
void RecordSignal(int signal) {
switch (signal) {
case SIGABRT:
case SIGFPE:
case SIGILL:
case SIGINT:
case SIGSEGV:
case SIGTERM:
g_signal_exit_count++;
break;
#if !defined(_WIN32)
case SIGTSTP:
g_signal_stop_count++;
break;
case SIGWINCH:
g_signal_resize_count++;
break;
#endif
default:
break;
}
}
void ExecuteSignalHandlers() {
int signal_exit_count = g_signal_exit_count.exchange(0);
while (signal_exit_count--) {
ScreenInteractive::Private::Signal(*g_active_screen, SIGABRT);
}
2020-03-25 08:15:46 +08:00
#if !defined(_WIN32)
int signal_stop_count = g_signal_stop_count.exchange(0);
while (signal_stop_count--) {
ScreenInteractive::Private::Signal(*g_active_screen, SIGTSTP);
}
int signal_resize_count = g_signal_resize_count.exchange(0);
while (signal_resize_count--) {
ScreenInteractive::Private::Signal(*g_active_screen, SIGWINCH);
}
#endif
}
void InstallSignalHandler(int sig) {
auto old_signal_handler = std::signal(sig, RecordSignal);
on_exit_functions.push(
[=] { std::ignore = std::signal(sig, old_signal_handler); });
}
// CSI: Control Sequence Introducer
2022-03-31 08:17:43 +08:00
const std::string CSI = "\x1b["; // NOLINT
//
// DCS: Device Control String
const std::string DCS = "\x1bP"; // NOLINT
// ST: String Terminator
const std::string ST = "\x1b\\"; // NOLINT
// DECRQSS: Request Status String
// DECSCUSR: Set Cursor Style
const std::string DECRQSS_DECSCUSR = DCS + "$q q" + ST; // NOLINT
2021-04-25 21:22:38 +08:00
// DEC: Digital Equipment Corporation
enum class DECMode {
kLineWrap = 7,
kCursor = 25,
kMouseX10 = 9,
2021-04-25 21:22:38 +08:00
kMouseVt200 = 1000,
kMouseVt200Highlight = 1001,
kMouseBtnEventMouse = 1002,
2021-04-25 21:22:38 +08:00
kMouseAnyEvent = 1003,
2021-04-25 21:22:38 +08:00
kMouseUtf8 = 1005,
kMouseSgrExtMode = 1006,
kMouseUrxvtMode = 1015,
kMouseSgrPixelsMode = 1016,
kAlternateScreen = 1049,
};
// Device Status Report (DSR) {
enum class DSRMode {
kCursor = 6,
};
2022-03-31 08:17:43 +08:00
std::string Serialize(const std::vector<DECMode>& parameters) {
2021-04-25 21:22:38 +08:00
bool first = true;
std::string out;
2022-12-20 01:51:25 +08:00
for (const DECMode parameter : parameters) {
2022-03-31 08:17:43 +08:00
if (!first) {
2021-04-25 21:22:38 +08:00
out += ";";
2022-03-31 08:17:43 +08:00
}
2021-04-25 21:22:38 +08:00
out += std::to_string(int(parameter));
first = false;
}
return out;
}
2021-04-25 21:22:38 +08:00
// DEC Private Mode Set (DECSET)
2022-03-31 08:17:43 +08:00
std::string Set(const std::vector<DECMode>& parameters) {
2021-04-25 21:22:38 +08:00
return CSI + "?" + Serialize(parameters) + "h";
}
2021-04-25 21:22:38 +08:00
// DEC Private Mode Reset (DECRST)
2022-03-31 08:17:43 +08:00
std::string Reset(const std::vector<DECMode>& parameters) {
2021-04-25 21:22:38 +08:00
return CSI + "?" + Serialize(parameters) + "l";
}
2021-04-25 21:22:38 +08:00
// Device Status Report (DSR)
2022-03-31 08:17:43 +08:00
std::string DeviceStatusReport(DSRMode ps) {
2021-04-25 21:22:38 +08:00
return CSI + std::to_string(int(ps)) + "n";
}
class CapturedMouseImpl : public CapturedMouseInterface {
public:
2022-03-31 08:17:43 +08:00
explicit CapturedMouseImpl(std::function<void(void)> callback)
: callback_(std::move(callback)) {}
~CapturedMouseImpl() override { callback_(); }
2022-03-31 08:17:43 +08:00
CapturedMouseImpl(const CapturedMouseImpl&) = delete;
CapturedMouseImpl(CapturedMouseImpl&&) = delete;
CapturedMouseImpl& operator=(const CapturedMouseImpl&) = delete;
CapturedMouseImpl& operator=(CapturedMouseImpl&&) = delete;
private:
std::function<void(void)> callback_;
};
2022-03-14 01:51:46 +08:00
void AnimationListener(std::atomic<bool>* quit, Sender<Task> out) {
2022-03-31 08:17:43 +08:00
// Animation at around 60fps.
const auto time_delta = std::chrono::milliseconds(15);
2022-03-14 01:51:46 +08:00
while (!*quit) {
out->Send(AnimationTask());
2022-03-31 08:17:43 +08:00
std::this_thread::sleep_for(time_delta);
2022-03-14 01:51:46 +08:00
}
}
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) {
task_receiver_ = MakeReceiver<Task>();
}
// static
2019-01-27 04:52:55 +08:00
ScreenInteractive ScreenInteractive::FixedSize(int dimx, int dimy) {
2022-09-06 02:56:41 +08:00
return {
dimx,
dimy,
Dimension::Fixed,
false,
};
}
/// @ingroup component
/// Create a ScreenInteractive taking the full terminal size. This is using the
/// alternate screen buffer to avoid messing with the terminal content.
/// @note This is the same as `ScreenInteractive::FullscreenAlternateScreen()`
// static
ScreenInteractive ScreenInteractive::Fullscreen() {
return FullscreenAlternateScreen();
}
/// @ingroup component
/// Create a ScreenInteractive taking the full terminal size. The primary screen
/// buffer is being used. It means if the terminal is resized, the previous
/// content might mess up with the terminal content.
// static
ScreenInteractive ScreenInteractive::FullscreenPrimaryScreen() {
return {
0,
0,
Dimension::Fullscreen,
false,
};
}
/// @ingroup component
/// Create a ScreenInteractive taking the full terminal size. This is using the
/// alternate screen buffer to avoid messing with the terminal content.
// static
ScreenInteractive ScreenInteractive::FullscreenAlternateScreen() {
2022-09-06 02:56:41 +08:00
return {
0,
0,
Dimension::Fullscreen,
true,
};
}
// static
ScreenInteractive ScreenInteractive::TerminalOutput() {
2022-09-06 02:56:41 +08:00
return {
0,
0,
Dimension::TerminalOutput,
false,
};
}
2019-01-19 07:20:29 +08:00
// static
ScreenInteractive ScreenInteractive::FitComponent() {
2022-09-06 02:56:41 +08:00
return {
0,
0,
Dimension::FitComponent,
false,
};
2019-01-19 07:20:29 +08:00
}
/// @ingroup component
/// @brief Set whether mouse is tracked and events reported.
/// called outside of the main loop. E.g `ScreenInteractive::Loop(...)`.
/// @param enable Whether to enable mouse event tracking.
/// @note This muse be called outside of the main loop. E.g. before calling
/// `ScreenInteractive::Loop`.
/// @note Mouse tracking is enabled by default.
/// @note Mouse tracking is only supported on terminals that supports it.
///
/// ### Example
///
/// ```cpp
/// auto screen = ScreenInteractive::TerminalOutput();
/// screen.TrackMouse(false);
/// screen.Loop(component);
/// ```
void ScreenInteractive::TrackMouse(bool enable) {
track_mouse_ = enable;
}
/// @brief Add a task to the main loop.
/// It will be executed later, after every other scheduled tasks.
/// @ingroup component
void ScreenInteractive::Post(Task task) {
// Task/Events sent toward inactive screen or screen waiting to become
// inactive are dropped.
if (!task_sender_) {
return;
}
task_sender_->Send(std::move(task));
}
/// @brief Add an event to the main loop.
/// It will be executed later, after every other scheduled events.
/// @ingroup component
void ScreenInteractive::PostEvent(Event event) {
Post(event);
2019-01-27 09:33:06 +08:00
}
/// @brief Add a task to draw the screen one more time, until all the animations
/// are done.
2022-03-14 01:51:46 +08:00
void ScreenInteractive::RequestAnimationFrame() {
2022-03-31 08:17:43 +08:00
if (animation_requested_) {
2022-03-14 01:51:46 +08:00
return;
2022-03-31 08:17:43 +08:00
}
2022-03-14 01:51:46 +08:00
animation_requested_ = true;
auto now = animation::Clock::now();
2022-03-31 08:17:43 +08:00
const auto time_histeresis = std::chrono::milliseconds(33);
if (now - previous_animation_time_ >= time_histeresis) {
previous_animation_time_ = now;
2022-03-31 08:17:43 +08:00
}
2022-03-14 01:51:46 +08:00
}
/// @brief Try to get the unique lock about behing able to capture the mouse.
/// @return A unique lock if the mouse is not already captured, otherwise a
/// null.
/// @ingroup component
CapturedMouse ScreenInteractive::CaptureMouse() {
2022-03-31 08:17:43 +08:00
if (mouse_captured) {
return nullptr;
2022-03-31 08:17:43 +08:00
}
mouse_captured = true;
return std::make_unique<CapturedMouseImpl>(
[this] { mouse_captured = false; });
}
/// @brief Execute the main loop.
/// @param component The component to draw.
/// @ingroup component
2022-03-31 08:17:43 +08:00
void ScreenInteractive::Loop(Component component) { // NOLINT
class Loop loop(this, std::move(component));
loop.Run();
}
/// @brief Return whether the main loop has been quit.
/// @ingroup component
bool ScreenInteractive::HasQuitted() {
return task_receiver_->HasQuitted();
}
// private
void ScreenInteractive::PreMain() {
2021-09-01 23:47:48 +08:00
// Suspend previously active screen:
if (g_active_screen) {
std::swap(suspended_screen_, g_active_screen);
// Reset cursor position to the top of the screen and clear the screen.
suspended_screen_->ResetCursorPosition();
std::cout << suspended_screen_->ResetPosition(/*clear=*/true);
2021-09-01 23:47:48 +08:00
suspended_screen_->dimx_ = 0;
suspended_screen_->dimy_ = 0;
// Reset dimensions to force drawing the screen again next time:
2021-09-01 23:47:48 +08:00
suspended_screen_->Uninstall();
}
// This screen is now active:
g_active_screen = this;
g_active_screen->Install();
previous_animation_time_ = animation::Clock::now();
}
// private
void ScreenInteractive::PostMain() {
2021-09-01 23:47:48 +08:00
// Put cursor position at the end of the drawing.
ResetCursorPosition();
g_active_screen = nullptr;
2021-09-01 23:47:48 +08:00
// Restore suspended screen.
if (suspended_screen_) {
// Clear screen, and put the cursor at the beginning of the drawing.
2021-09-01 23:47:48 +08:00
std::cout << ResetPosition(/*clear=*/true);
dimx_ = 0;
dimy_ = 0;
Uninstall();
2021-09-01 23:47:48 +08:00
std::swap(g_active_screen, suspended_screen_);
g_active_screen->Install();
} else {
Uninstall();
std::cout << '\r';
2021-09-01 23:47:48 +08:00
// On final exit, keep the current drawing and reset cursor position one
// line after it.
if (!use_alternative_screen_) {
std::cout << std::endl;
}
2021-09-01 23:47:48 +08:00
}
}
/// @brief Decorate a function. It executes the same way, but with the currently
/// active screen terminal hooks temporarilly uninstalled during its execution.
/// @param fn The function to decorate.
2022-03-31 08:17:43 +08:00
Closure ScreenInteractive::WithRestoredIO(Closure fn) { // NOLINT
return [this, fn] {
Uninstall();
fn();
Install();
};
}
/// @brief Return the currently active screen, or null if none.
2022-03-14 01:51:46 +08:00
// static
ScreenInteractive* ScreenInteractive::Active() {
return g_active_screen;
}
// private
2021-09-01 23:47:48 +08:00
void ScreenInteractive::Install() {
frame_valid_ = false;
// Flush the buffer for stdout to ensure whatever the user has printed before
// is fully applied before we start modifying the terminal configuration. This
// is important, because we are using two different channels (stdout vs
// termios/WinAPI) to communicate with the terminal emulator below. See
// https://github.com/ArthurSonzogni/FTXUI/issues/846
Flush();
// After uninstalling the new configuration, flush it to the terminal to
// ensure it is fully applied:
on_exit_functions.push([] { Flush(); });
on_exit_functions.push([this] { ExitLoopClosure()(); });
// Request the terminal to report the current cursor shape. We will restore it
// on exit.
std::cout << DECRQSS_DECSCUSR;
on_exit_functions.push([=] {
std::cout << "\033[?25h"; // Enable cursor.
std::cout << "\033[" + std::to_string(cursor_reset_shape_) + " q";
});
// Install signal handlers to restore the terminal state on exit. The default
// signal handlers are restored on exit.
2022-12-20 01:51:25 +08:00
for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
InstallSignalHandler(signal);
2022-03-31 08:17:43 +08:00
}
// 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
2022-12-20 01:51:25 +08:00
for (const int signal : {SIGWINCH, SIGTSTP}) {
InstallSignalHandler(signal);
}
2022-03-31 08:17:43 +08:00
struct termios terminal; // NOLINT
tcgetattr(STDIN_FILENO, &terminal);
on_exit_functions.push([=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); });
2022-03-31 08:17:43 +08:00
terminal.c_lflag &= ~ICANON; // NOLINT Non canonique terminal.
terminal.c_lflag &= ~ECHO; // NOLINT 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);
#endif
2022-03-31 08:17:43 +08:00
auto enable = [&](const std::vector<DECMode>& parameters) {
2021-04-25 21:22:38 +08:00
std::cout << Set(parameters);
on_exit_functions.push([=] { std::cout << Reset(parameters); });
};
2022-03-31 08:17:43 +08:00
auto disable = [&](const std::vector<DECMode>& parameters) {
2021-04-25 21:22:38 +08:00
std::cout << Reset(parameters);
on_exit_functions.push([=] { std::cout << Set(parameters); });
};
if (use_alternative_screen_) {
2021-04-25 21:22:38 +08:00
enable({
DECMode::kAlternateScreen,
});
}
2021-04-25 21:22:38 +08:00
disable({
// DECMode::kCursor,
2021-04-25 21:22:38 +08:00
DECMode::kLineWrap,
});
if (track_mouse_) {
enable({DECMode::kMouseVt200});
enable({DECMode::kMouseAnyEvent});
enable({DECMode::kMouseUrxvtMode});
enable({DECMode::kMouseSgrExtMode});
}
// After installing the new configuration, flush it to the terminal to
// ensure it is fully applied:
Flush();
2021-09-01 23:47:48 +08:00
quit_ = false;
task_sender_ = task_receiver_->MakeSender();
2021-09-01 23:47:48 +08:00
event_listener_ =
std::thread(&EventListener, &quit_, task_receiver_->MakeSender());
2022-03-14 01:51:46 +08:00
animation_listener_ =
std::thread(&AnimationListener, &quit_, task_receiver_->MakeSender());
2021-09-01 23:47:48 +08:00
}
// private
2021-09-01 23:47:48 +08:00
void ScreenInteractive::Uninstall() {
ExitNow();
2021-09-01 23:47:48 +08:00
event_listener_.join();
2022-03-14 01:51:46 +08:00
animation_listener_.join();
OnExit();
2021-09-01 23:47:48 +08:00
}
// private
2022-03-31 08:17:43 +08:00
// NOLINTNEXTLINE
void ScreenInteractive::RunOnceBlocking(Component component) {
ExecuteSignalHandlers();
Task task;
if (task_receiver_->Receive(&task)) {
HandleTask(component, task);
}
RunOnce(component);
}
// private
void ScreenInteractive::RunOnce(Component component) {
Task task;
while (task_receiver_->ReceiveNonBlocking(&task)) {
HandleTask(component, task);
ExecuteSignalHandlers();
}
Draw(std::move(component));
}
// private
void ScreenInteractive::HandleTask(Component component, Task& task) {
std::visit(
[&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
// clang-format off
// Handle Event.
if constexpr (std::is_same_v<T, Event>) {
if (arg.is_cursor_position()) {
cursor_x_ = arg.cursor_x();
cursor_y_ = arg.cursor_y();
return;
}
if (arg.is_cursor_shape()) {
cursor_reset_shape_= arg.cursor_shape();
return;
}
if (arg.is_mouse()) {
arg.mouse().x -= cursor_x_;
arg.mouse().y -= cursor_y_;
}
arg.screen_ = this;
component->OnEvent(arg);
frame_valid_ = false;
return;
}
// Handle callback
if constexpr (std::is_same_v<T, Closure>) {
arg();
return;
}
// Handle Animation
if constexpr (std::is_same_v<T, AnimationTask>) {
if (!animation_requested_) {
return;
}
animation_requested_ = false;
2022-12-20 01:51:25 +08:00
const animation::TimePoint now = animation::Clock::now();
const animation::Duration delta = now - previous_animation_time_;
previous_animation_time_ = now;
animation::Params params(delta);
component->OnAnimation(params);
frame_valid_ = false;
return;
}
},
task);
// clang-format on
}
// private
2022-03-31 08:17:43 +08:00
// NOLINTNEXTLINE
2021-05-10 02:32:27 +08:00
void ScreenInteractive::Draw(Component component) {
if (frame_valid_) {
return;
}
2019-01-13 01:24:46 +08:00
auto document = component->Render();
int dimx = 0;
int dimy = 0;
auto terminal = Terminal::Size();
document->ComputeRequirement();
switch (dimension_) {
case Dimension::Fixed:
2019-01-13 01:24:46 +08:00
dimx = dimx_;
dimy = dimy_;
break;
case Dimension::TerminalOutput:
dimx = terminal.dimx;
2020-06-01 22:13:29 +08:00
dimy = document->requirement().min_y;
break;
case Dimension::Fullscreen:
dimx = terminal.dimx;
dimy = terminal.dimy;
break;
2019-01-19 07:20:29 +08:00
case Dimension::FitComponent:
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;
}
2022-12-20 01:51:25 +08:00
const bool resized = (dimx != dimx_) || (dimy != dimy_);
ResetCursorPosition();
std::cout << ResetPosition(/*clear=*/resized);
2021-05-17 06:44:37 +08:00
// 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.
#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
static int i = -3;
++i;
2022-03-31 08:17:43 +08:00
if (!use_alternative_screen_ && (i % 150 == 0)) { // NOLINT
std::cout << DeviceStatusReport(DSRMode::kCursor);
2022-03-31 08:17:43 +08:00
}
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
#else
static int i = -3;
2021-05-17 06:44:37 +08:00
++i;
2022-03-31 08:17:43 +08:00
if (!use_alternative_screen_ &&
(previous_frame_resized_ || i % 40 == 0)) { // NOLINT
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
std::cout << DeviceStatusReport(DSRMode::kCursor);
2022-03-31 08:17:43 +08:00
}
#endif
previous_frame_resized_ = resized;
2021-05-17 06:44:37 +08:00
Render(*this, document);
// Set cursor position for user using tools to insert CJK characters.
{
const int dx = dimx_ - 1 - cursor_.x + int(dimx_ != terminal.dimx);
2022-12-20 01:51:25 +08:00
const int dy = dimy_ - 1 - cursor_.y;
set_cursor_position.clear();
reset_cursor_position.clear();
if (dy != 0) {
set_cursor_position += "\x1B[" + std::to_string(dy) + "A";
reset_cursor_position += "\x1B[" + std::to_string(dy) + "B";
}
if (dx != 0) {
set_cursor_position += "\x1B[" + std::to_string(dx) + "D";
reset_cursor_position += "\x1B[" + std::to_string(dx) + "C";
}
if (cursor_.shape == Cursor::Hidden) {
set_cursor_position += "\033[?25l";
} else {
set_cursor_position += "\033[?25h";
set_cursor_position +=
"\033[" + std::to_string(int(cursor_.shape)) + " q";
}
}
std::cout << ToString() << set_cursor_position;
Flush();
Clear();
frame_valid_ = true;
}
// private
void ScreenInteractive::ResetCursorPosition() {
std::cout << reset_cursor_position;
reset_cursor_position = "";
}
/// @brief Return a function to exit the main loop.
/// @ingroup component
Closure ScreenInteractive::ExitLoopClosure() {
return [this] { Exit(); };
}
/// @brief Exit the main loop.
/// @ingroup component
void ScreenInteractive::Exit() {
Post([this] { ExitNow(); });
}
// private:
void ScreenInteractive::ExitNow() {
quit_ = true;
task_sender_.reset();
}
// private:
void ScreenInteractive::Signal(int signal) {
if (signal == SIGABRT) {
OnExit();
return;
}
// Windows do no support SIGTSTP / SIGWINCH
#if !defined(_WIN32)
if (signal == SIGTSTP) {
Post([&] {
ResetCursorPosition();
std::cout << ResetPosition(/*clear*/ true); // Cursor to the beginning
Uninstall();
dimx_ = 0;
dimy_ = 0;
Flush();
std::ignore = std::raise(SIGTSTP);
Install();
});
return;
}
if (signal == SIGWINCH) {
Post(Event::Special({0}));
return;
}
#endif
}
} // namespace ftxui.