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 TimelineComponentFilter,
70 TimelineSortBy,
71 TimelineState,
72 ViewSwitcherState,
73 VulnChangeType,
74 VulnFilter,
75 VulnSort,
76 VulnerabilitiesState,
77 sort_component_changes,
78};
79
80pub struct ModeStates {
85 pub(crate) multi_diff: MultiDiffState,
86 pub(crate) timeline: TimelineState,
87 pub(crate) matrix: MatrixState,
88}
89
90pub struct AppOverlays {
94 pub(crate) show_help: bool,
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_help: false,
108 show_export: false,
109 show_legend: false,
110 search: DiffSearchState::new(),
111 threshold_tuning: ThresholdTuningState::default(),
112 view_switcher: ViewSwitcherState::new(),
113 shortcuts: ShortcutsOverlayState::new(),
114 component_deep_dive: ComponentDeepDiveState::new(),
115 }
116 }
117
118 pub const fn toggle_help(&mut self) {
119 self.show_help = !self.show_help;
120 if self.show_help {
121 self.show_export = false;
122 self.show_legend = false;
123 }
124 }
125
126 pub const fn toggle_export(&mut self) {
127 self.show_export = !self.show_export;
128 if self.show_export {
129 self.show_help = false;
130 self.show_legend = false;
131 }
132 }
133
134 pub const fn toggle_legend(&mut self) {
135 self.show_legend = !self.show_legend;
136 if self.show_legend {
137 self.show_help = false;
138 self.show_export = false;
139 }
140 }
141
142 pub const fn close_all(&mut self) {
143 self.show_help = false;
144 self.show_export = false;
145 self.show_legend = false;
146 self.search.active = false;
147 self.threshold_tuning.visible = false;
148 }
149
150 pub const fn has_active(&self) -> bool {
151 self.show_help
152 || self.show_export
153 || self.show_legend
154 || self.search.active
155 || self.threshold_tuning.visible
156 }
157}
158
159pub struct DataContext {
163 pub(crate) diff_result: Option<DiffResult>,
164 pub(crate) old_sbom: Option<NormalizedSbom>,
165 pub(crate) new_sbom: Option<NormalizedSbom>,
166 pub(crate) sbom: Option<NormalizedSbom>,
167 pub(crate) multi_diff_result: Option<MultiDiffResult>,
168 pub(crate) timeline_result: Option<TimelineResult>,
169 pub(crate) matrix_result: Option<MatrixResult>,
170 pub(crate) old_sbom_index: Option<NormalizedSbomIndex>,
171 pub(crate) new_sbom_index: Option<NormalizedSbomIndex>,
172 pub(crate) sbom_index: Option<NormalizedSbomIndex>,
173 pub(crate) old_quality: Option<QualityReport>,
174 pub(crate) new_quality: Option<QualityReport>,
175 pub(crate) quality_report: Option<QualityReport>,
176 pub(crate) old_cra_compliance: Option<ComplianceResult>,
177 pub(crate) new_cra_compliance: Option<ComplianceResult>,
178 pub(crate) old_compliance_results: Option<Vec<ComplianceResult>>,
179 pub(crate) new_compliance_results: Option<Vec<ComplianceResult>>,
180 pub(crate) cra_sidecar: Option<crate::model::CraSidecarMetadata>,
185 pub(crate) matching_threshold: f64,
186 #[cfg(feature = "enrichment")]
187 pub(crate) enrichment_stats_old: Option<EnrichmentStats>,
188 #[cfg(feature = "enrichment")]
189 pub(crate) enrichment_stats_new: Option<EnrichmentStats>,
190}
191
192pub struct App {
194 pub(crate) mode: AppMode,
196 pub(crate) active_tab: TabKind,
198 pub(crate) data: DataContext,
200 pub(crate) tabs: ModeStates,
202 pub(crate) overlays: AppOverlays,
204 pub(crate) should_quit: bool,
206 pub(crate) status_message: Option<String>,
208 pub(crate) status_sticky: bool,
210 pub(crate) tick: u64,
212 pub(crate) last_export_path: Option<String>,
214 pub(crate) navigation_ctx: NavigationContext,
216 pub(crate) security_cache: crate::tui::security::SecurityAnalysisCache,
218 pub(crate) compliance_state: crate::tui::app_states::PolicyComplianceState,
220 pub(crate) export_template: Option<String>,
222 pub(crate) components_view: crate::tui::view_states::ComponentsView,
228 pub(crate) dependencies_view: crate::tui::view_states::DependenciesView,
229 pub(crate) licenses_view: crate::tui::view_states::LicensesView,
230 pub(crate) vulnerabilities_view: crate::tui::view_states::VulnerabilitiesView,
231 pub(crate) quality_view: crate::tui::view_states::QualityView,
232 pub(crate) compliance_view: crate::tui::view_states::ComplianceView,
233 pub(crate) sidebyside_view: crate::tui::view_states::SideBySideView,
234 pub(crate) graph_changes_view: crate::tui::view_states::GraphChangesView,
235 pub(crate) source_view: crate::tui::view_states::SourceView,
236}
237
238impl App {
239 pub fn ensure_compliance_results(&mut self) {
246 let sidecar = self.data.cra_sidecar.as_ref();
247 if self.data.old_compliance_results.is_none()
248 && let Some(old_sbom) = &self.data.old_sbom
249 {
250 self.data.old_compliance_results =
251 Some(Self::compliance_results_for(old_sbom, sidecar));
252 }
253 if self.data.new_compliance_results.is_none()
254 && let Some(new_sbom) = &self.data.new_sbom
255 {
256 self.data.new_compliance_results =
257 Some(Self::compliance_results_for(new_sbom, sidecar));
258 }
259 }
260
261 fn compliance_results_for(
264 sbom: &crate::model::NormalizedSbom,
265 sidecar: Option<&crate::model::CraSidecarMetadata>,
266 ) -> Vec<crate::quality::ComplianceResult> {
267 crate::quality::ComplianceLevel::all()
268 .iter()
269 .map(|level| {
270 let mut checker = crate::quality::ComplianceChecker::new(*level);
271 if let Some(sc) = sidecar {
272 checker = checker.with_sidecar(sc.clone());
273 }
274 checker.check(sbom)
275 })
276 .collect()
277 }
278
279 pub const fn toggle_help(&mut self) {
281 self.overlays.toggle_help();
282 }
283
284 pub const fn toggle_export(&mut self) {
286 self.overlays.toggle_export();
287 }
288
289 pub const fn toggle_legend(&mut self) {
291 self.overlays.toggle_legend();
292 }
293
294 pub const fn close_overlays(&mut self) {
296 self.overlays.close_all();
297 }
298
299 #[must_use]
301 pub const fn has_overlay(&self) -> bool {
302 self.overlays.has_active()
303 }
304
305 pub fn toggle_threshold_tuning(&mut self) {
307 if self.overlays.threshold_tuning.visible {
308 self.overlays.threshold_tuning.visible = false;
309 } else {
310 self.show_threshold_tuning();
311 }
312 }
313
314 pub fn show_threshold_tuning(&mut self) {
316 self.overlays.close_all();
318
319 let total = match self.mode {
321 AppMode::Diff => {
322 self.data
323 .old_sbom
324 .as_ref()
325 .map_or(0, crate::model::NormalizedSbom::component_count)
326 + self
327 .data
328 .new_sbom
329 .as_ref()
330 .map_or(0, crate::model::NormalizedSbom::component_count)
331 }
332 _ => 0,
333 };
334
335 self.overlays.threshold_tuning =
337 ThresholdTuningState::new(self.data.matching_threshold, total);
338 self.update_threshold_preview();
339 }
340
341 pub fn update_threshold_preview(&mut self) {
343 if !self.overlays.threshold_tuning.visible {
344 return;
345 }
346
347 let estimated = if let Some(ref result) = self.data.diff_result {
350 let current_matches = result.components.modified.len();
352 let threshold = self.overlays.threshold_tuning.threshold;
353 let base_threshold = self.data.matching_threshold;
354
355 let ratio = if threshold < base_threshold {
357 (base_threshold - threshold).mul_add(2.0, 1.0)
358 } else {
359 (threshold - base_threshold).mul_add(-1.5, 1.0)
360 };
361 ((current_matches as f64 * ratio).max(0.0)) as usize
362 } else {
363 0
364 };
365
366 self.overlays
367 .threshold_tuning
368 .set_estimated_matches(estimated);
369 }
370
371 pub fn apply_threshold(&mut self) {
373 self.data.matching_threshold = self.overlays.threshold_tuning.threshold;
374 self.overlays.threshold_tuning.visible = false;
375 self.set_status_message(format!(
376 "Threshold set to {:.0}% - Re-run diff to apply",
377 self.data.matching_threshold * 100.0
378 ));
379 }
380
381 pub fn set_status_message(&mut self, msg: impl Into<String>) {
383 self.status_message = Some(msg.into());
384 }
385
386 pub fn clear_status_message(&mut self) {
391 if self.status_sticky {
392 self.status_sticky = false;
393 } else {
394 self.status_message = None;
395 }
396 }
397
398 pub fn export(&mut self, format: super::export::ExportFormat) {
403 use super::export::{
404 export_diff, export_view, tab_to_report_type, view_tab_to_report_type,
405 };
406 use crate::reports::ReportConfig;
407
408 let result = match self.mode {
409 AppMode::Diff => {
410 let report_type = tab_to_report_type(self.active_tab);
411 let config = ReportConfig::with_types(vec![report_type]);
412 if let (Some(diff_result), Some(old_sbom), Some(new_sbom)) = (
413 &self.data.diff_result,
414 &self.data.old_sbom,
415 &self.data.new_sbom,
416 ) {
417 export_diff(
418 format,
419 diff_result,
420 old_sbom,
421 new_sbom,
422 None,
423 &config,
424 self.export_template.as_deref(),
425 )
426 } else {
427 self.set_status_message("No diff data to export");
428 return;
429 }
430 }
431 AppMode::View => {
432 let report_type = match self.active_tab {
434 super::TabKind::Tree => view_tab_to_report_type(crate::tui::ViewTab::Tree),
435 super::TabKind::Vulnerabilities => {
436 view_tab_to_report_type(crate::tui::ViewTab::Vulnerabilities)
437 }
438 super::TabKind::Licenses => {
439 view_tab_to_report_type(crate::tui::ViewTab::Licenses)
440 }
441 super::TabKind::Dependencies => {
442 view_tab_to_report_type(crate::tui::ViewTab::Dependencies)
443 }
444 _ => view_tab_to_report_type(crate::tui::ViewTab::Overview),
445 };
446 let config = ReportConfig::with_types(vec![report_type]);
447 if let Some(ref sbom) = self.data.sbom {
448 export_view(format, sbom, None, &config, self.export_template.as_deref())
449 } else {
450 self.set_status_message("No SBOM data available for export");
451 return;
452 }
453 }
454 _ => {
455 self.set_status_message("Export not supported for this mode");
456 return;
457 }
458 };
459
460 if result.success {
461 self.last_export_path = Some(result.path.display().to_string());
462 self.set_status_message(result.message);
463 self.status_sticky = true;
464 } else {
465 self.set_status_message(format!("Export failed: {}", result.message));
466 }
467 }
468
469 pub fn export_compliance(&mut self, format: super::export::ExportFormat) {
471 use super::export::export_compliance;
472
473 self.ensure_compliance_results();
474
475 let selected_standard = self.diff_compliance_state().selected_standard;
477 let (results, selected) = if let Some(ref results) = self.data.new_compliance_results {
478 if !results.is_empty() {
479 (results, selected_standard)
480 } else if let Some(ref old_results) = self.data.old_compliance_results {
481 if old_results.is_empty() {
482 self.set_status_message("No compliance results to export");
483 return;
484 }
485 (old_results, selected_standard)
486 } else {
487 self.set_status_message("No compliance results to export");
488 return;
489 }
490 } else if let Some(ref old_results) = self.data.old_compliance_results {
491 if old_results.is_empty() {
492 self.set_status_message("No compliance results to export");
493 return;
494 }
495 (old_results, selected_standard)
496 } else {
497 self.set_status_message("No compliance results to export");
498 return;
499 };
500
501 let result = export_compliance(
502 format,
503 results,
504 selected,
505 None,
506 self.export_template.as_deref(),
507 );
508 if result.success {
509 self.last_export_path = Some(result.path.display().to_string());
510 self.set_status_message(result.message);
511 self.status_sticky = true;
512 } else {
513 self.set_status_message(format!("Export failed: {}", result.message));
514 }
515 }
516
517 pub fn export_matrix(&mut self, format: super::export::ExportFormat) {
519 use super::export::export_matrix;
520
521 let Some(ref matrix_result) = self.data.matrix_result else {
522 self.set_status_message("No matrix data to export");
523 return;
524 };
525
526 let result = export_matrix(format, matrix_result, self.export_template.as_deref());
527 if result.success {
528 self.last_export_path = Some(result.path.display().to_string());
529 self.set_status_message(result.message);
530 self.status_sticky = true;
531 } else {
532 self.set_status_message(format!("Export failed: {}", result.message));
533 }
534 }
535
536 pub fn run_compliance_check(&mut self) {
542 use crate::tui::security::{SecurityPolicy, check_compliance};
543
544 let preset = self.compliance_state.policy_preset;
545
546 if preset.is_standards_based() {
548 self.run_standards_compliance_check(preset);
549 return;
550 }
551
552 let policy = match preset {
553 super::app_states::PolicyPreset::Enterprise => SecurityPolicy::enterprise_default(),
554 super::app_states::PolicyPreset::Strict => SecurityPolicy::strict(),
555 super::app_states::PolicyPreset::Permissive => SecurityPolicy::permissive(),
556 _ => unreachable!(),
558 };
559
560 let components = self.collect_compliance_data();
562
563 if components.is_empty() {
564 self.set_status_message("No components to check");
565 return;
566 }
567
568 let result = check_compliance(&policy, &components);
569 let passes = result.passes;
570 let score = result.score;
571 let violation_count = result.violations.len();
572
573 self.compliance_state.result = Some(result);
574 self.compliance_state.checked = true;
575 self.compliance_state.selected_violation = 0;
576
577 if passes {
578 self.set_status_message(format!("Policy: {} - PASS (score: {})", policy.name, score));
579 } else {
580 self.set_status_message(format!(
581 "Policy: {} - FAIL ({} violations, score: {})",
582 policy.name, violation_count, score
583 ));
584 }
585 }
586
587 fn run_standards_compliance_check(&mut self, preset: super::app_states::PolicyPreset) {
590 use crate::quality::{ComplianceChecker, ViolationSeverity};
591 use crate::tui::security::{
592 ComplianceResult as PolicyResult, PolicySeverity, PolicyViolation,
593 };
594
595 let Some(level) = preset.compliance_level() else {
596 return;
597 };
598
599 let sbom = match self.mode {
601 AppMode::Diff => self.data.new_sbom.as_ref(),
602 _ => self.data.sbom.as_ref(),
603 };
604 let Some(sbom) = sbom else {
605 self.set_status_message("No SBOM loaded to check");
606 return;
607 };
608
609 let checker = ComplianceChecker::new(level);
610 let std_result = checker.check(sbom);
611
612 let violations: Vec<PolicyViolation> = std_result
614 .violations
615 .iter()
616 .map(|v| {
617 let severity = match v.severity {
618 ViolationSeverity::Error => PolicySeverity::High,
619 ViolationSeverity::Warning => PolicySeverity::Medium,
620 ViolationSeverity::Info => PolicySeverity::Low,
621 };
622 PolicyViolation {
623 rule_name: v.requirement.clone(),
624 severity,
625 component: v.element.clone(),
626 description: v.message.clone(),
627 remediation: v.remediation_guidance().to_string(),
628 }
629 })
630 .collect();
631
632 let penalty: u32 = violations
634 .iter()
635 .map(|v| match v.severity {
636 PolicySeverity::High | PolicySeverity::Critical => 10,
637 PolicySeverity::Medium => 5,
638 PolicySeverity::Low => 1,
639 })
640 .sum();
641 let score = 100u8.saturating_sub(penalty.min(100) as u8);
642
643 let passes = std_result.is_compliant;
644 let policy_name = format!("{} Compliance", preset.label());
645 let violation_count = violations.len();
646
647 let result = PolicyResult {
648 policy_name: policy_name.clone(),
649 components_checked: sbom.components.len(),
650 violations,
651 score,
652 passes,
653 };
654
655 self.compliance_state.result = Some(result);
656 self.compliance_state.checked = true;
657 self.compliance_state.selected_violation = 0;
658
659 if passes {
660 self.set_status_message(format!("{policy_name} - COMPLIANT (score: {score})"));
661 } else {
662 self.set_status_message(format!(
663 "{policy_name} - NON-COMPLIANT ({violation_count} violations, score: {score})"
664 ));
665 }
666 }
667
668 fn collect_compliance_data(&self) -> Vec<crate::tui::security::ComplianceComponentData> {
670 let mut components = Vec::new();
671
672 if self.mode == AppMode::Diff
673 && let Some(sbom) = &self.data.new_sbom
674 {
675 for comp in sbom.components.values() {
676 let licenses: Vec<String> = comp
677 .licenses
678 .declared
679 .iter()
680 .map(std::string::ToString::to_string)
681 .collect();
682 let vulns: Vec<(String, String)> = comp
683 .vulnerabilities
684 .iter()
685 .map(|v| {
686 let severity = v.severity.as_ref().map_or_else(
687 || "Unknown".to_string(),
688 std::string::ToString::to_string,
689 );
690 (v.id.clone(), severity)
691 })
692 .collect();
693 components.push((comp.name.clone(), comp.version.clone(), licenses, vulns));
694 }
695 }
696
697 components
698 }
699
700 pub const fn toggle_compliance_details(&mut self) {
702 self.compliance_state.toggle_details();
703 }
704
705 pub fn next_policy(&mut self) {
707 self.compliance_state.toggle_policy();
708 if self.compliance_state.checked {
710 self.run_compliance_check();
711 }
712 }
713
714 #[must_use]
720 pub const fn view_mode(&self) -> super::traits::ViewMode {
721 super::traits::ViewMode::from_app_mode(self.mode)
722 }
723
724 pub fn handle_event_result(&mut self, result: super::traits::EventResult) {
729 use super::traits::EventResult;
730
731 match result {
732 EventResult::Consumed | EventResult::Ignored => {
733 }
735 EventResult::NavigateTo(target) => {
736 self.navigate_to_target(target);
737 }
738 EventResult::Exit => {
739 self.should_quit = true;
740 }
741 EventResult::ShowOverlay(kind) => {
742 self.show_overlay_kind(&kind);
743 }
744 EventResult::StatusMessage(msg) => {
745 self.set_status_message(msg);
746 }
747 }
748 }
749
750 fn show_overlay_kind(&mut self, kind: &super::traits::OverlayKind) {
752 use super::traits::OverlayKind;
753
754 self.overlays.close_all();
756
757 match kind {
758 OverlayKind::Help => self.overlays.show_help = true,
759 OverlayKind::Export => self.overlays.show_export = true,
760 OverlayKind::Legend => self.overlays.show_legend = true,
761 OverlayKind::Search => {
762 self.overlays.search.active = true;
763 self.overlays.search.query.clear();
764 }
765 OverlayKind::Shortcuts => self.overlays.shortcuts.visible = true,
766 }
767 }
768
769 #[must_use]
771 pub const fn current_tab_target(&self) -> super::traits::TabTarget {
772 super::traits::TabTarget::from_tab_kind(self.active_tab)
773 }
774
775 #[must_use]
777 pub fn current_shortcuts(&self) -> Vec<super::traits::Shortcut> {
778 use super::traits::Shortcut;
779
780 let mut shortcuts = vec![
781 Shortcut::primary("?", "Help"),
782 Shortcut::primary("q", "Quit"),
783 Shortcut::primary("Tab", "Next tab"),
784 Shortcut::primary("/", "Search"),
785 ];
786
787 match self.active_tab {
789 TabKind::Components => {
790 shortcuts.push(Shortcut::new("f", "Filter"));
791 shortcuts.push(Shortcut::new("s", "Sort"));
792 shortcuts.push(Shortcut::new("m", "Multi-select"));
793 }
794 TabKind::Dependencies => {
795 shortcuts.push(Shortcut::new("t", "Transitive"));
796 shortcuts.push(Shortcut::new("+/-", "Depth"));
797 }
798 TabKind::Vulnerabilities => {
799 shortcuts.push(Shortcut::new("f", "Filter"));
800 shortcuts.push(Shortcut::new("s", "Sort"));
801 }
802 TabKind::Quality => {
803 shortcuts.push(Shortcut::new("v", "View mode"));
804 }
805 _ => {}
806 }
807
808 shortcuts
809 }
810
811 pub(crate) fn quality_state(&self) -> &super::app_states::QualityState {
816 self.quality_view.inner()
817 }
818 pub(crate) fn quality_state_mut(&mut self) -> &mut super::app_states::QualityState {
819 self.quality_view.inner_mut()
820 }
821
822 pub(crate) fn graph_changes_state(&self) -> &super::app_states::GraphChangesState {
823 self.graph_changes_view.inner()
824 }
825 pub(crate) fn graph_changes_state_mut(&mut self) -> &mut super::app_states::GraphChangesState {
826 self.graph_changes_view.inner_mut()
827 }
828
829 pub(crate) fn licenses_state(&self) -> &super::app_states::LicensesState {
830 self.licenses_view.inner()
831 }
832 pub(crate) fn licenses_state_mut(&mut self) -> &mut super::app_states::LicensesState {
833 self.licenses_view.inner_mut()
834 }
835
836 pub(crate) fn diff_compliance_state(&self) -> &super::app_states::DiffComplianceState {
837 self.compliance_view.inner()
838 }
839 pub(crate) fn components_state(&self) -> &ComponentsState {
840 self.components_view.inner()
841 }
842 pub(crate) fn components_state_mut(&mut self) -> &mut ComponentsState {
843 self.components_view.inner_mut()
844 }
845
846 pub(crate) fn vulnerabilities_state(&self) -> &super::app_states::VulnerabilitiesState {
847 self.vulnerabilities_view.inner()
848 }
849 pub(crate) fn vulnerabilities_state_mut(
850 &mut self,
851 ) -> &mut super::app_states::VulnerabilitiesState {
852 self.vulnerabilities_view.inner_mut()
853 }
854
855 pub(crate) fn side_by_side_state(&self) -> &super::app_states::SideBySideState {
856 self.sidebyside_view.inner()
857 }
858 pub(crate) fn side_by_side_state_mut(&mut self) -> &mut super::app_states::SideBySideState {
859 self.sidebyside_view.inner_mut()
860 }
861
862 pub(crate) fn dependencies_state(&self) -> &DependenciesState {
863 self.dependencies_view.inner()
864 }
865 pub(crate) fn dependencies_state_mut(&mut self) -> &mut DependenciesState {
866 self.dependencies_view.inner_mut()
867 }
868
869 pub(crate) fn source_state(&self) -> &crate::tui::app_states::SourceDiffState {
870 self.source_view.inner()
871 }
872 pub(crate) fn source_state_mut(&mut self) -> &mut crate::tui::app_states::SourceDiffState {
873 self.source_view.inner_mut()
874 }
875
876 pub fn prepare_render(&mut self) {
888 super::views::update_graph_cache(self.dependencies_view.inner_mut(), &self.data, self.mode);
890
891 self.ensure_compliance_results();
893
894 if matches!(self.mode, AppMode::Diff | AppMode::View) {
896 self.ensure_vulnerability_cache();
897 }
898
899 let comp_filter = self.components_state().filter;
901 let comp_total = match self.mode {
902 AppMode::Diff | AppMode::View => self.diff_component_count(comp_filter),
903 AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => 0,
904 };
905 self.components_state_mut().total = comp_total;
906 self.components_state_mut().clamp_selection();
907
908 self.prepare_vulnerability_totals();
910
911 let graph_total = self
913 .data
914 .diff_result
915 .as_ref()
916 .map_or(0, |r| r.graph_changes.len());
917 self.graph_changes_state_mut().set_total(graph_total);
918
919 self.dependencies_state_mut().update_breadcrumbs();
921
922 self.prepare_license_totals();
924
925 if matches!(self.mode, AppMode::Diff | AppMode::View)
929 && let Some(ref result) = self.data.diff_result
930 {
931 let left = result.components.removed.len() + result.components.modified.len();
932 let right = result.components.added.len() + result.components.modified.len();
933 self.side_by_side_state_mut().set_totals(left, right);
934 }
935
936 let rec_total = self
938 .data
939 .new_quality
940 .as_ref()
941 .or(self.data.old_quality.as_ref())
942 .map_or(0, |r| r.recommendations.len());
943 self.quality_state_mut().total_recommendations = rec_total;
944 }
945
946 fn prepare_license_totals(&mut self) {
948 match self.mode {
949 AppMode::Diff | AppMode::View => {
950 if let Some(ref result) = self.data.diff_result {
951 let focus_left = self.licenses_state().focus_left;
952 let risk_filter = self.licenses_state().risk_filter;
953 let count = if focus_left {
954 Self::filtered_license_count(&result.licenses.new_licenses, risk_filter)
955 } else {
956 Self::filtered_license_count(&result.licenses.removed_licenses, risk_filter)
957 };
958 self.licenses_state_mut().total = count;
959 }
960 }
961 AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => {
962 self.licenses_state_mut().total = 0;
963 }
964 }
965 self.licenses_state_mut().clamp_selection();
966 }
967
968 fn filtered_license_count(
970 licenses: &[crate::diff::LicenseChange],
971 risk_filter: Option<crate::tui::app_states::LicenseRiskFilter>,
972 ) -> usize {
973 use crate::tui::license_utils::{LicenseInfo, RiskLevel};
974 if let Some(min_risk) = risk_filter {
975 let min_level = match min_risk {
976 crate::tui::app_states::LicenseRiskFilter::Low => RiskLevel::Low,
977 crate::tui::app_states::LicenseRiskFilter::Medium => RiskLevel::Medium,
978 crate::tui::app_states::LicenseRiskFilter::High => RiskLevel::High,
979 crate::tui::app_states::LicenseRiskFilter::Critical => RiskLevel::Critical,
980 };
981 licenses
982 .iter()
983 .filter(|l| LicenseInfo::from_spdx(&l.license).risk_level >= min_level)
984 .count()
985 } else {
986 licenses.len()
987 }
988 }
989
990 fn prepare_vulnerability_totals(&mut self) {
992 let vuln_total = match self.mode {
993 AppMode::Diff | AppMode::View => self.diff_vulnerability_count(),
994 AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => 0,
995 };
996 self.vulnerabilities_state_mut().total = vuln_total;
997 self.vulnerabilities_state_mut().clamp_selection();
998
999 if self.vulnerabilities_state().group_by_component {
1001 let grouped_count = super::views::count_grouped_items(self);
1002 self.vulnerabilities_state_mut().total = grouped_count;
1003 self.vulnerabilities_state_mut().clamp_selection();
1004 }
1005 }
1006}
1007
1008#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1010pub enum AppMode {
1011 Diff,
1013 View,
1015 MultiDiff,
1017 Timeline,
1019 Matrix,
1021}
1022
1023#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1025pub enum TabKind {
1026 Summary,
1027 Overview,
1029 Tree,
1031 Components,
1032 Dependencies,
1033 Licenses,
1034 Vulnerabilities,
1035 Quality,
1036 Compliance,
1037 SideBySide,
1038 GraphChanges,
1039 Source,
1040}
1041
1042impl TabKind {
1043 #[must_use]
1044 pub const fn title(&self) -> &'static str {
1045 match self {
1046 Self::Summary => "Summary",
1047 Self::Overview => "Overview",
1048 Self::Tree => "Tree",
1049 Self::Components => "Components",
1050 Self::Dependencies => "Dependencies",
1051 Self::Licenses => "Licenses",
1052 Self::Vulnerabilities => "Vulnerabilities",
1053 Self::Quality => "Quality",
1054 Self::Compliance => "Compliance",
1055 Self::SideBySide => "Side-by-Side",
1056 Self::GraphChanges => "Graph",
1057 Self::Source => "Source",
1058 }
1059 }
1060
1061 #[must_use]
1063 pub const fn as_str(&self) -> &'static str {
1064 match self {
1065 Self::Summary => "summary",
1066 Self::Overview => "overview",
1067 Self::Tree => "tree",
1068 Self::Components => "components",
1069 Self::Dependencies => "dependencies",
1070 Self::Licenses => "licenses",
1071 Self::Vulnerabilities => "vulnerabilities",
1072 Self::Quality => "quality",
1073 Self::Compliance => "compliance",
1074 Self::SideBySide => "side-by-side",
1075 Self::GraphChanges => "graph",
1076 Self::Source => "source",
1077 }
1078 }
1079
1080 #[must_use]
1082 pub fn from_str_opt(s: &str) -> Option<Self> {
1083 match s {
1084 "summary" => Some(Self::Summary),
1085 "overview" => Some(Self::Overview),
1086 "tree" => Some(Self::Tree),
1087 "components" => Some(Self::Components),
1088 "dependencies" => Some(Self::Dependencies),
1089 "licenses" => Some(Self::Licenses),
1090 "vulnerabilities" => Some(Self::Vulnerabilities),
1091 "quality" => Some(Self::Quality),
1092 "compliance" => Some(Self::Compliance),
1093 "side-by-side" => Some(Self::SideBySide),
1094 "graph" => Some(Self::GraphChanges),
1095 "source" => Some(Self::Source),
1096 _ => None,
1097 }
1098 }
1099
1100 #[must_use]
1102 pub const fn tabs_for_mode(mode: AppMode) -> &'static [TabKind] {
1103 match mode {
1104 AppMode::View => &[
1105 TabKind::Overview,
1106 TabKind::Tree,
1107 TabKind::Vulnerabilities,
1108 TabKind::Licenses,
1109 TabKind::Dependencies,
1110 TabKind::Quality,
1111 TabKind::Compliance,
1112 TabKind::Source,
1113 ],
1114 AppMode::Diff => &[
1115 TabKind::Summary,
1116 TabKind::Components,
1117 TabKind::Dependencies,
1118 TabKind::Licenses,
1119 TabKind::Vulnerabilities,
1120 TabKind::Quality,
1121 TabKind::Compliance,
1122 TabKind::SideBySide,
1123 TabKind::GraphChanges,
1124 TabKind::Source,
1125 ],
1126 AppMode::MultiDiff | AppMode::Timeline | AppMode::Matrix => &[],
1128 }
1129 }
1130}