From be6cc4ca4498245f2691a033610672dae6fbaf2c Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Thu, 1 Jan 2026 00:51:05 +0800 Subject: [PATCH] feat: Add full in-page search functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completed Phase 1 high priority task - comprehensive search system: Search Features: - Press '/' to enter search mode with dedicated search input - Case-insensitive search across all content - Enter to execute search and find all matches - Real-time match highlighting: * Yellow background = current match * Blue background = other matches - Navigate results with 'n' (next) and 'N' (previous) - Smart scrolling - auto-scroll to current match - Match counter in status bar (e.g., "Match 3/10") - Esc to cancel search input - Search state clears when loading new pages Implementation Details: - Added search state to MainWindow::Impl * search_mode, search_query, search_matches, current_match - Search input component (similar to address bar) - executeSearch() - finds all matching lines - nextMatch()/previousMatch() - cycle through results - Content renderer highlights matches dynamically - Status panel shows search results with emoji indicator User Experience: - Intuitive vim-style '/' to search - Visual feedback with color highlighting - Match position indicator in status - Non-intrusive - doesn't interfere with navigation - Seamless integration with existing UI Keyboard shortcuts: - /: Start search - Enter: Execute search - n: Next match - N: Previous match - Esc: Cancel search Documentation: - Updated KEYBOARD.md with search section and usage example - Updated STATUS.md to reflect completion - Added search to interactive features list The browser now has powerful in-page search! ๐Ÿ”โœ… --- KEYBOARD.md | 22 +++++- STATUS.md | 20 ++--- src/ui/main_window.cpp | 162 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 180 insertions(+), 24 deletions(-) diff --git a/KEYBOARD.md b/KEYBOARD.md index 13c247d..24817b2 100644 --- a/KEYBOARD.md +++ b/KEYBOARD.md @@ -60,6 +60,18 @@ # 4. Press 'Esc' to cancel ``` +### In-Page Search +```bash +# Search for text on current page +# 1. Press '/' to open search bar +# 2. Type your search query (case-insensitive) +# 3. Press 'Enter' to find all matches +# 4. Press 'n' to go to next match +# 5. Press 'N' to go to previous match +# 6. Matches are highlighted (yellow = current, blue = other matches) +# 7. Status bar shows "Match X/Y" count +``` + ## ๐ŸŽจ UI Elements ### Top Bar @@ -93,16 +105,22 @@ 4. **Efficient Browsing**: Use `g` to jump to top, `G` to jump to bottom 5. **Address Bar**: Type `o` quickly to enter a new URL +### Search +| Key | Action | +|-----|--------| +| `/` | Start search (type query and press Enter) | +| `n` | Next search result | +| `N` | Previous search result | +| `Esc` | Cancel search | + ## ๐Ÿ› Known Limitations - Ctrl+L not yet working for address bar (use 'o' instead) -- No search functionality yet (/ key) - No bookmarks yet (Ctrl+D) - No history panel yet (F3) ## ๐Ÿš€ Coming Soon -- [ ] In-page search (`/` to search, `n`/`N` to navigate results) - [ ] Bookmarks (add, remove, list) - [ ] History (view and navigate) - [ ] Better link highlighting diff --git a/STATUS.md b/STATUS.md index 00a9c31..0718377 100644 --- a/STATUS.md +++ b/STATUS.md @@ -35,8 +35,9 @@ - **Link Navigation** - Tab, number keys (1-9), Enter to follow - **Address Bar** - 'o' to open, type URL, Enter to navigate - **Browser Controls** - Backspace to go back, 'f' to go forward, r/F5 to refresh - - **Real-time Status** - Load stats, scroll position, selected link - - **Visual Feedback** - Navigation button states, link highlighting + - **In-Page Search** - '/' to search, n/N to navigate results, highlighted matches + - **Real-time Status** - Load stats, scroll position, selected link, search results + - **Visual Feedback** - Navigation button states, link highlighting, search highlighting ### Build & Deployment - โœ… Binary size: **827KB** (well under 1MB target!) @@ -58,11 +59,6 @@ - No visual history panel (F3) - No persistence across sessions -- โš ๏ธ **Search** - Not implemented - - / search command not working - - n/N navigation not working - - No highlight of matches - ### Feature Gaps - โš ๏ธ No form support (input fields, buttons, etc.) - โš ๏ธ No image rendering (even ASCII art) @@ -72,18 +68,14 @@ ## ๐ŸŽฏ Next Steps Priority ### Phase 1: Enhanced UX (High Priority) -4. **Implement Search** (src/ui/content_view.cpp) - - Add / to start search - - Highlight matches - - n/N to navigate results -5. **Add Bookmark System** (new files) +1. **Add Bookmark System** (new files) - Implement bookmark storage (JSON file) - Create bookmark panel UI - Add Ctrl+D to bookmark - F2 to view bookmarks -6. **Add History** (new files) +2. **Add History** (new files) - Implement history storage (JSON file) - Create history panel UI - F3 to view history @@ -122,6 +114,8 @@ Interactive test: โœ… Enter to follow link - WORKS โœ… Backspace to go back - WORKS โœ… 'f' to go forward - WORKS +โœ… '/' to search - WORKS +โœ… 'n'/'N' to navigate search results - WORKS โœ… 'r' to refresh - WORKS โœ… 'o' to open address bar - WORKS ``` diff --git a/src/ui/main_window.cpp b/src/ui/main_window.cpp index 94ca3db..c106a24 100644 --- a/src/ui/main_window.cpp +++ b/src/ui/main_window.cpp @@ -12,6 +12,7 @@ #include #include +#include namespace tut { @@ -40,6 +41,12 @@ public: // Split content into lines for scrolling std::vector content_lines_; + // Search state + bool search_mode_{false}; + std::string search_query_; + std::vector search_matches_; // Line indices with matches + int current_match_{-1}; // Index into search_matches_ + void setContent(const std::string& content) { content_lines_.clear(); std::istringstream iss(content); @@ -48,6 +55,12 @@ public: content_lines_.push_back(line); } scroll_offset_ = 0; + + // Clear search state when new content is loaded + search_mode_ = false; + search_query_.clear(); + search_matches_.clear(); + current_match_ = -1; } void scrollDown(int lines = 1) { @@ -79,6 +92,54 @@ public: selected_link_ = static_cast(links_.size()) - 1; } } + + void executeSearch() { + search_matches_.clear(); + current_match_ = -1; + + if (search_query_.empty()) { + return; + } + + // Convert search query to lowercase for case-insensitive search + std::string query_lower = search_query_; + std::transform(query_lower.begin(), query_lower.end(), query_lower.begin(), ::tolower); + + // Find all lines containing the search query + for (size_t i = 0; i < content_lines_.size(); ++i) { + std::string line_lower = content_lines_[i]; + std::transform(line_lower.begin(), line_lower.end(), line_lower.begin(), ::tolower); + + if (line_lower.find(query_lower) != std::string::npos) { + search_matches_.push_back(static_cast(i)); + } + } + + if (!search_matches_.empty()) { + current_match_ = 0; + // Scroll to first match + scroll_offset_ = search_matches_[0]; + } + } + + void nextMatch() { + if (search_matches_.empty()) return; + + current_match_ = (current_match_ + 1) % static_cast(search_matches_.size()); + // Scroll to the match + scroll_offset_ = search_matches_[current_match_]; + } + + void previousMatch() { + if (search_matches_.empty()) return; + + current_match_--; + if (current_match_ < 0) { + current_match_ = static_cast(search_matches_.size()) - 1; + } + // Scroll to the match + scroll_offset_ = search_matches_[current_match_]; + } }; MainWindow::MainWindow() : impl_(std::make_unique()) {} @@ -99,6 +160,11 @@ int MainWindow::run() { auto address_input = Input(&address_content, "Enter URL..."); bool address_focused = false; + // ๆœ็ดข่พ“ๅ…ฅ + std::string search_content; + auto search_input = Input(&search_content, "Search..."); + bool search_focused = false; + // ๅ†…ๅฎนๆธฒๆŸ“ๅ™จ auto content_renderer = Renderer([this] { Elements lines; @@ -115,7 +181,28 @@ int MainWindow::run() { static_cast(impl_->content_lines_.size())); for (int i = start; i < end; i++) { - lines.push_back(text(impl_->content_lines_[i])); + // Check if this line is a search match + bool is_match = false; + bool is_current_match = false; + + if (!impl_->search_matches_.empty()) { + auto it = std::find(impl_->search_matches_.begin(), + impl_->search_matches_.end(), i); + if (it != impl_->search_matches_.end()) { + is_match = true; + int match_index = std::distance(impl_->search_matches_.begin(), it); + is_current_match = (match_index == impl_->current_match_); + } + } + + // Render line with appropriate highlighting + auto line_element = text(impl_->content_lines_[i]); + if (is_current_match) { + line_element = line_element | bgcolor(Color::Yellow) | color(Color::Black); + } else if (is_match) { + line_element = line_element | bgcolor(Color::Blue) | color(Color::White); + } + lines.push_back(line_element); } // Scroll indicator @@ -135,7 +222,16 @@ int MainWindow::run() { auto status_panel = Renderer([this] { Elements status_items; - if (impl_->loading_) { + // Search status takes priority + if (!impl_->search_matches_.empty()) { + std::string search_info = "๐Ÿ” Match " + + std::to_string(impl_->current_match_ + 1) + "/" + + std::to_string(impl_->search_matches_.size()) + + " for \"" + impl_->search_query_ + "\""; + status_items.push_back(text(search_info) | color(Color::Yellow)); + } else if (impl_->search_mode_ && !impl_->search_query_.empty()) { + status_items.push_back(text("๐Ÿ” No matches for \"" + impl_->search_query_ + "\"") | dim); + } else if (impl_->loading_) { status_items.push_back(text("โณ Loading...") | dim); } else if (impl_->load_time_ > 0) { std::string stats = "โฌ‡ " + std::to_string(impl_->load_bytes_ / 1024) + " KB " + @@ -146,7 +242,8 @@ int MainWindow::run() { status_items.push_back(text("Ready") | dim); } - if (impl_->selected_link_ >= 0 && impl_->selected_link_ < static_cast(impl_->links_.size())) { + if (impl_->selected_link_ >= 0 && impl_->selected_link_ < static_cast(impl_->links_.size()) && + impl_->search_matches_.empty()) { status_items.push_back(separator()); std::string link_info = "[" + std::to_string(impl_->selected_link_ + 1) + "] " + impl_->links_[impl_->selected_link_].url; @@ -158,9 +255,18 @@ int MainWindow::run() { // ไธปๅธƒๅฑ€ auto main_renderer = Renderer([&] { - return vbox({ - // ้กถ้ƒจๆ  - hbox({ + Elements top_bar_elements; + + if (search_focused) { + // Show search bar when in search mode + top_bar_elements.push_back(hbox({ + text("๐Ÿ” "), + search_input->Render() | flex | border | focus, + text(" [Esc to cancel]") | dim, + })); + } else { + // Normal address bar + top_bar_elements.push_back(hbox({ text(impl_->can_go_back_ ? "[โ—€]" : "[โ—€]") | (impl_->can_go_back_ ? bold : dim), text(" "), text(impl_->can_go_forward_ ? "[โ–ถ]" : "[โ–ถ]") | (impl_->can_go_forward_ ? bold : dim), @@ -172,7 +278,12 @@ int MainWindow::run() { text("[โš™]") | bold, text(" "), text("[?]") | bold, - }), + })); + } + + return vbox({ + // ้กถ้ƒจๆ  + vbox(top_bar_elements), separator(), // ๅ†…ๅฎนๅŒบ content_renderer->Render() | flex, @@ -235,11 +346,44 @@ int MainWindow::run() { return true; } - // Don't handle other keys if address bar is focused - if (address_focused) { + // Search mode activation + if (event == Event::Character('/') && !address_focused && !search_focused) { + search_focused = true; + search_content.clear(); + return true; + } + + // Execute search + if (event == Event::Return && search_focused) { + impl_->search_mode_ = true; + impl_->search_query_ = search_content; + impl_->executeSearch(); + search_focused = false; + return true; + } + + // Exit search mode + if (event == Event::Escape && search_focused) { + search_focused = false; + search_content.clear(); + return true; + } + + // Don't handle other keys if address bar or search is focused + if (address_focused || search_focused) { return false; } + // Navigate search results (only when not in input mode) + if (event == Event::Character('n') && !impl_->search_matches_.empty()) { + impl_->nextMatch(); + return true; + } + if (event == Event::Character('N') && !impl_->search_matches_.empty()) { + impl_->previousMatch(); + return true; + } + // Scrolling if (event == Event::Character('j') || event == Event::ArrowDown) { impl_->scrollDown(1);