diff options
| author | Richard Qin <richard.qin.x@gmail.com> | 2026-04-11 06:08:38 +0800 |
|---|---|---|
| committer | Richard Qin <richard.qin.x@gmail.com> | 2026-04-11 06:08:38 +0800 |
| commit | 72e4e43b1fc1743dc3e9dae70682f2945eeba45d (patch) | |
| tree | 2ac8bc85fd4214f1fa07f64aada4809fb1fc3fd6 | |
| parent | 4ef9c771444943008bb990343127a7d659fd6df2 (diff) | |
| download | rwebbrowser-72e4e43b1fc1743dc3e9dae70682f2945eeba45d.tar.gz rwebbrowser-72e4e43b1fc1743dc3e9dae70682f2945eeba45d.tar.xz | |
feat: implement URL navigation and input handling in browser TUI
| -rw-r--r-- | include/ricochet/tui/terminal.hh | 6 | ||||
| -rw-r--r-- | src/core/browser.cc | 154 | ||||
| -rw-r--r-- | src/tui/terminal.cc | 12 |
3 files changed, 122 insertions, 50 deletions
diff --git a/include/ricochet/tui/terminal.hh b/include/ricochet/tui/terminal.hh index 59ca298..2942a2a 100644 --- a/include/ricochet/tui/terminal.hh +++ b/include/ricochet/tui/terminal.hh @@ -43,6 +43,12 @@ public: // Get terminal size std::pair<std::size_t, std::size_t> get_size() const; + // Move cursor to a specific position + void move_cursor( std::size_t row, std::size_t col ) const; + + // Show or hide cursor + void show_cursor( bool visible ) const; + private: struct termios original_termios_ {}; // Save terminal original state }; diff --git a/src/core/browser.cc b/src/core/browser.cc index 650ebcf..4225986 100644 --- a/src/core/browser.cc +++ b/src/core/browser.cc @@ -24,79 +24,133 @@ #include "ricochet/tui/terminal.hh" #include <cstddef> #include <iostream> -#include <sstream> namespace ricochet::core { -int Browser::run( std::string_view initial_url ) +namespace { +std::vector<std::string> load_page( std::string_view url ) { - std::cout << "-> Fetching: " << initial_url << "...\n"; - const net::HttpClient client; - auto response_result = client.fetch( initial_url ); - + auto response_result = client.fetch( url ); if ( !response_result.has_value() ) { - std::cerr << "[!] Ricochet failed to load page: " << response_result.error() << "\n"; - return 1; + return { "[!] Failed to load: " + std::string( url ) }; } - const auto& response = response_result.value(); - const parser::HtmlLexer lexer; - const auto tokens = lexer.tokenize( response.body ); - const parser::TreeBuilder builder; - const auto dom_root = builder.build( tokens ); - - // std::cout << "\033[2J\033[1;1H"; - const render::Renderer renderer; - const std::string rendered_text = renderer.render( dom_root ); + + const auto dom_root = builder.build( lexer.tokenize( response_result.value().body ) ); + const std::string text = renderer.render( dom_root ); std::vector<std::string> lines; - std::stringstream ss( rendered_text ); - std::string line; - while ( std::getline( ss, line, '\n' ) ) { - lines.push_back( line ); + std::size_t start = 0; + while ( start < text.length() ) { + const std::size_t end = text.find( '\n', start ); + if ( end == std::string::npos ) { + lines.push_back( text.substr( start ) ); + break; + } + lines.push_back( text.substr( start, end - start ) ); + start = end + 1; } + return lines; +} - std::cout << "DEBUG: Status Code: " << response.status_code << "\r\n"; - std::cout << "DEBUG: Raw Body Length: " << response.body.length() << "\r\n"; - - // TUI Event Loop - const tui::Terminal terminal; +void draw_view( const tui::Terminal& terminal, + const std::vector<std::string>& lines, + std::size_t scroll_y, + std::string_view url ) +{ + terminal.clear_screen(); + auto [width, height] = terminal.get_size(); + const std::size_t usable_height = ( height > 1 ) ? ( height - 1 ) : 1; + + for ( std::size_t i = 0; i < usable_height; ++i ) { + if ( scroll_y + i < lines.size() ) { + std::cout << lines[scroll_y + i] << "\r\n"; + } else { + std::cout << "~\r\n"; + } + } + std::cout << "\033[7m URL: " << url << " | [j/k] Scroll [/] Jump [q] Quit \033[0m"; + std::cout.flush(); +} - std::size_t scroll_y = 0; +std::string get_url_input( const tui::Terminal& terminal ) +{ + auto [width, height] = terminal.get_size(); + terminal.move_cursor( height, 1 ); + std::cout << "\x1b[2K\033[7m URL: \033[0m "; + std::cout.flush(); + std::string input; + terminal.show_cursor( true ); while ( true ) { - terminal.clear_screen(); - - const auto [term_width, term_height] = terminal.get_size(); - const std::size_t usable_height = ( term_height > 1 ) ? ( term_height - 1 ) : 1; - - for ( std::size_t i = 0; i < usable_height; ++i ) { - const std::size_t line_idx = scroll_y + i; - if ( line_idx < lines.size() ) { - std::cout << lines[line_idx] << "\r\n"; - } else { - std::cout << "~\r\n"; + const char ch = terminal.read_key(); + if ( ch == '\r' || ch == '\n' ) { + break; + } + if ( ch == 27 ) { + input.clear(); + break; + } + if ( ch == 127 || ch == 8 ) { + if ( !input.empty() ) { + input.pop_back(); + std::cout << "\b \b"; + std::cout.flush(); } + } else if ( std::isprint( static_cast<unsigned char>( ch ) ) ) { + input += ch; + std::cout << ch; + std::cout.flush(); } + } + terminal.show_cursor( false ); - std::cout << "\033[7m" << " URL: " << initial_url << " | [j] Down [k] Up [q] Quit " << "\033[0m"; - std::cout.flush(); + if ( !input.empty() && !input.starts_with( "http://" ) && !input.starts_with( "https://" ) ) { + input.insert( 0, "https://" ); + } + return input; +} - const char c = terminal.read_key(); - if ( c == 'q' ) { - break; // Break loop, terminal destructor will restore terminal - } - if ( c == 'j' ) { - if ( scroll_y + usable_height < lines.size() ) { - scroll_y++; // Scroll down +} // namespace + +int Browser::run( std::string_view initial_url ) +{ + std::cout << "-> Fetching: " << initial_url << "...\n"; + + std::string current_url { initial_url }; + const tui::Terminal terminal; + terminal.clear_screen(); + + while ( !current_url.empty() ) { + auto lines = load_page( current_url ); + std::size_t scroll_y = 0; + bool navigate = false; + + while ( !navigate ) { + draw_view( terminal, 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' ) { + terminal.clear_screen(); + return 0; } - } else if ( c == 'k' ) { - if ( scroll_y > 0 ) { - scroll_y--; // Scroll up + 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; + } } } } diff --git a/src/tui/terminal.cc b/src/tui/terminal.cc index 75c6c1f..9e78a02 100644 --- a/src/tui/terminal.cc +++ b/src/tui/terminal.cc @@ -74,4 +74,16 @@ std::pair<std::size_t, std::size_t> Terminal::get_size() // NOLINT(readability-c return { static_cast<std::size_t>( w.ws_col ), static_cast<std::size_t>( w.ws_row ) }; } +void Terminal::move_cursor( std::size_t row, std::size_t col ) const // NOLINT +{ + // ANSI: \x1b[H move to 1,1; \x1b[row;colH move to specified row and column + std::cout << "\x1b[" << row << ";" << col << "H"; +} + +void Terminal::show_cursor( bool visible ) const // NOLINT +{ + // \x1b[?25h show cursor, \x1b[?25l hide cursor + std::cout << ( visible ? "\x1b[?25h" : "\x1b[?25l" ); +} + } // namespace ricochet::tui
\ No newline at end of file |
