sbom-tools 0.1.19

Semantic SBOM diff and analysis tool
Documentation
//! Navigation-related methods for App.

use super::app::{App, AppMode, TabKind};
use super::app_states::ComponentFilter;
use super::state::ListNavigation;

impl App {
    /// Switch to next tab
    pub fn next_tab(&mut self) {
        let has_graph_changes = self
            .data
            .diff_result
            .as_ref()
            .is_some_and(|r| !r.graph_changes.is_empty());

        self.active_tab = match self.active_tab {
            TabKind::Summary => TabKind::Components,
            TabKind::Overview => TabKind::Tree,
            TabKind::Tree => TabKind::Components,
            TabKind::Components => TabKind::Dependencies,
            TabKind::Dependencies => TabKind::Licenses,
            TabKind::Licenses => TabKind::Vulnerabilities,
            TabKind::Vulnerabilities => TabKind::Quality,
            TabKind::Quality => {
                if matches!(self.mode, AppMode::Diff | AppMode::View) {
                    TabKind::Compliance
                } else {
                    TabKind::Summary
                }
            }
            TabKind::Compliance => TabKind::SideBySide,
            TabKind::SideBySide => {
                if has_graph_changes {
                    TabKind::GraphChanges
                } else {
                    TabKind::Source
                }
            }
            TabKind::GraphChanges => TabKind::Source,
            TabKind::Source => TabKind::Summary,
        };
    }

    /// Switch to previous tab
    pub fn prev_tab(&mut self) {
        let has_graph_changes = self
            .data
            .diff_result
            .as_ref()
            .is_some_and(|r| !r.graph_changes.is_empty());

        self.active_tab = match self.active_tab {
            TabKind::Summary => {
                if matches!(self.mode, AppMode::Diff | AppMode::View) {
                    TabKind::Source
                } else {
                    TabKind::Quality
                }
            }
            TabKind::Overview => TabKind::Summary,
            TabKind::Tree => TabKind::Overview,
            TabKind::Components => TabKind::Summary,
            TabKind::Dependencies => TabKind::Components,
            TabKind::Licenses => TabKind::Dependencies,
            TabKind::Vulnerabilities => TabKind::Licenses,
            TabKind::Quality => TabKind::Vulnerabilities,
            TabKind::Compliance => TabKind::Quality,
            TabKind::SideBySide => TabKind::Compliance,
            TabKind::GraphChanges => TabKind::SideBySide,
            TabKind::Source => {
                if has_graph_changes {
                    TabKind::GraphChanges
                } else {
                    TabKind::SideBySide
                }
            }
        };
    }

    /// Select a specific tab
    pub const fn select_tab(&mut self, tab: TabKind) {
        self.active_tab = tab;
    }

    /// Move selection up
    pub fn select_up(&mut self) {
        match self.active_tab {
            TabKind::Components => self.components_state_mut().select_prev(),
            TabKind::Vulnerabilities => self.vulnerabilities_state_mut().select_prev(),
            TabKind::Licenses => self.licenses_state_mut().select_prev(),
            TabKind::Source => self.source_state_mut().select_prev(),
            _ => {}
        }
    }

    /// Move selection down
    pub fn select_down(&mut self) {
        match self.active_tab {
            TabKind::Components => self.components_state_mut().select_next(),
            TabKind::Vulnerabilities => self.vulnerabilities_state_mut().select_next(),
            TabKind::Licenses => self.licenses_state_mut().select_next(),
            TabKind::Source => self.source_state_mut().select_next(),
            _ => {}
        }
    }

    /// Move selection to first item
    pub fn select_first(&mut self) {
        match self.active_tab {
            TabKind::Components => self.components_state_mut().go_first(),
            TabKind::Vulnerabilities => self.vulnerabilities_state_mut().go_first(),
            TabKind::Licenses => self.licenses_state_mut().go_first(),
            TabKind::Source => self.source_state_mut().select_first(),
            _ => {}
        }
    }

    /// Move selection to last item
    pub fn select_last(&mut self) {
        match self.active_tab {
            TabKind::Components => self.components_state_mut().go_last(),
            TabKind::Vulnerabilities => self.vulnerabilities_state_mut().go_last(),
            TabKind::Source => self.source_state_mut().select_last(),
            _ => {}
        }
    }

    /// Page up
    pub fn page_up(&mut self) {
        match self.active_tab {
            TabKind::Components => self.components_state_mut().page_up(),
            TabKind::Vulnerabilities => self.vulnerabilities_state_mut().page_up(),
            TabKind::Source => self.source_state_mut().page_up(),
            _ => {}
        }
    }

    /// Page down
    pub fn page_down(&mut self) {
        match self.active_tab {
            TabKind::Components => self.components_state_mut().page_down(),
            TabKind::Vulnerabilities => self.vulnerabilities_state_mut().page_down(),
            TabKind::Source => self.source_state_mut().page_down(),
            _ => {}
        }
    }

    // ========================================================================
    // Cross-view Navigation
    // ========================================================================

    /// Navigate from vulnerability to the affected component
    pub fn navigate_vuln_to_component(&mut self, vuln_id: &str, component_name: &str) {
        // Save current position as breadcrumb
        let selected = self.vulnerabilities_state().selected;
        self.navigation_ctx.push_breadcrumb(
            TabKind::Vulnerabilities,
            vuln_id.to_string(),
            selected,
        );

        // Set target and switch to components tab
        self.navigation_ctx.target_component = Some(component_name.to_string());
        self.active_tab = TabKind::Components;

        // Try to find and select the component
        self.find_and_select_component(component_name);
    }

