Skip to main content

sbom_tools/tui/
app_impl_nav.rs

1//! Navigation-related methods for App.
2
3use super::app::{App, AppMode, TabKind};
4use super::app_states::ComponentFilter;
5
6impl App {
7    /// Switch to next tab
8    pub fn next_tab(&mut self) {
9        let has_graph_changes = self
10            .data.diff_result
11            .as_ref()
12            .map(|r| !r.graph_changes.is_empty())
13            .unwrap_or(false);
14
15        self.active_tab = match self.active_tab {
16            TabKind::Summary => TabKind::Components,
17            TabKind::Components => TabKind::Dependencies,
18            TabKind::Dependencies => TabKind::Licenses,
19            TabKind::Licenses => TabKind::Vulnerabilities,
20            TabKind::Vulnerabilities => TabKind::Quality,
21            TabKind::Quality => {
22                if self.mode == AppMode::Diff {
23                    TabKind::Compliance
24                } else {
25                    TabKind::Summary
26                }
27            }
28            TabKind::Compliance => TabKind::SideBySide,
29            TabKind::SideBySide => {
30                if has_graph_changes {
31                    TabKind::GraphChanges
32                } else {
33                    TabKind::Source
34                }
35            }
36            TabKind::GraphChanges => TabKind::Source,
37            TabKind::Source => TabKind::Summary,
38        };
39    }
40
41    /// Switch to previous tab
42    pub fn prev_tab(&mut self) {
43        let has_graph_changes = self
44            .data.diff_result
45            .as_ref()
46            .map(|r| !r.graph_changes.is_empty())
47            .unwrap_or(false);
48
49        self.active_tab = match self.active_tab {
50            TabKind::Summary => {
51                if self.mode == AppMode::Diff {
52                    TabKind::Source
53                } else {
54                    TabKind::Quality
55                }
56            }
57            TabKind::Components => TabKind::Summary,
58            TabKind::Dependencies => TabKind::Components,
59            TabKind::Licenses => TabKind::Dependencies,
60            TabKind::Vulnerabilities => TabKind::Licenses,
61            TabKind::Quality => TabKind::Vulnerabilities,
62            TabKind::Compliance => TabKind::Quality,
63            TabKind::SideBySide => TabKind::Compliance,
64            TabKind::GraphChanges => TabKind::SideBySide,
65            TabKind::Source => {
66                if has_graph_changes {
67                    TabKind::GraphChanges
68                } else {
69                    TabKind::SideBySide
70                }
71            }
72        };
73    }
74
75    /// Select a specific tab
76    pub fn select_tab(&mut self, tab: TabKind) {
77        self.active_tab = tab;
78    }
79
80    /// Move selection up
81    pub fn select_up(&mut self) {
82        match self.active_tab {
83            TabKind::Components => self.tabs.components.select_prev(),
84            TabKind::Vulnerabilities => self.tabs.vulnerabilities.select_prev(),
85            TabKind::Licenses => self.tabs.licenses.select_prev(),
86            TabKind::Source => self.tabs.source.active_panel_mut().select_prev(),
87            _ => {}
88        }
89    }
90
91    /// Move selection down
92    pub fn select_down(&mut self) {
93        match self.active_tab {
94            TabKind::Components => self.tabs.components.select_next(),
95            TabKind::Vulnerabilities => self.tabs.vulnerabilities.select_next(),
96            TabKind::Licenses => self.tabs.licenses.select_next(),
97            TabKind::Source => self.tabs.source.active_panel_mut().select_next(),
98            _ => {}
99        }
100    }
101
102    /// Move selection to first item
103    pub fn select_first(&mut self) {
104        match self.active_tab {
105            TabKind::Components => self.tabs.components.selected = 0,
106            TabKind::Vulnerabilities => self.tabs.vulnerabilities.selected = 0,
107            TabKind::Licenses => self.tabs.licenses.selected = 0,
108            TabKind::Source => self.tabs.source.active_panel_mut().select_first(),
109            _ => {}
110        }
111    }
112
113    /// Move selection to last item
114    pub fn select_last(&mut self) {
115        match self.active_tab {
116            TabKind::Components => {
117                if self.tabs.components.total > 0 {
118                    self.tabs.components.selected = self.tabs.components.total - 1;
119                }
120            }
121            TabKind::Vulnerabilities => {
122                if self.tabs.vulnerabilities.total > 0 {
123                    self.tabs.vulnerabilities.selected = self.tabs.vulnerabilities.total - 1;
124                }
125            }
126            TabKind::Source => self.tabs.source.active_panel_mut().select_last(),
127            _ => {}
128        }
129    }
130
131    /// Page up
132    pub fn page_up(&mut self) {
133        let page_size = 10;
134        match self.active_tab {
135            TabKind::Components => {
136                self.tabs.components.selected =
137                    self.tabs.components.selected.saturating_sub(page_size);
138            }
139            TabKind::Vulnerabilities => {
140                self.tabs.vulnerabilities.selected = self
141                    .tabs.vulnerabilities
142                    .selected
143                    .saturating_sub(page_size);
144            }
145            TabKind::Source => self.tabs.source.active_panel_mut().page_up(),
146            _ => {}
147        }
148    }
149
150    /// Page down
151    pub fn page_down(&mut self) {
152        let page_size = 10;
153        match self.active_tab {
154            TabKind::Components => {
155                self.tabs.components.selected = (self.tabs.components.selected + page_size)
156                    .min(self.tabs.components.total.saturating_sub(1));
157            }
158            TabKind::Vulnerabilities => {
159                self.tabs.vulnerabilities.selected = (self.tabs.vulnerabilities.selected
160                    + page_size)
161                    .min(self.tabs.vulnerabilities.total.saturating_sub(1));
162            }
163            TabKind::Source => self.tabs.source.active_panel_mut().page_down(),
164            _ => {}
165        }
166    }
167
168    // ========================================================================
169    // Cross-view Navigation
170    // ========================================================================
171
172    /// Navigate from vulnerability to the affected component
173    pub fn navigate_vuln_to_component(&mut self, vuln_id: &str, component_name: &str) {
174        // Save current position as breadcrumb
175        self.navigation_ctx.push_breadcrumb(
176            TabKind::Vulnerabilities,
177            vuln_id.to_string(),
178            self.tabs.vulnerabilities.selected,
179        );
180
181        // Set target and switch to components tab
182        self.navigation_ctx.target_component = Some(component_name.to_string());
183        self.active_tab = TabKind::Components;
184
185        // Try to find and select the component
186        self.find_and_select_component(component_name);
187    }
188
189    /// Navigate from dependency to the component
190    pub fn navigate_dep_to_component(&mut self, dep_name: &str) {
191        let dep_name = dep_name
192            .split_once(":+:")
193            .map(|(_, dep)| dep)
194            .or_else(|| dep_name.split_once(":-:").map(|(_, dep)| dep))
195            .unwrap_or(dep_name);
196
197        if dep_name.starts_with("__") {
198            return;
199        }
200
201        // Save current position as breadcrumb
202        self.navigation_ctx.push_breadcrumb(
203            TabKind::Dependencies,
204            dep_name.to_string(),
205            self.tabs.dependencies.selected,
206        );
207
208        // Set target and switch to components tab
209        self.navigation_ctx.target_component = Some(dep_name.to_string());
210        self.active_tab = TabKind::Components;
211
212        // Try to find and select the component
213        self.find_and_select_component(dep_name);
214    }
215
216    /// Navigate back using breadcrumbs
217    pub fn navigate_back(&mut self) -> bool {
218        if let Some(breadcrumb) = self.navigation_ctx.pop_breadcrumb() {
219            self.active_tab = breadcrumb.tab;
220
221            // Restore selection based on the tab we're returning to
222            match breadcrumb.tab {
223                TabKind::Vulnerabilities => {
224                    self.tabs.vulnerabilities.selected = breadcrumb.selection_index;
225                }
226                TabKind::Components => {
227                    self.tabs.components.selected = breadcrumb.selection_index;
228                }
229                TabKind::Dependencies => {
230                    self.tabs.dependencies.selected = breadcrumb.selection_index;
231                }
232                TabKind::Licenses => {
233                    self.tabs.licenses.selected = breadcrumb.selection_index;
234                }
235                TabKind::Source => {
236                    self.tabs.source.active_panel_mut().selected = breadcrumb.selection_index;
237                }
238                _ => {}
239            }
240
241            self.navigation_ctx.clear_targets();
242            true
243        } else {
244            false
245        }
246    }
247
248    /// Find and select a component by name in the current view
249    pub(super) fn find_and_select_component(&mut self, name: &str) {
250        if self.data.diff_result.is_some() {
251            // Reset filter to All to ensure we can find it
252            self.tabs.components.filter = ComponentFilter::All;
253
254            let name_lower = name.to_lowercase();
255            let index = {
256                let items = self.diff_component_items(ComponentFilter::All);
257                items
258                    .iter()
259                    .position(|comp| comp.name.to_lowercase() == name_lower)
260            };
261
262            if let Some(index) = index {
263                self.tabs.components.selected = index;
264            }
265        }
266    }
267
268    /// Check if we have navigation history
269    pub fn has_navigation_history(&self) -> bool {
270        self.navigation_ctx.has_history()
271    }
272
273    /// Get the breadcrumb trail for display
274    pub fn breadcrumb_trail(&self) -> String {
275        self.navigation_ctx.breadcrumb_trail()
276    }
277
278    /// Navigate to a target tab or item
279    pub(super) fn navigate_to_target(&mut self, target: super::traits::TabTarget) {
280        use super::traits::TabTarget;
281
282        match target {
283            TabTarget::Summary => self.active_tab = TabKind::Summary,
284            TabTarget::Components => self.active_tab = TabKind::Components,
285            TabTarget::Dependencies => self.active_tab = TabKind::Dependencies,
286            TabTarget::Licenses => self.active_tab = TabKind::Licenses,
287            TabTarget::Vulnerabilities => self.active_tab = TabKind::Vulnerabilities,
288            TabTarget::Quality => self.active_tab = TabKind::Quality,
289            TabTarget::Compliance => self.active_tab = TabKind::Compliance,
290            TabTarget::SideBySide => self.active_tab = TabKind::SideBySide,
291            TabTarget::GraphChanges => self.active_tab = TabKind::GraphChanges,
292            TabTarget::Source => self.active_tab = TabKind::Source,
293            TabTarget::ComponentByName(name) => {
294                self.active_tab = TabKind::Components;
295                self.find_and_select_component(&name);
296            }
297            TabTarget::VulnerabilityById(id) => {
298                self.active_tab = TabKind::Vulnerabilities;
299                if let Some(idx) = self.find_vulnerability_index(&id) {
300                    self.tabs.vulnerabilities.selected = idx;
301                }
302            }
303        }
304    }
305}