sbom_tools/tui/viewmodel/
search.rs1use crate::tui::state::ListNavigation;
7
8#[derive(Debug, Clone)]
29pub struct SearchState<R> {
30 pub active: bool,
32 pub query: String,
34 pub results: Vec<R>,
36 pub selected: usize,
38}
39
40impl<R> Default for SearchState<R> {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl<R> SearchState<R> {
47 #[must_use]
49 pub const fn new() -> Self {
50 Self {
51 active: false,
52 query: String::new(),
53 results: Vec::new(),
54 selected: 0,
55 }
56 }
57
58 pub fn start(&mut self) {
60 self.active = true;
61 self.query.clear();
62 self.results.clear();
63 self.selected = 0;
64 }
65
66 pub const fn stop(&mut self) {
68 self.active = false;
69 }
70
71 pub fn clear(&mut self) {
73 self.query.clear();
74 self.results.clear();
75 self.selected = 0;
76 }
77
78 pub fn push_char(&mut self, c: char) {
80 self.query.push(c);
81 }
82
83 pub fn pop_char(&mut self) {
85 self.query.pop();
86 }
87
88 #[must_use]
90 pub fn has_valid_query(&self) -> bool {
91 self.query.len() >= 2
92 }
93
94 #[must_use]
96 pub fn query_lower(&self) -> String {
97 self.query.to_lowercase()
98 }
99
100 pub fn set_results(&mut self, results: Vec<R>) {
102 self.results = results;
103 self.selected = 0;
104 }
105
106 #[must_use]
108 pub fn selected_result(&self) -> Option<&R> {
109 self.results.get(self.selected)
110 }
111
112 pub fn select_next(&mut self) {
114 if !self.results.is_empty() && self.selected < self.results.len() - 1 {
115 self.selected += 1;
116 }
117 }
118
119 pub const fn select_prev(&mut self) {
121 if self.selected > 0 {
122 self.selected -= 1;
123 }
124 }
125
126 #[must_use]
128 pub fn has_results(&self) -> bool {
129 !self.results.is_empty()
130 }
131
132 #[must_use]
134 pub fn result_count(&self) -> usize {
135 self.results.len()
136 }
137}
138
139impl<R> ListNavigation for SearchState<R> {
140 fn selected(&self) -> usize {
141 self.selected
142 }
143
144 fn set_selected(&mut self, idx: usize) {
145 self.selected = idx;
146 }
147
148 fn total(&self) -> usize {
149 self.results.len()
150 }
151
152 fn set_total(&mut self, _total: usize) {
153 }
155}
156
157#[derive(Debug, Clone, Default)]
161pub struct SearchStateCore {
162 pub active: bool,
164 pub query: String,
166}
167
168impl SearchStateCore {
169 #[must_use]
170 pub fn new() -> Self {
171 Self::default()
172 }
173
174 pub fn start(&mut self) {
175 self.active = true;
176 self.query.clear();
177 }
178
179 pub const fn stop(&mut self) {
180 self.active = false;
181 }
182
183 pub fn push_char(&mut self, c: char) {
184 self.query.push(c);
185 }
186
187 pub fn pop_char(&mut self) {
188 self.query.pop();
189 }
190
191 #[must_use]
192 pub fn has_valid_query(&self) -> bool {
193 self.query.len() >= 2
194 }
195
196 #[must_use]
197 pub fn query_lower(&self) -> String {
198 self.query.to_lowercase()
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn test_search_state_lifecycle() {
208 let mut state: SearchState<String> = SearchState::new();
209
210 assert!(!state.active);
211 assert!(state.query.is_empty());
212
213 state.start();
214 assert!(state.active);
215 assert!(state.query.is_empty());
216
217 state.push_char('t');
218 state.push_char('e');
219 state.push_char('s');
220 state.push_char('t');
221 assert_eq!(state.query, "test");
222 assert!(state.has_valid_query());
223
224 state.pop_char();
225 assert_eq!(state.query, "tes");
226
227 state.stop();
228 assert!(!state.active);
229 }
230
231 #[test]
232 fn test_search_state_results() {
233 let mut state: SearchState<String> = SearchState::new();
234
235 state.set_results(vec![
236 "result1".to_string(),
237 "result2".to_string(),
238 "result3".to_string(),
239 ]);
240
241 assert!(state.has_results());
242 assert_eq!(state.result_count(), 3);
243 assert_eq!(state.selected, 0);
244 assert_eq!(state.selected_result(), Some(&"result1".to_string()));
245
246 state.select_next();
247 assert_eq!(state.selected, 1);
248 assert_eq!(state.selected_result(), Some(&"result2".to_string()));
249
250 state.select_next();
251 assert_eq!(state.selected, 2);
252
253 state.select_next();
255 assert_eq!(state.selected, 2);
256
257 state.select_prev();
258 assert_eq!(state.selected, 1);
259
260 state.select_prev();
261 assert_eq!(state.selected, 0);
262
263 state.select_prev();
265 assert_eq!(state.selected, 0);
266 }
267
268 #[test]
269 fn test_search_state_clear() {
270 let mut state: SearchState<String> = SearchState::new();
271 state.query = "test".to_string();
272 state.set_results(vec!["a".to_string(), "b".to_string()]);
273 state.selected = 1;
274
275 state.clear();
276
277 assert!(state.query.is_empty());
278 assert!(state.results.is_empty());
279 assert_eq!(state.selected, 0);
280 }
281
282 #[test]
283 fn test_search_state_list_navigation() {
284 let mut state: SearchState<i32> = SearchState::new();
285 state.set_results(vec![1, 2, 3, 4, 5]);
286
287 assert_eq!(state.selected(), 0);
289 assert_eq!(state.total(), 5);
290
291 state.set_selected(2);
292 assert_eq!(state.selected(), 2);
293
294 state.page_down();
296 assert_eq!(state.selected(), 4); state.go_first();
299 assert_eq!(state.selected(), 0);
300
301 state.go_last();
302 assert_eq!(state.selected(), 4);
303 }
304
305 #[test]
306 fn test_search_state_core() {
307 let mut core = SearchStateCore::new();
308
309 assert!(!core.active);
310 assert!(core.query.is_empty());
311
312 core.start();
313 assert!(core.active);
314
315 core.push_char('A');
316 core.push_char('B');
317 assert_eq!(core.query, "AB");
318 assert!(core.has_valid_query());
319 assert_eq!(core.query_lower(), "ab");
320
321 core.stop();
322 assert!(!core.active);
323 }
324}