diff --git a/examples/colored_lists.rs b/examples/colored_lists.rs new file mode 100644 index 0000000..f768f8a --- /dev/null +++ b/examples/colored_lists.rs @@ -0,0 +1,28 @@ +use colored::*; +fn main() { + let no_colors = vec![]; + let one_color = vec![Color::Red]; + let two_colors = vec![Color::Blue, Color::Green]; + + println!("{}", "Text defaults to white".gradient(&no_colors)); + println!("{}", "This text is red".gradient(&one_color)); + println!("{}", "Transition from blue to green".gradient(&two_colors)); + println!("{}", "A lot of the colors of the rainbow".rainbow()); + + println!( + "{}", + "Transition from blue to green".on_gradient(&two_colors) + ); + println!("{}", "A lot of the colors of the rainbow".on_rainbow()); + + //Test edge cases + println!("{}", "".gradient(&no_colors)); + println!("{}", "a".gradient(&no_colors)); + println!("{}", "b".gradient(&one_color)); + println!("{}", "c".gradient(&two_colors)); + println!("{}", "de".gradient(&two_colors)); + println!( + "{}", + "fg".gradient(&vec![Color::Green, Color::Violet, Color::Blue]) + ); +} diff --git a/src/color.rs b/src/color.rs index 6e544e8..84e8201 100644 --- a/src/color.rs +++ b/src/color.rs @@ -12,6 +12,9 @@ pub enum Color { Magenta, Cyan, White, + Orange, + Indigo, + Violet, BrightBlack, BrightRed, BrightGreen, @@ -35,6 +38,9 @@ impl Color { Color::Magenta => "35".into(), Color::Cyan => "36".into(), Color::White => "37".into(), + Color::Orange => format!("38;2;{};{};{}", 255, 165, 0).into(), + Color::Indigo => format!("38;2;{};{};{}", 75, 0, 130).into(), + Color::Violet => format!("38;2;{};{};{}", 238, 130, 238).into(), Color::BrightBlack => "90".into(), Color::BrightRed => "91".into(), Color::BrightGreen => "92".into(), @@ -57,6 +63,9 @@ impl Color { Color::Magenta => "45".into(), Color::Cyan => "46".into(), Color::White => "47".into(), + Color::Orange => format!("48;2;{};{};{}", 255, 165, 0).into(), + Color::Indigo => format!("48;2;{};{};{}", 75, 0, 130).into(), + Color::Violet => format!("48;2;{};{};{}", 238, 130, 238).into(), Color::BrightBlack => "100".into(), Color::BrightRed => "101".into(), Color::BrightGreen => "102".into(), @@ -68,6 +77,79 @@ impl Color { Color::TrueColor { r, g, b } => format!("48;2;{};{};{}", r, g, b).into(), } } + + pub fn to_true_color(&self) -> Color { + match *self { + Color::Black => Color::TrueColor { r: 0, g: 0, b: 0 }, + Color::Red => Color::TrueColor { r: 255, g: 0, b: 0 }, + Color::Green => Color::TrueColor { r: 0, g: 255, b: 0 }, + Color::Yellow => Color::TrueColor { + r: 255, + g: 255, + b: 0, + }, + Color::Blue => Color::TrueColor { r: 0, g: 0, b: 255 }, + Color::Magenta => Color::TrueColor { + r: 255, + g: 0, + b: 255, + }, + Color::Cyan => Color::TrueColor { + r: 0, + g: 255, + b: 255, + }, + Color::White => Color::TrueColor { + r: 255, + g: 255, + b: 255, + }, + Color::Orange => Color::TrueColor { + r: 255, + g: 165, + b: 0, + }, + Color::Indigo => Color::TrueColor { + r: 75, + g: 0, + b: 130, + }, + Color::Violet => Color::TrueColor { + r: 238, + g: 130, + b: 238, + }, + Color::BrightBlack => Color::TrueColor { + r: 128, + g: 128, + b: 128, + }, + Color::BrightRed => Color::TrueColor { r: 255, g: 0, b: 0 }, + Color::BrightGreen => Color::TrueColor { r: 0, g: 255, b: 0 }, + Color::BrightYellow => Color::TrueColor { + r: 255, + g: 255, + b: 0, + }, + Color::BrightBlue => Color::TrueColor { r: 0, g: 0, b: 255 }, + Color::BrightMagenta => Color::TrueColor { + r: 255, + g: 0, + b: 255, + }, + Color::BrightCyan => Color::TrueColor { + r: 0, + g: 255, + b: 255, + }, + Color::BrightWhite => Color::TrueColor { + r: 255, + g: 255, + b: 255, + }, + Color::TrueColor { .. } => *self, // If it's already TrueColor, just return it as is + } + } } impl<'a> From<&'a str> for Color { @@ -98,6 +180,9 @@ impl FromStr for Color { "purple" => Ok(Color::Magenta), "cyan" => Ok(Color::Cyan), "white" => Ok(Color::White), + "orange" => Ok(Color::Orange), + "indigo" => Ok(Color::Indigo), + "violet" => Ok(Color::Violet), "bright black" => Ok(Color::BrightBlack), "bright red" => Ok(Color::BrightRed), "bright green" => Ok(Color::BrightGreen), diff --git a/src/lib.rs b/src/lib.rs index 71e042b..94d68d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,6 +56,12 @@ pub struct ColoredString { style: style::Style, } +/// A list of single character strings where each character may have a color applied to it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ColoredList { + list: Vec, +} + /// The trait that enables something to be given color. /// /// You can use `colored` effectively simply by importing this trait @@ -329,6 +335,18 @@ pub trait Colorize { fn strikethrough(self) -> ColoredString; } +/// The trait that enables color gradients for strings. +/// +/// You can use `colored` effectively simply by importing this trait +/// and then using its methods on `String` and `&str`. +#[allow(missing_docs)] +pub trait ColorizeList { + fn rainbow(self) -> ColoredList; + fn on_rainbow(self) -> ColoredList; + fn gradient(self, color: &[Color]) -> ColoredList; + fn on_gradient(self, color: &[Color]) -> ColoredList; +} + impl ColoredString { /// Get the current background color applied. /// @@ -600,6 +618,121 @@ impl<'a> Colorize for &'a str { } } +impl ColoredList { + fn calc_gradient(input_str: &str, colors: &[Color], on_bg: bool) -> ColoredList { + if colors.len() < 2 || input_str.len() < 2 { + //default entire string to one color + return ColoredList { + list: vec![ + (ColoredString { + fgcolor: Some(*colors.get(0).unwrap_or(&Color::White)), + input: String::from(input_str), + ..ColoredString::default() + }), + ], + }; + } + + //Convert all gradient colors to true colors + let true_colors: Vec = colors.iter().map(|color| color.to_true_color()).collect(); + + let chars_percent: Vec = (0..input_str.len()) + .map(|i| i as f32 / (input_str.len() - 1) as f32) + .collect(); + let colors_percent: Vec = (0..colors.len()) + .map(|i| i as f32 / (colors.len() - 1) as f32) + .collect(); + let mut left_index = 0; + + ColoredList { + list: input_str + .chars() + .enumerate() + .map(|(i, c)| { + if i > 0 && chars_percent[i] >= colors_percent[left_index + 1] { + left_index += 1; + } + + let right_index = + left_index + (chars_percent[i] > colors_percent[left_index]) as usize; + let divisor = colors_percent[right_index] + + (colors_percent[right_index] == 0.0) as u8 as f32; //default to 1.0 + let char_percent = chars_percent[i] / divisor; + let left_color = true_colors[left_index]; + let right_color = true_colors[right_index]; + + let (r1, g1, b1) = match left_color { + Color::TrueColor { r, g, b } => (r as f32, g as f32, b as f32), + _ => (255.0, 255.0, 255.0), + }; + let (r2, g2, b2) = match right_color { + Color::TrueColor { r, g, b } => (r as f32, g as f32, b as f32), + _ => (255.0, 255.0, 255.0), + }; + + let true_color = Color::TrueColor { + r: (r1 + char_percent * (r2 - r1)) as u8, + g: (g1 + char_percent * (g2 - g1)) as u8, + b: (b1 + char_percent * (b2 - b1)) as u8, + }; + + if on_bg { + ColoredString { + bgcolor: Some(true_color), + input: String::from(c), + ..ColoredString::default() + } + } else { + ColoredString { + fgcolor: Some(true_color), + input: String::from(c), + ..ColoredString::default() + } + } + }) + .collect(), + } + } +} + +impl<'a> ColorizeList for &'a str { + fn rainbow(self) -> ColoredList { + let colors = vec![ + Color::Red, + Color::Orange, + Color::Yellow, + Color::Green, + Color::Blue, + Color::Indigo, + Color::Violet, + ]; + self.gradient(&colors) + } + + fn on_rainbow(self) -> ColoredList { + let colors = vec![ + Color::Red, + Color::Orange, + Color::Yellow, + Color::Green, + Color::Blue, + Color::Indigo, + Color::Violet, + ]; + self.on_gradient(&colors) + } + + fn gradient(self, colors: &[Color]) -> ColoredList { + let on_bg = false; + ColoredList::calc_gradient(self, colors, on_bg) + } + + fn on_gradient(self, colors: &[Color]) -> ColoredList { + let on_bg = true; + ColoredList::calc_gradient(self, colors, on_bg) + } +} + impl fmt::Display for ColoredString { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if !self.has_colors() || self.is_plain() { @@ -616,6 +749,14 @@ impl fmt::Display for ColoredString { } } +impl fmt::Display for ColoredList { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.list + .iter() + .try_for_each(|color_string| write!(f, "{}", color_string)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -669,6 +810,7 @@ mod tests { println!("{}", toto.truecolor(255, 0, 0)); println!("{}", toto.truecolor(255, 255, 0)); println!("{}", toto.on_truecolor(0, 80, 80)); + println!("{}", toto.rainbow()); // uncomment to see term output // assert!(false) } @@ -838,12 +980,38 @@ mod tests { assert_eq!("blue".on_bright_blue(), "blue".on_color("bright blue")) } + #[test] + fn rainbow_fn() { + let clist = "abcdefg".rainbow(); + let colors: Vec = clist + .list + .iter() + .map(|cstring| cstring.fgcolor().unwrap()) + .collect(); + assert_eq!( + colors, + vec![ + Color::Red.to_true_color(), + Color::Orange.to_true_color(), + Color::Yellow.to_true_color(), + Color::Green.to_true_color(), + Color::Blue.to_true_color(), + Color::Indigo.to_true_color(), + Color::Violet.to_true_color() + ] + ); + } + #[test] fn exposing_tests() { let cstring = "".red(); assert_eq!(cstring.fgcolor(), Some(Color::Red)); assert_eq!(cstring.bgcolor(), None); + let clist = "".gradient(&vec![Color::Red]); + assert_eq!(clist.list[0].fgcolor(), Some(Color::Red)); + assert_eq!(clist.list[0].bgcolor(), None); + let cstring = cstring.clear(); assert_eq!(cstring.fgcolor(), None); assert_eq!(cstring.bgcolor(), None);