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