summaryrefslogtreecommitdiff
path: root/makima/src/daemon/tui/widgets/list_view.rs
blob: ff8269a73aa2a9931837a72893ada33a97e7d008 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//! 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);
}