1use crate::diff::{DiffResult, MatrixResult, MultiDiffResult, TimelineResult};
4#[cfg(feature = "enrichment")]
5use crate::enrichment::EnrichmentStats;
6use crate::model::{NormalizedSbom, NormalizedSbomIndex};
7use crate::quality::{ComplianceResult, QualityReport};
8use crate::tui::state::ListNavigation;
9use crate::tui::views::ThresholdTuningState;
10
11#[allow(unused_imports)]
13pub use super::app_states::{
14 AlignmentMode,
16 Breadcrumb,
18 ChangeType,
20 ChangeTypeFilter,
21 ComponentDeepDiveData,
23 ComponentDeepDiveState,
24 ComponentFilter,
26 ComponentSimilarityInfo,
27 ComponentSort,
28 ComponentTargetPresence,
29 ComponentVersionEntry,
30 ComponentVulnInfo,
31 ComponentsState,
32 DependenciesState,
34 DiffSearchResult,
35 DiffSearchState,
36 DiffVulnItem,
38 DiffVulnStatus,
39 GraphChangesState,
41 LicenseGroupBy,
43 LicenseRiskFilter,
44 LicenseSort,
45 LicensesState,
46 MatrixSortBy,
48 MatrixState,
49 MultiDiffState,
51 MultiViewFilterPreset,
52 MultiViewSearchState,
53 MultiViewSortBy,
54 MultiViewType,
56 NavigationContext,
57 QualityState,
59 QualityViewMode,
60 ScrollSyncMode,
61 SearchMode,
62 ShortcutsContext,
64 ShortcutsOverlayState,
65 SideBySideState,
66 SimilarityThreshold,
67 SortDirection,
68 TimelineChartMetric,
70 TimelineComponentFilter,
71 TimelineSortBy,
72 TimelineState,
73 ViewSwitcherState,
74 VulnChangeType,
75 VulnFilter,
76 VulnSort,
77 VulnerabilitiesState,
78 sort_component_changes,
79};
80
81pub struct ModeStates {
86 pub(crate) multi_diff: MultiDiffState,
87 pub(crate) timeline: TimelineState,
88 pub(crate) matrix: MatrixState,
89}
90
91pub struct AppOverlays {
95 pub(crate) show_export: bool,
96 pub(crate) show_legend: bool,
97 pub(crate) search: DiffSearchState,
98 pub(crate) threshold_tuning: ThresholdTuningState,
99 pub(crate) view_switcher: ViewSwitcherState,
100 pub(crate) shortcuts: ShortcutsOverlayState,
101 pub(crate) component_deep_dive: ComponentDeepDiveState,
102}
103
104impl AppOverlays {
105 pub fn new() -> Self {
106 Self {
107 show_export: false,
108 show_legend: false,
109 search: DiffSearchState::new(),
110 threshold_tuning: ThresholdTuningState::default(),
111 view_switcher: ViewSwitcherState::new(),
112 shortcuts: ShortcutsOverlayState::new(),
113 component_deep_dive: ComponentDeepDiveState::new(),
114 }
115 }
116
117 pub const fn toggle_export(&mut self) {
118 self.show_export = !self.show_export;
119 if self.show_export {
120 self.show_legend = false;
121 }
122 }
123
124 pub const fn toggle_legend(&mut self) {
125 self.show_legend = !self.show_legend;
126 if self.show_legend {
127 self.show_export = false;
128 }
129 }
130
131 pub const fn close_all(&mut self) {
132 self.show_export = false;
133 self.show_legend = false;
134 self.search.active = false;
135 self.threshold_tuning.visible = false;
136 self.shortcuts.visible = false;
137 self.component_deep_dive.visible = false;
138 self.view_switcher.visible = false;
139 }
140
141 pub const fn has_active(&self) -> bool {
142 self.show_export
143 || self.show_legend
144 || self.search.active
145 || self.threshold_tuning.visible
146 || self.shortcuts.visible
149 || self.component_deep_dive.visible
150 || self.view_switcher.visible
151 }
152}
153
154pub struct DataContext {
158 pub(crate) diff_result: Option<DiffResult>,
159 pub(crate) old_sbom: Option<NormalizedSbom>,
160 pub(crate) new_sbom: Option<NormalizedSbom>,
161 pub(crate) sbom: Option<NormalizedSbom>,
162 pub(crate) multi_diff_result: Option<MultiDiffResult>,
163 pub(crate) timeline_result: Option<TimelineResult>,
164 pub(crate) matrix_result: Option<MatrixResult>,
165 pub(crate) old_sbom_index: Option<NormalizedSbomIndex>,
166 pub(crate) new_sbom_index: Option<NormalizedSbomIndex>,
167 pub(crate) sbom_index: Option<NormalizedSbomIndex>,
168 pub(crate) old_quality: Option<QualityReport>,
169 pub(crate) new_quality: Option<QualityReport>,
170 pub(crate) quality_report: Option<QualityReport>,
171 pub(crate) old_cra_compliance: Option<ComplianceResult>,
172 pub(crate) new_cra_compliance: Option<ComplianceResult>,
173 pub(crate) old_compliance_results: Option<Vec<ComplianceResult>>,
174 pub(crate) new_compliance_results: Option<Vec<ComplianceResult>>,
175 pub(crate) old_cra_sidecar: Option<crate::model::CraSidecarMetadata>,
181 pub(crate) new_cra_sidecar: Option<crate::model::CraSidecarMetadata>,
187 pub(crate) matching_threshold: f64,
188 #[cfg(feature = "enrichment")]
189 pub(crate) enrichment_stats_old: Option<EnrichmentStats>,
190 #[cfg(feature = "enrichment")]
191 pub(crate) enrichment_stats_new: Option<EnrichmentStats>,
192}
193
194pub struct App {
196 pub(crate) mode: AppMode,
198 pub(crate) active_tab: TabKind,
200 pub(crate) data: DataContext,
202 pub(crate) tabs: ModeStates,
204 pub(crate) overlays: AppOverlays,
206 pub(crate) should_quit: bool,
208 pub(crate) status_message: Option<String>,
210 pub(crate) status_sticky: bool,
212 pub(crate) tick: u64,
214 pub(crate) last_export_path: Option<String>,
216 pub(crate) navigation_ctx: NavigationContext,
218 pub(crate) security_cache: crate::tui::security::SecurityAnalysisCache,
220 pub(crate) compliance_state: crate::tui::app_states::PolicyComplianceState,
222 pub(crate) export_template: Option<String>,
224 pub(crate) tab_window: crate::tui::shared::TabWindow,
232 pub(crate) last_frame_area: Option<ratatui::layout::Rect>,
237 pub(crate) summary_view: crate::tui::view_states::SummaryView,
238 pub(crate) components_view: crate::tui::view_states::ComponentsView,
239 pub(crate) dependencies_view: crate::tui::view_states::DependenciesView,
240 pub(crate) licenses_view: crate::tui::view_states::LicensesView,
241 pub(crate) vulnerabilities_view: crate::tui::view_states::VulnerabilitiesView,
242 pub(crate) quality_view: crate::tui::view_states::QualityView,
243 pub(crate) compliance_view: crate::tui::view_states::ComplianceView,
244 pub(crate) sidebyside_view: crate::tui::view_states::SideBySideView,
245 pub(crate) graph_changes_view: crate::tui::view_states::GraphChangesView,
246 pub(crate) source_view: crate::tui::view_states::SourceView,
247}
248
249impl App {
250 pub(crate) fn active_view_state(&self) -> Option<&dyn crate::tui::traits::ViewState> {
254 if self.mode != AppMode::Diff {
255 return None;
256 }
257 Some(match self.active_tab {
258 TabKind::Summary => &self.summary_view,
259 TabKind::Components => &self.components_view,
260 TabKind::Dependencies => &self.dependencies_view,
261 TabKind::Licenses => &self.licenses_view,
262 TabKind::Vulnerabilities => &self.vulnerabilities_view,
263 TabKind::Quality => &self.quality_view,
264 TabKind::Compliance => &self.compliance_view,
265 TabKind::SideBySide => &self.sidebyside_view,
266 TabKind::GraphChanges => &self.graph_changes_view,
267 TabKind::Source => &self.source_view,
268 })
269 }
270
271 pub fn ensure_compliance_results(&mut self) {
278 if self.data.old_compliance_results.is_none()
279 && let Some(old_sbom) = &self.data.old_sbom
280 {
281 self.data.old_compliance_results = Some(Self::compliance_results_for(
282 old_sbom,
283 self.data.old_cra_sidecar.as_ref(),
284 ));
285 }
286 if self.data.new_compliance_results.is_none()
287 && let Some(new_sbom) = &self.data.new_sbom
288 {
289 self.data.new_compliance_results = Some(Self::compliance_results_for(
290 new_sbom,
291 self.data.new_cra_sidecar.as_ref(),
292 ));
293 }
294 }
295
296 fn compliance_results_for(
299 sbom: &crate::model::NormalizedSbom,
300 sidecar: Option<&crate::model::CraSidecarMetadata>,
301 ) -> Vec<crate::quality::ComplianceResult> {
302 crate::quality::ComplianceLevel::all()
303 .iter()
304 .map(|level| {
305 let mut checker = crate::quality::ComplianceChecker::new(*level);
306 if let Some(sc) = sidecar {
307 checker = checker.with_sidecar(sc.clone());
308 }
309 checker.check(sbom)
310 })
311 .collect()
312 }
313
314 pub const fn toggle_export(&mut self) {
316 self.overlays.toggle_export();
317 }
318
319 pub const fn toggle_legend(&mut self) {
321 self.overlays.toggle_legend();
322 }
323
324 pub const fn close_overlays(&mut self) {
326 self.overlays.close_all();
327 }
328
329 #[must_use]
331 pub const fn has_overlay(&self) -> bool {
332 self.overlays.has_active()
333 }
334
335 pub fn toggle_threshold_tuning(&mut self) {
337 if self.overlays.threshold_tuning.visible {
338 self.overlays.threshold_tuning.visible = false;
339 } else {
340 self.show_threshold_tuning();
341 }
342 }
343
344 pub fn show_threshold_tuning(&mut self) {
346 self.overlays.close_all();
348
349 let total = match self.mode {
351 AppMode::Diff => {
352 self.data
353 .old_sbom
354 .as_ref()
355 .map_or(0, crate::model::NormalizedSbom::component_count)
356 + self
357 .data
358 .new_sbom
359 .as_ref()
360 .map_or(0, crate::model::NormalizedSbom::component_count)
361 }
362 _ => 0,
363 };
364
365 self.overlays.threshold_tuning =
367 ThresholdTuningState::new(self.data.matching_threshold, total);
368 self.update_threshold_preview();
369 }
370
371 pub fn update_threshold_preview(&mut self) {
373 if !self.overlays.threshold_tuning.visible {
374 return;
375 }
376
377 let estimated = if let Some(ref result) = self.data.diff_result {
380 let current_matches = result.components.modified.len();
382 let threshold = self.overlays.threshold_tuning.threshold;
383 let base_threshold = self.data.matching_threshold;
384
385 let ratio = if threshold < base_threshold {
387 (base_threshold - threshold).mul_add(2.0, 1.0)
388 } else {
389 (threshold - base_threshold).mul_add(-1.5, 1.0)
390 };
391 ((current_matches as f64 * ratio).max(0.0)) as usize
392 } else {
393 0
394 };
395
396 self.overlays
397 .threshold_tuning
398 .set_estimated_matches(estimated);
399 }
400
401 pub fn apply_threshold(&mut self) {
403 self.data.matching_threshold = self.overlays.threshold_tuning.threshold;
404 self.overlays.threshold_tuning.visible = false;
405 self.set_status_message(format!(
406 "Threshold set to {:.0}% - Re-run diff to apply",
407 self.data.matching_threshold * 100.0
408 ));
409 }
410
411 pub fn set_status_message(&mut self, msg: impl Into<String>) {
413 self.status_message = Some(msg.into());
414 }
415
416 pub fn clear_status_message(&mut self) {
421 if self.status_sticky {
422 self.status_sticky = false;
423 } else {
424 self.status_message = None;
425 }
426 }
427
428 pub fn export(&mut self, format: super::export::ExportFormat) {
433 use super::export::{export_diff, tab_to_report_type};
434 use crate::reports::ReportConfig;
435
436 let result = match self.mode {
437 AppMode::Diff => {
438 let report_type = tab_to_report_type(self.active_tab);
439 let config = ReportConfig {
443 old_cra_compliance: self.data.old_cra_compliance.clone(),
444 new_cra_compliance: self.data.new_cra_compliance.clone(),
445 ..ReportConfig::with_types(vec![report_type])
446 };
447 if let (Some(diff_result), Some(old_sbom), Some(new_sbom)) = (
448 &self.data.diff_result,
449 &self.data.old_sbom,
450 &self.data.new_sbom,
451 ) {
452 export_diff(
453 format,
454 diff_result,
455 old_sbom,
456 new_sbom,
457 None,
458 &config,
459 self.export_template.as_deref(),
460 )
461 } else {
462 self.set_status_message("No diff data to export");
463 return;
464 }
465 }
466 AppMode::MultiDiff => {
467 if let Some(ref result) = self.data.multi_diff_result {
468 super::export::export_multi_diff(
469 format,
470 result,
471 self.export_template.as_deref(),
472 )
473 } else {
474 self.set_status_message("No multi-diff data to export");
475 return;
476 }
477 }
478 AppMode::Timeline => {
479 if let Some(ref result) = self.data.timeline_result {
480 super::export::export_timeline(format, result, self.export_template.as_deref())
481 } else {
482 self.set_status_message("No timeline data to export");
483 return;
484 }
485 }
486 AppMode::Matrix => {
487 self.export_matrix(format);
488 return;
489 }
490 };
491
492 if result.success {
493 self.last_export_path = Some(result.path.display().to_string());
494 self.set_status_message(result.message);
495 self.status_sticky = true;
496 } else {
497 self.set_status_message(format!("Export failed: {}", result.message));
498 }
499 }
500
501 pub fn export_compliance(&mut self, format: super::export::ExportFormat) {
503 use super::export::export_compliance;
504
505 self.ensure_compliance_results();
506
507 let selected_standard = self.diff_compliance_state().selected_standard;
509 let (results, selected) = if let Some(ref results) = self.data.new_compliance_results {
510 if !results.is_empty() {
511 (results, selected_standard)
512 } else if let Some(ref old_results) = self.data.old_compliance_results {
513 if old_results.is_empty() {
514 self.set_status_message("No compliance results to export");
515 return;
516 }
517 (old_results, selected_standard)
518 } else {
519 self.set_status_message("No compliance results to export");
520 return;
521 }
522 } else if let Some(ref old_results) = self.data.old_compliance_results {
523 if old_results.is_empty() {
524 self.set_status_message("No compliance results to export");
525 return;
526 }
527 (old_results, selected_standard)
528 } else {
529 self.set_status_message("No compliance results to export");
530 return;
531 };
532
533 let result = export_compliance(
534 format,
535 results,
536 selected,
537 None,
538 self.export_template.as_deref(),
539 );
540 if result.success {
541 self.last_export_path = Some(result.path.display().to_string());
542 self.set_status_message(result.message);
543 self.status_sticky = true;
544 } else {
545 self.set_status_message(format!("Export failed: {}", result.message));
546 }
547 }
548
549 pub fn export_matrix(&mut self, format: super::export::ExportFormat) {
551 use super::export::export_matrix;
552
553 let Some(ref matrix_result) = self.data.matrix_result else {
554 self.set_status_message("No matrix data to export");
555 return;
556 };
557
558 let result = export_matrix(format, matrix_result, self.export_template.as_deref());
559 if result.success {
560 self.last_export_path = Some(result.path.display().to_string());
561 self.set_status_message(result.message);
562 self.status_sticky = true;
563 } else {
564 self.set_status_message(format!("Export failed: {}", result.message));
565 }
566 }
567
568 pub fn run_compliance_check(&mut self) {
574 use crate::tui::security::{SecurityPolicy, check_compliance};
575
576 let preset = self.compliance_state.policy_preset;
577
578 if preset.is_standards_based() {
580 self.run_standards_compliance_check(preset);
581 return;
582 }
583
584 let policy = match preset {
585 super::app_states::PolicyPreset::Enterprise => SecurityPolicy::enterprise_default(),
586 super::app_states::PolicyPreset::Strict => SecurityPolicy::strict(),
587 super::app_states::PolicyPreset::Permissive => SecurityPolicy::permissive(),
588 _ => unreachable!(),
590 };
591
592 let components = self.collect_compliance_data();
594
595 if components.is_empty() {
596 self.set_status_message("No components to check");
597 return;
598 }
599
600 let result = check_compliance(&policy, &components);
601 let passes = result.passes;
602 let score = result.score;
603 let violation_count = result.violations.len();
604
605 self.compliance_state.result = Some(result);
606 self.compliance_state.checked = true;
607 self.compliance_state.selected_violation = 0;
608
609 if passes {
610 self.set_status_message(format!("Policy: {} - PASS (score: {})", policy.name, score));
611 } else {
612 self.set_status_message(format!(
613 "Policy: {} - FAIL ({} violations, score: {})",
614 policy.name, violation_count, score
615 ));
616 }
617 }
618
619 fn run_standards_compliance_check(&mut self, preset: super::app_states::PolicyPreset) {
622 use crate::quality::{ComplianceChecker, ViolationSeverity};
623 use crate::tui::security::{
624 ComplianceResult as PolicyResult, PolicySeverity, PolicyViolation,
625 };
626
627 let Some(level) = preset.compliance_level() else {
628 return;
629 };
630
631 let (sbom, sidecar) = match self.mode {
636 AppMode::Diff => (
637 self.data.new_sbom.as_ref(),
638 self.data.new_cra_sidecar.as_ref(),
639 ),
640 _ => (self.data.sbom.as_ref(), None),
641 };
642 let Some(sbom) = sbom else {
643 self.set_status_message("No SBOM loaded to check");
644 return;
645 };
646
647 let mut checker = ComplianceChecker::new(level);
648 if let Some(sc) = sidecar {
649 checker = checker.with_sidecar(sc.clone());
650 }
651 let std_result = checker.check(sbom);
652
653 let violations: Vec<PolicyViolation> = std_result
655 .violations
656 .iter()
657 .map(|v| {
658 let severity = match v.severity {
659 ViolationSeverity::Error => PolicySeverity::High,
660 ViolationSeverity::Warning => PolicySeverity::Medium,
661 ViolationSeverity::Info => PolicySeverity::Low,
662 };
663 PolicyViolation {
664 rule_name: v.requirement.clone(),
665 severity,
666 component: v.element.clone(),
667 description: v.message.clone(),
668 remediation: v.remediation_guidance().to_string(),
669 }
670 })
671 .collect();
672
673 let score = std_result.score().unwrap_or(0);
677
678 let not_applicable = match &std_result.applicability {
681 crate::quality::Applicability::NotApplicable(reason) => Some(reason.clone()),
682 crate::quality::Applicability::Applicable => None,
683 };
684
685 let passes = std_result.is_compliant;
686 let policy_name = format!("{} Compliance", preset.label());
687 let violation_count = violations.len();
688
689 let result = PolicyResult {
690 policy_name: policy_name.clone(),
691 components_checked: sbom.components.len(),
692 violations,
693 score,
694 passes,
695 not_applicable: not_applicable.clone(),
696 };
697
698 self.compliance_state.result = Some(result);
699 self.compliance_state.checked = true;
700 self.compliance_state.selected_violation = 0;
701
702 if let Some(reason) = not_applicable {
703 self.set_status_message(format!("{policy_name} - NOT APPLICABLE ({reason})"));
704 } else if passes {
705 self.set_status_message(format!("{policy_name} - COMPLIANT (score: {score})"));
706 } else {
707 self.set_status_message(format!(
708 "{policy_name} - NON-COMPLIANT ({violation_count} violations, score: {score})"
709 ));
710 }
711 }
712
713 fn collect_compliance_data(&self) -> Vec<crate::tui::security::ComplianceComponentData> {
715 let mut components = Vec::new();
716
717 if self.mode == AppMode::Diff
718 && let Some(sbom) = &self.data.new_sbom
719 {
720 for comp in sbom.components.values() {
721 let licenses: Vec<String> = comp
722 .licenses
723 .declared
724 .iter()
725 .map(std::string::ToString::to_string)
726 .collect();
727 let vulns: Vec<(String, String)> = comp
728 .vulnerabilities
729 .iter()
730 .map(|v| {
731 let severity = v.severity.as_ref().map_or_else(
732 || "Unknown".to_string(),
733 std::string::ToString::to_string,
734 );
735 (v.id.clone(), severity)
736 })
737 .collect();
738 components.push((comp.name.clone(), comp.version.clone(), licenses, vulns));
739 }
740 }
741
742 components
743 }
744
745 pub const fn toggle_compliance_details(&mut self) {
747 self.compliance_state.toggle_details();
748 }
749
750 pub fn next_policy(&mut self) {
752 self.compliance_state.toggle_policy();
753 if self.compliance_state.checked {
755 self.run_compliance_check();
756 } else {
757 self.set_status_message(format!(
760 "Policy preset: {} — press P to check",
761 self.compliance_state.policy_preset.label()
762 ));
763 }
764 }
765
766 #[must_use]
772 pub const fn view_mode(&self) -> super::traits::ViewMode {
773 super::traits::ViewMode::from_app_mode(self.mode)
774 }
775
776 pub fn handle_event_result(&mut self, result: super::traits::EventResult) {
781 use super::traits::EventResult;
782
783 match result {
784 EventResult::Consumed | EventResult::Ignored => {
785 }
787 EventResult::NavigateTo(target) => {
788 self.navigate_to_target(target);
789 }
790 EventResult::Exit => {
791 self.should_quit = true;
792 }
793 EventResult::ShowOverlay(kind) => {
794 self.show_overlay_kind(&kind);
795 }
796 EventResult::StatusMessage(msg) => {
797 self.set_status_message(msg);
798 }
799 }
800 }
801
802 fn show_overlay_kind(&mut self, kind: &super::traits::OverlayKind) {
804 use super::traits::OverlayKind;
805
806 self.overlays.close_all();
808
809 match kind {
810 OverlayKind::Help => {
811 let context = match self.mode {
814 AppMode::MultiDiff => ShortcutsContext::MultiDiff,
815 AppMode::Timeline => ShortcutsContext::Timeline,
816 AppMode::Matrix => ShortcutsContext::Matrix,
817 AppMode::Diff => ShortcutsContext::Diff,
818 };
819 self.overlays.shortcuts.show(context);
820 }
821 OverlayKind::Export => self.overlays.show_export = true,
822 OverlayKind::Legend => self.overlays.show_legend = true,
823 OverlayKind::Search => {
824 self.overlays.search.active = true;
825 self.overlays.search.query.clear();
826 }
827 OverlayKind::Shortcuts => self.overlays.shortcuts.visible = true,
828 }
829 }
830
831 #[must_use]
833 pub const fn current_tab_target(&self) -> super::traits::TabTarget {
834 super::traits::TabTarget::from_tab_kind(self.active_tab)
835 }
836
837 pub(crate) fn quality_state(&self) -> &super::app_states::QualityState {
842 self.quality_view.inner()
843 }
844 pub(crate) fn quality_state_mut(&mut self) -> &mut super::app_states::QualityState {
845 self.quality_view.inner_mut()
846 }
847
848 pub(crate) fn summary_state(&self) -> &super::app_states::SummaryState {
849 self.summary_view.inner()
850 }
851 pub(crate) fn summary_state_mut(&mut self) -> &mut super::app_states::SummaryState {
852 self.summary_view.inner_mut()
853 }
854 pub(crate) fn graph_changes_state(&self) -> &super::app_states::GraphChangesState {
855 self.graph_changes_view.inner()
856 }
857 pub(crate) fn graph_changes_state_mut(&mut self) -> &mut super::app_states::GraphChangesState {
858 self.graph_changes_view.inner_mut()
859 }
860
861 pub(crate) fn licenses_state(&self) -> &super::app_states::LicensesState {
862 self.licenses_view.inner()
863 }
864 pub(crate) fn licenses_state_mut(&mut self) -> &mut super::app_states::LicensesState {
865 self.licenses_view.inner_mut()
866 }
867
868 pub(crate) fn diff_compliance_state(&self) -> &super::app_states::DiffComplianceState {
869 self.compliance_view.inner()
870 }
871 pub(crate) fn components_state(&self) -> &ComponentsState {
872 self.components_view.inner()
873 }
874 pub(crate) fn components_state_mut(&mut self) -> &mut ComponentsState {
875 self.components_view.inner_mut()
876 }
877
878 pub(crate) fn vulnerabilities_state(&self) -> &super::app_states::VulnerabilitiesState {
879 self.vulnerabilities_view.inner()
880 }
881 pub(crate) fn vulnerabilities_state_mut(
882 &mut self,
883 ) -> &mut super::app_states::VulnerabilitiesState {
884 self.vulnerabilities_view.inner_mut()
885 }
886
887 pub(crate) fn side_by_side_state(&self) -> &super::app_states::SideBySideState {
888 self.sidebyside_view.inner()
889 }
890 pub(crate) fn side_by_side_state_mut(&mut self) -> &mut super::app_states::SideBySideState {
891 self.sidebyside_view.inner_mut()
892 }
893
894 pub(crate) fn dependencies_state(&self) -> &DependenciesState {
895 self.dependencies_view.inner()
896 }
897 pub(crate) fn dependencies_state_mut(&mut self) -> &mut DependenciesState {
898 self.dependencies_view.inner_mut()
899 }
900
901 pub(crate) fn source_state(&self) -> &crate::tui::app_states::SourceDiffState {
902 self.source_view.inner()
903 }
904 pub(crate) fn source_state_mut(&mut self) -> &mut crate::tui::app_states::SourceDiffState {
905 self.source_view.inner_mut()
906 }
907
908 pub fn prepare_render(&mut self) {
920 super::views::update_graph_cache(self.dependencies_view.inner_mut(), &self.data, self.mode);
922
923 self.ensure_compliance_results();
925
926 if self.mode == AppMode::Diff {
928 self.ensure_vulnerability_cache();
929 }
930
931 let comp_filter = self.components_state().filter;
933 let comp_total = match self.mode {
934 AppMode::Diff => self.diff_component_count(comp_filter),
935 AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => 0,
936 };
937 self.components_state_mut().total = comp_total;
938 self.components_state_mut().clamp_selection();
939
940 self.prepare_vulnerability_totals();
942
943 let summary_total = self
945 .data
946 .diff_result
947 .as_ref()
948 .map_or(0, crate::tui::views::all_changes_line_count);
949 self.summary_state_mut().set_total(summary_total);
950
951 let graph_total = self
953 .data
954 .diff_result
955 .as_ref()
956 .map_or(0, |r| r.graph_changes.len());
957 self.graph_changes_state_mut().set_total(graph_total);
958
959 self.dependencies_state_mut().update_breadcrumbs();
961
962 self.prepare_license_totals();
964
965 if self.mode == AppMode::Diff {
970 let filter = self.side_by_side_state().filter.clone();
971 let (aligned, unified, grouped_left, grouped_right) =
972 self.data.diff_result.as_ref().map_or_else(
973 || (Vec::new(), Vec::new(), 0, 0),
974 |result| {
975 let grouped_left =
976 result.components.removed.len() + result.components.modified.len();
977 let grouped_right =
978 result.components.added.len() + result.components.modified.len();
979 (
980 crate::tui::views::build_aligned_rows(result, &filter),
981 crate::tui::views::build_unified_entries(result),
982 grouped_left,
983 grouped_right,
984 )
985 },
986 );
987 let st = self.side_by_side_state_mut();
988 st.aligned_rows = aligned;
989 st.unified_entries = unified;
990 st.set_totals(grouped_left, grouped_right);
991 st.recompute_row_model();
992 }
993
994 let rec_total = self
996 .data
997 .new_quality
998 .as_ref()
999 .or(self.data.old_quality.as_ref())
1000 .map_or(0, |r| r.recommendations.len());
1001 self.quality_state_mut().total_recommendations = rec_total;
1002 }
1003
1004 fn prepare_license_totals(&mut self) {
1006 match self.mode {
1007 AppMode::Diff => {
1008 if let Some(ref result) = self.data.diff_result {
1009 let focus_left = self.licenses_state().focus_left;
1010 let risk_filter = self.licenses_state().risk_filter;
1011 let count = if focus_left {
1012 Self::filtered_license_count(&result.licenses.new_licenses, risk_filter)
1013 } else {
1014 Self::filtered_license_count(&result.licenses.removed_licenses, risk_filter)
1015 };
1016 self.licenses_state_mut().total = count;
1017 }
1018 }
1019 AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => {
1020 self.licenses_state_mut().total = 0;
1021 }
1022 }
1023 self.licenses_state_mut().clamp_selection();
1024 }
1025
1026 fn filtered_license_count(
1028 licenses: &[crate::diff::LicenseChange],
1029 risk_filter: Option<crate::tui::app_states::LicenseRiskFilter>,
1030 ) -> usize {
1031 use crate::tui::license_utils::{LicenseInfo, RiskLevel};
1032 if let Some(min_risk) = risk_filter {
1033 let min_level = match min_risk {
1034 crate::tui::app_states::LicenseRiskFilter::Low => RiskLevel::Low,
1035 crate::tui::app_states::LicenseRiskFilter::Medium => RiskLevel::Medium,
1036 crate::tui::app_states::LicenseRiskFilter::High => RiskLevel::High,
1037 crate::tui::app_states::LicenseRiskFilter::Critical => RiskLevel::Critical,
1038 };
1039 licenses
1040 .iter()
1041 .filter(|l| LicenseInfo::from_spdx(&l.license).risk_level >= min_level)
1042 .count()
1043 } else {
1044 licenses.len()
1045 }
1046 }
1047
1048 fn prepare_vulnerability_totals(&mut self) {
1050 let vuln_total = match self.mode {
1051 AppMode::Diff => self.diff_vulnerability_count(),
1052 AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => 0,
1053 };
1054 self.vulnerabilities_state_mut().total = vuln_total;
1055 self.vulnerabilities_state_mut().clamp_selection();
1056
1057 if self.vulnerabilities_state().group_by_component {
1059 let grouped_count = super::views::count_grouped_items(self);
1060 self.vulnerabilities_state_mut().total = grouped_count;
1061 self.vulnerabilities_state_mut().clamp_selection();
1062 }
1063 }
1064}
1065
1066#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1068pub enum AppMode {
1069 Diff,
1071 MultiDiff,
1073 Timeline,
1075 Matrix,
1077}
1078
1079#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1081pub enum TabKind {
1082 Summary,
1083 Components,
1084 Dependencies,
1085 Licenses,
1086 Vulnerabilities,
1087 Quality,
1088 Compliance,
1089 SideBySide,
1090 GraphChanges,
1091 Source,
1092}
1093
1094impl TabKind {
1095 #[must_use]
1096 pub const fn title(&self) -> &'static str {
1097 match self {
1098 Self::Summary => "Summary",
1099 Self::Components => "Components",
1100 Self::Dependencies => "Dependencies",
1101 Self::Licenses => "Licenses",
1102 Self::Vulnerabilities => "Vulnerabilities",
1103 Self::Quality => "Quality",
1104 Self::Compliance => "Compliance",
1105 Self::SideBySide => "Side-by-Side",
1106 Self::GraphChanges => "Graph",
1107 Self::Source => "Source",
1108 }
1109 }
1110
1111 #[must_use]
1113 pub const fn as_str(&self) -> &'static str {
1114 match self {
1115 Self::Summary => "summary",
1116 Self::Components => "components",
1117 Self::Dependencies => "dependencies",
1118 Self::Licenses => "licenses",
1119 Self::Vulnerabilities => "vulnerabilities",
1120 Self::Quality => "quality",
1121 Self::Compliance => "compliance",
1122 Self::SideBySide => "side-by-side",
1123 Self::GraphChanges => "graph",
1124 Self::Source => "source",
1125 }
1126 }
1127
1128 #[must_use]
1130 pub fn from_str_opt(s: &str) -> Option<Self> {
1131 match s {
1132 "summary" => Some(Self::Summary),
1133 "components" => Some(Self::Components),
1134 "dependencies" => Some(Self::Dependencies),
1135 "licenses" => Some(Self::Licenses),
1136 "vulnerabilities" => Some(Self::Vulnerabilities),
1137 "quality" => Some(Self::Quality),
1138 "compliance" => Some(Self::Compliance),
1139 "side-by-side" => Some(Self::SideBySide),
1140 "graph" => Some(Self::GraphChanges),
1141 "source" => Some(Self::Source),
1142 _ => None,
1143 }
1144 }
1145}