use core::hash::{Hash, Hasher};
use owo_colors::Rgb;
use std::hash::DefaultHasher;
use std::sync::LazyLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Palette {
pub foreground: Rgb,
pub type_name: Rgb,
pub field_name: Rgb,
pub string: Rgb,
pub number: Rgb,
pub keyword: Rgb,
pub comment: Rgb,
pub punctuation: Rgb,
pub error: Rgb,
pub deletion: Rgb,
pub insertion: Rgb,
pub accents: [Rgb; 6],
}
impl Palette {
pub const MELANGE_DARK: Self = Self {
foreground: Rgb(236, 225, 215), type_name: Rgb(123, 150, 149), field_name: Rgb(236, 225, 215), string: Rgb(163, 169, 206), number: Rgb(207, 155, 194), keyword: Rgb(207, 155, 194), comment: Rgb(193, 167, 142), punctuation: Rgb(134, 116, 98), error: Rgb(212, 119, 102), deletion: Rgb(189, 129, 131), insertion: Rgb(133, 182, 149), accents: [
Rgb(212, 119, 102), Rgb(235, 192, 109), Rgb(133, 182, 149), Rgb(137, 179, 182), Rgb(163, 169, 206), Rgb(207, 155, 194), ],
};
pub const MELANGE_LIGHT: Self = Self {
foreground: Rgb(84, 67, 58), type_name: Rgb(115, 151, 151), field_name: Rgb(84, 67, 58), string: Rgb(70, 90, 164), number: Rgb(144, 65, 128), keyword: Rgb(144, 65, 128), comment: Rgb(125, 102, 88), punctuation: Rgb(169, 138, 120), error: Rgb(191, 0, 33), deletion: Rgb(199, 123, 139), insertion: Rgb(58, 104, 74), accents: [
Rgb(191, 0, 33), Rgb(160, 109, 0), Rgb(58, 104, 74), Rgb(61, 101, 104), Rgb(70, 90, 164), Rgb(144, 65, 128), ],
};
pub fn accent(&self, hash: u64) -> Rgb {
self.accents[(hash % self.accents.len() as u64) as usize]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Theme {
#[default]
Auto,
Dark,
Light,
Custom(Palette),
}
impl Theme {
pub fn palette(&self) -> Palette {
match self {
Theme::Auto => detected_palette(),
Theme::Dark => Palette::MELANGE_DARK,
Theme::Light => Palette::MELANGE_LIGHT,
Theme::Custom(palette) => *palette,
}
}
}
pub fn detected_palette() -> Palette {
static DETECTED: LazyLock<Palette> = LazyLock::new(detect);
*DETECTED
}
fn detect() -> Palette {
match std::env::var("FACET_PRETTY_THEME") {
Ok(v) if v.eq_ignore_ascii_case("light") => return Palette::MELANGE_LIGHT,
Ok(v) if v.eq_ignore_ascii_case("dark") => return Palette::MELANGE_DARK,
_ => {}
}
if terminal_is_light() {
Palette::MELANGE_LIGHT
} else {
Palette::MELANGE_DARK
}
}
#[cfg(all(feature = "detect-terminal-theme", not(target_arch = "wasm32")))]
fn terminal_is_light() -> bool {
use std::io::IsTerminal;
if !std::io::stdout().is_terminal() {
return false;
}
terminal_light::luma()
.map(|luma| luma > 0.6)
.unwrap_or(false)
}
#[cfg(not(all(feature = "detect-terminal-theme", not(target_arch = "wasm32"))))]
fn terminal_is_light() -> bool {
false
}
#[deprecated(
since = "0.46.3",
note = "use `Palette` / `owo_colors::Rgb`; colours now come from `Theme`/`Palette`"
)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RGB {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[allow(deprecated)]
impl RGB {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
pub fn write_fg<W: core::fmt::Write>(&self, f: &mut W) -> core::fmt::Result {
write!(f, "\x1b[38;2;{};{};{}m", self.r, self.g, self.b)
}
pub fn write_bg<W: core::fmt::Write>(&self, f: &mut W) -> core::fmt::Result {
write!(f, "\x1b[48;2;{};{};{}m", self.r, self.g, self.b)
}
}
#[deprecated(
since = "0.46.3",
note = "colours now come from `Theme`/`Palette`; use `Palette::accent` for per-value hues"
)]
#[derive(Clone, PartialEq)]
pub struct ColorGenerator {
base_hue: f32,
saturation: f32,
lightness: f32,
}
#[allow(deprecated)]
impl Default for ColorGenerator {
fn default() -> Self {
Self::new()
}
}
#[allow(deprecated)]
impl ColorGenerator {
pub const fn new() -> Self {
Self {
base_hue: 210.0,
saturation: 0.7,
lightness: 0.6,
}
}
pub const fn with_base_hue(mut self, hue: f32) -> Self {
self.base_hue = hue;
self
}
pub const fn with_saturation(mut self, saturation: f32) -> Self {
self.saturation = saturation.clamp(0.0, 1.0);
self
}
pub const fn with_lightness(mut self, lightness: f32) -> Self {
self.lightness = lightness.clamp(0.0, 1.0);
self
}
pub const fn generate_color(&self, hash: u64) -> RGB {
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 = DefaultHasher::new();
value.hash(&mut hasher);
let hash = hasher.finish();
self.generate_color(hash)
}
const fn hsl_to_rgb(&self, 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 accent_is_stable_and_in_palette() {
let p = Palette::MELANGE_DARK;
assert_eq!(p.accent(42), p.accent(42));
assert!(p.accents.contains(&p.accent(123_456_789)));
}
#[test]
fn theme_resolves_to_expected_palette() {
assert_eq!(Theme::Dark.palette(), Palette::MELANGE_DARK);
assert_eq!(Theme::Light.palette(), Palette::MELANGE_LIGHT);
let custom = Palette {
foreground: Rgb(1, 2, 3),
..Palette::MELANGE_DARK
};
assert_eq!(Theme::Custom(custom).palette(), custom);
}
}