sbom_tools/tui/
app_impl_search.rs1use super::app::{App, TabKind};
4use super::app_states::{
5 ChangeType, ComponentFilter, DiffSearchResult, SearchMode, VulnChangeType, VulnFilter, VulnSort,
6};
7
8impl App {
9 pub fn start_search(&mut self) {
11 self.overlays.search.active = true;
12 self.overlays.search.clear();
13 self.overlays.show_help = false;
14 self.overlays.show_export = false;
15 self.overlays.show_legend = false;
16 }
17
18 pub const fn stop_search(&mut self) {
20 self.overlays.search.active = false;
21 }
22
23 pub fn search_push(&mut self, c: char) {
25 self.overlays.search.push_char(c);
26 }
27
28 pub fn search_pop(&mut self) {
30 self.overlays.search.pop_char();
31 }
32
33 pub fn execute_search(&mut self) {
35 if self.overlays.search.query.len() < 2 {
36 self.overlays.search.results.clear();
37 return;
38 }
39
40 let query = &self.overlays.search.query;
41 let query_lower = query.to_lowercase();
42 let search_mode = self.overlays.search.mode;
43
44 let regex_matcher = if search_mode == SearchMode::Regex {
46 match regex::RegexBuilder::new(query)
47 .case_insensitive(true)
48 .build()
49 {
50 Ok(re) => {
51 self.overlays.search.search_error = None;
52 Some(re)
53 }
54 Err(e) => {
55 self.overlays.search.search_error = Some(format!("Invalid regex: {e}"));
56 self.overlays.search.results.clear();
57 return;
58 }
59 }
60 } else {
61 self.overlays.search.search_error = None;
62 None
63 };
64
65 let matches_query = |text: &str| -> bool {
67 match search_mode {
68 SearchMode::Substring => text.to_lowercase().contains(&query_lower),
69 SearchMode::Regex => regex_matcher.as_ref().is_some_and(|re| re.is_match(text)),
70 }
71 };
72
73 let mut results = Vec::new();
74
75 if let Some(ref diff) = self.data.diff_result {
77 for comp in &diff.components.added {
79 if matches_query(&comp.name) {
80 results.push(DiffSearchResult::Component {
81 name: comp.name.clone(),
82 version: comp.new_version.clone(),
83 change_type: ChangeType::Added,
84 });
85 }
86 }
87
88 for comp in &diff.components.removed {
90 if matches_query(&comp.name) {
91 results.push(DiffSearchResult::Component {
92 name: comp.name.clone(),
93 version: comp.old_version.clone(),
94 change_type: ChangeType::Removed,
95 });
96 }
97 }
98
99 for change in &diff.components.modified {
101 if matches_query(&change.name) {
102 results.push(DiffSearchResult::Component {
103 name: change.name.clone(),
104 version: change.new_version.clone(),
105 change_type: ChangeType::Modified,
106 });
107 }
108 }
109
110 for vuln in &diff.vulnerabilities.introduced {
112 if matches_query(&vuln.id) {
113 results.push(DiffSearchResult::Vulnerability {
114 id: vuln.id.clone(),
115 component_name: vuln.component_name.clone(),
116 severity: Some(vuln.severity.clone()),
117 change_type: VulnChangeType::Introduced,
118 });
119 }
120 }
121
122 for vuln in &diff.vulnerabilities.resolved {
124 if matches_query(&vuln.id) {
125 results.push(DiffSearchResult::Vulnerability {
126 id: vuln.id.clone(),
127 component_name: vuln.component_name.clone(),
128 severity: Some(vuln.severity.clone()),
129 change_type: VulnChangeType::Resolved,
130 });
131 }
132 }
133
134 for lic_change in &diff.licenses.new_licenses {
136 if matches_query(&lic_change.license) {
137 let component_name = lic_change
138 .components
139 .first()
140 .cloned()
141 .unwrap_or_else(|| "multiple".to_string());
142 results.push(DiffSearchResult::License {
143 license: lic_change.license.clone(),
144 component_name,
145 change_type: ChangeType::Added,
146 });
147 }
148 }
149
150 for lic_change in &diff.licenses.removed_licenses {
152 if matches_query(&lic_change.license) {
153 let component_name = lic_change
154 .components
155 .first()
156 .cloned()
157 .unwrap_or_else(|| "multiple".to_string());
158 results.push(DiffSearchResult::License {
159 license: lic_change.license.clone(),
160 component_name,
161 change_type: ChangeType::Removed,
162 });
163 }
164 }
165 }
166
167 if self.data.diff_result.is_none()
169 && let Some(ref sbom) = self.data.sbom
170 {
171 for comp in sbom.components.values() {
173 if matches_query(&comp.name) {
174 results.push(DiffSearchResult::Component {
175 name: comp.name.clone(),
176 version: comp.version.clone(),
177 change_type: ChangeType::Added, });
179 }
180 }
181
182 for comp in sbom.components.values() {
184 for vuln in &comp.vulnerabilities {
185 if matches_query(&vuln.id) {
186 results.push(DiffSearchResult::Vulnerability {
187 id: vuln.id.clone(),
188 component_name: comp.name.clone(),
189 severity: vuln.severity.as_ref().map(|s| format!("{s:?}")),
190 change_type: VulnChangeType::Introduced, });
192 }
193 }
194 }
195
196 for comp in sbom.components.values() {
198 for lic in &comp.licenses.declared {
199 if matches_query(&lic.expression) {
200 results.push(DiffSearchResult::License {
201 license: lic.expression.clone(),
202 component_name: comp.name.clone(),
203 change_type: ChangeType::Added, });
205 }
206 }
207 }
208 }
209
210 let comp_filter = self.components_state().filter;
213 if comp_filter != ComponentFilter::All {
214 results.retain(|r| match r {
215 DiffSearchResult::Component { change_type, .. } => match comp_filter {
216 ComponentFilter::Added => *change_type == ChangeType::Added,
217 ComponentFilter::Removed => *change_type == ChangeType::Removed,
218 ComponentFilter::Modified => *change_type == ChangeType::Modified,
219 _ => true,
221 },
222 DiffSearchResult::Vulnerability { .. } | DiffSearchResult::License { .. } => true,
224 });
225 }
226
227 results.truncate(50);
229 self.overlays.search.results = results;
230 self.overlays.search.selected = 0;
231 }
232
233 pub fn jump_to_search_result(&mut self) {
235 if let Some(result) = self
236 .overlays
237 .search
238 .results
239 .get(self.overlays.search.selected)
240 .cloned()
241 {
242 match result {
243 DiffSearchResult::Component {
244 name,
245 version,
246 change_type,
247 ..
248 } => {
249 if let Some(index) =
251 self.find_component_index_all(&name, Some(change_type), version.as_deref())
252 {
253 self.components_state_mut().filter = ComponentFilter::All;
254 self.components_state_mut().selected = index;
255 self.select_tab(TabKind::Components);
256 self.stop_search();
257 return;
258 }
259
260 if let Some(index) = self.find_component_index_all(&name, None, None) {
262 self.components_state_mut().filter = ComponentFilter::All;
263 self.components_state_mut().selected = index;
264 self.select_tab(TabKind::Components);
265 self.stop_search();
266 return;
267 }
268
269 self.components_state_mut().filter = ComponentFilter::All;
270 self.select_tab(TabKind::Components);
271 }
272 DiffSearchResult::Vulnerability {
273 id, change_type, ..
274 } => {
275 self.vulnerabilities_state_mut().sort_by = VulnSort::Id;
277 self.vulnerabilities_state_mut().filter = match change_type {
278 VulnChangeType::Introduced => VulnFilter::Introduced,
279 VulnChangeType::Resolved => VulnFilter::Resolved,
280 };
281
282 if let Some(index) = self.find_vulnerability_index(&id) {
283 self.vulnerabilities_state_mut().selected = index;
284 }
285
286 self.select_tab(TabKind::Vulnerabilities);
287 }
288 DiffSearchResult::License { license, .. } => {
289 if let Some(ref diff) = self.data.diff_result {
291 let mut index = 0;
292
293 for lic in &diff.licenses.new_licenses {
295 if lic.license == license {
296 self.licenses_state_mut().selected = index;
297 self.select_tab(TabKind::Licenses);
298 self.stop_search();
299 return;
300 }
301 index += 1;
302 }
303
304 for lic in &diff.licenses.removed_licenses {
306 if lic.license == license {
307 self.licenses_state_mut().selected = index;
308 self.select_tab(TabKind::Licenses);
309 self.stop_search();
310 return;
311 }
312 index += 1;
313 }
314 }
315 self.select_tab(TabKind::Licenses);
316 }
317 }
318 self.stop_search();
319 }
320 }
321}