Restore cursor shape on exit. (#793)

Fixed: https://github.com/ArthurSonzogni/FTXUI/issues/792
This commit is contained in:
Arthur Sonzogni 2023-12-17 10:24:33 +01:00 committed by GitHub
parent bfadcb7165
commit 348c3853d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 119 additions and 41 deletions

View File

@ -10,6 +10,7 @@ Checks: "*,
-android-*,
-bugprone-easily-swappable-parameters,
-cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-pro-type-union-access,
-fuchsia-*,
-google-*,
-hicpp-signed-bitwise,

View File

@ -32,6 +32,7 @@ current (development)
alternate screen.
- Bugfix: `Input` `onchange` was not called on backspace or delete key.
Fixed by @chrysante in chrysante in PR #776.
- Bugfix: Propertly restore cursor shape on exit. See #792.
### Dom
- Feature: Add `hscroll_indicator`. It display an horizontal indicator

View File

@ -33,7 +33,8 @@ struct Event {
static Event Character(wchar_t);
static Event Special(std::string);
static Event Mouse(std::string, Mouse mouse);
static Event CursorReporting(std::string, int x, int y);
static Event CursorPosition(std::string, int x, int y); // Internal
static Event CursorShape(std::string, int shape); // Internal
// --- Arrow ---
static const Event ArrowLeft;
@ -66,20 +67,24 @@ struct Event {
static const Event Custom;
//--- Method section ---------------------------------------------------------
bool operator==(const Event& other) const { return input_ == other.input_; }
bool operator!=(const Event& other) const { return !operator==(other); }
const std::string& input() const { return input_; }
bool is_character() const { return type_ == Type::Character; }
std::string character() const { return input_; }
bool is_mouse() const { return type_ == Type::Mouse; }
struct Mouse& mouse() { return data_.mouse; }
bool is_cursor_reporting() const { return type_ == Type::CursorReporting; }
// --- Internal Method section -----------------------------------------------
bool is_cursor_position() const { return type_ == Type::CursorPosition; }
int cursor_x() const { return data_.cursor.x; }
int cursor_y() const { return data_.cursor.y; }
const std::string& input() const { return input_; }
bool operator==(const Event& other) const { return input_ == other.input_; }
bool operator!=(const Event& other) const { return !operator==(other); }
bool is_cursor_shape() const { return type_ == Type::CursorShape; }
int cursor_shape() const { return data_.cursor_shape; }
//--- State section ----------------------------------------------------------
ScreenInteractive* screen_ = nullptr;
@ -91,7 +96,8 @@ struct Event {
Unknown,
Character,
Mouse,
CursorReporting,
CursorPosition,
CursorShape,
};
Type type_ = Type::Unknown;
@ -103,6 +109,7 @@ struct Event {
union {
struct Mouse mouse;
struct Cursor cursor;
int cursor_shape;
} data_ = {};
std::string input_;

View File

@ -114,6 +114,9 @@ class ScreenInteractive : public Screen {
bool frame_valid_ = false;
// The style of the cursor to restore on exit.
int cursor_reset_shape_ = 1;
Mouse latest_mouse_event_;
friend class Loop;

View File

@ -49,6 +49,16 @@ Event Event::Mouse(std::string input, struct Mouse mouse) {
return event;
}
/// @brief An event corresponding to a terminal DCS (Device Control String).
// static
Event Event::CursorShape(std::string input, int shape) {
Event event;
event.input_ = std::move(input);
event.type_ = Type::CursorShape;
event.data_.cursor_shape = shape; // NOLINT
return event;
}
/// @brief An custom event whose meaning is defined by the user of the library.
/// @param input An arbitrary sequence of character defined by the developer.
/// @ingroup component.
@ -61,10 +71,10 @@ Event Event::Special(std::string input) {
/// @internal
// static
Event Event::CursorReporting(std::string input, int x, int y) {
Event Event::CursorPosition(std::string input, int x, int y) {
Event event;
event.input_ = std::move(input);
event.type_ = Type::CursorReporting;
event.type_ = Type::CursorPosition;
event.data_.cursor = {x, y}; // NOLINT
return event;
}

View File

@ -253,7 +253,17 @@ void InstallSignalHandler(int sig) {
[=] { std::ignore = std::signal(sig, old_signal_handler); });
}
// CSI: Control Sequence Introducer
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
// DEC: Digital Equipment Corporation
enum class DECMode {
@ -566,6 +576,14 @@ void ScreenInteractive::Install() {
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.
for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) {
@ -640,11 +658,6 @@ void ScreenInteractive::Install() {
});
}
on_exit_functions.push([=] {
std::cout << "\033[?25h"; // Enable cursor.
std::cout << "\033[?1 q"; // Cursor block blinking.
});
disable({
// DECMode::kCursor,
DECMode::kLineWrap,
@ -700,18 +713,24 @@ void ScreenInteractive::RunOnce(Component component) {
// private
void ScreenInteractive::HandleTask(Component component, Task& task) {
// clang-format off
std::visit([&](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
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_reporting()) {
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_;

View File

@ -150,10 +150,15 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
pending_.clear();
return;
case CURSOR_REPORTING:
out_->Send(Event::CursorReporting(std::move(pending_), // NOLINT
output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT
case CURSOR_POSITION:
out_->Send(Event::CursorPosition(std::move(pending_), // NOLINT
output.cursor.x, // NOLINT
output.cursor.y)); // NOLINT
pending_.clear();
return;
case CURSOR_SHAPE:
out_->Send(Event::CursorShape(std::move(pending_), output.cursor_shape));
pending_.clear();
return;
}
@ -286,6 +291,7 @@ TerminalInputParser::Output TerminalInputParser::ParseESC() {
}
}
// ESC P ... ESC BACKSLASH
TerminalInputParser::Output TerminalInputParser::ParseDCS() {
// Parse until the string terminator ST.
while (true) {
@ -305,6 +311,16 @@ TerminalInputParser::Output TerminalInputParser::ParseDCS() {
continue;
}
if (pending_.size() == 10 && //
pending_[2] == '1' && //
pending_[3] == '$' && //
pending_[4] == 'r' && //
true) {
Output output(CURSOR_SHAPE);
output.cursor_shape = pending_[5] - '0';
return output;
}
return SPECIAL;
}
}
@ -351,7 +367,7 @@ TerminalInputParser::Output TerminalInputParser::ParseCSI() {
case 'm':
return ParseMouse(altered, false, std::move(arguments));
case 'R':
return ParseCursorReporting(std::move(arguments));
return ParseCursorPosition(std::move(arguments));
default:
return SPECIAL;
}
@ -405,12 +421,12 @@ TerminalInputParser::Output TerminalInputParser::ParseMouse( // NOLINT
}
// NOLINTNEXTLINE
TerminalInputParser::Output TerminalInputParser::ParseCursorReporting(
TerminalInputParser::Output TerminalInputParser::ParseCursorPosition(
std::vector<int> arguments) {
if (arguments.size() != 2) {
return SPECIAL;
}
Output output(CURSOR_REPORTING);
Output output(CURSOR_POSITION);
output.cursor.y = arguments[0]; // NOLINT
output.cursor.x = arguments[1]; // NOLINT
return output;

View File

@ -31,12 +31,13 @@ class TerminalInputParser {
UNCOMPLETED,
DROP,
CHARACTER,
SPECIAL,
MOUSE,
CURSOR_REPORTING,
CURSOR_POSITION,
CURSOR_SHAPE,
SPECIAL,
};
struct CursorReporting {
struct CursorPosition {
int x;
int y;
};
@ -45,7 +46,8 @@ class TerminalInputParser {
Type type;
union {
Mouse mouse;
CursorReporting cursor;
CursorPosition cursor;
int cursor_shape;
};
Output(Type t) : type(t) {}
@ -59,7 +61,7 @@ class TerminalInputParser {
Output ParseCSI();
Output ParseOSC();
Output ParseMouse(bool altered, bool pressed, std::vector<int> arguments);
Output ParseCursorReporting(std::vector<int> arguments);
Output ParseCursorPosition(std::vector<int> arguments);
Sender<Task> out_;
int position_ = -1;

View File

@ -146,7 +146,7 @@ TEST(Event, MouseReporting) {
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_cursor_reporting());
EXPECT_TRUE(std::get<Event>(received).is_cursor_position());
EXPECT_EQ(42, std::get<Event>(received).cursor_x());
EXPECT_EQ(12, std::get<Event>(received).cursor_y());
EXPECT_FALSE(event_receiver->Receive(&received));
@ -446,5 +446,28 @@ TEST(Event, Special) {
}
}
TEST(Event, DeviceControlString) {
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(27); // ESC
parser.Add(80); // P
parser.Add(49); // 1
parser.Add(36); // $
parser.Add(114); // r
parser.Add(49); // 1
parser.Add(32); // SP
parser.Add(113); // q
parser.Add(27); // ESC
parser.Add(92); // (backslash)
}
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_cursor_shape());
EXPECT_EQ(1, std::get<Event>(received).cursor_shape());
EXPECT_FALSE(event_receiver->Receive(&received));
}
} // namespace ftxui
// NOLINTEND
// NOLINTEND

View File

@ -8,8 +8,8 @@
#include "ftxui/dom/elements.hpp" // for operator|, Element, operator|=, text, vbox, Elements, border, focus, frame, vscroll_indicator
#include "ftxui/dom/node.hpp" // for Render
#include "ftxui/screen/color.hpp" // for Color, Color::Red
#include "ftxui/screen/screen.hpp" // for Screen
#include "ftxui/screen/color.hpp" // for Color, Color::Red
// NOLINTBEGIN
namespace ftxui {
@ -129,7 +129,6 @@ TEST(ScrollIndicator, BasicVertical) {
}
TEST(ScrollIndicator, VerticalColorable) {
// The list we generate looks like this
// "╭────╮\r\n"
// "│0 ┃│\r\n"
@ -147,7 +146,6 @@ TEST(ScrollIndicator, VerticalColorable) {
}
TEST(ScrollIndicator, VerticalBackgroundColorable) {
// The list we generate looks like this
// "╭────╮\r\n"
// "│0 ┃│\r\n"
@ -165,7 +163,6 @@ TEST(ScrollIndicator, VerticalBackgroundColorable) {
}
TEST(ScrollIndicator, VerticalFullColorable) {
// The list we generate looks like this
// "╭────╮\r\n"
// "│0 ┃│\r\n"
@ -174,7 +171,8 @@ TEST(ScrollIndicator, VerticalFullColorable) {
// "│3 │\r\n"
// "╰────╯"
auto element = MakeVerticalList(0, 10) | color(Color::Red) | bgcolor(Color::Red);
auto element =
MakeVerticalList(0, 10) | color(Color::Red) | bgcolor(Color::Red);
Screen screen(6, 6);
Render(screen, element);
@ -233,7 +231,6 @@ TEST(ScrollIndicator, BasicHorizontal) {
}
TEST(ScrollIndicator, HorizontalColorable) {
// The list we generate looks like this
// "╭────╮\r\n"
// "│5678│\r\n"
@ -249,7 +246,6 @@ TEST(ScrollIndicator, HorizontalColorable) {
}
TEST(ScrollIndicator, HorizontalBackgroundColorable) {
// The list we generate looks like this
// "╭────╮\r\n"
// "│5678│\r\n"
@ -265,14 +261,14 @@ TEST(ScrollIndicator, HorizontalBackgroundColorable) {
}
TEST(ScrollIndicator, HorizontalFullColorable) {
// The list we generate looks like this
// "╭────╮\r\n"
// "│5678│\r\n"
// "│ ──│\r\n"
// "╰────╯"
auto element = MakeHorizontalList(6, 10) | color(Color::Red) | bgcolor(Color::Red);
auto element =
MakeHorizontalList(6, 10) | color(Color::Red) | bgcolor(Color::Red);
Screen screen(6, 4);
Render(screen, element);