use crate::ErrorRecommendation;
use crate::error::{BoxenError, BoxenResult};
use crate::options::Color;
use colored::{ColoredString, Colorize};
pub fn parse_color(color: &Color) -> BoxenResult<colored::Color> {
match color {
Color::Named(name) => parse_named_color(name),
Color::Hex(hex) => parse_hex_color(hex),
Color::Rgb(r, g, b) => Ok(colored::Color::TrueColor {
r: *r,
g: *g,
b: *b,
}),
}
}
pub fn parse_named_color(name: &str) -> BoxenResult<colored::Color> {
let normalized = name.to_lowercase();
match normalized.as_str() {
"black" => Ok(colored::Color::Black),
"red" => Ok(colored::Color::Red),
"green" => Ok(colored::Color::Green),
"yellow" => Ok(colored::Color::Yellow),
"blue" => Ok(colored::Color::Blue),
"magenta" | "purple" => Ok(colored::Color::Magenta),
"cyan" => Ok(colored::Color::Cyan),
"white" => Ok(colored::Color::White),
"bright_black" | "brightblack" | "gray" | "grey" => Ok(colored::Color::BrightBlack),
"bright_red" | "brightred" => Ok(colored::Color::BrightRed),
"bright_green" | "brightgreen" => Ok(colored::Color::BrightGreen),
"bright_yellow" | "brightyellow" => Ok(colored::Color::BrightYellow),
"bright_blue" | "brightblue" => Ok(colored::Color::BrightBlue),
"bright_magenta" | "brightmagenta" | "bright_purple" | "brightpurple" => {
Ok(colored::Color::BrightMagenta)
}
"bright_cyan" | "brightcyan" => Ok(colored::Color::BrightCyan),
"bright_white" | "brightwhite" => Ok(colored::Color::BrightWhite),
_ => Err(BoxenError::invalid_color(
format!("Unknown color name: {name}"),
name.to_string(),
vec![
ErrorRecommendation::suggestion_only(
"Unknown color name".to_string(),
"Use a standard terminal color name like 'red', 'blue', 'green', etc."
.to_string(),
),
ErrorRecommendation::with_auto_fix(
"Use standard color".to_string(),
"Try using 'red' as a common color".to_string(),
"\"red\"".to_string(),
),
ErrorRecommendation::suggestion_only(
"Alternative: Use hex color".to_string(),
"You can also use hex colors like '#FF0000' for red".to_string(),
),
],
)),
}
}
#[allow(clippy::too_many_lines)]
pub fn parse_hex_color(hex: &str) -> BoxenResult<colored::Color> {
let hex = hex.trim_start_matches('#');
if hex.len() != 3 && hex.len() != 6 {
return Err(BoxenError::invalid_color(
format!("Invalid hex color format: #{hex}"),
format!("#{hex}"),
vec![
ErrorRecommendation::suggestion_only(
"Invalid hex length".to_string(),
"Hex colors must be 3 or 6 characters long (e.g., #F00 or #FF0000)".to_string(),
),
ErrorRecommendation::with_auto_fix(
"Use 6-digit format".to_string(),
"Try using the full 6-digit hex format".to_string(),
"\"#FF0000\"".to_string(),
),
],
));
}
if !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(BoxenError::invalid_color(
format!("Invalid hex color format: #{hex}"),
format!("#{hex}"),
vec![
ErrorRecommendation::suggestion_only(
"Invalid hex characters".to_string(),
"Hex colors can only contain digits 0-9 and letters A-F".to_string(),
),
ErrorRecommendation::with_auto_fix(
"Use valid hex color".to_string(),
"Try using a valid hex color".to_string(),
"\"#FF0000\"".to_string(),
),
],
));
}
let (r, g, b) = if hex.len() == 3 {
let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).map_err(|_| {
BoxenError::invalid_color(
format!("Invalid hex color: #{hex}"),
format!("#{hex}"),
vec![ErrorRecommendation::with_auto_fix(
"Invalid hex format".to_string(),
"Use a valid 3-digit hex color".to_string(),
"\"#F00\"".to_string(),
)],
)
})?;
let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).map_err(|_| {
BoxenError::invalid_color(
format!("Invalid hex color: #{hex}"),
format!("#{hex}"),
vec![ErrorRecommendation::with_auto_fix(
"Invalid hex format".to_string(),
"Use a valid 3-digit hex color".to_string(),
"\"#0F0\"".to_string(),
)],
)
})?;
let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).map_err(|_| {
BoxenError::invalid_color(
format!("Invalid hex color: #{hex}"),
format!("#{hex}"),
vec![ErrorRecommendation::with_auto_fix(
"Invalid hex format".to_string(),
"Use a valid 3-digit hex color".to_string(),
"\"#00F\"".to_string(),
)],
)
})?;
(r, g, b)
} else {
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| {
BoxenError::invalid_color(
format!("Invalid hex color: #{hex}"),
format!("#{hex}"),
vec![ErrorRecommendation::with_auto_fix(
"Invalid hex format".to_string(),
"Use a valid 6-digit hex color".to_string(),
"\"#FF0000\"".to_string(),
)],
)
})?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| {
BoxenError::invalid_color(
format!("Invalid hex color: #{hex}"),
format!("#{hex}"),
vec![ErrorRecommendation::with_auto_fix(
"Invalid hex format".to_string(),
"Use a valid 6-digit hex color".to_string(),
"\"#00FF00\"".to_string(),
)],
)
})?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| {
BoxenError::invalid_color(
format!("Invalid hex color: #{hex}"),
format!("#{hex}"),
vec![ErrorRecommendation::with_auto_fix(
"Invalid hex format".to_string(),
"Use a valid 6-digit hex color".to_string(),
"\"#0000FF\"".to_string(),
)],
)
})?;
(r, g, b)
};
Ok(colored::Color::TrueColor { r, g, b })
}
pub fn validate_color(color: &Color) -> BoxenResult<()> {
parse_color(color).map(|_| ())
}
pub fn apply_foreground_color(text: &str, color: &Color) -> BoxenResult<ColoredString> {
let parsed_color = parse_color(color)?;
Ok(text.color(parsed_color))
}
pub fn apply_background_color(text: &str, color: &Color) -> BoxenResult<ColoredString> {
let parsed_color = parse_color(color)?;
Ok(text.on_color(parsed_color))
}
pub fn apply_colors(
text: &str,
fg_color: Option<&Color>,
bg_color: Option<&Color>,
) -> BoxenResult<ColoredString> {
let mut styled = ColoredString::from(text);
if let Some(fg) = fg_color {
let parsed_fg = parse_color(fg)?;
styled = styled.color(parsed_fg);
}
if let Some(bg) = bg_color {
let parsed_bg = parse_color(bg)?;
styled = styled.on_color(parsed_bg);
}
Ok(styled)
}
#[must_use]
pub fn apply_dim(text: &str) -> ColoredString {
text.dimmed()
}
pub fn apply_color_with_dim(
text: &str,
color: Option<&Color>,
dim: bool,
) -> BoxenResult<ColoredString> {
let mut styled = if let Some(c) = color {
apply_foreground_color(text, c)?
} else {
ColoredString::from(text)
};
if dim {
styled = styled.dimmed();
}
Ok(styled)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::options::Color;
#[test]
fn test_parse_named_colors_basic() {
assert!(parse_named_color("red").is_ok());
assert!(parse_named_color("green").is_ok());
assert!(parse_named_color("blue").is_ok());
assert!(parse_named_color("black").is_ok());
assert!(parse_named_color("white").is_ok());
assert!(parse_named_color("yellow").is_ok());
assert!(parse_named_color("magenta").is_ok());
assert!(parse_named_color("cyan").is_ok());
}
#[test]
fn test_parse_named_colors_bright() {
assert!(parse_named_color("bright_red").is_ok());
assert!(parse_named_color("brightred").is_ok());
assert!(parse_named_color("bright_green").is_ok());
assert!(parse_named_color("bright_blue").is_ok());
assert!(parse_named_color("gray").is_ok());
assert!(parse_named_color("grey").is_ok());
assert!(parse_named_color("bright_black").is_ok());
assert!(parse_named_color("brightblack").is_ok());
}
#[test]
fn test_parse_named_colors_aliases() {
assert!(parse_named_color("purple").is_ok());
assert!(parse_named_color("bright_purple").is_ok());
assert!(parse_named_color("brightpurple").is_ok());
}
#[test]
fn test_parse_named_colors_case_insensitive() {
assert!(parse_named_color("RED").is_ok());
assert!(parse_named_color("Red").is_ok());
assert!(parse_named_color("rEd").is_ok());
assert!(parse_named_color("BRIGHT_BLUE").is_ok());
assert!(parse_named_color("Bright_Blue").is_ok());
}
#[test]
fn test_parse_named_colors_invalid() {
assert!(parse_named_color("invalid_color").is_err());
assert!(parse_named_color("").is_err());
assert!(parse_named_color("orange").is_err());
assert!(parse_named_color("pink").is_err());
}
#[test]
fn test_parse_hex_colors_long_format() {
assert!(parse_hex_color("#FF0000").is_ok()); assert!(parse_hex_color("#00FF00").is_ok()); assert!(parse_hex_color("#0000FF").is_ok()); assert!(parse_hex_color("#FFFFFF").is_ok()); assert!(parse_hex_color("#000000").is_ok()); assert!(parse_hex_color("#123456").is_ok()); assert!(parse_hex_color("#ABCDEF").is_ok()); assert!(parse_hex_color("#abcdef").is_ok()); }
#[test]
fn test_parse_hex_colors_short_format() {
assert!(parse_hex_color("#F00").is_ok()); assert!(parse_hex_color("#0F0").is_ok()); assert!(parse_hex_color("#00F").is_ok()); assert!(parse_hex_color("#FFF").is_ok()); assert!(parse_hex_color("#000").is_ok()); assert!(parse_hex_color("#123").is_ok()); assert!(parse_hex_color("#ABC").is_ok()); assert!(parse_hex_color("#abc").is_ok()); }
#[test]
fn test_parse_hex_colors_without_hash() {
assert!(parse_hex_color("FF0000").is_ok());
assert!(parse_hex_color("F00").is_ok());
assert!(parse_hex_color("123456").is_ok());
assert!(parse_hex_color("ABC").is_ok());
}
#[test]
fn test_parse_hex_colors_invalid_length() {
assert!(parse_hex_color("#FF").is_err()); assert!(parse_hex_color("#FFFF").is_err()); assert!(parse_hex_color("#FFFFF").is_err()); assert!(parse_hex_color("#FFFFFFF").is_err()); assert!(parse_hex_color("").is_err()); assert!(parse_hex_color("#").is_err()); }
#[test]
fn test_parse_hex_colors_invalid_characters() {
assert!(parse_hex_color("#GGGGGG").is_err()); assert!(parse_hex_color("#FF00ZZ").is_err()); assert!(parse_hex_color("#FF 000").is_err()); assert!(parse_hex_color("#FF-000").is_err()); assert!(parse_hex_color("#FF.000").is_err()); }
#[test]
fn test_parse_hex_color_values() {
let red = parse_hex_color("#FF0000").unwrap();
if let colored::Color::TrueColor { r, g, b } = red {
assert_eq!(r, 255);
assert_eq!(g, 0);
assert_eq!(b, 0);
} else {
panic!("Expected TrueColor");
}
let green = parse_hex_color("#00FF00").unwrap();
if let colored::Color::TrueColor { r, g, b } = green {
assert_eq!(r, 0);
assert_eq!(g, 255);
assert_eq!(b, 0);
} else {
panic!("Expected TrueColor");
}
let short_red = parse_hex_color("#F00").unwrap();
if let colored::Color::TrueColor { r, g, b } = short_red {
assert_eq!(r, 255); assert_eq!(g, 0); assert_eq!(b, 0); } else {
panic!("Expected TrueColor");
}
}
#[test]
fn test_parse_color_enum_variants() {
let named = Color::Named("red".to_string());
assert!(parse_color(&named).is_ok());
let hex = Color::Hex("#FF0000".to_string());
assert!(parse_color(&hex).is_ok());
let rgb = Color::Rgb(255, 0, 0);
let parsed = parse_color(&rgb).unwrap();
if let colored::Color::TrueColor { r, g, b } = parsed {
assert_eq!(r, 255);
assert_eq!(g, 0);
assert_eq!(b, 0);
} else {
panic!("Expected TrueColor");
}
}
#[test]
fn test_validate_color() {
assert!(validate_color(&Color::Named("red".to_string())).is_ok());
assert!(validate_color(&Color::Hex("#FF0000".to_string())).is_ok());
assert!(validate_color(&Color::Rgb(255, 0, 0)).is_ok());
assert!(validate_color(&Color::Named("invalid_color".to_string())).is_err());
assert!(validate_color(&Color::Hex("#GGGGGG".to_string())).is_err());
}
#[test]
fn test_apply_foreground_color() {
let color = Color::Named("red".to_string());
let result = apply_foreground_color("test", &color);
assert!(result.is_ok());
let invalid_color = Color::Named("invalid".to_string());
let result = apply_foreground_color("test", &invalid_color);
assert!(result.is_err());
}
#[test]
fn test_apply_background_color() {
let color = Color::Named("blue".to_string());
let result = apply_background_color("test", &color);
assert!(result.is_ok());
let invalid_color = Color::Named("invalid".to_string());
let result = apply_background_color("test", &invalid_color);
assert!(result.is_err());
}
#[test]
fn test_apply_colors_both() {
let fg_color = Color::Named("red".to_string());
let bg_color = Color::Named("blue".to_string());
let result = apply_colors("test", Some(&fg_color), Some(&bg_color));
assert!(result.is_ok());
}
#[test]
fn test_apply_colors_foreground_only() {
let fg_color = Color::Named("red".to_string());
let result = apply_colors("test", Some(&fg_color), None);
assert!(result.is_ok());
}
#[test]
fn test_apply_colors_background_only() {
let bg_color = Color::Named("blue".to_string());
let result = apply_colors("test", None, Some(&bg_color));
assert!(result.is_ok());
}
#[test]
fn test_apply_colors_neither() {
let result = apply_colors("test", None, None);
assert!(result.is_ok());
}
#[test]
fn test_apply_colors_invalid() {
let invalid_color = Color::Named("invalid".to_string());
let result = apply_colors("test", Some(&invalid_color), None);
assert!(result.is_err());
let result = apply_colors("test", None, Some(&invalid_color));
assert!(result.is_err());
}
#[test]
fn test_apply_dim() {
let result = apply_dim("test");
assert!(!result.to_string().is_empty());
}
#[test]
fn test_apply_color_with_dim() {
let color = Color::Named("red".to_string());
let result = apply_color_with_dim("test", Some(&color), true);
assert!(result.is_ok());
let result = apply_color_with_dim("test", Some(&color), false);
assert!(result.is_ok());
let result = apply_color_with_dim("test", None, true);
assert!(result.is_ok());
let result = apply_color_with_dim("test", None, false);
assert!(result.is_ok());
let invalid_color = Color::Named("invalid".to_string());
let result = apply_color_with_dim("test", Some(&invalid_color), false);
assert!(result.is_err());
}
#[test]
fn test_color_from_implementations() {
let color1: Color = "red".to_string().into();
assert!(matches!(color1, Color::Named(_)));
let color2: Color = "#FF0000".to_string().into();
assert!(matches!(color2, Color::Hex(_)));
let color3: Color = "blue".into();
assert!(matches!(color3, Color::Named(_)));
let color4: Color = "#00FF00".into();
assert!(matches!(color4, Color::Hex(_)));
let color5: Color = (255, 0, 0).into();
assert!(matches!(color5, Color::Rgb(255, 0, 0)));
}
#[test]
fn test_edge_cases() {
assert!(parse_named_color("").is_err());
assert!(parse_named_color(" red ").is_err());
assert!(parse_hex_color(" #FF0000 ").is_err());
let long_name = "a".repeat(1000);
assert!(parse_named_color(&long_name).is_err());
let rgb_max = Color::Rgb(255, 255, 255);
assert!(parse_color(&rgb_max).is_ok());
let rgb_min = Color::Rgb(0, 0, 0);
assert!(parse_color(&rgb_min).is_ok());
}
#[test]
fn test_hex_color_case_sensitivity() {
assert!(parse_hex_color("#ABCDEF").is_ok());
assert!(parse_hex_color("#abcdef").is_ok());
assert!(parse_hex_color("#AbCdEf").is_ok());
assert!(parse_hex_color("#ABC").is_ok());
assert!(parse_hex_color("#abc").is_ok());
assert!(parse_hex_color("#AbC").is_ok());
}
#[test]
fn test_comprehensive_color_validation() {
let valid_colors = vec![
Color::Named("red".to_string()),
Color::Named("BLUE".to_string()),
Color::Named("bright_green".to_string()),
Color::Hex("#FF0000".to_string()),
Color::Hex("#F00".to_string()),
Color::Hex("00FF00".to_string()),
Color::Rgb(128, 64, 192),
Color::Rgb(0, 0, 0),
Color::Rgb(255, 255, 255),
];
for color in valid_colors {
assert!(
validate_color(&color).is_ok(),
"Color should be valid: {color:?}"
);
}
let invalid_colors = vec![
Color::Named("invalid_color_name".to_string()),
Color::Named(String::new()),
Color::Hex("#GGGGGG".to_string()),
Color::Hex("#FF".to_string()),
Color::Hex("#FFFFFFF".to_string()),
Color::Hex(String::new()),
];
for color in invalid_colors {
assert!(
validate_color(&color).is_err(),
"Color should be invalid: {color:?}"
);
}
}
}