#![allow(dead_code)]
use std::collections::HashMap;
use std::sync::OnceLock;
use ratatui::style::{Color, Modifier, Style};
use terminal_colorsaurus::{QueryOptions, ThemeMode};
#[derive(Debug, Clone, Copy)]
pub struct Palette {
pub rosewater: Color,
pub flamingo: Color,
pub pink: Color,
pub mauve: Color,
pub red: Color,
pub maroon: Color,
pub peach: Color,
pub yellow: Color,
pub green: Color,
pub teal: Color,
pub sky: Color,
pub sapphire: Color,
pub blue: Color,
pub lavender: Color,
pub text: Color,
pub subtext1: Color,
pub subtext0: Color,
pub overlay1: Color,
pub overlay0: Color,
pub surface2: Color,
pub surface1: Color,
pub surface0: Color,
pub base: Color,
pub mantle: Color,
pub crust: Color,
}
const FIELDS: [&str; 25] = [
"rosewater",
"flamingo",
"pink",
"mauve",
"red",
"maroon",
"peach",
"yellow",
"green",
"teal",
"sky",
"sapphire",
"blue",
"lavender",
"text",
"subtext1",
"subtext0",
"overlay1",
"overlay0",
"surface2",
"surface1",
"surface0",
"base",
"mantle",
"crust",
];
impl Palette {
fn from_hexes(h: &[&str; 25]) -> Palette {
let c = |i: usize| parse_hex(h[i]).unwrap_or_else(|| panic!("bad built-in hex {}", h[i]));
Palette {
rosewater: c(0),
flamingo: c(1),
pink: c(2),
mauve: c(3),
red: c(4),
maroon: c(5),
peach: c(6),
yellow: c(7),
green: c(8),
teal: c(9),
sky: c(10),
sapphire: c(11),
blue: c(12),
lavender: c(13),
text: c(14),
subtext1: c(15),
subtext0: c(16),
overlay1: c(17),
overlay0: c(18),
surface2: c(19),
surface1: c(20),
surface0: c(21),
base: c(22),
mantle: c(23),
crust: c(24),
}
}
fn set(&mut self, key: &str, color: Color) -> bool {
match key {
"rosewater" => self.rosewater = color,
"flamingo" => self.flamingo = color,
"pink" => self.pink = color,
"mauve" => self.mauve = color,
"red" => self.red = color,
"maroon" => self.maroon = color,
"peach" => self.peach = color,
"yellow" => self.yellow = color,
"green" => self.green = color,
"teal" => self.teal = color,
"sky" => self.sky = color,
"sapphire" => self.sapphire = color,
"blue" => self.blue = color,
"lavender" => self.lavender = color,
"text" => self.text = color,
"subtext1" => self.subtext1 = color,
"subtext0" => self.subtext0 = color,
"overlay1" => self.overlay1 = color,
"overlay0" => self.overlay0 = color,
"surface2" => self.surface2 = color,
"surface1" => self.surface1 = color,
"surface0" => self.surface0 = color,
"base" => self.base = color,
"mantle" => self.mantle = color,
"crust" => self.crust = color,
_ => return false,
}
true
}
}
pub const BUILTIN_NAMES: &[&str] = &[
"catppuccin-mocha",
"catppuccin-latte",
"catppuccin-frappe",
"catppuccin-macchiato",
"gruvbox-dark",
"gruvbox-light",
"nord",
"dracula",
"solarized-dark",
"solarized-light",
"tokyo-night",
"one-dark",
"rose-pine",
"monokai",
];
pub fn builtin(name: &str) -> Option<Palette> {
let hex: [&str; 25] = match name {
"catppuccin-mocha" | "mocha" => [
"#f5e0dc", "#f2cdcd", "#f5c2e7", "#cba6f7", "#f38ba8", "#eba0ac", "#fab387", "#f9e2af",
"#a6e3a1", "#94e2d5", "#89dceb", "#74c7ec", "#89b4fa", "#b4befe", "#cdd6f4", "#bac2de",
"#a6adc8", "#7f849c", "#6c7086", "#585b70", "#45475a", "#313244", "#1e1e2e", "#181825",
"#11111b",
],
"catppuccin-latte" | "latte" => [
"#dc8a78", "#dd7878", "#ea76cb", "#8839ef", "#d20f39", "#e64553", "#fe640b", "#df8e1d",
"#40a02b", "#179299", "#04a5e5", "#209fb5", "#1e66f5", "#7287fd", "#4c4f69", "#5c5f77",
"#6c6f85", "#8c8fa1", "#9ca0b0", "#acb0be", "#bcc0cc", "#ccd0da", "#eff1f5", "#e6e9ef",
"#dce0e8",
],
"catppuccin-frappe" | "frappe" => [
"#f2d5cf", "#eebebe", "#f4b8e4", "#ca9ee6", "#e78284", "#ea999c", "#ef9f76", "#e5c890",
"#a6d189", "#81c8be", "#99d1db", "#85c1dc", "#8caaee", "#babbf1", "#c6d0f5", "#b5bfe2",
"#a5adce", "#949cbb", "#838ba7", "#626880", "#51576d", "#414559", "#303446", "#292c3c",
"#232634",
],
"catppuccin-macchiato" | "macchiato" => [
"#f4dbd6", "#f0c6c6", "#f5bde6", "#c6a0f6", "#ed8796", "#ee99a0", "#f5a97f", "#eed49f",
"#a6da95", "#8bd5ca", "#91d7e3", "#7dc4e4", "#8aadf4", "#b7bdf8", "#cad3f5", "#b8c0e0",
"#a5adcb", "#8087a2", "#6e738d", "#5b6078", "#494d64", "#363a4f", "#24273a", "#1e2030",
"#181926",
],
"gruvbox" | "gruvbox-dark" => [
"#ebdbb2", "#d5c4a1", "#d3869b", "#d3869b", "#fb4934", "#cc241d", "#fe8019", "#fabd2f",
"#b8bb26", "#8ec07c", "#83a598", "#458588", "#83a598", "#d3869b", "#ebdbb2", "#d5c4a1",
"#bdae93", "#a89984", "#928374", "#665c54", "#504945", "#3c3836", "#282828", "#1d2021",
"#1d2021",
],
"nord" => [
"#d8dee9", "#e5e9f0", "#b48ead", "#b48ead", "#bf616a", "#bf616a", "#d08770", "#ebcb8b",
"#a3be8c", "#8fbcbb", "#88c0d0", "#81a1c1", "#5e81ac", "#b48ead", "#eceff4", "#e5e9f0",
"#d8dee9", "#616e88", "#4c566a", "#434c5e", "#3b4252", "#333a47", "#2e3440", "#2b303b",
"#242933",
],
"dracula" => [
"#f8f8f2", "#ffb86c", "#ff79c6", "#bd93f9", "#ff5555", "#ff5555", "#ffb86c", "#f1fa8c",
"#50fa7b", "#8be9fd", "#8be9fd", "#62d6e8", "#6272a4", "#bd93f9", "#f8f8f2", "#d8d8d2",
"#b8b8b2", "#6272a4", "#565761", "#44475a", "#3a3c4e", "#343746", "#282a36", "#21222c",
"#191a21",
],
"gruvbox-light" => [
"#ebdbb2", "#d5c4a1", "#b16286", "#b16286", "#cc241d", "#9d0006", "#d65d0e", "#d79921",
"#98971a", "#689d6a", "#458588", "#076678", "#458588", "#b16286", "#282828", "#3c3836",
"#504945", "#665c54", "#7c6f64", "#928374", "#a89984", "#bdae93", "#fbf1c7", "#ebdbb2",
"#d5c4a1",
],
"solarized-dark" | "solarized" => [
"#d33682", "#d33682", "#d33682", "#6c71c4", "#dc322f", "#dc322f", "#cb4b16", "#b58900",
"#859900", "#2aa198", "#2aa198", "#268bd2", "#268bd2", "#6c71c4", "#93a1a1", "#839496",
"#657b83", "#586e75", "#586e75", "#586e75", "#073642", "#073642", "#002b36", "#002b36",
"#001e26",
],
"solarized-light" => [
"#d33682", "#d33682", "#d33682", "#6c71c4", "#dc322f", "#dc322f", "#cb4b16", "#b58900",
"#859900", "#2aa198", "#2aa198", "#268bd2", "#268bd2", "#6c71c4", "#002b36", "#073642",
"#586e75", "#657b83", "#839496", "#93a1a1", "#eee8d5", "#eee8d5", "#fdf6e3", "#eee8d5",
"#eee8d5",
],
"tokyo-night" | "tokyonight" => [
"#f7768e", "#f7768e", "#bb9af7", "#9d7cd8", "#f7768e", "#db4b4b", "#ff9e64", "#e0af68",
"#9ece6a", "#73daca", "#7dcfff", "#0db9d7", "#7aa2f7", "#9d7cd8", "#c0caf5", "#a9b1d6",
"#545c7e", "#545c7e", "#3b4261", "#3b4261", "#292e42", "#292e42", "#1a1b26", "#16161e",
"#13131a",
],
"one-dark" | "onedark" => [
"#e06c75", "#e06c75", "#c678dd", "#c678dd", "#e06c75", "#be5046", "#d19a66", "#e5c07b",
"#98c379", "#56b6c2", "#56b6c2", "#61afef", "#61afef", "#c678dd", "#abb2bf", "#828997",
"#5c6370", "#5c6370", "#4b5263", "#4b5263", "#3b4048", "#323842", "#282c34", "#21252b",
"#1b1d23",
],
"rose-pine" | "rosepine" => [
"#ebbcba", "#ebbcba", "#ebbcba", "#c4a7e7", "#eb6f92", "#eb6f92", "#f6c177", "#f6c177",
"#31748f", "#31748f", "#9ccfd8", "#9ccfd8", "#9ccfd8", "#c4a7e7", "#e0def4", "#908caa",
"#6e6a86", "#6e6a86", "#524f67", "#524f67", "#403d52", "#26233a", "#191724", "#191724",
"#14121d",
],
"monokai" => [
"#f92672", "#f92672", "#f92672", "#ae81ff", "#f92672", "#f92672", "#fd971f", "#e6db74",
"#a6e22e", "#66d9ef", "#66d9ef", "#66d9ef", "#66d9ef", "#ae81ff", "#f8f8f2", "#cfcfc2",
"#75715e", "#75715e", "#49483e", "#49483e", "#3e3d32", "#3e3d32", "#272822", "#23241f",
"#1e1f1c",
],
_ => return None,
};
Some(Palette::from_hexes(&hex))
}
fn catppuccin_mocha() -> Palette {
builtin("catppuccin-mocha").expect("mocha built-in")
}
fn catppuccin_latte() -> Palette {
builtin("catppuccin-latte").expect("latte built-in")
}
fn detect_terminal_mode() -> Option<ThemeMode> {
terminal_colorsaurus::theme_mode(QueryOptions::default()).ok()
}
pub fn resolve_skin(name: Option<&str>, colors: &HashMap<String, String>) -> Palette {
let mut p = match name {
None => match detect_terminal_mode() {
Some(ThemeMode::Light) => catppuccin_latte(),
_ => catppuccin_mocha(),
},
Some(n) => match builtin(&n.trim().to_ascii_lowercase()) {
Some(p) => p,
None => {
eprintln!(
"warning: unknown skin '{n}' (known: {}); using catppuccin-mocha",
BUILTIN_NAMES.join(", ")
);
catppuccin_mocha()
}
},
};
for (k, v) in colors {
let key = k.trim().to_ascii_lowercase();
match parse_hex(v) {
Some(c) if p.set(&key, c) => {}
Some(_) => eprintln!("warning: unknown skin color '{k}' (ignored)"),
None => eprintln!("warning: invalid hex '{v}' for skin color '{k}' (ignored)"),
}
}
p
}
fn parse_hex(s: &str) -> Option<Color> {
let s = s.trim().trim_start_matches('#');
if s.len() != 6 || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
let r = u8::from_str_radix(&s[0..2], 16).ok()?;
let g = u8::from_str_radix(&s[2..4], 16).ok()?;
let b = u8::from_str_radix(&s[4..6], 16).ok()?;
Some(Color::Rgb(r, g, b))
}
static ACTIVE: OnceLock<Palette> = OnceLock::new();
pub fn init(p: Palette) {
let _ = ACTIVE.set(p);
}
fn palette() -> &'static Palette {
ACTIVE.get_or_init(catppuccin_mocha)
}
pub fn rosewater() -> Color {
palette().rosewater
}
pub fn flamingo() -> Color {
palette().flamingo
}
pub fn pink() -> Color {
palette().pink
}
pub fn mauve() -> Color {
palette().mauve
}
pub fn red() -> Color {
palette().red
}
pub fn maroon() -> Color {
palette().maroon
}
pub fn peach() -> Color {
palette().peach
}
pub fn yellow() -> Color {
palette().yellow
}
pub fn green() -> Color {
palette().green
}
pub fn teal() -> Color {
palette().teal
}
pub fn sky() -> Color {
palette().sky
}
pub fn sapphire() -> Color {
palette().sapphire
}
pub fn blue() -> Color {
palette().blue
}
pub fn lavender() -> Color {
palette().lavender
}
pub fn text() -> Color {
palette().text
}
pub fn subtext1() -> Color {
palette().subtext1
}
pub fn subtext0() -> Color {
palette().subtext0
}
pub fn overlay1() -> Color {
palette().overlay1
}
pub fn overlay0() -> Color {
palette().overlay0
}
pub fn surface2() -> Color {
palette().surface2
}
pub fn surface1() -> Color {
palette().surface1
}
pub fn surface0() -> Color {
palette().surface0
}
pub fn base() -> Color {
palette().base
}
pub fn mantle() -> Color {
palette().mantle
}
pub fn crust() -> Color {
palette().crust
}
pub fn title() -> Style {
Style::default().fg(teal()).add_modifier(Modifier::BOLD)
}
pub fn border() -> Style {
Style::default().fg(mauve())
}
pub fn border_focused() -> Style {
Style::default().fg(lavender())
}
pub fn header_row() -> Style {
Style::default().fg(yellow())
}
pub fn selected_row() -> Style {
Style::default()
.bg(lavender())
.fg(base())
.add_modifier(Modifier::BOLD)
}
pub fn sorter() -> Color {
sky()
}
pub fn counter() -> Color {
yellow()
}
pub fn mark() -> Color {
rosewater()
}
pub fn dim() -> Style {
Style::default().fg(overlay1())
}
pub fn accent() -> Style {
Style::default().fg(teal())
}
pub fn status_color(s: &str) -> Color {
match s {
"Running" | "Ready" | "Active" | "Bound" | "True" => green(),
"Succeeded" | "Completed" => overlay0(),
"Pending" | "ContainerCreating" | "PodInitializing" | "Progressing" => yellow(),
"Terminating" => mauve(),
"Failed" | "Error" | "CrashLoopBackOff" | "ImagePullBackOff" | "ErrImagePull"
| "Evicted" | "OOMKilled" | "NotReady" | "False" => red(),
"Unknown" | "" => overlay1(),
_ => text(),
}
}
pub fn row_color(s: &str) -> Color {
match s {
"Failed" | "Error" | "CrashLoopBackOff" | "ImagePullBackOff" | "ErrImagePull"
| "Evicted" | "OOMKilled" | "NotReady" | "Unhealthy" | "False" => red(),
"Pending" | "ContainerCreating" | "PodInitializing" | "Progressing" => peach(),
"Completed" | "Succeeded" => overlay0(),
"Terminating" => mauve(),
_ => blue(),
}
}
pub fn restarts_severity(count: i64) -> Option<Color> {
if count >= 5 {
Some(red())
} else if count >= 1 {
Some(peach())
} else {
None
}
}
pub fn cpu_severity(millicores: i64) -> Option<Color> {
if millicores >= 1000 {
Some(red())
} else if millicores >= 200 {
Some(peach())
} else {
None
}
}
pub fn mem_severity(bytes: i64) -> Option<Color> {
let mi = bytes as f64 / (1024.0 * 1024.0);
if mi >= 1024.0 {
Some(red())
} else if mi >= 256.0 {
Some(peach())
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_builtins_parse() {
for name in BUILTIN_NAMES {
assert!(builtin(name).is_some(), "missing built-in {name}");
}
}
#[test]
fn parse_hex_forms() {
assert_eq!(parse_hex("#1e1e2e"), Some(Color::Rgb(30, 30, 46)));
assert_eq!(parse_hex("1e1e2e"), Some(Color::Rgb(30, 30, 46)));
assert_eq!(parse_hex("#fff"), None);
assert_eq!(parse_hex("#gggggg"), None);
}
#[test]
fn resolve_applies_overrides_and_ignores_junk() {
let mut colors = HashMap::new();
colors.insert("red".to_string(), "#010203".to_string());
colors.insert("nope".to_string(), "#040506".to_string()); colors.insert("green".to_string(), "zzzzzz".to_string()); let p = resolve_skin(Some("catppuccin-mocha"), &colors);
assert_eq!(p.red, Color::Rgb(1, 2, 3));
assert_eq!(p.green, builtin("catppuccin-mocha").unwrap().green); }
#[test]
fn row_color_matches_k9s_model() {
assert_eq!(row_color("Running"), blue());
assert_eq!(row_color("Ready"), blue());
assert_eq!(row_color(""), blue());
assert_eq!(row_color("CrashLoopBackOff"), red());
assert_eq!(row_color("Pending"), peach());
assert_eq!(row_color("Completed"), overlay0());
assert_eq!(row_color("Terminating"), mauve());
}
#[test]
fn status_color_fades_terminal_states_pops_active_ones() {
assert_eq!(status_color("Running"), green());
assert_eq!(status_color("Succeeded"), row_color("Succeeded"));
assert_eq!(status_color("Completed"), row_color("Completed"));
assert_eq!(status_color("Terminating"), row_color("Terminating"));
assert_eq!(status_color("CrashLoopBackOff"), red());
assert_eq!(
status_color("CrashLoopBackOff"),
row_color("CrashLoopBackOff")
);
assert_eq!(status_color("Pending"), yellow());
assert_ne!(status_color("Pending"), row_color("Pending"));
}
#[test]
fn resource_severity_thresholds() {
assert_eq!(restarts_severity(0), None);
assert_eq!(restarts_severity(1), Some(peach()));
assert_eq!(restarts_severity(5), Some(red()));
assert_eq!(cpu_severity(50), None);
assert_eq!(cpu_severity(200), Some(peach()));
assert_eq!(cpu_severity(1000), Some(red()));
assert_eq!(mem_severity(100 * 1024 * 1024), None); assert_eq!(mem_severity(300 * 1024 * 1024), Some(peach())); assert_eq!(mem_severity(2048 * 1024 * 1024), Some(red())); }
#[test]
fn unknown_skin_falls_back_to_mocha() {
let p = resolve_skin(Some("no-such-skin"), &HashMap::new());
assert_eq!(p.base, catppuccin_mocha().base);
}
#[test]
fn detect_terminal_mode_is_best_effort() {
let _ = detect_terminal_mode();
}
}