aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRichard Qin <richard.qin.x@gmail.com>2026-04-12 20:35:05 +0800
committerRichard Qin <richard.qin.x@gmail.com>2026-04-12 20:35:05 +0800
commit32bce7c278e8f52d96579650088f610d62920f00 (patch)
tree72265c1703aab26373fbe7670585d02eb79cb9cd
parent5a8bb74e922f9ecebffbb03832b559f9dec21473 (diff)
downloadrwebbrowser-32bce7c278e8f52d96579650088f610d62920f00.tar.gz
rwebbrowser-32bce7c278e8f52d96579650088f610d62920f00.tar.xz
feat: add support for interacting with form inputs via the 'i' key
-rw-r--r--include/ricochet/render/renderer.hh1
-rw-r--r--src/core/browser.cc93
-rw-r--r--src/render/renderer.cc30
3 files changed, 114 insertions, 10 deletions
diff --git a/include/ricochet/render/renderer.hh b/include/ricochet/render/renderer.hh
index 2a9acae..8ea660c 100644
--- a/include/ricochet/render/renderer.hh
+++ b/include/ricochet/render/renderer.hh
@@ -26,6 +26,7 @@ struct RenderResult
{
std::string text {};
std::vector<std::string> links {};
+ std::vector<std::string> inputs {};
};
class Renderer
diff --git a/src/core/browser.cc b/src/core/browser.cc
index 4c42bd0..e3a739a 100644
--- a/src/core/browser.cc
+++ b/src/core/browser.cc
@@ -33,6 +33,7 @@ struct PageData
{
std::vector<std::string> lines;
std::vector<std::string> links;
+ std::vector<std::string> inputs;
std::string title;
};
@@ -83,7 +84,8 @@ PageData load_page( std::string_view url )
const net::HttpClient client;
auto response_result = client.fetch( url );
if ( !response_result.has_value() ) {
- return { .lines = { "[!] Failed to load: " + std::string( url ) }, .links = {}, .title = "Error" };
+ return {
+ .lines = { "[!] Failed to load: " + std::string( url ) }, .links = {}, .inputs = {}, .title = "Error" };
}
const parser::HtmlLexer lexer;
@@ -105,7 +107,7 @@ PageData load_page( std::string_view url )
start = end + 1;
}
const std::string title = extract_title( dom_root );
- return { .lines = lines, .links = result.links, .title = title };
+ return { .lines = lines, .links = result.links, .inputs = result.inputs, .title = title };
}
void draw_view( const tui::Terminal& terminal,
@@ -147,7 +149,8 @@ void draw_view( const tui::Terminal& terminal,
}
}
- std::string footer = " URL: " + std::string( url ) + " | [j/k]Scroll [/]Jump [f]Follow [H/L]History [q]Quit ";
+ std::string footer
+ = " URL: " + std::string( url ) + " | [j/k]Scroll [/]Jump [f]Follow [i]Input [H/L]History [q]Quit ";
if ( footer.size() < width ) {
footer.append( width - footer.size(), ' ' );
} else if ( footer.size() > width && width > 3 ) {
@@ -327,6 +330,86 @@ enum class InputAction : std::uint8_t
Navigate
};
+std::string get_terminal_input( const tui::Terminal& terminal, const std::string& prompt )
+{
+ auto [width, height] = terminal.get_size();
+ terminal.move_cursor( height, 1 );
+ std::cout << "\x1b[2K\033[7m " << prompt << " \033[0m ";
+ std::cout.flush();
+
+ std::string input;
+ terminal.show_cursor( true );
+ while ( true ) {
+ 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 );
+ return input;
+}
+
+InputAction handle_form_input( const tui::Terminal& terminal,
+ const PageData& page_data,
+ std::string& current_url,
+ std::vector<std::string>& history,
+ std::size_t& history_idx )
+{
+ const std::string num_input = get_terminal_input( terminal, "Select Input [#]:" );
+ if ( num_input.empty() ) {
+ return InputAction::None;
+ }
+
+ std::size_t index = 0;
+ try {
+ index = std::stoul( num_input );
+ } catch ( ... ) {
+ (void)0;
+ }
+ if ( index == 0 || index > page_data.inputs.size() ) {
+ return InputAction::None;
+ }
+
+ const std::string query = get_terminal_input( terminal, "Enter Text [I" + std::to_string( index ) + "]:" );
+ if ( query.empty() ) {
+ return InputAction::None;
+ }
+
+ std::string target_url;
+ std::string encoded_query = query;
+ for ( char& ch : encoded_query ) {
+ if ( ch == ' ' ) {
+ ch = '+';
+ }
+ }
+
+ if ( current_url.contains( "wikipedia.org" ) ) {
+ target_url = "https://en.wikipedia.org/w/index.php?search=" + encoded_query;
+ } else {
+ target_url = "https://lite.duckduckgo.com/lite/?q=" + encoded_query;
+ }
+
+ history.resize( history_idx + 1 );
+ history.push_back( target_url );
+ history_idx++;
+ return InputAction::Navigate;
+}
+
InputAction process_key( char c,
const tui::Terminal& terminal,
const PageData& page_data,
@@ -377,6 +460,10 @@ InputAction process_key( char c,
return InputAction::Navigate;
}
}
+
+ if ( c == 'i' && !page_data.inputs.empty() ) {
+ return handle_form_input( terminal, page_data, current_url, history, history_idx );
+ }
return InputAction::None;
}
diff --git a/src/render/renderer.cc b/src/render/renderer.cc
index ed622b4..5f65ea7 100644
--- a/src/render/renderer.cc
+++ b/src/render/renderer.cc
@@ -90,16 +90,28 @@ std::string extract_href( const std::string& tag_name )
return "";
}
+bool is_ignored_tag( std::string_view tag )
+{
+ return tag == "head" || tag == "style" || tag == "script" || tag == "meta" || tag == "title" || tag == "link";
+}
+
+bool is_heading_tag( std::string_view tag )
+{
+ return tag == "h1" || tag == "h2" || tag == "h3" || tag == "h4";
+}
+
void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion)
- std::string& output,
- std::vector<std::string>& links )
+ RenderResult& result ) // NOLINT(bugprone-easily-swappable-parameters)
{
+ std::string& output = result.text;
+ std::vector<std::string>& links = result.links;
+ std::vector<std::string>& inputs = result.inputs;
+
const std::size_t space_pos = node.tag_name.find( ' ' );
const std::string base_tag
= ( space_pos == std::string::npos ) ? node.tag_name : node.tag_name.substr( 0, space_pos );
- if ( base_tag == "head" || base_tag == "style" || base_tag == "script" || base_tag == "meta"
- || base_tag == "title" || base_tag == "link" ) {
+ if ( is_ignored_tag( base_tag ) ) {
return;
}
@@ -108,9 +120,10 @@ void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion)
return;
}
- const bool is_header = ( base_tag == "h1" || base_tag == "h2" || base_tag == "h3" || base_tag == "h4" );
+ const bool is_header = is_heading_tag( base_tag );
const bool is_link = ( base_tag == "a" );
const bool is_list_item = ( base_tag == "li" );
+ const bool is_input = ( base_tag == "input" || base_tag == "textarea" );
if ( is_header ) {
output += "\n\n\033[1;31m";
@@ -118,6 +131,9 @@ void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion)
output += "\033[4;34m";
} else if ( is_list_item ) {
output += "\n • ";
+ } else if ( is_input ) {
+ inputs.emplace_back( "input_field" );
+ output += " \033[7;33m[I" + std::to_string( inputs.size() ) + "]\033[0m ";
} else if ( base_tag == "p" || base_tag == "div" || is_header ) {
if ( !output.empty() && output.back() != '\n' ) {
output += "\n";
@@ -125,7 +141,7 @@ void render_node( const parser::DomNode& node, // NOLINT(misc-no-recursion)
}
for ( const auto& child : node.children ) {
- render_node( child, output, links );
+ render_node( child, result );
}
if ( is_header ) {
@@ -161,7 +177,7 @@ RenderResult Renderer::render( // NOLINT(readability-convert-member-functions-to
const parser::DomNode& node ) const
{
RenderResult result;
- render_node( node, result.text, result.links );
+ render_node( node, result );
return result;
}