//! List view widget with fuzzy match highlighting. use std::collections::HashSet; use ratatui::{ prelude::*, widgets::{Block, Borders, List, ListItem, ListState}, }; use crate::daemon::tui::app::{App, ViewMode}; /// Style for matched characters in search results const MATCH_HIGHLIGHT_COLOR: Color = Color::Yellow; const MATCH_HIGHLIGHT_MODIFIER: Modifier = Modifier::BOLD; /// Build a Line with highlighted characters based on matched indices fn build_highlighted_name(name: &str, matched_indices: &[usize]) -> Vec> { if matched_indices.is_empty() { return vec![Span::raw(name.to_string())]; } let matched_set: HashSet = matched_indices.iter().cloned().collect(); let mut spans = Vec::new(); let mut current_run = String::new(); let mut is_highlighted = false; for (byte_idx, ch) in name.char_indices() { let should_highlight = matched_set.contains(&byte_idx); if should_highlight != is_highlighted { // Flush current run if !current_run.is_empty() { if is_highlighted { spans.push(Span::styled( current_run.clone(), Style::default() .fg(MATCH_HIGHLIGHT_COLOR) .add_modifier(MATCH_HIGHLIGHT_MODIFIER), )); } else { spans.push(Span::raw(current_run.clone())); } current_run.clear(); } is_highlighted = should_highlight; } current_run.push(ch); } // Flush remaining if !current_run.is_empty() { if is_highlighted { spans.push(Span::styled( current_run, Style::default() .fg(MATCH_HIGHLIGHT_COLOR) .add_modifier(MATCH_HIGHLIGHT_MODIFIER), )); } else { spans.push(Span::raw(current_run)); } } spans } /// Get status icon and color for an item fn get_status_display(status: Option<&str>) -> (&'static str, Color) { match status { Some("running") => ("▸", Color::Green), Some("done") => ("✓", Color::Blue), Some("failed") => ("✗", Color::Red), Some("pending") => ("○", Color::Yellow), Some("paused") => ("⏸", Color::Cyan), _ => (" ", Color::Gray), } } pub fn render(f: &mut Frame, area: Rect, app: &mut App) { let items: Vec = app .filtered_items .iter() .map(|filtered_item| { let item = &app.items[filtered_item.index]; let (status_icon, status_color) = get_status_display(item.status.as_deref()); // Build spans with highlighted matched characters let mut spans = vec![Span::styled( format!("{} ", status_icon), Style::default().fg(status_color), )]; // Add name with match highlighting spans.extend(build_highlighted_name(&item.name, &filtered_item.matched_indices)); ListItem::new(Line::from(spans)) }) .collect(); let view_label = match app.view_mode { ViewMode::Tasks => "Tasks", ViewMode::Contracts => "Contracts", ViewMode::Files => "Files", }; let title = format!( " {} ({}{}) ", view_label, app.filtered_items.len(), if app.filtered_items.len() != app.items.len() { format!("/{}", app.items.len()) } else { String::new() } ); let list = List::new(items) .block(Block::default().title(title).borders(Borders::ALL)) .highlight_style(Style::default().add_modifier(Modifier::REVERSED)) .highlight_symbol("> "); let mut state = ListState::default(); state.select(Some(app.selected_index)); f.render_stateful_widget(list, area, &mut state); }