Skip to main content

pcu/
output.rs

1use crate::global::{GlobalCheck, GlobalSource};
2use crate::uv_python::UvPythonCheck;
3use check_updates_core::UpdateSeverity;
4use colored::Colorize;
5use std::collections::BTreeMap;
6
7// Re-export TableRenderer from core for convenience
8pub use check_updates_core::TableRenderer;
9
10/// Renders global package check results grouped by source
11pub struct GlobalTableRenderer {
12    show_colors: bool,
13}
14
15impl GlobalTableRenderer {
16    pub fn new(show_colors: bool) -> Self {
17        Self { show_colors }
18    }
19
20    /// Render the global results table grouped by source
21    pub fn render(&self, checks: &[GlobalCheck]) {
22        if checks.is_empty() {
23            return;
24        }
25
26        // Group ALL checks by source (not just those with updates)
27        let mut uv_checks: Vec<&GlobalCheck> = Vec::new();
28        let mut pipx_checks: Vec<&GlobalCheck> = Vec::new();
29        let mut pip_by_python: BTreeMap<String, Vec<&GlobalCheck>> = BTreeMap::new();
30
31        for check in checks {
32            match &check.package.source {
33                GlobalSource::Uv => uv_checks.push(check),
34                GlobalSource::Pipx => pipx_checks.push(check),
35                GlobalSource::PipUser => {
36                    let py_version = check
37                        .package
38                        .python_version
39                        .clone()
40                        .unwrap_or_else(|| "unknown".to_string());
41                    pip_by_python
42                        .entry(py_version)
43                        .or_default()
44                        .push(check);
45                }
46            }
47        }
48
49        let mut first_group = true;
50
51        // Render uv tools
52        if !uv_checks.is_empty() {
53            if !first_group {
54                println!();
55            }
56            first_group = false;
57            self.render_group_or_uptodate("uv tools:", &uv_checks);
58        }
59
60        // Render pipx
61        if !pipx_checks.is_empty() {
62            if !first_group {
63                println!();
64            }
65            first_group = false;
66            self.render_group_or_uptodate("pipx:", &pipx_checks);
67        }
68
69        // Render pip --user grouped by Python version
70        for (py_version, pip_checks) in &pip_by_python {
71            if !first_group {
72                println!();
73            }
74            first_group = false;
75            let header = format!("pip --user (Python {py_version}):");
76            self.render_group_or_uptodate(&header, pip_checks);
77        }
78    }
79
80    /// Render a group, showing "All packages up to date." if no updates
81    fn render_group_or_uptodate(&self, header: &str, checks: &[&GlobalCheck]) {
82        // Filter to only those with updates
83        let updates: Vec<&GlobalCheck> = checks.iter().filter(|c| c.has_update).copied().collect();
84
85        println!("{header}");
86
87        if updates.is_empty() {
88            println!("  All packages up to date.");
89        } else {
90            self.render_group_rows(&updates);
91        }
92    }
93
94    fn render_group_rows(&self, checks: &[&GlobalCheck]) {
95        // Calculate widths
96        let max_name = checks.iter().map(|c| c.package.name.len()).max().unwrap_or(0);
97        let max_installed = checks
98            .iter()
99            .map(|c| c.package.installed_version.to_string().len())
100            .max()
101            .unwrap_or(0);
102        let max_latest = checks
103            .iter()
104            .map(|c| c.latest.to_string().len())
105            .max()
106            .unwrap_or(0);
107
108        // Sort checks by package name
109        let mut sorted_checks = checks.to_vec();
110        sorted_checks.sort_by_key(|a| a.package.name.to_lowercase());
111
112        // Print each row (indented)
113        for check in sorted_checks {
114            let severity_str = match check.update_severity() {
115                Some(UpdateSeverity::Major) => {
116                    if self.show_colors {
117                        "MAJOR".red().to_string()
118                    } else {
119                        "MAJOR".to_string()
120                    }
121                }
122                Some(UpdateSeverity::Minor) => {
123                    if self.show_colors {
124                        "minor".yellow().to_string()
125                    } else {
126                        "minor".to_string()
127                    }
128                }
129                Some(UpdateSeverity::Patch) => {
130                    if self.show_colors {
131                        "patch".green().to_string()
132                    } else {
133                        "patch".to_string()
134                    }
135                }
136                None => String::new(),
137            };
138
139            println!(
140                "  {:<name_w$}  {:>inst_w$} → {:<to_w$}  {}",
141                check.package.name,
142                check.package.installed_version.to_string(),
143                check.latest.to_string(),
144                severity_str,
145                name_w = max_name,
146                inst_w = max_installed,
147                to_w = max_latest,
148            );
149        }
150    }
151}
152
153/// Renders uv-managed Python version checks
154pub struct UvPythonTableRenderer {
155    show_colors: bool,
156}
157
158impl UvPythonTableRenderer {
159    pub fn new(show_colors: bool) -> Self {
160        Self { show_colors }
161    }
162
163    pub fn render(&self, checks: &[UvPythonCheck]) {
164        if checks.is_empty() {
165            return;
166        }
167
168        // Filter to only versions with updates
169        let updates: Vec<&UvPythonCheck> = checks.iter().filter(|c| c.has_update).collect();
170
171        println!("uv-managed Python installations:");
172
173        if updates.is_empty() {
174            println!("  All Python versions up to date.");
175            return;
176        }
177
178        // Calculate column widths
179        let max_series = updates.iter().map(|c| c.series.len()).max().unwrap_or(0);
180        let max_installed = updates
181            .iter()
182            .map(|c| c.installed_version.to_string().len())
183            .max()
184            .unwrap_or(0);
185
186        // Print rows sorted by series
187        let mut sorted = updates.clone();
188        sorted.sort_by(|a, b| a.series.cmp(&b.series));
189
190        for check in sorted {
191            let severity_str = if self.show_colors {
192                if check.is_patch_update() {
193                    "patch".green().to_string()
194                } else {
195                    "minor".yellow().to_string()
196                }
197            } else if check.is_patch_update() {
198                "patch".to_string()
199            } else {
200                "minor".to_string()
201            };
202
203            println!(
204                "  {:<series_w$}  {:>inst_w$} → {}  {}",
205                check.series,
206                check.installed_version.to_string(),
207                check.latest_version,
208                severity_str,
209                series_w = max_series,
210                inst_w = max_installed,
211            );
212        }
213    }
214}