aboutsummaryrefslogtreecommitdiff
path: root/ttwhy/routers
diff options
context:
space:
mode:
authorFelix Morgner <felix.morgner@gmail.com>2026-09-22 22:47:33 +0200
committerFelix Morgner <felix.morgner@gmail.com>2026-09-22 22:47:33 +0200
commitd05b9343f69a0841043963088e0003368e205bd9 (patch)
tree8cde07ff86d27cbc808a0a149b89d0024b4a586c /ttwhy/routers
parent22f6534081acfabdab4956d337604a21e7da5b64 (diff)
downloadttwhy-master.tar.xz
ttwhy-master.zip
lib: restructure into vertical slicesHEADmaster
Diffstat (limited to 'ttwhy/routers')
-rw-r--r--ttwhy/routers/concepts.cppm17
-rw-r--r--ttwhy/routers/echo.cppm206
-rw-r--r--ttwhy/routers/echo.tests.cpp257
-rw-r--r--ttwhy/routers/mod.cppm4
4 files changed, 0 insertions, 484 deletions
diff --git a/ttwhy/routers/concepts.cppm b/ttwhy/routers/concepts.cppm
deleted file mode 100644
index cfbcb5a..0000000
--- a/ttwhy/routers/concepts.cppm
+++ /dev/null
@@ -1,17 +0,0 @@
-module;
-
-#include <asio.hpp>
-
-#include <concepts>
-
-export module ttwhy.routers:concepts;
-
-namespace ttwhy
-{
-
- export template<typename Candidate, typename Event>
- concept router = requires(Candidate candidate, Event event) {
- { candidate.process(event) } -> std::same_as<asio::awaitable<void>>;
- };
-
-} // namespace ttwhy
diff --git a/ttwhy/routers/echo.cppm b/ttwhy/routers/echo.cppm
deleted file mode 100644
index c9cd7c7..0000000
--- a/ttwhy/routers/echo.cppm
+++ /dev/null
@@ -1,206 +0,0 @@
-module;
-
-#include <asio.hpp>
-
-#include <algorithm>
-
-#include <cstddef>
-#include <format>
-#include <string_view>
-
-export module ttwhy.routers:echo;
-
-import ttwhy.scanners;
-
-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_sequence = "\t"sv;
- constexpr auto vte_cursor_left_sequence = "\x1b[D"sv;
- constexpr auto vte_cursor_right_sequence = "\x1b[C"sv;
- constexpr auto vte_delete_character_sequence = "\x1b[P"sv;
- constexpr auto vte_cursor_left_sequence_format = "\x1b[{}D"sv;
- constexpr auto vte_cursor_right_sequence_format = "\x1b[{}C"sv;
-
- export template<typename StreamType>
- struct echo
- {
- explicit echo(StreamType & stream)
- : m_output_stream{stream}
- {}
-
- [[nodiscard]] auto cursor_position() const noexcept -> std::size_t
- {
- return m_cursor_position;
- }
-
- [[nodiscard]] auto max_position() const noexcept -> std::size_t
- {
- return m_max_position;
- }
-
- auto process(scanners::input_event event) -> asio::awaitable<void>
- {
- co_await std::visit(
- [&](auto && event) -> asio::awaitable<void> {
- using event_type = std::decay_t<decltype(event)>;
-
- if constexpr (std::same_as<scanners::character_event, event_type>)
- {
- co_await process_character(event.value);
- }
- else if constexpr (std::same_as<scanners::control_event, event_type>)
- {
- co_await process_control_key(event.key);
- }
- else if constexpr (std::same_as<scanners::navigation_event, event_type>)
- {
- co_await process_navigation_key(event);
- }
- },
- event);
- }
-
- private:
- auto process_character(char character) -> asio::awaitable<void>
- {
- auto [error, bytes_written] =
- co_await asio::async_write(m_output_stream, asio::buffer(&character, 1), asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- m_cursor_position += bytes_written;
- m_max_position = std::max(m_max_position, m_cursor_position);
- }
- }
-
- auto process_control_key(scanners::control_key key) -> asio::awaitable<void>
- {
- switch (key)
- {
- case scanners::control_key::backspace:
- if (m_cursor_position > 0)
- {
- auto [error, bytes_written] = co_await asio::async_write(
- m_output_stream, asio::buffer(vte_backspace_sequence), asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- if (m_cursor_position == m_max_position)
- {
- --m_max_position;
- }
- --m_cursor_position;
- }
- }
- break;
- case scanners::control_key::enter:
- {
- auto [error, bytes_written] = co_await asio::async_write(m_output_stream, asio::buffer(vte_newline_sequence),
- asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- m_cursor_position = 0;
- m_max_position = 0;
- }
- break;
- }
- case scanners::control_key::tab:
- {
- auto [error, bytes_written] = co_await asio::async_write(
- m_output_stream, asio::buffer(vte_horizontal_tab_sequence), asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- auto const tab_stop = 8uz - (m_cursor_position % 8uz);
- m_cursor_position += tab_stop;
- m_max_position = std::max(m_max_position, m_cursor_position);
- }
- 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;
- };
- }
-
- auto process_navigation_key(scanners::navigation_event event) -> asio::awaitable<void>
- {
- switch (event.key)
- {
- case scanners::navigation_key::left:
- if (m_cursor_position > 0)
- {
- auto [error, bytes_written] = co_await asio::async_write(
- m_output_stream, asio::buffer(vte_cursor_left_sequence), asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- --m_cursor_position;
- }
- }
- break;
-
- case scanners::navigation_key::right:
- if (m_cursor_position < m_max_position)
- {
- auto [error, bytes_written] = co_await asio::async_write(
- m_output_stream, asio::buffer(vte_cursor_right_sequence), asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- ++m_cursor_position;
- }
- }
- break;
-
- case scanners::navigation_key::home:
- if (m_cursor_position > 0)
- {
- auto sequence = std::format(vte_cursor_left_sequence_format, m_cursor_position);
- auto [error, bytes_written] = co_await asio::async_write(m_output_stream, asio::buffer(sequence),
- asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- m_cursor_position = 0;
- }
- }
- break;
-
- case scanners::navigation_key::end:
- if (m_cursor_position < m_max_position)
- {
- auto const distance = m_max_position - m_cursor_position;
- auto sequence = std::format(vte_cursor_right_sequence_format, distance);
- auto [error, bytes_written] = co_await asio::async_write(m_output_stream, asio::buffer(sequence),
- asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- m_cursor_position = m_max_position;
- }
- }
- break;
-
- case scanners::navigation_key::delete_key:
- if (m_cursor_position < m_max_position)
- {
- auto [error, bytes_written] = co_await asio::async_write(
- m_output_stream, asio::buffer(vte_delete_character_sequence), asio::as_tuple(asio::use_awaitable));
- if (!error)
- {
- --m_max_position;
- }
- }
- break;
-
- default:
- break;
- }
- }
-
- StreamType & m_output_stream;
- std::size_t m_cursor_position{};
- std::size_t m_max_position{};
- };
-
-} // namespace ttwhy::routers
diff --git a/ttwhy/routers/echo.tests.cpp b/ttwhy/routers/echo.tests.cpp
deleted file mode 100644
index da2db40..0000000
--- a/ttwhy/routers/echo.tests.cpp
+++ /dev/null
@@ -1,257 +0,0 @@
-#include <asio.hpp>
-
-#include <catch2/catch_test_macros.hpp>
-
-#include <string>
-#include <string_view>
-#include <utility>
-
-import ttwhy.routers;
-import ttwhy.scanners;
-
-using namespace std::string_view_literals;
-
-namespace
-{
- struct mock_stream
- {
- using executor_type = asio::any_io_executor;
-
- asio::any_io_executor executor;
- std::string written{};
-
- explicit mock_stream(asio::any_io_executor exec)
- : executor{std::move(exec)}
- {}
-
- auto get_executor() const noexcept -> executor_type
- {
- return executor;
- }
-
- template<typename ConstBufferSequence, typename Token>
- auto async_write_some(ConstBufferSequence const & buffers, Token && token)
- {
- return asio::async_initiate<Token, void(asio::error_code, std::size_t)>(
- [this](auto handler, ConstBufferSequence bufs) {
- auto const total = asio::buffer_size(bufs);
- auto const offset = written.size();
- written.resize(offset + total);
- asio::buffer_copy(asio::buffer(written.data() + offset, total), bufs);
- asio::post(executor, [handler = std::move(handler), total]() mutable {
- std::move(handler)(asio::error_code{}, total);
- });
- },
- token, buffers);
- }
- };
-
- struct incomplete_router_no_process
- {
- };
-
- struct incomplete_router_wrong_return
- {
- auto process(ttwhy::scanners::input_event) -> void;
- };
-
- struct incomplete_router_wrong_event
- {
- auto process(int) -> asio::awaitable<void>;
- };
-} // namespace
-
-TEST_CASE("The router concept validates router candidate interfaces", "[router][concept]")
-{
- STATIC_REQUIRE(ttwhy::router<ttwhy::routers::echo<mock_stream>, ttwhy::scanners::input_event>);
- STATIC_REQUIRE_FALSE(ttwhy::router<incomplete_router_no_process, ttwhy::scanners::input_event>);
- STATIC_REQUIRE_FALSE(ttwhy::router<incomplete_router_wrong_return, ttwhy::scanners::input_event>);
- STATIC_REQUIRE_FALSE(ttwhy::router<incomplete_router_wrong_event, ttwhy::scanners::input_event>);
-}
-
-SCENARIO("The echo router handles terminal navigation within the valid printed area", "[router][echo]")
-{
- GIVEN("An initialized echo router with a mock stream")
- {
- auto context = asio::io_context{};
- auto stream = mock_stream{context.get_executor()};
- auto router = ttwhy::routers::echo{stream};
-
- auto run_coro = [&](asio::awaitable<void> coro) {
- asio::co_spawn(context, std::move(coro), asio::detached);
- context.restart();
- context.run();
- };
-
- WHEN("Typing characters")
- {
- run_coro(router.process(ttwhy::scanners::character_event{'A'}));
- run_coro(router.process(ttwhy::scanners::character_event{'B'}));
-
- THEN("Characters are echoed and positions advance")
- {
- CHECK(stream.written == "AB");
- CHECK(router.cursor_position() == 2);
- CHECK(router.max_position() == 2);
- }
-
- AND_WHEN("Navigating left")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::left}));
-
- THEN("The cursor moves left")
- {
- CHECK(stream.written == "\x1b[D");
- CHECK(router.cursor_position() == 1);
- CHECK(router.max_position() == 2);
- }
-
- AND_WHEN("Navigating left again to position 0")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::left}));
-
- THEN("The cursor reaches 0")
- {
- CHECK(stream.written == "\x1b[D");
- CHECK(router.cursor_position() == 0);
- CHECK(router.max_position() == 2);
- }
-
- AND_WHEN("Attempting to navigate left beyond 0")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::left}));
-
- THEN("Navigation is prevented and nothing is emitted")
- {
- CHECK(stream.written.empty());
- CHECK(router.cursor_position() == 0);
- CHECK(router.max_position() == 2);
- }
- }
- }
- }
-
- AND_WHEN("Attempting to navigate right beyond what was printed")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::right}));
-
- THEN("Navigation is prevented and nothing is emitted")
- {
- CHECK(stream.written.empty());
- CHECK(router.cursor_position() == 2);
- CHECK(router.max_position() == 2);
- }
- }
-
- AND_WHEN("Navigating left and then overwriting with new character")
- {
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::left}));
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::character_event{'X'}));
-
- THEN("The new character is written and max position is preserved")
- {
- CHECK(stream.written == "X");
- CHECK(router.cursor_position() == 2);
- CHECK(router.max_position() == 2);
- }
- }
-
- AND_WHEN("Using Home and End keys")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::home}));
-
- THEN("Home navigates to 0")
- {
- CHECK(stream.written == "\x1b[2D");
- CHECK(router.cursor_position() == 0);
- CHECK(router.max_position() == 2);
- }
-
- AND_WHEN("Pressing Home when already at 0")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::home}));
-
- THEN("Nothing is emitted")
- {
- CHECK(stream.written.empty());
- CHECK(router.cursor_position() == 0);
- }
- }
-
- AND_WHEN("Pressing End from 0")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::end}));
-
- THEN("End moves to max position")
- {
- CHECK(stream.written == "\x1b[2C");
- CHECK(router.cursor_position() == 2);
- CHECK(router.max_position() == 2);
- }
- }
- }
-
- AND_WHEN("Pressing Enter")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::control_event{ttwhy::scanners::control_key::enter}));
-
- THEN("Newline is emitted and positions reset")
- {
- CHECK(stream.written == "\r\n");
- CHECK(router.cursor_position() == 0);
- CHECK(router.max_position() == 0);
- }
- }
-
- AND_WHEN("Pressing Backspace at position 0 after reset")
- {
- run_coro(router.process(ttwhy::scanners::control_event{ttwhy::scanners::control_key::enter}));
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::control_event{ttwhy::scanners::control_key::backspace}));
-
- THEN("Backspace does nothing at position 0")
- {
- CHECK(stream.written.empty());
- CHECK(router.cursor_position() == 0);
- CHECK(router.max_position() == 0);
- }
- }
-
- AND_WHEN("Using Delete key")
- {
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::left}));
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::delete_key}));
-
- THEN("Delete character sequence is emitted and max position shrinks")
- {
- CHECK(stream.written == "\x1b[P");
- CHECK(router.cursor_position() == 1);
- CHECK(router.max_position() == 1);
- }
-
- AND_WHEN("Pressing Delete when cursor is at max position")
- {
- stream.written.clear();
- run_coro(router.process(ttwhy::scanners::navigation_event{ttwhy::scanners::navigation_key::delete_key}));
-
- THEN("Nothing is emitted")
- {
- CHECK(stream.written.empty());
- CHECK(router.cursor_position() == 1);
- CHECK(router.max_position() == 1);
- }
- }
- }
- }
- }
-}
diff --git a/ttwhy/routers/mod.cppm b/ttwhy/routers/mod.cppm
deleted file mode 100644
index d1d2d93..0000000
--- a/ttwhy/routers/mod.cppm
+++ /dev/null
@@ -1,4 +0,0 @@
-export module ttwhy.routers;
-
-export import :echo;
-export import :concepts;