aboutsummaryrefslogtreecommitdiff
path: root/ttwhy
diff options
context:
space:
mode:
authorFelix Morgner <felix.morgner@gmail.com>2026-06-19 14:07:58 +0200
committerFelix Morgner <felix.morgner@gmail.com>2026-06-19 14:07:58 +0200
commitb198d40e35050c8692296f06acedfaf5e3c8a023 (patch)
tree1bfb0142b148dccca0723feb2e4642a8aed32f96 /ttwhy
parent324af400623a336eb74f51360822d134cad97537 (diff)
downloadttwhy-b198d40e35050c8692296f06acedfaf5e3c8a023.tar.xz
ttwhy-b198d40e35050c8692296f06acedfaf5e3c8a023.zip
lib/io: extend scanner
Diffstat (limited to 'ttwhy')
-rw-r--r--ttwhy/io.cppm42
-rw-r--r--ttwhy/routers/echo.cppm11
-rw-r--r--ttwhy/scanner.cppm195
3 files changed, 214 insertions, 34 deletions
diff --git a/ttwhy/io.cppm b/ttwhy/io.cppm
index be87d36..09f16c8 100644
--- a/ttwhy/io.cppm
+++ b/ttwhy/io.cppm
@@ -1,7 +1,9 @@
module;
#include <asio.hpp>
+#include <asio/experimental/awaitable_operators.hpp>
+#include <chrono>
#include <format>
#include <span>
#include <vector>
@@ -61,6 +63,12 @@ namespace ttwhy::io
export template<typename InputStream, typename AppRouter>
auto read_events(InputStream & stream, AppRouter & router) -> asio::awaitable<void>
{
+ using namespace asio::experimental::awaitable_operators;
+ using namespace std::chrono_literals;
+
+ auto executor = co_await asio::this_coro::executor;
+ auto timer = asio::steady_timer{executor};
+
auto queue = std::vector<input_event>{};
queue.reserve(16);
@@ -69,8 +77,36 @@ namespace ttwhy::io
while (true)
{
- auto [error, bytes_read] =
- co_await stream.async_read_some(asio::buffer(raw_buffer), asio::as_tuple(asio::use_awaitable));
+ auto error = asio::error_code{};
+ auto bytes_read = std::size_t{};
+
+ if (scanner.is_pending())
+ {
+ timer.expires_after(500ms);
+
+ auto result = co_await (stream.async_read_some(asio::buffer(raw_buffer), asio::as_tuple(asio::use_awaitable)) ||
+ timer.async_wait(asio::as_tuple(asio::use_awaitable)));
+
+ if (result.index() == 0)
+ {
+ std::tie(error, bytes_read) = std::get<0>(result);
+ }
+ else
+ {
+ scanner.timeout();
+ for (auto const & event : queue)
+ {
+ co_await router.process(event);
+ }
+ queue.clear();
+ continue;
+ }
+ }
+ else
+ {
+ std::tie(error, bytes_read) =
+ co_await stream.async_read_some(asio::buffer(raw_buffer), asio::as_tuple(asio::use_awaitable));
+ }
if (error)
{
@@ -82,14 +118,12 @@ namespace ttwhy::io
}
auto const byte_span = std::span<char const>{raw_buffer.data(), bytes_read};
-
scanner.process(byte_span);
for (auto const & event : queue)
{
co_await router.process(event);
}
-
queue.clear();
}
}
diff --git a/ttwhy/routers/echo.cppm b/ttwhy/routers/echo.cppm
index 41b7b51..fb392d6 100644
--- a/ttwhy/routers/echo.cppm
+++ b/ttwhy/routers/echo.cppm
@@ -15,6 +15,8 @@ namespace ttwhy::routers
using namespace std::string_view_literals;
constexpr auto vte_backspace_sequence = "\b \b"sv;
+ constexpr auto vte_newline_sequence = "\r\n"sv;
+ constexpr auto vte_horizontal_tab_sequqnce = "\t"sv;
export template<typename StreamType>
struct echo
@@ -59,9 +61,18 @@ namespace ttwhy::routers
co_await asio::async_write(m_output_stream, asio::buffer(vte_backspace_sequence),
asio::as_tuple(asio::use_awaitable));
break;
+ case control_key::enter:
+ co_await asio::async_write(m_output_stream, asio::buffer(vte_newline_sequence),
+ asio::as_tuple(asio::use_awaitable));
+ break;
+ case control_key::tab:
+ co_await asio::async_write(m_output_stream, asio::buffer(vte_horizontal_tab_sequqnce),
+ asio::as_tuple(asio::use_awaitable));
+ break;
default:
co_await asio::async_write(m_output_stream, asio::buffer(std::format("{{CTRL:{}}}", std::to_underlying(key))),
asio::as_tuple(asio::use_awaitable));
+ break;
};
}
diff --git a/ttwhy/scanner.cppm b/ttwhy/scanner.cppm
index 86a8493..2257f5f 100644
--- a/ttwhy/scanner.cppm
+++ b/ttwhy/scanner.cppm
@@ -3,8 +3,8 @@ module;
#include <algorithm>
#include <boost/sml.hpp>
-#include <cctype>
#include <span>
+#include <string>
#include <vector>
export module ttwhy:scanner;
@@ -21,27 +21,41 @@ namespace ttwhy::detail
char value;
};
+ struct timeout_expired
+ {
+ };
+
/// States
- auto const idle = boost::sml::state<class idle>;
- auto const escape_sequence = boost::sml::state<class escape_sequence>;
- auto const csi_sequence = boost::sml::state<class csi_sequence>;
+ 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>;
/// Actions
- auto push_character = [](byte_received const & event, std::vector<input_event> & queue) {
+ constexpr auto push_character = [](byte_received const & event, std::vector<input_event> & queue) {
queue.push_back(character_event{event.value});
};
- auto push_backspace = [](std::vector<input_event> & queue) {
+ constexpr auto push_backspace = [](std::vector<input_event> & queue) {
queue.push_back(control_event{control_key::backspace});
};
- auto push_delete = [](std::vector<input_event> & queue) {
- queue.push_back(navigation_event{navigation_key::delete_key});
+ constexpr auto push_tab = [](std::vector<input_event> & queue) {
+ queue.push_back(control_event(control_key::tab));
+ };
+
+ constexpr auto push_enter = [](std::vector<input_event> & queue) {
+ queue.push_back(control_event(control_key::enter));
};
- auto fallback_escape = [](byte_received const & event, std::vector<input_event> & queue) {
+ constexpr auto emit_timeout_escape = [](std::vector<input_event> & queue, std::string & buffer) {
+ queue.push_back(control_event{control_key::escape});
+ buffer.clear();
+ };
+
+ constexpr auto fallback_escape = [](byte_received const & event, std::vector<input_event> & queue) {
queue.push_back(control_event{control_key::escape});
if (event.value >= 0x20 && event.value <= 0x7e)
{
@@ -49,34 +63,135 @@ namespace ttwhy::detail
}
};
+ 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, std::vector<input_event> & queue) {
+ if (event.value == '~')
+ {
+ switch (buffer.at(0))
+ {
+ case '1':
+ case '7':
+ queue.push_back(navigation_event{navigation_key::home});
+ break;
+ case '2':
+ queue.push_back(navigation_event{navigation_key::insert_key});
+ break;
+ case '3':
+ queue.push_back(navigation_event{navigation_key::delete_key});
+ break;
+ case '4':
+ case '8':
+ queue.push_back(navigation_event{navigation_key::end});
+ break;
+ case '5':
+ queue.push_back(navigation_event{navigation_key::page_up});
+ break;
+ case '6':
+ queue.push_back(navigation_event{navigation_key::page_down});
+ break;
+ default:
+ }
+ }
+ else
+ {
+ switch (event.value)
+ {
+ case 'A':
+ queue.push_back(navigation_event{navigation_key::up});
+ break;
+ case 'B':
+ queue.push_back(navigation_event{navigation_key::down});
+ break;
+ case 'C':
+ queue.push_back(navigation_event{navigation_key::right});
+ break;
+ case 'D':
+ queue.push_back(navigation_event{navigation_key::left});
+ break;
+ case 'H':
+ queue.push_back(navigation_event{navigation_key::home});
+ break;
+ case 'F':
+ queue.push_back(navigation_event{navigation_key::end});
+ break;
+ default:
+ }
+ }
+ buffer.clear();
+ };
+
+ constexpr auto resolve_ss3 = [](byte_received const & event, std::vector<input_event> & queue) {
+ switch (event.value)
+ {
+ case 'A':
+ queue.push_back(navigation_event{navigation_key::up});
+ break;
+ case 'B':
+ queue.push_back(navigation_event{navigation_key::down});
+ break;
+ case 'C':
+ queue.push_back(navigation_event{navigation_key::right});
+ break;
+ case 'D':
+ queue.push_back(navigation_event{navigation_key::left});
+ break;
+ case 'H':
+ queue.push_back(navigation_event{navigation_key::home});
+ break;
+ case 'F':
+ queue.push_back(navigation_event{navigation_key::end});
+ break;
+ default:
+ }
+ };
+
/// Guards
- auto is_backspace = [](byte_received e) {
+ constexpr auto is_backspace = [](byte_received e) {
return e.value == '\x08' || e.value == '\x7f';
};
- auto is_escape = [](byte_received e) {
+ 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';
};
- auto is_printable = [](byte_received e) {
+ constexpr auto is_printable = [](byte_received e) {
return e.value >= 0x20 && e.value <= 0x7e;
};
- auto is_csi_introducer = [](byte_received e) {
+ constexpr auto is_csi_introducer = [](byte_received e) {
return e.value == '[';
};
- auto is_csi_param = [](byte_received e) {
- return e.value >= 0x20 && e.value <= 0x3f;
+ constexpr auto is_ss3_introducer = [](byte_received e) {
+ return e.value == 'O';
};
- auto is_csi_terminator = [](byte_received e) {
- return e.value >= 0x40 && e.value <= 0x7e;
+ 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;
};
- auto is_tilde = [](byte_received e) {
- return e.value == '~';
+ constexpr auto is_csi_terminator = [](byte_received e) {
+ return e.value >= 0x40 && e.value <= 0x7e;
};
/// Transitions
@@ -89,16 +204,23 @@ namespace ttwhy::detail
// 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_printable] / push_character = idle,
-
- escape_sequence + event<byte_received>[is_csi_introducer] = csi_sequence,
- escape_sequence + event<byte_received>[!is_csi_introducer] / fallback_escape = idle,
-
- csi_sequence + event<byte_received>[is_csi_param] = csi_sequence,
- csi_sequence + event<byte_received>[is_tilde] / push_delete = idle,
- csi_sequence + event<byte_received>[is_csi_terminator] = idle
+ *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
}
@@ -112,7 +234,7 @@ export namespace ttwhy
struct ansi_scanner
{
explicit ansi_scanner(std::vector<input_event> & queue)
- : m_state_machine{queue}
+ : m_state_machine{queue, m_csi_buffer}
{}
auto process(std::span<char const> buffer) -> void
@@ -120,7 +242,20 @@ export namespace ttwhy
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> m_state_machine;
};