use owo_colors::OwoColorize;
use crate::cli::ColorScheme;
use crate::version::compare::Update;
use crate::version::constraint::{extract_base_version, get_prefix};
struct Palette {
major: (u8, u8, u8),
minor: (u8, u8, u8),
patch: (u8, u8, u8),
}
fn palette_for(scheme: &ColorScheme) -> Palette {
match scheme {
ColorScheme::Default => Palette {
major: (215, 58, 73), minor: (3, 102, 214), patch: (40, 167, 69), },
ColorScheme::OkabeIto => Palette {
major: (230, 159, 0), minor: (0, 114, 178), patch: (0, 158, 115), },
ColorScheme::TrafficLight => Palette {
major: (231, 76, 60), minor: (241, 196, 15), patch: (46, 204, 113), },
ColorScheme::Severity => Palette {
major: (142, 68, 173), minor: (52, 152, 219), patch: (149, 165, 166), },
ColorScheme::HighContrast => Palette {
major: (204, 121, 167), minor: (0, 114, 178), patch: (240, 228, 66), },
}
}
pub fn print_table(updates: &[Update], summary: bool, color_scheme: &ColorScheme) {
if updates.is_empty() {
println!("All dependencies are up to date.");
return;
}
let name_w = updates
.iter()
.map(|u| u.name.len())
.max()
.unwrap_or(8)
.max(8);
let curr_w = updates
.iter()
.map(|u| u.current.len())
.max()
.unwrap_or(8)
.max(8);
println!();
for u in updates {
let name_padded = format!("{:<width$}", u.name, width = name_w);
let curr_padded = format!("{:>width$}", u.current, width = curr_w);
let colored_new =
color_updated_constraint(&u.current, &u.latest, &u.updated_constraint, color_scheme);
println!(
"{} {} {} {}",
name_padded.bold(),
curr_padded.dimmed(),
"→".cyan(),
colored_new,
);
}
println!();
if summary {
let n = updates.len();
if n == 1 {
println!("1 package can be updated.");
} else {
println!("{} packages can be updated.", n);
}
}
}
pub fn print_color_scheme_preview() {
let schemes: &[(&str, ColorScheme, &str, [&str; 3])] = &[
(
"default",
ColorScheme::Default,
"SemVer severity - GitHub style",
["red #D73A49", "blue #0366D6", "green #28A745"],
),
(
"okabe-ito",
ColorScheme::OkabeIto,
"Color-blind safe - Okabe-Ito palette",
["orange #E69F00", "blue #0072B2", "teal #009E73"],
),
(
"traffic-light",
ColorScheme::TrafficLight,
"Traffic-light - common CI/dashboard model",
["red #E74C3C", "yellow #F1C40F", "green #2ECC71"],
),
(
"severity",
ColorScheme::Severity,
"Monitoring style - Datadog/Grafana inspired",
["purple #8E44AD", "blue #3498DB", "gray #95A5A6"],
),
(
"high-contrast",
ColorScheme::HighContrast,
"High-contrast accessibility - color-blind safe",
["magenta #CC79A7", "blue #0072B2", "yellow #F0E442"],
),
];
let samples: &[(&str, &str, &str, &str)] = &[
("1.0.0", "2.0.0", "2.0.0", "major"),
("1.0.0", "1.1.0", "1.1.0", "minor"),
("1.0.0", "1.0.1", "1.0.1", "patch"),
];
println!();
println!(
"{}",
"Available color schemes (use --set-color-scheme <SCHEME>):".bold()
);
for (name, scheme, description, color_labels) in schemes {
println!();
println!(" {} - {}", name.bold().underline(), description.dimmed());
for (i, (current, latest, updated, bump)) in samples.iter().enumerate() {
let colored = color_updated_constraint(current, latest, updated, scheme);
let label_text = format!("{} ({})", bump, color_labels[i]);
let label = label_text.dimmed();
println!(
" {} {} {} {} {}",
"my-package".bold(),
current.dimmed(),
"→".cyan(),
colored,
label,
);
}
}
println!();
}
fn color_updated_constraint(
current: &str,
latest: &str,
updated_constraint: &str,
color_scheme: &ColorScheme,
) -> String {
let old_base = match extract_base_version(current) {
Some(v) => v,
None => return updated_constraint.to_string(),
};
let old_parts: Vec<&str> = old_base.split('.').collect();
let new_parts: Vec<&str> = latest.split('.').collect();
let diff_idx = old_parts
.iter()
.zip(new_parts.iter())
.position(|(a, b)| a != b)
.unwrap_or(new_parts.len());
let lower_part = if updated_constraint.contains(',') {
updated_constraint
.split_once(',')
.map_or(updated_constraint, |(before, _)| before)
} else {
updated_constraint
};
let prefix = get_prefix(lower_part);
let p = palette_for(color_scheme);
let colored_version = match diff_idx {
0 => {
let (r, g, b) = p.major;
latest.truecolor(r, g, b).to_string()
}
1 => {
let (r, g, b) = p.minor;
let plain = format!("{}.", new_parts[0]);
let colored = new_parts[1..].join(".").truecolor(r, g, b).to_string();
format!("{}{}", plain, colored)
}
2 => {
let (r, g, b) = p.patch;
let plain = format!("{}.{}.", new_parts[0], new_parts[1]);
let colored = new_parts[2..].join(".").truecolor(r, g, b).to_string();
format!("{}{}", plain, colored)
}
_ => latest.to_string(),
};
let upper_part = if let Some(comma) = updated_constraint.find(',') {
updated_constraint[comma..].to_string().dimmed().to_string()
} else {
String::new()
};
format!("{}{}{}", prefix, colored_version, upper_part)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::ColorScheme;
use crate::version::compare::{BumpKind, Update};
fn make_update(
name: &str,
current: &str,
latest: &str,
updated: &str,
bump: BumpKind,
) -> Update {
Update {
name: name.to_string(),
current: current.to_string(),
latest: latest.to_string(),
updated_constraint: updated.to_string(),
bump_kind: bump,
}
}
#[test]
fn test_print_color_scheme_preview_does_not_panic() {
print_color_scheme_preview();
}
#[test]
fn test_print_table_empty() {
print_table(&[], false, &ColorScheme::Default);
print_table(&[], true, &ColorScheme::Default);
}
#[test]
fn test_print_table_with_updates() {
let updates = vec![
make_update(
"fastapi",
">=0.109.0",
"0.135.1",
">=0.135.1",
BumpKind::Minor,
),
make_update("pydantic", ">=1.0.0", "2.0.0", ">=2.0.0", BumpKind::Major),
];
for scheme in &[
ColorScheme::Default,
ColorScheme::OkabeIto,
ColorScheme::TrafficLight,
ColorScheme::Severity,
ColorScheme::HighContrast,
] {
print_table(&updates, true, scheme);
}
}
#[test]
fn test_print_table_single_package_summary() {
let updates = vec![make_update(
"fastapi",
">=0.109.0",
"0.135.1",
">=0.135.1",
BumpKind::Minor,
)];
print_table(&updates, true, &ColorScheme::Default);
}
#[test]
fn test_color_updated_constraint_major() {
let result = color_updated_constraint(">=1.0.0", "2.0.0", ">=2.0.0", &ColorScheme::Default);
assert!(result.contains("2.0.0"));
assert!(result.contains(">="));
}
#[test]
fn test_color_updated_constraint_minor() {
let result =
color_updated_constraint(">=0.109.0", "0.110.0", ">=0.110.0", &ColorScheme::Default);
assert!(result.contains("0."));
assert!(result.contains("110"));
}
#[test]
fn test_color_updated_constraint_patch() {
let result = color_updated_constraint(">=1.0.0", "1.0.1", ">=1.0.1", &ColorScheme::Default);
assert!(result.contains("1.0."));
}
#[test]
fn test_color_updated_constraint_compound() {
let result = color_updated_constraint(
">=0.7.3,<0.8.0",
"1.0.0",
">=1.0.0,<2.0.0",
&ColorScheme::Default,
);
assert!(result.contains("1.0.0"));
assert!(result.contains("2.0.0"));
}
#[test]
fn test_color_updated_constraint_no_base_version() {
let result = color_updated_constraint("*", "1.0.0", "*", &ColorScheme::Default);
assert_eq!(result, "*");
}
#[test]
fn test_color_updated_constraint_four_component_version() {
let result =
color_updated_constraint(">=1.0.0.0", "1.0.0.1", ">=1.0.0.1", &ColorScheme::Default);
assert!(result.contains("1.0.0.1"));
assert!(result.contains(">="));
}
#[test]
fn test_all_schemes_produce_output() {
let cases = [
("1.0.0", "2.0.0", "2.0.0"),
("1.0.0", "1.1.0", "1.1.0"),
("1.0.0", "1.0.1", "1.0.1"),
];
for scheme in &[
ColorScheme::Default,
ColorScheme::OkabeIto,
ColorScheme::TrafficLight,
ColorScheme::Severity,
ColorScheme::HighContrast,
] {
for (cur, _lat, upd) in &cases {
let result = color_updated_constraint(cur, _lat, upd, scheme);
assert!(!result.is_empty());
}
}
}
}