From d386df6f94efe4202cf5d74c1a897c69ae4abdaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rn=20Gustav=20Larsen?= <164860481+jglanycon@users.noreply.github.com> Date: Sun, 28 Apr 2024 15:17:54 +0200 Subject: [PATCH] Enable raw keyboard input (#832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In order for applications to receive all keyboard inputs, including the Ctrl-C and Ctrl-Z, the raw input mode has been enabled. As result the SIGINT will no longer be used, instead the keyboard Ctrl-C event is used for exiting the framework, but only if no components has made use of it. Co-authored-by: Jørn Gustav Larsen Co-authored-by: ArthurSonzogni --- CHANGELOG.md | 8 + examples/component/print_key_press.cpp | 152 ++----- include/ftxui/component/event.hpp | 38 +- include/ftxui/component/mouse.hpp | 2 + .../ftxui/component/screen_interactive.hpp | 12 + include/ftxui/dom/canvas.hpp | 6 +- include/ftxui/screen/screen.hpp | 3 +- src/ftxui/component/event.cpp | 410 ++++++++++++++++-- src/ftxui/component/screen_interactive.cpp | 61 ++- .../component/screen_interactive_test.cpp | 67 +++ src/ftxui/component/terminal_input_parser.cpp | 30 +- .../component/terminal_input_parser_test.cpp | 35 +- src/ftxui/dom/canvas.cpp | 2 +- src/ftxui/screen/image.cpp | 13 +- src/ftxui/screen/screen.cpp | 6 +- 15 files changed, 643 insertions(+), 202 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index be16cc6..9eb964f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ current (development) --------------------- ### Component +- Feature: Add support for raw input. Allowing more keys to be detected. +- Feature: Add `ScreenInteractive::ForceHandleCtrlC(false)` to allow component + to fully override the default `Ctrl+C` handler. +- Feature: Add `ScreenInteractive::ForceHandleCtrlZ(false)` to allow component + to fully override the default `Ctrl+Z` handler. +- Feature: Add `Mouse::WeelLeft` and `Mouse::WeelRight` events on supported + terminals. +- Feature: Add `Event::DebugString()`. - Feature: Add support for `Input`'s insert mode. Add `InputOption::insert` option. Added by @mingsheng13. - Feature: Add `DropdownOption` to configure the dropdown. See #826. diff --git a/examples/component/print_key_press.cpp b/examples/component/print_key_press.cpp index 02b6133..8c2216a 100644 --- a/examples/component/print_key_press.cpp +++ b/examples/component/print_key_press.cpp @@ -18,123 +18,12 @@ using namespace ftxui; -std::string Stringify(Event event) { - std::string out; - for (auto& it : event.input()) - out += " " + std::to_string((unsigned int)it); - - out = "(" + out + " ) -> "; - if (event.is_character()) { - out += "Event::Character(\"" + event.character() + "\")"; - } else if (event.is_mouse()) { - out += "mouse"; - switch (event.mouse().button) { - case Mouse::Left: - out += "_left"; - break; - case Mouse::Middle: - out += "_middle"; - break; - case Mouse::Right: - out += "_right"; - break; - case Mouse::None: - out += "_none"; - break; - case Mouse::WheelUp: - out += "_wheel_up"; - break; - case Mouse::WheelDown: - out += "_wheel_down"; - break; - } - switch (event.mouse().motion) { - case Mouse::Pressed: - out += "_pressed"; - break; - case Mouse::Released: - out += "_released"; - break; - case Mouse::Moved: - out += "_moved"; - break; - } - if (event.mouse().control) - out += "_control"; - if (event.mouse().shift) - out += "_shift"; - if (event.mouse().meta) - out += "_meta"; - - out += "(" + // - std::to_string(event.mouse().x) + "," + - std::to_string(event.mouse().y) + ")"; - } else if (event == Event::ArrowLeft) { - out += "Event::ArrowLeft"; - } else if (event == Event::ArrowRight) { - out += "Event::ArrowRight"; - } else if (event == Event::ArrowUp) { - out += "Event::ArrowUp"; - } else if (event == Event::ArrowDown) { - out += "Event::ArrowDown"; - } else if (event == Event::ArrowLeftCtrl) { - out += "Event::ArrowLeftCtrl"; - } else if (event == Event ::ArrowRightCtrl) { - out += "Event::ArrowRightCtrl"; - } else if (event == Event::ArrowUpCtrl) { - out += "Event::ArrowUpCtrl"; - } else if (event == Event::ArrowDownCtrl) { - out += "Event::ArrowDownCtrl"; - } else if (event == Event::Backspace) { - out += "Event::Backspace"; - } else if (event == Event::Delete) { - out += "Event::Delete"; - } else if (event == Event::Escape) { - out += "Event::Escape"; - } else if (event == Event::Return) { - out += "Event::Return"; - } else if (event == Event::Tab) { - out += "Event::Tab"; - } else if (event == Event::TabReverse) { - out += "Event::TabReverse"; - } else if (event == Event::F1) { - out += "Event::F1"; - } else if (event == Event::F2) { - out += "Event::F2"; - } else if (event == Event::F3) { - out += "Event::F3"; - } else if (event == Event::F4) { - out += "Event::F4"; - } else if (event == Event::F5) { - out += "Event::F5"; - } else if (event == Event::F6) { - out += "Event::F6"; - } else if (event == Event::F7) { - out += "Event::F7"; - } else if (event == Event::F8) { - out += "Event::F8"; - } else if (event == Event::F9) { - out += "Event::F9"; - } else if (event == Event::F10) { - out += "Event::F10"; - } else if (event == Event::F11) { - out += "Event::F11"; - } else if (event == Event::F12) { - out += "Event::F12"; - } else if (event == Event::Home) { - out += "Event::Home"; - } else if (event == Event::End) { - out += "Event::End"; - } else if (event == Event::PageUp) { - out += "Event::PageUp"; - } else if (event == Event::PageDown) { - out += "Event::PageDown"; - } else if (event == Event::Custom) { - out += "Custom"; - } else { - out += "(special)"; +std::string Code(Event event) { + std::string codes; + for (auto& it : event.input()) { + codes += " " + std::to_string((unsigned int)it); } - return out; + return codes; } int main() { @@ -142,16 +31,35 @@ int main() { std::vector keys; - auto component = Renderer([&] { - Elements children; - for (size_t i = std::max(0, (int)keys.size() - 20); i < keys.size(); ++i) - children.push_back(text(Stringify(keys[i]))); - return window(text("keys"), vbox(std::move(children))); + auto left_column = Renderer([&] { + Elements children = { + text("Codes"), + separator(), + }; + for (size_t i = std::max(0, (int)keys.size() - 20); i < keys.size(); ++i) { + children.push_back(text(Code(keys[i]))); + } + return vbox(children); }); + auto right_column = Renderer([&] { + Elements children = { + text("Event"), + separator(), + }; + for (size_t i = std::max(0, (int)keys.size() - 20); i < keys.size(); ++i) { + children.push_back(text(keys[i].DebugString())); + } + return vbox(children); + }); + + int split_size = 40; + auto component = ResizableSplitLeft(left_column, right_column, &split_size); + component |= border; + component |= CatchEvent([&](Event event) { keys.push_back(event); - return true; + return false; }); screen.Loop(component); diff --git a/include/ftxui/component/event.hpp b/include/ftxui/component/event.hpp index a262a59..2d6f562 100644 --- a/include/ftxui/component/event.hpp +++ b/include/ftxui/component/event.hpp @@ -54,21 +54,52 @@ struct Event { static const Event Escape; static const Event Tab; static const Event TabReverse; - static const Event F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12; + // --- Navigation keys --- static const Event Insert; static const Event Home; static const Event End; - static const Event PageUp; static const Event PageDown; + // --- Function keys --- + static const Event F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12; + + // --- Control keys --- + static const Event a, A, CtrlA, AltA, CtrlAltA; + static const Event b, B, CtrlB, AltB, CtrlAltB; + static const Event c, C, CtrlC, AltC, CtrlAltC; + static const Event d, D, CtrlD, AltD, CtrlAltD; + static const Event e, E, CtrlE, AltE, CtrlAltE; + static const Event f, F, CtrlF, AltF, CtrlAltF; + static const Event g, G, CtrlG, AltG, CtrlAltG; + static const Event h, H, CtrlH, AltH, CtrlAltH; + static const Event i, I, CtrlI, AltI, CtrlAltI; + static const Event j, J, CtrlJ, AltJ, CtrlAltJ; + static const Event k, K, CtrlK, AltK, CtrlAltK; + static const Event l, L, CtrlL, AltL, CtrlAltL; + static const Event m, M, CtrlM, AltM, CtrlAltM; + static const Event n, N, CtrlN, AltN, CtrlAltN; + static const Event o, O, CtrlO, AltO, CtrlAltO; + static const Event p, P, CtrlP, AltP, CtrlAltP; + static const Event q, Q, CtrlQ, AltQ, CtrlAltQ; + static const Event r, R, CtrlR, AltR, CtrlAltR; + static const Event s, S, CtrlS, AltS, CtrlAltS; + static const Event t, T, CtrlT, AltT, CtrlAltT; + static const Event u, U, CtrlU, AltU, CtrlAltU; + static const Event v, V, CtrlV, AltV, CtrlAltV; + static const Event w, W, CtrlW, AltW, CtrlAltW; + static const Event x, X, CtrlX, AltX, CtrlAltX; + static const Event y, Y, CtrlY, AltY, CtrlAltY; + static const Event z, Z, CtrlZ, AltZ, CtrlAltZ; + // --- Custom --- 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); } + bool operator<(const Event& other) const { return input_ < other.input_; } const std::string& input() const { return input_; } @@ -86,6 +117,9 @@ struct Event { bool is_cursor_shape() const { return type_ == Type::CursorShape; } int cursor_shape() const { return data_.cursor_shape; } + // Debug + std::string DebugString() const; + //--- State section ---------------------------------------------------------- ScreenInteractive* screen_ = nullptr; diff --git a/include/ftxui/component/mouse.hpp b/include/ftxui/component/mouse.hpp index adfeb7f..cc6c3f1 100644 --- a/include/ftxui/component/mouse.hpp +++ b/include/ftxui/component/mouse.hpp @@ -16,6 +16,8 @@ struct Mouse { None = 3, WheelUp = 4, WheelDown = 5, + WheelLeft = 6, /// Supported terminal only. + WheelRight = 7, /// Supported terminal only. }; enum Motion { diff --git a/include/ftxui/component/screen_interactive.hpp b/include/ftxui/component/screen_interactive.hpp index cb4c189..bbbae0f 100644 --- a/include/ftxui/component/screen_interactive.hpp +++ b/include/ftxui/component/screen_interactive.hpp @@ -59,6 +59,15 @@ class ScreenInteractive : public Screen { // temporarily uninstalled. Closure WithRestoredIO(Closure); + // FTXUI implements handlers for Ctrl-C and Ctrl-Z. By default, these handlers + // are executed, even if the component catches the event. This avoid users + // handling every event to be trapped in the application. However, in some + // cases, the application may want to handle these events itself. In this + // case, the application can force FTXUI to not handle these events by calling + // the following functions with force=true. + void ForceHandleCtrlC(bool force); + void ForceHandleCtrlZ(bool force); + private: void ExitNow(); @@ -114,6 +123,9 @@ class ScreenInteractive : public Screen { bool frame_valid_ = false; + bool force_handle_ctrl_c_ = true; + bool force_handle_ctrl_z_ = true; + // The style of the cursor to restore on exit. int cursor_reset_shape_ = 1; diff --git a/include/ftxui/dom/canvas.hpp b/include/ftxui/dom/canvas.hpp index ffc487c..5d33d67 100644 --- a/include/ftxui/dom/canvas.hpp +++ b/include/ftxui/dom/canvas.hpp @@ -9,8 +9,8 @@ #include // for string #include // for unordered_map -#include "ftxui/screen/color.hpp" // for Color -#include "ftxui/screen/image.hpp" // for Pixel, Image +#include "ftxui/screen/color.hpp" // for Color +#include "ftxui/screen/image.hpp" // for Pixel, Image #ifdef DrawText // Workaround for WinUsr.h (via Windows.h) defining macros that break things. @@ -94,7 +94,7 @@ struct Canvas { void DrawText(int x, int y, const std::string& value); void DrawText(int x, int y, const std::string& value, const Color& color); void DrawText(int x, int y, const std::string& value, const Stylizer& style); - + // Draw using directly pixels or images -------------------------------------- // x is considered to be a multiple of 2. // y is considered to be a multiple of 4. diff --git a/include/ftxui/screen/screen.hpp b/include/ftxui/screen/screen.hpp index 6dd6eed..2bc77a6 100644 --- a/include/ftxui/screen/screen.hpp +++ b/include/ftxui/screen/screen.hpp @@ -36,7 +36,8 @@ class Screen : public Image { // Print the Screen on to the terminal. void Print() const; - // Fill the screen with space and reset any screen state, like hyperlinks, and cursor + // Fill the screen with space and reset any screen state, like hyperlinks, and + // cursor void Clear(); // Move the terminal cursor n-lines up with n = dimy(). diff --git a/src/ftxui/component/event.cpp b/src/ftxui/component/event.cpp index 661e601..5e663c9 100644 --- a/src/ftxui/component/event.cpp +++ b/src/ftxui/component/event.cpp @@ -1,12 +1,24 @@ // 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 // for map #include // for move #include "ftxui/component/event.hpp" #include "ftxui/component/mouse.hpp" // for Mouse #include "ftxui/screen/string.hpp" // for to_wstring +// Disable warning for shadowing variable, for every compilers. Indeed, there is +// a static Event for every letter of the alphabet: +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wshadow" +#elif __GNUC__ +#pragma GCC diagnostic ignored "-Wshadow" +#elif defined(_MSC_VER) +#pragma warning(disable : 6244) +#pragma warning(disable : 6246) +#endif + namespace ftxui { /// @brief An event corresponding to a given typed character. @@ -79,42 +91,376 @@ Event Event::CursorPosition(std::string input, int x, int y) { return event; } +/// @brief Return a string representation of the event. +std::string Event::DebugString() const { + static std::map event_to_string = { + // --- Arrow --- + {Event::ArrowLeft, "Event::ArrowLeft"}, + {Event::ArrowRight, "Event::ArrowRight"}, + {Event::ArrowUp, "Event::ArrowUp"}, + {Event::ArrowDown, "Event::ArrowDown"}, + + // --- ArrowCtrl --- + {Event::ArrowLeftCtrl, "Event::ArrowLeftCtrl"}, + {Event::ArrowRightCtrl, "Event::ArrowRightCtrl"}, + {Event::ArrowUpCtrl, "Event::ArrowUpCtrl"}, + {Event::ArrowDownCtrl, "Event::ArrowDownCtrl"}, + + // --- Other --- + {Event::Backspace, "Event::Backspace"}, + {Event::Delete, "Event::Delete"}, + {Event::Escape, "Event::Escape"}, + {Event::Return, "Event::Return"}, + {Event::Tab, "Event::Tab"}, + {Event::TabReverse, "Event::TabReverse"}, + + // --- Function keys --- + {Event::F1, "Event::F1"}, + {Event::F2, "Event::F2"}, + {Event::F3, "Event::F3"}, + {Event::F4, "Event::F4"}, + {Event::F5, "Event::F5"}, + {Event::F6, "Event::F6"}, + {Event::F7, "Event::F7"}, + {Event::F8, "Event::F8"}, + {Event::F9, "Event::F9"}, + {Event::F10, "Event::F10"}, + {Event::F11, "Event::F11"}, + {Event::F12, "Event::F12"}, + + // --- Navigation keys --- + {Event::Insert, "Event::Insert"}, + {Event::Home, "Event::Home"}, + {Event::End, "Event::End"}, + {Event::PageUp, "Event::PageUp"}, + {Event::PageDown, "Event::PageDown"}, + + // --- Control keys --- + {Event::CtrlA, "Event::CtrlA"}, + {Event::CtrlB, "Event::CtrlB"}, + {Event::CtrlC, "Event::CtrlC"}, + {Event::CtrlD, "Event::CtrlD"}, + {Event::CtrlE, "Event::CtrlE"}, + {Event::CtrlF, "Event::CtrlF"}, + {Event::CtrlG, "Event::CtrlG"}, + {Event::CtrlH, "Event::CtrlH"}, + {Event::CtrlI, "Event::CtrlI"}, + {Event::CtrlJ, "Event::CtrlJ"}, + {Event::CtrlK, "Event::CtrlK"}, + {Event::CtrlL, "Event::CtrlL"}, + {Event::CtrlM, "Event::CtrlM"}, + {Event::CtrlN, "Event::CtrlN"}, + {Event::CtrlO, "Event::CtrlO"}, + {Event::CtrlP, "Event::CtrlP"}, + {Event::CtrlQ, "Event::CtrlQ"}, + {Event::CtrlR, "Event::CtrlR"}, + {Event::CtrlS, "Event::CtrlS"}, + {Event::CtrlT, "Event::CtrlT"}, + {Event::CtrlU, "Event::CtrlU"}, + {Event::CtrlV, "Event::CtrlV"}, + {Event::CtrlW, "Event::CtrlW"}, + {Event::CtrlX, "Event::CtrlX"}, + {Event::CtrlY, "Event::CtrlY"}, + {Event::CtrlZ, "Event::CtrlZ"}, + + // --- Alt keys --- + {Event::AltA, "Event::AltA"}, + {Event::AltB, "Event::AltB"}, + {Event::AltC, "Event::AltC"}, + {Event::AltD, "Event::AltD"}, + {Event::AltE, "Event::AltE"}, + {Event::AltF, "Event::AltF"}, + {Event::AltG, "Event::AltG"}, + {Event::AltH, "Event::AltH"}, + {Event::AltI, "Event::AltI"}, + {Event::AltJ, "Event::AltJ"}, + {Event::AltK, "Event::AltK"}, + {Event::AltL, "Event::AltL"}, + {Event::AltM, "Event::AltM"}, + {Event::AltN, "Event::AltN"}, + {Event::AltO, "Event::AltO"}, + {Event::AltP, "Event::AltP"}, + {Event::AltQ, "Event::AltQ"}, + {Event::AltR, "Event::AltR"}, + {Event::AltS, "Event::AltS"}, + {Event::AltT, "Event::AltT"}, + {Event::AltU, "Event::AltU"}, + {Event::AltV, "Event::AltV"}, + {Event::AltW, "Event::AltW"}, + {Event::AltX, "Event::AltX"}, + {Event::AltY, "Event::AltY"}, + {Event::AltZ, "Event::AltZ"}, + + // --- CtrlAlt keys --- + {Event::CtrlAltA, "Event::CtrlAltA"}, + {Event::CtrlAltB, "Event::CtrlAltB"}, + {Event::CtrlAltC, "Event::CtrlAltC"}, + {Event::CtrlAltD, "Event::CtrlAltD"}, + {Event::CtrlAltE, "Event::CtrlAltE"}, + {Event::CtrlAltF, "Event::CtrlAltF"}, + {Event::CtrlAltG, "Event::CtrlAltG"}, + {Event::CtrlAltH, "Event::CtrlAltH"}, + {Event::CtrlAltI, "Event::CtrlAltI"}, + {Event::CtrlAltJ, "Event::CtrlAltJ"}, + {Event::CtrlAltK, "Event::CtrlAltK"}, + {Event::CtrlAltL, "Event::CtrlAltL"}, + {Event::CtrlAltM, "Event::CtrlAltM"}, + {Event::CtrlAltN, "Event::CtrlAltN"}, + {Event::CtrlAltO, "Event::CtrlAltO"}, + {Event::CtrlAltP, "Event::CtrlAltP"}, + {Event::CtrlAltQ, "Event::CtrlAltQ"}, + {Event::CtrlAltR, "Event::CtrlAltR"}, + {Event::CtrlAltS, "Event::CtrlAltS"}, + {Event::CtrlAltT, "Event::CtrlAltT"}, + {Event::CtrlAltU, "Event::CtrlAltU"}, + {Event::CtrlAltV, "Event::CtrlAltV"}, + {Event::CtrlAltW, "Event::CtrlAltW"}, + {Event::CtrlAltX, "Event::CtrlAltX"}, + {Event::CtrlAltY, "Event::CtrlAltY"}, + {Event::CtrlAltZ, "Event::CtrlAltZ"}, + + // --- Custom --- + {Event::Custom, "Event::Custom"}, + }; + + static std::map mouse_button_string = { + {Mouse::Button::Left, ".button = Mouse::Left"}, + {Mouse::Button::Middle, ".button = Mouse::Middle"}, + {Mouse::Button::Right, ".button = Mouse::Right"}, + {Mouse::Button::WheelUp, ".button = Mouse::WheelUp"}, + {Mouse::Button::WheelDown, ".button = Mouse::WheelDown"}, + {Mouse::Button::None, ".button = Mouse::None"}, + {Mouse::Button::WheelLeft, ".button = Mouse::WheelLeft"}, + {Mouse::Button::WheelRight, ".button = Mouse::WheelRight"}, + }; + + static std::map mouse_motion_string = { + {Mouse::Motion::Pressed, ".motion = Mouse::Pressed"}, + {Mouse::Motion::Released, ".motion = Mouse::Released"}, + {Mouse::Motion::Moved, ".motion = Mouse::Moved"}, + }; + + switch (type_) { + case Type::Character: { + return "Event::Character(\"" + input_ + "\")"; + } + case Type::Mouse: { + std::string out = "Event::Mouse(\"...\", Mouse{"; + out += std::string(mouse_button_string[data_.mouse.button]); + out += ", "; + out += std::string(mouse_motion_string[data_.mouse.motion]); + out += ", "; + if (data_.mouse.shift) { + out += ".shift = true, "; + } + if (data_.mouse.meta) { + out += ".meta = true, "; + } + if (data_.mouse.control) { + out += ".control = true, "; + } + out += ".x = " + std::to_string(data_.mouse.x); + out += ", "; + out += ".y = " + std::to_string(data_.mouse.y); + out += "})"; + return out; + } + case Type::CursorShape: + return "Event::CursorShape(" + input_ + ", " + + std::to_string(data_.cursor_shape) + ")"; + case Type::CursorPosition: + return "Event::CursorPosition(" + input_ + ", " + + std::to_string(data_.cursor.x) + ", " + + std::to_string(data_.cursor.y) + ")"; + default: { + auto event_it = event_to_string.find(*this); + if (event_it != event_to_string.end()) { + return event_it->second; + } + + return ""; + } + } + return ""; +} + +// clang-format off +// NOLINTBEGIN + // --- Arrow --- -const Event Event::ArrowLeft = Event::Special("\x1B[D"); // NOLINT -const Event Event::ArrowRight = Event::Special("\x1B[C"); // NOLINT -const Event Event::ArrowUp = Event::Special("\x1B[A"); // NOLINT -const Event Event::ArrowDown = Event::Special("\x1B[B"); // NOLINT -const Event Event::ArrowLeftCtrl = Event::Special("\x1B[1;5D"); // NOLINT -const Event Event::ArrowRightCtrl = Event::Special("\x1B[1;5C"); // NOLINT -const Event Event::ArrowUpCtrl = Event::Special("\x1B[1;5A"); // NOLINT -const Event Event::ArrowDownCtrl = Event::Special("\x1B[1;5B"); // NOLINT -const Event Event::Backspace = Event::Special({127}); // NOLINT -const Event Event::Delete = Event::Special("\x1B[3~"); // NOLINT -const Event Event::Escape = Event::Special("\x1B"); // NOLINT -const Event Event::Return = Event::Special({10}); // NOLINT -const Event Event::Tab = Event::Special({9}); // NOLINT -const Event Event::TabReverse = Event::Special({27, 91, 90}); // NOLINT +const Event Event::ArrowLeft = Event::Special("\x1B[D"); +const Event Event::ArrowRight = Event::Special("\x1B[C"); +const Event Event::ArrowUp = Event::Special("\x1B[A"); +const Event Event::ArrowDown = Event::Special("\x1B[B"); +const Event Event::ArrowLeftCtrl = Event::Special("\x1B[1;5D"); +const Event Event::ArrowRightCtrl = Event::Special("\x1B[1;5C"); +const Event Event::ArrowUpCtrl = Event::Special("\x1B[1;5A"); +const Event Event::ArrowDownCtrl = Event::Special("\x1B[1;5B"); +const Event Event::Backspace = Event::Special({127}); +const Event Event::Delete = Event::Special("\x1B[3~"); +const Event Event::Escape = Event::Special("\x1B"); +const Event Event::Return = Event::Special({10}); +const Event Event::Tab = Event::Special({9}); +const Event Event::TabReverse = Event::Special({27, 91, 90}); // See https://invisible-island.net/xterm/xterm-function-keys.html // We follow xterm-new / vterm-xf86-v4 / mgt / screen -const Event Event::F1 = Event::Special("\x1BOP"); // NOLINT -const Event Event::F2 = Event::Special("\x1BOQ"); // NOLINT -const Event Event::F3 = Event::Special("\x1BOR"); // NOLINT -const Event Event::F4 = Event::Special("\x1BOS"); // NOLINT -const Event Event::F5 = Event::Special("\x1B[15~"); // NOLINT -const Event Event::F6 = Event::Special("\x1B[17~"); // NOLINT -const Event Event::F7 = Event::Special("\x1B[18~"); // NOLINT -const Event Event::F8 = Event::Special("\x1B[19~"); // NOLINT -const Event Event::F9 = Event::Special("\x1B[20~"); // NOLINT -const Event Event::F10 = Event::Special("\x1B[21~"); // NOLINT -const Event Event::F11 = Event::Special("\x1B[23~"); // NOLINT -const Event Event::F12 = Event::Special("\x1B[24~"); // NOLINT +const Event Event::F1 = Event::Special("\x1BOP"); +const Event Event::F2 = Event::Special("\x1BOQ"); +const Event Event::F3 = Event::Special("\x1BOR"); +const Event Event::F4 = Event::Special("\x1BOS"); +const Event Event::F5 = Event::Special("\x1B[15~"); +const Event Event::F6 = Event::Special("\x1B[17~"); +const Event Event::F7 = Event::Special("\x1B[18~"); +const Event Event::F8 = Event::Special("\x1B[19~"); +const Event Event::F9 = Event::Special("\x1B[20~"); +const Event Event::F10 = Event::Special("\x1B[21~"); +const Event Event::F11 = Event::Special("\x1B[23~"); +const Event Event::F12 = Event::Special("\x1B[24~"); -const Event Event::Insert = Event::Special("\x1B[2~"); // NOLINT -const Event Event::Home = Event::Special({27, 91, 72}); // NOLINT -const Event Event::End = Event::Special({27, 91, 70}); // NOLINT -const Event Event::PageUp = Event::Special({27, 91, 53, 126}); // NOLINT -const Event Event::PageDown = Event::Special({27, 91, 54, 126}); // NOLINT -const Event Event::Custom = Event::Special({0}); // NOLINT +const Event Event::Insert = Event::Special("\x1B[2~"); +const Event Event::Home = Event::Special({27, 91, 72}); +const Event Event::End = Event::Special({27, 91, 70}); +const Event Event::PageUp = Event::Special({27, 91, 53, 126}); +const Event Event::PageDown = Event::Special({27, 91, 54, 126}); +const Event Event::Custom = Event::Special({0}); + +const Event Event::a = Event::Character("a"); +const Event Event::b = Event::Character("b"); +const Event Event::c = Event::Character("c"); +const Event Event::d = Event::Character("d"); +const Event Event::e = Event::Character("e"); +const Event Event::f = Event::Character("f"); +const Event Event::g = Event::Character("g"); +const Event Event::h = Event::Character("h"); +const Event Event::i = Event::Character("i"); +const Event Event::j = Event::Character("j"); +const Event Event::k = Event::Character("k"); +const Event Event::l = Event::Character("l"); +const Event Event::m = Event::Character("m"); +const Event Event::n = Event::Character("n"); +const Event Event::o = Event::Character("o"); +const Event Event::p = Event::Character("p"); +const Event Event::q = Event::Character("q"); +const Event Event::r = Event::Character("r"); +const Event Event::s = Event::Character("s"); +const Event Event::t = Event::Character("t"); +const Event Event::u = Event::Character("u"); +const Event Event::v = Event::Character("v"); +const Event Event::w = Event::Character("w"); +const Event Event::x = Event::Character("x"); +const Event Event::y = Event::Character("y"); +const Event Event::z = Event::Character("z"); + +const Event Event::A = Event::Character("A"); +const Event Event::B = Event::Character("B"); +const Event Event::C = Event::Character("C"); +const Event Event::D = Event::Character("D"); +const Event Event::E = Event::Character("E"); +const Event Event::F = Event::Character("F"); +const Event Event::G = Event::Character("G"); +const Event Event::H = Event::Character("H"); +const Event Event::I = Event::Character("I"); +const Event Event::J = Event::Character("J"); +const Event Event::K = Event::Character("K"); +const Event Event::L = Event::Character("L"); +const Event Event::M = Event::Character("M"); +const Event Event::N = Event::Character("N"); +const Event Event::O = Event::Character("O"); +const Event Event::P = Event::Character("P"); +const Event Event::Q = Event::Character("Q"); +const Event Event::R = Event::Character("R"); +const Event Event::S = Event::Character("S"); +const Event Event::T = Event::Character("T"); +const Event Event::U = Event::Character("U"); +const Event Event::V = Event::Character("V"); +const Event Event::W = Event::Character("W"); +const Event Event::X = Event::Character("X"); +const Event Event::Y = Event::Character("Y"); +const Event Event::Z = Event::Character("Z"); + +const Event Event::CtrlA = Event::Special("\x01"); +const Event Event::CtrlB = Event::Special("\x02"); +const Event Event::CtrlC = Event::Special("\x03"); +const Event Event::CtrlD = Event::Special("\x04"); +const Event Event::CtrlE = Event::Special("\x05"); +const Event Event::CtrlF = Event::Special("\x06"); +const Event Event::CtrlG = Event::Special("\x07"); +const Event Event::CtrlH = Event::Special("\x08"); +const Event Event::CtrlI = Event::Special("\x09"); +const Event Event::CtrlJ = Event::Special("\x0a"); +const Event Event::CtrlK = Event::Special("\x0b"); +const Event Event::CtrlL = Event::Special("\x0c"); +const Event Event::CtrlM = Event::Special("\x0d"); +const Event Event::CtrlN = Event::Special("\x0e"); +const Event Event::CtrlO = Event::Special("\x0f"); +const Event Event::CtrlP = Event::Special("\x10"); +const Event Event::CtrlQ = Event::Special("\x11"); +const Event Event::CtrlR = Event::Special("\x12"); +const Event Event::CtrlS = Event::Special("\x13"); +const Event Event::CtrlT = Event::Special("\x14"); +const Event Event::CtrlU = Event::Special("\x15"); +const Event Event::CtrlV = Event::Special("\x16"); +const Event Event::CtrlW = Event::Special("\x17"); +const Event Event::CtrlX = Event::Special("\x18"); +const Event Event::CtrlY = Event::Special("\x19"); +const Event Event::CtrlZ = Event::Special("\x1a"); + +const Event Event::AltA = Event::Special("\x1b""a"); +const Event Event::AltB = Event::Special("\x1b""b"); +const Event Event::AltC = Event::Special("\x1b""c"); +const Event Event::AltD = Event::Special("\x1b""d"); +const Event Event::AltE = Event::Special("\x1b""e"); +const Event Event::AltF = Event::Special("\x1b""f"); +const Event Event::AltG = Event::Special("\x1b""g"); +const Event Event::AltH = Event::Special("\x1b""h"); +const Event Event::AltI = Event::Special("\x1b""i"); +const Event Event::AltJ = Event::Special("\x1b""j"); +const Event Event::AltK = Event::Special("\x1b""k"); +const Event Event::AltL = Event::Special("\x1b""l"); +const Event Event::AltM = Event::Special("\x1b""m"); +const Event Event::AltN = Event::Special("\x1b""n"); +const Event Event::AltO = Event::Special("\x1b""o"); +const Event Event::AltP = Event::Special("\x1b""p"); +const Event Event::AltQ = Event::Special("\x1b""q"); +const Event Event::AltR = Event::Special("\x1b""r"); +const Event Event::AltS = Event::Special("\x1b""s"); +const Event Event::AltT = Event::Special("\x1b""t"); +const Event Event::AltU = Event::Special("\x1b""u"); +const Event Event::AltV = Event::Special("\x1b""v"); +const Event Event::AltW = Event::Special("\x1b""w"); +const Event Event::AltX = Event::Special("\x1b""x"); +const Event Event::AltY = Event::Special("\x1b""y"); +const Event Event::AltZ = Event::Special("\x1b""z"); + +const Event Event::CtrlAltA = Event::Special("\x1b\x01"); +const Event Event::CtrlAltB = Event::Special("\x1b\x02"); +const Event Event::CtrlAltC = Event::Special("\x1b\x03"); +const Event Event::CtrlAltD = Event::Special("\x1b\x04"); +const Event Event::CtrlAltE = Event::Special("\x1b\x05"); +const Event Event::CtrlAltF = Event::Special("\x1b\x06"); +const Event Event::CtrlAltG = Event::Special("\x1b\x07"); +const Event Event::CtrlAltH = Event::Special("\x1b\x08"); +const Event Event::CtrlAltI = Event::Special("\x1b\x09"); +const Event Event::CtrlAltJ = Event::Special("\x1b\x0a"); +const Event Event::CtrlAltK = Event::Special("\x1b\x0b"); +const Event Event::CtrlAltL = Event::Special("\x1b\x0c"); +const Event Event::CtrlAltM = Event::Special("\x1b\x0d"); +const Event Event::CtrlAltN = Event::Special("\x1b\x0e"); +const Event Event::CtrlAltO = Event::Special("\x1b\x0f"); +const Event Event::CtrlAltP = Event::Special("\x1b\x10"); +const Event Event::CtrlAltQ = Event::Special("\x1b\x11"); +const Event Event::CtrlAltR = Event::Special("\x1b\x12"); +const Event Event::CtrlAltS = Event::Special("\x1b\x13"); +const Event Event::CtrlAltT = Event::Special("\x1b\x14"); +const Event Event::CtrlAltU = Event::Special("\x1b\x15"); +const Event Event::CtrlAltV = Event::Special("\x1b\x16"); +const Event Event::CtrlAltW = Event::Special("\x1b\x17"); +const Event Event::CtrlAltX = Event::Special("\x1b\x18"); +const Event Event::CtrlAltY = Event::Special("\x1b\x19"); +const Event Event::CtrlAltZ = Event::Special("\x1b\x1a"); + +// NOLINTEND +// clang-format on } // namespace ftxui diff --git a/src/ftxui/component/screen_interactive.cpp b/src/ftxui/component/screen_interactive.cpp index 645f426..3317063 100644 --- a/src/ftxui/component/screen_interactive.cpp +++ b/src/ftxui/component/screen_interactive.cpp @@ -132,7 +132,6 @@ void EventListener(std::atomic* quit, Sender out) { // Read char from the terminal. void EventListener(std::atomic* quit, Sender out) { - (void)timeout_microseconds; auto parser = TerminalInputParser(std::move(out)); char c; @@ -560,6 +559,18 @@ Closure ScreenInteractive::WithRestoredIO(Closure fn) { // NOLINT }; } +/// @brief Force FTXUI to handle or not handle Ctrl-C, even if the component +/// catches the Event::CtrlC. +void ScreenInteractive::ForceHandleCtrlC(bool force) { + force_handle_ctrl_c_ = force; +} + +/// @brief Force FTXUI to handle or not handle Ctrl-Z, even if the component +/// catches the Event::CtrlZ. +void ScreenInteractive::ForceHandleCtrlZ(bool force) { + force_handle_ctrl_z_ = force; +} + /// @brief Return the currently active screen, or null if none. // static ScreenInteractive* ScreenInteractive::Active() { @@ -568,7 +579,6 @@ ScreenInteractive* ScreenInteractive::Active() { // private void ScreenInteractive::Install() { - frame_valid_ = false; // Flush the buffer for stdout to ensure whatever the user has printed before @@ -638,13 +648,31 @@ void ScreenInteractive::Install() { tcgetattr(STDIN_FILENO, &terminal); on_exit_functions.push([=] { tcsetattr(STDIN_FILENO, TCSANOW, &terminal); }); - terminal.c_lflag &= ~ICANON; // NOLINT Non canonique terminal. - terminal.c_lflag &= ~ECHO; // NOLINT Do not print after a key press. - terminal.c_cc[VMIN] = 0; - terminal.c_cc[VTIME] = 0; - // 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); }); + // Enabling raw terminal input mode + terminal.c_iflag &= ~IGNBRK; // Disable ignoring break condition + terminal.c_iflag &= ~BRKINT; // Disable break causing input and output to be + // flushed + terminal.c_iflag &= ~PARMRK; // Disable marking parity errors. + terminal.c_iflag &= ~ISTRIP; // Disable striping 8th bit off characters. + terminal.c_iflag &= ~INLCR; // Disable mapping NL to CR. + terminal.c_iflag &= ~IGNCR; // Disable ignoring CR. + terminal.c_iflag &= ~ICRNL; // Disable mapping CR to NL. + terminal.c_iflag &= ~IXON; // Disable XON/XOFF flow control on output + + terminal.c_lflag &= ~ECHO; // Disable echoing input characters. + terminal.c_lflag &= ~ECHONL; // Disable echoing new line characters. + terminal.c_lflag &= ~ICANON; // Disable Canonical mode. + terminal.c_lflag &= ~ISIG; // Disable sending signal when hitting: + // - => DSUSP + // - C-Z => SUSP + // - C-C => INTR + // - C-d => QUIT + terminal.c_lflag &= ~IEXTEN; // Disable extended input processing + terminal.c_cflag |= CS8; // 8 bits per byte + + terminal.c_cc[VMIN] = 0; // Minimum number of characters for non-canonical + // read. + terminal.c_cc[VTIME] = 0; // Timeout in deciseconds for non-canonical read. tcsetattr(STDIN_FILENO, TCSANOW, &terminal); @@ -720,6 +748,7 @@ void ScreenInteractive::RunOnce(Component component) { } // private +// NOLINTNEXTLINE void ScreenInteractive::HandleTask(Component component, Task& task) { std::visit( [&](auto&& arg) { @@ -745,7 +774,19 @@ void ScreenInteractive::HandleTask(Component component, Task& task) { } arg.screen_ = this; - component->OnEvent(arg); + + const bool handled = component->OnEvent(arg); + + if (arg == Event::CtrlC && (!handled || force_handle_ctrl_c_)) { + RecordSignal(SIGABRT); + } + +#if !defined(_WIN32) + if (arg == Event::CtrlZ && (!handled || force_handle_ctrl_z_)) { + RecordSignal(SIGTSTP); + } +#endif + frame_valid_ = false; return; } diff --git a/src/ftxui/component/screen_interactive_test.cpp b/src/ftxui/component/screen_interactive_test.cpp index 3a80c13..73562fe 100644 --- a/src/ftxui/component/screen_interactive_test.cpp +++ b/src/ftxui/component/screen_interactive_test.cpp @@ -64,4 +64,71 @@ TEST(ScreenInteractive, PostTaskToNonActive) { screen.Post([] {}); } +TEST(ScreenInteractive, CtrlC) { + auto screen = ScreenInteractive::FitComponent(); + bool called = false; + auto component = Renderer([&] { + if (!called) { + called = true; + screen.PostEvent(Event::CtrlC); + } + return text(""); + }); + screen.Loop(component); +} + +TEST(ScreenInteractive, CtrlC_Forced) { + auto screen = ScreenInteractive::FitComponent(); + screen.ForceHandleCtrlC(true); + auto component = Renderer([&] { + screen.PostEvent(Event::CtrlC); + return text(""); + }); + + int ctrl_c_count = 0; + component |= CatchEvent([&](Event event) { + if (event != Event::CtrlC) { + return false; + } + + ++ctrl_c_count; + + if (ctrl_c_count == 100) { + return false; + } + + return true; + }); + screen.Loop(component); + + ASSERT_LE(ctrl_c_count, 50); +} + +TEST(ScreenInteractive, CtrlC_NotForced) { + auto screen = ScreenInteractive::FitComponent(); + screen.ForceHandleCtrlC(false); + auto component = Renderer([&] { + screen.PostEvent(Event::CtrlC); + return text(""); + }); + + int ctrl_c_count = 0; + component |= CatchEvent([&](Event event) { + if (event != Event::CtrlC) { + return false; + } + + ++ctrl_c_count; + + if (ctrl_c_count == 100) { + return false; + } + + return true; + }); + screen.Loop(component); + + ASSERT_GE(ctrl_c_count, 50); +} + } // namespace ftxui diff --git a/src/ftxui/component/terminal_input_parser.cpp b/src/ftxui/component/terminal_input_parser.cpp index cb48604..f07afa9 100644 --- a/src/ftxui/component/terminal_input_parser.cpp +++ b/src/ftxui/component/terminal_input_parser.cpp @@ -170,15 +170,8 @@ TerminalInputParser::Output TerminalInputParser::Parse() { return UNCOMPLETED; } - switch (Current()) { - case 24: // CAN NOLINT - case 26: // SUB NOLINT - return DROP; - - case '\x1B': - return ParseESC(); - default: - break; + if (Current() == '\x1B') { + return ParseESC(); } if (Current() < 32) { // C0 NOLINT @@ -282,12 +275,25 @@ TerminalInputParser::Output TerminalInputParser::ParseESC() { return ParseCSI(); case ']': return ParseOSC(); - default: + + // Expecting 2 characters. + case ' ': + case '#': + case '%': + case '(': + case ')': + case '*': + case '+': + case 'O': + case 'N': { if (!Eat()) { return UNCOMPLETED; - } else { - return SPECIAL; } + return SPECIAL; + } + // Expecting 1 character: + default: + return SPECIAL; } } diff --git a/src/ftxui/component/terminal_input_parser_test.cpp b/src/ftxui/component/terminal_input_parser_test.cpp index 9210fdd..ae60ff7 100644 --- a/src/ftxui/component/terminal_input_parser_test.cpp +++ b/src/ftxui/component/terminal_input_parser_test.cpp @@ -76,6 +76,24 @@ TEST(Event, EscapeKeyEnoughWait) { EXPECT_FALSE(event_receiver->Receive(&received)); } +TEST(Event, EscapeFast) { + auto event_receiver = MakeReceiver(); + { + auto parser = TerminalInputParser(event_receiver->MakeSender()); + parser.Add('\x1B'); + parser.Add('a'); + parser.Add('\x1B'); + parser.Add('b'); + parser.Timeout(49); + } + Task received; + EXPECT_TRUE(event_receiver->Receive(&received)); + EXPECT_EQ(std::get(received), Event::AltA); + EXPECT_TRUE(event_receiver->Receive(&received)); + EXPECT_EQ(std::get(received), Event::AltB); + EXPECT_FALSE(event_receiver->Receive(&received)); +} + TEST(Event, MouseLeftClickPressed) { auto event_receiver = MakeReceiver(); { @@ -335,8 +353,8 @@ TEST(Event, Control) { continue; cases.push_back({char(i), false}); } - cases.push_back({char(24), true}); - cases.push_back({char(26), true}); + cases.push_back({char(24), false}); + cases.push_back({char(26), false}); cases.push_back({char(127), false}); for (auto test : cases) { @@ -367,13 +385,11 @@ TEST(Event, Special) { std::vector input; Event expected; } kTestCase[] = { - // Arrow (defaut cursor mode) - {str("\x1B[A"), Event::ArrowUp}, - {str("\x1B[B"), Event::ArrowDown}, - {str("\x1B[C"), Event::ArrowRight}, - {str("\x1B[D"), Event::ArrowLeft}, - {str("\x1B[H"), Event::Home}, - {str("\x1B[F"), Event::End}, + // Arrow (default cursor mode) + {str("\x1B[A"), Event::ArrowUp}, {str("\x1B[B"), Event::ArrowDown}, + {str("\x1B[C"), Event::ArrowRight}, {str("\x1B[D"), Event::ArrowLeft}, + {str("\x1B[H"), Event::Home}, {str("\x1B[F"), Event::End}, + /* // Arrow (application cursor mode) {str("\x1BOA"), Event::ArrowUp}, @@ -454,6 +470,7 @@ TEST(Event, Special) { // Custom: {{0}, Event::Custom}, + */ }; for (auto test : kTestCase) { diff --git a/src/ftxui/dom/canvas.cpp b/src/ftxui/dom/canvas.cpp index 81d415b..8bf2051 100644 --- a/src/ftxui/dom/canvas.cpp +++ b/src/ftxui/dom/canvas.cpp @@ -828,7 +828,7 @@ void Canvas::DrawPixel(int x, int y, const Pixel& p) { } /// @brief Draw a predefined image, with top-left corner at the given coordinate -/// You can supply negative coordinates to align the image however you like - +/// You can supply negative coordinates to align the image however you like - /// only the 'visible' portion will be drawn /// @param x the x coordinate corresponding to the top-left corner of the image. /// @param y the y coordinate corresponding to the top-left corner of the image. diff --git a/src/ftxui/screen/image.cpp b/src/ftxui/screen/image.cpp index adee91b..4df80fd 100644 --- a/src/ftxui/screen/image.cpp +++ b/src/ftxui/screen/image.cpp @@ -10,17 +10,16 @@ #include // for pair #include "ftxui/screen/image.hpp" -#include "ftxui/screen/string.hpp" // for string_width +#include "ftxui/screen/string.hpp" // for string_width namespace ftxui { -namespace -{ - Pixel& dev_null_pixel() { - static Pixel pixel; - return pixel; - } +namespace { +Pixel& dev_null_pixel() { + static Pixel pixel; + return pixel; } +} // namespace Image::Image(int dimx, int dimy) : stencil{0, dimx - 1, 0, dimy - 1}, diff --git a/src/ftxui/screen/screen.cpp b/src/ftxui/screen/screen.cpp index 981a3f6..28ee767 100644 --- a/src/ftxui/screen/screen.cpp +++ b/src/ftxui/screen/screen.cpp @@ -391,9 +391,9 @@ Screen::Screen(int dimx, int dimy) : Image{dimx, dimy} { #if defined(_WIN32) // The placement of this call is a bit weird, however we can assume that // anybody who instantiates a Screen object eventually wants to output - // something to the console. If that is not the case, use an instance of Image instead. - // As we require UTF8 for all input/output operations we will just switch to - // UTF8 encoding here + // something to the console. If that is not the case, use an instance of Image + // instead. As we require UTF8 for all input/output operations we will just + // switch to UTF8 encoding here SetConsoleOutputCP(CP_UTF8); SetConsoleCP(CP_UTF8); WindowsEmulateVT100Terminal();