antifacet 0.1.0

crate for general utilities
Documentation
//! Color generation utilities for pretty-printing

use {
	core::{
		fmt,
		hash::{
			Hash,
			Hasher,
		},
	},
	siphasher::sip::SipHasher,
};

/// RGB color representation
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RGB {
	/// Red component (0-255)
	pub r: u8,
	/// Green component (0-255)
	pub g: u8,
	/// Blue component (0-255)
	pub b: u8,
}

impl RGB {
	pub const NONE: Self = Self { r: 0, g: 0, b: 0 };

	/// Create a new RGB color
	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,
		}
	}

	/// Write the RGB color as ANSI foreground color code to the formatter
	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 }
	}

	/// Write the RGB color as ANSI background color code to the formatter
	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(())
	}
}

/// A color generator that produces unique colors based on a hash value
#[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,
	};

	/// Create a new color generator with default settings
	pub fn new() -> Self {
		Self::default()
	}

	/// Set the base hue (0-360)
	pub fn with_base_hue(
		mut self,
		hue: f32,
	) -> Self {
		self.base_hue = hue;
		self
	}

	/// Set the saturation (0.0-1.0)
	pub fn with_saturation(
		mut self,
		saturation: f32,
	) -> Self {
		self.saturation = saturation.clamp(0.0, 1.0);
		self
	}

	/// Set the lightness (0.0-1.0)
	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 }
	}

	/// Generate an RGB color based on a hash value
	pub fn generate_color(
		&self,
		hash: u64,
	) -> RGB {
		if self.is_none() {
			RGB::NONE
		} else {
			// Use the hash to generate a hue offset
			let hue_offset = (hash % 360) as f32;
			let hue = (self.base_hue + hue_offset) % 360.0;

			// Convert HSL to RGB
			Self::hsl_to_rgb(hue, self.saturation, self.lightness)
		}
	}

	/// Generate an RGB color based on a hashable value
	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)
	}

	/// Convert HSL color values to RGB
	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();

		// Same hash should produce same color
		let color1 = generator.generate_color(42);
		let color2 = generator.generate_color(42);
		assert_eq!(color1, color2);

		// Different hashes should produce different colors
		let color3 = generator.generate_color(100);
		assert_ne!(color1, color3);
	}

	#[test]
	fn test_generate_color_for() {
		let generator = ColorGenerator::default();

		// Same value should produce same color
		let color1 = generator.generate_color_for(&"test");
		let color2 = generator.generate_color_for(&"test");
		assert_eq!(color1, color2);

		// Different values should produce different colors
		let color3 = generator.generate_color_for(&"other");
		assert_ne!(color1, color3);
	}
}