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