aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-04-13 03:52:07 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-04-13 03:52:07 +0800
commit59fcb7e4db666084be4f75ff4746db19b1eeeeb2 (patch)
tree7d78586222c6b460300b559fc7995e0c4d3a724b
parent543ab7cf4e4af26962dbbe1c5cfe6a5fb8da214f (diff)
downloadrwebbrowser-59fcb7e4db666084be4f75ff4746db19b1eeeeb2.tar.gz
rwebbrowser-59fcb7e4db666084be4f75ff4746db19b1eeeeb2.tar.xz
feat: replace numeric link selection with two-character link hints
-rw-r--r--src/core/browser.cc160
-rw-r--r--src/parser/html_lexer.cc2
-rw-r--r--src/render/renderer.cc2
3 files changed, 111 insertions, 53 deletions
diff --git a/src/core/browser.cc b/src/core/browser.cc
index f73f5b1..779bdb2 100644
--- a/src/core/browser.cc
+++ b/src/core/browser.cc
@@ -243,7 +243,7 @@ PageData load_page( const HttpRequest& req )
std::string build_footer( std::string_view url, std::size_t width )
{
- const std::string_view keys = " | [j/k]Scroll [n/N]Find [/]Nav [f]Link [i]In [r]Ref [H/L]Hist [b/B]Mark [q]Quit ";
+ const std::string_view keys = " | [j/k]Scroll [n/N]Find [/]Nav [f]Hint [i]In [r]Ref [H/L]Hist [b/B]Mark [q]Quit ";
const std::string_view prefix = " URL: ";
std::string footer;
@@ -378,39 +378,39 @@ std::string get_url_input( const tui::Terminal& terminal )
return format_url( std::move( input ) );
}
-std::string get_link_input( const tui::Terminal& terminal )
-{
- auto [width, height] = terminal.get_size();
- terminal.move_cursor( height, 1 );
- std::cout << "\x1b[2K\033[7m Follow Link [#]: \033[0m ";
- std::cout.flush();
-
- std::string num_input;
- terminal.show_cursor( true );
- while ( true ) {
- const char ch = terminal.read_key();
- if ( ch == '\r' || ch == '\n' ) {
- break;
- }
- if ( ch == 27 ) {
- num_input.clear();
- break;
- }
- if ( ch == 127 || ch == 8 ) {
- if ( !num_input.empty() ) {
- num_input.pop_back();
- std::cout << "\b \b";
- std::cout.flush();
- }
- } else if ( std::isdigit( static_cast<unsigned char>( ch ) ) ) {
- num_input += ch;
- std::cout << ch;
- std::cout.flush();
- }
- }
- terminal.show_cursor( false );
- return num_input;
-}
+// std::string get_link_input( const tui::Terminal& terminal )
+// {
+// auto [width, height] = terminal.get_size();
+// terminal.move_cursor( height, 1 );
+// std::cout << "\x1b[2K\033[7m Follow Link [#]: \033[0m ";
+// std::cout.flush();
+
+// std::string num_input;
+// terminal.show_cursor( true );
+// while ( true ) {
+// const char ch = terminal.read_key();
+// if ( ch == '\r' || ch == '\n' ) {
+// break;
+// }
+// if ( ch == 27 ) {
+// num_input.clear();
+// break;
+// }
+// if ( ch == 127 || ch == 8 ) {
+// if ( !num_input.empty() ) {
+// num_input.pop_back();
+// std::cout << "\b \b";
+// std::cout.flush();
+// }
+// } else if ( std::isdigit( static_cast<unsigned char>( ch ) ) ) {
+// num_input += ch;
+// std::cout << ch;
+// std::cout.flush();
+// }
+// }
+// terminal.show_cursor( false );
+// return num_input;
+// }
std::string normalize_url( const std::string& target, const std::string& current_url )
{
@@ -458,28 +458,86 @@ std::string strip_ddg_tracker( std::string target )
return target;
}
+std::string index_to_hint( std::size_t index )
+{
+ const std::string alphabet = "asdfghjklqwertyuiopzxcvbnm";
+ std::string hint;
+ hint += alphabet[( index / alphabet.length() ) % alphabet.length()];
+ hint += alphabet[index % alphabet.length()];
+ return hint;
+}
+
+void toggle_link_hints( PageData& page_data, bool show )
+{
+ const std::string hl_start = "\033[0;1;35;7m ";
+ const std::string hl_end = " \033[0m";
+
+ std::vector<std::string> formatted_hints;
+ if ( show ) {
+ formatted_hints.reserve( page_data.links.size() );
+ for ( std::size_t i = 0; i < page_data.links.size(); ++i ) {
+ std::string h = hl_start;
+ h += index_to_hint( i );
+ h += hl_end;
+ formatted_hints.push_back( std::move( h ) );
+ }
+ }
+
+ for ( auto& line : page_data.lines ) {
+ for ( std::size_t i = 0; i < page_data.links.size(); ++i ) {
+ const std::string marker = "{L:" + std::to_string( i + 1 ) + "}";
+ const std::size_t pos = line.find( marker );
+ if ( pos != std::string::npos ) {
+ if ( show ) {
+ line.replace( pos, marker.length(), formatted_hints[i] );
+ } else {
+ const std::size_t end_pos = line.find( hl_end, pos );
+ if ( end_pos != std::string::npos ) {
+ line.replace( pos, end_pos + hl_end.length() - pos, marker );
+ }
+ }
+ }
+ }
+ }
+}
+
void handle_follow_link( const tui::Terminal& terminal,
- const std::vector<std::string>& links,
+ PageData& page_data,
std::string& current_url,
+ std::size_t& scroll_y, // NOLINT(bugprone-easily-swappable-parameters)
bool& navigate )
{
- const std::string num_input = get_link_input( terminal );
- if ( !num_input.empty() ) {
- try {
- const std::size_t index = std::stoul( num_input );
- if ( index > 0 && index <= links.size() ) {
- std::string target = links[index - 1];
+ toggle_link_hints( page_data, true );
- target = normalize_url( target, current_url );
- target = strip_ddg_tracker( target );
+ draw_view( terminal, page_data.lines, scroll_y, current_url, page_data.title );
- current_url = target;
- navigate = true;
- }
- } catch ( const std::exception& e ) {
- (void)e; // NOLINT
+ auto [w, h] = terminal.get_size();
+ terminal.move_cursor( h, 1 );
+ std::cout << "\x1b[2K\033[1;35;7m Follow Hint (2 keys): \033[0m ";
+ std::cout.flush();
+
+ std::string input;
+ input += terminal.read_key();
+ std::cout << input[0];
+ std::cout.flush();
+ input += terminal.read_key();
+
+ bool found = false;
+ for ( std::size_t i = 0; i < page_data.links.size(); ++i ) {
+ if ( index_to_hint( i ) == input ) {
+ std::string target = page_data.links[i];
+ target = normalize_url( target, current_url );
+ target = strip_ddg_tracker( target );
+ current_url = target;
+ navigate = true;
+ found = true;
+ break;
}
}
+
+ if ( !found ) {
+ toggle_link_hints( page_data, false );
+ }
}
enum class InputAction : std::uint8_t
@@ -730,7 +788,7 @@ InputAction handle_form_input( const tui::Terminal& terminal,
InputAction process_key( char c,
const tui::Terminal& terminal,
PageData& page_data,
- std::string current_url,
+ std::string& current_url,
std::vector<HttpRequest>& history,
std::size_t& history_idx, // NOLINT(bugprone-easily-swappable-parameters)
std::size_t& scroll_y ) // NOLINT(bugprone-easily-swappable-parameters)
@@ -792,7 +850,7 @@ InputAction process_key( char c,
if ( c == 'f' ) {
const std::string old_url = current_url;
bool nav = false;
- handle_follow_link( terminal, page_data.links, current_url, nav );
+ handle_follow_link( terminal, page_data, current_url, scroll_y, nav );
if ( nav && current_url != old_url ) {
history.resize( history_idx + 1 );
history.push_back( HttpRequest { .url = current_url, .method = "GET", .body = "" } );
@@ -852,7 +910,7 @@ int Browser::run( std::string_view initial_url )
while ( true ) {
const HttpRequest current_req = history[history_idx];
- const std::string current_url = current_req.url;
+ std::string current_url = current_req.url;
terminal.clear_screen();
std::cout << "-> Fetching: " << current_req.url << "...\r\n";
std::cout.flush();
diff --git a/src/parser/html_lexer.cc b/src/parser/html_lexer.cc
index 2aa05b1..4d70288 100644
--- a/src/parser/html_lexer.cc
+++ b/src/parser/html_lexer.cc
@@ -36,7 +36,7 @@ enum class LexerState : std::uint8_t
void emit_text( std::string& buffer, std::vector<HtmlToken>& tokens )
{
if ( buffer.empty() ) {
- return; // 提前返回,减少 if 嵌套层级
+ return;
}
bool all_whitespace = true;
diff --git a/src/render/renderer.cc b/src/render/renderer.cc
index fda21c3..53eca1b 100644
--- a/src/render/renderer.cc
+++ b/src/render/renderer.cc
@@ -109,7 +109,7 @@ void render_link_end( const parser::DomNode& node, RenderResult& result )
const std::string href = extract_attr( node.tag_name, "href" );
if ( !href.empty() && !href.starts_with( "javascript:" ) && !href.starts_with( "#" ) ) {
result.links.push_back( decode_entities( href ) );
- result.text += " \033[7m[" + std::to_string( result.links.size() ) + "]\033[0m";
+ result.text += "\033[8m{L:" + std::to_string( result.links.size() ) + "}\033[0m";
}
}