Skip to main content

rpi_tui/
search_bar.rs

1//! Search bar component for transcript search.
2
3use std::any::Any;
4use std::sync::Mutex;
5
6use crate::component::Component;
7use crate::theme::theme;
8
9/// Search bar component that displays search query and match count.
10pub struct SearchBar {
11    query: Mutex<String>,
12    match_count: Mutex<usize>,
13    current_match: Mutex<usize>,
14    visible: Mutex<bool>,
15}
16
17impl SearchBar {
18    pub fn new() -> Self {
19        Self {
20            query: Mutex::new(String::new()),
21            match_count: Mutex::new(0),
22            current_match: Mutex::new(0),
23            visible: Mutex::new(false),
24        }
25    }
26
27    pub fn set_query(&self, query: &str) {
28        *self.query.lock().unwrap() = query.to_string();
29    }
30
31    pub fn set_match_info(&self, current: usize, total: usize) {
32        *self.current_match.lock().unwrap() = current;
33        *self.match_count.lock().unwrap() = total;
34    }
35
36    pub fn set_visible(&self, visible: bool) {
37        *self.visible.lock().unwrap() = visible;
38    }
39
40    pub fn is_visible(&self) -> bool {
41        *self.visible.lock().unwrap()
42    }
43}
44
45impl Default for SearchBar {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl Component for SearchBar {
52    fn render(&self, width: usize) -> Vec<String> {
53        if !self.is_visible() {
54            return vec![];
55        }
56
57        let query = self.query.lock().unwrap().clone();
58        let match_count = *self.match_count.lock().unwrap();
59        let current_match = *self.current_match.lock().unwrap();
60
61        let theme = theme();
62        let colors = &theme.colors;
63
64        // Build search bar text
65        let search_icon = "🔍";
66        let match_info = if match_count == 0 {
67            if query.is_empty() {
68                String::new()
69            } else {
70                format!("No matches")
71            }
72        } else {
73            format!("{}/{}", current_match + 1, match_count)
74        };
75
76        let left = format!("{} Search: {}", search_icon, query);
77        let right = match_info;
78
79        // Calculate padding
80        let left_len = left.chars().count();
81        let right_len = right.chars().count();
82        let padding = if width > left_len + right_len + 2 {
83            width - left_len - right_len - 2
84        } else {
85            1
86        };
87
88        let line = format!(
89            "{}{}{}{}",
90            colors.background.fg(&left),
91            " ".repeat(padding),
92            colors.dim.fg(&right),
93            " ".repeat(2)
94        );
95
96        vec![line]
97    }
98
99    fn invalidate(&self) {
100        // No cached state to invalidate
101    }
102
103    fn as_any(&self) -> &dyn Any {
104        self
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_search_bar_hidden() {
114        let bar = SearchBar::new();
115        assert_eq!(bar.render(80).len(), 0);
116    }
117
118    #[test]
119    fn test_search_bar_visible() {
120        let bar = SearchBar::new();
121        bar.set_visible(true);
122        bar.set_query("test");
123        bar.set_match_info(0, 5);
124        let lines = bar.render(80);
125        assert_eq!(lines.len(), 1);
126        assert!(lines[0].contains("test"));
127        assert!(lines[0].contains("1/5"));
128    }
129}