2 Commits

Author SHA1 Message Date
ArthurSonzogni
ce9964131d Coding style + fix tests 2025-06-24 14:53:15 +02:00
Jørn Gustav Larsen
2715503516 First version of supporting extraction of the terminal id. 2025-06-24 11:27:35 +02:00
21 changed files with 522 additions and 308 deletions

View File

@@ -144,6 +144,7 @@ add_library(component
src/ftxui/component/resizable_split.cpp src/ftxui/component/resizable_split.cpp
src/ftxui/component/screen_interactive.cpp src/ftxui/component/screen_interactive.cpp
src/ftxui/component/slider.cpp src/ftxui/component/slider.cpp
src/ftxui/component/terminal_id.cpp
src/ftxui/component/terminal_input_parser.cpp src/ftxui/component/terminal_input_parser.cpp
src/ftxui/component/terminal_input_parser.hpp src/ftxui/component/terminal_input_parser.hpp
src/ftxui/component/util.cpp src/ftxui/component/util.cpp

View File

@@ -46,6 +46,7 @@ example(slider_direction)
example(slider_rgb) example(slider_rgb)
example(tab_horizontal) example(tab_horizontal)
example(tab_vertical) example(tab_vertical)
example(terminal_id)
example(textarea) example(textarea)
example(toggle) example(toggle)
example(window) example(window)

View File

@@ -1,64 +1,9 @@
// Copyright 2020 Arthur Sonzogni. All rights reserved. #include "ftxui/component/component.hpp"
// Use of this source code is governed by the MIT license that can be found in #include "ftxui/component/screen_interactive.hpp"
// the LICENSE file.
#include <memory> // for shared_ptr, __shared_ptr_access
#include <string> // for operator+, to_string
#include "ftxui/component/captured_mouse.hpp" // for ftxui int main(){
#include "ftxui/component/component.hpp" // for Button, Horizontal, Renderer auto screen = ftxui::ScreenInteractive::Fullscreen();
#include "ftxui/component/component_base.hpp" // for ComponentBase auto testComponent = ftxui::Renderer([](){return ftxui::text("test Component");});
#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive screen.Loop(testComponent);
#include "ftxui/dom/elements.hpp" // for separator, gauge, text, Element, operator|, vbox, border
using namespace ftxui;
// This is a helper function to create a button with a custom style.
// The style is defined by a lambda function that takes an EntryState and
// returns an Element.
// We are using `center` to center the text inside the button, then `border` to
// add a border around the button, and finally `flex` to make the button fill
// the available space.
ButtonOption Style() {
auto option = ButtonOption::Animated();
option.transform = [](const EntryState& s) {
auto element = text(s.label);
if (s.focused) {
element |= bold;
}
return element | center | borderEmpty | flex;
};
return option;
}
int main() {
int value = 50;
// clang-format off
auto btn_dec_01 = Button("-1", [&] { value += 1; }, Style());
auto btn_inc_01 = Button("+1", [&] { value -= 1; }, Style());
auto btn_dec_10 = Button("-10", [&] { value -= 10; }, Style());
auto btn_inc_10 = Button("+10", [&] { value += 10; }, Style());
// clang-format on
// The tree of components. This defines how to navigate using the keyboard.
// The selected `row` is shared to get a grid layout.
int row = 0;
auto buttons = Container::Vertical({
Container::Horizontal({btn_dec_01, btn_inc_01}, &row) | flex,
Container::Horizontal({btn_dec_10, btn_inc_10}, &row) | flex,
});
// Modify the way to render them on screen:
auto component = Renderer(buttons, [&] {
return vbox({
text("value = " + std::to_string(value)),
separator(),
buttons->Render() | flex,
}) |
flex | border;
});
auto screen = ScreenInteractive::FitComponent();
screen.Loop(component);
return 0; return 0;
} }

View File

@@ -0,0 +1,33 @@
// Copyright 2025 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 "ftxui/component/component.hpp" // for Renderer
#include "ftxui/component/screen_interactive.hpp" // for ScreenInteractive
#include "ftxui/dom/elements.hpp" // for text, hbox, separator, Element, operator|, vbox, border
int main() {
using namespace ftxui;
std::string terminal_id = "UNKNOWN";
auto screen =
ScreenInteractive::TerminalOutput();
screen.OnTerminalIDUpdate([&terminal_id] (
TerminalID const& terminal_id_update)
{
std::stringstream stream;
stream << terminal_id_update;
terminal_id = stream.str();
});
auto renderer = Renderer([&]
{
return vbox({
text("Terminal id " + terminal_id),
}) | border;
});
screen.Loop(renderer);
}

View File

