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::marketing()
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    /// Marketing site inspired theme - dark with cyan accents
118    /// Matches the CodeTether marketing site design
119    pub fn marketing() -> Self {
120        Self {
121            // Cyan accent for user (matches marketing site buttons/highlights)
122            user_color: ColorDef::Rgb(6, 182, 212), // cyan-400
123            // Soft cyan for assistant
124            assistant_color: ColorDef::Rgb(34, 211, 238), // cyan-300
125            // Yellow for system (warnings/notifications)
126            system_color: ColorDef::Rgb(250, 204, 21), // yellow-400
127            // Magenta for tools
128            tool_color: ColorDef::Rgb(232, 121, 249), // fuchsia-400
129            // Red for errors
130            error_color: ColorDef::Rgb(248, 113, 113), // red-400
131            // Gray-800 for borders (subtle, matches marketing cards)
132            border_color: ColorDef::Rgb(31, 41, 55), // gray-800
133            // Gray-700 for input borders
134            input_border_color: ColorDef::Rgb(55, 65, 81), // gray-700
135            // Cyan for help borders
136            help_border_color: ColorDef::Rgb(6, 182, 212), // cyan-400
137            // Gray-500 for timestamps
138            timestamp_color: ColorDef::Rgb(107, 114, 128), // gray-500
139            // Gray-600 for code blocks
140            code_block_color: ColorDef::Rgb(75, 85, 99), // gray-600
141            // Dark foreground for status bar
142            status_bar_foreground: ColorDef::Rgb(17, 24, 39), // gray-900
143            // Cyan background for status bar
144            status_bar_background: ColorDef::Rgb(6, 182, 212), // cyan-400
145            // Gray-950 background (near black, matches marketing site)
146            background: Some(ColorDef::Rgb(3, 7, 18)), // gray-950
147        }
148    }
149
150    /// Get a style for a specific role
151    pub fn get_role_style(&self, role: &str) -> Style {
152        let color = match role {
153            "user" => self.user_color.to_color(),
154            "assistant" => self.assistant_color.to_color(),
155            "system" => self.system_color.to_color(),
156            "tool" => self.tool_color.to_color(),
157            "error" => self.error_color.to_color(),
158            _ => Color::White,
159        };
160        Style::default().fg(color).add_modifier(Modifier::BOLD)
161    }
162}
163
164/// Color definition that can be serialized/deserialized
165#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
166#[serde(untagged)]
167pub enum ColorDef {
168    /// Named color
169    Named(String),
170    /// RGB color (r, g, b)
171    Rgb(u8, u8, u8),
172    /// Indexed color (for 256-color terminals)
173    Indexed(u8),
174}
175
176impl ColorDef {
177    /// Convert to ratatui Color
178    pub fn to_color(&self) -> Color {
179        match self {
180            ColorDef::Named(name) => match name.to_lowercase().as_str() {
181                "black" => Color::Black,
182                "red" => Color::Red,
183                "green" => Color::Green,
184                "yellow" => Color::Yellow,
185                "blue" => Color::Blue,
186                "magenta" => Color::Magenta,
187                "cyan" => Color::Cyan,
188                "gray" | "grey" => Color::Gray,
189                "darkgray" | "dark_gray" | "darkgrey" => Color::DarkGray,
190                "lightgray" | "light_gray" | "lightgrey" => Color::Gray,
191                // Dark colors map to base colors (ratatui 0.30 doesn't have Dark* variants)
192                "darkred" | "dark_red" => Color::Red,
193                "darkgreen" | "dark_green" => Color::Green,
194                "darkyellow" | "dark_yellow" => Color::Yellow,
195                "darkblue" | "dark_blue" => Color::Blue,
196                "darkmagenta" | "dark_magenta" => Color::Magenta,
197                "darkcyan" | "dark_cyan" => Color::Cyan,
198                // Light colors
199                "lightred" | "light_red" => Color::LightRed,
200                "lightgreen" | "light_green" => Color::LightGreen,
201                "lightyellow" | "light_yellow" => Color::LightYellow,
202                "lightblue" | "light_blue" => Color::LightBlue,
203                "lightmagenta" | "light_magenta" => Color::LightMagenta,
204                "lightcyan" | "light_cyan" => Color::LightCyan,
205                "white" => Color::White,
206                _ => Color::White,
207            },
208            ColorDef::Rgb(r, g, b) => Color::Rgb(*r, *g, *b),
209            ColorDef::Indexed(idx) => Color::Indexed(*idx),
210        }
211    }
212}
213
214impl From<Color> for ColorDef {
215    fn from(color: Color) -> Self {
216        match color {
217            Color::Black => ColorDef::Named("black".to_string()),
218            Color::Red => ColorDef::Named("red".to_string()),
219            Color::Green => ColorDef::Named("green".to_string()),
220            Color::Yellow => ColorDef::Named("yellow".to_string()),
221            Color::Blue => ColorDef::Named("blue".to_string()),
222            Color::Magenta => ColorDef::Named("magenta".to_string()),
223            Color::Cyan => ColorDef::Named("cyan".to_string()),
224            Color::Gray => ColorDef::Named("gray".to_string()),
225            Color::DarkGray => ColorDef::Named("darkgray".to_string()),
226            Color::LightRed => ColorDef::Named("lightred".to_string()),
227            Color::LightGreen => ColorDef::Named("lightgreen".to_string()),
228            Color::LightYellow => ColorDef::Named("lightyellow".to_string()),
229            Color::LightBlue => ColorDef::Named("lightblue".to_string()),
230            Color::LightMagenta => ColorDef::Named("lightmagenta".to_string()),
231            Color::LightCyan => ColorDef::Named("lightcyan".to_string()),
232            Color::White => ColorDef::Named("white".to_string()),
233            Color::Rgb(r, g, b) => ColorDef::Rgb(r, g, b),
234            Color::Indexed(idx) => ColorDef::Indexed(idx),
235            _ => ColorDef::Named("white".to_string()),
236        }
237    }
238}
239
240// Helper functions for creating common colors
241impl ColorDef {
242    pub fn black() -> Self {
243        ColorDef::Named("black".to_string())
244    }
245    pub fn red() -> Self {
246        ColorDef::Named("red".to_string())
247    }
248    pub fn green() -> Self {
249        ColorDef::Named("green".to_string())
250    }
251    pub fn yellow() -> Self {
252        ColorDef::Named("yellow".to_string())
253    }
254    pub fn blue() -> Self {
255        ColorDef::Named("blue".to_string())
256    }
257    pub fn magenta() -> Self {
258        ColorDef::Named("magenta".to_string())
259    }
260    pub fn cyan() -> Self {
261        ColorDef::Named("cyan".to_string())
262    }
263    pub fn white() -> Self {
264        ColorDef::Named("white".to_string())
265    }
266    pub fn gray() -> Self {
267        ColorDef::Named("gray".to_string())
268    }
269    pub fn dark_gray() -> Self {
270        ColorDef::Named("darkgray".to_string())
271    }
272    pub fn light_gray() -> Self {
273        ColorDef::Named("gray".to_string())
274    }
275}