aboutsummaryrefslogtreecommitdiff
path: root/ricochet/include
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-10-30 02:24:12 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-10-30 02:24:12 +0800
commit464313d02b76385d3a2e8809a4595afabca88dae (patch)
tree6cbf81416b856ff15fdc4239de5e3624ef10b2cc /ricochet/include
parentcf58b3b4dfa0dbbd0ca492484ca241b320c4ba9e (diff)
parentbdefdb301db843e87aee8c6c35808d295bb66ee5 (diff)
downloadrcc-project-master.tar.gz
rcc-project-master.tar.xz
Merge remote-tracking branch 'ricochet-import/master'HEADmaster
Diffstat (limited to 'ricochet/include')
-rw-r--r--ricochet/include/.gitkeep0
-rw-r--r--ricochet/include/ricochet/core/browser.hh40
-rw-r--r--ricochet/include/ricochet/core/form_handler.hh27
-rw-r--r--ricochet/include/ricochet/core/license.hh29
-rw-r--r--ricochet/include/ricochet/core/local_data.hh32
-rw-r--r--ricochet/include/ricochet/core/page_loader.hh32
-rw-r--r--ricochet/include/ricochet/core/page_processor.hh32
-rw-r--r--ricochet/include/ricochet/core/types.hh50
-rw-r--r--ricochet/include/ricochet/core/utils.hh30
-rw-r--r--ricochet/include/ricochet/net/config.hh35
-rw-r--r--ricochet/include/ricochet/net/cookie_jar.hh35
-rw-r--r--ricochet/include/ricochet/net/http_client.hh63
-rw-r--r--ricochet/include/ricochet/net/http_utils.hh30
-rw-r--r--ricochet/include/ricochet/net/ssl_socket.hh93
-rw-r--r--ricochet/include/ricochet/net/sys/address.hh88
-rw-r--r--ricochet/include/ricochet/net/sys/exception.hh80
-rw-r--r--ricochet/include/ricochet/net/sys/file_descriptor.hh141
-rw-r--r--ricochet/include/ricochet/net/sys/socket.hh160
-rw-r--r--ricochet/include/ricochet/net/sys/string_view_range.hh18
-rw-r--r--ricochet/include/ricochet/net/url_parser.hh37
-rw-r--r--ricochet/include/ricochet/parser/dom_tree.hh36
-rw-r--r--ricochet/include/ricochet/parser/html_lexer.hh64
-rw-r--r--ricochet/include/ricochet/parser/tree_builder.hh45
-rw-r--r--ricochet/include/ricochet/render/input_renderer.hh34
-rw-r--r--ricochet/include/ricochet/render/internal.hh35
-rw-r--r--ricochet/include/ricochet/render/renderer.hh60
-rw-r--r--ricochet/include/ricochet/render/table_renderer.hh30
-rw-r--r--ricochet/include/ricochet/render/utils.hh33
-rw-r--r--ricochet/include/ricochet/tui/document.hh255
-rw-r--r--ricochet/include/ricochet/tui/identity.hh63
-rw-r--r--ricochet/include/ricochet/tui/input.hh119
-rw-r--r--ricochet/include/ricochet/tui/messages.hh268
-rw-r--r--ricochet/include/ricochet/tui/position.hh116
-rw-r--r--ricochet/include/ricochet/tui/session.hh110
-rw-r--r--ricochet/include/ricochet/tui/view.hh74
35 files changed, 2394 insertions, 0 deletions
diff --git a/ricochet/include/.gitkeep b/ricochet/include/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ricochet/include/.gitkeep
diff --git a/ricochet/include/ricochet/core/browser.hh b/ricochet/include/ricochet/core/browser.hh
new file mode 100644
index 0000000..fabcba4
--- /dev/null
+++ b/ricochet/include/ricochet/core/browser.hh
@@ -0,0 +1,40 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace ricochet::core {
+
+class Browser
+{
+public:
+ Browser() = default;
+ ~Browser() = default;
+
+ Browser( const Browser& ) = delete;
+ Browser& operator=( const Browser& ) = delete;
+ Browser( Browser&& ) = delete;
+ Browser& operator=( Browser&& ) = delete;
+
+ static std::string format_url( std::string input );
+ static std::string get_configured_homepage();
+};
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/form_handler.hh b/ricochet/include/ricochet/core/form_handler.hh
new file mode 100644
index 0000000..d95bd02
--- /dev/null
+++ b/ricochet/include/ricochet/core/form_handler.hh
@@ -0,0 +1,27 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#pragma once
+
+#include "ricochet/core/types.hh"
+#include "ricochet/render/renderer.hh"
+#include <cstddef>
+#include <string>
+#include <vector>
+
+namespace ricochet::core {
+
+InputAction submit_form( const render::InputField& target_input,
+ const std::vector<render::InputField>& inputs,
+ const std::string& current_url,
+ std::vector<HttpRequest>& history,
+ std::size_t& history_idx );
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/license.hh b/ricochet/include/ricochet/core/license.hh
new file mode 100644
index 0000000..32c04c6
--- /dev/null
+++ b/ricochet/include/ricochet/core/license.hh
@@ -0,0 +1,29 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+namespace ricochet::core {
+
+void print_license_notice();
+
+void show_warranty();
+
+void show_copying();
+
+} // namespace ricochet::core \ No newline at end of file
diff --git a/ricochet/include/ricochet/core/local_data.hh b/ricochet/include/ricochet/core/local_data.hh
new file mode 100644
index 0000000..ca07846
--- /dev/null
+++ b/ricochet/include/ricochet/core/local_data.hh
@@ -0,0 +1,32 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace ricochet::core {
+
+std::string get_config_path( const std::string& filename );
+void save_bookmark( const std::string& title, const std::string& url );
+std::string generate_bookmarks_html();
+void append_to_history( const std::string& url, const std::string& title );
+std::string generate_history_html();
+std::string generate_help_html();
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/page_loader.hh b/ricochet/include/ricochet/core/page_loader.hh
new file mode 100644
index 0000000..11131e2
--- /dev/null
+++ b/ricochet/include/ricochet/core/page_loader.hh
@@ -0,0 +1,32 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/core/types.hh"
+// #include "ricochet/net/http_client.hh"
+// #include "ricochet/parser/dom_tree.hh"
+// #include <string>
+
+namespace ricochet::core {
+
+// std::string extract_title( const parser::DomNode& root_node );
+// PageData process_binary_download( const HttpRequest& req, const net::HttpResponse& resp );
+PageData load_page( const HttpRequest& req );
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/page_processor.hh b/ricochet/include/ricochet/core/page_processor.hh
new file mode 100644
index 0000000..8f40d3a
--- /dev/null
+++ b/ricochet/include/ricochet/core/page_processor.hh
@@ -0,0 +1,32 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/core/types.hh"
+#include "ricochet/net/http_client.hh"
+#include "ricochet/parser/dom_tree.hh"
+#include <string>
+
+namespace ricochet::core {
+
+std::string extract_title( const parser::DomNode& root_node );
+
+PageData process_binary_download( const HttpRequest& req, const net::HttpResponse& resp );
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/types.hh b/ricochet/include/ricochet/core/types.hh
new file mode 100644
index 0000000..ef48016
--- /dev/null
+++ b/ricochet/include/ricochet/core/types.hh
@@ -0,0 +1,50 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/render/renderer.hh"
+#include <cstdint>
+#include <string>
+#include <vector>
+
+namespace ricochet::core {
+
+struct PageData
+{
+ std::vector<std::string> lines;
+ std::vector<std::string> links;
+ std::vector<render::InputField> inputs;
+ std::string title;
+};
+
+struct HttpRequest
+{
+ std::string url {};
+ std::string method = "GET";
+ std::string body {};
+};
+
+enum class InputAction : std::uint8_t
+{
+ None,
+ Quit,
+ Navigate
+};
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/core/utils.hh b/ricochet/include/ricochet/core/utils.hh
new file mode 100644
index 0000000..61d480b
--- /dev/null
+++ b/ricochet/include/ricochet/core/utils.hh
@@ -0,0 +1,30 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace ricochet::core {
+
+std::vector<std::string> split_into_lines( const std::string& text );
+std::string normalize_url( const std::string& target, const std::string& current_url );
+std::string strip_ddg_tracker( std::string target );
+
+} // namespace ricochet::core
diff --git a/ricochet/include/ricochet/net/config.hh b/ricochet/include/ricochet/net/config.hh
new file mode 100644
index 0000000..fa73193
--- /dev/null
+++ b/ricochet/include/ricochet/net/config.hh
@@ -0,0 +1,35 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+
+namespace ricochet::net {
+
+std::string get_config_path( const std::string& filename );
+
+struct BrowserConfig
+{
+ std::string user_agent;
+ std::string homepage;
+};
+
+BrowserConfig& get_browser_config();
+
+} // namespace ricochet::net
diff --git a/ricochet/include/ricochet/net/cookie_jar.hh b/ricochet/include/ricochet/net/cookie_jar.hh
new file mode 100644
index 0000000..5c5befb
--- /dev/null
+++ b/ricochet/include/ricochet/net/cookie_jar.hh
@@ -0,0 +1,35 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <unordered_map>
+
+namespace ricochet::net {
+
+using CookieJar = std::unordered_map<std::string, std::unordered_map<std::string, std::string>>;
+
+void load_cookies_from_file( CookieJar& jar );
+CookieJar& get_cookie_jar();
+void save_cookies_to_file( const CookieJar& jar );
+
+void update_cookies( const std::string& raw_response, const std::string& host );
+std::string build_cookie_header( const std::string& host );
+
+} // namespace ricochet::net
diff --git a/ricochet/include/ricochet/net/http_client.hh b/ricochet/include/ricochet/net/http_client.hh
new file mode 100644
index 0000000..f57cbcf
--- /dev/null
+++ b/ricochet/include/ricochet/net/http_client.hh
@@ -0,0 +1,63 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <expected>
+#include <string>
+#include <string_view>
+#include <unordered_map>
+
+namespace ricochet::net {
+/**
+ * @brief Encapsulate HTTP response result
+ */
+
+struct HttpResponse
+{
+ int status_code { 0 };
+ std::unordered_map<std::string, std::string> headers {};
+ std::string body {};
+ std::string content_type {};
+};
+
+class HttpClient
+{
+public:
+ HttpClient() = default;
+ ~HttpClient() = default;
+
+ HttpClient( const HttpClient& ) = delete;
+ HttpClient& operator=( const HttpClient& ) = delete;
+ HttpClient( HttpClient&& ) = delete;
+ HttpClient& operator=( HttpClient&& ) = delete;
+
+ /**
+ * @brief Initiates an HTTP GET request (only supports http:// in MVP stage)
+ * @param url Target URL, for example "http://example.com"
+ * @return Returns HttpResponse on success, returns an error message string on failure
+ */
+ std::expected<HttpResponse, std::string> fetch( std::string_view url,
+ std::string_view method
+ = "GET", // NOLINT(bugprone-easily-swappable-parameters)
+ std::string_view body = "" ) const;
+
+private:
+};
+
+} // namespace ricochet::net \ No newline at end of file
diff --git a/ricochet/include/ricochet/net/http_utils.hh b/ricochet/include/ricochet/net/http_utils.hh
new file mode 100644
index 0000000..3d96e79
--- /dev/null
+++ b/ricochet/include/ricochet/net/http_utils.hh
@@ -0,0 +1,30 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+namespace ricochet::net {
+
+std::string dechunk( std::string_view body );
+std::string remove_html_comments( std::string html );
+std::string remove_tag_blocks( std::string html, const std::string& tag_name );
+
+} // namespace ricochet::net
diff --git a/ricochet/include/ricochet/net/ssl_socket.hh b/ricochet/include/ricochet/net/ssl_socket.hh
new file mode 100644
index 0000000..36a92b6
--- /dev/null
+++ b/ricochet/include/ricochet/net/ssl_socket.hh
@@ -0,0 +1,93 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "sys/address.hh"
+#include "sys/socket.hh"
+#include <memory>
+#include <string>
+#include <string_view>
+
+extern "C" {
+using SSL_CTX = struct ssl_ctx_st;
+using SSL = struct ssl_st;
+}
+
+namespace ricochet::net {
+
+struct SslCtxDeleter
+{
+ void operator()( SSL_CTX* ptr ) const;
+};
+struct SslDeleter
+{
+ void operator()( SSL* ptr ) const;
+};
+
+using UniqueSslCtx = std::unique_ptr<SSL_CTX, SslCtxDeleter>;
+using UniqueSsl = std::unique_ptr<SSL, SslDeleter>;
+
+/**
+ * @brief Socket wrapper that supports TLS/SSL encrypted handshake
+ */
+class SSLSocket
+{
+public:
+ SSLSocket();
+ ~SSLSocket() = default;
+
+ SSLSocket( const SSLSocket& ) = delete;
+ SSLSocket& operator=( const SSLSocket& ) = delete;
+ SSLSocket( SSLSocket&& ) = delete;
+ SSLSocket& operator=( SSLSocket&& ) = delete;
+
+ /**
+ * @brief Connect to a remote host using TLS/SSL
+ * @param host The hostname to connect to
+ * @param port The port to connect to
+ * @return True if the connection was successful, false otherwise
+ */
+ void connect( const Address& address, const std::string& hostname );
+
+ /**
+ * @brief Send data over the TLS/SSL connection
+ * @param data The data to send
+ * @return The number of bytes sent, or -1 if an error occurred
+ */
+ std::size_t write( std::string_view data );
+
+ /**
+ * @brief Receive data from the TLS/SSL connection
+ * @param buffer The buffer to store the received data
+ */
+ void read( std::string& buffer );
+
+ /**
+ * @brief Check if the TLS/SSL connection is closed
+ * @return True if the connection is closed, false otherwise
+ */
+ bool eof() const;
+
+private:
+ TCPSocket underlying_socket_ {}; // TCP Socket
+ UniqueSslCtx ctx_ { nullptr }; // OpenSSL global context
+ UniqueSsl ssl_ { nullptr }; // Current connection's SSL state
+ bool eof_ { false }; // EOF flag
+};
+} // namespace ricochet::net \ No newline at end of file
diff --git a/ricochet/include/ricochet/net/sys/address.hh b/ricochet/include/ricochet/net/sys/address.hh
new file mode 100644
index 0000000..39c9553
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/address.hh
@@ -0,0 +1,88 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+#include <netdb.h>
+#include <string>
+#include <sys/socket.h>
+#include <utility>
+
+//! Wrapper around [IPv4 addresses](@ref man7::ip) and DNS operations.
+class Address
+{
+public:
+ //! \brief Wrapper around [sockaddr_storage](@ref man7::socket).
+ //! \details A `sockaddr_storage` is enough space to store any socket address (IPv4 or IPv6).
+ class Raw
+ {
+ public:
+ sockaddr_storage storage {}; //!< The wrapped struct itself.
+ // NOLINTBEGIN (*-explicit-*)
+ operator sockaddr*();
+ operator const sockaddr*() const;
+ // NOLINTEND (*-explicit-*)
+ };
+
+private:
+ socklen_t size_; //!< Size of the wrapped address.
+ Raw address_ {}; //!< A wrapped [sockaddr_storage](@ref man7::socket) containing the address.
+
+ //! Constructor from ip/host, service/port, and hints to the resolver.
+ Address( const std::string& node, const std::string& service, const addrinfo& hints );
+
+public:
+ //! Construct by resolving a hostname and servicename.
+ Address( const std::string& hostname, const std::string& service );
+
+ //! Construct from dotted-quad string ("18.243.0.1") and numeric port.
+ explicit Address( const std::string& ip, std::uint16_t port = 0 );
+
+ //! Construct from a [sockaddr *](@ref man7::socket).
+ Address( const sockaddr* addr, std::size_t size );
+
+ //! Default constructor
+ Address() : Address( nullptr, 0 ) {}
+
+ //! Equality comparison.
+ bool operator==( const Address& other ) const;
+ bool operator!=( const Address& other ) const { return not operator==( other ); }
+
+ //! \name Conversions
+ //!@{
+
+ //! Dotted-quad IP address string ("18.243.0.1") and numeric port.
+ std::pair<std::string, uint16_t> ip_port() const;
+ //! Dotted-quad IP address string ("18.243.0.1").
+ std::string ip() const { return ip_port().first; }
+ //! Numeric port (host byte order).
+ uint16_t port() const { return ip_port().second; }
+ //! Numeric IP address as an integer (i.e., in [host byte order](\ref man3::byteorder)).
+ uint32_t ipv4_numeric() const;
+ //! Create an Address from a 32-bit raw numeric IP address
+ static Address from_ipv4_numeric( uint32_t ip_address );
+ //! Human-readable string, e.g., "8.8.8.8:53".
+ std::string to_string() const;
+ //!@}
+
+ //! \name Low-level operations
+ //!@{
+
+ //! Size of the underlying address storage.
+ socklen_t size() const { return size_; }
+ //! Const pointer to the underlying socket address storage.
+ const sockaddr* raw() const { return static_cast<const sockaddr*>( address_ ); }
+ //! Safely convert to underlying sockaddr type
+ template<typename sockaddr_type>
+ const sockaddr_type* as() const;
+
+ //!@}
+};
diff --git a/ricochet/include/ricochet/net/sys/exception.hh b/ricochet/include/ricochet/net/sys/exception.hh
new file mode 100644
index 0000000..9587e5c
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/exception.hh
@@ -0,0 +1,80 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include <cxxabi.h>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <system_error>
+
+class tagged_error : public std::system_error
+{
+private:
+ std::string attempt_and_error_;
+ int error_code_;
+
+public:
+ tagged_error( const std::error_category& category, const std::string_view s_attempt, const int error_code )
+ : system_error( error_code, category )
+ , attempt_and_error_( std::string( s_attempt ) + ": " + std::system_error::what() )
+ , error_code_( error_code )
+ {}
+
+ const char* what() const noexcept override { return attempt_and_error_.c_str(); }
+
+ int error_code() const { return error_code_; }
+};
+
+class unix_error : public tagged_error
+{
+public:
+ explicit unix_error( const std::string_view s_attempt, const int s_errno = errno )
+ : tagged_error( std::system_category(), s_attempt, s_errno )
+ {}
+};
+
+inline int CheckSystemCall( const std::string_view s_attempt, const int return_value )
+{
+ if ( return_value >= 0 ) {
+ return return_value;
+ }
+
+ throw unix_error { s_attempt };
+}
+
+template<typename T>
+inline T* notnull( const std::string_view context, T* const x )
+{
+ return x ? x : throw std::runtime_error( std::string( context ) + ": returned null pointer" );
+}
+
+template<typename T>
+inline std::unique_ptr<T> notnull( const std::string_view context, std::unique_ptr<T> x )
+{
+ return x ? x : throw std::runtime_error( std::string( context ) + ": returned null pointer" );
+}
+
+template<typename T>
+inline std::shared_ptr<T> notnull( const std::string_view context, std::shared_ptr<T>&& x )
+{
+ return x ? std::move( x ) : throw std::runtime_error( std::string( context ) + ": returned null pointer" );
+}
+
+inline std::string demangle( const char* name )
+{
+ int status {};
+ const std::unique_ptr<char, decltype( &free )> res { abi::__cxa_demangle( name, nullptr, nullptr, &status ),
+ free };
+ if ( status ) {
+ throw std::runtime_error( "cxa_demangle" );
+ }
+ return res.get();
+}
diff --git a/ricochet/include/ricochet/net/sys/file_descriptor.hh b/ricochet/include/ricochet/net/sys/file_descriptor.hh
new file mode 100644
index 0000000..4af56cb
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/file_descriptor.hh
@@ -0,0 +1,141 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include "string_view_range.hh"
+
+#include <bits/types/struct_iovec.h>
+#include <cstddef>
+#include <memory>
+#include <vector>
+
+// A reference-counted handle to a file descriptor
+class FileDescriptor
+{
+public:
+ // Construct from a file descriptor number returned by the kernel
+ explicit FileDescriptor( int fd );
+
+ // Read from the file descriptor into `buffer` (a single string) or `buffers` (a vector of strings)
+ void read( std::string& buffer );
+ void read( std::vector<std::string>& buffers );
+
+ // `write_all` writes a buffer completely.
+ void write_all( std::string_view buffer );
+
+ // `write` writes *from* a buffer or range of buffers and returns the number of bytes it actually wrote.
+ size_t write( std::string_view buffer );
+ size_t write( const StringViewRange auto&& buffers )
+ {
+ static thread_local std::vector<iovec> iovecs;
+ const size_t total_size = to_iovecs( buffers, iovecs );
+ return write( iovecs, total_size );
+ }
+
+ // Close the underlying file descriptor
+ void close() { internal_fd_->close(); }
+
+ // Set blocking(true) or non-blocking(false) status on the file descriptor
+ void set_blocking( bool blocking );
+
+ // Copy a FileDescriptor explicitly, increasing the internal reference count
+ FileDescriptor duplicate() const;
+
+ // The destructor calls close() when no more references remain.
+ ~FileDescriptor() = default;
+
+ // Accessors
+ int fd_num() const { return internal_fd_->fd_; } // underlying descriptor number
+ bool eof() const { return internal_fd_->eof_; } // EOF flag state
+ bool closed() const { return internal_fd_->closed_; } // closed flag state
+ bool blocking() const { return not internal_fd_->non_blocking_; } // blocking state
+ unsigned int read_count() const { return internal_fd_->read_count_; } // number of reads
+ unsigned int write_count() const { return internal_fd_->write_count_; } // number of writes
+
+ // Copy/move constructor/assignment operators
+ // FileDescriptor can be moved, but cannot be copied implicitly (see duplicate())
+ FileDescriptor( const FileDescriptor& other ) = delete; // copy construction is forbidden
+ FileDescriptor& operator=( const FileDescriptor& other ) = delete; // copy assignment is forbidden
+ FileDescriptor( FileDescriptor&& other ) = default; // move construction is allowed
+ FileDescriptor& operator=( FileDescriptor&& other ) = default; // move assignment is allowed
+
+private:
+ // FDWrapper: A handle on a kernel file descriptor.
+ // FileDescriptor objects contain a std::shared_ptr to a FDWrapper.
+ class FDWrapper
+ {
+ public:
+ int fd_; // The file descriptor number returned by the kernel
+ bool eof_ = false; // Flag indicating whether FDWrapper::fd_ is at EOF
+ bool closed_ = false; // Flag indicating whether FDWrapper::fd_ has been closed
+ bool non_blocking_ = false; // Flag indicating whether FDWrapper::fd_ is non-blocking
+ unsigned read_count_ = 0; // The number of times FDWrapper::fd_ has been read
+ unsigned write_count_ = 0; // The numberof times FDWrapper::fd_ has been written
+
+ // Construct from a file descriptor number returned by the kernel
+ explicit FDWrapper( int fd );
+ // Closes the file descriptor upon destruction
+ ~FDWrapper();
+ // Calls [close(2)](\ref man2::close) on FDWrapper::fd_
+ void close();
+
+ size_t CheckFDSystemCall( std::string_view what, ssize_t return_value ) const;
+ size_t CheckRead( std::string_view what, ssize_t return_value );
+
+ // An FDWrapper cannot be copied or moved
+ FDWrapper( const FDWrapper& other ) = delete;
+ FDWrapper& operator=( const FDWrapper& other ) = delete;
+ FDWrapper( FDWrapper&& other ) = delete;
+ FDWrapper& operator=( FDWrapper&& other ) = delete;
+ };
+
+ // A reference-counted handle to a shared FDWrapper
+ std::shared_ptr<FDWrapper> internal_fd_;
+
+ // Private constructor used to duplicate the FileDescriptor (increase the reference count)
+ explicit FileDescriptor( std::shared_ptr<FDWrapper> other_shared_ptr );
+
+ // Private write method that takes a vector of iovecs (an internal OS structure)
+ size_t write( const std::vector<iovec>& iovecs, size_t total_size );
+
+protected:
+ // size of buffer to allocate by read(), recv(), etc., when passed-in buffer is empty
+ static constexpr size_t kReadBufferSize = 16384;
+
+ void set_eof() { internal_fd_->eof_ = true; }
+ void register_read() { ++internal_fd_->read_count_; } // increment read count
+ void register_write() { ++internal_fd_->write_count_; } // increment write count
+
+ size_t CheckFDSystemCall( std::string_view what, ssize_t return_value ) const;
+ size_t CheckRead( std::string_view what, ssize_t return_value );
+
+ // Convert a range of string_view-convertible objects to a vector of iovecs
+ static size_t to_iovecs( const StringViewRange auto& buffers, std::vector<iovec>& iovecs )
+ {
+ if ( buffers.empty() ) {
+ throw std::runtime_error( "to_iovecs called with empty buffer list" );
+ }
+ iovecs.clear();
+ iovecs.reserve( buffers.size() );
+ size_t total_size = 0;
+ for ( const auto& buf : buffers ) {
+ const std::string_view x { buf };
+ if ( x.empty() ) {
+ throw std::runtime_error( "to_iovecs called with empty buffer in buffer list" );
+ }
+ iovecs.push_back( { const_cast<char*>( x.data() ), x.size() } ); // NOLINT(*-const-cast)
+ total_size += x.size();
+ }
+ if ( total_size == 0 ) {
+ throw std::runtime_error( "to_iovecs called with zero-size buffer list" );
+ }
+ return total_size;
+ }
+};
diff --git a/ricochet/include/ricochet/net/sys/socket.hh b/ricochet/include/ricochet/net/sys/socket.hh
new file mode 100644
index 0000000..f84be26
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/socket.hh
@@ -0,0 +1,160 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include "address.hh"
+#include "file_descriptor.hh"
+
+#include <functional>
+#include <sys/socket.h>
+
+//! \brief Base class for network sockets (TCP, UDP, etc.)
+//! \details Socket is generally used via a subclass. See TCPSocket and UDPSocket for usage examples.
+class Socket : public FileDescriptor
+{
+public:
+ //! Bind a socket to a specified address with [bind(2)](\ref man2::bind), usually for listen/accept
+ void bind( const Address& address );
+
+ //! Bind a socket to a specified device
+ void bind_to_device( std::string_view device_name );
+
+ //! Connect a socket to a specified peer address with [connect(2)](\ref man2::connect)
+ void connect( const Address& address );
+
+ //! Shut down a socket via [shutdown(2)](\ref man2::shutdown)
+ void shutdown( int how );
+
+ //! Get local address of socket with [getsockname(2)](\ref man2::getsockname)
+ Address local_address() const;
+ //! Get peer address of socket with [getpeername(2)](\ref man2::getpeername)
+ Address peer_address() const;
+
+ //! Allow local address to be reused sooner via [SO_REUSEADDR](\ref man7::socket)
+ void set_reuseaddr();
+
+ //! Check for errors (will be seen on non-blocking sockets)
+ void throw_if_error() const;
+
+private:
+ //! Get the local or peer address the socket is connected to
+ Address get_address( const std::string& name_of_function,
+ const std::function<int( int, sockaddr*, socklen_t* )>& function ) const;
+
+protected:
+ //! Construct via [socket(2)](\ref man2::socket)
+ Socket( int domain, int type, int protocol = 0 );
+
+ //! Construct from a file descriptor.
+ Socket( FileDescriptor&& fd, int domain, int type, int protocol = 0 );
+
+ //! Wrapper around [getsockopt(2)](\ref man2::getsockopt)
+ template<typename option_type>
+ socklen_t getsockopt( int level, int option, option_type& option_value ) const;
+
+ //! Wrappers around [setsockopt(2)](\ref man2::setsockopt)
+ template<typename option_type>
+ void setsockopt( int level, int option, const option_type& option_value );
+
+ void setsockopt( int level, int option, std::string_view option_val );
+};
+
+class DatagramSocket : public Socket
+{
+public:
+ // Receive a datagram and the Address of its sender, into a buffer or collection of buffers.
+ void recv( Address& source_address, std::string& payload );
+ void recv( Address& source_address, std::vector<std::string>& payloads );
+
+ //! Send a datagram to specified Address (if destination is missing, sends to already-connected address)
+ void send( std::string_view payload, const std::optional<Address>& destination = {} );
+ void send( const StringViewRange auto&& payloads, const std::optional<Address>& destination = {} )
+ {
+ static thread_local std::vector<iovec> iovecs;
+ const size_t total_size = to_iovecs( payloads, iovecs );
+ send( iovecs, total_size, destination );
+ }
+
+private:
+ void send( std::vector<iovec>& iovecs, size_t total_size, const std::optional<Address>& destination = {} );
+
+protected:
+ DatagramSocket( int domain, int type, int protocol = 0 ) : Socket( domain, type, protocol ) {}
+
+ //! Construct from a file descriptor.
+ DatagramSocket( FileDescriptor&& fd, int domain, int type, int protocol = 0 )
+ : Socket( std::move( fd ), domain, type, protocol )
+ {}
+};
+
+//! A wrapper around [UDP sockets](\ref man7::udp)
+class UDPSocket : public DatagramSocket
+{
+ //! \param[in] fd is the FileDescriptor from which to construct
+ explicit UDPSocket( FileDescriptor&& fd ) : DatagramSocket( std::move( fd ), AF_INET, SOCK_DGRAM ) {}
+
+public:
+ //! Default: construct an unbound, unconnected UDP socket
+ UDPSocket() : DatagramSocket( AF_INET, SOCK_DGRAM ) {}
+};
+
+//! A wrapper around [TCP sockets](\ref man7::tcp)
+class TCPSocket : public Socket
+{
+private:
+ //! \brief Construct from FileDescriptor (used by accept())
+ //! \param[in] fd is the FileDescriptor from which to construct
+ explicit TCPSocket( FileDescriptor&& fd ) : Socket( std::move( fd ), AF_INET, SOCK_STREAM, IPPROTO_TCP ) {}
+
+public:
+ //! Default: construct an unbound, unconnected TCP socket
+ TCPSocket() : Socket( AF_INET, SOCK_STREAM ) {}
+
+ //! Mark a socket as listening for incoming connections
+ void listen( int backlog = 16 );
+
+ //! Accept a new incoming connection
+ TCPSocket accept();
+};
+
+//! A wrapper around [packet sockets](\ref man7:packet)
+class PacketSocket : public DatagramSocket
+{
+public:
+ PacketSocket( const int type, const int protocol ) : DatagramSocket( AF_PACKET, type, protocol ) {}
+
+ void set_promiscuous();
+};
+
+//! A wrapper around [raw sockets](\ref man7:raw)
+class RawSocket : public DatagramSocket
+{
+public:
+ RawSocket() : DatagramSocket( AF_INET, SOCK_RAW, IPPROTO_RAW ) {}
+};
+
+//! A wrapper around [Unix-domain stream sockets](\ref man7::unix)
+class LocalStreamSocket : public Socket
+{
+public:
+ //! Construct from a file descriptor
+ explicit LocalStreamSocket( FileDescriptor&& fd ) : Socket( std::move( fd ), AF_UNIX, SOCK_STREAM ) {}
+};
+
+//! A wrapper around [Unix-domain datagram sockets](\ref man7::unix)
+class LocalDatagramSocket : public DatagramSocket
+{
+ //! \param[in] fd is the FileDescriptor from which to construct
+ explicit LocalDatagramSocket( FileDescriptor&& fd ) : DatagramSocket( std::move( fd ), AF_UNIX, SOCK_DGRAM ) {}
+
+public:
+ //! Default: construct an unbound, unconnected socket
+ LocalDatagramSocket() : DatagramSocket( AF_UNIX, SOCK_DGRAM ) {}
+};
diff --git a/ricochet/include/ricochet/net/sys/string_view_range.hh b/ricochet/include/ricochet/net/sys/string_view_range.hh
new file mode 100644
index 0000000..84be394
--- /dev/null
+++ b/ricochet/include/ricochet/net/sys/string_view_range.hh
@@ -0,0 +1,18 @@
+/**
+ * ---------------------------------------------------------------------------
+ * @notice This file was adapted from the Minnow networking framework,
+ * originally developed for Stanford University's CS144 course.
+ * It has been incorporated into the Ricochet project as foundational
+ * system utility code.
+ * ---------------------------------------------------------------------------
+ */
+
+#pragma once
+
+#include <ranges>
+#include <type_traits>
+
+// This concept represents any range (e.g. vector) whose elements can be converted to string_view.
+template<typename R>
+concept StringViewRange
+ = std::ranges::sized_range<R> && std::is_constructible_v<std::string_view, std::ranges::range_value_t<R>>;
diff --git a/ricochet/include/ricochet/net/url_parser.hh b/ricochet/include/ricochet/net/url_parser.hh
new file mode 100644
index 0000000..039e4dc
--- /dev/null
+++ b/ricochet/include/ricochet/net/url_parser.hh
@@ -0,0 +1,37 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <expected>
+#include <string>
+#include <string_view>
+
+namespace ricochet::net {
+
+struct ParsedUrl
+{
+ std::string host {};
+ std::string path {};
+};
+
+std::expected<ParsedUrl, std::string> parse_url( std::string_view url );
+
+std::string extract_redirect_url( const std::string& raw_response, bool is_https, const std::string& host );
+
+} // namespace ricochet::net
diff --git a/ricochet/include/ricochet/parser/dom_tree.hh b/ricochet/include/ricochet/parser/dom_tree.hh
new file mode 100644
index 0000000..e3b3664
--- /dev/null
+++ b/ricochet/include/ricochet/parser/dom_tree.hh
@@ -0,0 +1,36 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+namespace ricochet::parser {
+
+/**
+ * @brief DOM Node. Pure value semantics, no pointers.
+ */
+struct DomNode
+{
+ std::string tag_name {}; // If empty, it's a pure text node
+ std::string text_content {}; // Only meaningful when tag_name is empty
+ std::vector<DomNode> children {}; // Recursive child nodes list
+};
+
+} // namespace ricochet::parser \ No newline at end of file
diff --git a/ricochet/include/ricochet/parser/html_lexer.hh b/ricochet/include/ricochet/parser/html_lexer.hh
new file mode 100644
index 0000000..d9ce23a
--- /dev/null
+++ b/ricochet/include/ricochet/parser/html_lexer.hh
@@ -0,0 +1,64 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <variant>
+#include <vector>
+
+namespace ricochet::parser {
+
+struct TextToken
+{
+ std::string content {};
+};
+
+struct TagOpenToken
+{
+ std::string name {};
+};
+
+struct TagCloseToken
+{
+ std::string name {};
+};
+
+using HtmlToken = std::variant<TextToken, TagOpenToken, TagCloseToken>;
+
+class HtmlLexer
+{
+public:
+ HtmlLexer() = default;
+ ~HtmlLexer() = default;
+
+ HtmlLexer( const HtmlLexer& ) = delete;
+ HtmlLexer& operator=( const HtmlLexer& ) = delete;
+ HtmlLexer( HtmlLexer&& ) = delete;
+ HtmlLexer& operator=( HtmlLexer&& ) = delete;
+
+ /**
+ * @brief Perform lexical analysis to convert HTML source code into a structured sequence of tokens
+ */
+ std::vector<HtmlToken> tokenize( std::string_view html ) const;
+
+private:
+};
+
+} // namespace ricochet::parser \ No newline at end of file
diff --git a/ricochet/include/ricochet/parser/tree_builder.hh b/ricochet/include/ricochet/parser/tree_builder.hh
new file mode 100644
index 0000000..2cb0748
--- /dev/null
+++ b/ricochet/include/ricochet/parser/tree_builder.hh
@@ -0,0 +1,45 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include "ricochet/parser/html_lexer.hh"
+
+#include <vector>
+
+namespace ricochet::parser {
+
+class TreeBuilder
+{
+public:
+ TreeBuilder() = default;
+ ~TreeBuilder() = default;
+
+ TreeBuilder( const TreeBuilder& ) = delete;
+ TreeBuilder& operator=( const TreeBuilder& ) = delete;
+ TreeBuilder( TreeBuilder&& ) = delete;
+ TreeBuilder& operator=( TreeBuilder&& ) = delete;
+
+ /**
+ * @brief Assemble a one-dimensional token sequence into a multi-dimensional DOM tree
+ */
+ DomNode build( const std::vector<HtmlToken>& tokens ) const;
+};
+
+} // namespace ricochet::parser \ No newline at end of file
diff --git a/ricochet/include/ricochet/render/input_renderer.hh b/ricochet/include/ricochet/render/input_renderer.hh
new file mode 100644
index 0000000..0114673
--- /dev/null
+++ b/ricochet/include/ricochet/render/input_renderer.hh
@@ -0,0 +1,34 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include "ricochet/render/renderer.hh"
+#include <string>
+
+namespace ricochet::render {
+
+struct FormContext;
+
+void render_input_node( const parser::DomNode& node,
+ RenderResult& result,
+ const FormContext& ctx,
+ std::string& output );
+
+} // namespace ricochet::render
diff --git a/ricochet/include/ricochet/render/internal.hh b/ricochet/include/ricochet/render/internal.hh
new file mode 100644
index 0000000..5e7cac0
--- /dev/null
+++ b/ricochet/include/ricochet/render/internal.hh
@@ -0,0 +1,35 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include "ricochet/render/renderer.hh"
+#include <string>
+
+namespace ricochet::render {
+
+struct FormContext
+{
+ std::string action;
+ std::string method;
+};
+
+void render_node( const parser::DomNode& node, RenderResult& result, FormContext ctx );
+
+} // namespace ricochet::render
diff --git a/ricochet/include/ricochet/render/renderer.hh b/ricochet/include/ricochet/render/renderer.hh
new file mode 100644
index 0000000..472afaa
--- /dev/null
+++ b/ricochet/include/ricochet/render/renderer.hh
@@ -0,0 +1,60 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include <string>
+
+namespace ricochet::render {
+
+struct InputField
+{
+ std::string name;
+ std::string action;
+ std::string method;
+ std::string value {};
+ std::string type {};
+ bool checked { false };
+};
+struct RenderResult
+{
+ std::string text {};
+ std::vector<std::string> links {};
+ std::vector<InputField> inputs {};
+};
+
+class Renderer
+{
+public:
+ Renderer() = default;
+ ~Renderer() = default;
+
+ Renderer( const Renderer& ) = delete;
+ Renderer& operator=( const Renderer& ) = delete;
+ Renderer( Renderer&& ) = delete;
+ Renderer& operator=( Renderer&& ) = delete;
+
+ /**
+ * @brief Traverse the DOM tree and output the formatted result to the terminal using ANSI escape codes
+ * @param node The root node of the DOM tree
+ */
+ RenderResult render( const parser::DomNode& node ) const;
+};
+
+} // namespace ricochet::render \ No newline at end of file
diff --git a/ricochet/include/ricochet/render/table_renderer.hh b/ricochet/include/ricochet/render/table_renderer.hh
new file mode 100644
index 0000000..5efc42c
--- /dev/null
+++ b/ricochet/include/ricochet/render/table_renderer.hh
@@ -0,0 +1,30 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/parser/dom_tree.hh"
+#include "ricochet/render/renderer.hh"
+
+namespace ricochet::render {
+
+struct FormContext;
+
+void render_table( const parser::DomNode& node, RenderResult& result, const FormContext& ctx );
+
+} // namespace ricochet::render
diff --git a/ricochet/include/ricochet/render/utils.hh b/ricochet/include/ricochet/render/utils.hh
new file mode 100644
index 0000000..38b3022
--- /dev/null
+++ b/ricochet/include/ricochet/render/utils.hh
@@ -0,0 +1,33 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <string>
+#include <string_view>
+
+namespace ricochet::render {
+
+std::string decode_entities( std::string text );
+std::string extract_attr( const std::string& tag_name, std::string_view attr_name );
+std::size_t get_visible_length( std::string_view str );
+
+bool is_ignored_tag( std::string_view tag );
+bool is_heading_tag( std::string_view tag );
+
+} // namespace ricochet::render
diff --git a/ricochet/include/ricochet/tui/document.hh b/ricochet/include/ricochet/tui/document.hh
new file mode 100644
index 0000000..bd76442
--- /dev/null
+++ b/ricochet/include/ricochet/tui/document.hh
@@ -0,0 +1,255 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/position.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace ricochet::tui {
+
+struct TextStyle
+{
+ bool emphasis { false };
+ bool strong { false };
+ bool code { false };
+ bool subdued { false };
+ bool warning { false };
+};
+
+struct StyledRange
+{
+ ByteRange bytes {};
+ TextStyle style {};
+};
+
+// Leaf IDs and structural block IDs share one document-local namespace.
+// All text offsets address a Text leaf, not a concatenation of rendered lines.
+struct Text
+{
+ BlockId id {};
+ // LF is an explicit hard break; soft wrapping never changes this string.
+ std::string utf8 {};
+ std::vector<StyledRange> styles {};
+};
+
+struct ControlPlaceholder
+{ ControlId control {}; };
+
+using Inline = std::variant<Text, ControlPlaceholder>;
+
+struct Paragraph
+{ std::vector<Inline> content {}; };
+
+struct Heading
+{
+ unsigned int level { 1 };
+ std::vector<Inline> content {};
+};
+
+struct Preformatted
+{
+ Text content {};
+ std::optional<std::string> language {};
+};
+
+struct Block;
+
+struct ListItem
+{
+ std::optional<std::string> marker {};
+ std::vector<Block> content {};
+};
+
+enum class ListKind : std::uint8_t
+{
+ Unordered,
+ Ordered
+};
+
+struct List
+{
+ ListKind kind { ListKind::Unordered };
+ std::int64_t first_number { 1 };
+ std::vector<ListItem> items {};
+};
+
+struct TableCell
+{
+ // The producer resolves HTML spans to grid coordinates before delivery.
+ std::size_t column { 0 };
+ std::size_t column_span { 1 };
+ std::size_t row_span { 1 };
+ bool header { false };
+ std::vector<Block> content {};
+};
+
+struct TableRow
+{ std::vector<TableCell> cells {}; };
+
+struct Table
+{
+ std::size_t columns { 0 };
+ std::vector<TableRow> rows {};
+};
+
+struct Alternative
+{ Text description {}; };
+
+struct Separator
+{};
+
+struct Block
+{
+ // Structural identity supports empty blocks and per-block horizontal scroll.
+ BlockId id {};
+ std::variant<Paragraph, Heading, Preformatted, List, Table, ControlPlaceholder, Alternative, Separator>
+ content {};
+};
+
+struct Link
+{
+ LinkId id {};
+ std::string target {};
+ std::string description {};
+ // Multiple ranges can belong to one link, independently of text styling.
+ std::vector<TextRange> ranges {};
+};
+
+enum class TextControlKind : std::uint8_t
+{
+ SingleLine,
+ MultiLine,
+ Password
+};
+
+struct TextControl
+{
+ TextControlKind kind { TextControlKind::SingleLine };
+ std::string value {};
+ std::string default_value {};
+};
+
+struct Checkbox
+{
+ bool checked { false };
+ bool default_checked { false };
+};
+
+struct RadioButton
+{
+ RadioGroupId group {};
+ bool checked { false };
+ bool default_checked { false };
+};
+
+struct SelectOption
+{
+ OptionId id {};
+ std::string label {};
+ bool disabled { false };
+};
+
+enum class SelectKind : std::uint8_t
+{
+ Single,
+ Multiple
+};
+
+struct SelectControl
+{
+ SelectKind kind { SelectKind::Single };
+ std::vector<SelectOption> options {};
+ std::vector<OptionId> selected {};
+ std::vector<OptionId> default_selected {};
+};
+
+enum class ButtonKind : std::uint8_t
+{
+ Submit,
+ Reset
+};
+
+struct Button
+{ ButtonKind kind { ButtonKind::Submit }; };
+
+struct UnsupportedControl
+{ std::string description {}; };
+
+struct Control
+{
+ ControlId id {};
+ std::optional<FormId> form {};
+ std::string label {};
+ std::string hint {};
+ std::optional<std::string> error {};
+ bool disabled { false };
+ bool read_only { false };
+ std::variant<TextControl, Checkbox, RadioButton, SelectControl, Button, UnsupportedControl> content {};
+};
+
+struct Form
+{
+ FormId id {};
+ std::string label {};
+};
+
+struct Anchor
+{
+ std::string name {};
+ ContentPoint point {};
+};
+
+struct DocumentMetadata
+{
+ std::string title {};
+ std::string display_url {};
+ std::optional<std::string> language {};
+};
+
+// Producers build owned values. No DOM pointers, terminal codes or screen coordinates.
+struct DocumentData
+{
+ DocumentVersion version {};
+ DocumentMetadata metadata {};
+ std::vector<Block> blocks {};
+ std::vector<Link> links {};
+ std::vector<Control> controls {};
+ std::vector<Form> forms {};
+ std::vector<Anchor> anchors {};
+};
+
+class Document
+{
+public:
+ explicit Document( DocumentData data );
+
+ const DocumentData& data() const noexcept;
+
+private:
+ std::shared_ptr<const DocumentData> data_;
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/identity.hh b/ricochet/include/ricochet/tui/identity.hh
new file mode 100644
index 0000000..624c6cc
--- /dev/null
+++ b/ricochet/include/ricochet/tui/identity.hh
@@ -0,0 +1,63 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <compare>
+#include <cstdint>
+
+namespace ricochet::tui {
+
+template<class Tag>
+struct Identity
+{
+ std::uint64_t value { 0 };
+
+ auto operator<=>( const Identity& ) const = default;
+};
+
+using TabId = Identity<struct TabTag>;
+using DocumentId = Identity<struct DocumentTag>;
+using Revision = Identity<struct RevisionTag>;
+using DraftRevision = Identity<struct DraftRevisionTag>;
+using BlockId = Identity<struct BlockTag>;
+using LinkId = Identity<struct LinkTag>;
+using ControlId = Identity<struct ControlTag>;
+using FormId = Identity<struct FormTag>;
+using OptionId = Identity<struct OptionTag>;
+using RadioGroupId = Identity<struct RadioGroupTag>;
+using TaskId = Identity<struct TaskTag>;
+using RequestId = Identity<struct RequestTag>;
+
+struct DocumentVersion
+{
+ DocumentId document {};
+ Revision revision {};
+
+ bool operator==( const DocumentVersion& ) const = default;
+};
+
+struct DocumentScope
+{
+ TabId tab {};
+ DocumentVersion version {};
+
+ bool operator==( const DocumentScope& ) const = default;
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/input.hh b/ricochet/include/ricochet/tui/input.hh
new file mode 100644
index 0000000..df5b458
--- /dev/null
+++ b/ricochet/include/ricochet/tui/input.hh
@@ -0,0 +1,119 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <chrono>
+#include <cstdint>
+#include <string>
+#include <variant>
+
+namespace ricochet::tui {
+
+struct ScreenSize
+{
+ int rows { 0 };
+ int columns { 0 };
+ bool operator==( const ScreenSize& ) const = default;
+};
+
+struct CellPoint
+{
+ int row { 0 };
+ int column { 0 };
+};
+
+struct Modifiers
+{
+ bool shift { false };
+ bool control { false };
+ bool alt { false };
+ bool operator==( const Modifiers& ) const = default;
+};
+
+enum class KeyCode : std::uint8_t
+{
+ Character,
+ Escape,
+ Enter,
+ Tab,
+ Backspace,
+ Delete,
+ Insert,
+ Left,
+ Right,
+ Up,
+ Down,
+ Home,
+ End,
+ PageUp,
+ PageDown,
+ Function,
+ Unsupported
+};
+
+struct Key
+{
+ KeyCode code { KeyCode::Unsupported };
+ Modifiers modifiers {};
+ char32_t character { 0 };
+ unsigned int function { 0 };
+};
+
+struct TextInput
+{ char32_t character { 0 }; };
+
+struct Paste
+{ std::u32string text {}; };
+
+struct MouseButtons
+{
+ bool left { false };
+ bool middle { false };
+ bool right { false };
+};
+
+struct Mouse
+{
+ // Screen coordinates; the view router converts these to region-local coordinates.
+ CellPoint position {};
+ Modifiers modifiers {};
+ MouseButtons pressed {};
+ MouseButtons released {};
+ bool motion { false };
+ int wheel_steps { 0 }; // Positive scrolls down.
+};
+
+struct Resize
+{ ScreenSize size {}; };
+
+struct Tick
+{ std::chrono::steady_clock::time_point now {}; };
+
+enum class TerminalCloseReason : std::uint8_t
+{
+ Disconnected,
+ TerminationSignal
+};
+
+struct TerminalClosed
+{ TerminalCloseReason reason { TerminalCloseReason::Disconnected }; };
+
+using InputEvent = std::variant<Key, TextInput, Paste, Mouse, Resize, Tick, TerminalClosed>;
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/messages.hh b/ricochet/include/ricochet/tui/messages.hh
new file mode 100644
index 0000000..9d51fa3
--- /dev/null
+++ b/ricochet/include/ricochet/tui/messages.hh
@@ -0,0 +1,268 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/document.hh"
+#include "ricochet/tui/view.hh"
+
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace ricochet::tui {
+
+struct TabInfo
+{
+ TabId id {};
+ std::string title {};
+ std::string display_url {};
+ std::optional<TaskId> loading_task {};
+};
+
+struct SyncTabs
+{
+ // Vector order is tab order. Request IDs acknowledge create/close actions.
+ std::vector<TabInfo> tabs {};
+ std::optional<TabId> active {};
+ std::vector<RequestId> completed_requests {};
+};
+
+struct NavigationStarted
+{
+ TabId tab {};
+ RequestId request {};
+ TaskId task {};
+ std::string display_target {};
+};
+
+struct CommitDocument
+{
+ TabId tab {};
+ RequestId navigation {};
+ Document document;
+ std::optional<ViewSnapshot> restore {};
+};
+
+struct TextCorrespondence
+{
+ // The producer identifies identical text in the two revisions, including every
+ // changed portion crossed by a selection. This is not an inferred text diff.
+ TextRange source {};
+ TextRange target {};
+};
+
+struct PositionMapping
+{
+ DocumentVersion source {};
+ DocumentVersion target {};
+ std::vector<TextCorrespondence> text {};
+};
+
+enum class ViewRetention : std::uint8_t
+{
+ Reset,
+ PreserveUnchanged
+};
+
+struct UpdateDocument
+{
+ TabId tab {};
+ RequestId navigation {};
+ DocumentVersion source {};
+ Document document;
+ ViewRetention retention { ViewRetention::PreserveUnchanged };
+ std::optional<PositionMapping> mapping {};
+};
+
+struct LocateView
+{
+ DocumentScope scope {};
+ std::variant<std::string, DocumentPoint> target {};
+};
+
+// Values are keyed by control identity, never by field name or action URL.
+// These business messages may carry passwords; ViewSnapshot deliberately does not.
+using ControlValue = std::variant<std::string, bool, std::vector<OptionId>>;
+
+struct ControlDraft
+{
+ ControlId control {};
+ ControlValue value {};
+};
+
+struct FieldError
+{
+ ControlId control {};
+ std::string message {};
+};
+
+struct FormFeedback
+{
+ DocumentScope scope {};
+ FormId form {};
+ RequestId request {};
+ DraftRevision draft_revision {};
+ std::vector<FieldError> errors {};
+ std::vector<ControlDraft> normalized_values {};
+};
+
+struct ActionFailed
+{
+ RequestId request {};
+ std::optional<TabId> tab {};
+ std::string summary {};
+ std::optional<std::string> details {};
+};
+
+struct ExitDecision
+{
+ RequestId request {};
+ bool approved { false };
+ std::optional<std::string> explanation {};
+};
+
+enum class CopyStatus : std::uint8_t
+{
+ Confirmed,
+ Dispatched,
+ Unavailable,
+ Failed
+};
+
+// The host's injected clipboard provider completes a CopySelection request once.
+// Dispatched must not be used as confirmation that a terminal accepted the text.
+struct CopyResult
+{
+ RequestId request {};
+ CopyStatus status { CopyStatus::Unavailable };
+ std::string summary {};
+};
+
+using HostUpdate = std::variant<SyncTabs,
+ NavigationStarted,
+ CommitDocument,
+ UpdateDocument,
+ LocateView,
+ FormFeedback,
+ CopyResult,
+ ActionFailed,
+ ExitDecision>;
+
+enum class OpenTarget : std::uint8_t
+{
+ CurrentTab,
+ NewTab
+};
+
+struct OpenLocation
+{
+ RequestId request {};
+ TabId tab {};
+ std::string input {};
+ OpenTarget target { OpenTarget::CurrentTab };
+};
+
+struct OpenLink
+{
+ RequestId request {};
+ DocumentScope source {};
+ LinkId link {};
+ std::string target {};
+ OpenTarget destination { OpenTarget::CurrentTab };
+};
+
+enum class NavigationCommand : std::uint8_t
+{
+ Back,
+ Forward,
+ Reload
+};
+
+struct Navigate
+{
+ RequestId request {};
+ TabId tab {};
+ std::optional<DocumentVersion> document {};
+ NavigationCommand command { NavigationCommand::Reload };
+};
+
+struct CreateTab
+{ RequestId request {}; };
+
+struct CloseTab
+{
+ RequestId request {};
+ TabId tab {};
+};
+
+enum class FormOperation : std::uint8_t
+{
+ Submit,
+ Reset
+};
+
+struct FormAction
+{
+ RequestId request {};
+ DocumentScope scope {};
+ FormId form {};
+ std::optional<ControlId> button {};
+ DraftRevision draft_revision {};
+ FormOperation operation { FormOperation::Submit };
+ std::vector<ControlDraft> values {};
+};
+
+struct ExportSelection
+{
+ RequestId request {};
+ DocumentScope source {};
+ // Already extracted from one view snapshot; passwords are masked.
+ std::string text {};
+};
+
+struct CopySelection
+{
+ RequestId request {};
+ DocumentScope source {};
+ DraftRevision draft_revision {};
+ // Frozen before dispatch; later cursor/selection changes cannot change this copy.
+ // Password content is already masked. Clipboard providers never receive drafts.
+ std::string text {};
+};
+
+struct RequestExit
+{
+ RequestId request {};
+ std::optional<TabId> active {};
+ std::vector<ViewSnapshot> views {};
+};
+
+using UserAction = std::variant<OpenLocation,
+ OpenLink,
+ Navigate,
+ CreateTab,
+ CloseTab,
+ FormAction,
+ ExportSelection,
+ CopySelection,
+ RequestExit>;
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/position.hh b/ricochet/include/ricochet/tui/position.hh
new file mode 100644
index 0000000..49b63f2
--- /dev/null
+++ b/ricochet/include/ricochet/tui/position.hh
@@ -0,0 +1,116 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/identity.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <variant>
+
+namespace ricochet::tui {
+
+// UTF-8 byte ranges are half-open. Text operations use grapheme boundaries.
+struct ByteRange
+{
+ std::size_t start { 0 };
+ std::size_t end { 0 };
+
+ bool operator==( const ByteRange& ) const = default;
+};
+
+struct TextPosition
+{
+ BlockId block {};
+ std::size_t byte_offset { 0 };
+
+ bool operator==( const TextPosition& ) const = default;
+};
+
+struct ControlTextPosition
+{
+ ControlId control {};
+ DraftRevision revision {};
+ // Offset in display text; password offsets refer only to its mask.
+ std::size_t byte_offset { 0 };
+
+ bool operator==( const ControlTextPosition& ) const = default;
+};
+
+enum class BoundarySide : std::uint8_t
+{
+ Before,
+ After
+};
+
+struct ObjectPosition
+{
+ std::variant<BlockId, ControlId> object {};
+ BoundarySide side { BoundarySide::Before };
+
+ bool operator==( const ObjectPosition& ) const = default;
+};
+
+struct DocumentStart
+{ bool operator==( const DocumentStart& ) const = default; };
+
+// Local to a document snapshot. Stored anchors cannot depend on mutable drafts.
+using ContentPoint = std::variant<DocumentStart, TextPosition, ObjectPosition>;
+using DocumentPoint = std::variant<DocumentStart, TextPosition, ControlTextPosition, ObjectPosition>;
+
+struct LogicalPosition
+{
+ DocumentScope scope {};
+ DocumentPoint point {};
+
+ bool operator==( const LogicalPosition& ) const = default;
+};
+
+// Endpoints follow reading order, never the numeric order of their IDs.
+struct DocumentRange
+{
+ DocumentPoint start {};
+ DocumentPoint end {};
+
+ bool operator==( const DocumentRange& ) const = default;
+};
+
+struct TextRange
+{
+ BlockId block {};
+ ByteRange bytes {};
+
+ bool operator==( const TextRange& ) const = default;
+};
+
+enum class WrapAffinity : std::uint8_t
+{
+ PreviousLine,
+ NextLine
+};
+
+struct DisplayPosition
+{
+ DocumentPoint point {};
+ WrapAffinity affinity { WrapAffinity::NextLine };
+
+ bool operator==( const DisplayPosition& ) const = default;
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/session.hh b/ricochet/include/ricochet/tui/session.hh
new file mode 100644
index 0000000..94cae68
--- /dev/null
+++ b/ricochet/include/ricochet/tui/session.hh
@@ -0,0 +1,110 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/input.hh"
+#include "ricochet/tui/messages.hh"
+
+#include <chrono>
+#include <cstdio>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace ricochet::tui {
+
+namespace detail {
+class Mailbox;
+}
+
+struct TerminalOptions
+{
+ bool capture_mouse { true };
+ // Explicit host opt-in: terminfo does not establish bracketed-paste support.
+ bool bracketed_paste { false };
+ std::chrono::milliseconds escape_delay { 25 };
+};
+
+struct TerminalCapabilities
+{
+ bool colors { false };
+ bool mouse_buttons { false };
+ bool mouse_motion { false };
+ bool bracketed_paste { false };
+};
+
+enum class PostResult : std::uint8_t
+{
+ Queued,
+ Closed
+};
+
+// A sender can outlive the UI session. Posting is thread-safe; it never calls curses.
+class UpdateSender
+{
+public:
+ PostResult post_update( HostUpdate update ) const;
+
+private:
+ friend class Session;
+ explicit UpdateSender( std::shared_ptr<detail::Mailbox> mailbox );
+ std::shared_ptr<detail::Mailbox> mailbox_;
+};
+
+struct StepResult
+{
+ bool running { true };
+ // Incremental document updates and editing are subsequent milestones. Until then,
+ // unsupported updates are returned intact rather than silently acknowledged.
+ std::vector<HostUpdate> unhandled_updates {};
+ std::optional<TerminalClosed> terminal_closed {};
+ std::optional<std::string> error {};
+};
+
+class Session
+{
+public:
+ explicit Session( TerminalOptions options = {} );
+ // Streams must remain open until destruction. Construction, step, close and
+ // destruction run on the same UI thread; only one terminal session may exist.
+ Session( std::FILE& input, std::FILE& output, TerminalOptions options = {} );
+ ~Session();
+
+ Session( const Session& ) = delete;
+ Session& operator=( const Session& ) = delete;
+ Session( Session&& ) = delete;
+ Session& operator=( Session&& ) = delete;
+
+ UpdateSender sender() const;
+ PostResult post_update( HostUpdate update );
+ // max_wait bounds idle waiting. Decoding a partial terminal key sequence can
+ // additionally wait for escape_delay; processing batches is bounded separately.
+ StepResult step( std::chrono::milliseconds max_wait );
+ std::vector<UserAction> take_actions();
+ std::optional<ViewSnapshot> snapshot( TabId tab ) const;
+ TerminalCapabilities capabilities() const;
+ void close() noexcept;
+
+private:
+ class Impl;
+ std::unique_ptr<Impl> impl_;
+};
+
+} // namespace ricochet::tui
diff --git a/ricochet/include/ricochet/tui/view.hh b/ricochet/include/ricochet/tui/view.hh
new file mode 100644
index 0000000..cc1d0fd
--- /dev/null
+++ b/ricochet/include/ricochet/tui/view.hh
@@ -0,0 +1,74 @@
+/*
+ * Ricochet - A minimalist CLI web browser for fast, distraction-free navigation.
+ * Copyright (C) 2026 Richard Qin
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "ricochet/tui/position.hh"
+
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace ricochet::tui {
+
+enum class SelectionDirection : std::uint8_t
+{
+ Forward,
+ Backward
+};
+
+struct SelectionSnapshot
+{
+ // The normalized half-open range also covers any controls between its endpoints.
+ DocumentRange range {};
+ SelectionDirection direction { SelectionDirection::Forward };
+ // Inclusive endpoint units retain the anchor through reversal and history restore.
+ DocumentRange anchor {};
+ DocumentRange active {};
+};
+
+struct HorizontalScroll
+{
+ BlockId block {};
+ std::size_t columns { 0 };
+};
+
+struct SearchSnapshot
+{
+ std::string query {};
+ std::optional<DocumentRange> current_match {};
+};
+
+// All positions and ranges belong to scope and draft_revision.
+// Editing sessions and control values (including passwords) are not exported here.
+struct ViewSnapshot
+{
+ DocumentScope scope {};
+ DraftRevision draft_revision {};
+ DisplayPosition cursor {};
+ DisplayPosition viewport_anchor {};
+ // Relative visual row for blank table space with no text at the exact top.
+ std::int64_t viewport_row_offset { 0 };
+ std::optional<SelectionSnapshot> selection {};
+ std::vector<HorizontalScroll> horizontal_scroll {};
+ SearchSnapshot search {};
+};
+
+} // namespace ricochet::tui