use chrono::{Datelike, NaiveDate};
use ratatui::style::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb {
pub const fn hex(value: u32) -> Self {
Self((value >> 16) as u8, (value >> 8) as u8, value as u8)
}
pub fn over(self, under: Rgb, alpha: f32) -> Rgb {
let mix = |a: u8, b: u8| {
(f32::from(a) * alpha + f32::from(b) * (1.0 - alpha))
.round()
.clamp(0.0, 255.0) as u8
};
Rgb(
mix(self.0, under.0),
mix(self.1, under.1),
mix(self.2, under.2),
)
}
pub fn brightness(self) -> f32 {
let c = |v: u8| f32::from(v) / 255.0;
0.2126 * c(self.0) + 0.7152 * c(self.1) + 0.0722 * c(self.2)
}
pub fn lab(self) -> (f32, f32, f32) {
let linear = |channel: u8| {
let c = f32::from(channel) / 255.0;
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
};
let (r, g, b) = (linear(self.0), linear(self.1), linear(self.2));
let x = (0.412_456_4 * r + 0.357_576_1 * g + 0.180_437_5 * b) / 0.950_47;
let y = 0.212_672_9 * r + 0.715_152_2 * g + 0.072_175_0 * b;
let z = (0.019_333_9 * r + 0.119_192 * g + 0.950_304_1 * b) / 1.088_83;
let f = |t: f32| {
if t > 216.0 / 24389.0 {
t.cbrt()
} else {
(841.0 / 108.0) * t + 4.0 / 29.0
}
};
let (fx, fy, fz) = (f(x), f(y), f(z));
(116.0 * fy - 16.0, 500.0 * (fx - fy), 200.0 * (fy - fz))
}
pub fn separation(self, other: Rgb) -> f32 {
let (l1, a1, b1) = self.lab();
let (l2, a2, b2) = other.lab();
((l1 - l2).powi(2) + (a1 - a2).powi(2) + (b1 - b2).powi(2)).sqrt()
}
pub fn ansi(self, truecolor: bool) -> Color {
if truecolor {
Color::Rgb(self.0, self.1, self.2)
} else {
Color::Indexed(self.xterm256())
}
}
fn xterm256(self) -> u8 {
const STEPS: [u8; 6] = [0, 95, 135, 175, 215, 255];
let nearest = |v: u8| {
STEPS
.iter()
.enumerate()
.min_by_key(|(_, step)| i32::from(**step).abs_diff(i32::from(v)))
.map_or(0, |(index, _)| index)
};
let (r, g, b) = (nearest(self.0), nearest(self.1), nearest(self.2));
let cube = Rgb(STEPS[r], STEPS[g], STEPS[b]);
let average = (u32::from(self.0) + u32::from(self.1) + u32::from(self.2)) as f32 / 3.0;
let step = (((average - 8.0) / 10.0).round()).clamp(0.0, 23.0) as u8;
let level = 8 + 10 * step;
let grey = Rgb(level, level, level);
if distance(self, grey) < distance(self, cube) {
232 + step
} else {
(16 + 36 * r + 6 * g + b) as u8
}
}
}
fn distance(a: Rgb, b: Rgb) -> u32 {
let d = |x: u8, y: u8| {
let delta = i32::from(x) - i32::from(y);
(delta * delta) as u32
};
d(a.0, b.0) + d(a.1, b.1) + d(a.2, b.2)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Appearance {
Light,
Dark,
Dimmed,
}
impl Appearance {
pub fn from_background(background: Rgb) -> Self {
if background.brightness() > 0.5 {
Self::Light
} else {
Self::Dark
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Season {
Default,
Winter,
Halloween,
}
impl Season {
pub fn on(date: NaiveDate) -> Self {
match (date.month(), date.day()) {
(10, 25..=31) => Self::Halloween,
(12, 19..=25) => Self::Winter,
_ => Self::Default,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Palette {
pub appearance: Appearance,
pub season: Season,
pub truecolor: bool,
pub levels: [Rgb; 5],
pub edge: Rgb,
pub canvas: Rgb,
pub fg: Rgb,
pub muted: Rgb,
pub rule: Rgb,
pub tooltip_bg: Rgb,
pub tooltip_fg: Rgb,
pub accent: Rgb,
pub danger: Rgb,
}
pub const EDGE_ALPHA: f32 = 0.05;
impl Palette {
pub fn new(appearance: Appearance, season: Season, truecolor: bool) -> Self {
let mut palette = match appearance {
Appearance::Light => Self {
appearance,
season,
truecolor,
levels: [
Rgb::hex(0xeff2f5),
Rgb::hex(0xaceebb),
Rgb::hex(0x4ac26b),
Rgb::hex(0x2da44e),
Rgb::hex(0x116329),
],
edge: Rgb::hex(0x1f2328),
canvas: Rgb::hex(0xffffff),
fg: Rgb::hex(0x1f2328),
muted: Rgb::hex(0x59636e),
rule: Rgb::hex(0xd1d9e0),
tooltip_bg: Rgb::hex(0x25292e),
tooltip_fg: Rgb::hex(0xffffff),
accent: Rgb::hex(0x0969da),
danger: Rgb::hex(0xd1242f),
},
Appearance::Dark => Self {
appearance,
season,
truecolor,
levels: [
Rgb::hex(0x151b23),
Rgb::hex(0x033a16),
Rgb::hex(0x196c2e),
Rgb::hex(0x2ea043),
Rgb::hex(0x56d364),
],
edge: Rgb::hex(0x010409),
canvas: Rgb::hex(0x0d1117),
fg: Rgb::hex(0xf0f6fc),
muted: Rgb::hex(0x9198a1),
rule: Rgb::hex(0x3d444d),
tooltip_bg: Rgb::hex(0x3d444d),
tooltip_fg: Rgb::hex(0xffffff),
accent: Rgb::hex(0x4493f8),
danger: Rgb::hex(0xf85149),
},
Appearance::Dimmed => Self {
appearance,
season,
truecolor,
levels: [
Rgb::hex(0x2a313c),
Rgb::hex(0x1b4721),
Rgb::hex(0x2b6a30),
Rgb::hex(0x46954a),
Rgb::hex(0x6bc46d),
],
edge: Rgb::hex(0x010409),
canvas: Rgb::hex(0x212830),
fg: Rgb::hex(0xd1d7e0),
muted: Rgb::hex(0x9198a1),
rule: Rgb::hex(0x3d444d),
tooltip_bg: Rgb::hex(0x3d444d),
tooltip_fg: Rgb::hex(0xf0f6fc),
accent: Rgb::hex(0x478be6),
danger: Rgb::hex(0xe5534b),
},
};
let holiday = match (season, appearance) {
(Season::Default, _) => None,
(Season::Winter, Appearance::Light) => Some([0xb6e3ff, 0x54aeff, 0x0969da, 0x0a3069]),
(Season::Winter, Appearance::Dark) => Some([0x0c2d6b, 0x1158c7, 0x58a6ff, 0xcae8ff]),
(Season::Winter, Appearance::Dimmed) => Some([0x143d79, 0x255ab2, 0x539bf5, 0xc6e6ff]),
(Season::Halloween, Appearance::Light) => {
Some([0xf0db3d, 0xffd642, 0xf68c41, 0x1f2328])
}
(Season::Halloween, _) => Some([0xfac68f, 0xc46212, 0x984b10, 0xe3d04f]),
};
if let Some(colors) = holiday {
for (level, hex) in palette.levels[1..].iter_mut().zip(colors) {
*level = Rgb::hex(hex);
}
}
palette
}
pub fn ansi(&self, color: Rgb) -> Color {
color.ansi(self.truecolor)
}
pub fn level(&self, level: u8) -> Color {
let level = usize::from(level).min(4);
if self.truecolor {
return self.ansi(self.levels[level]);
}
let light = self.appearance == Appearance::Light;
let ramp: [u8; 5] = match (self.season, light) {
(Season::Default, false) => [236, 22, 28, 34, 40],
(Season::Default, true) => [254, 157, 78, 35, 22],
(Season::Winter, false) => [236, 17, 26, 75, 195],
(Season::Winter, true) => [254, 153, 75, 26, 17],
(Season::Halloween, false) => [236, 223, 166, 130, 185],
(Season::Halloween, true) => [254, 227, 220, 208, 235],
};
Color::Indexed(ramp[level])
}
pub fn cell(&self, level: u8) -> Rgb {
self.levels[usize::from(level).min(4)]
}
pub fn separation(&self, a: u8, b: u8) -> f32 {
self.cell(a).separation(self.cell(b))
}
pub fn truecolor_env() -> bool {
std::env::var("COLORTERM").is_ok_and(|v| v.contains("truecolor") || v.contains("24bit"))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Legibility {
Faint,
Readable,
Clear,
}
impl Legibility {
pub fn of(separation: f32) -> Self {
if separation >= 35.0 {
Self::Clear
} else if separation >= 20.0 {
Self::Readable
} else {
Self::Faint
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Faint => "faint",
Self::Readable => "readable",
Self::Clear => "clear",
}
}
}
impl std::fmt::Display for Legibility {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}