aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFelix Morgner <felix.morgner@gmail.com>2026-06-20 11:54:06 +0200
committerFelix Morgner <felix.morgner@gmail.com>2026-06-20 11:54:06 +0200
commit912e7b10facdd3b2565423206ae25b1d6e4f7dd9 (patch)
treee744bc76893e8b1faa216b1aae93c7ec6e933a04
parent280190248bfbadaec4575932dc956401f7bb6390 (diff)
downloadttwhy-912e7b10facdd3b2565423206ae25b1d6e4f7dd9.tar.xz
ttwhy-912e7b10facdd3b2565423206ae25b1d6e4f7dd9.zip
lib: add xterm policy
-rw-r--r--CMakeLists.txt4
-rw-r--r--ttwhy/io.cppm6
-rw-r--r--ttwhy/routers/echo.cppm21
-rw-r--r--ttwhy/scanners/events.cppm52
-rw-r--r--ttwhy/scanners/mod.cppm4
-rw-r--r--ttwhy/scanners/terminal_concepts.cppm (renamed from ttwhy/scanners/concepts.cppm)5
-rw-r--r--ttwhy/scanners/terminal_events.cppm100
-rw-r--r--ttwhy/scanners/terminal_policies.cppm180
-rw-r--r--ttwhy/scanners/terminal_scanner.cppm6
9 files changed, 264 insertions, 114 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 12536ce..71b27c6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -61,9 +61,9 @@ target_sources("ttwhy-core" PUBLIC
"ttwhy/routers/echo.cppm"
"ttwhy/routers/mod.cppm"
- "ttwhy/scanners/concepts.cppm"
- "ttwhy/scanners/events.cppm"
"ttwhy/scanners/mod.cppm"
+ "ttwhy/scanners/terminal_concepts.cppm"
+ "ttwhy/scanners/terminal_events.cppm"
"ttwhy/scanners/terminal_policies.cppm"
"ttwhy/scanners/terminal_scanner.cppm"
)
diff --git a/ttwhy/io.cppm b/ttwhy/io.cppm
index 4b83996..fe973ed 100644
--- a/ttwhy/io.cppm
+++ b/ttwhy/io.cppm
@@ -74,14 +74,16 @@ namespace ttwhy::io
auto sink = [&queue](auto const & event) {
queue.push_back(event);
};
- auto scanner = scanners::ansi{sink};
+
+ using terminal_policy = ttwhy::scanners::associated_terminal_policy_t<AppRouter>;
+ auto scanner = scanners::terminal_scanner<decltype(sink), terminal_policy>{sink};
auto raw_buffer = std::array<char, 64>{};
while (true)
{
auto error = asio::error_code{};
- auto bytes_read = std::size_t{};
+ auto bytes_read = 0uz;
if (scanner.is_pending())
{
diff --git a/ttwhy/routers/echo.cppm b/ttwhy/routers/echo.cppm
index 40e68d6..7f2550e 100644
--- a/ttwhy/routers/echo.cppm
+++ b/ttwhy/routers/echo.cppm
@@ -22,6 +22,8 @@ namespace ttwhy::routers
export template<typename StreamType>
struct echo
{
+ using terminal_policy = scanners::xterm_policy;
+
explicit echo(StreamType & stream)
: m_output_stream{stream}
{}
@@ -42,7 +44,7 @@ namespace ttwhy::routers
}
else if constexpr (std::same_as<scanners::navigation_event, event_type>)
{
- co_await process_navigation_key(event.key);
+ co_await process_navigation_key(event);
}
},
event);
@@ -77,16 +79,23 @@ namespace ttwhy::routers
};
}
- auto process_navigation_key(scanners::navigation_key key) -> asio::awaitable<void>
+ auto process_navigation_key(scanners::navigation_event event) -> asio::awaitable<void>
{
- switch (key)
+ auto modifiers = std::to_underlying(event.modifiers);
+
+ switch (event.key)
{
case scanners::navigation_key::delete_key:
- co_await asio::async_write(m_output_stream, asio::buffer("{DEL}"), asio::as_tuple(asio::use_awaitable));
+ {
+ auto message = std::format("{{DEL, MODS:{}}}", static_cast<int>(modifiers));
+ co_await asio::async_write(m_output_stream, asio::buffer(message), asio::as_tuple(asio::use_awaitable));
break;
+ }
default:
- co_await asio::async_write(m_output_stream, asio::buffer(std::format("{{NAV:{}}}", std::to_underlying(key))),
- asio::as_tuple(asio::use_awaitable));
+ {
+ auto message = std::format("{{NAV:{}, MODS:{}}}", static_cast<int>(event.key), static_cast<int>(modifiers));
+ co_await asio::async_write(m_output_stream, asio::buffer(message), asio::as_tuple(asio::use_awaitable));
+ }
}
}
diff --git a/ttwhy/scanners/events.cppm b/ttwhy/scanners/events.cppm
deleted file mode 100644
index 03ab9eb..0000000
--- a/ttwhy/scanners/events.cppm
+++ /dev/null
@@ -1,52 +0,0 @@
-module;
-
-#include <variant>
-
-export module ttwhy.scanners:events;
-
-namespace ttwhy::scanners
-{
- export enum class control_key
- {
- enter,
- escape,
- tab,
- backspace,
- };
-
- export enum class navigation_key
- {
- up,
- down,
- left,
- right,
- home,
- end,
- delete_key,
- insert_key,
- page_up,
- page_down,
- };
-
- export struct character_event
- {
- char value;
- };
-
- export struct control_event
- {
- control_key key;
- };
-
- export struct navigation_event
- {
- navigation_key key;
- };
-
- 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 54f1e0e..a1f45c1 100644
--- a/ttwhy/scanners/mod.cppm
+++ b/ttwhy/scanners/mod.cppm
@@ -1,6 +1,6 @@
export module ttwhy.scanners;
-export import :concepts;
-export import :events;
+export import :terminal_concepts;
+export import :terminal_events;
export import :terminal_policies;
export import :terminal_scanner;
diff --git a/ttwhy/scanners/concepts.cppm b/ttwhy/scanners/terminal_concepts.cppm
index 8db3ae6..e82a8d0 100644
--- a/ttwhy/scanners/concepts.cppm
+++ b/ttwhy/scanners/terminal_concepts.cppm
@@ -2,11 +2,10 @@ module;
#include <concepts>
#include <span>
-#include <string>
-export module ttwhy.scanners:concepts;
+export module ttwhy.scanners:terminal_concepts;
-import :events;
+import :terminal_events;
namespace ttwhy
{
diff --git a/ttwhy/scanners/terminal_events.cppm b/ttwhy/scanners/terminal_events.cppm
new file mode 100644
index 0000000..3e4c849
--- /dev/null
+++ b/ttwhy/scanners/terminal_events.cppm
@@ -0,0 +1,100 @@
+module;
+
+#include <cstdint>
+#include <utility>
+#include <variant>
+
+export module ttwhy.scanners:terminal_events;
+
+export namespace ttwhy::scanners
+{
+ enum struct control_key : std::uint8_t
+ {
+ enter,
+ escape,
+ tab,
+ backspace,
+ };
+
+ enum struct navigation_key : std::uint8_t
+ {
+ up,
+ down,
+ left,
+ right,
+ home,
+ end,
+ delete_key,
+ insert_key,
+ page_up,
+ page_down,
+ };
+
+ enum struct modifier : std::uint8_t
+ {
+ none = 0,
+ shift = 1,
+ alt = 2,
+ ctrl = 4,
+ meta = 8,
+ };
+
+ enum struct modifier_set : std::uint8_t
+ {
+ };
+
+ constexpr auto operator|(modifier lhs, modifier rhs) -> modifier_set
+ {
+ return static_cast<modifier_set>(std::to_underlying(lhs) | std::to_underlying(rhs));
+ }
+
+ constexpr auto operator|(modifier_set lhs, modifier rhs) -> modifier_set
+ {
+ return static_cast<modifier_set>(std::to_underlying(lhs) | std::to_underlying(rhs));
+ }
+
+ constexpr auto operator&(modifier lhs, modifier rhs) -> modifier_set
+ {
+ return static_cast<modifier_set>(std::to_underlying(lhs) & std::to_underlying(rhs));
+ }
+
+ constexpr auto operator&(modifier_set lhs, modifier rhs) -> modifier_set
+ {
+ return static_cast<modifier_set>(std::to_underlying(lhs) & std::to_underlying(rhs));
+ }
+
+ template<std::same_as<modifier>... Modifier>
+ constexpr auto has_all(modifier_set mask, Modifier... flag) -> bool
+ {
+ return (true && ... && (mask & flag));
+ }
+
+ template<std::same_as<modifier>... Modifier>
+ constexpr auto has_any(modifier_set mask, Modifier... flag) -> bool
+ {
+ return (false || ... || (mask & flag));
+ }
+
+ struct character_event
+ {
+ char value{};
+ };
+
+ struct control_event
+ {
+ control_key key{};
+ };
+
+ struct navigation_event
+ {
+ navigation_key key{};
+ modifier_set modifiers{};
+ };
+
+ struct ctrl_chord_event
+ {
+ char key{};
+ };
+
+ using input_event = std::variant<character_event, control_event, navigation_event, ctrl_chord_event>;
+} // namespace ttwhy::scanners
diff --git a/ttwhy/scanners/terminal_policies.cppm b/ttwhy/scanners/terminal_policies.cppm
index e5863d3..bf7c126 100644
--- a/ttwhy/scanners/terminal_policies.cppm
+++ b/ttwhy/scanners/terminal_policies.cppm
@@ -1,70 +1,162 @@
module;
+#include <optional>
#include <string>
export module ttwhy.scanners:terminal_policies;
-import :events;
-import :concepts;
+import :terminal_events;
+import :terminal_concepts;
namespace ttwhy::scanners
{
+ namespace detail
+ {
+ constexpr auto map_vt220_keypad(char terminator) -> std::optional<navigation_key>
+ {
+ 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 std::nullopt;
+ }
+ }
+
+ constexpr auto map_vt100_cursor(char terminator) -> std::optional<navigation_key>
+ {
+ 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 std::nullopt;
+ }
+ }
+
+ } // namespace detail
+
+ export template<typename T>
+ using associated_terminal_policy_t = typename T::terminal_policy;
+
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});
+ if (buffer.empty())
+ {
+ return;
+ }
+
+ if (auto key = detail::map_vt220_keypad(buffer.front()))
+ {
+ sink(navigation_event{*key});
+ }
+ buffer.clear();
}
- constexpr auto static resolve_vt100_cursor(char terminator, ttwhy::ansi_sink auto & sink) -> void
+ constexpr auto static resolve_vt100_cursor(char terminator, std::string &, ttwhy::ansi_sink auto & sink) -> void
{
- auto key = [terminator] {
- switch (terminator)
+ if (auto key = detail::map_vt100_cursor(terminator))
+ {
+ sink(navigation_event{*key});
+ }
+ }
+
+ constexpr auto static resolve_ss3(char terminator, ttwhy::ansi_sink auto & sink) -> void
+ {
+ if (auto key = detail::map_vt100_cursor(terminator))
+ {
+ sink(navigation_event{*key});
+ }
+ }
+ };
+
+ export struct xterm_policy
+ {
+ constexpr auto static parse_modifiers(std::string_view buffer) -> modifier_set
+ {
+ auto delimiter = buffer.find(';');
+ if (delimiter == std::string_view::npos)
+ {
+ return {};
+ }
+
+ auto modifier_string = buffer.substr(delimiter + 1);
+ auto modifier_value = 0u;
+
+ for (auto character : modifier_string)
+ {
+ if (character >= '0' && character <= '9')
{
- 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);
+ modifier_value = modifier_value * 10 + static_cast<unsigned int>(character - '0');
}
- }();
- sink(navigation_event{key});
+ }
+
+ if (modifier_value <= 1)
+ {
+ return {};
+ }
+
+ return static_cast<modifier_set>(modifier_value - 1);
+ }
+
+ constexpr auto static resolve_vt220_keypad(std::string & buffer, ttwhy::ansi_sink auto & sink) -> void
+ {
+ if (buffer.empty())
+ {
+ return;
+ }
+
+ auto modifiers = parse_modifiers(buffer);
+
+ if (auto key = detail::map_vt220_keypad(buffer.front()))
+ {
+ sink(navigation_event{*key, modifiers});
+ }
+ buffer.clear();
+ }
+
+ constexpr auto static resolve_vt100_cursor(char terminator, std::string & buffer, ttwhy::ansi_sink auto & sink)
+ -> void
+ {
+ auto modifiers = parse_modifiers(buffer);
+
+ if (auto key = detail::map_vt100_cursor(terminator))
+ {
+ sink(navigation_event{*key, modifiers});
+ }
}
constexpr auto static resolve_ss3(char terminator, ttwhy::ansi_sink auto & sink) -> void
{
- return resolve_vt100_cursor(terminator, sink);
+ if (auto key = detail::map_vt100_cursor(terminator))
+ {
+ sink(navigation_event{*key});
+ }
}
};
diff --git a/ttwhy/scanners/terminal_scanner.cppm b/ttwhy/scanners/terminal_scanner.cppm
index d877350..510299c 100644
--- a/ttwhy/scanners/terminal_scanner.cppm
+++ b/ttwhy/scanners/terminal_scanner.cppm
@@ -9,8 +9,8 @@ module;
export module ttwhy.scanners:terminal_scanner;
-import :concepts;
-import :events;
+import :terminal_concepts;
+import :terminal_events;
import :terminal_policies;
namespace ttwhy::scanners::detail
@@ -153,7 +153,7 @@ namespace ttwhy::scanners::detail
};
constexpr auto resolve_vt100_cursor = [](byte_received event, std::string & buffer, Sink & sink) {
- TerminalPolicy::resolve_vt100_cursor(event.value, sink);
+ TerminalPolicy::resolve_vt100_cursor(event.value, buffer, sink);
buffer.clear();
};