sara-tasks 2.7.5

Sara — folder-aware task manager
use ratatui::style::Color;

use super::theme::{Ink, Palette};

mod catppuccin;
mod classic;
mod dracula;
mod gruvbox;
mod high_contrast;
mod nord;
mod one_dark;
mod retro;
mod rose_pine;
mod solarized_dark;
mod solarized_light;
mod tokyo_night;

pub(super) struct Scheme {
    text: u32,
    soft: u32,
    muted: u32,
    accent: u32,
    ok: u32,
    warn: u32,
    err: u32,
    info: u32,
    special: u32,
    glow: u32,
    base: u32,
    select: u32,
    header: u32,
    void: u32,
    heat: [u32; 5],
}

impl Scheme {
    fn ink(&self, i: Ink, truecolor: bool) -> Color {
        let hex = match i {
            Ink::Text => self.text,
            Ink::Soft => self.soft,
            Ink::Muted => self.muted,
            Ink::Accent => self.accent,
            Ink::Ok => self.ok,
            Ink::Warn => self.warn,
            Ink::Err => self.err,
            Ink::Info => self.info,
            Ink::Special => self.special,
            Ink::Glow => self.glow,
            Ink::Base => self.base,
            Ink::Select => self.select,
            Ink::Header => self.header,
            Ink::Void => self.void,
            Ink::Plain => return Color::Reset,
        };
        shade(hex, truecolor)
    }

    fn heat(&self, level: usize, truecolor: bool) -> Color {
        shade(self.heat[level], truecolor)
    }
}

pub(super) fn ink(p: Palette, i: Ink, truecolor: bool) -> Color {
    match scheme(p) {
        Some(s) => s.ink(i, truecolor),
        None if p == Palette::Retro => retro::ink(i, truecolor),
        None => classic::ink(i),
    }
}

pub(super) fn heat(p: Palette, level: usize, truecolor: bool) -> Color {
    match scheme(p) {
        Some(s) => s.heat(level, truecolor),
        None if p == Palette::Retro => retro::heat(level, truecolor),
        None => classic::HEAT[level],
    }
}

fn scheme(p: Palette) -> Option<&'static Scheme> {
    Some(match p {
        Palette::Classic | Palette::Retro => return None,
        Palette::Dracula => &dracula::SCHEME,
        Palette::Nord => &nord::SCHEME,
        Palette::Gruvbox => &gruvbox::SCHEME,
        Palette::Catppuccin => &catppuccin::SCHEME,
        Palette::TokyoNight => &tokyo_night::SCHEME,
        Palette::SolarizedDark => &solarized_dark::SCHEME,
        Palette::SolarizedLight => &solarized_light::SCHEME,
        Palette::OneDark => &one_dark::SCHEME,
        Palette::RosePine => &rose_pine::SCHEME,
        Palette::HighContrast => &high_contrast::SCHEME,
    })
}

fn rgb(hex: u32) -> Color {
    Color::Rgb((hex >> 16) as u8, (hex >> 8) as u8, hex as u8)
}

fn shade(hex: u32, truecolor: bool) -> Color {
    if truecolor {
        rgb(hex)
    } else {
        Color::Indexed(xterm256(hex))
    }
}

pub(super) fn xterm256(hex: u32) -> u8 {
    const LEVELS: [i32; 6] = [0, 95, 135, 175, 215, 255];
    let (r, g, b) = (
        (hex >> 16 & 0xff) as i32,
        (hex >> 8 & 0xff) as i32,
        (hex & 0xff) as i32,
    );
    let step = |v: i32| match v {
        v if v < 48 => 0,
        v if v < 115 => 1,
        v => ((v - 35) / 40) as usize,
    };
    let (qr, qg, qb) = (step(r), step(g), step(b));
    let dist = |cr: i32, cg: i32, cb: i32| (r - cr).pow(2) + (g - cg).pow(2) + (b - cb).pow(2);
    let cube = dist(LEVELS[qr], LEVELS[qg], LEVELS[qb]);
    let grey = ((r + g + b) / 3 - 3).clamp(0, 230) / 10;
    let gv = 8 + 10 * grey;
    if dist(gv, gv, gv) < cube {
        232 + grey as u8
    } else {
        16 + (36 * qr + 6 * qg + qb) as u8
    }
}

#[cfg(test)]
#[path = "../../../../tests/unit/infrastructure/tui/themes.rs"]
mod tests;