module; #include #include #include #include #include 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; constexpr auto escape_sequence = boost::sml::state; constexpr auto csi_sequence = boost::sml::state; constexpr auto ss3_sequence = boost::sml::state; /// 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 >= ' ' && e.value <= '~'; }; constexpr auto is_c0_chord = [](byte_received e) { return e.value >= '\0' && e.value <= '\x1f' && !is_backspace(e) && !is_tab(e) && !is_escape(e); }; constexpr auto is_fe = [](byte_received e) { return e.value >= '@' && e.value <= '_'; }; constexpr auto is_csi = [](c1_received e) { return e.value == u'\x9b'; }; constexpr auto is_ss3 = [](c1_received e) { return e.value == u'\x8f'; }; 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 >= ' ' && e.value <= '?'; }; constexpr auto is_vt100_terminator = [](byte_received e) { return e.value >= '@' && e.value <= '}'; }; constexpr auto is_vt220_terminator = [](byte_received e) { return e.value == '~'; }; /// Transitions template 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(event.value + 0x40)}); }; constexpr auto dispatch_c1 = [](byte_received event, back::process process) { process(c1_received{static_cast(event.value + 0x40)}); }; constexpr auto fallback_fe = [](c1_received event, Sink & sink) { sink(control_event{control_key::escape}); sink(character_event{static_cast(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 >= ' ' && event.value <= '~') { 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[is_escape] = escape_sequence, idle + event[is_backspace] / push_backspace = idle, idle + event[is_tab] / push_tab = idle, idle + event[is_enter] / push_enter = idle, idle + event[is_c0_chord] / push_c0_chord = idle, idle + event[is_printable] / push_character = idle, escape_sequence + event[is_fe] / dispatch_c1 = idle, escape_sequence + event[!is_fe] / fallback_escape = idle, escape_sequence + event / emit_timeout_escape = idle, idle + event[is_csi] / clear_csi = csi_sequence, idle + event[is_ss3] = ss3_sequence, idle + event[is_unhandled_c1] / fallback_fe = idle, csi_sequence + event[is_csi_param] / push_csi_parameter = csi_sequence, csi_sequence + event[is_vt220_terminator] / resolve_vt220_keypad = idle, csi_sequence + event[is_vt100_terminator] / resolve_vt100_cursor = idle, csi_sequence + event / emit_timeout_escape = idle, ss3_sequence + event / resolve_ss3 = idle, ss3_sequence + event / emit_timeout_escape = idle ); // clang-format on } }; } // namespace ttwhy::scanners::detail export namespace ttwhy::scanners { template struct terminal_scanner { explicit terminal_scanner(Sink & sink) : m_state_machine{sink, m_csi_buffer} {} auto process(std::span 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, boost::sml::process_queue> m_state_machine; }; template using ansi = terminal_scanner; } // namespace ttwhy::scanners