python_check_updates/
output.rs

1use crate::global::{GlobalCheck, GlobalSource};
2use crate::resolver::{DependencyCheck, UpdateSeverity};
3use crate::uv_python::UvPythonCheck;
4use colored::Colorize;
5use std::collections::BTreeMap;
6
7/// Column widths for table layout
8struct ColumnWidths {
9    package: usize,
10    defined: usize,
11    installed: usize,
12    in_range: usize,
13    latest: usize,
14    update_to: usize,
15}
16
17/// Renders the dependency check results as a table
18pub struct TableRenderer {
19    show_colors: bool,
20}
21
22impl TableRenderer {
23    pub fn new(show_colors: bool) -> Self {
24        Self { show_colors }
25    }
26
27    /// Render the results table
28    pub fn render(&self, checks: &[DependencyCheck]) {
29        // Filter to only show rows with updates
30        let checks_with_updates: Vec<&DependencyCheck> = checks
31            .iter()
32            .filter(|check| check.has_update())
33            .collect();
34
35        if checks_with_updates.is_empty() {
36            return;
37        }
38
39        // Calculate column widths
40        let widths = self.calculate_widths(&checks_with_updates);
41
42        // Print header
43        self.print_header(&widths);
44
45        // Print each row
46        for check in checks_with_updates {
47            self.print_row(check, &widths);
48        }
49    }
50
51    /// Calculate the maximum width needed for each column
52    fn calculate_widths(&self, checks: &[&DependencyCheck]) -> ColumnWidths {
53        let mut widths = ColumnWidths {
54            package: "Package".len(),
55            defined: "Defined".len(),
56            installed: "Installed".len(),
57            in_range: "In Range".len(),
58            latest: "Latest".len(),
59            update_to: "Update To".len(),
60        };
61
62        for check in checks {
63            widths.package = widths.package.max(check.dependency.name.len());
64            widths.defined = widths.defined.max(check.dependency.version_spec.to_string().len());
65
66            let installed_str = check.installed.as_ref()
67                .map(|v| v.to_string())
68                .unwrap_or_else(|| "-".to_string());
69            widths.installed = widths.installed.max(installed_str.len());
70
71            let in_range_str = check.in_range.as_ref()
72                .map(|v| v.to_string())
73                .unwrap_or_else(|| "-".to_string());
74            widths.in_range = widths.in_range.max(in_range_str.len());
75
76            widths.latest = widths.latest.max(check.latest.to_string().len());
77
78            let update_to_str = check.update_to.as_ref()
79                .map(|v| v.to_string())
80                .unwrap_or_else(|| "-".to_string());
81            widths.update_to = widths.update_to.max(update_to_str.len());
82        }
83
84        widths
85    }
86
87    /// Print the header
88    fn print_header(&self, widths: &ColumnWidths) {
89        println!(
90            "{:<package_w$}  {:>defined_w$}  {:>installed_w$}  {:>in_range_w$}  {:>latest_w$}  {:>update_to_w$}",
91            "Package",
92            "Defined",
93            "Installed",
94            "In Range",
95            "Latest",
96            "Update To",
97            package_w = widths.package,
98            defined_w = widths.defined,
99            installed_w = widths.installed,
100            in_range_w = widths.in_range,
101            latest_w = widths.latest,
102            update_to_w = widths.update_to,
103        );
104    }
105
106    /// Print a single row
107    fn print_row(&self, check: &DependencyCheck, widths: &ColumnWidths) {
108        let installed = check.installed.as_ref()
109            .map(|v| v.to_string())
110            .unwrap_or_else(|| "-".to_string());
111
112        let in_range = check.in_range.as_ref()
113            .map(|v| v.to_string())
114            .unwrap_or_else(|| "-".to_string());
115
116        let update_to = check.update_to.as_ref()
117            .map(|v| v.to_string())
118            .unwrap_or_else(|| "-".to_string());
119
120        // Get severity for coloring the update_to column
121        let severity = check.update_severity();
122        let colored_update = self.colorize(&update_to, severity);
123
124        println!(
125            "{:<package_w$}  {:>defined_w$}  {:>installed_w$}  {:>in_range_w$}  {:>latest_w$}  {:>update_to_w$}",
126            check.dependency.name,
127            check.dependency.version_spec.to_string(),
128            installed,
129            in_range,
130            check.latest.to_string(),
131            colored_update,
132            package_w = widths.package,
133            defined_w = widths.defined,
134            installed_w = widths.installed,
135            in_range_w = widths.in_range,
136            latest_w = widths.latest,
137            update_to_w = widths.update_to,
138        );
139    }
140
141    /// Colorize text based on update severity
142    fn colorize(&self, text: &str, severity: Option<UpdateSeverity>) -> String {
143        if !self.show_colors {
144            return text.to_string();
145        }
146
147        match severity {
148            Some(UpdateSeverity::Major) => text.red().to_string(),
149            Some(UpdateSeverity::Minor) => text.yellow().to_string(),
150            Some(UpdateSeverity::Patch) => text.green().to_string(),
151            None => text.to_string(),
152        }
153    }
154}
155
156/// Column widths for global table layout (3 columns)
157struct GlobalColumnWidths {
158    package: usize,
159    installed: usize,
160    latest: usize,
161}
162
163/// Renders global package check results grouped by source
164pub struct GlobalTableRenderer {
165    show_colors: bool,
166}
167
168impl GlobalTableRenderer {
169    pub fn new(show_colors: bool) -> Self {
170        Self { show_colors }
171    }
172
173    /// Render the global results table grouped by source
174    pub fn render(&self, checks: &[GlobalCheck]) {
175        if checks.is_empty() {
176            return;
177        }
178
179        // Group ALL checks by source (not just those with updates)
180        let mut uv_checks: Vec<&GlobalCheck> = Vec::new();
181        let mut pipx_checks: Vec<&GlobalCheck> = Vec::new();
182        let mut pip_by_python: BTreeMap<String, Vec<&GlobalCheck>> = BTreeMap::new();
183
184        for check in checks {
185            match &check.package.source {
186                GlobalSource::Uv => uv_checks.push(check),
187                GlobalSource::Pipx => pipx_checks.push(check),
188                GlobalSource::PipUser => {
189                    let py_version = check
190                        .package
191                        .python_version
192                        .clone()
193                        .unwrap_or_else(|| "unknown".to_string());
194                    pip_by_python
195                        .entry(py_version)
196                        .or_insert_with(Vec::new)
197                        .push(check);
198                }
199            }
200        }
201
202        let mut first_group = true;
203
204        // Render uv tools
205        if !uv_checks.is_empty() {
206            if !first_group {
207                println!();
208            }
209            first_group = false;
210            self.render_group_or_uptodate("uv tools:", &uv_checks);
211        }
212
213        // Render pipx
214        if !pipx_checks.is_empty() {
215            if !first_group {
216                println!();
217            }
218            first_group = false;
219            self.render_group_or_uptodate("pipx:", &pipx_checks);
220        }
221
222        // Render pip --user grouped by Python version
223        for (py_version, pip_checks) in &pip_by_python {
224            if !first_group {
225                println!();
226            }
227            first_group = false;
228            let header = format!("pip --user (Python {}):", py_version);
229            self.render_group_or_uptodate(&header, pip_checks);
230        }
231    }
232
233    /// Render a group, showing "All packages up to date." if no updates
234    fn render_group_or_uptodate(&self, header: &str, checks: &[&GlobalCheck]) {
235        // Filter to only those with updates
236        let updates: Vec<&GlobalCheck> = checks.iter().filter(|c| c.has_update).copied().collect();
237
238        println!("{}", header);
239
240        if updates.is_empty() {
241            println!("  All packages up to date.");
242        } else {
243            let widths = self.calculate_widths(&updates);
244            self.render_group_rows(&updates, &widths);
245        }
246    }
247
248    fn render_group_rows(&self, checks: &[&GlobalCheck], widths: &GlobalColumnWidths) {
249        // Print column headers (indented)
250        println!(
251            "  {:<pkg_w$}  {:>inst_w$}  {:>latest_w$}",
252            "Package",
253            "Installed",
254            "Latest",
255            pkg_w = widths.package,
256            inst_w = widths.installed,
257            latest_w = widths.latest,
258        );
259
260        // Sort checks by package name
261        let mut sorted_checks = checks.to_vec();
262        sorted_checks.sort_by(|a, b| a.package.name.to_lowercase().cmp(&b.package.name.to_lowercase()));
263
264        // Print each row (indented)
265        for check in sorted_checks {
266            self.print_row(check, widths);
267        }
268    }
269
270    fn calculate_widths(&self, checks: &[&GlobalCheck]) -> GlobalColumnWidths {
271        let mut widths = GlobalColumnWidths {
272            package: "Package".len(),
273            installed: "Installed".len(),
274            latest: "Latest".len(),
275        };
276
277        for check in checks {
278            widths.package = widths.package.max(check.package.name.len());
279            widths.installed = widths
280                .installed
281                .max(check.package.installed_version.to_string().len());
282            widths.latest = widths.latest.max(check.latest.to_string().len());
283        }
284
285        widths
286    }
287
288    fn print_row(&self, check: &GlobalCheck, widths: &GlobalColumnWidths) {
289        let latest_str = check.latest.to_string();
290        let colored_latest = self.colorize(&latest_str, check.update_severity());
291
292        println!(
293            "  {:<pkg_w$}  {:>inst_w$}  {:>latest_w$}",
294            check.package.name,
295            check.package.installed_version.to_string(),
296            colored_latest,
297            pkg_w = widths.package,
298            inst_w = widths.installed,
299            latest_w = widths.latest,
300        );
301    }
302
303    fn colorize(&self, text: &str, severity: Option<UpdateSeverity>) -> String {
304        if !self.show_colors {
305            return text.to_string();
306        }
307
308        match severity {
309            Some(UpdateSeverity::Major) => text.red().to_string(),
310            Some(UpdateSeverity::Minor) => text.yellow().to_string(),
311            Some(UpdateSeverity::Patch) => text.green().to_string(),
312            None => text.to_string(),
313        }
314    }
315}
316
317/// Column widths for uv Python version table
318struct UvPythonColumnWidths {
319    series: usize,
320    installed: usize,
321    latest: usize,
322}
323
324/// Renders uv-managed Python version checks
325pub struct UvPythonTableRenderer {
326    show_colors: bool,
327}
328
329impl UvPythonTableRenderer {
330    pub fn new(show_colors: bool) -> Self {
331        Self { show_colors }
332    }
333
334    pub fn render(&self, checks: &[UvPythonCheck]) {
335        if checks.is_empty() {
336            return;
337        }
338
339        // Filter to only versions with updates
340        let updates: Vec<&UvPythonCheck> = checks.iter().filter(|c| c.has_update).collect();
341
342        println!("uv-managed Python installations:");
343
344        if updates.is_empty() {
345            println!("  All Python versions up to date.");
346            return;
347        }
348
349        // Calculate column widths
350        let widths = self.calculate_widths(&updates);
351
352        // Print header (indented like global renderer)
353        println!(
354            "  {:<series_w$}  {:>installed_w$}  {:>latest_w$}",
355            "Series",
356            "Installed",
357            "Latest",
358            series_w = widths.series,
359            installed_w = widths.installed,
360            latest_w = widths.latest,
361        );
362
363        // Print rows sorted by series
364        let mut sorted = updates.to_vec();
365        sorted.sort_by(|a, b| a.series.cmp(&b.series));
366
367        for check in sorted {
368            self.print_row(check, &widths);
369        }
370    }
371
372    fn calculate_widths(&self, checks: &[&UvPythonCheck]) -> UvPythonColumnWidths {
373        let mut widths = UvPythonColumnWidths {
374            series: "Series".len(),
375            installed: "Installed".len(),
376            latest: "Latest".len(),
377        };
378
379        for check in checks {
380            widths.series = widths.series.max(check.series.len());
381            widths.installed = widths
382                .installed
383                .max(check.installed_version.to_string().len());
384            widths.latest = widths.latest.max(check.latest_version.to_string().len());
385        }
386
387        widths
388    }
389
390    fn print_row(&self, check: &UvPythonCheck, widths: &UvPythonColumnWidths) {
391        let latest_str = check.latest_version.to_string();
392
393        // Color the latest version based on update type
394        let colored_latest = if self.show_colors {
395            if check.is_patch_update() {
396                latest_str.green().to_string()
397            } else {
398                // Minor/major update (rare for Python, but handle it)
399                latest_str.yellow().to_string()
400            }
401        } else {
402            latest_str
403        };
404
405        println!(
406            "  {:<series_w$}  {:>installed_w$}  {:>latest_w$}",
407            check.series,
408            check.installed_version.to_string(),
409            colored_latest,
410            series_w = widths.series,
411            installed_w = widths.installed,
412            latest_w = widths.latest,
413        );
414    }
415}