From 348c3853d4d28f283d0517596b668bd61f954293 Mon Sep 17 00:00:00 2001
From: Arthur Sonzogni <sonzogniarthur@gmail.com>
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 7feeaaff..130bda72 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 53d128f4..cc45103e 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 b4816225..a262a59a 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 7629f031..496e2c1d 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 72a57f13..661e6010 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 a8156b88..7be7f913 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<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_;
diff --git a/src/ftxui/component/terminal_input_parser.cpp b/src/ftxui/component/terminal_input_parser.cpp
index 3ba0e69d..7ad3f704 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<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;
diff --git a/src/ftxui/component/terminal_input_parser.hpp b/src/ftxui/component/terminal_input_parser.hpp
index 5a808c56..55f3b157 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<int> arguments);
-  Output ParseCursorReporting(std::vector<int> arguments);
+  Output ParseCursorPosition(std::vector<int> arguments);
 
   Sender<Task> 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 971c0867..9e99a349 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<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
diff --git a/src/ftxui/dom/scroll_indicator_test.cpp b/src/ftxui/dom/scroll_indicator_test.cpp
index 7dbd4c0a..ae5cb0b9 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);
 
-- 
GitLab