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