summaryrefslogtreecommitdiff
path: root/makima/src/daemon/tui/widgets/search_input.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-19 13:47:32 +0000
committerGitHub <noreply@github.com>2026-01-19 13:47:32 +0000
commit0833fb1f30c0c3b920157deb882e0e902c3af02a (patch)
tree45110fb8cb9277dfbaccfeb53ed9c1f76975022b /makima/src/daemon/tui/widgets/search_input.rs
parent786510379bed060db2b3742b7dfca671552d2c34 (diff)
downloadsoryu-0833fb1f30c0c3b920157deb882e0e902c3af02a.tar.gz
soryu-0833fb1f30c0c3b920157deb882e0e902c3af02a.zip
Add interactive TUI browser for tasks, contracts, and files (makima view) (#7)
* feat(tui): Implement fuzzy search with real-time filtering and highlighting Adds comprehensive fuzzy search functionality to the TUI browser: ## Fuzzy Matching (fuzzy.rs) - FuzzyMatcher wrapper using SkimMatcherV2 from fuzzy-matcher crate - fuzzy_match() returns score and matched character indices - fuzzy_match_all() supports multi-term search (space-separated) - Recency-aware scoring to boost recent items in results - Unit tests for all matching scenarios ## App State (app.rs) - FilteredItem struct with index, score, and matched_indices - apply_filter() uses fuzzy matching with score-based sorting - match_count() and has_no_matches() helper methods - Results sorted by match score (highest first) ## List View (list_view.rs) - Highlighted matched characters in search results - Yellow bold styling for matched chars - Status icons with color coding ## Search Input (search_input.rs) - Real-time match count display (X/Y matches) - Visual feedback for no matches (red border) - Placeholder text when search is empty - Active search mode indication (yellow border) ## Event Handling (event.rs) - Arrow key navigation while in search mode - Ctrl+K/J for vim-style navigation during search - Delete key support alongside backspace - Ctrl+U to clear search query - Tab toggles preview while searching - Escape clears search and exits search mode Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Task completion checkpoint * [WIP] Heartbeat checkpoint - 2026-01-19 11:20:34 UTC * Task completion checkpoint * [WIP] Heartbeat checkpoint - 2026-01-19 11:31:19 UTC * Task completion checkpoint * [WIP] Heartbeat checkpoint - 2026-01-19 11:39:07 UTC * fix(tui): Fix module exports and main binary integration - Update mod.rs to properly export app, event, fuzzy, and ui modules - Add run() function for TUI entry point - Fix run_view() to use ViewCommand enum instead of ViewArgs - Fix event handling to use poll_event and handle_key_event Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'makima/src/daemon/tui/widgets/search_input.rs')
-rw-r--r--makima/src/daemon/tui/widgets/search_input.rs82
1 files changed, 82 insertions, 0 deletions
diff --git a/makima/src/daemon/tui/widgets/search_input.rs b/makima/src/daemon/tui/widgets/search_input.rs
new file mode 100644
index 0000000..311b4f0
--- /dev/null
+++ b/makima/src/daemon/tui/widgets/search_input.rs
@@ -0,0 +1,82 @@
+//! Search input widget with match count and visual feedback.
+
+use ratatui::{
+ prelude::*,
+ widgets::{Block, Borders, Paragraph},
+};
+
+use crate::daemon::tui::app::{App, InputMode, ViewMode};
+
+/// Color for the search bar when there are no matches
+const NO_MATCH_COLOR: Color = Color::Red;
+/// Color for the search bar when actively searching
+const SEARCH_ACTIVE_COLOR: Color = Color::Yellow;
+
+pub fn render(f: &mut Frame, area: Rect, app: &App) {
+ let view_label = match app.view_mode {
+ ViewMode::Tasks => "Tasks",
+ ViewMode::Contracts => "Contracts",
+ ViewMode::Files => "Files",
+ };
+
+ let (matched, total) = app.match_count();
+ let has_no_matches = app.has_no_matches();
+ let is_searching = matches!(app.input_mode, InputMode::Search);
+ let has_query = !app.search_query.is_empty();
+
+ // Determine border style based on state
+ let border_style = if has_no_matches {
+ Style::default().fg(NO_MATCH_COLOR)
+ } else if is_searching {
+ Style::default().fg(SEARCH_ACTIVE_COLOR)
+ } else {
+ Style::default()
+ };
+
+ // Build the search input content
+ let search_text = if app.search_query.is_empty() {
+ if is_searching {
+ " Type to search...".to_string()
+ } else {
+ " Press / to search".to_string()
+ }
+ } else {
+ format!(" {}", app.search_query)
+ };
+
+ // Build the title with match count
+ let title = if has_query {
+ if has_no_matches {
+ format!(" 🔍 Search [{}] - No matches ", view_label)
+ } else {
+ format!(" 🔍 Search [{}] - {}/{} matches ", view_label, matched, total)
+ }
+ } else {
+ format!(" 🔍 Search [{}] ", view_label)
+ };
+
+ // Create input text with appropriate style
+ let text_style = if app.search_query.is_empty() && !is_searching {
+ Style::default().fg(Color::DarkGray)
+ } else if has_no_matches {
+ Style::default().fg(NO_MATCH_COLOR)
+ } else {
+ Style::default()
+ };
+
+ let input = Paragraph::new(Span::styled(search_text, text_style)).block(
+ Block::default()
+ .title(title)
+ .borders(Borders::ALL)
+ .border_style(border_style),
+ );
+
+ f.render_widget(input, area);
+
+ // Show cursor in search mode
+ if is_searching {
+ // Calculate cursor position based on actual search query length
+ let cursor_x = area.x + app.search_query.len() as u16 + 2;
+ f.set_cursor_position(Position::new(cursor_x, area.y + 1));
+ }
+}