1use crate::global::{GlobalCheck, GlobalSource};
2use crate::uv_python::UvPythonCheck;
3use check_updates_core::UpdateSeverity;
4use colored::Colorize;
5use std::collections::BTreeMap;
6
7pub use check_updates_core::TableRenderer;
9
10pub 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 pub fn render(&self, checks: &[GlobalCheck]) {
22 if checks.is_empty() {
23 return;
24 }
25
26 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 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 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 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 fn render_group_or_uptodate(&self, header: &str, checks: &[&GlobalCheck]) {
82 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 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 let mut sorted_checks = checks.to_vec();
110 sorted_checks.sort_by_key(|a| a.package.name.to_lowercase());
111
112 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
153pub 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 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 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 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}