From d05b9343f69a0841043963088e0003368e205bd9 Mon Sep 17 00:00:00 2001 From: Felix Morgner Date: Tue, 22 Sep 2026 22:47:33 +0200 Subject: lib: restructure into vertical slices --- ttwhy/routers/concepts.cppm | 17 --- ttwhy/routers/echo.cppm | 206 ---------------------------------- ttwhy/routers/echo.tests.cpp | 257 ------------------------------------------- ttwhy/routers/mod.cppm | 4 - 4 files changed, 484 deletions(-) delete mode 100644 ttwhy/routers/concepts.cppm delete mode 100644 ttwhy/routers/echo.cppm delete mode 100644 ttwhy/routers/echo.tests.cpp delete mode 100644 ttwhy/routers/mod.cppm (limited to 'ttwhy/routers') 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 - -#include - -export module ttwhy.routers:concepts; - -namespace ttwhy -{ - - export template - concept router = requires(Candidate candidate, Event event) { - { candidate.process(event) } -> std::same_as>; - }; - -} // 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 - -#include - -#include -#include -#include - -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 - 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 - { - co_await std::visit( - [&](auto && event) -> asio::awaitable { - using event_type = std::decay_t; - - if constexpr (std::same_as) - { - co_await process_character(event.value); - } - else if constexpr (std::same_as) - { - co_await process_control_key(event.key); - } - else if constexpr (std::same_as) - { - co_await process_navigation_key(event); - } - }, - event); - } - - private: - auto process_character(char character) -> asio::awaitable - { - 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 - { - 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 - { - 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 - -#include - -#include -#include -#include - -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 - auto async_write_some(ConstBufferSequence const & buffers, Token && token) - { - return asio::async_initiate( - [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; - }; -} // namespace - -TEST_CASE("The router concept validates router candidate interfaces", "[router][concept]") -{ - STATIC_REQUIRE(ttwhy::router, ttwhy::scanners::input_event>); - STATIC_REQUIRE_FALSE(ttwhy::router); - STATIC_REQUIRE_FALSE(ttwhy::router); - STATIC_REQUIRE_FALSE(ttwhy::router); -} - -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 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; -- cgit v1.2.3