aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-04-11 06:08:38 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-04-11 06:08:38 +0800
commit72e4e43b1fc1743dc3e9dae70682f2945eeba45d (patch)
tree2ac8bc85fd4214f1fa07f64aada4809fb1fc3fd6
parent4ef9c771444943008bb990343127a7d659fd6df2 (diff)
downloadrwebbrowser-72e4e43b1fc1743dc3e9dae70682f2945eeba45d.tar.gz
rwebbrowser-72e4e43b1fc1743dc3e9dae70682f2945eeba45d.tar.xz
feat: implement URL navigation and input handling in browser TUI
-rw-r--r--include/ricochet/tui/terminal.hh6
-rw-r--r--src/core/browser.cc154
-rw-r--r--src/tui/terminal.cc12
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