1use imp_llm::ThinkingLevel;
2use ratatui::style::{Color, Modifier, Style};
3use serde::Deserialize;
4
5#[derive(Debug, Clone)]
7pub struct Theme {
8 pub fg: Color,
9 pub bg: Color,
10 pub accent: Color,
11 pub error: Color,
12 pub warning: Color,
13 pub success: Color,
14 pub muted: Color,
15 pub border: Color,
16 pub user_prefix: Color,
17 pub tool_name: Color,
18 pub code_bg: Color,
19 pub header_fg: Color,
20 pub selection_bg: Color,
21 pub selection_fg: Color,
22}
23
24impl Default for Theme {
25 fn default() -> Self {
27 Self {
28 bg: Color::Rgb(0x14, 0x12, 0x10), fg: Color::Rgb(0xb8, 0xa8, 0x98), accent: Color::Rgb(0xc0, 0xa1, 0x70), error: Color::Rgb(0xce, 0x5b, 0x47), warning: Color::Rgb(0xcb, 0x97, 0x73), success: Color::Rgb(0x8a, 0x9a, 0x6b), muted: Color::Rgb(0x83, 0x7e, 0x78), border: Color::Rgb(0x2a, 0x26, 0x22), user_prefix: Color::Rgb(0xc0, 0xa1, 0x70), tool_name: Color::Rgb(0xb9, 0x9c, 0x72), code_bg: Color::Rgb(0x1a, 0x18, 0x16), header_fg: Color::Rgb(0xb8, 0xa8, 0x98), selection_bg: Color::Rgb(0x2a, 0x26, 0x22), selection_fg: Color::Rgb(0xb8, 0xa8, 0x98), }
43 }
44}
45
46impl Theme {
47 pub fn named(name: &str) -> Self {
49 match name {
50 "light" => Self::light(),
51 _ => Self::default(),
52 }
53 }
54
55 pub fn light() -> Self {
57 Self {
58 bg: Color::Rgb(0xf5, 0xf0, 0xe8), fg: Color::Rgb(0x2a, 0x26, 0x22), accent: Color::Rgb(0x8a, 0x70, 0x48), error: Color::Rgb(0xa0, 0x38, 0x28), warning: Color::Rgb(0x9a, 0x6a, 0x40), success: Color::Rgb(0x50, 0x6a, 0x3a), muted: Color::Rgb(0x8a, 0x84, 0x7e), border: Color::Rgb(0xd0, 0xc8, 0xbc), user_prefix: Color::Rgb(0x8a, 0x70, 0x48), tool_name: Color::Rgb(0x7a, 0x68, 0x50), code_bg: Color::Rgb(0xec, 0xe6, 0xdc), header_fg: Color::Rgb(0x2a, 0x26, 0x22), selection_bg: Color::Rgb(0xd8, 0xd0, 0xc0), selection_fg: Color::Rgb(0x2a, 0x26, 0x22), }
73 }
74
75 pub fn apply_overrides(&mut self, overrides: &ThemeOverrides) {
77 if let Some(ref c) = overrides.fg {
78 if let Some(c) = parse_hex(c) {
79 self.fg = c;
80 }
81 }
82 if let Some(ref c) = overrides.bg {
83 if let Some(c) = parse_hex(c) {
84 self.bg = c;
85 }
86 }
87 if let Some(ref c) = overrides.accent {
88 if let Some(c) = parse_hex(c) {
89 self.accent = c;
90 }
91 }
92 if let Some(ref c) = overrides.error {
93 if let Some(c) = parse_hex(c) {
94 self.error = c;
95 }
96 }
97 if let Some(ref c) = overrides.warning {
98 if let Some(c) = parse_hex(c) {
99 self.warning = c;
100 }
101 }
102 if let Some(ref c) = overrides.success {
103 if let Some(c) = parse_hex(c) {
104 self.success = c;
105 }
106 }
107 if let Some(ref c) = overrides.muted {
108 if let Some(c) = parse_hex(c) {
109 self.muted = c;
110 }
111 }
112 if let Some(ref c) = overrides.border {
113 if let Some(c) = parse_hex(c) {
114 self.border = c;
115 }
116 }
117 if let Some(ref c) = overrides.user_prefix {
118 if let Some(c) = parse_hex(c) {
119 self.user_prefix = c;
120 }
121 }
122 if let Some(ref c) = overrides.tool_name {
123 if let Some(c) = parse_hex(c) {
124 self.tool_name = c;
125 }
126 }
127 if let Some(ref c) = overrides.code_bg {
128 if let Some(c) = parse_hex(c) {
129 self.code_bg = c;
130 }
131 }
132 }
133
134 pub fn style(&self) -> Style {
135 Style::default().fg(self.fg).bg(self.bg)
136 }
137
138 pub fn accent_style(&self) -> Style {
139 Style::default().fg(self.accent)
140 }
141
142 pub fn error_style(&self) -> Style {
143 Style::default().fg(self.error)
144 }
145
146 pub fn warning_style(&self) -> Style {
147 Style::default().fg(self.warning)
148 }
149
150 pub fn success_style(&self) -> Style {
151 Style::default().fg(self.success)
152 }
153
154 pub fn muted_style(&self) -> Style {
155 Style::default().fg(self.muted)
156 }
157
158 pub fn border_style(&self) -> Style {
159 Style::default().fg(self.border)
160 }
161
162 pub fn bold_style(&self) -> Style {
163 Style::default().add_modifier(Modifier::BOLD)
164 }
165
166 pub fn italic_style(&self) -> Style {
167 Style::default().add_modifier(Modifier::ITALIC)
168 }
169
170 pub fn code_inline_style(&self) -> Style {
171 Style::default().fg(self.warning).bg(self.code_bg)
172 }
173
174 pub fn header_style(&self) -> Style {
175 Style::default()
176 .fg(self.header_fg)
177 .add_modifier(Modifier::BOLD)
178 }
179
180 pub fn selected_style(&self) -> Style {
181 Style::default().fg(self.selection_fg).bg(self.selection_bg)
182 }
183
184 pub fn thinking_border_color(&self, level: ThinkingLevel) -> Color {
186 match level {
187 ThinkingLevel::Off => self.border, ThinkingLevel::Minimal => Color::Rgb(0x83, 0x7e, 0x78), ThinkingLevel::Low => Color::Rgb(0xb9, 0x9c, 0x72), ThinkingLevel::Medium => self.accent, ThinkingLevel::High => Color::Rgb(0xce, 0x5b, 0x47), ThinkingLevel::XHigh => Color::Rgb(0xcb, 0x97, 0x73), }
194 }
195}
196
197#[derive(Debug, Clone, Default, Deserialize)]
199pub struct ThemeOverrides {
200 pub fg: Option<String>,
201 pub bg: Option<String>,
202 pub accent: Option<String>,
203 pub error: Option<String>,
204 pub warning: Option<String>,
205 pub success: Option<String>,
206 pub muted: Option<String>,
207 pub border: Option<String>,
208 pub user_prefix: Option<String>,
209 pub tool_name: Option<String>,
210 pub code_bg: Option<String>,
211}
212
213fn parse_hex(s: &str) -> Option<Color> {
215 let s = s.strip_prefix('#').unwrap_or(s);
216 if s.len() != 6 {
217 return None;
218 }
219 let r = u8::from_str_radix(&s[0..2], 16).ok()?;
220 let g = u8::from_str_radix(&s[2..4], 16).ok()?;
221 let b = u8::from_str_radix(&s[4..6], 16).ok()?;
222 Some(Color::Rgb(r, g, b))
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn parse_hex_valid() {
231 assert_eq!(parse_hex("#ff0000"), Some(Color::Rgb(255, 0, 0)));
232 assert_eq!(parse_hex("00ff00"), Some(Color::Rgb(0, 255, 0)));
233 assert_eq!(parse_hex("#151820"), Some(Color::Rgb(0x15, 0x18, 0x20)));
234 }
235
236 #[test]
237 fn parse_hex_invalid() {
238 assert_eq!(parse_hex("nope"), None);
239 assert_eq!(parse_hex("#fff"), None);
240 assert_eq!(parse_hex(""), None);
241 }
242
243 #[test]
244 fn default_theme_is_dungeon() {
245 let t = Theme::default();
246 assert_eq!(t.accent, Color::Rgb(0xc0, 0xa1, 0x70));
248 assert_eq!(t.bg, Color::Rgb(0x14, 0x12, 0x10));
250 assert_eq!(t.error, Color::Rgb(0xce, 0x5b, 0x47));
252 }
253
254 #[test]
255 fn overrides_apply() {
256 let mut t = Theme::default();
257 let overrides = ThemeOverrides {
258 accent: Some("#ff0000".into()),
259 ..Default::default()
260 };
261 t.apply_overrides(&overrides);
262 assert_eq!(t.accent, Color::Rgb(255, 0, 0));
263 assert_eq!(t.user_prefix, Color::Rgb(0xc0, 0xa1, 0x70));
265 }
266
267 #[test]
268 fn named_themes() {
269 let default = Theme::named("default");
270 assert_eq!(default.accent, Color::Rgb(0xc0, 0xa1, 0x70));
271
272 let light = Theme::named("light");
273 assert_eq!(light.bg, Color::Rgb(0xf5, 0xf0, 0xe8));
274
275 let unknown = Theme::named("nonexistent");
277 assert_eq!(unknown.accent, Color::Rgb(0xc0, 0xa1, 0x70));
278 }
279}