claw-code/rust/crates/rusty-claude-cli/src/theme.rs

679 lines
26 KiB
Rust

//! Theme system for TUI — 11 built-in themes + custom JSON loading.
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TuiTheme {
pub name: String,
// Conversation pane
pub conversation_text: ColorDef,
pub conversation_user: ColorDef,
pub conversation_system: ColorDef,
pub conversation_error: ColorDef,
pub conversation_assistant: ColorDef,
pub conversation_dim: ColorDef,
// Code blocks
pub code_fg: ColorDef,
pub code_bg: ColorDef,
pub code_border: ColorDef,
pub code_language_label: ColorDef,
// Dashboard
pub dashboard_header: ColorDef,
pub dashboard_key: ColorDef,
pub dashboard_value: ColorDef,
pub dashboard_separator: ColorDef,
pub dashboard_keys_hint: ColorDef,
// Context gauge
pub gauge_fill_green: ColorDef,
pub gauge_fill_yellow: ColorDef,
pub gauge_fill_red: ColorDef,
pub gauge_bg: ColorDef,
pub gauge_label: ColorDef,
// Input area
pub input_fg: ColorDef,
pub input_cursor_fg: ColorDef,
pub input_cursor_bg: ColorDef,
pub input_border: ColorDef,
pub input_border_active: ColorDef,
// UI chrome
pub border: ColorDef,
pub border_active: ColorDef,
pub scrollbar: ColorDef,
pub spinner: ColorDef,
pub status_message: ColorDef,
pub key_hint: ColorDef,
// Completions
pub completion_fg: ColorDef,
pub completion_bg: ColorDef,
pub completion_selected_fg: ColorDef,
pub completion_selected_bg: ColorDef,
// Agent View
pub agent_running: ColorDef,
pub agent_waiting: ColorDef,
pub agent_done: ColorDef,
pub agent_failed: ColorDef,
pub agent_cancelled: ColorDef,
// Syntax highlighting
pub syntax_theme: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum ColorDef {
Named(String),
Ansi256(u8),
Rgb { r: u8, g: u8, b: u8 },
}
impl ColorDef {
pub fn to_color(&self) -> Color {
match self {
ColorDef::Named(name) => match name.to_lowercase().as_str() {
"black" => Color::Black,
"red" => Color::Red,
"green" => Color::Green,
"yellow" => Color::Yellow,
"blue" => Color::Blue,
"magenta" | "purple" => Color::Magenta,
"cyan" => Color::Cyan,
"white" => Color::White,
"gray" | "grey" | "darkgray" => Color::DarkGray,
"lightgray" | "lightgrey" => Color::Gray,
_ => Color::White,
},
ColorDef::Ansi256(n) => Color::Indexed(*n),
ColorDef::Rgb { r, g, b } => Color::Rgb(*r, *g, *b),
}
}
}
fn named(s: &str) -> ColorDef {
ColorDef::Named(s.to_string())
}
fn rgb(r: u8, g: u8, b: u8) -> ColorDef {
ColorDef::Rgb { r, g, b }
}
impl TuiTheme {
pub fn builtin(name: &str) -> Option<Self> {
match name {
"default" => Some(Self::theme_default()),
"tokyonight" => Some(Self::theme_tokyonight()),
"catppuccin-mocha" => Some(Self::theme_catppuccin_mocha()),
"catppuccin-latte" => Some(Self::theme_catppuccin_latte()),
"nord" => Some(Self::theme_nord()),
"gruvbox" => Some(Self::theme_gruvbox()),
"dracula" => Some(Self::theme_dracula()),
"solarized-dark" => Some(Self::theme_solarized_dark()),
"solarized-light" => Some(Self::theme_solarized_light()),
"monokai" => Some(Self::theme_monokai()),
"system" => Some(Self::theme_system()),
_ => None,
}
}
pub fn all_builtin_names() -> Vec<&'static str> {
vec![
"default",
"tokyonight",
"catppuccin-mocha",
"catppuccin-latte",
"nord",
"gruvbox",
"dracula",
"solarized-dark",
"solarized-light",
"monokai",
"system",
]
}
pub fn from_json(json: &str) -> Result<Self, String> {
serde_json::from_str(json).map_err(|e| e.to_string())
}
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_default()
}
// -----------------------------------------------------------------------
// Built-in themes
// -----------------------------------------------------------------------
fn theme_default() -> Self {
Self {
name: "default".into(),
conversation_text: named("white"),
conversation_user: named("cyan"),
conversation_system: named("yellow"),
conversation_error: named("red"),
conversation_assistant: named("white"),
conversation_dim: named("darkgray"),
code_fg: named("white"),
code_bg: named("black"),
code_border: named("darkgray"),
code_language_label: named("darkgray"),
dashboard_header: named("cyan"),
dashboard_key: named("darkgray"),
dashboard_value: named("white"),
dashboard_separator: named("darkgray"),
dashboard_keys_hint: named("darkgray"),
gauge_fill_green: named("green"),
gauge_fill_yellow: named("yellow"),
gauge_fill_red: named("red"),
gauge_bg: named("darkgray"),
gauge_label: named("white"),
input_fg: named("white"),
input_cursor_fg: named("black"),
input_cursor_bg: named("cyan"),
input_border: named("cyan"),
input_border_active: named("cyan"),
border: named("darkgray"),
border_active: named("cyan"),
scrollbar: named("darkgray"),
spinner: named("blue"),
status_message: named("blue"),
key_hint: named("darkgray"),
completion_fg: named("white"),
completion_bg: rgb(40, 40, 40),
completion_selected_fg: named("black"),
completion_selected_bg: named("cyan"),
agent_running: named("cyan"),
agent_waiting: named("yellow"),
agent_done: named("green"),
agent_failed: named("red"),
agent_cancelled: named("darkgray"),
syntax_theme: "base16-ocean.dark".into(),
}
}
fn theme_tokyonight() -> Self {
Self {
name: "tokyonight".into(),
conversation_text: rgb(192, 202, 245),
conversation_user: rgb(125, 207, 255),
conversation_system: rgb(224, 175, 104),
conversation_error: rgb(247, 118, 142),
conversation_assistant: rgb(192, 202, 245),
conversation_dim: rgb(86, 95, 137),
code_fg: rgb(192, 202, 245),
code_bg: rgb(26, 27, 48),
code_border: rgb(86, 95, 137),
code_language_label: rgb(86, 95, 137),
dashboard_header: rgb(125, 207, 255),
dashboard_key: rgb(86, 95, 137),
dashboard_value: rgb(192, 202, 245),
dashboard_separator: rgb(59, 66, 97),
dashboard_keys_hint: rgb(86, 95, 137),
gauge_fill_green: rgb(158, 206, 106),
gauge_fill_yellow: rgb(224, 175, 104),
gauge_fill_red: rgb(247, 118, 142),
gauge_bg: rgb(59, 66, 97),
gauge_label: rgb(192, 202, 245),
input_fg: rgb(192, 202, 245),
input_cursor_fg: rgb(26, 27, 48),
input_cursor_bg: rgb(125, 207, 255),
input_border: rgb(59, 66, 97),
input_border_active: rgb(125, 207, 255),
border: rgb(59, 66, 97),
border_active: rgb(125, 207, 255),
scrollbar: rgb(59, 66, 97),
spinner: rgb(125, 207, 255),
status_message: rgb(125, 207, 255),
key_hint: rgb(86, 95, 137),
completion_fg: rgb(192, 202, 245),
completion_bg: rgb(35, 40, 65),
completion_selected_fg: rgb(26, 27, 48),
completion_selected_bg: rgb(125, 207, 255),
agent_running: rgb(125, 207, 255),
agent_waiting: rgb(224, 175, 104),
agent_done: rgb(158, 206, 106),
agent_failed: rgb(247, 118, 142),
agent_cancelled: rgb(86, 95, 137),
syntax_theme: "base16-ocean.dark".into(),
}
}
fn theme_catppuccin_mocha() -> Self {
Self {
name: "catppuccin-mocha".into(),
conversation_text: rgb(205, 214, 244),
conversation_user: rgb(137, 180, 250),
conversation_system: rgb(249, 226, 175),
conversation_error: rgb(243, 139, 168),
conversation_assistant: rgb(205, 214, 244),
conversation_dim: rgb(108, 112, 134),
code_fg: rgb(205, 214, 244),
code_bg: rgb(30, 30, 46),
code_border: rgb(108, 112, 134),
code_language_label: rgb(108, 112, 134),
dashboard_header: rgb(137, 180, 250),
dashboard_key: rgb(108, 112, 134),
dashboard_value: rgb(205, 214, 244),
dashboard_separator: rgb(69, 71, 90),
dashboard_keys_hint: rgb(108, 112, 134),
gauge_fill_green: rgb(166, 227, 161),
gauge_fill_yellow: rgb(249, 226, 175),
gauge_fill_red: rgb(243, 139, 168),
gauge_bg: rgb(69, 71, 90),
gauge_label: rgb(205, 214, 244),
input_fg: rgb(205, 214, 244),
input_cursor_fg: rgb(30, 30, 46),
input_cursor_bg: rgb(137, 180, 250),
input_border: rgb(69, 71, 90),
input_border_active: rgb(137, 180, 250),
border: rgb(69, 71, 90),
border_active: rgb(137, 180, 250),
scrollbar: rgb(69, 71, 90),
spinner: rgb(137, 180, 250),
status_message: rgb(137, 180, 250),
key_hint: rgb(108, 112, 134),
completion_fg: rgb(205, 214, 244),
completion_bg: rgb(49, 50, 68),
completion_selected_fg: rgb(30, 30, 46),
completion_selected_bg: rgb(137, 180, 250),
agent_running: rgb(137, 180, 250),
agent_waiting: rgb(249, 226, 175),
agent_done: rgb(166, 227, 161),
agent_failed: rgb(243, 139, 168),
agent_cancelled: rgb(108, 112, 134),
syntax_theme: "base16-ocean.dark".into(),
}
}
fn theme_catppuccin_latte() -> Self {
Self {
name: "catppuccin-latte".into(),
conversation_text: rgb(76, 79, 105),
conversation_user: rgb(30, 102, 245),
conversation_system: rgb(223, 142, 29),
conversation_error: rgb(210, 15, 57),
conversation_assistant: rgb(76, 79, 105),
conversation_dim: rgb(156, 160, 176),
code_fg: rgb(76, 79, 105),
code_bg: rgb(239, 241, 245),
code_border: rgb(156, 160, 176),
code_language_label: rgb(156, 160, 176),
dashboard_header: rgb(30, 102, 245),
dashboard_key: rgb(156, 160, 176),
dashboard_value: rgb(76, 79, 105),
dashboard_separator: rgb(204, 208, 218),
dashboard_keys_hint: rgb(156, 160, 176),
gauge_fill_green: rgb(64, 160, 43),
gauge_fill_yellow: rgb(223, 142, 29),
gauge_fill_red: rgb(210, 15, 57),
gauge_bg: rgb(204, 208, 218),
gauge_label: rgb(76, 79, 105),
input_fg: rgb(76, 79, 105),
input_cursor_fg: rgb(239, 241, 245),
input_cursor_bg: rgb(30, 102, 245),
input_border: rgb(204, 208, 218),
input_border_active: rgb(30, 102, 245),
border: rgb(204, 208, 218),
border_active: rgb(30, 102, 245),
scrollbar: rgb(204, 208, 218),
spinner: rgb(30, 102, 245),
status_message: rgb(30, 102, 245),
key_hint: rgb(156, 160, 176),
completion_fg: rgb(76, 79, 105),
completion_bg: rgb(220, 224, 232),
completion_selected_fg: rgb(239, 241, 245),
completion_selected_bg: rgb(30, 102, 245),
agent_running: rgb(30, 102, 245),
agent_waiting: rgb(223, 142, 29),
agent_done: rgb(64, 160, 43),
agent_failed: rgb(210, 15, 57),
agent_cancelled: rgb(156, 160, 176),
syntax_theme: "InspiredGitHub".into(),
}
}
fn theme_nord() -> Self {
Self {
name: "nord".into(),
conversation_text: rgb(216, 222, 233),
conversation_user: rgb(136, 192, 208),
conversation_system: rgb(235, 203, 139),
conversation_error: rgb(191, 97, 106),
conversation_assistant: rgb(216, 222, 233),
conversation_dim: rgb(76, 86, 106),
code_fg: rgb(216, 222, 233),
code_bg: rgb(46, 52, 64),
code_border: rgb(76, 86, 106),
code_language_label: rgb(76, 86, 106),
dashboard_header: rgb(136, 192, 208),
dashboard_key: rgb(76, 86, 106),
dashboard_value: rgb(216, 222, 233),
dashboard_separator: rgb(59, 66, 81),
dashboard_keys_hint: rgb(76, 86, 106),
gauge_fill_green: rgb(163, 190, 140),
gauge_fill_yellow: rgb(235, 203, 139),
gauge_fill_red: rgb(191, 97, 106),
gauge_bg: rgb(59, 66, 81),
gauge_label: rgb(216, 222, 233),
input_fg: rgb(216, 222, 233),
input_cursor_fg: rgb(46, 52, 64),
input_cursor_bg: rgb(136, 192, 208),
input_border: rgb(59, 66, 81),
input_border_active: rgb(136, 192, 208),
border: rgb(59, 66, 81),
border_active: rgb(136, 192, 208),
scrollbar: rgb(59, 66, 81),
spinner: rgb(136, 192, 208),
status_message: rgb(136, 192, 208),
key_hint: rgb(76, 86, 106),
completion_fg: rgb(216, 222, 233),
completion_bg: rgb(67, 76, 94),
completion_selected_fg: rgb(46, 52, 64),
completion_selected_bg: rgb(136, 192, 208),
agent_running: rgb(136, 192, 208),
agent_waiting: rgb(235, 203, 139),
agent_done: rgb(163, 190, 140),
agent_failed: rgb(191, 97, 106),
agent_cancelled: rgb(76, 86, 106),
syntax_theme: "base16-ocean.dark".into(),
}
}
fn theme_gruvbox() -> Self {
Self {
name: "gruvbox".into(),
conversation_text: rgb(235, 219, 178),
conversation_user: rgb(131, 165, 152),
conversation_system: rgb(250, 189, 47),
conversation_error: rgb(251, 73, 52),
conversation_assistant: rgb(235, 219, 178),
conversation_dim: rgb(146, 131, 116),
code_fg: rgb(235, 219, 178),
code_bg: rgb(40, 40, 40),
code_border: rgb(146, 131, 116),
code_language_label: rgb(146, 131, 116),
dashboard_header: rgb(131, 165, 152),
dashboard_key: rgb(146, 131, 116),
dashboard_value: rgb(235, 219, 178),
dashboard_separator: rgb(80, 73, 69),
dashboard_keys_hint: rgb(146, 131, 116),
gauge_fill_green: rgb(184, 187, 38),
gauge_fill_yellow: rgb(250, 189, 47),
gauge_fill_red: rgb(251, 73, 52),
gauge_bg: rgb(80, 73, 69),
gauge_label: rgb(235, 219, 178),
input_fg: rgb(235, 219, 178),
input_cursor_fg: rgb(40, 40, 40),
input_cursor_bg: rgb(131, 165, 152),
input_border: rgb(80, 73, 69),
input_border_active: rgb(131, 165, 152),
border: rgb(80, 73, 69),
border_active: rgb(131, 165, 152),
scrollbar: rgb(80, 73, 69),
spinner: rgb(131, 165, 152),
status_message: rgb(131, 165, 152),
key_hint: rgb(146, 131, 116),
completion_fg: rgb(235, 219, 178),
completion_bg: rgb(60, 56, 54),
completion_selected_fg: rgb(40, 40, 40),
completion_selected_bg: rgb(131, 165, 152),
agent_running: rgb(131, 165, 152),
agent_waiting: rgb(250, 189, 47),
agent_done: rgb(184, 187, 38),
agent_failed: rgb(251, 73, 52),
agent_cancelled: rgb(146, 131, 116),
syntax_theme: "base16-ocean.dark".into(),
}
}
fn theme_dracula() -> Self {
Self {
name: "dracula".into(),
conversation_text: rgb(248, 248, 242),
conversation_user: rgb(139, 233, 253),
conversation_system: rgb(241, 250, 140),
conversation_error: rgb(255, 85, 85),
conversation_assistant: rgb(248, 248, 242),
conversation_dim: rgb(98, 114, 164),
code_fg: rgb(248, 248, 242),
code_bg: rgb(40, 42, 54),
code_border: rgb(98, 114, 164),
code_language_label: rgb(98, 114, 164),
dashboard_header: rgb(139, 233, 253),
dashboard_key: rgb(98, 114, 164),
dashboard_value: rgb(248, 248, 242),
dashboard_separator: rgb(68, 71, 90),
dashboard_keys_hint: rgb(98, 114, 164),
gauge_fill_green: rgb(80, 250, 123),
gauge_fill_yellow: rgb(241, 250, 140),
gauge_fill_red: rgb(255, 85, 85),
gauge_bg: rgb(68, 71, 90),
gauge_label: rgb(248, 248, 242),
input_fg: rgb(248, 248, 242),
input_cursor_fg: rgb(40, 42, 54),
input_cursor_bg: rgb(139, 233, 253),
input_border: rgb(68, 71, 90),
input_border_active: rgb(139, 233, 253),
border: rgb(68, 71, 90),
border_active: rgb(139, 233, 253),
scrollbar: rgb(68, 71, 90),
spinner: rgb(139, 233, 253),
status_message: rgb(139, 233, 253),
key_hint: rgb(98, 114, 164),
completion_fg: rgb(248, 248, 242),
completion_bg: rgb(55, 57, 72),
completion_selected_fg: rgb(40, 42, 54),
completion_selected_bg: rgb(139, 233, 253),
agent_running: rgb(139, 233, 253),
agent_waiting: rgb(241, 250, 140),
agent_done: rgb(80, 250, 123),
agent_failed: rgb(255, 85, 85),
agent_cancelled: rgb(98, 114, 164),
syntax_theme: "base16-ocean.dark".into(),
}
}
fn theme_solarized_dark() -> Self {
Self {
name: "solarized-dark".into(),
conversation_text: rgb(147, 161, 161),
conversation_user: rgb(42, 161, 152),
conversation_system: rgb(181, 137, 0),
conversation_error: rgb(220, 50, 47),
conversation_assistant: rgb(147, 161, 161),
conversation_dim: rgb(88, 110, 117),
code_fg: rgb(147, 161, 161),
code_bg: rgb(0, 43, 54),
code_border: rgb(88, 110, 117),
code_language_label: rgb(88, 110, 117),
dashboard_header: rgb(42, 161, 152),
dashboard_key: rgb(88, 110, 117),
dashboard_value: rgb(147, 161, 161),
dashboard_separator: rgb(7, 54, 66),
dashboard_keys_hint: rgb(88, 110, 117),
gauge_fill_green: rgb(133, 153, 0),
gauge_fill_yellow: rgb(181, 137, 0),
gauge_fill_red: rgb(220, 50, 47),
gauge_bg: rgb(7, 54, 66),
gauge_label: rgb(147, 161, 161),
input_fg: rgb(147, 161, 161),
input_cursor_fg: rgb(0, 43, 54),
input_cursor_bg: rgb(42, 161, 152),
input_border: rgb(7, 54, 66),
input_border_active: rgb(42, 161, 152),
border: rgb(7, 54, 66),
border_active: rgb(42, 161, 152),
scrollbar: rgb(7, 54, 66),
spinner: rgb(42, 161, 152),
status_message: rgb(42, 161, 152),
key_hint: rgb(88, 110, 117),
completion_fg: rgb(147, 161, 161),
completion_bg: rgb(14, 63, 75),
completion_selected_fg: rgb(0, 43, 54),
completion_selected_bg: rgb(42, 161, 152),
agent_running: rgb(42, 161, 152),
agent_waiting: rgb(181, 137, 0),
agent_done: rgb(133, 153, 0),
agent_failed: rgb(220, 50, 47),
agent_cancelled: rgb(88, 110, 117),
syntax_theme: "base16-ocean.dark".into(),
}
}
fn theme_solarized_light() -> Self {
Self {
name: "solarized-light".into(),
conversation_text: rgb(101, 123, 131),
conversation_user: rgb(42, 161, 152),
conversation_system: rgb(181, 137, 0),
conversation_error: rgb(220, 50, 47),
conversation_assistant: rgb(101, 123, 131),
conversation_dim: rgb(147, 161, 161),
code_fg: rgb(101, 123, 131),
code_bg: rgb(253, 246, 227),
code_border: rgb(147, 161, 161),
code_language_label: rgb(147, 161, 161),
dashboard_header: rgb(42, 161, 152),
dashboard_key: rgb(147, 161, 161),
dashboard_value: rgb(101, 123, 131),
dashboard_separator: rgb(238, 232, 213),
dashboard_keys_hint: rgb(147, 161, 161),
gauge_fill_green: rgb(133, 153, 0),
gauge_fill_yellow: rgb(181, 137, 0),
gauge_fill_red: rgb(220, 50, 47),
gauge_bg: rgb(238, 232, 213),
gauge_label: rgb(101, 123, 131),
input_fg: rgb(101, 123, 131),
input_cursor_fg: rgb(253, 246, 227),
input_cursor_bg: rgb(42, 161, 152),
input_border: rgb(238, 232, 213),
input_border_active: rgb(42, 161, 152),
border: rgb(238, 232, 213),
border_active: rgb(42, 161, 152),
scrollbar: rgb(238, 232, 213),
spinner: rgb(42, 161, 152),
status_message: rgb(42, 161, 152),
key_hint: rgb(147, 161, 161),
completion_fg: rgb(101, 123, 131),
completion_bg: rgb(238, 232, 213),
completion_selected_fg: rgb(253, 246, 227),
completion_selected_bg: rgb(42, 161, 152),
agent_running: rgb(42, 161, 152),
agent_waiting: rgb(181, 137, 0),
agent_done: rgb(133, 153, 0),
agent_failed: rgb(220, 50, 47),
agent_cancelled: rgb(147, 161, 161),
syntax_theme: "InspiredGitHub".into(),
}
}
fn theme_monokai() -> Self {
Self {
name: "monokai".into(),
conversation_text: rgb(248, 248, 242),
conversation_user: rgb(166, 226, 46),
conversation_system: rgb(230, 219, 116),
conversation_error: rgb(249, 38, 114),
conversation_assistant: rgb(248, 248, 242),
conversation_dim: rgb(117, 113, 94),
code_fg: rgb(248, 248, 242),
code_bg: rgb(39, 40, 34),
code_border: rgb(117, 113, 94),
code_language_label: rgb(117, 113, 94),
dashboard_header: rgb(166, 226, 46),
dashboard_key: rgb(117, 113, 94),
dashboard_value: rgb(248, 248, 242),
dashboard_separator: rgb(75, 73, 62),
dashboard_keys_hint: rgb(117, 113, 94),
gauge_fill_green: rgb(166, 226, 46),
gauge_fill_yellow: rgb(230, 219, 116),
gauge_fill_red: rgb(249, 38, 114),
gauge_bg: rgb(75, 73, 62),
gauge_label: rgb(248, 248, 242),
input_fg: rgb(248, 248, 242),
input_cursor_fg: rgb(39, 40, 34),
input_cursor_bg: rgb(166, 226, 46),
input_border: rgb(75, 73, 62),
input_border_active: rgb(166, 226, 46),
border: rgb(75, 73, 62),
border_active: rgb(166, 226, 46),
scrollbar: rgb(75, 73, 62),
spinner: rgb(166, 226, 46),
status_message: rgb(166, 226, 46),
key_hint: rgb(117, 113, 94),
completion_fg: rgb(248, 248, 242),
completion_bg: rgb(55, 54, 46),
completion_selected_fg: rgb(39, 40, 34),
completion_selected_bg: rgb(166, 226, 46),
agent_running: rgb(166, 226, 46),
agent_waiting: rgb(230, 219, 116),
agent_done: rgb(166, 226, 46),
agent_failed: rgb(249, 38, 114),
agent_cancelled: rgb(117, 113, 94),
syntax_theme: "base16-monokai.dark".into(),
}
}
fn theme_system() -> Self {
let mut theme = Self::theme_default();
theme.name = "system".into();
theme.syntax_theme = "InspiredGitHub".into();
theme
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_all_builtins_exist() {
for name in TuiTheme::all_builtin_names() {
assert!(TuiTheme::builtin(name).is_some(), "missing theme: {name}");
}
}
#[test]
fn test_color_def_named() {
assert_eq!(ColorDef::Named("red".into()).to_color(), Color::Red);
assert_eq!(ColorDef::Named("cyan".into()).to_color(), Color::Cyan);
}
#[test]
fn test_color_def_rgb() {
assert_eq!(
ColorDef::Rgb { r: 255, g: 0, b: 0 }.to_color(),
Color::Rgb(255, 0, 0)
);
}
#[test]
fn test_color_def_ansi256() {
assert_eq!(ColorDef::Ansi256(196).to_color(), Color::Indexed(196));
}
#[test]
fn test_from_json_valid() {
let theme = TuiTheme::builtin("default").unwrap();
let json = theme.to_json();
let parsed = TuiTheme::from_json(&json);
assert!(parsed.is_ok());
assert_eq!(parsed.unwrap().name, "default");
}
#[test]
fn test_from_json_invalid() {
assert!(TuiTheme::from_json("not json").is_err());
}
#[test]
fn test_unknown_theme_returns_none() {
assert!(TuiTheme::builtin("nonexistent").is_none());
}
#[test]
fn test_system_theme_uses_ansi_only() {
let theme = TuiTheme::builtin("system").unwrap();
// All fields should be Named colors
assert!(matches!(theme.conversation_text, ColorDef::Named(_)));
assert!(matches!(theme.conversation_user, ColorDef::Named(_)));
}
}