Skip to main content

ncu/
output.rs

1use crate::global::{GlobalCheck, GlobalSource};
2use check_updates_core::UpdateSeverity;
3use colored::Colorize;
4
5// Re-export TableRenderer from core for convenience
6pub use check_updates_core::TableRenderer;
7
8/// Renders global package check results grouped by source
9pub struct GlobalTableRenderer {
10    show_colors: bool,
11}
12
13impl GlobalTableRenderer {
14    pub fn new(show_colors: bool) -> Self {
15        Self { show_colors }
16    }
17
18    /// Render the global results table grouped by source
19    pub fn render(&self, checks: &[GlobalCheck]) {
20        if checks.is_empty() {
21            return;
22        }
23
24        // Group by source (currently only npm, but extensible)
25        let mut by_source: std::collections::HashMap<&GlobalSource, Vec<&GlobalCheck>> =
26            std::collections::HashMap::new();
27
28        for check in checks {
29            by_source
30                .entry(&check.package.source)
31                .or_default()
32                .push(check);
33        }
34
35        let mut first_group = true;
36
37        if let Some(npm_checks) = by_source.get(&GlobalSource::Npm) {
38            if !first_group {
39                println!();
40            }
41            first_group = false;
42            self.render_group_or_uptodate("npm global:", npm_checks);
43        }
44
45        // Suppress unused variable warning — first_group will be used when more sources are added
46        let _ = first_group;
47    }
48
49    fn render_group_or_uptodate(&self, header: &str, checks: &[&GlobalCheck]) {
50        let updates: Vec<&GlobalCheck> = checks.iter().filter(|c| c.has_update).copied().collect();
51
52        println!("{header}");
53
54        if updates.is_empty() {
55            println!("  All packages up to date.");
56        } else {
57            self.render_group_rows(&updates);
58        }
59    }
60
61    fn render_group_rows(&self, checks: &[&GlobalCheck]) {
62        let max_name = checks.iter().map(|c| c.package.name.len()).max().unwrap_or(0);
63        let max_installed = checks
64            .iter()
65            .map(|c| c.package.installed_version.to_string().len())
66            .max()
67            .unwrap_or(0);
68        let max_latest = checks
69            .iter()
70            .map(|c| c.latest.to_string().len())
71            .max()
72            .unwrap_or(0);
73
74        let mut sorted_checks = checks.to_vec();
75        sorted_checks.sort_by_key(|a| a.package.name.to_lowercase());
76
77        for check in sorted_checks {
78            let severity_str = match check.update_severity() {
79                Some(UpdateSeverity::Major) => {
80                    if self.show_colors {
81                        "MAJOR".red().to_string()
82                    } else {
83                        "MAJOR".to_string()
84                    }
85                }
86                Some(UpdateSeverity::Minor) => {
87                    if self.show_colors {
88                        "minor".yellow().to_string()
89                    } else {
90                        "minor".to_string()
91                    }
92                }
93                Some(UpdateSeverity::Patch) => {
94                    if self.show_colors {
95                        "patch".green().to_string()
96                    } else {
97                        "patch".to_string()
98                    }
99                }
100                None => String::new(),
101            };
102
103            println!(
104                "  {:<name_w$}  {:>inst_w$} \u{2192} {:<to_w$}  {}",
105                check.package.name,
106                check.package.installed_version.to_string(),
107                check.latest.to_string(),
108                severity_str,
109                name_w = max_name,
110                inst_w = max_installed,
111                to_w = max_latest,
112            );
113        }
114    }
115}