use ratatui::style::{Color, Modifier, Style};
use std::sync::RwLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum Mode {
#[default]
Dark,
Light,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Palette {
pub accent: Color,
pub bright: Color,
pub grey: Color,
pub heading: Color,
pub dim: Color,
pub link: Color,
pub code: Color,
pub code_bg: Color,
pub code_fg: Color,
pub border: Color,
pub danger: Color,
pub ground: Color,
}
pub const COLOR_KEYS: [&str; 12] = [
"accent", "bright", "grey", "heading", "dim", "link", "code", "code_bg", "code_fg", "border",
"danger", "ground",
];
impl Palette {
pub fn set(&mut self, key: &str, color: Color) -> bool {
match key {
"accent" => self.accent = color,
"bright" => self.bright = color,
"grey" => self.grey = color,
"heading" => self.heading = color,
"dim" => self.dim = color,
"link" => self.link = color,
"code" => self.code = color,
"code_bg" => self.code_bg = color,
"code_fg" => self.code_fg = color,
"border" => self.border = color,
"danger" => self.danger = color,
"ground" => self.ground = color,
_ => return false,
}
true
}
pub fn get(&self, key: &str) -> Option<Color> {
Some(match key {
"accent" => self.accent,
"bright" => self.bright,
"grey" => self.grey,
"heading" => self.heading,
"dim" => self.dim,
"link" => self.link,
"code" => self.code,
"code_bg" => self.code_bg,
"code_fg" => self.code_fg,
"border" => self.border,
"danger" => self.danger,
"ground" => self.ground,
_ => return None,
})
}
}
pub const DARK: Palette = Palette {
accent: Color::Rgb(0xff, 0x9e, 0x64),
bright: Color::Rgb(0xe1, 0xe1, 0xe1),
grey: Color::Rgb(0x78, 0x78, 0x78),
heading: Color::Rgb(0x8f, 0xb4, 0xd9),
dim: Color::Rgb(0x82, 0x82, 0x82),
link: Color::Rgb(0xb4, 0xb4, 0xb4),
code: Color::Rgb(0xd9, 0xa2, 0x7a),
code_bg: Color::Rgb(0x1c, 0x1c, 0x1c),
code_fg: Color::Rgb(0xe1, 0xe1, 0xe1),
border: Color::Rgb(0x32, 0x32, 0x37),
danger: Color::Rgb(0xf7, 0x76, 0x8e),
ground: Color::Rgb(0x14, 0x14, 0x14),
};
pub const LIGHT: Palette = Palette {
accent: Color::Rgb(0xb8, 0x5c, 0x18),
bright: Color::Rgb(0x26, 0x26, 0x26),
grey: Color::Rgb(0x55, 0x55, 0x55),
heading: Color::Rgb(0x3d, 0x6a, 0x99),
dim: Color::Rgb(0x8d, 0x8d, 0x8d),
link: Color::Rgb(0x5a, 0x58, 0x52),
code: Color::Rgb(0x8a, 0x4a, 0x14),
code_bg: Color::Rgb(0xe2, 0xe2, 0xe2),
code_fg: Color::Rgb(0x26, 0x26, 0x26),
border: Color::Rgb(0xc8, 0xc8, 0xcd),
danger: Color::Rgb(0xcd, 0x30, 0x48),
ground: Color::Rgb(0xee, 0xee, 0xee),
};
static PALETTE: RwLock<Palette> = RwLock::new(DARK);
static BOLD_HEADINGS: RwLock<bool> = RwLock::new(true);
static DETECTED: RwLock<Mode> = RwLock::new(Mode::Dark);
pub fn set_detected(mode: Mode) {
if let Ok(mut w) = DETECTED.write() {
*w = mode;
}
}
static FOLLOWS_SYSTEM: RwLock<bool> = RwLock::new(false);
pub fn set_follows_system(on: bool) {
if let Ok(mut w) = FOLLOWS_SYSTEM.write() {
*w = on;
}
}
pub fn follows_system() -> bool {
FOLLOWS_SYSTEM.read().map(|b| *b).unwrap_or(false)
}
pub fn system_mode() -> Option<Mode> {
if !cfg!(target_os = "macos") {
return None;
}
let out = std::process::Command::new("defaults")
.args(["read", "-g", "AppleInterfaceStyle"])
.output()
.ok()?;
Some(if String::from_utf8_lossy(&out.stdout).trim() == "Dark" {
Mode::Dark
} else {
Mode::Light
})
}
pub fn detected() -> Mode {
DETECTED.read().map(|m| *m).unwrap_or(Mode::Dark)
}
pub fn mode_of_background(r: u8, g: u8, b: u8) -> Mode {
let lum = 0.2126 * r as f32 + 0.7152 * g as f32 + 0.0722 * b as f32;
if lum > 127.5 {
Mode::Light
} else {
Mode::Dark
}
}
pub fn base(mode: Mode) -> Palette {
match mode {
Mode::Dark => DARK,
Mode::Light => LIGHT,
}
}
pub fn set_palette(p: Palette) {
if let Ok(mut w) = PALETTE.write() {
*w = p;
}
}
pub fn set_bold_headings(on: bool) {
if let Ok(mut w) = BOLD_HEADINGS.write() {
*w = on;
}
}
pub fn palette() -> Palette {
PALETTE.read().map(|p| *p).unwrap_or(DARK)
}
fn bold() -> Modifier {
let on = BOLD_HEADINGS.read().map(|b| *b).unwrap_or(true);
if on {
Modifier::BOLD
} else {
Modifier::empty()
}
}
pub const PLAIN: Style = Style::new();
pub fn heading(level: usize) -> Style {
match level {
1 => Style::new().fg(palette().accent).add_modifier(bold()),
2 => Style::new().fg(palette().heading).add_modifier(bold()),
3 => Style::new().fg(palette().bright).add_modifier(bold()),
_ => Style::new().add_modifier(bold()),
}
}
pub fn quote() -> Style {
Style::new()
}
pub fn marker() -> Style {
Style::new().fg(palette().dim)
}
pub fn grey() -> Style {
Style::new().fg(palette().grey)
}
pub fn inline_code() -> Style {
Style::new().fg(palette().code)
}
pub fn code() -> Style {
Style::new().fg(palette().code_fg).bg(palette().code_bg)
}
pub fn link() -> Style {
Style::new()
.fg(palette().link)
.add_modifier(Modifier::UNDERLINED)
}
pub fn highlight() -> Style {
Style::new().fg(palette().ground).bg(palette().accent)
}
pub fn done() -> Style {
Style::new().fg(palette().accent)
}
pub fn done_text() -> Style {
Style::new()
.fg(palette().grey)
.add_modifier(Modifier::CROSSED_OUT)
}
pub fn tag() -> Style {
Style::new().fg(palette().accent)
}
pub fn state() -> Style {
Style::new().fg(palette().accent)
}
pub fn border() -> Style {
Style::new().fg(palette().border)
}
pub fn danger() -> Style {
Style::new().fg(palette().danger)
}
pub fn bright() -> Style {
Style::new().fg(palette().bright)
}
pub fn fold() -> Style {
Style::new().fg(palette().accent)
}
pub fn row() -> Style {
Style::new().bg(palette().border)
}
pub fn parse_color(text: &str) -> Option<Color> {
let t = text.trim();
if let Some(hex) = t.strip_prefix('#') {
let digits: Vec<u32> = hex
.chars()
.map(|c| c.to_digit(16))
.collect::<Option<Vec<u32>>>()?;
return match digits.len() {
3 => Some(Color::Rgb(
(digits[0] * 17) as u8,
(digits[1] * 17) as u8,
(digits[2] * 17) as u8,
)),
6 => Some(Color::Rgb(
(digits[0] * 16 + digits[1]) as u8,
(digits[2] * 16 + digits[3]) as u8,
(digits[4] * 16 + digits[5]) as u8,
)),
_ => None,
};
}
Some(match t.to_ascii_lowercase().as_str() {
"black" => Color::Black,
"red" => Color::Red,
"green" => Color::Green,
"yellow" => Color::Yellow,
"blue" => Color::Blue,
"magenta" => Color::Magenta,
"cyan" => Color::Cyan,
"white" => Color::White,
"gray" | "grey" | "darkgray" | "darkgrey" => Color::DarkGray,
"brightred" => Color::LightRed,
"brightgreen" => Color::LightGreen,
"brightyellow" => Color::LightYellow,
"brightblue" => Color::LightBlue,
"brightmagenta" => Color::LightMagenta,
"brightcyan" => Color::LightCyan,
"brightwhite" => Color::Gray,
"default" | "terminal" => Color::Reset,
_ => return None,
})
}
pub fn color_to_string(c: Color) -> String {
match c {
Color::Rgb(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
Color::Reset => "default".to_string(),
other => format!("{other:?}").to_lowercase(),
}
}
pub const CHECKED: &str = "\u{2713}";
pub const UNCHECKED: &str = "\u{2610}";
pub const BULLET: &str = "\u{2022}";
pub const FOLDED: &str = "\u{25b8} ";
pub const QUOTE_BAR: &str = "\u{258c}";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn each_of_the_first_three_heading_levels_takes_its_own_colour() {
for p in [DARK, LIGHT] {
set_palette(p);
let fg: Vec<_> = (1..=4).map(|l| heading(l).fg).collect();
assert_eq!(fg[..3], [Some(p.accent), Some(p.heading), Some(p.bright)]);
assert_eq!(fg[3], None);
assert_ne!(p.accent, p.heading);
assert_ne!(p.heading, p.bright);
}
set_palette(DARK);
}
#[test]
fn code_states_both_halves_so_a_light_terminal_is_not_black_on_black() {
let c = code();
assert!(c.bg.is_some());
assert!(
c.fg.is_some(),
"code without a foreground is unreadable on a terminal whose ink matches code_bg"
);
}
}