use crate::diff::{DiffResult, MatrixResult, MultiDiffResult, TimelineResult};
#[cfg(feature = "enrichment")]
use crate::enrichment::EnrichmentStats;
use crate::model::{NormalizedSbom, NormalizedSbomIndex};
use crate::quality::{ComplianceResult, QualityReport};
use crate::tui::state::ListNavigation;
use crate::tui::views::ThresholdTuningState;
#[allow(unused_imports)]
pub use super::app_states::{
AlignmentMode,
Breadcrumb,
ChangeType,
ChangeTypeFilter,
ComponentDeepDiveData,
ComponentDeepDiveState,
ComponentFilter,
ComponentSimilarityInfo,
ComponentSort,
ComponentTargetPresence,
ComponentVersionEntry,
ComponentVulnInfo,
ComponentsState,
DependenciesState,
DiffSearchResult,
DiffSearchState,
DiffVulnItem,
DiffVulnStatus,
GraphChangesState,
LicenseGroupBy,
LicenseRiskFilter,
LicenseSort,
LicensesState,
MatrixSortBy,
MatrixState,
MultiDiffState,
MultiViewFilterPreset,
MultiViewSearchState,
MultiViewSortBy,
MultiViewType,
NavigationContext,
QualityState,
QualityViewMode,
ScrollSyncMode,
SearchMode,
ShortcutsContext,
ShortcutsOverlayState,
SideBySideState,
SimilarityThreshold,
SortDirection,
TimelineComponentFilter,
TimelineSortBy,
TimelineState,
ViewSwitcherState,
VulnChangeType,
VulnFilter,
VulnSort,
VulnerabilitiesState,
sort_component_changes,
};
pub struct ModeStates {
pub(crate) multi_diff: MultiDiffState,
pub(crate) timeline: TimelineState,
pub(crate) matrix: MatrixState,
}
pub struct AppOverlays {
pub(crate) show_help: bool,
pub(crate) show_export: bool,
pub(crate) show_legend: bool,
pub(crate) search: DiffSearchState,
pub(crate) threshold_tuning: ThresholdTuningState,
pub(crate) view_switcher: ViewSwitcherState,
pub(crate) shortcuts: ShortcutsOverlayState,
pub(crate) component_deep_dive: ComponentDeepDiveState,
}
impl AppOverlays {
pub fn new() -> Self {
Self {
show_help: false,
show_export: false,
show_legend: false,
search: DiffSearchState::new(),
threshold_tuning: ThresholdTuningState::default(),
view_switcher: ViewSwitcherState::new(),
shortcuts: ShortcutsOverlayState::new(),
component_deep_dive: ComponentDeepDiveState::new(),
}
}
pub const fn toggle_help(&mut self) {
self.show_help = !self.show_help;
if self.show_help {
self.show_export = false;
self.show_legend = false;
}
}
pub const fn toggle_export(&mut self) {
self.show_export = !self.show_export;
if self.show_export {
self.show_help = false;
self.show_legend = false;
}
}
pub const fn toggle_legend(&mut self) {
self.show_legend = !self.show_legend;
if self.show_legend {
self.show_help = false;
self.show_export = false;
}
}
pub const fn close_all(&mut self) {
self.show_help = false;
self.show_export = false;
self.show_legend = false;
self.search.active = false;
self.threshold_tuning.visible = false;
}
pub const fn has_active(&self) -> bool {
self.show_help
|| self.show_export
|| self.show_legend
|| self.search.active
|| self.threshold_tuning.visible
}
}
pub struct DataContext {
pub(crate) diff_result: Option<DiffResult>,
pub(crate) old_sbom: Option<NormalizedSbom>,
pub(crate) new_sbom: Option<NormalizedSbom>,
pub(crate) sbom: Option<NormalizedSbom>,
pub(crate) multi_diff_result: Option<MultiDiffResult>,
pub(crate) timeline_result: Option<TimelineResult>,
pub(crate) matrix_result: Option<MatrixResult>,
pub(crate) old_sbom_index: Option<NormalizedSbomIndex>,
pub(crate) new_sbom_index: Option<NormalizedSbomIndex>,
pub(crate) sbom_index: Option<NormalizedSbomIndex>,
pub(crate) old_quality: Option<QualityReport>,
pub(crate) new_quality: Option<QualityReport>,
pub(crate) quality_report: Option<QualityReport>,
pub(crate) old_cra_compliance: Option<ComplianceResult>,
pub(crate) new_cra_compliance: Option<ComplianceResult>,
pub(crate) old_compliance_results: Option<Vec<ComplianceResult>>,
pub(crate) new_compliance_results: Option<Vec<ComplianceResult>>,
pub(crate) matching_threshold: f64,
#[cfg(feature = "enrichment")]
pub(crate) enrichment_stats_old: Option<EnrichmentStats>,
#[cfg(feature = "enrichment")]
pub(crate) enrichment_stats_new: Option<EnrichmentStats>,
}
pub struct App {
pub(crate) mode: AppMode,
pub(crate) active_tab: TabKind,
pub(crate) data: DataContext,
pub(crate) tabs: ModeStates,
pub(crate) overlays: AppOverlays,
pub(crate) should_quit: bool,
pub(crate) status_message: Option<String>,
pub(crate) status_sticky: bool,
pub(crate) tick: u64,
pub(crate) last_export_path: Option<String>,
pub(crate) navigation_ctx: NavigationContext,
pub(crate) security_cache: crate::tui::security::SecurityAnalysisCache,
pub(crate) compliance_state: crate::tui::app_states::PolicyComplianceState,
pub(crate) export_template: Option<String>,
pub(crate) components_view: crate::tui::view_states::ComponentsView,
pub(crate) dependencies_view: crate::tui::view_states::DependenciesView,
pub(crate) licenses_view: crate::tui::view_states::LicensesView,
pub(crate) vulnerabilities_view: crate::tui::view_states::VulnerabilitiesView,
pub(crate) quality_view: crate::tui::view_states::QualityView,
pub(crate) compliance_view: crate::tui::view_states::ComplianceView,
pub(crate) sidebyside_view: crate::tui::view_states::SideBySideView,
pub(crate) graph_changes_view: crate::tui::view_states::GraphChangesView,
pub(crate) source_view: crate::tui::view_states::SourceView,
}
impl App {
pub fn ensure_compliance_results(&mut self) {
if self.data.old_compliance_results.is_none()
&& let Some(old_sbom) = &self.data.old_sbom
{
self.data.old_compliance_results = Some(
crate::quality::ComplianceLevel::all()
.iter()
.map(|level| crate::quality::ComplianceChecker::new(*level).check(old_sbom))
.collect(),
);
}
if self.data.new_compliance_results.is_none()
&& let Some(new_sbom) = &self.data.new_sbom
{
self.data.new_compliance_results = Some(
crate::quality::ComplianceLevel::all()
.iter()
.map(|level| crate::quality::ComplianceChecker::new(*level).check(new_sbom))
.collect(),
);
}
}
pub const fn toggle_help(&mut self) {
self.overlays.toggle_help();
}
pub const fn toggle_export(&mut self) {
self.overlays.toggle_export();
}
pub const fn toggle_legend(&mut self) {
self.overlays.toggle_legend();
}
pub const fn close_overlays(&mut self) {
self.overlays.close_all();
}
#[must_use]
pub const fn has_overlay(&self) -> bool {
self.overlays.has_active()
}
pub fn toggle_threshold_tuning(&mut self) {
if self.overlays.threshold_tuning.visible {
self.overlays.threshold_tuning.visible = false;
} else {
self.show_threshold_tuning();
}
}
pub fn show_threshold_tuning(&mut self) {
self.overlays.close_all();
let total = match self.mode {
AppMode::Diff => {
self.data
.old_sbom
.as_ref()
.map_or(0, crate::model::NormalizedSbom::component_count)
+ self
.data
.new_sbom
.as_ref()
.map_or(0, crate::model::NormalizedSbom::component_count)
}
_ => 0,
};
self.overlays.threshold_tuning =
ThresholdTuningState::new(self.data.matching_threshold, total);
self.update_threshold_preview();
}
pub fn update_threshold_preview(&mut self) {
if !self.overlays.threshold_tuning.visible {
return;
}
let estimated = if let Some(ref result) = self.data.diff_result {
let current_matches = result.components.modified.len();
let threshold = self.overlays.threshold_tuning.threshold;
let base_threshold = self.data.matching_threshold;
let ratio = if threshold < base_threshold {
(base_threshold - threshold).mul_add(2.0, 1.0)
} else {
(threshold - base_threshold).mul_add(-1.5, 1.0)
};
((current_matches as f64 * ratio).max(0.0)) as usize
} else {
0
};
self.overlays
.threshold_tuning
.set_estimated_matches(estimated);
}
pub fn apply_threshold(&mut self) {
self.data.matching_threshold = self.overlays.threshold_tuning.threshold;
self.overlays.threshold_tuning.visible = false;
self.set_status_message(format!(
"Threshold set to {:.0}% - Re-run diff to apply",
self.data.matching_threshold * 100.0
));
}
pub fn set_status_message(&mut self, msg: impl Into<String>) {
self.status_message = Some(msg.into());
}
pub fn clear_status_message(&mut self) {
if self.status_sticky {
self.status_sticky = false;
} else {
self.status_message = None;
}
}
pub fn export(&mut self, format: super::export::ExportFormat) {
use super::export::{
export_diff, export_view, tab_to_report_type, view_tab_to_report_type,
};
use crate::reports::ReportConfig;
let result = match self.mode {
AppMode::Diff => {
let report_type = tab_to_report_type(self.active_tab);
let config = ReportConfig::with_types(vec![report_type]);
if let (Some(diff_result), Some(old_sbom), Some(new_sbom)) = (
&self.data.diff_result,
&self.data.old_sbom,
&self.data.new_sbom,
) {
export_diff(
format,
diff_result,
old_sbom,
new_sbom,
None,
&config,
self.export_template.as_deref(),
)
} else {
self.set_status_message("No diff data to export");
return;
}
}
AppMode::View => {
let report_type = match self.active_tab {
super::TabKind::Tree => view_tab_to_report_type(crate::tui::ViewTab::Tree),
super::TabKind::Vulnerabilities => {
view_tab_to_report_type(crate::tui::ViewTab::Vulnerabilities)
}
super::TabKind::Licenses => {
view_tab_to_report_type(crate::tui::ViewTab::Licenses)
}
super::TabKind::Dependencies => {
view_tab_to_report_type(crate::tui::ViewTab::Dependencies)
}
_ => view_tab_to_report_type(crate::tui::ViewTab::Overview),
};
let config = ReportConfig::with_types(vec![report_type]);
if let Some(ref sbom) = self.data.sbom {
export_view(format, sbom, None, &config, self.export_template.as_deref())
} else {
self.set_status_message("No SBOM data available for export");
return;
}
}
_ => {
self.set_status_message("Export not supported for this mode");
return;
}
};
if result.success {
self.last_export_path = Some(result.path.display().to_string());
self.set_status_message(result.message);
self.status_sticky = true;
} else {
self.set_status_message(format!("Export failed: {}", result.message));
}
}
pub fn export_compliance(&mut self, format: super::export::ExportFormat) {
use super::export::export_compliance;
self.ensure_compliance_results();
let selected_standard = self.diff_compliance_state().selected_standard;
let (results, selected) = if let Some(ref results) = self.data.new_compliance_results {
if !results.is_empty() {
(results, selected_standard)
} else if let Some(ref old_results) = self.data.old_compliance_results {
if old_results.is_empty() {
self.set_status_message("No compliance results to export");
return;
}
(old_results, selected_standard)
} else {
self.set_status_message("No compliance results to export");
return;
}
} else if let Some(ref old_results) = self.data.old_compliance_results {
if old_results.is_empty() {
self.set_status_message("No compliance results to export");
return;
}
(old_results, selected_standard)
} else {
self.set_status_message("No compliance results to export");
return;
};
let result = export_compliance(
format,
results,
selected,
None,
self.export_template.as_deref(),
);
if result.success {
self.last_export_path = Some(result.path.display().to_string());
self.set_status_message(result.message);
self.status_sticky = true;
} else {
self.set_status_message(format!("Export failed: {}", result.message));
}
}
pub fn export_matrix(&mut self, format: super::export::ExportFormat) {
use super::export::export_matrix;
let Some(ref matrix_result) = self.data.matrix_result else {
self.set_status_message("No matrix data to export");
return;
};
let result = export_matrix(format, matrix_result, self.export_template.as_deref());
if result.success {
self.last_export_path = Some(result.path.display().to_string());
self.set_status_message(result.message);
self.status_sticky = true;
} else {
self.set_status_message(format!("Export failed: {}", result.message));
}
}
pub fn run_compliance_check(&mut self) {
use crate::tui::security::{SecurityPolicy, check_compliance};
let preset = self.compliance_state.policy_preset;
if preset.is_standards_based() {
self.run_standards_compliance_check(preset);
return;
}
let policy = match preset {
super::app_states::PolicyPreset::Enterprise => SecurityPolicy::enterprise_default(),
super::app_states::PolicyPreset::Strict => SecurityPolicy::strict(),
super::app_states::PolicyPreset::Permissive => SecurityPolicy::permissive(),
_ => unreachable!(),
};
let components = self.collect_compliance_data();
if components.is_empty() {
self.set_status_message("No components to check");
return;
}
let result = check_compliance(&policy, &components);
let passes = result.passes;
let score = result.score;
let violation_count = result.violations.len();
self.compliance_state.result = Some(result);
self.compliance_state.checked = true;
self.compliance_state.selected_violation = 0;
if passes {
self.set_status_message(format!("Policy: {} - PASS (score: {})", policy.name, score));
} else {
self.set_status_message(format!(
"Policy: {} - FAIL ({} violations, score: {})",
policy.name, violation_count, score
));
}
}
fn run_standards_compliance_check(&mut self, preset: super::app_states::PolicyPreset) {
use crate::quality::{ComplianceChecker, ViolationSeverity};
use crate::tui::security::{
ComplianceResult as PolicyResult, PolicySeverity, PolicyViolation,
};
let Some(level) = preset.compliance_level() else {
return;
};
let sbom = match self.mode {
AppMode::Diff => self.data.new_sbom.as_ref(),
_ => self.data.sbom.as_ref(),
};
let Some(sbom) = sbom else {
self.set_status_message("No SBOM loaded to check");
return;
};
let checker = ComplianceChecker::new(level);
let std_result = checker.check(sbom);
let violations: Vec<PolicyViolation> = std_result
.violations
.iter()
.map(|v| {
let severity = match v.severity {
ViolationSeverity::Error => PolicySeverity::High,
ViolationSeverity::Warning => PolicySeverity::Medium,
ViolationSeverity::Info => PolicySeverity::Low,
};
PolicyViolation {
rule_name: v.requirement.clone(),
severity,
component: v.element.clone(),
description: v.message.clone(),
remediation: v.remediation_guidance().to_string(),
}
})
.collect();
let penalty: u32 = violations
.iter()
.map(|v| match v.severity {
PolicySeverity::High | PolicySeverity::Critical => 10,
PolicySeverity::Medium => 5,
PolicySeverity::Low => 1,
})
.sum();
let score = 100u8.saturating_sub(penalty.min(100) as u8);
let passes = std_result.is_compliant;
let policy_name = format!("{} Compliance", preset.label());
let violation_count = violations.len();
let result = PolicyResult {
policy_name: policy_name.clone(),
components_checked: sbom.components.len(),
violations,
score,
passes,
};
self.compliance_state.result = Some(result);
self.compliance_state.checked = true;
self.compliance_state.selected_violation = 0;
if passes {
self.set_status_message(format!("{policy_name} - COMPLIANT (score: {score})"));
} else {
self.set_status_message(format!(
"{policy_name} - NON-COMPLIANT ({violation_count} violations, score: {score})"
));
}
}
fn collect_compliance_data(&self) -> Vec<crate::tui::security::ComplianceComponentData> {
let mut components = Vec::new();
if self.mode == AppMode::Diff
&& let Some(sbom) = &self.data.new_sbom
{
for comp in sbom.components.values() {
let licenses: Vec<String> = comp
.licenses
.declared
.iter()
.map(std::string::ToString::to_string)
.collect();
let vulns: Vec<(String, String)> = comp
.vulnerabilities
.iter()
.map(|v| {
let severity = v.severity.as_ref().map_or_else(
|| "Unknown".to_string(),
std::string::ToString::to_string,
);
(v.id.clone(), severity)
})
.collect();
components.push((comp.name.clone(), comp.version.clone(), licenses, vulns));
}
}
components
}
pub const fn toggle_compliance_details(&mut self) {
self.compliance_state.toggle_details();
}
pub fn next_policy(&mut self) {
self.compliance_state.toggle_policy();
if self.compliance_state.checked {
self.run_compliance_check();
}
}
#[must_use]
pub const fn view_mode(&self) -> super::traits::ViewMode {
super::traits::ViewMode::from_app_mode(self.mode)
}
pub fn handle_event_result(&mut self, result: super::traits::EventResult) {
use super::traits::EventResult;
match result {
EventResult::Consumed | EventResult::Ignored => {
}
EventResult::NavigateTo(target) => {
self.navigate_to_target(target);
}
EventResult::Exit => {
self.should_quit = true;
}
EventResult::ShowOverlay(kind) => {
self.show_overlay_kind(&kind);
}
EventResult::StatusMessage(msg) => {
self.set_status_message(msg);
}
}
}
fn show_overlay_kind(&mut self, kind: &super::traits::OverlayKind) {
use super::traits::OverlayKind;
self.overlays.close_all();
match kind {
OverlayKind::Help => self.overlays.show_help = true,
OverlayKind::Export => self.overlays.show_export = true,
OverlayKind::Legend => self.overlays.show_legend = true,
OverlayKind::Search => {
self.overlays.search.active = true;
self.overlays.search.query.clear();
}
OverlayKind::Shortcuts => self.overlays.shortcuts.visible = true,
}
}
#[must_use]
pub const fn current_tab_target(&self) -> super::traits::TabTarget {
super::traits::TabTarget::from_tab_kind(self.active_tab)
}
#[must_use]
pub fn current_shortcuts(&self) -> Vec<super::traits::Shortcut> {
use super::traits::Shortcut;
let mut shortcuts = vec![
Shortcut::primary("?", "Help"),
Shortcut::primary("q", "Quit"),
Shortcut::primary("Tab", "Next tab"),
Shortcut::primary("/", "Search"),
];
match self.active_tab {
TabKind::Components => {
shortcuts.push(Shortcut::new("f", "Filter"));
shortcuts.push(Shortcut::new("s", "Sort"));
shortcuts.push(Shortcut::new("m", "Multi-select"));
}
TabKind::Dependencies => {
shortcuts.push(Shortcut::new("t", "Transitive"));
shortcuts.push(Shortcut::new("+/-", "Depth"));
}
TabKind::Vulnerabilities => {
shortcuts.push(Shortcut::new("f", "Filter"));
shortcuts.push(Shortcut::new("s", "Sort"));
}
TabKind::Quality => {
shortcuts.push(Shortcut::new("v", "View mode"));
}
_ => {}
}
shortcuts
}
pub(crate) fn quality_state(&self) -> &super::app_states::QualityState {
self.quality_view.inner()
}
pub(crate) fn quality_state_mut(&mut self) -> &mut super::app_states::QualityState {
self.quality_view.inner_mut()
}
pub(crate) fn graph_changes_state(&self) -> &super::app_states::GraphChangesState {
self.graph_changes_view.inner()
}
pub(crate) fn graph_changes_state_mut(&mut self) -> &mut super::app_states::GraphChangesState {
self.graph_changes_view.inner_mut()
}
pub(crate) fn licenses_state(&self) -> &super::app_states::LicensesState {
self.licenses_view.inner()
}
pub(crate) fn licenses_state_mut(&mut self) -> &mut super::app_states::LicensesState {
self.licenses_view.inner_mut()
}
pub(crate) fn diff_compliance_state(&self) -> &super::app_states::DiffComplianceState {
self.compliance_view.inner()
}
pub(crate) fn components_state(&self) -> &ComponentsState {
self.components_view.inner()
}
pub(crate) fn components_state_mut(&mut self) -> &mut ComponentsState {
self.components_view.inner_mut()
}
pub(crate) fn vulnerabilities_state(&self) -> &super::app_states::VulnerabilitiesState {
self.vulnerabilities_view.inner()
}
pub(crate) fn vulnerabilities_state_mut(
&mut self,
) -> &mut super::app_states::VulnerabilitiesState {
self.vulnerabilities_view.inner_mut()
}
pub(crate) fn side_by_side_state(&self) -> &super::app_states::SideBySideState {
self.sidebyside_view.inner()
}
pub(crate) fn side_by_side_state_mut(&mut self) -> &mut super::app_states::SideBySideState {
self.sidebyside_view.inner_mut()
}
pub(crate) fn dependencies_state(&self) -> &DependenciesState {
self.dependencies_view.inner()
}
pub(crate) fn dependencies_state_mut(&mut self) -> &mut DependenciesState {
self.dependencies_view.inner_mut()
}
pub(crate) fn source_state(&self) -> &crate::tui::app_states::SourceDiffState {
self.source_view.inner()
}
pub(crate) fn source_state_mut(&mut self) -> &mut crate::tui::app_states::SourceDiffState {
self.source_view.inner_mut()
}
pub fn prepare_render(&mut self) {
super::views::update_graph_cache(self.dependencies_view.inner_mut(), &self.data, self.mode);
self.ensure_compliance_results();
if matches!(self.mode, AppMode::Diff | AppMode::View) {
self.ensure_vulnerability_cache();
}
let comp_filter = self.components_state().filter;
let comp_total = match self.mode {
AppMode::Diff | AppMode::View => self.diff_component_count(comp_filter),
AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => 0,
};
self.components_state_mut().total = comp_total;
self.components_state_mut().clamp_selection();
self.prepare_vulnerability_totals();
let graph_total = self
.data
.diff_result
.as_ref()
.map_or(0, |r| r.graph_changes.len());
self.graph_changes_state_mut().set_total(graph_total);
self.dependencies_state_mut().update_breadcrumbs();
self.prepare_license_totals();
if matches!(self.mode, AppMode::Diff | AppMode::View)
&& let Some(ref result) = self.data.diff_result
{
let left = result.components.removed.len() + result.components.modified.len();
let right = result.components.added.len() + result.components.modified.len();
self.side_by_side_state_mut().set_totals(left, right);
}
let rec_total = self
.data
.new_quality
.as_ref()
.or(self.data.old_quality.as_ref())
.map_or(0, |r| r.recommendations.len());
self.quality_state_mut().total_recommendations = rec_total;
}
fn prepare_license_totals(&mut self) {
match self.mode {
AppMode::Diff | AppMode::View => {
if let Some(ref result) = self.data.diff_result {
let focus_left = self.licenses_state().focus_left;
let risk_filter = self.licenses_state().risk_filter;
let count = if focus_left {
Self::filtered_license_count(&result.licenses.new_licenses, risk_filter)
} else {
Self::filtered_license_count(&result.licenses.removed_licenses, risk_filter)
};
self.licenses_state_mut().total = count;
}
}
AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => {
self.licenses_state_mut().total = 0;
}
}
self.licenses_state_mut().clamp_selection();
}
fn filtered_license_count(
licenses: &[crate::diff::LicenseChange],
risk_filter: Option<crate::tui::app_states::LicenseRiskFilter>,
) -> usize {
use crate::tui::license_utils::{LicenseInfo, RiskLevel};
if let Some(min_risk) = risk_filter {
let min_level = match min_risk {
crate::tui::app_states::LicenseRiskFilter::Low => RiskLevel::Low,
crate::tui::app_states::LicenseRiskFilter::Medium => RiskLevel::Medium,
crate::tui::app_states::LicenseRiskFilter::High => RiskLevel::High,
crate::tui::app_states::LicenseRiskFilter::Critical => RiskLevel::Critical,
};
licenses
.iter()
.filter(|l| LicenseInfo::from_spdx(&l.license).risk_level >= min_level)
.count()
} else {
licenses.len()
}
}
fn prepare_vulnerability_totals(&mut self) {
let vuln_total = match self.mode {
AppMode::Diff | AppMode::View => self.diff_vulnerability_count(),
AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => 0,
};
self.vulnerabilities_state_mut().total = vuln_total;
self.vulnerabilities_state_mut().clamp_selection();
if self.vulnerabilities_state().group_by_component {
let grouped_count = super::views::count_grouped_items(self);
self.vulnerabilities_state_mut().total = grouped_count;
self.vulnerabilities_state_mut().clamp_selection();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode {
Diff,
View,
MultiDiff,
Timeline,
Matrix,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabKind {
Summary,
Overview,
Tree,
Components,
Dependencies,
Licenses,
Vulnerabilities,
Quality,
Compliance,
SideBySide,
GraphChanges,
Source,
}
impl TabKind {
#[must_use]
pub const fn title(&self) -> &'static str {
match self {
Self::Summary => "Summary",
Self::Overview => "Overview",
Self::Tree => "Tree",
Self::Components => "Components",
Self::Dependencies => "Dependencies",
Self::Licenses => "Licenses",
Self::Vulnerabilities => "Vulnerabilities",
Self::Quality => "Quality",
Self::Compliance => "Compliance",
Self::SideBySide => "Side-by-Side",
Self::GraphChanges => "Graph",
Self::Source => "Source",
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Summary => "summary",
Self::Overview => "overview",
Self::Tree => "tree",
Self::Components => "components",
Self::Dependencies => "dependencies",
Self::Licenses => "licenses",
Self::Vulnerabilities => "vulnerabilities",
Self::Quality => "quality",
Self::Compliance => "compliance",
Self::SideBySide => "side-by-side",
Self::GraphChanges => "graph",
Self::Source => "source",
}
}
#[must_use]
pub fn from_str_opt(s: &str) -> Option<Self> {
match s {
"summary" => Some(Self::Summary),
"overview" => Some(Self::Overview),
"tree" => Some(Self::Tree),
"components" => Some(Self::Components),
"dependencies" => Some(Self::Dependencies),
"licenses" => Some(Self::Licenses),
"vulnerabilities" => Some(Self::Vulnerabilities),
"quality" => Some(Self::Quality),
"compliance" => Some(Self::Compliance),
"side-by-side" => Some(Self::SideBySide),
"graph" => Some(Self::GraphChanges),
"source" => Some(Self::Source),
_ => None,
}
}
#[must_use]
pub const fn tabs_for_mode(mode: AppMode) -> &'static [TabKind] {
match mode {
AppMode::View => &[
TabKind::Overview,
TabKind::Tree,
TabKind::Vulnerabilities,
TabKind::Licenses,
TabKind::Dependencies,
TabKind::Quality,
TabKind::Compliance,
TabKind::Source,
],
AppMode::Diff => &[
TabKind::Summary,
TabKind::Components,
TabKind::Dependencies,
TabKind::Licenses,
TabKind::Vulnerabilities,
TabKind::Quality,
TabKind::Compliance,
TabKind::SideBySide,
TabKind::GraphChanges,
TabKind::Source,
],
AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => &[],
}
}
}