Skip to main content

codetether_agent/tui/
theme.rs

1use ratatui::style::{Color, Modifier, Style};
2use serde::{Deserialize, Serialize};
3
4/// Theme configuration for the TUI
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
6pub struct Theme {
7    /// User message color
8    pub user_color: ColorDef,
9    /// Assistant message color
10    pub assistant_color: ColorDef,
11    /// System message color
12    pub system_color: ColorDef,
13    /// Tool message color
14    pub tool_color: ColorDef,
15    /// Error message color
16    pub error_color: ColorDef,
17    /// Border color for main areas
18    pub border_color: ColorDef,
19    /// Input border color
20    pub input_border_color: ColorDef,
21    /// Help border color
22    pub help_border_color: ColorDef,
23    /// Timestamp color
24    pub timestamp_color: ColorDef,
25    /// Code block border color
26    pub code_block_color: ColorDef,
27    /// Status bar colors
28    pub status_bar_foreground: ColorDef,
29    pub status_bar_background: ColorDef,
30    /// Background color (terminal default if None)
31    pub background: Option<ColorDef>,
32}
33
34impl Default for Theme {
35    fn default() -> Self {
36        Self::dark()
37    }
38}
39
40impl Theme {
41    /// Dark theme (default)
42    pub fn dark() -> Self {
43        Self {
44            user_color: ColorDef::Named("green".to_string()),
45            assistant_color: ColorDef::Named("cyan".to_string()),
46            system_color: ColorDef::Named("yellow".to_string()),
47            tool_color: ColorDef::Named("magenta".to_string()),
48            error_color: ColorDef::Named("red".to_string()),
49            border_color: ColorDef::Named("cyan".to_string()),
50            input_border_color: ColorDef::Named("white".to_string()),
51            help_border_color: ColorDef::Named("yellow".to_string()),
52            timestamp_color: ColorDef::Named("darkgray".to_string()),
53            code_block_color: ColorDef::Named("darkgray".to_string()),
54            status_bar_foreground: ColorDef::Named("black".to_string()),
55            status_bar_background: ColorDef::Named("white".to_string()),
56            background: None,
57        }
58    }
59
60    /// Light theme
61    pub fn light() -> Self {
62        Self {
63            user_color: ColorDef::Named("darkgreen".to_string()),
64            assistant_color: ColorDef::Named("darkblue".to_string()),
65            system_color: ColorDef::Named("darkyellow".to_string()),
66            tool_color: ColorDef::Named("darkmagenta".to_string()),
67            error_color: ColorDef::Named("darkred".to_string()),
68            border_color: ColorDef::Named("darkgray".to_string()),
69            input_border_color: ColorDef::Named("black".to_string()),
70            help_border_color: ColorDef::Named("darkyellow".to_string()),
71            timestamp_color: ColorDef::Named("gray".to_string()),
72            code_block_color: ColorDef::Named("gray".to_string()),
73            status_bar_foreground: ColorDef::Named("white".to_string()),
74            status_bar_background: ColorDef::Named("black".to_string()),
75            background: Some(ColorDef::Named("white".to_string())),
76        }
77    }
78
79    /// Solarized dark theme
80    pub fn solarized_dark() -> Self {
81        Self {
82            user_color: ColorDef::Rgb(133, 153, 0),              // green
83            assistant_color: ColorDef::Rgb(42, 161, 152),        // cyan
84            system_color: ColorDef::Rgb(181, 137, 0),            // yellow
85            tool_color: ColorDef::Rgb(211, 54, 130),             // magenta
86            error_color: ColorDef::Rgb(220, 50, 47),             // red
87            border_color: ColorDef::Rgb(131, 148, 150),          // base1
88            input_border_color: ColorDef::Rgb(238, 232, 213),    // base2
89            help_border_color: ColorDef::Rgb(181, 137, 0),       // yellow
90            timestamp_color: ColorDef::Rgb(101, 123, 131),       // base01
91            code_block_color: ColorDef::Rgb(88, 110, 117),       // base03
92            status_bar_foreground: ColorDef::Rgb(0, 43, 54),     // base03
93            status_bar_background: ColorDef::Rgb(238, 232, 213), // base2
94            background: Some(ColorDef::Rgb(0, 43, 54)),          // base03
95        }
96    }
97
98    /// Solarized light theme
99    pub fn solarized_light() -> Self {
100        Self {
101            user_color: ColorDef::Rgb(133, 153, 0),              // green
102            assistant_color: ColorDef::Rgb(42, 161, 152),        // cyan
103            system_color: ColorDef::Rgb(181, 137, 0),            // yellow
104            tool_color: ColorDef::Rgb(211, 54, 130),             // magenta
105            error_color: ColorDef::Rgb(220, 50, 47),             // red
106            border_color: ColorDef::Rgb(88, 110, 117),           // base00
107            input_border_color: ColorDef::Rgb(147, 161, 161),    // base1
108            help_border_color: ColorDef::Rgb(181, 137, 0),       // yellow
109            timestamp_color: ColorDef::Rgb(131, 148, 150),       // base1
110            code_block_color: ColorDef::Rgb(147, 161, 161),      // base1
111            status_bar_foreground: ColorDef::Rgb(238, 232, 213), // base2
112            status_bar_background: ColorDef::Rgb(88, 110, 117),  // base00
113            background: Some(ColorDef::Rgb(253, 246, 227)),      // base3
114        }
115    }
116
117    /// Get a style for a specific role
118    pub fn get_role_style(&self, role: &str) -> Style {
119        let color = match role {
120            "user" => self.user_color.to_color(),
121            "assistant" => self.assistant_color.to_color(),
122            "system" => self.system_color.to_color(),
123            "tool" => self.tool_color.to_color(),
124            "error" => self.error_color.to_color(),
125            _ => Color::White,
126        };
127        Style::default().fg(color).add_modifier(Modifier::BOLD)
128    }
129}
130
131/// Color definition that can be serialized/deserialized
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
133#[serde(untagged)]
134pub enum ColorDef {
135    /// Named color
136    Named(String),
137    /// RGB color (r, g, b)
138    Rgb(u8, u8, u8),
139    /// Indexed color (for 256-color terminals)
140    Indexed(u8),
141}
142
143impl ColorDef {
144    /// Convert to ratatui Color
145    pub fn to_color(&self) -> Color {
146        match self {
147            ColorDef::Named(name) => match name.to_lowercase().as_str() {
148                "black" => Color::Black,
149                "red" => Color::Red,
150                "green" => Color::Green,
151                "yellow" => Color::Yellow,
152                "blue" => Color::Blue,
153                "magenta" => Color::Magenta,
154                "cyan" => Color::Cyan,
155                "gray" | "grey" => Color::Gray,
156                "darkgray" | "dark_gray" | "darkgrey" => Color::DarkGray,
157                "lightgray" | "light_gray" | "lightgrey" => Color::Gray,
158                // Dark colors map to base colors (ratatui 0.30 doesn't have Dark* variants)
159                "darkred" | "dark_red" => Color::Red,
160                "darkgreen" | "dark_green" => Color::Green,
161                "darkyellow" | "dark_yellow" => Color::Yellow,
162                "darkblue" | "dark_blue" => Color::Blue,
163                "darkmagenta" | "dark_magenta" => Color::Magenta,
164                "darkcyan" | "dark_cyan" => Color::Cyan,
165                // Light colors
166                "lightred" | "light_red" => Color::LightRed,
167                "lightgreen" | "light_green" => Color::LightGreen,
168                "lightyellow" | "light_yellow" => Color::LightYellow,
169                "lightblue" | "light_blue" => Color::LightBlue,
170                "lightmagenta" | "light_magenta" => Color::LightMagenta,
171                "lightcyan" | "light_cyan" => Color::LightCyan,
172                "white" => Color::White,
173                _ => Color::White,
174            },
175            ColorDef::Rgb(r, g, b) => Color::Rgb(*r, *g, *b),
176            ColorDef::Indexed(idx) => Color::Indexed(*idx),
177        }
178    }
179}
180
181impl From<Color> for ColorDef {
182    fn from(color: Color) -> Self {
183        match color {
184            Color::Black => ColorDef::Named("black".to_string()),
185            Color::Red => ColorDef::Named("red".to_string()),
186            Color::Green => ColorDef::Named("green".to_string()),
187            Color::Yellow => ColorDef::Named("yellow".to_string()),
188            Color::Blue => ColorDef::Named("blue".to_string()),
189            Color::Magenta => ColorDef::Named("magenta".to_string()),
190            Color::Cyan => ColorDef::Named("cyan".to_string()),
191            Color::Gray => ColorDef::Named("gray".to_string()),
192            Color::DarkGray => ColorDef::Named("darkgray".to_string()),
193            Color::LightRed => ColorDef::Named("lightred".to_string()),
194            Color::LightGreen => ColorDef::Named("lightgreen".to_string()),
195            Color::LightYellow => ColorDef::Named("lightyellow".to_string()),
196            Color::LightBlue => ColorDef::Named("lightblue".to_string()),
197            Color::LightMagenta => ColorDef::Named("lightmagenta".to_string()),
198            Color::LightCyan => ColorDef::Named("lightcyan".to_string()),
199            Color::White => ColorDef::Named("white".to_string()),
200            Color::Rgb(r, g, b) => ColorDef::Rgb(r, g, b),
201            Color::Indexed(idx) => ColorDef::Indexed(idx),
202            _ => ColorDef::Named("white".to_string()),
203        }
204    }
205}
206
207// Helper functions for creating common colors
208impl ColorDef {
209    pub fn black() -> Self {
210        ColorDef::Named("black".to_string())
211    }
212    pub fn red() -> Self {
213        ColorDef::Named("red".to_string())
214    }
215    pub fn green() -> Self {
216        ColorDef::Named("green".to_string())
217    }
218    pub fn yellow() -> Self {
219        ColorDef::Named("yellow".to_string())
220    }
221    pub fn blue() -> Self {
222        ColorDef::Named("blue".to_string())
223    }
224    pub fn magenta() -> Self {
225        ColorDef::Named("magenta".to_string())
226    }
227    pub fn cyan() -> Self {
228        ColorDef::Named("cyan".to_string())
229    }
230    pub fn white() -> Self {
231        ColorDef::Named("white".to_string())
232    }
233    pub fn gray() -> Self {
234        ColorDef::Named("gray".to_string())
235    }
236    pub fn dark_gray() -> Self {
237        ColorDef::Named("darkgray".to_string())
238    }
239    pub fn light_gray() -> Self {
240        ColorDef::Named("gray".to_string())
241    }
242}