1use std::collections::HashSet;
6
7use super::{HostListItem, PingStatus};
8use crate::app::App;
9
10#[derive(Default)]
12pub struct SearchState {
13 pub(in crate::app) query: Option<String>,
14 pub(in crate::app) filtered_indices: Vec<usize>,
15 pub(in crate::app) filtered_pattern_indices: Vec<usize>,
16 pub(in crate::app) pre_search_selection: Option<usize>,
17 pub(in crate::app) scope_indices: Option<HashSet<usize>>,
20}
21
22impl SearchState {
23 pub fn query(&self) -> Option<&str> {
24 self.query.as_deref()
25 }
26
27 pub fn filtered_indices(&self) -> &[usize] {
28 &self.filtered_indices
29 }
30
31 pub fn filtered_pattern_indices(&self) -> &[usize] {
32 &self.filtered_pattern_indices
33 }
34
35 pub fn scope_indices(&self) -> Option<&HashSet<usize>> {
36 self.scope_indices.as_ref()
37 }
38
39 pub fn set_query(&mut self, value: Option<String>) {
40 self.query = value;
41 }
42
43 pub fn clear_filtered_indices(&mut self) {
44 self.filtered_indices.clear();
45 }
46
47 pub fn clear_filtered_pattern_indices(&mut self) {
48 self.filtered_pattern_indices.clear();
49 }
50
51 pub fn push_query_char(&mut self, c: char) {
53 if let Some(q) = self.query.as_mut() {
54 q.push(c);
55 }
56 }
57
58 pub fn pop_query_char(&mut self) {
60 if let Some(q) = self.query.as_mut() {
61 q.pop();
62 }
63 }
64}
65
66impl App {
67 fn compute_search_scope(&self) -> Option<HashSet<usize>> {
69 self.hosts_state.group_filter.as_ref()?;
70 Some(
71 self.hosts_state
72 .display_list
73 .iter()
74 .filter_map(|item| {
75 if let HostListItem::Host { index } = item {
76 Some(*index)
77 } else {
78 None
79 }
80 })
81 .collect(),
82 )
83 }
84
85 pub fn start_search(&mut self) {
87 self.search.pre_search_selection = self.ui.list_state.selected();
88 self.search.scope_indices = self.compute_search_scope();
89 self.search.query = Some(String::new());
90 self.apply_filter();
91 }
92
93 pub fn start_search_with(&mut self, query: &str) {
95 self.search.pre_search_selection = self.ui.list_state.selected();
96 self.search.scope_indices = self.compute_search_scope();
97 self.search.query = Some(query.to_string());
98 self.apply_filter();
99 }
100
101 pub fn cancel_search(&mut self) {
103 self.ping.filter_down_only = false;
104 self.search.query = None;
105 self.search.filtered_indices.clear();
106 self.search.filtered_pattern_indices.clear();
107 self.search.scope_indices = None;
108 if let Some(pos) = self.search.pre_search_selection.take() {
110 if pos < self.hosts_state.display_list.len() {
111 self.ui.list_state.select(Some(pos));
112 } else if let Some(first) = self.hosts_state.display_list.iter().position(|item| {
113 matches!(
114 item,
115 HostListItem::Host { .. } | HostListItem::Pattern { .. }
116 )
117 }) {
118 self.ui.list_state.select(Some(first));
119 }
120 }
121 }
122
123 pub fn apply_filter(&mut self) {
125 log::debug!(
126 "[purple] apply_filter: query={:?} down_only={} scope={}",
127 self.search.query.as_deref().unwrap_or(""),
128 self.ping.filter_down_only,
129 self.search.scope_indices.as_ref().map_or(0, |s| s.len())
130 );
131 self.hosts_state.render_cache.invalidate();
134 let query = match &self.search.query {
135 Some(q) if !q.is_empty() => q.clone(),
136 Some(_) => {
137 self.search.filtered_indices = (0..self.hosts_state.list.len()).collect();
138 self.search.filtered_pattern_indices =
139 (0..self.hosts_state.patterns.len()).collect();
140 if let Some(ref scope) = self.search.scope_indices {
142 self.search.filtered_indices.retain(|i| scope.contains(i));
143 }
144 if !self.ping.filter_down_only {
145 let total = self.search.filtered_indices.len()
146 + self.search.filtered_pattern_indices.len();
147 if total == 0 {
148 self.ui.list_state.select(None);
149 } else {
150 self.ui.list_state.select(Some(0));
151 }
152 return;
153 }
154 String::new()
156 }
157 None => {
158 if !self.ping.filter_down_only {
159 return;
160 }
161 self.search.filtered_indices = (0..self.hosts_state.list.len()).collect();
163 self.search.filtered_pattern_indices = Vec::new();
164 if let Some(ref scope) = self.search.scope_indices {
166 self.search.filtered_indices.retain(|i| scope.contains(i));
167 }
168 String::new()
170 }
171 };
172
173 if let Some(tag_exact) = query.strip_prefix("tag=") {
174 let provider_config = &self.providers.config;
176 self.search.filtered_indices = self
177 .hosts_state
178 .list
179 .iter()
180 .enumerate()
181 .filter(|(_, host)| {
182 (super::eq_ci("stale", tag_exact) && host.stale.is_some())
183 || (super::eq_ci("vault-ssh", tag_exact)
184 && crate::vault_ssh::resolve_vault_role(
185 host.vault_ssh.as_deref(),
186 host.provider.as_deref(),
187 host.provider_label.as_deref(),
188 provider_config,
189 )
190 .is_some())
191 || (super::eq_ci("vault-kv", tag_exact)
192 && host
193 .askpass
194 .as_deref()
195 .map(|s| s.starts_with("vault:"))
196 .unwrap_or(false))
197 || host
198 .provider_tags
199 .iter()
200 .chain(host.tags.iter())
201 .any(|t| super::eq_ci(t, tag_exact))
202 || host
203 .provider
204 .as_ref()
205 .is_some_and(|p| super::eq_ci(p, tag_exact))
206 })
207 .map(|(i, _)| i)
208 .collect();
209 self.search.filtered_pattern_indices = self
210 .hosts_state
211 .patterns
212 .iter()
213 .enumerate()
214 .filter(|(_, p)| p.tags.iter().any(|t| super::eq_ci(t, tag_exact)))
215 .map(|(i, _)| i)
216 .collect();
217 } else if let Some(tag_query) = query.strip_prefix("tag:") {
218 let provider_config = &self.providers.config;
221 let terms: Vec<&str> = tag_query.split_whitespace().collect();
222 self.search.filtered_indices = self
223 .hosts_state
224 .list
225 .iter()
226 .enumerate()
227 .filter(|(_, host)| {
228 terms.iter().all(|term| {
229 (super::contains_ci("stale", term) && host.stale.is_some())
230 || (super::contains_ci("vault-ssh", term)
231 && crate::vault_ssh::resolve_vault_role(
232 host.vault_ssh.as_deref(),
233 host.provider.as_deref(),
234 host.provider_label.as_deref(),
235 provider_config,
236 )
237 .is_some())
238 || (super::contains_ci("vault-kv", term)
239 && host
240 .askpass
241 .as_deref()
242 .map(|s| s.starts_with("vault:"))
243 .unwrap_or(false))
244 || host
245 .provider_tags
246 .iter()
247 .chain(host.tags.iter())
248 .any(|t| super::contains_ci(t, term))
249 || host
250 .provider
251 .as_ref()
252 .is_some_and(|p| super::contains_ci(p, term))
253 })
254 })
255 .map(|(i, _)| i)
256 .collect();
257 self.search.filtered_pattern_indices = self
258 .hosts_state
259 .patterns
260 .iter()
261 .enumerate()
262 .filter(|(_, p)| {
263 terms
264 .iter()
265 .all(|term| p.tags.iter().any(|t| super::contains_ci(t, term)))
266 })
267 .map(|(i, _)| i)
268 .collect();
269 } else {
270 let terms: Vec<&str> = query.split_whitespace().collect();
275 self.search.filtered_indices = self
276 .hosts_state
277 .list
278 .iter()
279 .enumerate()
280 .filter(|(_, host)| {
281 terms.iter().all(|term| {
282 super::contains_ci(&host.alias, term)
283 || super::contains_ci(&host.hostname, term)
284 || super::contains_ci(&host.user, term)
285 || host
286 .provider_tags
287 .iter()
288 .chain(host.tags.iter())
289 .any(|t| super::contains_ci(t, term))
290 || host
291 .provider
292 .as_ref()
293 .is_some_and(|p| super::contains_ci(p, term))
294 })
295 })
296 .map(|(i, _)| i)
297 .collect();
298 self.search.filtered_pattern_indices = self
299 .hosts_state
300 .patterns
301 .iter()
302 .enumerate()
303 .filter(|(_, p)| {
304 terms.iter().all(|term| {
305 super::contains_ci(&p.pattern, term)
306 || p.tags.iter().any(|t| super::contains_ci(t, term))
307 })
308 })
309 .map(|(i, _)| i)
310 .collect();
311 }
312
313 if let Some(ref scope) = self.search.scope_indices {
315 self.search.filtered_indices.retain(|i| scope.contains(i));
316 }
317
318 if self.ping.filter_down_only {
320 self.search.filtered_indices.retain(|&idx| {
321 let alias = &self.hosts_state.list[idx].alias;
322 matches!(self.ping.status.get(alias), Some(PingStatus::Unreachable))
323 });
324 self.search.filtered_pattern_indices.clear();
326 }
327
328 let total_results =
330 self.search.filtered_indices.len() + self.search.filtered_pattern_indices.len();
331 log::debug!(
332 "[purple] apply_filter matched: hosts={} patterns={}",
333 self.search.filtered_indices.len(),
334 self.search.filtered_pattern_indices.len()
335 );
336 if total_results == 0 {
337 self.ui.list_state.select(None);
338 } else {
339 self.ui.list_state.select(Some(0));
340 }
341 }
342 pub fn filtered_snippet_indices(&self) -> Vec<usize> {
344 match &self.ui.snippet_search {
345 None => (0..self.snippets.store.snippets.len()).collect(),
346 Some(query) if query.is_empty() => (0..self.snippets.store.snippets.len()).collect(),
347 Some(query) => self
348 .snippets
349 .store
350 .snippets
351 .iter()
352 .enumerate()
353 .filter(|(_, s)| {
354 super::contains_ci(&s.name, query)
355 || super::contains_ci(&s.command, query)
356 || super::contains_ci(&s.description, query)
357 })
358 .map(|(i, _)| i)
359 .collect(),
360 }
361 }
362}