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;
220 self.search.filtered_indices = self
221 .hosts_state
222 .list
223 .iter()
224 .enumerate()
225 .filter(|(_, host)| {
226 (super::contains_ci("stale", tag_query) && host.stale.is_some())
227 || (super::contains_ci("vault-ssh", tag_query)
228 && crate::vault_ssh::resolve_vault_role(
229 host.vault_ssh.as_deref(),
230 host.provider.as_deref(),
231 host.provider_label.as_deref(),
232 provider_config,
233 )
234 .is_some())
235 || (super::contains_ci("vault-kv", tag_query)
236 && host
237 .askpass
238 .as_deref()
239 .map(|s| s.starts_with("vault:"))
240 .unwrap_or(false))
241 || host
242 .provider_tags
243 .iter()
244 .chain(host.tags.iter())
245 .any(|t| super::contains_ci(t, tag_query))
246 || host
247 .provider
248 .as_ref()
249 .is_some_and(|p| super::contains_ci(p, tag_query))
250 })
251 .map(|(i, _)| i)
252 .collect();
253 self.search.filtered_pattern_indices = self
254 .hosts_state
255 .patterns
256 .iter()
257 .enumerate()
258 .filter(|(_, p)| p.tags.iter().any(|t| super::contains_ci(t, tag_query)))
259 .map(|(i, _)| i)
260 .collect();
261 } else {
262 self.search.filtered_indices = self
263 .hosts_state
264 .list
265 .iter()
266 .enumerate()
267 .filter(|(_, host)| {
268 super::contains_ci(&host.alias, &query)
269 || super::contains_ci(&host.hostname, &query)
270 || super::contains_ci(&host.user, &query)
271 || host
272 .provider_tags
273 .iter()
274 .chain(host.tags.iter())
275 .any(|t| super::contains_ci(t, &query))
276 || host
277 .provider
278 .as_ref()
279 .is_some_and(|p| super::contains_ci(p, &query))
280 })
281 .map(|(i, _)| i)
282 .collect();
283 self.search.filtered_pattern_indices = self
284 .hosts_state
285 .patterns
286 .iter()
287 .enumerate()
288 .filter(|(_, p)| {
289 super::contains_ci(&p.pattern, &query)
290 || p.tags.iter().any(|t| super::contains_ci(t, &query))
291 })
292 .map(|(i, _)| i)
293 .collect();
294 }
295
296 if let Some(ref scope) = self.search.scope_indices {
298 self.search.filtered_indices.retain(|i| scope.contains(i));
299 }
300
301 if self.ping.filter_down_only {
303 self.search.filtered_indices.retain(|&idx| {
304 let alias = &self.hosts_state.list[idx].alias;
305 matches!(self.ping.status.get(alias), Some(PingStatus::Unreachable))
306 });
307 self.search.filtered_pattern_indices.clear();
309 }
310
311 let total_results =
313 self.search.filtered_indices.len() + self.search.filtered_pattern_indices.len();
314 if total_results == 0 {
315 self.ui.list_state.select(None);
316 } else {
317 self.ui.list_state.select(Some(0));
318 }
319 }
320 pub fn filtered_snippet_indices(&self) -> Vec<usize> {
322 match &self.ui.snippet_search {
323 None => (0..self.snippets.store.snippets.len()).collect(),
324 Some(query) if query.is_empty() => (0..self.snippets.store.snippets.len()).collect(),
325 Some(query) => self
326 .snippets
327 .store
328 .snippets
329 .iter()
330 .enumerate()
331 .filter(|(_, s)| {
332 super::contains_ci(&s.name, query)
333 || super::contains_ci(&s.command, query)
334 || super::contains_ci(&s.description, query)
335 })
336 .map(|(i, _)| i)
337 .collect(),
338 }
339 }
340}