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-*, -android-*,
-bugprone-easily-swappable-parameters, -bugprone-easily-swappable-parameters,
-cppcoreguidelines-non-private-member-variables-in-classes, -cppcoreguidelines-non-private-member-variables-in-classes,
-cppcoreguidelines-pro-type-union-access,
-fuchsia-*, -fuchsia-*,
-google-*, -google-*,
-hicpp-signed-bitwise, -hicpp-signed-bitwise,

View File

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

View File

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

View File

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

View File

@ -49,6 +49,16 @@ Event Event::Mouse(std::string input, struct Mouse mouse) {
return event; 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. /// @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. /// @param input An arbitrary sequence of character defined by the developer.
/// @ingroup component. /// @ingroup component.
@ -61,10 +71,10 @@ Event Event::Special(std::string input) {
/// @internal /// @internal
// static // static
Event Event::CursorReporting(std::string input, int x, int y) { Event Event::CursorPosition(std::string input, int x, int y) {
Event event; Event event;
event.input_ = std::move(input); event.input_ = std::move(input);
event.type_ = Type::CursorReporting; event.type_ = Type::CursorPosition;
event.data_.cursor = {x, y}; // NOLINT event.data_.cursor = {x, y}; // NOLINT
return event; return event;
} }

View File

@ -253,7 +253,17 @@ void InstallSignalHandler(int sig) {
[=] { std::ignore = std::signal(sig, old_signal_handler); }); [=] { std::ignore = std::signal(sig, old_signal_handler); });
} }
// CSI: Control Sequence Introducer
const std::string CSI = "\x1b["; // NOLINT 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 // DEC: Digital Equipment Corporation
enum class DECMode { enum class DECMode {
@ -566,6 +576,14 @@ void ScreenInteractive::Install() {
on_exit_functions.push([this] { ExitLoopClosure()(); }); 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 // Install signal handlers to restore the terminal state on exit. The default
// signal handlers are restored on exit. // signal handlers are restored on exit.
for (const int signal : {SIGTERM, SIGSEGV, SIGINT, SIGILL, SIGABRT, SIGFPE}) { 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({ disable({
// DECMode::kCursor, // DECMode::kCursor,
DECMode::kLineWrap, DECMode::kLineWrap,
@ -700,18 +713,24 @@ void ScreenInteractive::RunOnce(Component component) {
// private // private
void ScreenInteractive::HandleTask(Component component, Task& task) { void ScreenInteractive::HandleTask(Component component, Task& task) {
// clang-format off std::visit(
std::visit([&](auto&& arg) { [&](auto&& arg) {
using T = std::decay_t<decltype(arg)>; using T = std::decay_t<decltype(arg)>;
// clang-format off
// Handle Event. // Handle Event.
if constexpr (std::is_same_v<T, 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_x_ = arg.cursor_x();
cursor_y_ = arg.cursor_y(); cursor_y_ = arg.cursor_y();
return; return;
} }
if (arg.is_cursor_shape()) {
cursor_reset_shape_= arg.cursor_shape();
return;
}
if (arg.is_mouse()) { if (arg.is_mouse()) {
arg.mouse().x -= cursor_x_; arg.mouse().x -= cursor_x_;
arg.mouse().y -= cursor_y_; arg.mouse().y -= cursor_y_;

View File

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

View File

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

View File

@ -146,7 +146,7 @@ TEST(Event, MouseReporting) {
Task received; Task received;
EXPECT_TRUE(event_receiver->Receive(&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(42, std::get<Event>(received).cursor_x());
EXPECT_EQ(12, std::get<Event>(received).cursor_y()); EXPECT_EQ(12, std::get<Event>(received).cursor_y());
EXPECT_FALSE(event_receiver->Receive(&received)); 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 } // 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/elements.hpp" // for operator|, Element, operator|=, text, vbox, Elements, border, focus, frame, vscroll_indicator
#include "ftxui/dom/node.hpp" // for Render #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/screen.hpp" // for Screen
#include "ftxui/screen/color.hpp" // for Color, Color::Red
// NOLINTBEGIN // NOLINTBEGIN
namespace ftxui { namespace ftxui {
@ -129,7 +129,6 @@ TEST(ScrollIndicator, BasicVertical) {
} }
TEST(ScrollIndicator, VerticalColorable) { TEST(ScrollIndicator, VerticalColorable) {
// The list we generate looks like this // The list we generate looks like this
// "╭────╮\r\n" // "╭────╮\r\n"
// "│0 ┃│\r\n" // "│0 ┃│\r\n"
@ -147,7 +146,6 @@ TEST(ScrollIndicator, VerticalColorable) {
} }
TEST(ScrollIndicator, VerticalBackgroundColorable) { TEST(ScrollIndicator, VerticalBackgroundColorable) {
// The list we generate looks like this // The list we generate looks like this
// "╭────╮\r\n" // "╭────╮\r\n"
// "│0 ┃│\r\n" // "│0 ┃│\r\n"
@ -165,7 +163,6 @@ TEST(ScrollIndicator, VerticalBackgroundColorable) {
} }
TEST(ScrollIndicator, VerticalFullColorable) { TEST(ScrollIndicator, VerticalFullColorable) {
// The list we generate looks like this // The list we generate looks like this
// "╭────╮\r\n" // "╭────╮\r\n"
// "│0 ┃│\r\n" // "│0 ┃│\r\n"
@ -174,7 +171,8 @@ TEST(ScrollIndicator, VerticalFullColorable) {
// "│3 │\r\n" // "│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); Screen screen(6, 6);
Render(screen, element); Render(screen, element);
@ -233,7 +231,6 @@ TEST(ScrollIndicator, BasicHorizontal) {
} }
TEST(ScrollIndicator, HorizontalColorable) { TEST(ScrollIndicator, HorizontalColorable) {
// The list we generate looks like this // The list we generate looks like this
// "╭────╮\r\n" // "╭────╮\r\n"
// "│5678│\r\n" // "│5678│\r\n"
@ -249,7 +246,6 @@ TEST(ScrollIndicator, HorizontalColorable) {
} }
TEST(ScrollIndicator, HorizontalBackgroundColorable) { TEST(ScrollIndicator, HorizontalBackgroundColorable) {
// The list we generate looks like this // The list we generate looks like this
// "╭────╮\r\n" // "╭────╮\r\n"
// "│5678│\r\n" // "│5678│\r\n"
@ -265,14 +261,14 @@ TEST(ScrollIndicator, HorizontalBackgroundColorable) {
} }
TEST(ScrollIndicator, HorizontalFullColorable) { TEST(ScrollIndicator, HorizontalFullColorable) {
// The list we generate looks like this // The list we generate looks like this
// "╭────╮\r\n" // "╭────╮\r\n"
// "│5678│\r\n" // "│5678│\r\n"
// "│ ──│\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); Screen screen(6, 4);
Render(screen, element); Render(screen, element);