aboutsummaryrefslogtreecommitdiff
path: root/ttwhy/terminal/scanner.tests.cpp
diff options
context:
space:
mode:
authorFelix Morgner <felix.morgner@gmail.com>2026-10-26 19:15:33 +0100
committerFelix Morgner <felix.morgner@gmail.com>2026-10-26 19:15:33 +0100
commitb051983db21290871c7a33cf8189f415c07f8a24 (patch)
tree4258f9fdc05f5eec0341147047d8eb40ea055454 /ttwhy/terminal/scanner.tests.cpp
parent79a35574ce6d472f7a7cc4a200a1ed7befe34066 (diff)
downloadttwhy-b051983db21290871c7a33cf8189f415c07f8a24.tar.xz
ttwhy-b051983db21290871c7a33cf8189f415c07f8a24.zip
terminal: improve function and control key handling
Diffstat (limited to 'ttwhy/terminal/scanner.tests.cpp')
-rw-r--r--ttwhy/terminal/scanner.tests.cpp283
1 files changed, 283 insertions, 0 deletions
diff --git a/ttwhy/terminal/scanner.tests.cpp b/ttwhy/terminal/scanner.tests.cpp
index 64acb2d..b2a7f1e 100644
--- a/ttwhy/terminal/scanner.tests.cpp
+++ b/ttwhy/terminal/scanner.tests.cpp
@@ -22,6 +22,22 @@ using namespace std::string_view_literals;
return data != nullptr && data->key == expected;
}
+[[nodiscard]] constexpr auto static is_navigation(ttwhy::terminal::input_event const & event,
+ ttwhy::terminal::navigation_key expected_key,
+ ttwhy::terminal::modifier_set expected_modifiers = {}) -> bool
+{
+ auto const * data = std::get_if<ttwhy::terminal::navigation_event>(&event);
+ return data != nullptr && data->key == expected_key && data->modifiers == expected_modifiers;
+}
+
+[[nodiscard]] constexpr auto static is_function(ttwhy::terminal::input_event const & event,
+ ttwhy::terminal::function_key expected_key,
+ ttwhy::terminal::modifier_set expected_modifiers = {}) -> bool
+{
+ auto const * data = std::get_if<ttwhy::terminal::function_key_event>(&event);
+ return data != nullptr && data->key == expected_key && data->modifiers == expected_modifiers;
+}
+
SCENARIO("The ANSI scanner processes printable ASCII and standard C0 control characters", "[scanner][ansi]")
{
GIVEN("An initialized scanner and event sink")
@@ -99,3 +115,270 @@ SCENARIO("The ANSI scanner processes printable ASCII and standard C0 control cha
}
}
}
+
+SCENARIO("The ANSI scanner processes VT220 keypad and cursor navigation sequences", "[scanner][ansi]")
+{
+ GIVEN("An initialized ANSI scanner and event sink")
+ {
+ auto queue = std::vector<ttwhy::terminal::input_event>{};
+ auto sink = [&queue](auto const & event) {
+ queue.push_back(event);
+ };
+ auto scanner = ttwhy::terminal::ansi{sink};
+
+ WHEN("Processing VT220 keypad Home (\\x1b[1~)")
+ {
+ scanner.process("\x1b[1~"sv);
+
+ THEN("It yields a navigation event for home")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::home));
+ }
+ }
+
+ WHEN("Processing VT220 keypad Insert (\\x1b[2~)")
+ {
+ scanner.process("\x1b[2~"sv);
+
+ THEN("It yields a navigation event for insert")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::insert_key));
+ }
+ }
+
+ WHEN("Processing VT220 keypad Delete (\\x1b[3~)")
+ {
+ scanner.process("\x1b[3~"sv);
+
+ THEN("It yields a navigation event for delete")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::delete_key));
+ }
+ }
+
+ WHEN("Processing VT220 keypad End (\\x1b[4~)")
+ {
+ scanner.process("\x1b[4~"sv);
+
+ THEN("It yields a navigation event for end")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::end));
+ }
+ }
+
+ WHEN("Processing VT220 keypad Page Up (\\x1b[5~)")
+ {
+ scanner.process("\x1b[5~"sv);
+
+ THEN("It yields a navigation event for page up")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::page_up));
+ }
+ }
+
+ WHEN("Processing VT220 keypad Page Down (\\x1b[6~)")
+ {
+ scanner.process("\x1b[6~"sv);
+
+ THEN("It yields a navigation event for page down")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::page_down));
+ }
+ }
+
+ WHEN("Processing VT100 cursor arrows")
+ {
+ scanner.process("\x1b[A\x1b[B\x1b[C\x1b[D"sv);
+
+ THEN("It yields navigation events for up, down, right, and left")
+ {
+ REQUIRE(queue.size() == 4);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::up));
+ CHECK(is_navigation(queue.at(1), ttwhy::terminal::navigation_key::down));
+ CHECK(is_navigation(queue.at(2), ttwhy::terminal::navigation_key::right));
+ CHECK(is_navigation(queue.at(3), ttwhy::terminal::navigation_key::left));
+ }
+ }
+ }
+}
+
+SCENARIO("The ANSI scanner processes multi-digit function keys and SS3 sequences", "[scanner][ansi]")
+{
+ GIVEN("An initialized ANSI scanner and event sink")
+ {
+ auto queue = std::vector<ttwhy::terminal::input_event>{};
+ auto sink = [&queue](auto const & event) {
+ queue.push_back(event);
+ };
+ auto scanner = ttwhy::terminal::ansi{sink};
+
+ WHEN("Processing F1 (\\x1b[11~)")
+ {
+ scanner.process("\x1b[11~"sv);
+
+ THEN("It yields a function key event for F1 and not home")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f1));
+ }
+ }
+
+ WHEN("Processing F5 (\\x1b[15~)")
+ {
+ scanner.process("\x1b[15~"sv);
+
+ THEN("It yields a function key event for F5 and not home")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f5));
+ }
+ }
+
+ WHEN("Processing F6 (\\x1b[17~)")
+ {
+ scanner.process("\x1b[17~"sv);
+
+ THEN("It yields a function key event for F6")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f6));
+ }
+ }
+
+ WHEN("Processing F12 (\\x1b[24~)")
+ {
+ scanner.process("\x1b[24~"sv);
+
+ THEN("It yields a function key event for F12")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f12));
+ }
+ }
+
+ WHEN("Processing F20 (\\x1b[34~)")
+ {
+ scanner.process("\x1b[34~"sv);
+
+ THEN("It yields a function key event for F20")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f20));
+ }
+ }
+
+ WHEN("Processing an unmapped multi-digit code (\\x1b[99~)")
+ {
+ scanner.process("\x1b[99~"sv);
+
+ THEN("No event is emitted")
+ {
+ CHECK(queue.empty());
+ }
+ }
+
+ WHEN("Processing SS3 function keys F1 through F4 (\\x1bOP to \\x1bOS)")
+ {
+ scanner.process("\x1bOP\x1bOQ\x1bOR\x1bOS"sv);
+
+ THEN("It yields function key events for F1 through F4")
+ {
+ REQUIRE(queue.size() == 4);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f1));
+ CHECK(is_function(queue.at(1), ttwhy::terminal::function_key::f2));
+ CHECK(is_function(queue.at(2), ttwhy::terminal::function_key::f3));
+ CHECK(is_function(queue.at(3), ttwhy::terminal::function_key::f4));
+ }
+ }
+ }
+}
+
+SCENARIO("The xterm scanner decodes keypad sequences and function keys with modifiers", "[scanner][xterm]")
+{
+ GIVEN("An initialized xterm scanner and event sink")
+ {
+ auto queue = std::vector<ttwhy::terminal::input_event>{};
+ auto sink = [&queue](auto const & event) {
+ queue.push_back(event);
+ };
+ auto scanner = ttwhy::terminal::xterm{sink};
+
+ WHEN("Processing Shift+Home (\\x1b[1;2~)")
+ {
+ scanner.process("\x1b[1;2~"sv);
+
+ THEN("It yields a navigation event for Home with shift modifier")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::home,
+ ttwhy::terminal::modifier::shift | ttwhy::terminal::modifier::none));
+ }
+ }
+
+ WHEN("Processing Ctrl+Delete (\\x1b[3;5~)")
+ {
+ scanner.process("\x1b[3;5~"sv);
+
+ THEN("It yields a navigation event for Delete with ctrl modifier")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::delete_key,
+ ttwhy::terminal::modifier::ctrl | ttwhy::terminal::modifier::none));
+ }
+ }
+
+ WHEN("Processing Shift+F5 (\\x1b[15;2~)")
+ {
+ scanner.process("\x1b[15;2~"sv);
+
+ THEN("It yields a function key event for F5 with shift modifier")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f5,
+ ttwhy::terminal::modifier::shift | ttwhy::terminal::modifier::none));
+ }
+ }
+
+ WHEN("Processing Ctrl+F5 (\\x1b[15;5~)")
+ {
+ scanner.process("\x1b[15;5~"sv);
+
+ THEN("It yields a function key event for F5 with ctrl modifier")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f5,
+ ttwhy::terminal::modifier::ctrl | ttwhy::terminal::modifier::none));
+ }
+ }
+
+ WHEN("Processing Alt+F12 (\\x1b[24;3~)")
+ {
+ scanner.process("\x1b[24;3~"sv);
+
+ THEN("It yields a function key event for F12 with alt modifier")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_function(queue.at(0), ttwhy::terminal::function_key::f12,
+ ttwhy::terminal::modifier::alt | ttwhy::terminal::modifier::none));
+ }
+ }
+
+ WHEN("Processing Shift+Up cursor (\\x1b[1;2A)")
+ {
+ scanner.process("\x1b[1;2A"sv);
+
+ THEN("It yields a navigation event for Up with shift modifier")
+ {
+ REQUIRE(queue.size() == 1);
+ CHECK(is_navigation(queue.at(0), ttwhy::terminal::navigation_key::up,
+ ttwhy::terminal::modifier::shift | ttwhy::terminal::modifier::none));
+ }
+ }
+ }
+}