diff options
| -rw-r--r-- | CMakeLists.txt | 7 | ||||
| -rw-r--r-- | ttwhy/scanners/ansi.cppm | 261 | ||||
| -rw-r--r-- | ttwhy/scanners/concepts.cppm | 10 | ||||
| -rw-r--r-- | ttwhy/scanners/events.cppm | 7 | ||||
| -rw-r--r-- | ttwhy/scanners/mod.cppm | 3 | ||||
| -rw-r--r-- | ttwhy/scanners/terminal_policies.cppm | 72 | ||||
| -rw-r--r-- | ttwhy/scanners/terminal_scanner.cppm | 236 | ||||
| -rw-r--r-- | ttwhy/scanners/terminal_scanner.tests.cpp (renamed from ttwhy/scanners/ansi.tests.cpp) | 0 |
8 files changed, 330 insertions, 266 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 390afbc..12536ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,10 +61,11 @@ target_sources("ttwhy-core" PUBLIC "ttwhy/routers/echo.cppm" "ttwhy/routers/mod.cppm" - "ttwhy/scanners/ansi.cppm" "ttwhy/scanners/concepts.cppm" - "ttwhy/scanners/mod.cppm" "ttwhy/scanners/events.cppm" + "ttwhy/scanners/mod.cppm" + "ttwhy/scanners/terminal_policies.cppm" + "ttwhy/scanners/terminal_scanner.cppm" ) target_include_directories("ttwhy-core" PUBLIC @@ -111,7 +112,7 @@ if(BUILD_TESTING) add_executable("ttwhy::tests" ALIAS "ttwhy-tests") target_sources("ttwhy-tests" PRIVATE - "ttwhy/scanners/ansi.tests.cpp" + "ttwhy/scanners/terminal_scanner.tests.cpp" ) target_link_libraries("ttwhy-tests" PRIVATE diff --git a/ttwhy/scanners/ansi.cppm b/ttwhy/scanners/ansi.cppm deleted file mode 100644 index 886e699..0000000 --- a/ttwhy/scanners/ansi.cppm +++ /dev/null @@ -1,261 +0,0 @@ -module; - -#include <algorithm> -#include <boost/sml.hpp> - -#include <span> -#include <string> - -export module ttwhy.scanners:ansi; - -import :events; - -namespace ttwhy::scanners::detail -{ - - /// Events - - struct byte_received - { - char value; - }; - - struct timeout_expired - { - }; - - /// States - - constexpr auto idle = boost::sml::state<class idle>; - constexpr auto escape_sequence = boost::sml::state<class escape_sequence>; - constexpr auto csi_sequence = boost::sml::state<class csi_sequence>; - constexpr auto ss3_sequence = boost::sml::state<class ss3_sequence>; - - /// Guards - - constexpr auto is_backspace = [](byte_received e) { - return e.value == '\x08' || e.value == '\x7f'; - }; - - constexpr auto is_tab = [](byte_received e) { - return e.value == '\x09'; - }; - - constexpr auto is_enter = [](byte_received e) { - return e.value == '\x0a' || e.value == '\x0d'; - }; - - constexpr auto is_escape = [](byte_received e) { - return e.value == '\x1b'; - }; - - constexpr auto is_printable = [](byte_received e) { - return e.value >= 0x20 && e.value <= 0x7e; - }; - - constexpr auto is_csi_introducer = [](byte_received e) { - return e.value == '['; - }; - - constexpr auto is_ss3_introducer = [](byte_received e) { - return e.value == 'O'; - }; - - constexpr auto is_invalid_introducer = [](byte_received e) { - return !(is_ss3_introducer(e) || is_csi_introducer(e)); - }; - - constexpr auto is_csi_param = [](byte_received e) { - return e.value >= 0x20 && e.value <= 0x3f; - }; - - constexpr auto is_csi_terminator = [](byte_received e) { - return e.value >= 0x40 && e.value <= 0x7e; - }; - - /// Transitions - - template<typename SinkType> - struct transition_table - { - auto operator()() const noexcept - { - using namespace boost::sml; - - constexpr auto push_character = [](byte_received const & event, SinkType & sink) { - sink(character_event{event.value}); - }; - - constexpr auto push_backspace = [](SinkType & sink) { - sink(control_event{control_key::backspace}); - }; - - constexpr auto push_tab = [](SinkType & sink) { - sink(control_event(control_key::tab)); - }; - - constexpr auto push_enter = [](SinkType & sink) { - sink(control_event(control_key::enter)); - }; - - constexpr auto emit_timeout_escape = [](SinkType & sink, std::string & buffer) { - sink(control_event{control_key::escape}); - buffer.clear(); - }; - - constexpr auto fallback_escape = [](byte_received const & event, SinkType & sink) { - sink(control_event{control_key::escape}); - if (event.value >= 0x20 && event.value <= 0x7e) - { - sink(character_event{event.value}); - } - }; - - constexpr auto clear_csi = [](std::string & buffer) { - buffer.clear(); - }; - - constexpr auto push_csi_parameter = [](byte_received const & event, std::string & buffer) { - buffer.push_back(event.value); - }; - - constexpr auto resolve_csi = [](byte_received const & event, std::string & buffer, SinkType & sink) { - if (event.value == '~') - { - switch (buffer.at(0)) - { - case '1': - case '7': - sink(navigation_event{navigation_key::home}); - break; - case '2': - sink(navigation_event{navigation_key::insert_key}); - break; - case '3': - sink(navigation_event{navigation_key::delete_key}); - break; - case '4': - case '8': - sink(navigation_event{navigation_key::end}); - break; - case '5': - sink(navigation_event{navigation_key::page_up}); - break; - case '6': - sink(navigation_event{navigation_key::page_down}); - break; - default: - } - } - else - { - switch (event.value) - { - case 'A': - sink(navigation_event{navigation_key::up}); - break; - case 'B': - sink(navigation_event{navigation_key::down}); - break; - case 'C': - sink(navigation_event{navigation_key::right}); - break; - case 'D': - sink(navigation_event{navigation_key::left}); - break; - case 'H': - sink(navigation_event{navigation_key::home}); - break; - case 'F': - sink(navigation_event{navigation_key::end}); - break; - default: - } - } - buffer.clear(); - }; - - constexpr auto resolve_ss3 = [](byte_received const & event, SinkType & sink) { - switch (event.value) - { - case 'A': - sink(navigation_event{navigation_key::up}); - break; - case 'B': - sink(navigation_event{navigation_key::down}); - break; - case 'C': - sink(navigation_event{navigation_key::right}); - break; - case 'D': - sink(navigation_event{navigation_key::left}); - break; - case 'H': - sink(navigation_event{navigation_key::home}); - break; - case 'F': - sink(navigation_event{navigation_key::end}); - break; - default: - } - }; - - // clang-format off - return make_transition_table( - *idle + event<byte_received>[is_escape] = escape_sequence, - idle + event<byte_received>[is_backspace] / push_backspace = idle, - idle + event<byte_received>[is_tab] / push_tab = idle, - idle + event<byte_received>[is_enter] / push_enter = idle, - idle + event<byte_received>[is_printable] / push_character = idle, - - escape_sequence + event<byte_received>[is_csi_introducer] / clear_csi = csi_sequence, - escape_sequence + event<byte_received>[is_ss3_introducer] = ss3_sequence, - escape_sequence + event<byte_received>[is_invalid_introducer] / fallback_escape = idle, - escape_sequence + event<timeout_expired> / emit_timeout_escape = idle, - - csi_sequence + event<byte_received>[is_csi_param] / push_csi_parameter = csi_sequence, - csi_sequence + event<byte_received>[is_csi_terminator] / resolve_csi = idle, - csi_sequence + event<timeout_expired> / emit_timeout_escape = idle, - - ss3_sequence + event<byte_received> / resolve_ss3 = idle, - ss3_sequence + event<timeout_expired> / emit_timeout_escape = idle - ); - // clang-format on - } - }; - -} // namespace ttwhy::scanners::detail - -export namespace ttwhy::scanners -{ - - template<typename SinkType> - struct ansi - { - explicit ansi(SinkType & sink) - : m_state_machine{sink, m_csi_buffer} - {} - - auto process(std::span<char const> buffer) -> void - { - std::ranges::for_each(buffer, [&](auto byte) { m_state_machine.process_event(detail::byte_received{byte}); }); - } - - auto timeout() - { - m_state_machine.process_event(detail::timeout_expired{}); - } - - [[nodiscard]] auto is_pending() const -> bool - { - return m_state_machine.is(detail::escape_sequence) || // - m_state_machine.is(detail::csi_sequence) || // - m_state_machine.is(detail::ss3_sequence); - } - - private: - std::string m_csi_buffer{}; - boost::sml::sm<detail::transition_table<SinkType>> m_state_machine; - }; - -} // namespace ttwhy::scanners diff --git a/ttwhy/scanners/concepts.cppm b/ttwhy/scanners/concepts.cppm index 003874c..8db3ae6 100644 --- a/ttwhy/scanners/concepts.cppm +++ b/ttwhy/scanners/concepts.cppm @@ -2,9 +2,12 @@ module; #include <concepts> #include <span> +#include <string> export module ttwhy.scanners:concepts; +import :events; + namespace ttwhy { @@ -15,4 +18,11 @@ namespace ttwhy { a.is_pending() } -> std::same_as<bool>; }; + export template<typename Sink> + concept ansi_sink = requires(Sink sink) { + { sink(std::declval<ttwhy::scanners::character_event>()) } -> std::same_as<void>; + { sink(std::declval<ttwhy::scanners::control_event>()) } -> std::same_as<void>; + { sink(std::declval<ttwhy::scanners::navigation_event>()) } -> std::same_as<void>; + }; + } // namespace ttwhy diff --git a/ttwhy/scanners/events.cppm b/ttwhy/scanners/events.cppm index 3e2f4f0..03ab9eb 100644 --- a/ttwhy/scanners/events.cppm +++ b/ttwhy/scanners/events.cppm @@ -43,5 +43,10 @@ namespace ttwhy::scanners navigation_key key; }; - export using input_event = std::variant<character_event, control_event, navigation_event>; + export struct ctrl_chord_event + { + char key; + }; + + export using input_event = std::variant<character_event, control_event, navigation_event, ctrl_chord_event>; } // namespace ttwhy::scanners diff --git a/ttwhy/scanners/mod.cppm b/ttwhy/scanners/mod.cppm index 8dc3009..54f1e0e 100644 --- a/ttwhy/scanners/mod.cppm +++ b/ttwhy/scanners/mod.cppm @@ -1,5 +1,6 @@ export module ttwhy.scanners; -export import :ansi; export import :concepts; export import :events; +export import :terminal_policies; +export import :terminal_scanner; diff --git a/ttwhy/scanners/terminal_policies.cppm b/ttwhy/scanners/terminal_policies.cppm new file mode 100644 index 0000000..87e54b3 --- /dev/null +++ b/ttwhy/scanners/terminal_policies.cppm @@ -0,0 +1,72 @@ +module; + +#include <string> + +export module ttwhy.scanners:terminal_policies; + +import :events; +import :concepts; + +namespace ttwhy::scanners +{ + + export struct ansi_policy + { + constexpr auto static resolve_vt220_keypad(std::string & buffer, ttwhy::ansi_sink auto & sink) -> void + { + auto key = [](auto terminator) { + switch (terminator) + { + case '1': + case '7': + return navigation_key::home; + case '2': + return navigation_key::insert_key; + case '3': + return navigation_key::delete_key; + case '4': + case '8': + return navigation_key::end; + case '5': + return navigation_key::page_up; + case '6': + return navigation_key::page_down; + default: + return static_cast<navigation_key>(-1); + } + }(buffer.back()); + sink(navigation_event{key}); + buffer.clear(); + } + + constexpr auto static resolve_vt100_cursor(char terminator, ttwhy::ansi_sink auto & sink) -> void + { + auto key = [terminator] { + switch (terminator) + { + case 'A': + return navigation_key::up; + case 'B': + return navigation_key::down; + case 'C': + return navigation_key::right; + case 'D': + return navigation_key::left; + case 'H': + return navigation_key::home; + case 'F': + return navigation_key::end; + default: + return static_cast<navigation_key>(-1); + } + }(); + sink(navigation_event{key}); + } + + constexpr auto static resolve_ss3(char terminator, ttwhy::ansi_sink auto & sink) -> void + { + return resolve_vt100_cursor(terminator, sink); + } + }; + +} // namespace ttwhy::scanners diff --git a/ttwhy/scanners/terminal_scanner.cppm b/ttwhy/scanners/terminal_scanner.cppm new file mode 100644 index 0000000..9284b84 --- /dev/null +++ b/ttwhy/scanners/terminal_scanner.cppm @@ -0,0 +1,236 @@ +module; + +#include <algorithm> +#include <boost/sml.hpp> + +#include <queue> +#include <span> +#include <string> + +export module ttwhy.scanners:terminal_scanner; + +import :concepts; +import :events; +import :terminal_policies; + +namespace ttwhy::scanners::detail +{ + + /// Events + + struct byte_received + { + char value; + }; + + struct c1_received + { + unsigned char value; + }; + + struct timeout_expired + { + }; + + /// States + + constexpr auto idle = boost::sml::state<class idle>; + constexpr auto escape_sequence = boost::sml::state<class escape_sequence>; + constexpr auto csi_sequence = boost::sml::state<class csi_sequence>; + constexpr auto ss3_sequence = boost::sml::state<class ss3_sequence>; + + /// Guards + + constexpr auto is_backspace = [](byte_received e) { + return e.value == '\x08' || e.value == '\x7f'; + }; + + constexpr auto is_tab = [](byte_received e) { + return e.value == '\x09'; + }; + + constexpr auto is_enter = [](byte_received e) { + return e.value == '\x0a' || e.value == '\x0d'; + }; + + constexpr auto is_escape = [](byte_received e) { + return e.value == '\x1b'; + }; + + constexpr auto is_printable = [](byte_received e) { + return e.value >= 0x20 && e.value <= 0x7e; + }; + + constexpr auto is_c0_chord = [](byte_received e) { + return e.value >= 0x00 && e.value <= 0x1f && !is_backspace(e) && !is_tab(e) && !is_escape(e); + }; + + constexpr auto is_fe = [](byte_received e) { + return e.value >= 0x40 && e.value <= 0x5f; + }; + + constexpr auto is_csi = [](c1_received e) { + return e.value == 0x9b; + }; + + constexpr auto is_ss3 = [](c1_received e) { + return e.value == 0x8f; + }; + + constexpr auto is_unhandled_c1 = [](c1_received e) { + return !(is_csi(e) || is_ss3(e)); + }; + + constexpr auto is_csi_param = [](byte_received e) { + return e.value >= 0x20 && e.value <= 0x3f; + }; + + constexpr auto is_vt100_terminator = [](byte_received e) { + return e.value >= 0x40 && e.value <= 0x7d; + }; + + constexpr auto is_vt220_terminator = [](byte_received e) { + return e.value == '~'; + }; + + /// Transitions + + template<ansi_sink Sink, typename TerminalPolicy> + struct transition_table + { + auto operator()() const noexcept + { + using namespace boost::sml; + + constexpr auto push_character = [](byte_received event, Sink & sink) { + sink(character_event{event.value}); + }; + + constexpr auto push_backspace = [](Sink & sink) { + sink(control_event{control_key::backspace}); + }; + + constexpr auto push_tab = [](Sink & sink) { + sink(control_event(control_key::tab)); + }; + + constexpr auto push_enter = [](Sink & sink) { + sink(control_event(control_key::enter)); + }; + + constexpr auto push_c0_chord = [](byte_received event, Sink & sink) { + sink(ctrl_chord_event{static_cast<char>(event.value + 0x40)}); + }; + + constexpr auto dispatch_c1 = [](byte_received event, back::process<c1_received> process) { + process(c1_received{static_cast<unsigned char>(event.value + 0x40)}); + }; + + constexpr auto fallback_fe = [](c1_received event, Sink & sink) { + sink(control_event{control_key::escape}); + sink(character_event{static_cast<char>(event.value - 0x40)}); + }; + + constexpr auto emit_timeout_escape = [](Sink & sink, std::string & buffer) { + sink(control_event{control_key::escape}); + buffer.clear(); + }; + + constexpr auto fallback_escape = [](byte_received event, Sink & sink) { + sink(control_event{control_key::escape}); + if (event.value >= 0x20 && event.value <= 0x7e) + { + sink(character_event{event.value}); + } + }; + + constexpr auto clear_csi = [](std::string & buffer) { + buffer.clear(); + }; + + constexpr auto push_csi_parameter = [](byte_received const & event, std::string & buffer) { + buffer.push_back(event.value); + }; + + constexpr auto resolve_vt100_cursor = [](byte_received event, std::string & buffer, Sink & sink) { + TerminalPolicy::resolve_vt100_cursor(event.value, sink); + buffer.clear(); + }; + + constexpr auto resolve_vt220_keypad = [](std::string & buffer, Sink & sink) { + TerminalPolicy::resolve_vt220_keypad(buffer, sink); + buffer.clear(); + }; + + constexpr auto resolve_ss3 = [](byte_received event, Sink & sink) { + TerminalPolicy::resolve_ss3(event.value, sink); + }; + + // clang-format off + return make_transition_table( + *idle + event<byte_received>[is_escape] = escape_sequence, + idle + event<byte_received>[is_backspace] / push_backspace = idle, + idle + event<byte_received>[is_tab] / push_tab = idle, + idle + event<byte_received>[is_enter] / push_enter = idle, + idle + event<byte_received>[is_c0_chord] / push_c0_chord = idle, + idle + event<byte_received>[is_printable] / push_character = idle, + + escape_sequence + event<byte_received>[is_fe] / dispatch_c1 = idle, + escape_sequence + event<byte_received>[!is_fe] / fallback_escape = idle, + escape_sequence + event<timeout_expired> / emit_timeout_escape = idle, + + idle + event<c1_received>[is_csi] / clear_csi = csi_sequence, + idle + event<c1_received>[is_ss3] = ss3_sequence, + idle + event<c1_received>[is_unhandled_c1] / fallback_fe = idle, + + csi_sequence + event<byte_received>[is_csi_param] / push_csi_parameter = csi_sequence, + csi_sequence + event<byte_received>[is_vt220_terminator] / resolve_vt220_keypad = idle, + csi_sequence + event<byte_received>[is_vt100_terminator] / resolve_vt100_cursor = idle, + csi_sequence + event<timeout_expired> / emit_timeout_escape = idle, + + ss3_sequence + event<byte_received> / resolve_ss3 = idle, + ss3_sequence + event<timeout_expired> / emit_timeout_escape = idle + ); + // clang-format on + } + }; + +} // namespace ttwhy::scanners::detail + +export namespace ttwhy::scanners +{ + + template<ansi_sink Sink, typename TerminalPolicy> + struct terminal_scanner + { + explicit terminal_scanner(Sink & sink) + : m_state_machine{sink, m_csi_buffer} + {} + + auto process(std::span<char const> buffer) -> void + { + std::ranges::for_each(buffer, [&](auto byte) { m_state_machine.process_event(detail::byte_received{byte}); }); + } + + auto timeout() + { + m_state_machine.process_event(detail::timeout_expired{}); + } + + [[nodiscard]] auto is_pending() const -> bool + { + return m_state_machine.is(detail::escape_sequence) || // + m_state_machine.is(detail::csi_sequence) || // + m_state_machine.is(detail::ss3_sequence); + } + + private: + std::string m_csi_buffer{}; + boost::sml::sm<detail::transition_table<Sink, TerminalPolicy>, boost::sml::process_queue<std::queue>> + m_state_machine; + }; + + template<ansi_sink Sink> + using ansi = terminal_scanner<Sink, ansi_policy>; + +} // namespace ttwhy::scanners diff --git a/ttwhy/scanners/ansi.tests.cpp b/ttwhy/scanners/terminal_scanner.tests.cpp index 6fdc8af..6fdc8af 100644 --- a/ttwhy/scanners/ansi.tests.cpp +++ b/ttwhy/scanners/terminal_scanner.tests.cpp |
