use std::fmt;
use std::str::FromStr;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Color {
pub const BLACK: Self = Self::rgb(0, 0, 0);
pub const WHITE: Self = Self::rgb(255, 255, 255);
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
pub fn from_hex(input: &str) -> Result<Self, ParseColorError> {
let hex = input.trim().trim_start_matches('#');
if hex.len() != 6 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(ParseColorError(input.to_string()));
}
let r =
u8::from_str_radix(&hex[0..2], 16).map_err(|_| ParseColorError(input.to_string()))?;
let g =
u8::from_str_radix(&hex[2..4], 16).map_err(|_| ParseColorError(input.to_string()))?;
let b =
u8::from_str_radix(&hex[4..6], 16).map_err(|_| ParseColorError(input.to_string()))?;
Ok(Self::rgb(r, g, b))
}
pub fn from_xterm(code: u8) -> Self {
const SYSTEM: [Color; 16] = [
Color::rgb(0, 0, 0),
Color::rgb(128, 0, 0),
Color::rgb(0, 128, 0),
Color::rgb(128, 128, 0),
Color::rgb(0, 0, 128),
Color::rgb(128, 0, 128),
Color::rgb(0, 128, 128),
Color::rgb(192, 192, 192),
Color::rgb(128, 128, 128),
Color::rgb(255, 0, 0),
Color::rgb(0, 255, 0),
Color::rgb(255, 255, 0),
Color::rgb(0, 0, 255),
Color::rgb(255, 0, 255),
Color::rgb(0, 255, 255),
Color::rgb(255, 255, 255),
];
if code < 16 {
return SYSTEM[code as usize];
}
if code <= 231 {
let n = code - 16;
let scale = [0, 95, 135, 175, 215, 255];
let r = scale[(n / 36) as usize];
let g = scale[((n % 36) / 6) as usize];
let b = scale[(n % 6) as usize];
return Self::rgb(r, g, b);
}
let gray = 8 + (code - 232) * 10;
Self::rgb(gray, gray, gray)
}
pub fn to_hex(self) -> String {
format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
}
pub fn blend(self, other: Color, amount: f32) -> Self {
let t = amount.clamp(0.0, 1.0);
let lerp = |a: u8, b: u8| a as f32 + (b as f32 - a as f32) * t;
Self::rgb(
lerp(self.r, other.r).round() as u8,
lerp(self.g, other.g).round() as u8,
lerp(self.b, other.b).round() as u8,
)
}
pub fn dim(self, amount: f32) -> Self {
let t = amount.clamp(0.0, 1.0);
Self::rgb(
(self.r as f32 * t).round() as u8,
(self.g as f32 * t).round() as u8,
(self.b as f32 * t).round() as u8,
)
}
pub fn brighten(self, amount: f32) -> Self {
self.blend(Color::WHITE, amount)
}
}
impl FromStr for Color {
type Err = ParseColorError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let trimmed = input.trim();
if let Ok(code) = trimmed.parse::<u8>() {
return Ok(Self::from_xterm(code));
}
Self::from_hex(trimmed)
}
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParseColorError(String);
impl fmt::Display for ParseColorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"invalid color {:?}; expected #rrggbb, rrggbb, or xterm 0-255",
self.0
)
}
}
impl std::error::Error for ParseColorError {}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ColorPair {
pub fg: Option<Color>,
pub bg: Option<Color>,
}
impl ColorPair {
pub const fn new(fg: Option<Color>, bg: Option<Color>) -> Self {
Self { fg, bg }
}
pub const fn fg(fg: Color) -> Self {
Self {
fg: Some(fg),
bg: None,
}
}
pub const fn bg(bg: Color) -> Self {
Self {
fg: None,
bg: Some(bg),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum GradientDirection {
#[default]
Vertical,
Horizontal,
Radial,
Diagonal,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Gradient {
stops: Vec<Color>,
steps: usize,
looped: bool,
spectrum: Vec<Color>,
}
impl Gradient {
pub fn new(stops: impl Into<Vec<Color>>, steps: usize) -> Self {
Self::with_loop(stops, steps, false)
}
pub fn with_loop(stops: impl Into<Vec<Color>>, steps: usize, looped: bool) -> Self {
let mut stops = stops.into();
if stops.is_empty() {
stops.push(Color::WHITE);
}
let steps = steps.max(1);
let mut gradient = Self {
stops,
steps,
looped,
spectrum: Vec::new(),
};
gradient.spectrum = gradient.generate();
gradient
}
pub fn terminal_text_default() -> Self {
Self::new(
vec![
Color::rgb(0x02, 0xb8, 0xbd),
Color::rgb(0xc1, 0xf0, 0xe3),
Color::rgb(0x00, 0xff, 0xa0),
],
24,
)
}
pub fn stops(&self) -> &[Color] {
&self.stops
}
pub fn spectrum(&self) -> &[Color] {
&self.spectrum
}
pub fn color_at(&self, fraction: f32) -> Color {
if self.spectrum.is_empty() {
return Color::WHITE;
}
let index = (fraction.clamp(0.0, 1.0) * (self.spectrum.len().saturating_sub(1)) as f32)
.round() as usize;
self.spectrum[index.min(self.spectrum.len() - 1)]
}
pub fn coordinate_color(
&self,
x: usize,
y: usize,
width: usize,
height: usize,
direction: GradientDirection,
) -> Color {
let max_x = width.saturating_sub(1).max(1) as f32;
let max_y = height.saturating_sub(1).max(1) as f32;
let x = x as f32;
let y = y as f32;
let fraction = match direction {
GradientDirection::Vertical => y / max_y,
GradientDirection::Horizontal => x / max_x,
GradientDirection::Diagonal => (x / max_x + y / max_y) / 2.0,
GradientDirection::Radial => {
let cx = max_x / 2.0;
let cy = max_y / 2.0;
let max_dist = (cx.powi(2) + cy.powi(2)).sqrt().max(1.0);
(((x - cx).powi(2) + (y - cy).powi(2)).sqrt() / max_dist).clamp(0.0, 1.0)
}
};
self.color_at(fraction)
}
fn generate(&self) -> Vec<Color> {
if self.stops.len() == 1 {
return vec![self.stops[0]; self.steps + 1];
}
let mut stops = self.stops.clone();
if self.looped {
stops.push(self.stops[0]);
}
let mut spectrum = Vec::new();
for pair in stops.windows(2) {
for step in 0..self.steps {
let t = step as f32 / self.steps as f32;
spectrum.push(pair[0].blend(pair[1], t));
}
}
spectrum.push(*stops.last().unwrap_or(&Color::WHITE));
spectrum
}
}
impl Default for Gradient {
fn default() -> Self {
Self::terminal_text_default()
}
}