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::Components => self.tabs.components.select_prev(),
85            TabKind::Vulnerabilities => self.tabs.vulnerabilities.select_prev(),
86            TabKind::Licenses => self.tabs.licenses.select_prev(),
87            TabKind::Source => self.tabs.source.select_prev(),
88            _ => {}
89        }
90    }
91
92    /// Move selection down
93    pub fn select_down(&mut self) {
94        match self.active_tab {
95            TabKind::Components => self.tabs.components.select_next(),
96            TabKind::Vulnerabilities => self.tabs.vulnerabilities.select_next(),
97            TabKind::Licenses => self.tabs.licenses.select_next(),
98            TabKind::Source => self.tabs.source.select_next(),
99            _ => {}
100        }
101    }
102
103    /// Move selection to first item
104    pub fn select_first(&mut self) {
105        match self.active_tab {
106            TabKind::Components => self.tabs.components.go_first(),
107            TabKind::Vulnerabilities => self.tabs.vulnerabilities.go_first(),
108            TabKind::Licenses => self.tabs.licenses.go_first(),
109            TabKind::Source => self.tabs.source.select_first(),
110            _ => {}
111        }
112    }
113
114    /// Move selection to last item
115    pub fn select_last(&mut self) {
116        match self.active_tab {
117            TabKind::Components => self.tabs.components.go_last(),
118            TabKind::Vulnerabilities => self.tabs.vulnerabilities.go_last(),
119            TabKind::Source => self.tabs.source.select_last(),
120            _ => {}
121        }
122    }
123
124    /// Page up
125    pub fn page_up(&mut self) {
126        match self.active_tab {
127            TabKind::Components => self.tabs.components.page_up(),
128            TabKind::Vulnerabilities => self.tabs.vulnerabilities.page_up(),
129            TabKind::Source => self.tabs.source.page_up(),
130            _ => {}
131        }
132    }
133
134    /// Page down
135    pub fn page_down(&mut self) {
136        match self.active_tab {
137            TabKind::Components => self.tabs.components.page_down(),
138            TabKind::Vulnerabilities => self.tabs.vulnerabilities.page_down(),
139            TabKind::Source => self.tabs.source.page_down(),
140            _ => {}
141        }
142    }
143
144    // ========================================================================
145    // Cross-view Navigation
146    // ========================================================================
147
148    /// Navigate from vulnerability to the affected component
149    pub fn navigate_vuln_to_component(&mut self, vuln_id: &str, component_name: &str) {
150        // Save current position as breadcrumb
151        self.navigation_ctx.push_breadcrumb(
152            TabKind::Vulnerabilities,
153            vuln_id.to_string(),
154            self.tabs.vulnerabilities.selected,
155        );
156
157        // Set target and switch to components tab
158        self.navigation_ctx.target_component = Some(component_name.to_string());
159        self.active_tab = TabKind::Components;
160
161        // Try to find and select the component
162        self.find_and_select_component(component_name);
163    }
164
165    /// Navigate from dependency to the component
166    pub fn navigate_dep_to_component(&mut self, dep_name: &str) {
167        let dep_name = dep_name
168            .split_once(":+:")
169            .map(|(_, dep)| dep)
170            .or_else(|| dep_name.split_once(":-:").map(|(_, dep)| dep))
171            .unwrap_or(dep_name);
172
173        if dep_name.starts_with("__") {
174            return;
175        }
176
177        // Save current position as breadcrumb
178        self.navigation_ctx.push_breadcrumb(
179            TabKind::Dependencies,
180            dep_name.to_string(),
181            self.tabs.dependencies.selected,
182        );
183
184        // Set target and switch to components tab
185        self.navigation_ctx.target_component = Some(dep_name.to_string());
186        self.active_tab = TabKind::Components;
187
188        // Try to find and select the component
189        self.find_and_select_component(dep_name);
190    }
191
192    /// Navigate back using breadcrumbs
193    pub fn navigate_back(&mut self) -> bool {
194        if let Some(breadcrumb) = self.navigation_ctx.pop_breadcrumb() {
195            self.active_tab = breadcrumb.tab;
196
197            // Restore selection based on the tab we're returning to
198            match breadcrumb.tab {
199                TabKind::Vulnerabilities => {
200                    self.tabs.vulnerabilities.selected = breadcrumb.selection_index;
201                }
202                TabKind::Components => {
203                    self.tabs.components.selected = breadcrumb.selection_index;
204                }
205                TabKind::Dependencies => {
206                    self.tabs.dependencies.selected = breadcrumb.selection_index;
207                }
208                TabKind::Licenses => {
209                    self.tabs.licenses.selected = breadcrumb.selection_index;
210                }
211                TabKind::Source => {
212                    self.tabs.source.active_panel_mut().selected = breadcrumb.selection_index;
213                }
214                _ => {}
215            }
216
217            self.navigation_ctx.clear_targets();
218            true
219        } else {
220            false
221        }
222    }
223
224    /// Find and select a component by name in the current view
225    pub(super) fn find_and_select_component(&mut self, name: &str) {
226        if self.data.diff_result.is_some() {
227            // Reset filter to All to ensure we can find it
228            self.tabs.components.filter = ComponentFilter::All;
229
230            let name_lower = name.to_lowercase();
231            let index = {
232                let items = self.diff_component_items(ComponentFilter::All);
233                items
234                    .iter()
235                    .position(|comp| comp.name.to_lowercase() == name_lower)
236            };
237
238            if let Some(index) = index {
239                self.tabs.components.selected = index;
240            }
241        }
242    }
243
244    /// Check if we have navigation history
245    #[must_use]
246    pub fn has_navigation_history(&self) -> bool {
247        self.navigation_ctx.has_history()
248    }
249
250    /// Get the breadcrumb trail for display
251    #[must_use]
252    pub fn breadcrumb_trail(&self) -> String {
253        self.navigation_ctx.breadcrumb_trail()
254    }
255
256    /// Navigate to a target tab or item
257    pub(super) fn navigate_to_target(&mut self, target: super::traits::TabTarget) {
258        use super::traits::TabTarget;
259
260        match target {
261            TabTarget::Summary => self.active_tab = TabKind::Summary,
262            TabTarget::Components => self.active_tab = TabKind::Components,
263            TabTarget::Dependencies => self.active_tab = TabKind::Dependencies,
264            TabTarget::Licenses => self.active_tab = TabKind::Licenses,
265            TabTarget::Vulnerabilities => self.active_tab = TabKind::Vulnerabilities,
266            TabTarget::Quality => self.active_tab = TabKind::Quality,
267            TabTarget::Compliance => self.active_tab = TabKind::Compliance,
268            TabTarget::SideBySide => self.active_tab = TabKind::SideBySide,
269            TabTarget::GraphChanges => self.active_tab = TabKind::GraphChanges,
270            TabTarget::Source => self.active_tab = TabKind::Source,
271            TabTarget::ComponentByName(name) => {
272                self.active_tab = TabKind::Components;
273                self.find_and_select_component(&name);
274            }
275            TabTarget::VulnerabilityById(id) => {
276                self.active_tab = TabKind::Vulnerabilities;
277                if let Some(idx) = self.find_vulnerability_index(&id) {
278                    self.tabs.vulnerabilities.selected = idx;
279                }
280            }
281        }
282    }
283}