aboutsummaryrefslogtreecommitdiff
path: root/ttwhy
diff options
context:
space:
mode:
authorFelix Morgner <felix.morgner@gmail.com>2026-06-20 10:25:09 +0200
committerFelix Morgner <felix.morgner@gmail.com>2026-06-20 10:28:48 +0200
commita4b92ca698770dc64639e51c76edb5dee16d2792 (patch)
tree68f2dd6d99f50f5dd5d875703a1999863e9a650a /ttwhy
parentc5f8cd91186adbe9c68835675a019449cc275c6d (diff)
downloadttwhy-a4b92ca698770dc64639e51c76edb5dee16d2792.tar.xz
ttwhy-a4b92ca698770dc64639e51c76edb5dee16d2792.zip
lib: switch to policy based scanner design
Diffstat (limited to 'ttwhy')
-rw-r--r--ttwhy/scanners/ansi.cppm261
-rw-r--r--ttwhy/scanners/concepts.cppm10
-rw-r--r--ttwhy/scanners/events.cppm7
-rw-r--r--ttwhy/scanners/mod.cppm3
-rw-r--r--ttwhy/scanners/terminal_policies.cppm72
-rw-r--r--ttwhy/scanners/terminal_scanner.cppm236
-rw-r--r--ttwhy/scanners/terminal_scanner.tests.cpp (renamed from ttwhy/scanners/ansi.tests.cpp)0
7 files changed, 326 insertions, 263 deletions
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