Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gradients and Rainbows #151

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions examples/colored_lists.rs
Original file line number Diff line number Diff line change
@@ -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])
);
}
85 changes: 85 additions & 0 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ pub enum Color {
Magenta,
Cyan,
White,
Orange,
Indigo,
Violet,
BrightBlack,
BrightRed,
BrightGreen,
Expand All @@ -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(),
Expand All @@ -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(),
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down
168 changes: 168 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ColoredString>,
}

/// The trait that enables something to be given color.
///
/// You can use `colored` effectively simply by importing this trait
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<Color> = colors.iter().map(|color| color.to_true_color()).collect();

let chars_percent: Vec<f32> = (0..input_str.len())
.map(|i| i as f32 / (input_str.len() - 1) as f32)
.collect();
let colors_percent: Vec<f32> = (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() {
Expand All @@ -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::*;
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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<Color> = 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);
Expand Down