use std::sync::OnceLock;
use ratatui::style::Color;
use super::rows::ChangeType;
#[derive(Debug, Clone)]
pub(crate) struct Theme {
pub(crate) light: bool,
pub(crate) fg_dim: Color,
pub(crate) fg_bright: Color,
pub(crate) blue: Color,
pub(crate) green: Color,
pub(crate) red: Color,
pub(crate) amber: Color,
pub(crate) border: Color,
pub(crate) badge_fg: Color,
pub(crate) scrollbar_thumb: Color,
pub(crate) keycap_fg: Color,
pub(crate) keycap_bg: Color,
pub(crate) hint_fg: Color,
pub(crate) logo: Color,
pub(crate) placeholder_fg: Color,
pub(crate) rpg_frame: Color,
pub(crate) rpg_accent: Color,
pub(crate) rpg_hp: Color,
pub(crate) rpg_mp: Color,
pub(crate) rpg_exp: Color,
pub(crate) rpg_gauge_empty: Color,
pub(crate) rpg_gold: Color,
band_modified: (Color, Color),
band_added: (Color, Color),
band_deleted: (Color, Color),
band_conflict: (Color, Color),
emph_modified: (Color, Color),
emph_added: (Color, Color),
emph_deleted: (Color, Color),
emph_conflict: (Color, Color),
}
impl Theme {
pub(crate) fn select(light: bool) -> Self {
if light {
Self::light()
} else {
Self::tokyo_night()
}
}
pub(crate) fn tokyo_night() -> Self {
Self {
light: false,
fg_dim: term_color(108, 116, 130),
fg_bright: term_color(205, 214, 244),
blue: term_color(122, 162, 247),
green: term_color(158, 206, 106),
red: term_color(224, 108, 117),
amber: term_color(229, 192, 123),
border: term_color(102, 112, 148),
badge_fg: term_color(16, 18, 24),
scrollbar_thumb: term_color(160, 168, 192),
keycap_fg: term_color(205, 214, 244),
keycap_bg: term_color(45, 50, 66),
hint_fg: term_color(170, 178, 196),
logo: term_color(255, 118, 48),
placeholder_fg: term_color(196, 132, 138),
rpg_frame: term_color(234, 225, 198),
rpg_accent: term_color(255, 122, 47),
rpg_hp: term_color(235, 110, 110),
rpg_mp: term_color(108, 178, 255),
rpg_exp: term_color(255, 176, 79),
rpg_gauge_empty: term_color(76, 90, 122),
rpg_gold: term_color(255, 195, 92),
band_modified: (term_color(36, 56, 96), term_color(52, 80, 132)),
band_added: (term_color(26, 42, 31), term_color(40, 66, 48)),
band_deleted: (term_color(44, 47, 56), term_color(60, 64, 76)),
band_conflict: (term_color(58, 30, 34), term_color(94, 45, 53)),
emph_modified: (term_color(72, 102, 158), term_color(90, 126, 186)),
emph_added: (term_color(52, 88, 62), term_color(66, 110, 78)),
emph_deleted: (term_color(76, 81, 94), term_color(94, 100, 116)),
emph_conflict: (term_color(110, 52, 62), term_color(140, 66, 78)),
}
}
pub(crate) fn light() -> Self {
Self {
light: true,
fg_dim: term_color(100, 108, 122),
fg_bright: term_color(30, 41, 59),
blue: term_color(5, 133, 168),
green: term_color(71, 143, 20),
red: term_color(189, 81, 81),
amber: term_color(195, 117, 34),
border: term_color(148, 156, 176),
badge_fg: term_color(255, 255, 255),
scrollbar_thumb: term_color(110, 120, 145),
keycap_fg: term_color(30, 41, 59),
keycap_bg: term_color(226, 230, 238),
hint_fg: term_color(71, 85, 105),
logo: term_color(214, 77, 0),
placeholder_fg: term_color(158, 70, 76),
rpg_frame: term_color(90, 74, 47),
rpg_accent: term_color(217, 95, 16),
rpg_hp: term_color(196, 72, 72),
rpg_mp: term_color(36, 110, 196),
rpg_exp: term_color(200, 120, 20),
rpg_gauge_empty: term_color(198, 190, 170),
rpg_gold: term_color(170, 122, 12),
band_modified: (term_color(219, 233, 249), term_color(196, 219, 244)),
band_added: (term_color(222, 240, 216), term_color(200, 229, 192)),
band_deleted: (term_color(229, 231, 236), term_color(212, 215, 223)),
band_conflict: (term_color(250, 223, 224), term_color(245, 200, 202)),
emph_modified: (term_color(176, 208, 242), term_color(152, 192, 236)),
emph_added: (term_color(185, 220, 172), term_color(163, 206, 148)),
emph_deleted: (term_color(203, 207, 217), term_color(186, 191, 204)),
emph_conflict: (term_color(240, 178, 181), term_color(233, 154, 158)),
}
}
pub(crate) fn band_bg(&self, change: ChangeType, current: bool) -> Option<Color> {
let (normal, selected) = match change {
ChangeType::None => return None,
ChangeType::Modified => self.band_modified,
ChangeType::Added => self.band_added,
ChangeType::Deleted => self.band_deleted,
ChangeType::Conflict => self.band_conflict,
};
Some(if current { selected } else { normal })
}
pub(crate) fn emph_bg(&self, change: ChangeType, current: bool) -> Color {
let (normal, selected) = match change {
ChangeType::Added => self.emph_added,
ChangeType::Deleted => self.emph_deleted,
ChangeType::Conflict => self.emph_conflict,
_ => self.emph_modified,
};
if current { selected } else { normal }
}
pub(crate) fn accent(&self, change: ChangeType) -> Color {
match change {
ChangeType::Modified => self.blue,
ChangeType::Added => self.green,
ChangeType::Conflict => self.red,
ChangeType::Deleted | ChangeType::None => self.fg_dim,
}
}
}
impl Default for Theme {
fn default() -> Self {
Self::tokyo_night()
}
}
pub(crate) fn term_color(r: u8, g: u8, b: u8) -> Color {
static TRUECOLOR: OnceLock<bool> = OnceLock::new();
let truecolor = *TRUECOLOR.get_or_init(|| {
std::env::var("COLORTERM").is_ok_and(|v| {
let v = v.to_ascii_lowercase();
v.contains("truecolor") || v.contains("24bit")
})
});
if truecolor {
Color::Rgb(r, g, b)
} else {
Color::Indexed(rgb_to_xterm256(r, g, b))
}
}
fn rgb_to_xterm256(r: u8, g: u8, b: u8) -> u8 {
let level = |v: u8| -> i32 {
if v < 48 {
0
} else if v < 115 {
1
} else {
i32::from((v - 35) / 40).min(5)
}
};
let level_value = |i: i32| -> i32 { if i == 0 { 0 } else { 55 + 40 * i } };
let sq = |a: i32, b: i32| (a - b) * (a - b);
let (r, g, b) = (i32::from(r), i32::from(g), i32::from(b));
let (ri, gi, bi) = (level(r as u8), level(g as u8), level(b as u8));
let cube_dist = sq(r, level_value(ri)) + sq(g, level_value(gi)) + sq(b, level_value(bi));
let avg = (r + g + b) / 3;
let gray_idx = ((avg - 3) / 10).clamp(0, 23);
let gray_value = 8 + 10 * gray_idx;
let gray_dist = sq(r, gray_value) + sq(g, gray_value) + sq(b, gray_value);
if gray_dist < cube_dist {
(232 + gray_idx) as u8
} else {
(16 + 36 * ri + 6 * gi + bi) as u8
}
}
pub(crate) fn detect_light() -> bool {
std::env::var("COLORFGBG")
.ok()
.and_then(|v| parse_colorfgbg(&v))
.unwrap_or(false)
}
fn parse_colorfgbg(value: &str) -> Option<bool> {
let bg = value.split(';').next_back()?.trim().parse::<u8>().ok()?;
Some(bg == 7 || bg == 15)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rgb_quantizes_to_xterm256() {
assert_eq!(rgb_to_xterm256(0, 0, 0), 16); assert_eq!(rgb_to_xterm256(255, 255, 255), 231); assert_eq!(rgb_to_xterm256(255, 0, 0), 196); assert_eq!(rgb_to_xterm256(95, 135, 175), 67); assert_eq!(rgb_to_xterm256(128, 128, 128), 244); }
#[test]
fn parse_colorfgbg_variants() {
assert_eq!(parse_colorfgbg("0;15"), Some(true)); assert_eq!(parse_colorfgbg("15;0"), Some(false)); assert_eq!(parse_colorfgbg("0;default;7"), Some(true)); assert_eq!(parse_colorfgbg("12;8"), Some(false)); assert_eq!(parse_colorfgbg("garbage"), None); assert_eq!(parse_colorfgbg(""), None);
}
#[test]
fn both_variants_provide_bands() {
for theme in [Theme::tokyo_night(), Theme::light()] {
assert!(theme.band_bg(ChangeType::Conflict, false).is_some());
assert!(theme.band_bg(ChangeType::None, false).is_none());
}
assert!(Theme::select(true).light);
assert!(!Theme::select(false).light);
}
}