use crate::config::ThemeColors; use crate::theme::Theme; pub const RESET: &str = "\x1b[0m"; pub const BOLD: &str = "\x1b[1m"; pub const DIM: &str = "\x1b[2m"; pub const ITALIC: &str = "\x1b[3m"; pub const UNDERLINE: &str = "\x1b[4m"; pub const STRIKETHROUGH: &str = "\x1b[9m"; pub const RED: &str = "\x1b[31m"; pub const GREEN: &str = "\x1b[32m"; pub const YELLOW: &str = "\x1b[33m"; pub const BLUE: &str = "\x1b[34m"; pub const MAGENTA: &str = "\x1b[35m"; pub const CYAN: &str = "\x1b[36m"; pub const WHITE: &str = "\x1b[37m"; // Named background colors const BG_RED: &str = "\x1b[41m"; const BG_GREEN: &str = "\x1b[42m"; const BG_YELLOW: &str = "\x1b[43m"; const BG_BLUE: &str = "\x1b[44m"; const BG_MAGENTA: &str = "\x1b[45m"; const BG_CYAN: &str = "\x1b[46m"; const BG_WHITE: &str = "\x1b[47m"; /// Parse a hex color string (#RRGGBB or #RGB) into (R, G, B). pub fn parse_hex(s: &str) -> Option<(u8, u8, u8)> { let hex = s.strip_prefix('#')?; match hex.len() { 6 => { let r = u8::from_str_radix(&hex[0..2], 16).ok()?; let g = u8::from_str_radix(&hex[2..4], 16).ok()?; let b = u8::from_str_radix(&hex[4..6], 16).ok()?; Some((r, g, b)) } 3 => { let r = u8::from_str_radix(&hex[0..1], 16).ok()? * 17; let g = u8::from_str_radix(&hex[1..2], 16).ok()? * 17; let b = u8::from_str_radix(&hex[2..3], 16).ok()? * 17; Some((r, g, b)) } _ => None, } } /// Emit a 24-bit foreground ANSI escape for an (R, G, B) tuple. pub fn fg_rgb(r: u8, g: u8, b: u8) -> String { format!("\x1b[38;2;{r};{g};{b}m") } /// Emit a 24-bit background ANSI escape for an (R, G, B) tuple. pub fn bg_rgb(r: u8, g: u8, b: u8) -> String { format!("\x1b[48;2;{r};{g};{b}m") } /// Interpolate a gradient between two hex colors at position `t` (0.0..=1.0). /// Returns a 24-bit foreground ANSI escape. pub fn gradient_fg(from_hex: &str, to_hex: &str, t: f32) -> String { use colorgrad::Gradient; let grad = colorgrad::GradientBuilder::new() .html_colors(&[from_hex, to_hex]) .build::() .unwrap_or_else(|_| { colorgrad::GradientBuilder::new() .html_colors(&["#00ff00", "#ff0000"]) .build::() .expect("fallback gradient must build") }); let c = grad.at(t.clamp(0.0, 1.0)); let [r, g, b, _] = c.to_rgba8(); fg_rgb(r, g, b) } /// Build a multi-stop gradient from hex color strings (e.g., ["#50fa7b", "#f1fa8c", "#ff5555"]). pub fn make_gradient(colors: &[&str]) -> colorgrad::LinearGradient { colorgrad::GradientBuilder::new() .html_colors(colors) .build::() .unwrap_or_else(|_| { colorgrad::GradientBuilder::new() .html_colors(&["#00ff00", "#ffff00", "#ff0000"]) .build::() .expect("fallback gradient must build") }) } /// Sample a gradient at position `t` and return a 24-bit foreground ANSI escape. pub fn sample_fg(grad: &colorgrad::LinearGradient, t: f32) -> String { use colorgrad::Gradient; let c = grad.at(t.clamp(0.0, 1.0)); let [r, g, b, _] = c.to_rgba8(); fg_rgb(r, g, b) } /// Resolve a color name to ANSI escape sequence(s). /// /// Supported formats (space-separated, combinable): /// - Named: red, green, yellow, blue, magenta, cyan, white /// - Modifiers: dim, bold, italic, underline, strikethrough /// - Hex: #FF6B35, #F00 /// - Background: bg:red, bg:#FF6B35 /// - Palette: p:success (resolved through theme palette) pub fn resolve_color(name: &str, theme: Theme, palette: &ThemeColors) -> String { if let Some(key) = name.strip_prefix("p:") { let map = match theme { Theme::Dark => &palette.dark, Theme::Light => &palette.light, }; if let Some(resolved) = map.get(key) { return resolve_color(resolved, theme, palette); } return RESET.to_string(); } let mut result = String::new(); for part in name.split_whitespace() { let resolved = match part { // Named foreground colors "red" => RED.to_string(), "green" => GREEN.to_string(), "yellow" => YELLOW.to_string(), "blue" => BLUE.to_string(), "magenta" => MAGENTA.to_string(), "cyan" => CYAN.to_string(), "white" => WHITE.to_string(), // Modifiers "dim" => DIM.to_string(), "bold" => BOLD.to_string(), "italic" => ITALIC.to_string(), "underline" => UNDERLINE.to_string(), "strikethrough" => STRIKETHROUGH.to_string(), // Hex foreground s if s.starts_with('#') => { if let Some((r, g, b)) = parse_hex(s) { fg_rgb(r, g, b) } else { String::new() } } // Background colors s if s.starts_with("bg:") => { let bg_val = &s[3..]; match bg_val { "red" => BG_RED.to_string(), "green" => BG_GREEN.to_string(), "yellow" => BG_YELLOW.to_string(), "blue" => BG_BLUE.to_string(), "magenta" => BG_MAGENTA.to_string(), "cyan" => BG_CYAN.to_string(), "white" => BG_WHITE.to_string(), hex if hex.starts_with('#') => { if let Some((r, g, b)) = parse_hex(hex) { bg_rgb(r, g, b) } else { String::new() } } _ => String::new(), } } _ => String::new(), }; result.push_str(&resolved); } if result.is_empty() { RESET.to_string() } else { result } } /// Determine whether color output should be used. /// Precedence: NO_COLOR > --color= CLI flag > CLAUDE_STATUSLINE_COLOR env > config pub fn should_use_color(cli_color: Option<&str>, config_color: &crate::config::ColorMode) -> bool { if std::env::var("NO_COLOR").is_ok() { return false; } if let Some(flag) = cli_color { return match flag { "always" => true, "never" => false, _ => atty_stdout(), }; } if let Ok(env_color) = std::env::var("CLAUDE_STATUSLINE_COLOR") { return match env_color.as_str() { "always" => true, "never" => false, _ => atty_stdout(), }; } match config_color { crate::config::ColorMode::Always => true, crate::config::ColorMode::Never => false, crate::config::ColorMode::Auto => { atty_stdout() && std::env::var("TERM").map_or(true, |t| t != "dumb") } } } fn atty_stdout() -> bool { unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 } }