module; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include export module ttwhy.terminal:echo; import :events; import ttwhy.core; namespace ttwhy::terminal { 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; namespace detail { //! Maps a function key to its CSI parameter code (F5 through F20). auto function_key_parameter(function_key key) -> unsigned int { switch (key) { case function_key::f5: return 15; case function_key::f6: return 17; case function_key::f7: return 18; case function_key::f8: return 19; case function_key::f9: return 20; case function_key::f10: return 21; case function_key::f11: return 23; case function_key::f12: return 24; case function_key::f13: return 25; case function_key::f14: return 26; case function_key::f15: return 28; case function_key::f16: return 29; case function_key::f17: return 31; case function_key::f18: return 32; case function_key::f19: return 33; case function_key::f20: return 34; default: return 0; } } //! Appends an xterm-style modifier parameter to a CSI sequence. auto apply_modifier(std::string sequence, modifier_set modifiers) -> std::string { auto const mask = static_cast(modifiers); auto const modifier_param = [&mask]() -> unsigned int { if ((mask & static_cast(modifier::shift)) != 0) { return 2; } if ((mask & static_cast(modifier::alt)) != 0) { return 3; } if ((mask & static_cast(modifier::ctrl)) != 0) { return 4; } if ((mask & static_cast(modifier::meta)) != 0) { return 5; } return 1; }(); if (modifier_param == 1) { return sequence; } auto const terminator = sequence.find_last_of('~'); if (terminator == std::string::npos) { return sequence; } return sequence.substr(0, terminator) + std::to_string(modifier_param) + sequence.substr(terminator); } //! Builds the terminal escape sequence that represents a function key press. auto function_key_sequence(function_key key, modifier_set modifiers) -> std::string { switch (key) { case function_key::f1: return apply_modifier(std::string{"\x1bOP"}, modifiers); case function_key::f2: return apply_modifier(std::string{"\x1bOQ"}, modifiers); case function_key::f3: return apply_modifier(std::string{"\x1bOR"}, modifiers); case function_key::f4: return apply_modifier(std::string{"\x1bOS"}, modifiers); default: return apply_modifier(std::format("\x1b[{}~", function_key_parameter(key)), modifiers); } } } // namespace detail //! An asynchronous router that echoes characters and handles terminal navigation. //! //! @tparam StreamType The output stream descriptor type. export template struct echo { //! Construct an echo router writing to an output stream. //! //! @param stream The output stream to write echoed text and escape sequences to. explicit echo(StreamType & stream) : m_output_stream{stream} {} //! Get the current cursor position within the printed line. //! //! @return The zero-based cursor position. [[nodiscard]] auto cursor_position() const noexcept -> std::size_t { return m_cursor_position; } //! Get the maximum cursor position reached on the current line. //! //! @return The maximum position reached. [[nodiscard]] auto max_position() const noexcept -> std::size_t { return m_max_position; } //! Asynchronously process an incoming terminal input event. //! //! @param event The input event to handle. //! @return An awaitable task that completes when processing finishes. auto process(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); } else if constexpr (std::same_as) { co_await process_function_key(event); } else if constexpr (std::same_as) { co_await process_ctrl_chord(event); } }, event); } private: auto process_function_key(function_key_event event) -> asio::awaitable { auto const sequence = detail::function_key_sequence(event.key, event.modifiers); co_await asio::async_write(m_output_stream, asio::buffer(sequence.data(), sequence.size()), asio::as_tuple(asio::use_awaitable)); } auto process_ctrl_chord(ctrl_chord_event event) -> asio::awaitable { auto const control_character = static_cast(static_cast(event.key) - 0x40u); co_await asio::async_write(m_output_stream, asio::buffer(&control_character, 1), asio::as_tuple(asio::use_awaitable)); } 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(control_key key) -> asio::awaitable { switch (key) { case 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 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 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(navigation_event event) -> asio::awaitable { switch (event.key) { case 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 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 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 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 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::terminal