Skip to main content

sbom_tools/tui/
app_impl_constructors.rs

1//! Constructor methods for App.
2
3use super::app::{App, AppMode, AppOverlays, DataContext, ModeStates, TabKind};
4use super::app_states::{
5    MatrixState, MultiDiffState, NavigationContext, SourceDiffState, TimelineState,
6};
7use crate::diff::{DiffResult, MatrixResult, MultiDiffResult, TimelineResult};
8use crate::model::NormalizedSbom;
9use crate::quality::{ComplianceChecker, ComplianceLevel, QualityScorer};
10
11impl App {
12    /// Shared default initialization for all mode-independent fields.
13    /// Mode-specific fields (mode, `active_tab`, and data fields) must be set by the caller.
14    fn base(mode: AppMode) -> Self {
15        Self {
16            mode,
17            active_tab: crate::config::TuiPreferences::load()
18                .last_tab
19                .as_deref()
20                .and_then(TabKind::from_str_opt)
21                .unwrap_or(TabKind::Summary),
22            data: DataContext {
23                diff_result: None,
24                old_sbom: None,
25                new_sbom: None,
26                sbom: None,
27                multi_diff_result: None,
28                timeline_result: None,
29                matrix_result: None,
30                old_sbom_index: None,
31                new_sbom_index: None,
32                sbom_index: None,
33                old_quality: None,
34                new_quality: None,
35                quality_report: None,
36                old_cra_compliance: None,
37                new_cra_compliance: None,
38                old_compliance_results: None,
39                new_compliance_results: None,
40                cra_sidecar: None,
41                matching_threshold: 0.85,
42                #[cfg(feature = "enrichment")]
43                enrichment_stats_old: None,
44                #[cfg(feature = "enrichment")]
45                enrichment_stats_new: None,
46            },
47            tabs: ModeStates {
48                multi_diff: MultiDiffState::new(),
49                timeline: TimelineState::new(),
50                matrix: MatrixState::new(),
51            },
52            overlays: AppOverlays::new(),
53            should_quit: false,
54            status_message: None,
55            status_sticky: false,
56            tick: 0,
57            last_export_path: None,
58            navigation_ctx: NavigationContext::new(),
59            security_cache: crate::tui::security::SecurityAnalysisCache::new(),
60            compliance_state: crate::tui::app_states::PolicyComplianceState::new(),
61            export_template: None,
62            components_view: crate::tui::view_states::ComponentsView::new(),
63            dependencies_view: crate::tui::view_states::DependenciesView::new(),
64            licenses_view: crate::tui::view_states::LicensesView::new(),
65            vulnerabilities_view: crate::tui::view_states::VulnerabilitiesView::new(),
66            quality_view: crate::tui::view_states::QualityView::new(),
67            compliance_view: crate::tui::view_states::ComplianceView::new(),
68            sidebyside_view: crate::tui::view_states::SideBySideView::new(),
69            graph_changes_view: crate::tui::view_states::GraphChangesView::new(),
70            source_view: crate::tui::view_states::SourceView::new(),
71        }
72    }
73
74    /// Create a new app for diff mode
75    #[must_use]
76    pub fn new_diff(
77        diff_result: DiffResult,
78        old_sbom: NormalizedSbom,
79        new_sbom: NormalizedSbom,
80        old_raw: &str,
81        new_raw: &str,
82    ) -> Self {
83        // Calculate quality reports for both SBOMs using profile-aware scoring
84        let old_profile =
85            crate::tui::scoring_profile_for(crate::model::BomProfile::detect(&old_sbom));
86        let old_scorer = QualityScorer::new(old_profile);
87        let old_quality = Some(old_scorer.score(&old_sbom));
88
89        let new_profile =
90            crate::tui::scoring_profile_for(crate::model::BomProfile::detect(&new_sbom));
91        let new_scorer = QualityScorer::new(new_profile);
92        let new_quality = Some(new_scorer.score(&new_sbom));
93
94        // Compute only CRA Phase2 for the summary card; full compliance is lazy
95        let old_cra_compliance =
96            Some(ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&old_sbom));
97        let new_cra_compliance =
98            Some(ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&new_sbom));
99
100        // Build indexes for fast lookups (O(1) instead of O(n))
101        let old_sbom_index = Some(old_sbom.build_index());
102        let new_sbom_index = Some(new_sbom.build_index());
103
104        let mut app = Self::base(AppMode::Diff);
105        let mut source = SourceDiffState::new(old_raw, new_raw);
106        source.populate_annotations(&diff_result);
107        app.source_view = crate::tui::view_states::SourceView::with_state(source);
108        app.data.diff_result = Some(diff_result);
109        app.data.old_sbom = Some(old_sbom);
110        app.data.new_sbom = Some(new_sbom);
111        app.data.old_quality = old_quality;
112        app.data.new_quality = new_quality;
113        app.data.old_cra_compliance = old_cra_compliance;
114        app.data.new_cra_compliance = new_cra_compliance;
115        app.data.old_sbom_index = old_sbom_index;
116        app.data.new_sbom_index = new_sbom_index;
117        app
118    }
119
120    /// Attach CRA sidecar metadata so the compliance tab renders sidecar-driven
121    /// verdicts (EU AI Act high-risk escalation, OSS-Steward, EUCC, Article 14)
122    /// identically to the CLI. Invalidates any cached compliance results so the
123    /// sidecar takes effect on the next `ensure_compliance_results`.
124    #[must_use]
125    pub fn with_cra_sidecar(mut self, sidecar: crate::model::CraSidecarMetadata) -> Self {
126        self.data.old_compliance_results = None;
127        self.data.new_compliance_results = None;
128        self.data.cra_sidecar = Some(sidecar);
129        self
130    }
131
132    /// Set enrichment statistics for the diff mode
133    #[must_use]
134    #[cfg(feature = "enrichment")]
135    pub fn with_enrichment_stats(
136        mut self,
137        old_stats: Option<crate::enrichment::EnrichmentStats>,
138        new_stats: Option<crate::enrichment::EnrichmentStats>,
139    ) -> Self {
140        self.data.enrichment_stats_old = old_stats;
141        self.data.enrichment_stats_new = new_stats;
142        self
143    }
144
145    /// Get combined enrichment stats for display
146    #[cfg(feature = "enrichment")]
147    #[must_use]
148    pub fn combined_enrichment_stats(&self) -> Option<crate::enrichment::EnrichmentStats> {
149        match (
150            &self.data.enrichment_stats_old,
151            &self.data.enrichment_stats_new,
152        ) {
153            (Some(old), Some(new)) => {
154                let mut combined = old.clone();
155                combined.merge(new);
156                Some(combined)
157            }
158            (Some(stats), None) | (None, Some(stats)) => Some(stats.clone()),
159            (None, None) => None,
160        }
161    }
162
163    /// Create a new app for single-SBOM view mode.
164    ///
165    /// Sets up quality scoring, compliance checking, and source viewing
166    /// for exploring a single SBOM interactively.
167    #[must_use]
168    pub fn new_view(sbom: NormalizedSbom, raw_content: &str) -> Self {
169        // Quality scoring — profile-aware so AI-BOMs / CBOMs score correctly.
170        let profile = crate::tui::scoring_profile_for(crate::model::BomProfile::detect(&sbom));
171        let scorer = QualityScorer::new(profile);
172        let quality_report = Some(scorer.score(&sbom));
173
174        // Build index for O(1) lookups
175        let sbom_index = Some(sbom.build_index());
176
177        // Source viewer (single SBOM, no diff annotations)
178        let source = SourceDiffState::new("", raw_content);
179
180        let mut app = Self::base(AppMode::View);
181
182        // Restore last view tab preference
183        app.active_tab = crate::config::TuiPreferences::load()
184            .last_view_tab
185            .as_deref()
186            .and_then(TabKind::from_str_opt)
187            .unwrap_or(TabKind::Overview);
188
189        app.source_view = crate::tui::view_states::SourceView::with_state(source);
190        app.data.sbom = Some(sbom);
191        app.data.sbom_index = sbom_index;
192        app.data.quality_report = quality_report;
193        app
194    }
195
196    /// Create a new app for multi-diff mode
197    #[must_use]
198    pub fn new_multi_diff(result: MultiDiffResult) -> Self {
199        let target_count = result.comparisons.len();
200
201        let mut app = Self::base(AppMode::MultiDiff);
202        app.data.multi_diff_result = Some(result);
203        app.tabs.multi_diff = MultiDiffState::new_with_targets(target_count);
204        app
205    }
206
207    /// Create a new app for timeline mode
208    #[must_use]
209    pub fn new_timeline(result: TimelineResult) -> Self {
210        let version_count = result.sboms.len();
211
212        let mut app = Self::base(AppMode::Timeline);
213        app.data.timeline_result = Some(result);
214        app.tabs.timeline = TimelineState::new_with_versions(version_count);
215        app
216    }
217
218    /// Create a new app for matrix mode
219    #[must_use]
220    pub fn new_matrix(result: MatrixResult) -> Self {
221        let sbom_count = result.sboms.len();
222
223        let mut app = Self::base(AppMode::Matrix);
224        app.data.matrix_result = Some(result);
225        app.tabs.matrix = MatrixState::new_with_size(sbom_count);
226        app
227    }
228}