use {
core::{
fmt,
hash::{
Hash,
Hasher,
},
},
siphasher::sip::SipHasher,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RGB {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl RGB {
pub const NONE: Self = Self { r: 0, g: 0, b: 0 };
pub fn new(
r: u8,
g: u8,
b: u8,
) -> Self {
Self { r, g, b }
}
pub fn hex(rgb: u32) -> Self {
Self {
r: (rgb >> 16) as u8 & 0xFF,
g: (rgb >> 8) as u8 & 0xFF,
b: (rgb >> 0) as u8 & 0xFF,
}
}
pub fn write_fg(
self,
f: &mut dyn fmt::Write,
) -> fmt::Result {
if self != Self::NONE {
write!(f, "\x1b[38;2;{};{};{}m", self.r, self.g, self.b)?;
}
Ok(())
}
pub fn write_bold(
self,
f: &mut dyn fmt::Write,
) -> fmt::Result {
if self != Self::NONE {
write!(f, "\x1b[1m")?;
}
Ok(())
}
pub fn reset(
self,
f: &mut dyn fmt::Write,
) -> fmt::Result {
if self != Self::NONE {
write!(f, "\x1b[0m")?;
}
Ok(())
}
pub fn bg(
self,
value: &(impl ?Sized + fmt::Display),
) -> impl Copy + fmt::Display {
struct Bg<'a, T: ?Sized> {
value: &'a T,
rgb: RGB,
}
impl<T: ?Sized> Copy for Bg<'_, T> {}
impl<T: ?Sized> Clone for Bg<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized + fmt::Display> fmt::Display for Bg<'_, T> {
fn fmt(
&self,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
self.rgb.write_bg(f)?;
self.value.fmt(f)?;
self.rgb.reset(f)
}
}
Bg { value, rgb: self }
}
pub fn fg(
self,
value: &(impl ?Sized + fmt::Display),
) -> impl '_ + Copy + fmt::Display {
struct Fg<'a, T: ?Sized> {
value: &'a T,
rgb: RGB,
}
impl<T: ?Sized> Copy for Fg<'_, T> {}
impl<T: ?Sized> Clone for Fg<'_, T> {
fn clone(&self) -> Self {
*self
}
}
impl<T: ?Sized + fmt::Display> fmt::Display for Fg<'_, T> {
fn fmt(
&self,
f: &mut fmt::Formatter<'_>,
) -> fmt::Result {
self.rgb.write_fg(f)?;
self.value.fmt(f)?;
self.rgb.reset(f)
}
}
Fg { value, rgb: self }
}
pub fn write_bg(
self,
f: &mut dyn fmt::Write,
) -> fmt::Result {
if self != Self::NONE {
write!(f, "\x1b[48;2;{};{};{}m", self.r, self.g, self.b)?;
}
Ok(())
}
}
#[derive(Copy, Clone, Debug)]
pub struct ColorGenerator {
base_hue: f32,
saturation: f32,
lightness: f32,
}
impl Default for ColorGenerator {
fn default() -> Self {
#[cfg(feature = "std")]
{
if std::env::var_os("NO_COLOR").is_some() {
Self::NONE
} else {
Self::DEFAULT
}
}
#[cfg(not(feature = "std"))]
{
Self::NONE
}
}
}
impl ColorGenerator {
const DEFAULT: Self = Self {
base_hue: 210.0,
saturation: 0.7,
lightness: 0.4,
};
pub const NONE: Self = Self {
base_hue: f32::NAN,
saturation: f32::NAN,
lightness: f32::NAN,
};
pub fn new() -> Self {
Self::default()
}
pub fn with_base_hue(
mut self,
hue: f32,
) -> Self {
self.base_hue = hue;
self
}
pub fn with_saturation(
mut self,
saturation: f32,
) -> Self {
self.saturation = saturation.clamp(0.0, 1.0);
self
}
pub fn with_lightness(
mut self,
lightness: f32,
) -> Self {
self.lightness = lightness.clamp(0.0, 1.0);
self
}
pub fn is_none(&self) -> bool {
self.base_hue.is_nan() || self.saturation.is_nan() || self.lightness.is_nan()
}
pub fn map_color(
&self,
color: RGB,
) -> RGB {
if self.is_none() { RGB::NONE } else { color }
}
pub fn generate_color(
&self,
hash: u64,
) -> RGB {
if self.is_none() {
RGB::NONE
} else {
let hue_offset = (hash % 360) as f32;
let hue = (self.base_hue + hue_offset) % 360.0;
Self::hsl_to_rgb(hue, self.saturation, self.lightness)
}
}
pub fn generate_color_for<T: Hash>(
&self,
value: &T,
) -> RGB {
let mut hasher = SipHasher::new();
value.hash(&mut hasher);
let hash = hasher.finish();
self.generate_color(hash)
}
fn hsl_to_rgb(
h: f32,
s: f32,
l: f32,
) -> RGB {
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let x = c * (1.0 - ((h / 60.0) % 2.0 - 1.0).abs());
let m = l - c / 2.0;
let (r, g, b) = match h as u32 {
0..=59 => (c, x, 0.0),
60..=119 => (x, c, 0.0),
120..=179 => (0.0, c, x),
180..=239 => (0.0, x, c),
240..=299 => (x, 0.0, c),
_ => (c, 0.0, x),
};
RGB::new(
((r + m) * 255.0) as u8,
((g + m) * 255.0) as u8,
((b + m) * 255.0) as u8,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_generator_default() {
let generator = ColorGenerator::default();
assert_eq!(generator.base_hue, 210.0);
assert_eq!(generator.saturation, 0.7);
assert_eq!(generator.lightness, 0.6);
}
#[test]
fn test_color_generator_with_methods() {
let generator = ColorGenerator::new()
.with_base_hue(180.0)
.with_saturation(0.5)
.with_lightness(0.7);
assert_eq!(generator.base_hue, 180.0);
assert_eq!(generator.saturation, 0.5);
assert_eq!(generator.lightness, 0.7);
}
#[test]
fn test_saturation_clamping() {
let generator = ColorGenerator::new().with_saturation(1.5);
assert_eq!(generator.saturation, 1.0);
let generator = ColorGenerator::new().with_saturation(-0.5);
assert_eq!(generator.saturation, 0.0);
}
#[test]
fn test_lightness_clamping() {
let generator = ColorGenerator::new().with_lightness(1.5);
assert_eq!(generator.lightness, 1.0);
let generator = ColorGenerator::new().with_lightness(-0.5);
assert_eq!(generator.lightness, 0.0);
}
#[test]
fn test_generate_color() {
let generator = ColorGenerator::default();
let color1 = generator.generate_color(42);
let color2 = generator.generate_color(42);
assert_eq!(color1, color2);
let color3 = generator.generate_color(100);
assert_ne!(color1, color3);
}
#[test]
fn test_generate_color_for() {
let generator = ColorGenerator::default();
let color1 = generator.generate_color_for(&"test");
let color2 = generator.generate_color_for(&"test");
assert_eq!(color1, color2);
let color3 = generator.generate_color_for(&"other");
assert_ne!(color1, color3);
}
}