1use std::collections::HashSet;
6
7use super::{HostListItem, PingStatus};
8use crate::app::App;
9
10#[derive(Default)]
12pub struct SearchState {
13 pub query: Option<String>,
14 pub filtered_indices: Vec<usize>,
15 pub filtered_pattern_indices: Vec<usize>,
16 pub pre_search_selection: Option<usize>,
17 pub scope_indices: Option<HashSet<usize>>,
20}
21
22impl App {
23 fn compute_search_scope(&self) -> Option<HashSet<usize>> {
25 self.hosts_state.group_filter.as_ref()?;
26 Some(
27 self.hosts_state
28 .display_list
29 .iter()
30 .filter_map(|item| {
31 if let HostListItem::Host { index } = item {
32 Some(*index)
33 } else {
34 None
35 }
36 })
37 .collect(),
38 )
39 }
40
41 pub fn start_search(&mut self) {
43 self.search.pre_search_selection = self.ui.list_state.selected();
44 self.search.scope_indices = self.compute_search_scope();
45 self.search.query = Some(String::new());
46 self.apply_filter();
47 }
48
49 pub fn start_search_with(&mut self, query: &str) {
51 self.search.pre_search_selection = self.ui.list_state.selected();
52 self.search.scope_indices = self.compute_search_scope();
53 self.search.query = Some(query.to_string());
54 self.apply_filter();
55 }
56
57 pub fn cancel_search(&mut self) {
59 self.ping.filter_down_only = false;
60 self.search.query = None;
61 self.search.filtered_indices.clear();
62 self.search.filtered_pattern_indices.clear();
63 self.search.scope_indices = None;
64 if let Some(pos) = self.search.pre_search_selection.take() {
66 if pos < self.hosts_state.display_list.len() {
67 self.ui.list_state.select(Some(pos));
68 } else if let Some(first) = self.hosts_state.display_list.iter().position(|item| {
69 matches!(
70 item,
71 HostListItem::Host { .. } | HostListItem::Pattern { .. }
72 )
73 }) {
74 self.ui.list_state.select(Some(first));
75 }
76 }
77 }
78
79 pub fn apply_filter(&mut self) {
81 log::debug!(
82 "[purple] apply_filter: query={:?} down_only={} scope={}",
83 self.search.query.as_deref().unwrap_or(""),
84 self.ping.filter_down_only,
85 self.search.scope_indices.as_ref().map_or(0, |s| s.len())
86 );
87 self.hosts_state.render_cache.invalidate();
90 let query = match &self.search.query {
91 Some(q) if !q.is_empty() => q.clone(),
92 Some(_) => {
93 self.search.filtered_indices = (0..self.hosts_state.list.len()).collect();
94 self.search.filtered_pattern_indices =
95 (0..self.hosts_state.patterns.len()).collect();
96 if let Some(ref scope) = self.search.scope_indices {
98 self.search.filtered_indices.retain(|i| scope.contains(i));
99 }
100 if !self.ping.filter_down_only {
101 let total = self.search.filtered_indices.len()
102 + self.search.filtered_pattern_indices.len();
103 if total == 0 {
104 self.ui.list_state.select(None);
105 } else {
106 self.ui.list_state.select(Some(0));
107 }
108 return;
109 }
110 String::new()
112 }
113 None => {
114 if !self.ping.filter_down_only {
115 return;
116 }
117 self.search.filtered_indices = (0..self.hosts_state.list.len()).collect();
119 self.search.filtered_pattern_indices = Vec::new();
120 if let Some(ref scope) = self.search.scope_indices {
122 self.search.filtered_indices.retain(|i| scope.contains(i));
123 }
124 String::new()
126 }
127 };
128
129 if let Some(tag_exact) = query.strip_prefix("tag=") {
130 let provider_config = &self.providers.config;
132 self.search.filtered_indices = self
133 .hosts_state
134 .list
135 .iter()
136 .enumerate()
137 .filter(|(_, host)| {
138 (super::eq_ci("stale", tag_exact) && host.stale.is_some())
139 || (super::eq_ci("vault-ssh", tag_exact)
140 && crate::vault_ssh::resolve_vault_role(
141 host.vault_ssh.as_deref(),
142 host.provider.as_deref(),
143 host.provider_label.as_deref(),
144 provider_config,
145 )
146 .is_some())
147 || (super::eq_ci("vault-kv", tag_exact)
148 && host
149 .askpass
150 .as_deref()
151 .map(|s| s.starts_with("vault:"))
152 .unwrap_or(false))
153 || host
154 .provider_tags
155 .iter()
156 .chain(host.tags.iter())
157 .any(|t| super::eq_ci(t, tag_exact))
158 || host
159 .provider
160 .as_ref()
161 .is_some_and(|p| super::eq_ci(p, tag_exact))
162 })
163 .map(|(i, _)| i)
164 .collect();
165 self.search.filtered_pattern_indices = self
166 .hosts_state
167 .patterns
168 .iter()
169 .enumerate()
170 .filter(|(_, p)| p.tags.iter().any(|t| super::eq_ci(t, tag_exact)))
171 .map(|(i, _)| i)
172 .collect();
173 } else if let Some(tag_query) = 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::contains_ci("stale", tag_query) && host.stale.is_some())
183 || (super::contains_ci("vault-ssh", tag_query)
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::contains_ci("vault-kv", tag_query)
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::contains_ci(t, tag_query))
202 || host
203 .provider
204 .as_ref()
205 .is_some_and(|p| super::contains_ci(p, tag_query))
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::contains_ci(t, tag_query)))
215 .map(|(i, _)| i)
216 .collect();
217 } else {
218 self.search.filtered_indices = self
219 .hosts_state
220 .list
221 .iter()
222 .enumerate()
223 .filter(|(_, host)| {
224 super::contains_ci(&host.alias, &query)
225 || super::contains_ci(&host.hostname, &query)
226 || super::contains_ci(&host.user, &query)
227 || host
228 .provider_tags
229 .iter()
230 .chain(host.tags.iter())
231 .any(|t| super::contains_ci(t, &query))
232 || host
233 .provider
234 .as_ref()
235 .is_some_and(|p| super::contains_ci(p, &query))
236 })
237 .map(|(i, _)| i)
238 .collect();
239 self.search.filtered_pattern_indices = self
240 .hosts_state
241 .patterns
242 .iter()
243 .enumerate()
244 .filter(|(_, p)| {
245 super::contains_ci(&p.pattern, &query)
246 || p.tags.iter().any(|t| super::contains_ci(t, &query))
247 })
248 .map(|(i, _)| i)
249 .collect();
250 }
251
252 if let Some(ref scope) = self.search.scope_indices {
254 self.search.filtered_indices.retain(|i| scope.contains(i));
255 }
256
257 if self.ping.filter_down_only {
259 self.search.filtered_indices.retain(|&idx| {
260 let alias = &self.hosts_state.list[idx].alias;
261 matches!(self.ping.status.get(alias), Some(PingStatus::Unreachable))
262 });
263 self.search.filtered_pattern_indices.clear();
265 }
266
267 let total_results =
269 self.search.filtered_indices.len() + self.search.filtered_pattern_indices.len();
270 if total_results == 0 {
271 self.ui.list_state.select(None);
272 } else {
273 self.ui.list_state.select(Some(0));
274 }
275 }
276 pub fn filtered_snippet_indices(&self) -> Vec<usize> {
278 match &self.ui.snippet_search {
279 None => (0..self.snippets.store.snippets.len()).collect(),
280 Some(query) if query.is_empty() => (0..self.snippets.store.snippets.len()).collect(),
281 Some(query) => self
282 .snippets
283 .store
284 .snippets
285 .iter()
286 .enumerate()
287 .filter(|(_, s)| {
288 super::contains_ci(&s.name, query)
289 || super::contains_ci(&s.command, query)
290 || super::contains_ci(&s.description, query)
291 })
292 .map(|(i, _)| i)
293 .collect(),
294 }
295 }
296}