use crate::error::{GwmError, Result};
use ratatui::style::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Theme {
pub focus: Color,
pub accent: Color,
pub branch: Color,
pub clean: Color,
pub dirty: Color,
pub main: Color,
pub locked: Color,
pub prunable: Color,
pub muted: Color,
pub selection_bg: Color,
pub name: Color,
pub path: Color,
pub staged: Color,
pub modified: Color,
pub untracked: Color,
}
impl Default for Theme {
fn default() -> Self {
Self {
focus: Color::Cyan,
accent: Color::Cyan,
branch: Color::Green,
clean: Color::Green,
dirty: Color::Yellow,
main: Color::Yellow,
locked: Color::Magenta,
prunable: Color::Red,
muted: Color::DarkGray,
selection_bg: Color::DarkGray,
name: Color::White,
path: Color::Gray,
staged: Color::Cyan,
modified: Color::Yellow,
untracked: Color::Green,
}
}
}
impl Theme {
pub fn preset(name: &str) -> Option<Self> {
match name {
"catppuccin" | "catppuccin-mocha" => Some(Self::catppuccin_mocha()),
"gruvbox" | "gruvbox-dark" => Some(Self::gruvbox_dark()),
"tokyo-night" | "tokyonight" => Some(Self::tokyo_night()),
"claude-dark" | "claude" => Some(Self::claude_dark()),
_ => None,
}
}
fn claude_dark() -> Self {
Self {
focus: Color::Rgb(0xC1, 0x5F, 0x3C), accent: Color::Rgb(0xD4, 0x82, 0x5D), branch: Color::Rgb(0x86, 0xE8, 0x9A), clean: Color::Rgb(0x86, 0xE8, 0x9A), dirty: Color::Rgb(0xFF, 0xDF, 0x61), main: Color::Rgb(0xFF, 0xDF, 0x61), locked: Color::Rgb(0xC7, 0x9B, 0xFF), prunable: Color::Rgb(0xFF, 0x7A, 0x7A), muted: Color::Rgb(0x99, 0x99, 0x99), selection_bg: Color::Rgb(0x38, 0x38, 0x38), name: Color::Rgb(0xE0, 0xE0, 0xE0), path: Color::Rgb(0xB0, 0xB0, 0xB0), staged: Color::Rgb(0xD4, 0x82, 0x5D), modified: Color::Rgb(0xFF, 0xDF, 0x61), untracked: Color::Rgb(0x86, 0xE8, 0x9A), }
}
fn catppuccin_mocha() -> Self {
Self {
focus: Color::Rgb(0x89, 0xb4, 0xfa), accent: Color::Rgb(0xcb, 0xa6, 0xf7), branch: Color::Rgb(0xa6, 0xe3, 0xa1), clean: Color::Rgb(0xa6, 0xe3, 0xa1), dirty: Color::Rgb(0xf9, 0xe2, 0xaf), main: Color::Rgb(0xf9, 0xe2, 0xaf), locked: Color::Rgb(0xcb, 0xa6, 0xf7), prunable: Color::Rgb(0xf3, 0x8b, 0xa8), muted: Color::Rgb(0x6c, 0x70, 0x86), selection_bg: Color::Rgb(0x31, 0x32, 0x44), name: Color::Rgb(0xcd, 0xd6, 0xf4), path: Color::Rgb(0xa6, 0xad, 0xc8), staged: Color::Rgb(0xcb, 0xa6, 0xf7), modified: Color::Rgb(0xf9, 0xe2, 0xaf), untracked: Color::Rgb(0xa6, 0xe3, 0xa1), }
}
fn gruvbox_dark() -> Self {
Self {
focus: Color::Rgb(0x83, 0xa5, 0x98), accent: Color::Rgb(0xfa, 0xbd, 0x2f), branch: Color::Rgb(0xb8, 0xbb, 0x26), clean: Color::Rgb(0xb8, 0xbb, 0x26), dirty: Color::Rgb(0xfa, 0xbd, 0x2f), main: Color::Rgb(0xfe, 0x80, 0x19), locked: Color::Rgb(0xd3, 0x86, 0x9b), prunable: Color::Rgb(0xfb, 0x49, 0x34), muted: Color::Rgb(0x92, 0x83, 0x74), selection_bg: Color::Rgb(0x3c, 0x38, 0x36), name: Color::Rgb(0xeb, 0xdb, 0xb2), path: Color::Rgb(0xa8, 0x99, 0x84), staged: Color::Rgb(0xfa, 0xbd, 0x2f), modified: Color::Rgb(0xfa, 0xbd, 0x2f), untracked: Color::Rgb(0xb8, 0xbb, 0x26), }
}
fn tokyo_night() -> Self {
Self {
focus: Color::Rgb(0x7a, 0xa2, 0xf7), accent: Color::Rgb(0xbb, 0x9a, 0xf7), branch: Color::Rgb(0x9e, 0xce, 0x6a), clean: Color::Rgb(0x9e, 0xce, 0x6a), dirty: Color::Rgb(0xe0, 0xaf, 0x68), main: Color::Rgb(0xe0, 0xaf, 0x68), locked: Color::Rgb(0xbb, 0x9a, 0xf7), prunable: Color::Rgb(0xf7, 0x76, 0x8e), muted: Color::Rgb(0x56, 0x5f, 0x89), selection_bg: Color::Rgb(0x33, 0x3a, 0x55), name: Color::Rgb(0xc0, 0xca, 0xf5), path: Color::Rgb(0x73, 0x7a, 0xa2), staged: Color::Rgb(0xbb, 0x9a, 0xf7), modified: Color::Rgb(0xe0, 0xaf, 0x68), untracked: Color::Rgb(0x9e, 0xce, 0x6a), }
}
pub fn apply_override(&mut self, role: &str, value: &str) -> Result<()> {
let color = parse_color(value).map_err(|e| {
let inner = match e {
GwmError::Config(msg) => msg,
other => other.to_string(),
};
GwmError::Config(format!("theme.{}: {}", role, inner))
})?;
let slot = self
.role_mut(role)
.ok_or_else(|| GwmError::Config(format!("theme: unknown role {:?}", role)))?;
*slot = color;
Ok(())
}
fn role_mut(&mut self, role: &str) -> Option<&mut Color> {
match role {
"focus" => Some(&mut self.focus),
"accent" => Some(&mut self.accent),
"branch" => Some(&mut self.branch),
"clean" => Some(&mut self.clean),
"dirty" => Some(&mut self.dirty),
"main" => Some(&mut self.main),
"locked" => Some(&mut self.locked),
"prunable" => Some(&mut self.prunable),
"muted" => Some(&mut self.muted),
"selection_bg" => Some(&mut self.selection_bg),
"name" => Some(&mut self.name),
"path" => Some(&mut self.path),
"staged" => Some(&mut self.staged),
"modified" => Some(&mut self.modified),
"untracked" => Some(&mut self.untracked),
_ => None,
}
}
}
pub fn preset_names() -> &'static [&'static str] {
&["catppuccin", "gruvbox", "tokyo-night", "claude-dark"]
}
pub fn parse_color(s: &str) -> Result<Color> {
let trimmed = s.trim();
if trimmed.is_empty() {
return Err(GwmError::Config("empty color string".into()));
}
if let Some(stripped) = trimmed.strip_prefix('#') {
if stripped.len() != 6 || !stripped.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(GwmError::Config(format!(
"invalid hex color {:?} (expected #RRGGBB)",
s
)));
}
let r = u8::from_str_radix(&stripped[0..2], 16).unwrap();
let g = u8::from_str_radix(&stripped[2..4], 16).unwrap();
let b = u8::from_str_radix(&stripped[4..6], 16).unwrap();
return Ok(Color::Rgb(r, g, b));
}
if trimmed.chars().all(|c| c.is_ascii_digit()) {
let n: u16 = trimmed
.parse()
.map_err(|_| GwmError::Config(format!("invalid indexed color {:?}", s)))?;
if n > 255 {
return Err(GwmError::Config(format!("indexed color {} out of range (0..=255)", n)));
}
return Ok(Color::Indexed(n as u8));
}
let lower = trimmed.to_ascii_lowercase();
match lower.as_str() {
"black" => Ok(Color::Black),
"red" => Ok(Color::Red),
"green" => Ok(Color::Green),
"yellow" => Ok(Color::Yellow),
"blue" => Ok(Color::Blue),
"magenta" => Ok(Color::Magenta),
"cyan" => Ok(Color::Cyan),
"gray" | "grey" => Ok(Color::Gray),
"dark_gray" | "darkgray" | "dark_grey" | "darkgrey" => Ok(Color::DarkGray),
"light_red" | "lightred" | "bright_red" | "brightred" => Ok(Color::LightRed),
"light_green" | "lightgreen" | "bright_green" | "brightgreen" => Ok(Color::LightGreen),
"light_yellow" | "lightyellow" | "bright_yellow" | "brightyellow" => Ok(Color::LightYellow),
"light_blue" | "lightblue" | "bright_blue" | "brightblue" => Ok(Color::LightBlue),
"light_magenta" | "lightmagenta" | "bright_magenta" | "brightmagenta" => Ok(Color::LightMagenta),
"light_cyan" | "lightcyan" | "bright_cyan" | "brightcyan" => Ok(Color::LightCyan),
"white" => Ok(Color::White),
"reset" => Ok(Color::Reset),
_ => Err(GwmError::Config(format!(
"unknown color name {:?} (try `cyan` / `#89b4fa` / `220`)",
s
))),
}
}