use crate::options::{Options, SizeUnits, Theme};
use ratatui::style::Color;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
pub struct Palette {
pub background: Color,
pub gutter: Color,
pub light: bool,
pub syntax_theme: &'static str,
pub border: Color,
pub columns: Color,
pub directory: Color,
pub directory_dark: Color,
pub directory_bracket: Color,
pub file: Color,
pub rename_background: Color,
pub selected_background: Color,
pub selected_background_inactive: Color,
pub selected_foreground: Color,
pub title: Color,
pub selected_marker: Color,
}
const fn rgb(red: u8, green: u8, blue: u8) -> Color {
Color::Rgb(red, green, blue)
}
const DARK_SYNTAX: &str = "base16-ocean.dark";
const LIGHT_SYNTAX: &str = "InspiredGitHub";
const SYNTHWAVE: Palette = Palette {
background: rgb(0, 0, 0), gutter: rgb(14, 7, 26), light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(116, 58, 213), columns: rgb(0, 255, 255), directory: rgb(255, 0, 255), directory_dark: rgb(150, 0, 150), directory_bracket: rgb(255, 0, 255), file: rgb(114, 137, 218), rename_background: rgb(255, 0, 128), selected_background: rgb(148, 0, 211), selected_background_inactive: rgb(45, 0, 75), selected_foreground: rgb(0, 255, 255), title: rgb(242, 34, 255), selected_marker: rgb(255, 215, 0), };
const OUTRUN: Palette = Palette {
background: rgb(24, 10, 30),
gutter: rgb(16, 6, 22),
light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(255, 60, 120), columns: rgb(255, 190, 0), directory: rgb(255, 120, 40), directory_dark: rgb(160, 70, 30), directory_bracket: rgb(255, 120, 40), file: rgb(120, 160, 255), rename_background: rgb(255, 0, 80), selected_background: rgb(200, 30, 90), selected_background_inactive: rgb(60, 10, 40), selected_foreground: rgb(255, 230, 120), title: rgb(255, 110, 60), selected_marker: rgb(0, 255, 200), };
const VAPORWAVE: Palette = Palette {
background: rgb(32, 26, 52),
gutter: rgb(24, 18, 40),
light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(150, 120, 255), columns: rgb(120, 255, 230), directory: rgb(255, 130, 220), directory_dark: rgb(150, 80, 130), directory_bracket: rgb(255, 130, 220), file: rgb(180, 200, 255), rename_background: rgb(200, 50, 130), selected_background: rgb(110, 80, 200), selected_background_inactive: rgb(40, 30, 70), selected_foreground: rgb(255, 240, 250), title: rgb(255, 160, 230), selected_marker: rgb(255, 230, 120), };
const HIGH_CONTRAST: Palette = Palette {
background: rgb(0, 0, 0),
gutter: rgb(24, 24, 24),
light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(255, 255, 255),
columns: rgb(255, 255, 0),
directory: rgb(0, 255, 255),
directory_dark: rgb(0, 170, 170),
directory_bracket: rgb(0, 255, 255),
file: rgb(235, 235, 235),
rename_background: rgb(200, 0, 0),
selected_background: rgb(0, 80, 220),
selected_background_inactive: rgb(70, 70, 70),
selected_foreground: rgb(255, 255, 255),
title: rgb(255, 255, 255),
selected_marker: rgb(255, 255, 0),
};
const DRACULA: Palette = Palette {
background: rgb(40, 42, 54), gutter: rgb(33, 34, 44), light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(98, 114, 164), columns: rgb(139, 233, 253), directory: rgb(189, 147, 249), directory_dark: rgb(98, 114, 164), directory_bracket: rgb(189, 147, 249), file: rgb(248, 248, 242), rename_background: rgb(200, 50, 60), selected_background: rgb(98, 114, 164), selected_background_inactive: rgb(68, 71, 90), selected_foreground: rgb(248, 248, 242), title: rgb(255, 121, 198), selected_marker: rgb(241, 250, 140), };
const MONOKAI: Palette = Palette {
background: rgb(39, 40, 34),
gutter: rgb(30, 31, 27),
light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(117, 113, 94), columns: rgb(102, 217, 239), directory: rgb(166, 226, 46), directory_dark: rgb(110, 140, 40),
directory_bracket: rgb(166, 226, 46), file: rgb(248, 248, 242), rename_background: rgb(190, 20, 85), selected_background: rgb(90, 88, 72),
selected_background_inactive: rgb(62, 61, 50), selected_foreground: rgb(230, 219, 116), title: rgb(249, 38, 114), selected_marker: rgb(253, 151, 31), };
const NORD: Palette = Palette {
background: rgb(46, 52, 64), gutter: rgb(41, 46, 57),
light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(76, 86, 106), columns: rgb(136, 192, 208), directory: rgb(129, 161, 193), directory_dark: rgb(94, 110, 140),
directory_bracket: rgb(129, 161, 193), file: rgb(216, 222, 233), rename_background: rgb(191, 97, 106), selected_background: rgb(94, 129, 172), selected_background_inactive: rgb(59, 66, 82), selected_foreground: rgb(236, 239, 244), title: rgb(143, 188, 187), selected_marker: rgb(235, 203, 139), };
const GRUVBOX_DARK: Palette = Palette {
background: rgb(40, 40, 40), gutter: rgb(29, 32, 33), light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(146, 131, 116), columns: rgb(142, 192, 124), directory: rgb(250, 189, 47), directory_dark: rgb(181, 118, 20), directory_bracket: rgb(250, 189, 47), file: rgb(235, 219, 178), rename_background: rgb(204, 36, 29), selected_background: rgb(102, 92, 84), selected_background_inactive: rgb(60, 56, 54), selected_foreground: rgb(251, 241, 199), title: rgb(254, 128, 25), selected_marker: rgb(184, 187, 38), };
const SOLARIZED_DARK: Palette = Palette {
background: rgb(0, 43, 54), gutter: rgb(0, 36, 46),
light: false,
syntax_theme: "Solarized (dark)",
border: rgb(88, 110, 117), columns: rgb(42, 161, 152), directory: rgb(38, 139, 210), directory_dark: rgb(30, 95, 140),
directory_bracket: rgb(38, 139, 210), file: rgb(131, 148, 150), rename_background: rgb(220, 50, 47), selected_background: rgb(88, 110, 117), selected_background_inactive: rgb(7, 54, 66), selected_foreground: rgb(253, 246, 227), title: rgb(211, 54, 130), selected_marker: rgb(181, 137, 0), };
const TOKYO_NIGHT: Palette = Palette {
background: rgb(26, 27, 38),
gutter: rgb(22, 22, 30),
light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(86, 95, 137), columns: rgb(125, 207, 255), directory: rgb(122, 162, 247), directory_dark: rgb(70, 90, 150),
directory_bracket: rgb(122, 162, 247), file: rgb(192, 202, 245), rename_background: rgb(170, 50, 75), selected_background: rgb(51, 70, 124), selected_background_inactive: rgb(41, 46, 66), selected_foreground: rgb(192, 202, 245), title: rgb(187, 154, 247), selected_marker: rgb(224, 175, 104), };
const CATPPUCCIN_MOCHA: Palette = Palette {
background: rgb(30, 30, 46), gutter: rgb(24, 24, 37), light: false,
syntax_theme: DARK_SYNTAX,
border: rgb(108, 112, 134), columns: rgb(148, 226, 213), directory: rgb(137, 180, 250), directory_dark: rgb(90, 120, 170),
directory_bracket: rgb(137, 180, 250), file: rgb(205, 214, 244), rename_background: rgb(170, 60, 90), selected_background: rgb(88, 91, 112), selected_background_inactive: rgb(49, 50, 68), selected_foreground: rgb(205, 214, 244), title: rgb(203, 166, 247), selected_marker: rgb(249, 226, 175), };
const SOLARIZED_LIGHT: Palette = Palette {
background: rgb(253, 246, 227), gutter: rgb(238, 232, 213), light: true,
syntax_theme: "Solarized (light)",
border: rgb(147, 161, 161), columns: rgb(30, 125, 118), directory: rgb(38, 139, 210), directory_dark: rgb(110, 150, 180),
directory_bracket: rgb(38, 139, 210), file: rgb(101, 123, 131), rename_background: rgb(246, 196, 190),
selected_background: rgb(197, 222, 240),
selected_background_inactive: rgb(238, 232, 213), selected_foreground: rgb(7, 54, 66), title: rgb(211, 54, 130), selected_marker: rgb(203, 75, 22), };
const GRUVBOX_LIGHT: Palette = Palette {
background: rgb(251, 241, 199), gutter: rgb(242, 229, 188), light: true,
syntax_theme: "base16-ocean.light",
border: rgb(146, 131, 116), columns: rgb(66, 123, 88), directory: rgb(7, 102, 120), directory_dark: rgb(100, 140, 150),
directory_bracket: rgb(7, 102, 120), file: rgb(60, 56, 54), rename_background: rgb(240, 180, 160),
selected_background: rgb(213, 196, 161), selected_background_inactive: rgb(235, 219, 178), selected_foreground: rgb(40, 40, 40), title: rgb(175, 58, 3), selected_marker: rgb(143, 63, 113), };
const CATPPUCCIN_LATTE: Palette = Palette {
background: rgb(239, 241, 245), gutter: rgb(230, 233, 239), light: true,
syntax_theme: LIGHT_SYNTAX,
border: rgb(156, 160, 176), columns: rgb(23, 146, 153), directory: rgb(30, 102, 245), directory_dark: rgb(120, 150, 220),
directory_bracket: rgb(30, 102, 245), file: rgb(76, 79, 105), rename_background: rgb(240, 180, 195),
selected_background: rgb(188, 192, 204), selected_background_inactive: rgb(220, 224, 232), selected_foreground: rgb(76, 79, 105), title: rgb(136, 57, 239), selected_marker: rgb(254, 100, 11), };
const GITHUB_LIGHT: Palette = Palette {
background: rgb(255, 255, 255),
gutter: rgb(246, 248, 250),
light: true,
syntax_theme: LIGHT_SYNTAX,
border: rgb(175, 184, 193),
columns: rgb(26, 127, 55), directory: rgb(9, 105, 218), directory_dark: rgb(84, 140, 210),
directory_bracket: rgb(9, 105, 218), file: rgb(31, 35, 40), rename_background: rgb(255, 210, 210),
selected_background: rgb(200, 225, 255),
selected_background_inactive: rgb(234, 238, 242),
selected_foreground: rgb(31, 35, 40), title: rgb(130, 80, 223), selected_marker: rgb(188, 76, 0), };
pub fn palette_of(theme: Theme) -> &'static Palette {
match theme {
Theme::Synthwave => &SYNTHWAVE,
Theme::Outrun => &OUTRUN,
Theme::Vaporwave => &VAPORWAVE,
Theme::HighContrast => &HIGH_CONTRAST,
Theme::Dracula => &DRACULA,
Theme::Monokai => &MONOKAI,
Theme::Nord => &NORD,
Theme::GruvboxDark => &GRUVBOX_DARK,
Theme::SolarizedDark => &SOLARIZED_DARK,
Theme::TokyoNight => &TOKYO_NIGHT,
Theme::CatppuccinMocha => &CATPPUCCIN_MOCHA,
Theme::SolarizedLight => &SOLARIZED_LIGHT,
Theme::GruvboxLight => &GRUVBOX_LIGHT,
Theme::CatppuccinLatte => &CATPPUCCIN_LATTE,
Theme::GithubLight => &GITHUB_LIGHT,
}
}
static THEME: AtomicU8 = AtomicU8::new(0);
static THEME_BACKGROUND: AtomicBool = AtomicBool::new(true);
static TAB_WIDTH: AtomicUsize = AtomicUsize::new(4);
static DECIMAL_SIZES: AtomicBool = AtomicBool::new(false);
pub fn apply(options: &Options) {
let index = Theme::ALL.iter().position(|&theme| theme == options.theme).unwrap_or(0);
THEME.store(index as u8, Ordering::Relaxed);
THEME_BACKGROUND.store(options.theme_background, Ordering::Relaxed);
TAB_WIDTH.store(options.tab_width, Ordering::Relaxed);
DECIMAL_SIZES.store(options.size_units == SizeUnits::Decimal, Ordering::Relaxed);
}
pub fn palette() -> &'static Palette {
let index = THEME.load(Ordering::Relaxed) as usize;
palette_of(Theme::ALL.get(index).copied().unwrap_or(Theme::Synthwave))
}
pub fn background() -> Option<Color> {
THEME_BACKGROUND.load(Ordering::Relaxed).then(|| palette().background)
}
pub fn light_background() -> bool {
background().is_some() && palette().light
}
pub fn gutter() -> Color {
if THEME_BACKGROUND.load(Ordering::Relaxed) { palette().gutter } else { Color::Black }
}
pub fn tab_width() -> usize {
TAB_WIDTH.load(Ordering::Relaxed)
}
pub fn decimal_sizes() -> bool {
DECIMAL_SIZES.load(Ordering::Relaxed)
}
#[cfg(test)]
mod tests {
use super::*;
fn channels(color: Color) -> (f64, f64, f64) {
match color {
Color::Rgb(red, green, blue) => (red as f64, green as f64, blue as f64),
other => panic!("palettes are all RGB, not {other:?}"),
}
}
fn luminance(color: Color) -> f64 {
let linear = |channel: f64| {
let c = channel / 255.0;
if c <= 0.03928 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
};
let (red, green, blue) = channels(color);
0.2126 * linear(red) + 0.7152 * linear(green) + 0.0722 * linear(blue)
}
fn contrast(a: Color, b: Color) -> f64 {
let (a, b) = (luminance(a), luminance(b));
(a.max(b) + 0.05) / (a.min(b) + 0.05)
}
#[test]
fn every_theme_can_be_read() {
let mut failures = Vec::new();
for theme in Theme::ALL {
let palette = palette_of(theme);
let name = theme.label();
assert_eq!(palette.light, luminance(palette.background) > 0.5, "{name}: light flag");
for (what, fg, bg) in [
("file", palette.file, palette.background),
("directory", palette.directory, palette.background),
("columns", palette.columns, palette.background),
("title", palette.title, palette.background),
("cursor", palette.selected_foreground, palette.selected_background),
("inactive cursor", palette.selected_foreground, palette.selected_background_inactive),
("rename", palette.selected_foreground, palette.rename_background),
] {
let ratio = contrast(fg, bg);
if ratio < 3.0 {
failures.push(format!("{name}: {what} contrast {ratio:.2}"));
}
}
}
assert!(failures.is_empty(), "{}", failures.join("\n"));
}
#[test]
fn extension_colours_read_on_every_background() {
let extensions: Vec<String> = (b'a'..=b'z').flat_map(|a| (b'a'..=b'z').map(move |b| format!("{}{}", a as char, b as char))).collect();
let mut worst = (f64::MAX, String::new());
for theme in Theme::ALL {
let palette = palette_of(theme);
for extension in &extensions {
let ratio = contrast(crate::utils::extension_color(extension, palette.light), palette.background);
if ratio < worst.0 {
worst = (ratio, format!("{} .{}", theme.label(), extension));
}
}
}
assert!(worst.0 >= 3.0, "{}: contrast {:.2}", worst.1, worst.0);
}
#[test]
fn every_syntax_theme_exists() {
let themes = syntect::highlighting::ThemeSet::load_defaults();
for theme in Theme::ALL {
let name = palette_of(theme).syntax_theme;
assert!(themes.themes.contains_key(name), "{}: no syntect theme {name}", theme.label());
}
}
}