use ratatui::style::Color;
#[derive(Clone)]
pub struct Theme {
pub crust: Color,
pub mantle: Color,
pub base: Color,
pub surface0: Color,
pub surface1: Color,
pub overlay0: Color,
pub overlay1: Color,
pub subtext0: Color,
pub subtext1: Color,
pub text: Color,
pub accent: Color, pub sel_bg: Color, pub border: Color, pub border_focus: Color, pub green: Color,
pub mint: Color,
pub amber: Color,
pub coral: Color,
}
impl Theme {
pub fn noir() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x07, 0x07, 0x09),
mantle: rgb(0x11, 0x11, 0x16), base: rgb(0x20, 0x20, 0x28), surface0: rgb(0x1a, 0x1a, 0x20),
surface1: rgb(0x25, 0x25, 0x2d),
overlay0: rgb(0x4a, 0x4a, 0x54),
overlay1: rgb(0x68, 0x68, 0x73),
subtext0: rgb(0x93, 0x93, 0x9f),
subtext1: rgb(0xb6, 0xb6, 0xc0),
text: rgb(0xe7, 0xe7, 0xed),
accent: rgb(0xc6, 0xff, 0x1a), sel_bg: rgb(0x33, 0x45, 0x0e), border: rgb(0x38, 0x38, 0x40), border_focus: rgb(0x8c, 0x8c, 0x96), green: rgb(0x8f, 0xbc, 0x7a), mint: rgb(0x6f, 0xc6, 0xa3), amber: rgb(0xe0, 0x9a, 0x4d), coral: rgb(0xe0, 0x6c, 0x66), }
}
pub fn mono() -> Self {
let g = |v| Color::Rgb(v, v, v);
Theme {
crust: g(0x07),
mantle: g(0x12),
base: g(0x1e),
surface0: g(0x18),
surface1: g(0x28),
overlay0: g(0x4a),
overlay1: g(0x68),
subtext0: g(0x93),
subtext1: g(0xb6),
text: g(0xec),
accent: g(0xea), sel_bg: g(0x33),
border: g(0x52),
border_focus: g(0x90),
green: g(0x82),
mint: g(0xa6),
amber: g(0xc8),
coral: g(0xe6),
}
}
pub fn mocha() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x11, 0x11, 0x1b),
mantle: rgb(0x18, 0x18, 0x25), base: rgb(0x1e, 0x1e, 0x2e), surface0: rgb(0x31, 0x32, 0x44),
surface1: rgb(0x45, 0x47, 0x5a),
overlay0: rgb(0x6c, 0x70, 0x86),
overlay1: rgb(0x7f, 0x84, 0x9c),
subtext0: rgb(0xa6, 0xad, 0xc8),
subtext1: rgb(0xba, 0xc2, 0xde),
text: rgb(0xcd, 0xd6, 0xf4),
accent: rgb(0xcb, 0xa6, 0xf7), sel_bg: rgb(0x3f, 0x33, 0x59), border: rgb(0x45, 0x47, 0x5a),
border_focus: rgb(0x7f, 0x84, 0x9c),
green: rgb(0xa6, 0xe3, 0xa1), mint: rgb(0x94, 0xe2, 0xd5), amber: rgb(0xfa, 0xb3, 0x87), coral: rgb(0xf3, 0x8b, 0xa8), }
}
pub fn macchiato() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x18, 0x19, 0x26),
mantle: rgb(0x1e, 0x20, 0x30),
base: rgb(0x24, 0x27, 0x3a),
surface0: rgb(0x36, 0x3a, 0x4f),
surface1: rgb(0x49, 0x4d, 0x64),
overlay0: rgb(0x6e, 0x73, 0x8d),
overlay1: rgb(0x80, 0x87, 0xa2),
subtext0: rgb(0xa5, 0xad, 0xcb),
subtext1: rgb(0xb8, 0xc0, 0xe0),
text: rgb(0xca, 0xd3, 0xf5),
accent: rgb(0xc6, 0xa0, 0xf6), sel_bg: rgb(0x3b, 0x32, 0x54),
border: rgb(0x49, 0x4d, 0x64),
border_focus: rgb(0x80, 0x87, 0xa2),
green: rgb(0xa6, 0xda, 0x95),
mint: rgb(0x8b, 0xd5, 0xca),
amber: rgb(0xf5, 0xa9, 0x7f),
coral: rgb(0xed, 0x87, 0x96),
}
}
pub fn frappe() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x23, 0x26, 0x34),
mantle: rgb(0x29, 0x2c, 0x3c),
base: rgb(0x30, 0x34, 0x46),
surface0: rgb(0x41, 0x45, 0x59),
surface1: rgb(0x51, 0x57, 0x6d),
overlay0: rgb(0x73, 0x79, 0x94),
overlay1: rgb(0x83, 0x8b, 0xa7),
subtext0: rgb(0xa5, 0xad, 0xce),
subtext1: rgb(0xb5, 0xbf, 0xe2),
text: rgb(0xc6, 0xd0, 0xf5),
accent: rgb(0xca, 0x9e, 0xe6), sel_bg: rgb(0x46, 0x3e, 0x5e),
border: rgb(0x51, 0x57, 0x6d),
border_focus: rgb(0x83, 0x8b, 0xa7),
green: rgb(0xa6, 0xd1, 0x89),
mint: rgb(0x81, 0xc8, 0xbe),
amber: rgb(0xef, 0x9f, 0x76),
coral: rgb(0xe7, 0x82, 0x84),
}
}
pub fn gruvbox() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x1d, 0x20, 0x21), mantle: rgb(0x28, 0x28, 0x28), base: rgb(0x3c, 0x38, 0x36), surface0: rgb(0x32, 0x30, 0x2f),
surface1: rgb(0x50, 0x49, 0x45),
overlay0: rgb(0x66, 0x5c, 0x54),
overlay1: rgb(0x7c, 0x6f, 0x64),
subtext0: rgb(0xa8, 0x99, 0x84),
subtext1: rgb(0xbd, 0xae, 0x93),
text: rgb(0xeb, 0xdb, 0xb2),
accent: rgb(0xfa, 0xbd, 0x2f), sel_bg: rgb(0x45, 0x3d, 0x21), border: rgb(0x50, 0x49, 0x45),
border_focus: rgb(0x92, 0x83, 0x74),
green: rgb(0xb8, 0xbb, 0x26), mint: rgb(0x8e, 0xc0, 0x7c), amber: rgb(0xfe, 0x80, 0x19), coral: rgb(0xfb, 0x49, 0x34), }
}
pub fn gruvbox_light() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0xfb, 0xf1, 0xc7), mantle: rgb(0xf2, 0xe5, 0xbc), base: rgb(0xeb, 0xdb, 0xb2), surface0: rgb(0xd5, 0xc4, 0xa1),
surface1: rgb(0xbd, 0xae, 0x93),
overlay0: rgb(0xa8, 0x99, 0x84),
overlay1: rgb(0x92, 0x83, 0x74),
subtext0: rgb(0x66, 0x5c, 0x54),
subtext1: rgb(0x50, 0x49, 0x45),
text: rgb(0x3c, 0x38, 0x36),
accent: rgb(0xaf, 0x3a, 0x03), sel_bg: rgb(0xe0, 0xc6, 0x8a),
border: rgb(0xbd, 0xae, 0x93),
border_focus: rgb(0x7c, 0x6f, 0x64),
green: rgb(0x79, 0x74, 0x0e), mint: rgb(0x42, 0x7b, 0x58), amber: rgb(0xb5, 0x76, 0x14), coral: rgb(0x9d, 0x00, 0x06), }
}
pub fn latte() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0xef, 0xf1, 0xf5),
mantle: rgb(0xe6, 0xe9, 0xef),
base: rgb(0xdc, 0xe0, 0xe8),
surface0: rgb(0xcc, 0xd0, 0xda),
surface1: rgb(0xbc, 0xc0, 0xcc),
overlay0: rgb(0x9c, 0xa0, 0xb0),
overlay1: rgb(0x7c, 0x80, 0x90),
subtext0: rgb(0x6c, 0x6f, 0x85),
subtext1: rgb(0x50, 0x52, 0x6c),
text: rgb(0x4c, 0x4f, 0x69),
accent: rgb(0x40, 0xa0, 0x2b), sel_bg: rgb(0xc6, 0xe8, 0xa8),
border: rgb(0xac, 0xb0, 0xbe),
border_focus: rgb(0x7c, 0x80, 0x90),
green: rgb(0x40, 0xa0, 0x2b),
mint: rgb(0x17, 0x92, 0x99),
amber: rgb(0xdf, 0x8e, 0x1d),
coral: rgb(0xd2, 0x0f, 0x39),
}
}
pub fn ocean() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x02, 0x10, 0x2a),
mantle: rgb(0x06, 0x1d, 0x42),
base: rgb(0x0c, 0x2e, 0x5e),
surface0: rgb(0x05, 0x22, 0x4a),
surface1: rgb(0x12, 0x3a, 0x6e),
overlay0: rgb(0x35, 0x58, 0x86),
overlay1: rgb(0x52, 0x76, 0xa4),
subtext0: rgb(0x8f, 0xa8, 0xc8),
subtext1: rgb(0xb8, 0xcc, 0xe6),
text: rgb(0xe8, 0xf2, 0xff),
accent: rgb(0x46, 0xc6, 0xff), sel_bg: rgb(0x12, 0x3e, 0x76),
border: rgb(0x2a, 0x4e, 0x80),
border_focus: rgb(0x5a, 0x86, 0xc0),
green: rgb(0x6f, 0xcf, 0x97),
mint: rgb(0x4f, 0xd6, 0xc8),
amber: rgb(0xf2, 0xc1, 0x4e),
coral: rgb(0xf0, 0x6a, 0x6a),
}
}
pub fn homebrew() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x00, 0x00, 0x00),
mantle: rgb(0x04, 0x0a, 0x04),
base: rgb(0x08, 0x16, 0x08),
surface0: rgb(0x06, 0x10, 0x06),
surface1: rgb(0x0e, 0x24, 0x0e),
overlay0: rgb(0x1e, 0x4c, 0x1e),
overlay1: rgb(0x2e, 0x6c, 0x2e),
subtext0: rgb(0x22, 0xb4, 0x22),
subtext1: rgb(0x2e, 0xe0, 0x2e),
text: rgb(0x3c, 0xff, 0x3c), accent: rgb(0x00, 0xff, 0x41), sel_bg: rgb(0x0a, 0x3a, 0x0a),
border: rgb(0x1a, 0x4e, 0x1a),
border_focus: rgb(0x2e, 0xa8, 0x2e),
green: rgb(0x35, 0xe0, 0x35),
mint: rgb(0x5e, 0xff, 0xb0),
amber: rgb(0xc8, 0xff, 0x3c),
coral: rgb(0xff, 0x60, 0x50),
}
}
pub fn red_sands() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x1f, 0x0a, 0x06),
mantle: rgb(0x38, 0x12, 0x0c),
base: rgb(0x4e, 0x1c, 0x12),
surface0: rgb(0x2c, 0x0e, 0x08),
surface1: rgb(0x5c, 0x26, 0x1a),
overlay0: rgb(0x8a, 0x4a, 0x38),
overlay1: rgb(0xa8, 0x68, 0x54),
subtext0: rgb(0xc8, 0x9a, 0x80),
subtext1: rgb(0xdc, 0xba, 0x9c),
text: rgb(0xf2, 0xda, 0xba), accent: rgb(0xff, 0x8a, 0x3c), sel_bg: rgb(0x70, 0x2e, 0x1e),
border: rgb(0x7c, 0x3c, 0x2a),
border_focus: rgb(0xba, 0x6e, 0x4c),
green: rgb(0xa6, 0xbf, 0x5e),
mint: rgb(0x5e, 0xc8, 0xa8),
amber: rgb(0xff, 0xb4, 0x54),
coral: rgb(0xff, 0x6a, 0x5a),
}
}
pub fn grass() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x05, 0x20, 0x12),
mantle: rgb(0x0a, 0x36, 0x1d),
base: rgb(0x10, 0x4c, 0x28),
surface0: rgb(0x08, 0x2c, 0x18),
surface1: rgb(0x15, 0x58, 0x30),
overlay0: rgb(0x36, 0x78, 0x50),
overlay1: rgb(0x54, 0x96, 0x6c),
subtext0: rgb(0xa8, 0xc8, 0x98),
subtext1: rgb(0xc8, 0xe0, 0xae),
text: rgb(0xff, 0xf0, 0xa5), accent: rgb(0xbe, 0xe6, 0x32), sel_bg: rgb(0x1a, 0x6c, 0x3a),
border: rgb(0x28, 0x76, 0x48),
border_focus: rgb(0x56, 0xa6, 0x6c),
green: rgb(0x8f, 0xd0, 0x7a),
mint: rgb(0x5e, 0xd6, 0xb0),
amber: rgb(0xf2, 0xc8, 0x4e),
coral: rgb(0xf0, 0x6a, 0x5a),
}
}
pub fn dracula() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x1a, 0x1b, 0x23),
mantle: rgb(0x28, 0x2a, 0x36),
base: rgb(0x33, 0x36, 0x47),
surface0: rgb(0x21, 0x22, 0x2c),
surface1: rgb(0x3c, 0x3f, 0x52),
overlay0: rgb(0x56, 0x58, 0x69),
overlay1: rgb(0x62, 0x72, 0xa4),
subtext0: rgb(0xa0, 0xa4, 0xc0),
subtext1: rgb(0xcc, 0xce, 0xe0),
text: rgb(0xf8, 0xf8, 0xf2),
accent: rgb(0xbd, 0x93, 0xf9), sel_bg: rgb(0x44, 0x47, 0x5a),
border: rgb(0x44, 0x47, 0x5a),
border_focus: rgb(0x62, 0x72, 0xa4),
green: rgb(0x50, 0xfa, 0x7b),
mint: rgb(0x8b, 0xe9, 0xfd),
amber: rgb(0xff, 0xb8, 0x6c),
coral: rgb(0xff, 0x55, 0x55),
}
}
pub fn nord() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x24, 0x29, 0x33),
mantle: rgb(0x2e, 0x34, 0x40),
base: rgb(0x3b, 0x42, 0x52),
surface0: rgb(0x29, 0x2f, 0x3a),
surface1: rgb(0x43, 0x4c, 0x5e),
overlay0: rgb(0x4c, 0x56, 0x6a),
overlay1: rgb(0x61, 0x6e, 0x88),
subtext0: rgb(0xb0, 0xb8, 0xc8),
subtext1: rgb(0xd8, 0xde, 0xe9),
text: rgb(0xec, 0xef, 0xf4),
accent: rgb(0x88, 0xc0, 0xd0), sel_bg: rgb(0x3b, 0x4a, 0x5e),
border: rgb(0x43, 0x4c, 0x5e),
border_focus: rgb(0x6a, 0x76, 0x90),
green: rgb(0xa3, 0xbe, 0x8c),
mint: rgb(0x8f, 0xbc, 0xbb),
amber: rgb(0xeb, 0xcb, 0x8b),
coral: rgb(0xbf, 0x61, 0x6a),
}
}
pub fn sunset() -> Self {
let rgb = |r, g, b| Color::Rgb(r, g, b);
Theme {
crust: rgb(0x16, 0x0a, 0x1e),
mantle: rgb(0x22, 0x10, 0x30),
base: rgb(0x2e, 0x16, 0x40),
surface0: rgb(0x1c, 0x0d, 0x2a),
surface1: rgb(0x3a, 0x1d, 0x50),
overlay0: rgb(0x5a, 0x3a, 0x70),
overlay1: rgb(0x7a, 0x5a, 0x90),
subtext0: rgb(0xb8, 0x9a, 0xd0),
subtext1: rgb(0xd6, 0xbc, 0xe8),
text: rgb(0xf6, 0xe8, 0xff),
accent: rgb(0xff, 0x5f, 0xd0), sel_bg: rgb(0x4a, 0x24, 0x66),
border: rgb(0x4a, 0x2a, 0x64),
border_focus: rgb(0x8a, 0x5a, 0xa8),
green: rgb(0x5f, 0xe0, 0xa8),
mint: rgb(0x5f, 0xd6, 0xe0),
amber: rgb(0xff, 0xb5, 0x4f),
coral: rgb(0xff, 0x5f, 0x8f),
}
}
pub fn to_256(&self) -> Theme {
let c = crate::ipc::protocol::to_256;
Theme {
crust: c(self.crust),
mantle: c(self.mantle),
base: c(self.base),
surface0: c(self.surface0),
surface1: c(self.surface1),
overlay0: c(self.overlay0),
overlay1: c(self.overlay1),
subtext0: c(self.subtext0),
subtext1: c(self.subtext1),
text: c(self.text),
accent: c(self.accent),
sel_bg: c(self.sel_bg),
border: c(self.border),
border_focus: c(self.border_focus),
green: c(self.green),
mint: c(self.mint),
amber: c(self.amber),
coral: c(self.coral),
}
}
}
pub const THEMES: &[&str] = &[
"noir",
"ocean",
"dracula",
"nord",
"catppuccin-mocha",
"catppuccin-macchiato",
"catppuccin-frappe",
"gruvbox",
"sunset",
"homebrew",
"grass",
"redsands",
"catppuccin-latte",
"gruvbox-light",
"mono",
];
pub fn canonical(name: &str) -> &str {
match name {
"latte" => "catppuccin-latte",
"mocha" => "catppuccin-mocha",
"macchiato" => "catppuccin-macchiato",
"frappe" => "catppuccin-frappe",
"gruvboxlight" => "gruvbox-light",
other => other,
}
}
pub fn by_name(name: &str) -> Theme {
match name {
"ocean" => Theme::ocean(),
"homebrew" => Theme::homebrew(),
"redsands" => Theme::red_sands(),
"grass" => Theme::grass(),
"dracula" => Theme::dracula(),
"nord" => Theme::nord(),
"catppuccin-mocha" | "mocha" => Theme::mocha(),
"catppuccin-macchiato" | "macchiato" => Theme::macchiato(),
"catppuccin-frappe" | "frappe" => Theme::frappe(),
"gruvbox" => Theme::gruvbox(),
"gruvbox-light" | "gruvboxlight" => Theme::gruvbox_light(),
"sunset" => Theme::sunset(),
"catppuccin-latte" | "latte" => Theme::latte(),
"mono" => Theme::mono(),
_ => Theme::noir(),
}
}
pub fn describe(name: &str) -> &'static str {
match name {
"ocean" => "deep cmd-blue, cyan accent",
"homebrew" => "classic green-on-black",
"redsands" => "warm dark red, orange accent",
"grass" => "green field, pale-yellow text",
"dracula" => "indigo dark, violet accent",
"nord" => "cool arctic blue-grey",
"catppuccin-mocha" | "mocha" => "darkest Catppuccin, mauve",
"catppuccin-macchiato" | "macchiato" => "softer dark Catppuccin",
"catppuccin-frappe" | "frappe" => "lightest dark Catppuccin",
"gruvbox" => "retro warm dark, yellow accent",
"gruvbox-light" | "gruvboxlight" => "Gruvbox on cream, burnt orange",
"sunset" => "neon synthwave, hot-pink",
"catppuccin-latte" | "latte" => "light Catppuccin, warm",
"mono" => "grayscale, no accent color",
_ => "near-black, fluo-green accent",
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum State {
Blocked,
Working,
Done,
Idle,
Unknown,
}
impl State {
pub fn dot(self) -> &'static str {
match self {
State::Idle | State::Unknown => "○",
_ => "●",
}
}
pub fn color(self, t: &Theme) -> Color {
match self {
State::Blocked => t.coral,
State::Working => t.amber,
State::Done => t.mint,
State::Idle => t.green,
State::Unknown => t.overlay0,
}
}
pub fn label(self) -> &'static str {
match self {
State::Blocked => "blocked",
State::Working => "working",
State::Done => "done",
State::Idle => "idle",
State::Unknown => "—",
}
}
}
const FRAMES: [&str; 4] = ["⠆", "⠒", "⠰", "⠤"];
pub const SPINNER_FRAMES: u64 = FRAMES.len() as u64;
pub fn spinner_frame(n: u64) -> &'static str {
FRAMES[(n % SPINNER_FRAMES) as usize]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn state_glyphs_are_one_column() {
use unicode_width::UnicodeWidthStr;
let mut glyphs: Vec<&str> = [
State::Blocked,
State::Working,
State::Done,
State::Idle,
State::Unknown,
]
.iter()
.map(|s| s.dot())
.collect();
glyphs.extend((0..SPINNER_FRAMES).map(spinner_frame));
for g in glyphs {
assert_eq!(g.chars().count(), 1, "{g:?} must be a single glyph");
assert_eq!(g.width(), 1, "{g:?} must occupy one column");
}
let widths: std::collections::HashSet<usize> = (0..SPINNER_FRAMES)
.map(|i| spinner_frame(i).width_cjk())
.collect();
assert_eq!(
widths.len(),
1,
"spinner frames disagree on East Asian width, so the icon changes \
size mid-animation: {:?}",
(0..SPINNER_FRAMES)
.map(|i| (spinner_frame(i), spinner_frame(i).width_cjk()))
.collect::<Vec<_>>()
);
assert_eq!(
State::Idle.dot().width_cjk(),
State::Blocked.dot().width_cjk(),
"the idle and active dots must be the same width class"
);
let frames: std::collections::HashSet<&str> =
(0..SPINNER_FRAMES).map(spinner_frame).collect();
assert_eq!(
frames.len() as u64,
SPINNER_FRAMES,
"spinner frames must all differ"
);
assert_eq!(spinner_frame(0), spinner_frame(SPINNER_FRAMES));
}
#[test]
fn theme_registry_is_consistent() {
let mut swatches = std::collections::HashSet::new();
let mut swatches_256 = std::collections::HashSet::new();
for &name in THEMES {
assert!(!describe(name).is_empty(), "{name} needs a description");
let pal = by_name(name);
assert!(
swatches.insert(format!("{:?}{:?}", pal.base, pal.accent)),
"{name} should have a distinct background+accent swatch"
);
let px = crate::ipc::protocol::to_256(pal.base);
let pa = crate::ipc::protocol::to_256(pal.accent);
assert!(
swatches_256.insert(format!("{px:?}{pa:?}")),
"{name}'s swatch must stay distinct after 256-color downsampling"
);
}
assert!(THEMES.len() >= 15, "the new palettes are registered");
for (old, new) in [
("mocha", "catppuccin-mocha"),
("latte", "catppuccin-latte"),
("gruvboxlight", "gruvbox-light"),
] {
assert_eq!(canonical(old), new, "{old} should map to {new}");
assert_eq!(
format!("{:?}", by_name(old).accent),
format!("{:?}", by_name(new).accent),
"{old} must still resolve to the {new} palette"
);
}
}
}