From 348c3853d4d28f283d0517596b668bd61f954293 Mon Sep 17 00:00:00 2001 From: Arthur Sonzogni Date: Sun, 17 Dec 2023 10:24:33 +0100 Subject: [PATCH] Restore cursor shape on exit. (#793) Fixed: https://github.com/ArthurSonzogni/FTXUI/issues/792 --- .clang-tidy | 1 + CHANGELOG.md | 1 + include/ftxui/component/event.hpp | 21 +++++++---- .../ftxui/component/screen_interactive.hpp | 3 ++ src/ftxui/component/event.cpp | 14 ++++++- src/ftxui/component/screen_interactive.cpp | 37 ++++++++++++++----- src/ftxui/component/terminal_input_parser.cpp | 30 +++++++++++---- src/ftxui/component/terminal_input_parser.hpp | 12 +++--- .../component/terminal_input_parser_test.cpp | 27 +++++++++++++- src/ftxui/dom/scroll_indicator_test.cpp | 14 +++---- 10 files changed, 119 insertions(+), 41 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index 7feeaaf..130bda7 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -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, diff --git a/CHANGELOG.md b/CHANGELOG.md index 53d128f..cc45103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/include/ftxui/component/event.hpp b/include/ftxui/component/event.hpp index b481622..a262a59 100644 --- a/include/ftxui/component/event.hpp +++ b/include/ftxui/component/event.hpp @@ -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_; diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index 7629f03..496e2c1 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -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; diff --git a/src/ftxui/component/event.cpp b/src/ftxui/component/event.cpp index 72a57f1..661e601 100644 --- a/src/ftxui/component/event.cpp +++ b/src/ftxui/component/event.cpp @@ -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; } diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index a8156b8..7be7f91 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -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; + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + // clang-format off // Handle Event. if constexpr (std::is_same_v) { - 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_; diff --git a/src/ftxui/component/terminal_input_parser.cpp b/src/ftxui/component/terminal_input_parser.cpp index 3ba0e69..7ad3f70 100644 --- a/src/ftxui/component/terminal_input_parser.cpp +++ b/src/ftxui/component/terminal_input_parser.cpp @@ -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 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; diff --git a/src/ftxui/component/terminal_input_parser.hpp b/src/ftxui/component/terminal_input_parser.hpp index 5a808c5..55f3b15 100644 --- a/src/ftxui/component/terminal_input_parser.hpp +++ b/src/ftxui/component/terminal_input_parser.hpp @@ -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 arguments); - Output ParseCursorReporting(std::vector arguments); + Output ParseCursorPosition(std::vector arguments); Sender out_; int position_ = -1; diff --git a/src/ftxui/component/terminal_input_parser_test.cpp b/src/ftxui/component/terminal_input_parser_test.cpp index 971c086..9e99a34 100644 --- a/src/ftxui/component/terminal_input_parser_test.cpp +++ b/src/ftxui/component/terminal_input_parser_test.cpp @@ -146,7 +146,7 @@ TEST(Event, MouseReporting) { Task received; EXPECT_TRUE(event_receiver->Receive(&received)); - EXPECT_TRUE(std::get(received).is_cursor_reporting()); + EXPECT_TRUE(std::get(received).is_cursor_position()); EXPECT_EQ(42, std::get(received).cursor_x()); EXPECT_EQ(12, std::get(received).cursor_y()); EXPECT_FALSE(event_receiver->Receive(&received)); @@ -446,5 +446,28 @@ TEST(Event, Special) { } } +TEST(Event, DeviceControlString) { + auto event_receiver = MakeReceiver(); + { + 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(received).is_cursor_shape()); + EXPECT_EQ(1, std::get(received).cursor_shape()); + EXPECT_FALSE(event_receiver->Receive(&received)); +} + } // namespace ftxui -// NOLINTEND + // NOLINTEND diff --git a/src/ftxui/dom/scroll_indicator_test.cpp b/src/ftxui/dom/scroll_indicator_test.cpp index 7dbd4c0..ae5cb0b 100644 --- a/src/ftxui/dom/scroll_indicator_test.cpp +++ b/src/ftxui/dom/scroll_indicator_test.cpp @@ -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);