    /// Navigate from dependency to the component
    pub fn navigate_dep_to_component(&mut self, dep_name: &str) {
        let dep_name = dep_name
            .split_once(":+:")
            .map(|(_, dep)| dep)
            .or_else(|| dep_name.split_once(":-:").map(|(_, dep)| dep))
            .unwrap_or(dep_name);

        if dep_name.starts_with("__") {
            return;
        }

        // Save current position as breadcrumb
        self.navigation_ctx.push_breadcrumb(
            TabKind::Dependencies,
            dep_name.to_string(),
            self.dependencies_state().selected,
        );

        // Set target and switch to components tab
        self.navigation_ctx.target_component = Some(dep_name.to_string());
        self.active_tab = TabKind::Components;

        // Try to find and select the component
        self.find_and_select_component(dep_name);
    }

    /// Navigate back using breadcrumbs
    pub fn navigate_back(&mut self) -> bool {
        if let Some(breadcrumb) = self.navigation_ctx.pop_breadcrumb() {
            self.active_tab = breadcrumb.tab;

            // Restore selection based on the tab we're returning to
            match breadcrumb.tab {
                TabKind::Vulnerabilities => {
                    self.vulnerabilities_state_mut().selected = breadcrumb.selection_index;
                }
                TabKind::Components => {
                    self.components_state_mut().selected = breadcrumb.selection_index;
                }
                TabKind::Dependencies => {
                    self.dependencies_state_mut().selected = breadcrumb.selection_index;
                }
                TabKind::Licenses => {
                    self.licenses_state_mut().selected = breadcrumb.selection_index;
                }
                TabKind::Source => {
                    self.source_state_mut().active_panel_mut().selected =
                        breadcrumb.selection_index;
                }
                _ => {}
            }

            self.navigation_ctx.clear_targets();
            true
        } else {
            false
        }
    }

    /// Find and select a component by name in the current view
    pub(super) fn find_and_select_component(&mut self, name: &str) {
        if self.data.diff_result.is_some() {
            // Reset filter to All to ensure we can find it
            self.components_state_mut().filter = ComponentFilter::All;

            let name_lower = name.to_lowercase();
            let index = {
                let items = self.diff_component_items(ComponentFilter::All);
                items
                    .iter()
                    .position(|comp| comp.name.to_lowercase() == name_lower)
            };

            if let Some(index) = index {
                self.components_state_mut().selected = index;
            }
        }
    }

    /// Check if we have navigation history
    #[must_use]
    pub fn has_navigation_history(&self) -> bool {
        self.navigation_ctx.has_history()
    }

    /// Get the breadcrumb trail for display
    #[must_use]
    pub fn breadcrumb_trail(&self) -> String {
        self.navigation_ctx.breadcrumb_trail()
    }

    /// Navigate to a target tab or item
    pub(super) fn navigate_to_target(&mut self, target: super::traits::TabTarget) {
        use super::traits::TabTarget;

        // For cross-tab navigation variants, save a breadcrumb so the user can go back
        if matches!(
            target,
            TabTarget::ComponentByName(_)
                | TabTarget::ComponentByLicense(_)
                | TabTarget::VulnerabilityById(_)
        ) {
            let selection_index = self.current_tab_selection_index();
            self.navigation_ctx
                .push_breadcrumb(self.active_tab, String::new(), selection_index);
        }

        match target {
            TabTarget::Summary => self.active_tab = TabKind::Summary,
            TabTarget::Overview => self.active_tab = TabKind::Overview,
            TabTarget::Tree => self.active_tab = TabKind::Tree,
            TabTarget::Components => self.active_tab = TabKind::Components,
            TabTarget::Dependencies => self.active_tab = TabKind::Dependencies,
            TabTarget::Licenses => self.active_tab = TabKind::Licenses,
            TabTarget::Vulnerabilities => self.active_tab = TabKind::Vulnerabilities,
            TabTarget::Quality => self.active_tab = TabKind::Quality,
            TabTarget::Compliance => self.active_tab = TabKind::Compliance,
            TabTarget::SideBySide => self.active_tab = TabKind::SideBySide,
            TabTarget::GraphChanges => self.active_tab = TabKind::GraphChanges,
            TabTarget::Source => self.active_tab = TabKind::Source,
            TabTarget::ComponentByName(name) => {
                self.active_tab = TabKind::Components;
                self.find_and_select_component(&name);
            }
            TabTarget::VulnerabilityById(id) => {
                self.active_tab = TabKind::Vulnerabilities;
                if let Some(idx) = self.find_vulnerability_index(&id) {
                    self.vulnerabilities_state_mut().selected = idx;
                }
            }
            TabTarget::ComponentByLicense(license) => {
                self.active_tab = TabKind::Components;
                self.set_status_message(format!("Showing components with license: {license}"));
            }
        }
    }

    /// Get the selection index for the currently active tab (for breadcrumb saving).
    fn current_tab_selection_index(&self) -> usize {
        match self.active_tab {
            TabKind::Components => self.components_state().selected,
            TabKind::Vulnerabilities => self.vulnerabilities_state().selected,
            TabKind::Licenses => self.licenses_state().selected,
            TabKind::Dependencies => self.dependencies_state().selected,
            TabKind::Compliance => self.compliance_view.inner().selected_violation,
            TabKind::Source => {
                let state = self.source_state();
                match state.active_side {
                    crate::tui::app_states::source::SourceSide::Old => state.old_panel.selected,
                    crate::tui::app_states::source::SourceSide::New => state.new_panel.selected,
                }
            }
            _ => 0,
        }
    }
}