1use crate::global::{GlobalCheck, GlobalSource};
2use crate::resolver::{DependencyCheck, UpdateSeverity};
3use crate::uv_python::UvPythonCheck;
4use colored::Colorize;
5use std::collections::BTreeMap;
6
7struct ColumnWidths {
9 package: usize,
10 defined: usize,
11 installed: usize,
12 in_range: usize,
13 latest: usize,
14 update_to: usize,
15}
16
17pub 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 pub fn render(&self, checks: &[DependencyCheck]) {
29 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 let widths = self.calculate_widths(&checks_with_updates);
41
42 self.print_header(&widths);
44
45 for check in checks_with_updates {
47 self.print_row(check, &widths);
48 }
49 }
50
51 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 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 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 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 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
156struct GlobalColumnWidths {
158 package: usize,
159 installed: usize,
160 latest: usize,
161}
162
163pub 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 pub fn render(&self, checks: &[GlobalCheck]) {
175 if checks.is_empty() {
176 return;
177 }
178
179 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 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 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 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 fn render_group_or_uptodate(&self, header: &str, checks: &[&GlobalCheck]) {
235 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 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 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 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
317struct UvPythonColumnWidths {
319 series: usize,
320 installed: usize,
321 latest: usize,
322}
323
324pub 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 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 let widths = self.calculate_widths(&updates);
351
352 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 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 let colored_latest = if self.show_colors {
395 if check.is_patch_update() {
396 latest_str.green().to_string()
397 } else {
398 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}