//! 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<Span<'static>> {
if matched_indices.is_empty() {
return vec![Span::raw(name.to_string())];
}
let matched_set: HashSet<usize> = 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<ListItem> = 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);
}