sbom-tools 0.1.18

Semantic SBOM diff and analysis tool
Documentation
//! Compliance tab `ViewState` implementation.

use crate::tui::app_states::compliance::DiffComplianceState;
use crate::tui::traits::{EventResult, Shortcut, ViewContext, ViewState};
use crossterm::event::{KeyCode, KeyEvent, MouseEvent};

/// Compliance tab view implementing the `ViewState` trait.
///
/// Wraps `DiffComplianceState` for multi-standard navigation and
/// violation selection. Export and violation count resolution
/// remain in the sync bridge since they need `App` data access.
pub struct ComplianceView {
    inner: DiffComplianceState,
    /// Maximum number of violations in the current view (set by bridge).
    max_violations: usize,
    /// Whether a compliance export was requested (consumed by bridge).
    export_requested: bool,
    /// Whether a "go to component" navigation was requested (consumed by bridge).
    go_to_component_requested: bool,
    /// Whether a group toggle was requested via Enter on a grouped header (consumed by bridge).
    toggle_group_entry_requested: bool,
}

impl ComplianceView {
    pub(crate) fn new() -> Self {
        Self {
            inner: DiffComplianceState::new(),
            max_violations: 0,
            export_requested: false,
            go_to_component_requested: false,
            toggle_group_entry_requested: false,
        }
    }

    /// Access the inner state.
    pub(crate) const fn inner(&self) -> &DiffComplianceState {
        &self.inner
    }

    /// Access the inner state mutably.
    pub(crate) fn inner_mut(&mut self) -> &mut DiffComplianceState {
        &mut self.inner
    }

    pub(crate) fn set_max_violations(&mut self, max: usize) {
        self.max_violations = max;
    }

    /// Whether a compliance export was requested (set during `handle_key`, consumed by bridge).
    pub(crate) fn take_export_request(&mut self) -> bool {
        let req = self.export_requested;
        self.export_requested = false;
        req
    }

    /// Whether a "go to component" navigation was requested (consumed by bridge).
    pub(crate) fn take_go_to_component_request(&mut self) -> bool {
        let req = self.go_to_component_requested;
        self.go_to_component_requested = false;
        req
    }

    /// Whether a group header toggle was requested via Enter (consumed by bridge).
    pub(crate) fn take_toggle_group_entry_request(&mut self) -> bool {
        let req = self.toggle_group_entry_requested;
        self.toggle_group_entry_requested = false;
        req
    }
}

impl Default for ComplianceView {
    fn default() -> Self {
        Self::new()
    }
}

impl ViewState for ComplianceView {
    fn handle_key(&mut self, key: KeyEvent, _ctx: &mut ViewContext) -> EventResult {
        // Detail overlay mode
        if self.inner.show_detail {
            match key.code {
                KeyCode::Esc | KeyCode::Enter => {
                    self.inner.show_detail = false;
                    return EventResult::Consumed;
                }
                _ => return EventResult::Consumed,
            }
        }

        match key.code {
            KeyCode::Left | KeyCode::Char('h') => {
                self.inner.prev_standard();
                EventResult::Consumed
            }
            KeyCode::Right | KeyCode::Char('l') => {
                self.inner.next_standard();
                EventResult::Consumed
            }
            KeyCode::Up | KeyCode::Char('k') => {
                self.inner.select_prev();
                EventResult::Consumed
            }
            KeyCode::Down | KeyCode::Char('j') => {
                self.inner.select_next(self.max_violations);
                EventResult::Consumed
            }
            KeyCode::Enter => {
                if self.max_violations > 0 {
                    if self.inner.group_by_element {
                        // In grouped mode, Enter toggles group expand/collapse
                        // (the bridge resolves which group is selected)
                        self.toggle_group_entry_requested = true;
                    } else {
                        self.inner.show_detail = true;
                    }
                }
                EventResult::Consumed
            }
            KeyCode::Char('f') => {
                self.inner.toggle_severity_filter();
                let label = self.inner.severity_filter.label();
                EventResult::status(format!("Compliance filter: {label}"))
            }
            KeyCode::Char('g') => {
                self.inner.toggle_group_by_element();
                EventResult::status(if self.inner.group_by_element {
                    "Violations grouped by component"
                } else {
                    "Flat violation list"
                })
            }
            KeyCode::Char('v') => {
                self.inner.next_view_mode();
                let mode_label = match self.inner.view_mode {
                    crate::tui::app_states::compliance::DiffComplianceViewMode::Overview => "Overview",
                    crate::tui::app_states::compliance::DiffComplianceViewMode::NewViolations => "New violations",
                    crate::tui::app_states::compliance::DiffComplianceViewMode::ResolvedViolations => "Resolved violations",
                    crate::tui::app_states::compliance::DiffComplianceViewMode::OldViolations => "Old SBOM violations",
                    crate::tui::app_states::compliance::DiffComplianceViewMode::NewSbomViolations => "New SBOM violations",
                };
                EventResult::status(format!("View: {mode_label}"))
            }
            KeyCode::Char('c') => {
                // Navigate to the component referenced by the selected violation.
                // The bridge resolves the component name from the violation data.
                if self.max_violations > 0 {
                    self.go_to_component_requested = true;
                }
                EventResult::Consumed
            }
            KeyCode::Char('E') => {
                self.export_requested = true;
                EventResult::Consumed
            }
            KeyCode::Home => {
                self.inner.selected_violation = 0;
                EventResult::Consumed
            }
            KeyCode::End | KeyCode::Char('G') => {
                if self.max_violations > 0 {
                    self.inner.selected_violation = self.max_violations - 1;
                }
                EventResult::Consumed
            }
            KeyCode::PageUp => {
                for _ in 0..crate::tui::constants::PAGE_SIZE {
                    self.inner.select_prev();
                }
                EventResult::Consumed
            }
            KeyCode::PageDown => {
                for _ in 0..crate::tui::constants::PAGE_SIZE {
                    self.inner.select_next(self.max_violations);
                }
                EventResult::Consumed
            }
            _ => EventResult::Ignored,
        }
    }

