From 3f39df2356504bc68a4e493a723fd4d907c43ff4 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Sat, 11 Apr 2026 00:50:38 +0800 Subject: feat: initialize project structure with CMake build system, C++23 support, and core browser scaffolding --- ricochet/src/main.cc | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ricochet/src/main.cc (limited to 'ricochet/src/main.cc') diff --git a/ricochet/src/main.cc b/ricochet/src/main.cc new file mode 100644 index 0000000..0371cda --- /dev/null +++ b/ricochet/src/main.cc @@ -0,0 +1,71 @@ +/* + * 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 . + */ + +#include +#include +#include +#include + +#include "ricochet/core/browser.hh" +#include "ricochet/core/license.hh" + +namespace { + +void print_usage( std::string_view program_name ) +{ + std::cout << "Usage: " << program_name << " [URL]\n" + << "Example: " << program_name << " https://wikipedia.org\n"; +} + +} // namespace + +int main( int argc, char* argv[] ) +{ + const std::span args_view( argv, static_cast( argc ) ); + const std::vector args( args_view.begin(), args_view.end() ); + + ricochet::core::print_license_notice(); + + if ( args.size() < 2 ) { + std::cerr << "Error: No URL provided.\n"; + print_usage( args[0] ); + return 1; + } + + const std::string_view input = args[1]; + + if ( input == "show" ) { + if ( args[2] == "w" ) { + ricochet::core::show_warranty(); + } else if ( args[2] == "c" ) { + ricochet::core::show_copying(); + } else { + std::cerr << "Error: Invalid argument for show command.\n"; + print_usage( args[0] ); + return 1; + } + return 0; + } + + try { + return ricochet::core::Browser::run( input ); + } catch ( const std::exception& e ) { + std::cerr << "Critical Error: " << e.what() << "\n"; + return 1; + } +} \ No newline at end of file -- cgit v1.2.3 From 79ab6fcb6e74109c46e836594b2cc1ead87d7b30 Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Sun, 12 Apr 2026 16:43:23 +0800 Subject: feat: implement HTTP redirect support, URL search formatting, image alt text rendering, and improved terminal cursor management --- ricochet/src/core/browser.cc | 131 +++++++++++++++++++++++++++++++--------- ricochet/src/main.cc | 7 ++- ricochet/src/net/http_client.cc | 126 +++++++++++++++++++++++++++----------- ricochet/src/render/renderer.cc | 16 ++++- ricochet/src/tui/terminal.cc | 1 + 5 files changed, 214 insertions(+), 67 deletions(-) (limited to 'ricochet/src/main.cc') diff --git a/ricochet/src/core/browser.cc b/ricochet/src/core/browser.cc index 8bd921c..2968ab1 100644 --- a/ricochet/src/core/browser.cc +++ b/ricochet/src/core/browser.cc @@ -80,10 +80,34 @@ void draw_view( const tui::Terminal& terminal, std::cout << "~\r\n"; } } - std::cout << "\033[7m URL: " << url << " | [j/k] Scroll [/] Jump [f] Follow [q] Quit \033[0m"; + std::cout << "\033[7m URL: " << url << " | [j/k] Scroll [/] Jump [f] Follow [H/L] History [q] Quit \033[0m"; std::cout.flush(); } +std::string format_url( std::string input ) +{ + if ( input.empty() ) { + return input; + } + + if ( input.starts_with( "http://" ) || input.starts_with( "https://" ) ) { + return input; + } + + const bool is_domain = input.contains( '.' ) && !input.contains( ' ' ); + if ( is_domain ) { + input.insert( 0, "https://" ); + return input; + } + + for ( char& ch : input ) { + if ( ch == ' ' ) { + ch = '+'; + } + } + return "https://lite.duckduckgo.com/lite/?q=" + input; +} + std::string get_url_input( const tui::Terminal& terminal ) { auto [width, height] = terminal.get_size(); @@ -116,10 +140,7 @@ std::string get_url_input( const tui::Terminal& terminal ) } terminal.show_cursor( false ); - if ( !input.empty() && !input.starts_with( "http://" ) && !input.starts_with( "https://" ) ) { - input.insert( 0, "https://" ); - } - return input; + return format_url( std::move( input ) ); } std::string get_link_input( const tui::Terminal& terminal ) @@ -195,50 +216,100 @@ void handle_follow_link( const tui::Terminal& terminal, } } +enum class InputAction : std::uint8_t +{ + None, + Quit, + Navigate +}; + +InputAction process_key( char c, + const tui::Terminal& terminal, + const PageData& page_data, + std::string& current_url, + std::vector& history, + std::size_t& history_idx, // NOLINT(bugprone-easily-swappable-parameters) + std::size_t& scroll_y ) // NOLINT(bugprone-easily-swappable-parameters) +{ + auto [width, height] = terminal.get_size(); + const std::size_t view_h = ( height > 1 ) ? ( height - 1 ) : 1; + + if ( c == 'q' ) { + return InputAction::Quit; + } + if ( c == 'j' && scroll_y + view_h < page_data.lines.size() ) { + scroll_y++; + } + if ( c == 'k' && scroll_y > 0 ) { + scroll_y--; + } + if ( c == 'H' && history_idx > 0 ) { + history_idx--; + return InputAction::Navigate; + } + if ( c == 'L' && history_idx + 1 < history.size() ) { + history_idx++; + return InputAction::Navigate; + } + + if ( c == '/' ) { + const std::string next_url = get_url_input( terminal ); + if ( !next_url.empty() ) { + history.resize( history_idx + 1 ); + history.push_back( next_url ); + history_idx++; + return InputAction::Navigate; + } + } + + if ( c == 'f' ) { + const std::string old_url = current_url; + bool nav = false; + handle_follow_link( terminal, page_data.links, current_url, nav ); + if ( nav && current_url != old_url ) { + history.resize( history_idx + 1 ); + history.push_back( current_url ); + history_idx++; + return InputAction::Navigate; + } + } + return InputAction::None; +} + } // namespace int Browser::run( std::string_view initial_url ) { - std::cout << "-> Fetching: " << initial_url << "...\n"; - - std::string current_url { initial_url }; + std::vector history; + history.emplace_back( initial_url ); + std::size_t history_idx = 0; const tui::Terminal terminal; - terminal.clear_screen(); + terminal.show_cursor( false ); + + while ( true ) { + std::string current_url = history[history_idx]; + terminal.clear_screen(); + std::cout << "-> Fetching: " << current_url << "...\r\n"; + std::cout.flush(); - while ( !current_url.empty() ) { auto page_data = load_page( current_url ); - const auto& lines = page_data.lines; - const auto& links = page_data.links; std::size_t scroll_y = 0; bool navigate = false; while ( !navigate ) { - draw_view( terminal, lines, scroll_y, current_url ); - + draw_view( terminal, page_data.lines, scroll_y, current_url ); const char c = terminal.read_key(); - auto [width, height] = terminal.get_size(); - const std::size_t view_h = ( height > 1 ) ? ( height - 1 ) : 1; - if ( c == 'q' ) { + const InputAction action = process_key( c, terminal, page_data, current_url, history, history_idx, scroll_y ); + if ( action == InputAction::Quit ) { terminal.clear_screen(); return 0; } - if ( c == 'j' && scroll_y + view_h < lines.size() ) { - scroll_y++; - } else if ( c == 'k' && scroll_y > 0 ) { - scroll_y--; - } else if ( c == '/' ) { - std::string next_url = get_url_input( terminal ); - if ( !next_url.empty() ) { - current_url = std::move( next_url ); - navigate = true; - } - } else if ( c == 'f' ) { - handle_follow_link( terminal, links, current_url, navigate ); + if ( action == InputAction::Navigate ) { + navigate = true; } } } - terminal.clear_screen(); return 0; } diff --git a/ricochet/src/main.cc b/ricochet/src/main.cc index 0371cda..1037833 100644 --- a/ricochet/src/main.cc +++ b/ricochet/src/main.cc @@ -62,8 +62,13 @@ int main( int argc, char* argv[] ) return 0; } + std::string start_url( input ); + if ( !start_url.starts_with( "http://" ) && !start_url.starts_with( "https://" ) ) { + start_url.insert( 0, "https://" ); + } + try { - return ricochet::core::Browser::run( input ); + return ricochet::core::Browser::run( start_url ); } catch ( const std::exception& e ) { std::cerr << "Critical Error: " << e.what() << "\n"; return 1; diff --git a/ricochet/src/net/http_client.cc b/ricochet/src/net/http_client.cc index b31976f..5cf6a30 100644 --- a/ricochet/src/net/http_client.cc +++ b/ricochet/src/net/http_client.cc @@ -133,63 +133,111 @@ std::string remove_tag_blocks( std::string html, const std::string& tag_name ) / return html; } -} // namespace - -std::expected -HttpClient::fetch( // NOLINT(readability-convert-member-functions-to-static) - std::string_view url ) const +std::string extract_redirect_url( const std::string& raw_response, bool is_https, const std::string& host ) { - auto parsed_url = parse_url( url ); - if ( !parsed_url.has_value() ) { - return std::unexpected( parsed_url.error() ); + auto loc_pos = raw_response.find( "Location: " ); + if ( loc_pos == std::string::npos ) { + loc_pos = raw_response.find( "location: " ); } + if ( loc_pos != std::string::npos ) { + loc_pos += 10; + auto end_pos = raw_response.find( '\r', loc_pos ); + if ( end_pos == std::string::npos ) { + end_pos = raw_response.find( '\n', loc_pos ); + } + if ( end_pos != std::string::npos ) { + std::string next_url = raw_response.substr( loc_pos, end_pos - loc_pos ); + while ( !next_url.empty() && std::isspace( static_cast( next_url.front() ) ) ) { + next_url.erase( 0, 1 ); + } + if ( next_url.starts_with( "/" ) ) { + next_url = ( is_https ? "https://" : "http://" ) + host + next_url; + } + return next_url; + } + } + return ""; +} - auto [host, path] = parsed_url.value(); - const bool is_https = url.starts_with( "https://" ); - const std::string service = is_https ? "https" : "http"; - +struct FetchResult +{ + std::string raw_response {}; + std::string error {}; +}; +FetchResult do_network_request( const std::string& host, + const std::string& service, // NOLINT(bugprone-easily-swappable-parameters) + const std::string& path, // NOLINT(bugprone-easily-swappable-parameters) + bool is_https ) +{ + FetchResult res; try { const Address server_addr( host, service ); + const std::string request + = std::format( "GET {} HTTP/1.1\r\n" + "Host: {}\r\n" + "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/124.0.0.0 Safari/537.36\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Language: en-US,en;q=0.9\r\n" + "DNT: 1\r\n" + "Connection: close\r\n\r\n", + path, + host ); - const std::string request = std::format( "GET {} HTTP/1.1\r\n" - "Host: {}\r\n" - "User-Agent: Mozilla/5.0 (Ricochet/0.1.0)\r\n" - "Accept: text/html\r\n" - "Accept-Encoding: identity\r\n" - "Connection: close\r\n\r\n", - path, - host ); - - std::string raw_response; if ( is_https ) { SSLSocket ssl_socket; ssl_socket.connect( server_addr, host ); - size_t bytes_written = 0; - while ( bytes_written < request.size() ) { - bytes_written += ssl_socket.write( request.substr( bytes_written ) ); + for ( size_t bw = 0; bw < request.size(); ) { + bw += ssl_socket.write( request.substr( bw ) ); } while ( !ssl_socket.eof() ) { std::string buffer; ssl_socket.read( buffer ); - raw_response += buffer; + res.raw_response += buffer; } } else { TCPSocket tcp_socket; tcp_socket.connect( server_addr ); - size_t bytes_written = 0; - while ( bytes_written < request.size() ) { - bytes_written += tcp_socket.write( request.substr( bytes_written ) ); + for ( size_t bw = 0; bw < request.size(); ) { + bw += tcp_socket.write( request.substr( bw ) ); } while ( !tcp_socket.eof() ) { std::string buffer; tcp_socket.read( buffer ); - raw_response += buffer; + res.raw_response += buffer; } } + } catch ( const std::exception& e ) { + res.error = std::string( "Network Error: " ) + e.what(); + } + return res; +} - HttpResponse response; - auto header_end = raw_response.find( "\r\n\r\n" ); +} // namespace + +std::expected HttpClient::fetch( std::string_view url ) const +{ + (void)this; // 完美绕过 static 方法警告,满足最严苛的 tidy 规则 + std::string current_url( url ); + + for ( int redirect_count = 0; redirect_count < 5; ++redirect_count ) { + const auto parsed_url = parse_url( current_url ); + if ( !parsed_url.has_value() ) { + return std::unexpected( parsed_url.error() ); + } + + const auto [host, path] = parsed_url.value(); + const bool is_https = current_url.starts_with( "https://" ); + const std::string service = is_https ? "https" : "http"; + + const FetchResult net_res = do_network_request( host, service, path, is_https ); + if ( !net_res.error.empty() ) { + return std::unexpected( net_res.error ); + } + const std::string& raw_response = net_res.raw_response; + HttpResponse response; + const auto header_end = raw_response.find( "\r\n\r\n" ); if ( header_end != std::string::npos ) { response.body = raw_response.substr( header_end + 4 ); if ( raw_response.size() >= 12 ) { @@ -201,19 +249,27 @@ HttpClient::fetch( // NOLINT(readability-convert-member-functions-to-static) } } else { response.body = raw_response; + response.status_code = 0; + } + + if ( response.status_code >= 300 && response.status_code < 400 ) { + const std::string next_url = extract_redirect_url( raw_response, is_https, host ); + if ( !next_url.empty() ) { + current_url = next_url; + continue; + } } if ( raw_response.contains( "Transfer-Encoding: chunked" ) ) { response.body = dechunk( response.body ); } - response.body = remove_html_comments( response.body ); response.body = remove_tag_blocks( response.body, "script" ); response.body = remove_tag_blocks( response.body, "style" ); return response; - } catch ( const std::exception& e ) { - return std::unexpected( std::string( "Network Error: " ) + e.what() ); } + + return std::unexpected( "Too many redirects" ); } } // namespace ricochet::net \ No newline at end of file diff --git a/ricochet/src/render/renderer.cc b/ricochet/src/render/renderer.cc index 1af40f1..2b690ca 100644 --- a/ricochet/src/render/renderer.cc +++ b/ricochet/src/render/renderer.cc @@ -83,11 +83,14 @@ void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion) const bool is_header = ( base_tag == "h1" || base_tag == "h2" || base_tag == "h3" || base_tag == "h4" ); const bool is_link = ( base_tag == "a" ); + const bool is_list_item = ( base_tag == "li" ); if ( is_header ) { output += "\n\n\033[1;31m"; } else if ( is_link ) { - output += "\033[4;34m"; // 【核心修复】:必须是 output += ,绝对不能用 std::cout + output += "\033[4;34m"; + } else if ( is_list_item ) { + output += "\n • "; } else if ( base_tag == "p" || base_tag == "div" || is_header ) { if ( !output.empty() && output.back() != '\n' ) { output += "\n"; @@ -112,6 +115,17 @@ void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion) if ( base_tag == "h1" || base_tag == "p" || base_tag == "div" ) { output += "\n"; } + + if ( base_tag == "img" ) { + std::size_t alt_pos = node.tag_name.find( "alt=\"" ); + if ( alt_pos != std::string::npos ) { + alt_pos += 5; + const std::size_t alt_end = node.tag_name.find( '"', alt_pos ); + output += " \033[90m[Image: " + node.tag_name.substr( alt_pos, alt_end - alt_pos ) + "]\033[0m "; + } else { + output += " \033[90m[IMAGE]\033[0m "; + } + } } } // namespace diff --git a/ricochet/src/tui/terminal.cc b/ricochet/src/tui/terminal.cc index 9e78a02..44c0f3b 100644 --- a/ricochet/src/tui/terminal.cc +++ b/ricochet/src/tui/terminal.cc @@ -44,6 +44,7 @@ Terminal::Terminal() Terminal::~Terminal() { + show_cursor( true ); // RAII: restore terminal attributes when program exits tcsetattr( STDIN_FILENO, TCSAFLUSH, &original_termios_ ); } -- cgit v1.2.3 From 2141017d7c688a0f6659c3ffe5730f281f7551ae Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Mon, 13 Apr 2026 04:34:50 +0800 Subject: feat: implement configurable homepage and user-agent via config.txt --- ricochet/include/ricochet/core/browser.hh | 1 + ricochet/src/core/browser.cc | 15 ++++++++ ricochet/src/main.cc | 28 +++++++------- ricochet/src/net/http_client.cc | 61 ++++++++++++++++++++++++------- 4 files changed, 77 insertions(+), 28 deletions(-) (limited to 'ricochet/src/main.cc') diff --git a/ricochet/include/ricochet/core/browser.hh b/ricochet/include/ricochet/core/browser.hh index a8d2cd3..36e876d 100644 --- a/ricochet/include/ricochet/core/browser.hh +++ b/ricochet/include/ricochet/core/browser.hh @@ -33,6 +33,7 @@ public: Browser( Browser&& ) = delete; Browser& operator=( Browser&& ) = delete; + static std::string get_configured_homepage(); static int run( std::string_view initial_url ); }; diff --git a/ricochet/src/core/browser.cc b/ricochet/src/core/browser.cc index dd96c7c..571783a 100644 --- a/ricochet/src/core/browser.cc +++ b/ricochet/src/core/browser.cc @@ -1003,6 +1003,21 @@ std::vector wrap_lines( const std::vector& original_li } // namespace +std::string Browser::get_configured_homepage() +{ + std::ifstream file( get_config_path( "config.txt" ) ); + if ( file.is_open() ) { + std::string line; + while ( std::getline( file, line ) ) { + const std::size_t eq = line.find( '=' ); + if ( eq != std::string::npos && line.substr( 0, eq ) == "homepage" ) { + return line.substr( eq + 1 ); + } + } + } + return "https://wikipedia.org"; +} + int Browser::run( std::string_view initial_url ) { std::vector history; diff --git a/ricochet/src/main.cc b/ricochet/src/main.cc index 1037833..562861a 100644 --- a/ricochet/src/main.cc +++ b/ricochet/src/main.cc @@ -16,6 +16,7 @@ * along with this program. If not, see . */ +#include #include #include #include @@ -41,28 +42,27 @@ int main( int argc, char* argv[] ) ricochet::core::print_license_notice(); + std::string start_url; if ( args.size() < 2 ) { - std::cerr << "Error: No URL provided.\n"; - print_usage( args[0] ); - return 1; + start_url = ricochet::core::Browser::get_configured_homepage(); + } else { + start_url = std::string( args[1] ); } - const std::string_view input = args[1]; - - if ( input == "show" ) { - if ( args[2] == "w" ) { + if ( args.size() >= 2 && args[1] == "show" ) { + if ( args.size() >= 3 && args[2] == "w" ) { ricochet::core::show_warranty(); - } else if ( args[2] == "c" ) { + return 0; + } + if ( args.size() >= 3 && args[2] == "c" ) { ricochet::core::show_copying(); - } else { - std::cerr << "Error: Invalid argument for show command.\n"; - print_usage( args[0] ); - return 1; + return 0; } - return 0; + std::cerr << "Error: Invalid argument for show command.\n"; + print_usage( args[0] ); + return 1; } - std::string start_url( input ); if ( !start_url.starts_with( "http://" ) && !start_url.starts_with( "https://" ) ) { start_url.insert( 0, "https://" ); } diff --git a/ricochet/src/net/http_client.cc b/ricochet/src/net/http_client.cc index 2ed33e3..5f5b449 100644 --- a/ricochet/src/net/http_client.cc +++ b/ricochet/src/net/http_client.cc @@ -174,6 +174,40 @@ std::string get_config_path( const std::string& filename ) return dir + "/" + filename; } +struct BrowserConfig +{ + std::string user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/124.0.0.0 Safari/537.36"; + std::string homepage = "https://wikipedia.org"; +}; + +BrowserConfig& get_browser_config() +{ + static BrowserConfig config; + static bool loaded = false; + if ( !loaded ) { + std::ifstream file( get_config_path( "config.txt" ) ); + if ( file.is_open() ) { + std::string line; + while ( std::getline( file, line ) ) { + const std::size_t eq = line.find( '=' ); + if ( eq == std::string::npos ) { + continue; + } + const std::string key = line.substr( 0, eq ); + const std::string val = line.substr( eq + 1 ); + if ( key == "user_agent" ) { + config.user_agent = val; + } else if ( key == "homepage" ) { + config.homepage = val; + } + } + } + loaded = true; + } + return config; +} + using CookieJar = std::unordered_map>; void load_cookies_from_file( CookieJar& jar ) @@ -290,20 +324,19 @@ FetchResult do_network_request( const std::string& host, FetchResult res; try { const Address server_addr( host, service ); - std::string request - = std::format( "{} {} HTTP/1.1\r\n" - "Host: {}\r\n" - "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/124.0.0.0 Safari/537.36\r\n" - "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" - "Accept-Language: en-US,en;q=0.9\r\n" - "DNT: 1\r\n" - "{}" - "Connection: close\r\n", - method, - path, - host, - cookie_header ); + std::string request = std::format( "{} {} HTTP/1.1\r\n" + "Host: {}\r\n" + "User-Agent: {}\r\n" + "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" + "Accept-Language: en-US,en;q=0.9\r\n" + "DNT: 1\r\n" + "{}" + "Connection: close\r\n", + method, + path, + host, + get_browser_config().user_agent, + cookie_header ); if ( !body.empty() ) { request += std::format( "Content-Length: {}\r\n" "Content-Type: application/x-www-form-urlencoded\r\n", -- cgit v1.2.3 From e84e117735f7d922f84cd13f56f6eea93b204e5c Mon Sep 17 00:00:00 2001 From: Richard Qin Date: Mon, 13 Apr 2026 04:50:49 +0800 Subject: feat: move URL formatting to Browser class and update documentation --- ricochet/README.md | 83 ++++++++++++++++++++++++------- ricochet/include/ricochet/core/browser.hh | 1 + ricochet/src/core/browser.cc | 50 +++++++++---------- ricochet/src/main.cc | 17 +++---- 4 files changed, 99 insertions(+), 52 deletions(-) (limited to 'ricochet/src/main.cc') diff --git a/ricochet/README.md b/ricochet/README.md index fd300d2..a09bf4a 100644 --- a/ricochet/README.md +++ b/ricochet/README.md @@ -9,6 +9,19 @@ Modern browsing has become a minefield of non-free scripts, tracking pixels, and * **Minimalist:** Focused entirely on text and structure. * **Distraction-Free:** No ads, no popups, no auto-playing media. Just the data you came for. +## Instructions +- Keyboard-Driven Navigation: Fully controllable via Vim-style keybindings (j/k for scrolling, H/L for history). + +- Link Hinting: Press f to highlight all links with two-letter codes. Jump instantly without using a mouse or typing long numbers. + +- Privacy First: Industrial-grade Cookie Domain Isolation ensures your session data is never leaked across sites. + +- Fast Search: Built-in "Find in Page" (n/N) with real-time ANSI highlighting. + +- Persistent Data: Bookmarks and Browsing History are automatically saved to ~/.ricochet/ for a seamless experience. + +- Highly Configurable: Customize your User-Agent and default Homepage via a simple config file. + ## Key Features - [x] **High-Performance C++ Core:** Native speed with minimal memory footprint. - [x] **Robust HTML5 Parser:** A custom, fault-tolerant DOM builder for the "wild" web. @@ -23,6 +36,40 @@ Ricochet is built with a strictly modular approach: * **`Layout`**: Calculates box models and word-wrapping for terminal constraints. * **`TUI`**: An event-driven terminal UI handling viewport rendering and user input. +## Usage +```bash +# Start with default homepage +./build/bin/ricochet + +# Open a specific URL +./build/bin/ricochet https://www.gnu.org + +# Use DuckDuckGo smart search +./build/bin/ricochet "linux kernel news" +``` + +## Navigation +| Key | Action | +| :--- | :--- | +| `j / k` | Scroll Down / Up | +| `f` | Follow Link (Hint Mode) | +| `n / N` | Find in Page / Next Match | +| `H / L` | History Back / Forward | +| `h` | View Browsing History | +| `b / B` | Add Bookmark / View Bookmarks | +| `/` | Open URL Prompt | +| `i` | Form Input Mode | +| `?` | Show Help Manual | +| `q` | Quit | + +## Configuration +Ricochet stores data in ~/.ricochet/. You can create config.txt to customize your experience: + +```plaintext +homepage=https://v2ex.com +user_agent=MyCustomBrowser/1.0 +``` + ## Getting Started ### Prerequisites @@ -33,26 +80,35 @@ To build Ricochet, ensure your environment meets the following requirements: * `clang-format` for automated code styling. * `clang-tidy` for static analysis and linting. -### Build Instructions +### Building from Source Ricochet enforces out-of-source builds to keep the project structure clean. By default, the project builds in Debug mode with Address and Undefined Behavior sanitizers enabled. +#### 1. Clone the repository ```bash -# 1. Clone the repository git clone https://github.com/yourusername/ricochet.git cd ricochet +``` -# 2. Configure the project -# This will generate compile commands and set the build type to Release +#### 2. Configure the project +This will generate compile commands and set the build type to Release +```bash cmake -B build -DCMAKE_BUILD_TYPE=Release +``` -# 3. Compile the project using all available cores +#### 3. Compile the project using all available cores +```bash cmake --build build -j$(nproc) +``` -# 4. Launch Ricochet -# The executable is located in the bin/ directory -./build/bin/ricochet https://wikipedia.org +### Install +To install Ricochet to your system (defaulting to /usr/local/bin), run the following command with root privileges: + +```bash +sudo cmake --install build ``` +This will install the `ricochet` executable to the `bin` destination and required libraries to `lib`. + ### Build Parameters & Configuration You can customize the build by passing `-D=` to the initial `cmake -B build` command. @@ -94,15 +150,8 @@ To install the compiled binaries to your system, use: cmake --install build --prefix /usr/local ``` This installs the `ricochet` binary to `bin` and supporting libraries to `lib`. -## Navigation -| Key | Action | -| :--- | :--- | -| `j / k` | Scroll Down / Up | -| `Tab` | Cycle through links | -| `Enter` | Follow selected link | -| `b` | Go Back in history | -| `u` | Enter new URL | -| `q` | Quit Ricochet | + + ## Acknowledgments The low-level POSIX socket wrappers in `src/net/sys/` are adapted from the Minnow networking framework developed for Stanford University's CS144 course. These utilities are used gracefully to handle file descriptors and network interfaces. diff --git a/ricochet/include/ricochet/core/browser.hh b/ricochet/include/ricochet/core/browser.hh index 36e876d..ab7a473 100644 --- a/ricochet/include/ricochet/core/browser.hh +++ b/ricochet/include/ricochet/core/browser.hh @@ -33,6 +33,7 @@ public: Browser( Browser&& ) = delete; Browser& operator=( Browser&& ) = delete; + static std::string format_url( std::string input ); static std::string get_configured_homepage(); static int run( std::string_view initial_url ); }; diff --git a/ricochet/src/core/browser.cc b/ricochet/src/core/browser.cc index 571783a..c4dbb08 100644 --- a/ricochet/src/core/browser.cc +++ b/ricochet/src/core/browser.cc @@ -412,30 +412,6 @@ void draw_view( const tui::Terminal& terminal, std::cout.flush(); } -std::string format_url( std::string input ) -{ - if ( input.empty() ) { - return input; - } - - if ( input.starts_with( "http://" ) || input.starts_with( "https://" ) ) { - return input; - } - - const bool is_domain = input.contains( '.' ) && !input.contains( ' ' ); - if ( is_domain ) { - input.insert( 0, "https://" ); - return input; - } - - for ( char& ch : input ) { - if ( ch == ' ' ) { - ch = '+'; - } - } - return "https://lite.duckduckgo.com/lite/?q=" + input; -} - std::string get_url_input( const tui::Terminal& terminal ) { auto [width, height] = terminal.get_size(); @@ -468,7 +444,7 @@ std::string get_url_input( const tui::Terminal& terminal ) } terminal.show_cursor( false ); - return format_url( std::move( input ) ); + return Browser::format_url( std::move( input ) ); } // std::string get_link_input( const tui::Terminal& terminal ) @@ -1003,6 +979,30 @@ std::vector wrap_lines( const std::vector& original_li } // namespace +std::string Browser::format_url( std::string input ) +{ + if ( input.empty() ) { + return input; + } + + if ( input.starts_with( "http://" ) || input.starts_with( "https://" ) ) { + return input; + } + + const bool is_domain = input.contains( '.' ) && !input.contains( ' ' ); + if ( is_domain ) { + input.insert( 0, "https://" ); + return input; + } + + for ( char& ch : input ) { + if ( ch == ' ' ) { + ch = '+'; + } + } + return "https://lite.duckduckgo.com/lite/?q=" + input; +} + std::string Browser::get_configured_homepage() { std::ifstream file( get_config_path( "config.txt" ) ); diff --git a/ricochet/src/main.cc b/ricochet/src/main.cc index 562861a..d27ab55 100644 --- a/ricochet/src/main.cc +++ b/ricochet/src/main.cc @@ -16,7 +16,6 @@ * along with this program. If not, see . */ -#include #include #include #include @@ -42,13 +41,6 @@ int main( int argc, char* argv[] ) ricochet::core::print_license_notice(); - std::string start_url; - if ( args.size() < 2 ) { - start_url = ricochet::core::Browser::get_configured_homepage(); - } else { - start_url = std::string( args[1] ); - } - if ( args.size() >= 2 && args[1] == "show" ) { if ( args.size() >= 3 && args[2] == "w" ) { ricochet::core::show_warranty(); @@ -63,10 +55,15 @@ int main( int argc, char* argv[] ) return 1; } - if ( !start_url.starts_with( "http://" ) && !start_url.starts_with( "https://" ) ) { - start_url.insert( 0, "https://" ); + std::string start_url; + if ( args.size() < 2 ) { + start_url = ricochet::core::Browser::get_configured_homepage(); + } else { + start_url = std::string( args[1] ); } + start_url = ricochet::core::Browser::format_url( std::move( start_url ) ); + try { return ricochet::core::Browser::run( start_url ); } catch ( const std::exception& e ) { -- cgit v1.2.3