#include #include #include #include import ttwhy.core; import ttwhy.terminal; using namespace std::string_view_literals; [[nodiscard]] constexpr auto static is_character(ttwhy::terminal::input_event & event, char expected) -> bool { auto const * data = std::get_if(&event); return data != nullptr && data->value == expected; } [[nodiscard]] constexpr auto static is_control(ttwhy::terminal::input_event & event, ttwhy::terminal::control_key expected) -> bool { auto const * data = std::get_if(&event); 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(&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(&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") { auto queue = std::vector{}; auto sink = [&queue](auto const & event) { queue.push_back(event); }; auto scanner = ttwhy::terminal::ansi{sink}; WHEN("Processing a standard printable character") { scanner.process("A"sv); THEN("It yields a single character event") { REQUIRE(queue.size() == 1); CHECK(is_character(queue.at(0), 'A')); } } WHEN("Processing a BS byte (\\x08)") { scanner.process("\x08"sv); THEN("It yields a backspace control event") { REQUIRE(queue.size() == 1); CHECK(is_control(queue.at(0), ttwhy::terminal::control_key::backspace)); } } WHEN("Processing a DEL byte (\\x7f as BS)") { scanner.process("\x7f"sv); THEN("It yields a backspace control event") { REQUIRE(queue.size() == 1); CHECK(is_control(queue.at(0), ttwhy::terminal::control_key::backspace)); } } WHEN("Processing a TAB byte") { scanner.process("\x09"sv); THEN("It yields a tab control event") { REQUIRE(queue.size() == 1); CHECK(is_control(queue.at(0), ttwhy::terminal::control_key::tab)); } } WHEN("Processing an LF byte") { scanner.process("\x0a"sv); THEN("It yields an enter control event") { REQUIRE(queue.size() == 1); CHECK(is_control(queue.at(0), ttwhy::terminal::control_key::enter)); } } WHEN("Processing an CR byte") { scanner.process("\x0d"sv); THEN("It yields an enter control event") { REQUIRE(queue.size() == 1); CHECK(is_control(queue.at(0), ttwhy::terminal::control_key::enter)); } } } } 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{}; 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{}; 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{}; 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)); } } } }