@@ -5,6 +5,7 @@
#define FTXUI_COMPONENT_EVENT_HPP #define FTXUI_COMPONENT_EVENT_HPP
#include <ftxui/component/mouse.hpp> // for Mouse #include <ftxui/component/mouse.hpp> // for Mouse
#include <ftxui/component/terminal_id.hpp> // for TerminalID
#include <string> // for string, operator== #include <string> // for string, operator==
namespace ftxui { namespace ftxui {
@@ -35,6 +36,7 @@ struct Event {
static Event Mouse(std::string, Mouse mouse); static Event Mouse(std::string, Mouse mouse);
static Event CursorPosition(std::string, int x, int y); // Internal static Event CursorPosition(std::string, int x, int y); // Internal
static Event CursorShape(std::string, int shape); // Internal static Event CursorShape(std::string, int shape); // Internal
static Event TerminalID(std::string input, enum TerminalID terminal_id); // Internal
// --- Arrow --- // --- Arrow ---
static const Event ArrowLeft; static const Event ArrowLeft;
@@ -117,6 +119,9 @@ struct Event {
bool is_cursor_shape() const { return type_ == Type::CursorShape; } bool is_cursor_shape() const { return type_ == Type::CursorShape; }
int cursor_shape() const { return data_.cursor_shape; } int cursor_shape() const { return data_.cursor_shape; }
bool is_terminal_id() const { return type_ == Type::TerminalID; }
enum TerminalID terminal_id() const { return data_.terminal_id; }
// Debug // Debug
std::string DebugString() const; std::string DebugString() const;
@@ -132,6 +137,7 @@ struct Event {
Mouse, Mouse,
CursorPosition, CursorPosition,
CursorShape, CursorShape,
TerminalID
}; };
Type type_ = Type::Unknown; Type type_ = Type::Unknown;
@@ -144,6 +150,7 @@ struct Event {
struct Mouse mouse; struct Mouse mouse;
struct Cursor cursor; struct Cursor cursor;
int cursor_shape; int cursor_shape;
enum TerminalID terminal_id;
} data_ = {}; } data_ = {};
std::string input_; std::string input_;

View File

@@ -10,6 +10,7 @@
#include <memory> // for shared_ptr #include <memory> // for shared_ptr
#include <string> // for string #include <string> // for string
#include <thread> // for thread #include <thread> // for thread
#include <list>
#include "ftxui/component/animation.hpp" // for TimePoint #include "ftxui/component/animation.hpp" // for TimePoint
#include "ftxui/component/captured_mouse.hpp" // for CapturedMouse #include "ftxui/component/captured_mouse.hpp" // for CapturedMouse
@@ -24,6 +25,8 @@ class Loop;
struct Event; struct Event;
using Component = std::shared_ptr<ComponentBase>; using Component = std::shared_ptr<ComponentBase>;
using TerminalIDUpdateCallback = std::function<void (const TerminalID &)>;
class ScreenInteractivePrivate; class ScreenInteractivePrivate;
/// @brief ScreenInteractive is a `Screen` that can handle events, run a main /// @brief ScreenInteractive is a `Screen` that can handle events, run a main
@@ -72,6 +75,10 @@ class ScreenInteractive : public Screen {
void ForceHandleCtrlC(bool force); void ForceHandleCtrlC(bool force);
void ForceHandleCtrlZ(bool force); void ForceHandleCtrlZ(bool force);
TerminalID TerminalId() const;
void OnTerminalIDUpdate(const TerminalIDUpdateCallback& callback);
// Selection API. // Selection API.
std::string GetSelection(); std::string GetSelection();
void SelectionChange(std::function<void()> callback); void SelectionChange(std::function<void()> callback);
@@ -156,6 +163,11 @@ class ScreenInteractive : public Screen {
std::unique_ptr<Selection> selection_; std::unique_ptr<Selection> selection_;
std::function<void()> selection_on_change_; std::function<void()> selection_on_change_;
TerminalID m_terminal_id = TerminalID::Unknown;
using TerminalIDUpdateCallbackContainer = std::list<TerminalIDUpdateCallback>;
TerminalIDUpdateCallbackContainer terminal_id_callback_;
friend class Loop; friend class Loop;
public: public:

View File

@@ -0,0 +1,35 @@
// Copyright 2025 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in
// the LICENSE file.
#ifndef FTXUI_COMPONENT_TERMINAL_ID_HPP
#define FTXUI_COMPONENT_TERMINAL_ID_HPP
#include <ostream>
#include <string>
namespace ftxui {
/// @brief The TerminalID enum class represents different types of terminal
/// emulators that FTXUI can detect. It is used to identify the terminal
/// emulator in use, which can affect how FTXUI renders its output and handles
/// input events.
/// @ingroup component
enum class TerminalID {
Unknown,
// --
Konsole,
LinuxVC,
Urxvt,
Vte,
Xterm,
};
std::ostream& operator<<(
std::ostream& os,
TerminalID terminal_id);
} // namespace ftxui
#endif /* end of include guard: FTXUI_COMPONENT_TERMINAL_ID_HPP */

View File

@@ -12,6 +12,7 @@
namespace ftxui { namespace ftxui {
/// @brief FlexboxConfig is a configuration structure that defines the layout /// @brief FlexboxConfig is a configuration structure that defines the layout
/// properties for a flexbox container. /// properties for a flexbox container.
// //

View File

@@ -139,6 +139,7 @@ class ButtonBase : public ComponentBase, public ButtonOption {
private: private:
bool mouse_hover_ = false; bool mouse_hover_ = false;
Box box_; Box box_;
ButtonOption option_;
float animation_background_ = 0; float animation_background_ = 0;
float animation_foreground_ = 0; float animation_foreground_ = 0;
animation::Animator animator_background_ = animation::Animator animator_background_ =

View File

@@ -2,7 +2,6 @@
// Use of this source code is governed by the MIT license that can be found in // Use of this source code is governed by the MIT license that can be found in
// the LICENSE file. // the LICENSE file.
#include <cassert> #include <cassert>
#include <ftxui/component/event.hpp>
#include <vector> #include <vector>
#include "ftxui/component/component.hpp" #include "ftxui/component/component.hpp"
#include "ftxui/component/terminal_input_parser.hpp" #include "ftxui/component/terminal_input_parser.hpp"
@@ -213,17 +212,16 @@ extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
auto screen = auto screen =
Screen::Create(Dimension::Fixed(width), Dimension::Fixed(height)); Screen::Create(Dimension::Fixed(width), Dimension::Fixed(height));
// Generate some events. auto event_receiver = MakeReceiver<Task>();
std::vector<Event> events; {
auto parser = auto parser = TerminalInputParser(event_receiver->MakeSender());
TerminalInputParser([&](const Event& event) { events.push_back(event); }); for (size_t i = 0; i < size; ++i)
for (size_t i = 0; i < size; ++i) {
parser.Add(data[i]); parser.Add(data[i]);
} }
for (const auto& event : events) { Task event;
component->OnEvent(event); while (event_receiver->Receive(&event)) {
component->OnEvent(std::get<Event>(event));
auto document = component->Render(); auto document = component->Render();
Render(screen, document); Render(screen, document);
} }

View File

@@ -87,6 +87,16 @@ Event Event::CursorPosition(std::string input, int x, int y) {
return event; return event;
} }
/// @internal
// static
Event Event::TerminalID(std::string input, enum TerminalID terminal_id) {
Event event;
event.input_ = std::move(input);
event.type_ = Type::TerminalID;
event.data_.terminal_id = terminal_id;
return event;
}
/// @brief Return a string representation of the event. /// @brief Return a string representation of the event.
std::string Event::DebugString() const { std::string Event::DebugString() const {
static std::map<Event, const char*> event_to_string = { static std::map<Event, const char*> event_to_string = {

View File

@@ -1,6 +1,7 @@
// Copyright 2022 Arthur Sonzogni. All rights reserved. // Copyright 2022 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in // Use of this source code is governed by the MIT license that can be found in
// the LICENSE file. // the LICENSE file.
#include <iostream>
#include "ftxui/component/loop.hpp" #include "ftxui/component/loop.hpp"
#include <utility> // for move #include <utility> // for move

View File

@@ -84,8 +84,7 @@ constexpr int timeout_milliseconds = 20;
void EventListener(std::atomic<bool>* quit, Sender<Task> out) { void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto console = GetStdHandle(STD_INPUT_HANDLE); auto console = GetStdHandle(STD_INPUT_HANDLE);
auto parser = auto parser = TerminalInputParser(out->Clone());
TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
while (!*quit) { while (!*quit) {
// Throttle ReadConsoleInput by waiting 250ms, this wait function will // Throttle ReadConsoleInput by waiting 250ms, this wait function will
// return if there is input in the console. // return if there is input in the console.
@@ -138,8 +137,7 @@ void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
// Read char from the terminal. // Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Task> out) { void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto parser = auto parser = TerminalInputParser(std::move(out));
TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
char c; char c;
while (!*quit) { while (!*quit) {
@@ -175,8 +173,7 @@ int CheckStdinReady(int usec_timeout) {
// Read char from the terminal. // Read char from the terminal.
void EventListener(std::atomic<bool>* quit, Sender<Task> out) { void EventListener(std::atomic<bool>* quit, Sender<Task> out) {
auto parser = auto parser = TerminalInputParser(std::move(out));
TerminalInputParser([&](Event event) { out->Send(std::move(event)); });
while (!*quit) { while (!*quit) {
if (!CheckStdinReady(timeout_microseconds)) { if (!CheckStdinReady(timeout_microseconds)) {
@@ -324,6 +321,8 @@ std::string DeviceStatusReport(DSRMode ps) {
return CSI + std::to_string(int(ps)) + "n"; return CSI + std::to_string(int(ps)) + "n";
} }
const std::string TerminalIdReport = "\x1b[0c";
class CapturedMouseImpl : public CapturedMouseInterface { class CapturedMouseImpl : public CapturedMouseInterface {
public: public:
explicit CapturedMouseImpl(std::function<void(void)> callback) explicit CapturedMouseImpl(std::function<void(void)> callback)
@@ -727,6 +726,9 @@ void ScreenInteractive::Install() {
enable({DECMode::kMouseSgrExtMode}); enable({DECMode::kMouseSgrExtMode});
} }
// Report the Terminal ID.
std::cout << TerminalIdReport;
// After installing the new configuration, flush it to the terminal to // After installing the new configuration, flush it to the terminal to
// ensure it is fully applied: // ensure it is fully applied:
Flush(); Flush();
@@ -797,6 +799,17 @@ void ScreenInteractive::HandleTask(Component component, Task& task) {
return; return;
} }
if (arg.is_terminal_id()) {
m_terminal_id = arg.terminal_id();
for(auto & callback : terminal_id_callback_) {
if (callback) {
callback(m_terminal_id);
}
}
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_;
@@ -1087,4 +1100,13 @@ bool ScreenInteractive::SelectionData::operator!=(
return !(*this == other); return !(*this == other);
} }
TerminalID ScreenInteractive::TerminalId() const {
return m_terminal_id;
}
void ScreenInteractive::OnTerminalIDUpdate(
const TerminalIDUpdateCallback& callback) {
terminal_id_callback_.push_back(callback);
}
} // namespace ftxui. } // namespace ftxui.

View File

@@ -27,9 +27,9 @@ namespace {
// Capture the standard output (stdout) to a string. // Capture the standard output (stdout) to a string.
class StdCapture { class StdCapture {
public: public:
explicit StdCapture(std::string* captured) : captured_(captured) { explicit StdCapture(std::string* captured)
if (pipe(pipefd_) != 0) : captured_(captured) {
return; if (pipe(pipefd_) != 0) return;
old_stdout_ = dup(fileno(stdout)); old_stdout_ = dup(fileno(stdout));
fflush(stdout); fflush(stdout);
dup2(pipefd_[1], fileno(stdout)); dup2(pipefd_[1], fileno(stdout));
@@ -188,7 +188,9 @@ TEST(ScreenInteractive, FixedSizeInitialFrame) {
auto capture = StdCapture(&output); auto capture = StdCapture(&output);
auto screen = ScreenInteractive::FixedSize(2, 2); auto screen = ScreenInteractive::FixedSize(2, 2);
auto component = Renderer([&] { return text("AB"); }); auto component = Renderer([&] {
return text("AB");
});
Loop loop(&screen, component); Loop loop(&screen, component);
loop.RunOnce(); loop.RunOnce();
@@ -205,6 +207,7 @@ TEST(ScreenInteractive, FixedSizeInitialFrame) {
"\x1B[?1003h" // Enable mouse motion tracking. "\x1B[?1003h" // Enable mouse motion tracking.
"\x1B[?1015h" // Enable mouse wheel tracking. "\x1B[?1015h" // Enable mouse wheel tracking.
"\x1B[?1006h" // Enable SGR mouse tracking. "\x1B[?1006h" // Enable SGR mouse tracking.
"\x1B[0c" // Query terminal ID.
"\0" // Flush stdout. "\0" // Flush stdout.
// Reset the screen. // Reset the screen.
@@ -239,6 +242,7 @@ TEST(ScreenInteractive, FixedSizeInitialFrame) {
"\r\n"sv; "\r\n"sv;
ASSERT_EQ(expected, output); ASSERT_EQ(expected, output);
#endif #endif
} }
} // namespace ftxui } // namespace ftxui

View File

@@ -0,0 +1,41 @@
// Copyright 2025 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 "ftxui/component/terminal_id.hpp"
namespace ftxui
{
std::ostream& operator<<(std::ostream& os, TerminalID terminal_id) {
switch (terminal_id) {
case TerminalID::Unknown: {
os << "Unknown";
break;
}
case TerminalID::Urxvt: {
os << "Urxvt";
break;
}
case TerminalID::LinuxVC: {
os << "LinuxVC";
break;
}
case TerminalID::Konsole: {
os << "Konsole";
break;
}
case TerminalID::Vte: {
os << "Vte";
break;
}
case TerminalID::Xterm: {
os << "Xterm";
break;
}
}
return os;
}
} // namespace ftxui

View File

@@ -5,7 +5,7 @@
#include <cstdint> // for uint32_t #include <cstdint> // for uint32_t
#include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Button, Mouse::Motion #include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Button, Mouse::Motion
#include <functional> // for std::function #include <ftxui/component/receiver.hpp> // for SenderImpl, Sender
#include <map> #include <map>
#include <memory> // for unique_ptr, allocator #include <memory> // for unique_ptr, allocator
#include <utility> // for move #include <utility> // for move
@@ -90,7 +90,7 @@ const std::map<std::string, std::string> g_uniformize = {
{"\x1B[X", "\x1B[24~"}, // F12 {"\x1B[X", "\x1B[24~"}, // F12
}; };
TerminalInputParser::TerminalInputParser(std::function<void(Event)> out) TerminalInputParser::TerminalInputParser(Sender<Task> out)
: out_(std::move(out)) {} : out_(std::move(out)) {}
void TerminalInputParser::Timeout(int time) { void TerminalInputParser::Timeout(int time) {
@@ -131,7 +131,7 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
return; return;
case CHARACTER: case CHARACTER:
out_(Event::Character(std::move(pending_))); out_->Send(Event::Character(std::move(pending_)));
pending_.clear(); pending_.clear();
return; return;
@@ -140,25 +140,30 @@ void TerminalInputParser::Send(TerminalInputParser::Output output) {
if (it != g_uniformize.end()) { if (it != g_uniformize.end()) {
pending_ = it->second; pending_ = it->second;
} }
out_(Event::Special(std::move(pending_))); out_->Send(Event::Special(std::move(pending_)));
pending_.clear(); pending_.clear();
} }
return; return;
case MOUSE: case MOUSE:
out_(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT out_->Send(Event::Mouse(std::move(pending_), output.mouse)); // NOLINT
pending_.clear(); pending_.clear();
return; return;
case CURSOR_POSITION: case CURSOR_POSITION:
out_(Event::CursorPosition(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(); pending_.clear();
return; return;
case CURSOR_SHAPE: case CURSOR_SHAPE:
out_(Event::CursorShape(std::move(pending_), output.cursor_shape)); out_->Send(Event::CursorShape(std::move(pending_), output.cursor_shape));
pending_.clear();
return;
case TERMINAL_ID:
out_->Send(Event::TerminalID(std::move(pending_), output.terminal_id));
pending_.clear(); pending_.clear();
return; return;
} }
@@ -374,6 +379,8 @@ TerminalInputParser::Output TerminalInputParser::ParseCSI() {
return ParseMouse(altered, false, std::move(arguments)); return ParseMouse(altered, false, std::move(arguments));
case 'R': case 'R':
return ParseCursorPosition(std::move(arguments)); return ParseCursorPosition(std::move(arguments));
case 'c':
return ParseTerminalID(std::move(arguments));
default: default:
return SPECIAL; return SPECIAL;
} }
@@ -461,4 +468,22 @@ TerminalInputParser::Output TerminalInputParser::ParseCursorPosition(
return output; return output;
} }
TerminalInputParser::Output TerminalInputParser::ParseTerminalID(
std::vector<int> arguments) {
if (arguments.empty()) {
return TerminalID::Unknown;
}
switch (arguments[0]) {
case 1:
return TerminalID::Xterm;
case 6:
return TerminalID::LinuxVC;
case 62:
return TerminalID::Konsole;
default:
return TerminalID::Unknown;
}
}
} // namespace ftxui } // namespace ftxui

View File

@@ -4,11 +4,13 @@
#ifndef FTXUI_COMPONENT_TERMINAL_INPUT_PARSER #ifndef FTXUI_COMPONENT_TERMINAL_INPUT_PARSER
#define FTXUI_COMPONENT_TERMINAL_INPUT_PARSER #define FTXUI_COMPONENT_TERMINAL_INPUT_PARSER
#include <functional>
#include <string> // for string #include <string> // for string
#include <vector> // for vector #include <vector> // for vector
#include "ftxui/component/mouse.hpp" // for Mouse #include "ftxui/component/mouse.hpp" // for Mouse
#include "ftxui/component/terminal_id.hpp" // for TerminalId
#include "ftxui/component/receiver.hpp" // for Sender
#include "ftxui/component/task.hpp" // for Task
namespace ftxui { namespace ftxui {
struct Event; struct Event;
@@ -16,7 +18,7 @@ struct Event;
// Parse a sequence of |char| accross |time|. Produces |Event|. // Parse a sequence of |char| accross |time|. Produces |Event|.
class TerminalInputParser { class TerminalInputParser {
public: public:
explicit TerminalInputParser(std::function<void(Event)> out); explicit TerminalInputParser(Sender<Task> out);
void Timeout(int time); void Timeout(int time);
void Add(char c); void Add(char c);
@@ -32,6 +34,7 @@ class TerminalInputParser {
CURSOR_POSITION, CURSOR_POSITION,
CURSOR_SHAPE, CURSOR_SHAPE,
SPECIAL, SPECIAL,
TERMINAL_ID
}; };
struct CursorPosition { struct CursorPosition {
@@ -45,10 +48,14 @@ class TerminalInputParser {
Mouse mouse; Mouse mouse;
CursorPosition cursor{}; CursorPosition cursor{};
int cursor_shape; int cursor_shape;
TerminalID terminal_id;
}; };
Output(Type t) // NOLINT Output(Type t) // NOLINT
: type(t) {} : type(t) {}
Output(TerminalID terminal_id) // NOLINT
: type(TERMINAL_ID), terminal_id(terminal_id) {}
}; };
void Send(Output output); void Send(Output output);
@@ -60,8 +67,9 @@ class TerminalInputParser {
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 ParseCursorPosition(std::vector<int> arguments); Output ParseCursorPosition(std::vector<int> arguments);
Output ParseTerminalID(std::vector<int> arguments);
std::function<void(Event)> out_; Sender<Task> out_;
int position_ = -1; int position_ = -1;
int timeout_ = 0; int timeout_ = 0;
std::string pending_; std::string pending_;

View File

@@ -2,12 +2,12 @@
// Use of this source code is governed by the MIT license that can be found in // Use of this source code is governed by the MIT license that can be found in
// the LICENSE file. // the LICENSE file.
#include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Left, Mouse::Middle, Mouse::Pressed, Mouse::Released, Mouse::Right #include <ftxui/component/mouse.hpp> // for Mouse, Mouse::Left, Mouse::Middle, Mouse::Pressed, Mouse::Released, Mouse::Right
#include <functional> // for function #include <ftxui/component/task.hpp> // for Task
#include <initializer_list> // for initializer_list #include <initializer_list> // for initializer_list
#include <memory> // for allocator, unique_ptr #include <memory> // for allocator, unique_ptr
#include <vector> // for vector
#include "ftxui/component/event.hpp" // for Event, Event::Return, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp, Event::Backspace, Event::End, Event::Home, Event::Custom, Event::Delete, Event::F1, Event::F10, Event::F11, Event::F12, Event::F2, Event::F3, Event::F4, Event::F5, Event::F6, Event::F7, Event::F8, Event::F9, Event::PageDown, Event::PageUp, Event::Tab, Event::TabReverse, Event::Escape #include "ftxui/component/event.hpp" // for Event, Event::Return, Event::ArrowDown, Event::ArrowLeft, Event::ArrowRight, Event::ArrowUp, Event::Backspace, Event::End, Event::Home, Event::Custom, Event::Delete, Event::F1, Event::F10, Event::F11, Event::F12, Event::F2, Event::F3, Event::F4, Event::F5, Event::F6, Event::F7, Event::F8, Event::F9, Event::PageDown, Event::PageUp, Event::Tab, Event::TabReverse, Event::Escape
#include "ftxui/component/receiver.hpp" // for MakeReceiver, ReceiverImpl
#include "ftxui/component/terminal_input_parser.hpp" #include "ftxui/component/terminal_input_parser.hpp"
#include "gtest/gtest.h" // for AssertionResult, Test, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, TEST, EXPECT_FALSE #include "gtest/gtest.h" // for AssertionResult, Test, Message, TestPartResult, EXPECT_EQ, EXPECT_TRUE, TEST, EXPECT_FALSE
@@ -22,69 +22,82 @@ TEST(Event, Character) {
for (char c = 'A'; c <= 'Z'; ++c) for (char c = 'A'; c <= 'Z'; ++c)
basic_char.push_back(c); basic_char.push_back(c);
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
for (char c : basic_char) for (char c : basic_char)
parser.Add(c); parser.Add(c);
for (size_t i = 0; i < basic_char.size(); ++i) {
EXPECT_TRUE(received_events[i].is_character());
EXPECT_EQ(basic_char[i], received_events[i].character()[0]);
} }
EXPECT_EQ(received_events.size(), basic_char.size());
Task received;
for (char c : basic_char) {
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_character());
EXPECT_EQ(c, std::get<Event>(received).character()[0]);
}
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, EscapeKeyWithoutWaiting) { TEST(Event, EscapeKeyWithoutWaiting) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
}
EXPECT_TRUE(received_events.empty()); Task received;
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, EscapeKeyNotEnoughWait) { TEST(Event, EscapeKeyNotEnoughWait) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Timeout(49); parser.Timeout(49);
}
EXPECT_TRUE(received_events.empty()); Task received;
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, EscapeKeyEnoughWait) { TEST(Event, EscapeKeyEnoughWait) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Timeout(50); parser.Timeout(50);
}
EXPECT_EQ(1, received_events.size()); Task received;
EXPECT_EQ(received_events[0], Event::Escape); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::Escape);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, EscapeFast) { TEST(Event, EscapeFast) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Add('a'); parser.Add('a');
parser.Add(''); parser.Add('\x1B');
parser.Add('b'); parser.Add('b');
parser.Timeout(49); parser.Timeout(49);
}
EXPECT_EQ(2, received_events.size()); Task received;
EXPECT_EQ(received_events[0], Event::AltA); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(received_events[1], Event::AltB); EXPECT_EQ(std::get<Event>(received), Event::AltA);
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), Event::AltB);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseLeftClickPressed) { TEST(Event, MouseLeftClickPressed) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Add('['); parser.Add('[');
parser.Add('0'); parser.Add('0');
parser.Add(';'); parser.Add(';');
@@ -94,20 +107,23 @@ TEST(Event, MouseLeftClickPressed) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('M'); parser.Add('M');
}
EXPECT_EQ(1, received_events.size()); Task received;
EXPECT_TRUE(received_events[0].is_mouse()); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(Mouse::Left, received_events[0].mouse().button); EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(12, received_events[0].mouse().x); EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button);
EXPECT_EQ(42, received_events[0].mouse().y); EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(received_events[0].mouse().motion, Mouse::Pressed); EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Pressed);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseLeftMoved) { TEST(Event, MouseLeftMoved) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Add('['); parser.Add('[');
parser.Add('3'); parser.Add('3');
parser.Add('2'); parser.Add('2');
@@ -118,20 +134,23 @@ TEST(Event, MouseLeftMoved) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('M'); parser.Add('M');
}
EXPECT_EQ(1, received_events.size()); Task received;
EXPECT_TRUE(received_events[0].is_mouse()); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(Mouse::Left, received_events[0].mouse().button); EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(12, received_events[0].mouse().x); EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button);
EXPECT_EQ(42, received_events[0].mouse().y); EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(received_events[0].mouse().motion, Mouse::Moved); EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Moved);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseLeftClickReleased) { TEST(Event, MouseLeftClickReleased) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Add('['); parser.Add('[');
parser.Add('0'); parser.Add('0');
parser.Add(';'); parser.Add(';');
@@ -141,20 +160,23 @@ TEST(Event, MouseLeftClickReleased) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('m'); parser.Add('m');
}
EXPECT_EQ(1, received_events.size()); Task received;
EXPECT_TRUE(received_events[0].is_mouse()); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(Mouse::Left, received_events[0].mouse().button); EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(12, received_events[0].mouse().x); EXPECT_EQ(Mouse::Left, std::get<Event>(received).mouse().button);
EXPECT_EQ(42, received_events[0].mouse().y); EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(received_events[0].mouse().motion, Mouse::Released); EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_EQ(std::get<Event>(received).mouse().motion, Mouse::Released);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseReporting) { TEST(Event, MouseReporting) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Add('['); parser.Add('[');
parser.Add('1'); parser.Add('1');
parser.Add('2'); parser.Add('2');
@@ -162,18 +184,21 @@ TEST(Event, MouseReporting) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('R'); parser.Add('R');
}
EXPECT_EQ(1, received_events.size()); Task received;
EXPECT_TRUE(received_events[0].is_cursor_position()); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(42, received_events[0].cursor_x()); EXPECT_TRUE(std::get<Event>(received).is_cursor_position());
EXPECT_EQ(12, received_events[0].cursor_y()); EXPECT_EQ(42, std::get<Event>(received).cursor_x());
EXPECT_EQ(12, std::get<Event>(received).cursor_y());
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseMiddleClick) { TEST(Event, MouseMiddleClick) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Add('['); parser.Add('[');
parser.Add('3'); parser.Add('3');
parser.Add('3'); parser.Add('3');
@@ -184,19 +209,22 @@ TEST(Event, MouseMiddleClick) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('M'); parser.Add('M');
}
EXPECT_EQ(1, received_events.size()); Task received;
EXPECT_TRUE(received_events[0].is_mouse()); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(Mouse::Middle, received_events[0].mouse().button); EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(12, received_events[0].mouse().x); EXPECT_EQ(Mouse::Middle, std::get<Event>(received).mouse().button);
EXPECT_EQ(42, received_events[0].mouse().y); EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, MouseRightClick) { TEST(Event, MouseRightClick) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(''); parser.Add('\x1B');
parser.Add('['); parser.Add('[');
parser.Add('3'); parser.Add('3');
parser.Add('4'); parser.Add('4');
@@ -207,12 +235,15 @@ TEST(Event, MouseRightClick) {
parser.Add('4'); parser.Add('4');
parser.Add('2'); parser.Add('2');
parser.Add('M'); parser.Add('M');
}
EXPECT_EQ(1, received_events.size()); Task received;
EXPECT_TRUE(received_events[0].is_mouse()); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(Mouse::Right, received_events[0].mouse().button); EXPECT_TRUE(std::get<Event>(received).is_mouse());
EXPECT_EQ(12, received_events[0].mouse().x); EXPECT_EQ(Mouse::Right, std::get<Event>(received).mouse().button);
EXPECT_EQ(42, received_events[0].mouse().y); EXPECT_EQ(12, std::get<Event>(received).mouse().x);
EXPECT_EQ(42, std::get<Event>(received).mouse().y);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
TEST(Event, UTF8) { TEST(Event, UTF8) {
@@ -282,29 +313,31 @@ TEST(Event, UTF8) {
}; };
for (auto test : kTestCase) { for (auto test : kTestCase) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
for (auto input : test.input) for (auto input : test.input)
parser.Add(input); parser.Add(input);
if (test.valid) {
EXPECT_EQ(1, received_events.size());
EXPECT_TRUE(received_events[0].is_character());
} else {
EXPECT_TRUE(received_events.empty());
} }
Task received;
if (test.valid) {
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_character());
}
EXPECT_FALSE(event_receiver->Receive(&received));
} }
} }
TEST(Event, NewLine) { TEST(Event, NewLine) {
for (char newline : {'\r', '\n'}) { for (char newline : {'\r', '\n'}) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(newline); parser.Add(newline);
EXPECT_EQ(1, received_events.size()); }
EXPECT_TRUE(received_events[0] == Event::Return); Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received) == Event::Return);
} }
} }
@@ -324,16 +357,17 @@ TEST(Event, Control) {
cases.push_back({char(127), false}); cases.push_back({char(127), false});
for (auto test : cases) { for (auto test : cases) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(test.input); parser.Add(test.input);
}
Task received;
if (test.cancel) { if (test.cancel) {
EXPECT_TRUE(received_events.empty()); EXPECT_FALSE(event_receiver->Receive(&received));
} else { } else {
EXPECT_EQ(1, received_events.size()); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(received_events[0], Event::Special({test.input})); EXPECT_EQ(std::get<Event>(received), Event::Special({test.input}));
} }
} }
} }
@@ -351,9 +385,10 @@ TEST(Event, Special) {
Event expected; Event expected;
} kTestCase[] = { } kTestCase[] = {
// Arrow (default cursor mode) // Arrow (default cursor mode)
{str(""), Event::ArrowUp}, {str(""), Event::ArrowDown}, {str("\x1B[A"), Event::ArrowUp}, {str("\x1B[B"), Event::ArrowDown},
{str(""), Event::ArrowRight}, {str(""), Event::ArrowLeft}, {str("\x1B[C"), Event::ArrowRight}, {str("\x1B[D"), Event::ArrowLeft},
{str(""), Event::Home}, {str(""), Event::End}, {str("\x1B[H"), Event::Home}, {str("\x1B[F"), Event::End},
/*
// Arrow (application cursor mode) // Arrow (application cursor mode)
{str("\x1BOA"), Event::ArrowUp}, {str("\x1BOA"), Event::ArrowUp},
@@ -434,24 +469,28 @@ TEST(Event, Special) {
// Custom: // Custom:
{{0}, Event::Custom}, {{0}, Event::Custom},
*/
}; };
for (auto test : kTestCase) { for (auto test : kTestCase) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
for (auto input : test.input) { for (auto input : test.input) {
parser.Add(input); parser.Add(input);
} }
EXPECT_EQ(1, received_events.size()); }
EXPECT_EQ(received_events[0], test.expected); Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(std::get<Event>(received), test.expected);
EXPECT_FALSE(event_receiver->Receive(&received));
} }
} }
TEST(Event, DeviceControlString) { TEST(Event, DeviceControlString) {
std::vector<Event> received_events; auto event_receiver = MakeReceiver<Task>();
auto parser = TerminalInputParser( {
[&](Event event) { received_events.push_back(std::move(event)); }); auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add(27); // ESC parser.Add(27); // ESC
parser.Add(80); // P parser.Add(80); // P
parser.Add(49); // 1 parser.Add(49); // 1
@@ -462,10 +501,35 @@ TEST(Event, DeviceControlString) {
parser.Add(113); // q parser.Add(113); // q
parser.Add(27); // ESC parser.Add(27); // ESC
parser.Add(92); // (backslash) parser.Add(92); // (backslash)
}
EXPECT_EQ(1, received_events.size()); Task received;
EXPECT_TRUE(received_events[0].is_cursor_shape()); EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_EQ(1, received_events[0].cursor_shape()); EXPECT_TRUE(std::get<Event>(received).is_cursor_shape());
EXPECT_EQ(1, std::get<Event>(received).cursor_shape());
EXPECT_FALSE(event_receiver->Receive(&received));
}
TEST(Event, TerminalID) {
// Test terminal id for KDE konsole
auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
parser.Add('\x1B');
parser.Add('[');
parser.Add('?');
parser.Add('6');
parser.Add('2');
parser.Add(';');
parser.Add('2');
parser.Add('c');
}
Task received;
EXPECT_TRUE(event_receiver->Receive(&received));
EXPECT_TRUE(std::get<Event>(received).is_terminal_id());
EXPECT_EQ(TerminalID::Konsole, std::get<Event>(received).terminal_id());
EXPECT_FALSE(event_receiver->Receive(&received));
} }
} // namespace ftxui } // namespace ftxui

View File

@@ -1,16 +1,21 @@
// Copyright 2021 Arthur Sonzogni. All rights reserved. // Copyright 2021 Arthur Sonzogni. All rights reserved.
// Use of this source code is governed by the MIT license that can be found in // Use of this source code is governed by the MIT license that can be found in
// the LICENSE file. // the LICENSE file.
#include <cstddef>
#include <ftxui/component/event.hpp>
#include "ftxui/component/terminal_input_parser.hpp" #include "ftxui/component/terminal_input_parser.hpp"
extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) { extern "C" int LLVMFuzzerTestOneInput(const char* data, size_t size) {
using namespace ftxui; using namespace ftxui;
auto parser = TerminalInputParser([&](Event) {}); auto event_receiver = MakeReceiver<Task>();
{
auto parser = TerminalInputParser(event_receiver->MakeSender());
for (size_t i = 0; i < size; ++i) { for (size_t i = 0; i < size; ++i) {
parser.Add(data[i]); parser.Add(data[i]);
} }
}
Task received;
while (event_receiver->Receive(&received)) {
// Do nothing.
}
return 0; // Non-zero return values are reserved for future use. return 0; // Non-zero return values are reserved for future use.
} }