    fn handle_mouse(&mut self, _mouse: MouseEvent, _ctx: &mut ViewContext) -> EventResult {
        EventResult::Ignored
    }

    fn title(&self) -> &'static str {
        "Compliance"
    }

    fn shortcuts(&self) -> Vec<Shortcut> {
        vec![
            Shortcut::primary("j/k", "Navigate"),
            Shortcut::new("h/l", "Switch standard"),
            Shortcut::new("v", "View mode"),
            Shortcut::new("Enter", "Detail"),
            Shortcut::new("g", "Group"),
            Shortcut::new("c", "Go to component"),
            Shortcut::new("E", "Export"),
            Shortcut::new("G", "Last"),
        ]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tui::traits::ViewMode;
    use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};

    fn make_key(code: KeyCode) -> KeyEvent {
        KeyEvent::new(code, KeyModifiers::NONE)
    }

    fn make_ctx() -> ViewContext<'static> {
        let status: &'static mut Option<String> = Box::leak(Box::new(None));
        ViewContext {
            mode: ViewMode::Diff,
            focused: true,
            width: 80,
            height: 24,
            tick: 0,
            status_message: status,
        }
    }

    #[test]
    fn test_standard_navigation() {
        let mut view = ComplianceView::new();
        let mut ctx = make_ctx();
        let initial = view.inner().selected_standard;

        view.handle_key(make_key(KeyCode::Right), &mut ctx);
        assert_ne!(view.inner().selected_standard, initial);
    }

    #[test]
    fn test_violation_navigation() {
        let mut view = ComplianceView::new();
        view.set_max_violations(5);
        let mut ctx = make_ctx();

        view.handle_key(make_key(KeyCode::Down), &mut ctx);
        assert_eq!(view.inner().selected_violation, 1);

        view.handle_key(make_key(KeyCode::Up), &mut ctx);
        assert_eq!(view.inner().selected_violation, 0);
    }

    #[test]
    fn test_detail_toggle() {
        let mut view = ComplianceView::new();
        view.set_max_violations(3);
        let mut ctx = make_ctx();

        assert!(!view.inner().show_detail);
        view.handle_key(make_key(KeyCode::Enter), &mut ctx);
        assert!(view.inner().show_detail);

        view.handle_key(make_key(KeyCode::Esc), &mut ctx);
        assert!(!view.inner().show_detail);
    }

    #[test]
    fn test_export_request() {
        let mut view = ComplianceView::new();
        let mut ctx = make_ctx();

        view.handle_key(make_key(KeyCode::Char('E')), &mut ctx);
        assert!(view.take_export_request());
        assert!(!view.take_export_request()); // consumed
    }

    #[test]
    fn test_go_to_component_request() {
        let mut view = ComplianceView::new();
        view.set_max_violations(3);
        let mut ctx = make_ctx();

        view.handle_key(make_key(KeyCode::Char('c')), &mut ctx);
        assert!(view.take_go_to_component_request());
        assert!(!view.take_go_to_component_request()); // consumed
    }

    #[test]
    fn test_go_to_component_ignored_when_no_violations() {
        let mut view = ComplianceView::new();
        // max_violations is 0
        let mut ctx = make_ctx();

        view.handle_key(make_key(KeyCode::Char('c')), &mut ctx);
        assert!(!view.take_go_to_component_request());
    }
}