返回 CodeWhale
themes.rs
根目录 / crates / tui / src / palette / themes.rs
1 //! Named theme presets and theme resolution.
2
3 use ratatui::style::Color;
4
5 use super::detect::PaletteMode;
6 use super::tokens::*;
7
8 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
9 pub struct UiTheme {
10 pub name: &'static str,
11 pub mode: PaletteMode,
12 // Surface hierarchy
13 pub surface_bg: Color,
14 pub panel_bg: Color,
15 pub elevated_bg: Color,
16 pub composer_bg: Color,
17 pub selection_bg: Color,
18 pub header_bg: Color,
19 pub footer_bg: Color,
20 /// Text hierarchy
21 pub text_dim: Color,
22 pub text_hint: Color,
23 pub text_muted: Color,
24 pub text_body: Color,
25 pub text_soft: Color,
26 pub border: Color,
27 // Accent roles
28 pub accent_primary: Color,
29 pub accent_secondary: Color,
30 pub accent_action: Color,
31 // Error / destructive
32 pub error_fg: Color,
33 pub error_hover: Color,
34 pub error_surface: Color,
35 pub error_border: Color,
36 pub error_text: Color,
37 // Status roles (warning / success / info)
38 pub warning: Color,
39 pub success: Color,
40 pub info: Color,
41 // Mode badge colors (act/plan/operate; mode_yolo kept for legacy theme data).
42 // These must be distinct from one another and from the general accent,
43 // human, warning, danger, and success lanes: the terminal backend receives
44 // only the final `Color` and needs that identity to perform semantic ANSI
45 // adaptation.
46 pub mode_agent: Color,
47 pub mode_yolo: Color,
48 pub mode_plan: Color,
49 pub mode_operate: Color,
50 // Permission posture colors (Ask / Auto-Review / Full Access). These are
51 // explicit theme slots because the warm permission ramp is independent
52 // from the cool mode ramp.
53 pub permission_ask: Color,
54 pub permission_auto_review: Color,
55 pub permission_full_access: Color,
56 // Footer statusline colors
57 pub status_ready: Color,
58 pub status_working: Color,
59 pub status_warning: Color,
60 // Diff colors
61 pub diff_added_fg: Color,
62 pub diff_deleted_fg: Color,
63 pub diff_added_bg: Color,
64 pub diff_deleted_bg: Color,
65 // Tool cell colors
66 pub tool_running: Color,
67 pub tool_success: Color,
68 pub tool_failed: Color,
69 }
70
71 pub const UI_THEME: UiTheme = UiTheme {
72 name: "whale",
73 mode: PaletteMode::Dark,
74 surface_bg: WHALE_BG,
75 panel_bg: WHALE_PANEL,
76 elevated_bg: SURFACE_ELEVATED,
77 composer_bg: WHALE_COMPOSER,
78 selection_bg: SELECTION_BG,
79 header_bg: WHALE_CHROME,
80 footer_bg: WHALE_CHROME,
81 text_dim: TEXT_DIM,
82 text_hint: TEXT_HINT,
83 text_muted: TEXT_MUTED,
84 text_body: TEXT_BODY,
85 text_soft: TEXT_SOFT,
86 border: BORDER_COLOR,
87 accent_primary: WHALE_ACTION,
88 accent_secondary: WHALE_LIVE,
89 accent_action: WHALE_HUMAN,
90 error_fg: WHALE_ERROR,
91 error_hover: Color::Rgb(
92 WHALE_ERROR_HOVER_RGB.0,
93 WHALE_ERROR_HOVER_RGB.1,
94 WHALE_ERROR_HOVER_RGB.2,
95 ),
96 error_surface: Color::Rgb(
97 WHALE_ERROR_SURFACE_RGB.0,
98 WHALE_ERROR_SURFACE_RGB.1,
99 WHALE_ERROR_SURFACE_RGB.2,
100 ),
101 error_border: Color::Rgb(
102 WHALE_ERROR_BORDER_RGB.0,
103 WHALE_ERROR_BORDER_RGB.1,
104 WHALE_ERROR_BORDER_RGB.2,
105 ),
106 error_text: Color::Rgb(
107 WHALE_ERROR_TEXT_RGB.0,
108 WHALE_ERROR_TEXT_RGB.1,
109 WHALE_ERROR_TEXT_RGB.2,
110 ),
111 warning: STATUS_WARNING,
112 success: Color::Rgb(
113 WHALE_SUCCESS_RGB.0,
114 WHALE_SUCCESS_RGB.1,
115 WHALE_SUCCESS_RGB.2,
116 ),
117 info: WHALE_ACTION,
118 mode_agent: MODE_AGENT,
119 mode_yolo: MODE_YOLO,
120 mode_plan: MODE_PLAN,
121 mode_operate: MODE_OPERATE,
122 permission_ask: TEXT_REASONING,
123 permission_auto_review: WHALE_HUMAN,
124 permission_full_access: STATUS_WARNING,
125 status_ready: TEXT_MUTED,
126 status_working: WHALE_LIVE,
127 status_warning: STATUS_WARNING,
128 diff_added_fg: DIFF_ADDED,
129 diff_deleted_fg: WHALE_ERROR,
130 diff_added_bg: DIFF_ADDED_BG,
131 diff_deleted_bg: DIFF_DELETED_BG,
132 tool_running: WHALE_LIVE,
133 tool_success: Color::Rgb(
134 WHALE_WORKING_GREEN_RGB.0,
135 WHALE_WORKING_GREEN_RGB.1,
136 WHALE_WORKING_GREEN_RGB.2,
137 ),
138 tool_failed: WHALE_ERROR,
139 };
140
141 pub const LIGHT_UI_THEME: UiTheme = UiTheme {
142 name: "whale-light",
143 mode: PaletteMode::Light,
144 surface_bg: LIGHT_SURFACE,
145 panel_bg: LIGHT_PANEL,
146 elevated_bg: LIGHT_ELEVATED,
147 composer_bg: LIGHT_PANEL,
148 selection_bg: LIGHT_SELECTION_BG,
149 header_bg: LIGHT_SURFACE,
150 footer_bg: LIGHT_SURFACE,
151 text_dim: LIGHT_TEXT_HINT,
152 text_hint: LIGHT_TEXT_HINT,
153 text_muted: LIGHT_TEXT_MUTED,
154 text_body: LIGHT_TEXT_BODY,
155 text_soft: LIGHT_TEXT_SOFT,
156 border: LIGHT_BORDER,
157 accent_primary: LIGHT_ACTION,
158 accent_secondary: LIGHT_LIVE,
159 accent_action: LIGHT_HUMAN,
160 error_fg: LIGHT_DANGER,
161 error_hover: Color::Rgb(201, 71, 120), // #C94778
162 error_surface: LIGHT_ERROR,
163 error_border: LIGHT_DANGER,
164 error_text: Color::Rgb(109, 22, 56), // #6D1638
165 warning: LIGHT_WARNING,
166 success: LIGHT_SUCCESS_FG,
167 info: LIGHT_ACTION,
168 mode_agent: LIGHT_MODE_AGENT,
169 mode_yolo: LIGHT_MODE_YOLO,
170 mode_plan: LIGHT_MODE_PLAN,
171 mode_operate: LIGHT_OPERATE,
172 permission_ask: Color::Rgb(146, 64, 14),
173 permission_auto_review: LIGHT_HUMAN,
174 permission_full_access: LIGHT_WARNING,
175 status_ready: LIGHT_TEXT_MUTED,
176 status_working: LIGHT_LIVE,
177 status_warning: LIGHT_WARNING,
178 diff_added_fg: Color::Rgb(22, 101, 52), // green
179 diff_deleted_fg: LIGHT_DANGER,
180 diff_added_bg: Color::Rgb(223, 247, 231), // light green
181 diff_deleted_bg: LIGHT_ERROR,
182 tool_running: LIGHT_LIVE,
183 tool_success: LIGHT_SUCCESS_FG,
184 tool_failed: LIGHT_DANGER,
185 };
186
187 pub const SOLARIZED_LIGHT_UI_THEME: UiTheme = UiTheme {
188 name: "solarized-light",
189 mode: PaletteMode::SolarizedLight,
190 surface_bg: SOLARIZED_SURFACE,
191 panel_bg: SOLARIZED_PANEL,
192 elevated_bg: SOLARIZED_ELEVATED,
193 composer_bg: SOLARIZED_COMPOSER,
194 selection_bg: SOLARIZED_SELECT_BG,
195 header_bg: SOLARIZED_SURFACE,
196 footer_bg: SOLARIZED_SURFACE,
197 text_dim: SOLARIZED_TEXT_DIM,
198 text_hint: SOLARIZED_TEXT_HINT,
199 text_muted: SOLARIZED_TEXT_MUTED,
200 text_body: SOLARIZED_TEXT_BODY,
201 text_soft: SOLARIZED_TEXT_SOFT,
202 border: SOLARIZED_BORDER,
203 accent_primary: SOLARIZED_BLUE,
204 accent_secondary: SOLARIZED_CYAN,
205 accent_action: SOLARIZED_ORANGE,
206 error_fg: SOLARIZED_RED,
207 error_hover: SOLARIZED_ERROR_HOVER,
208 error_surface: SOLARIZED_ERROR_SURFACE,
209 error_border: SOLARIZED_RED,
210 error_text: SOLARIZED_ERROR_TEXT,
211 warning: SOLARIZED_YELLOW,
212 success: SOLARIZED_GREEN,
213 info: SOLARIZED_BLUE,
214 mode_agent: Color::Rgb(0x27, 0x8B, 0xD2),
215 mode_yolo: Color::Rgb(0xDD, 0x32, 0x2F),
216 mode_plan: Color::Rgb(0xCC, 0x4B, 0x16),
217 mode_operate: Color::Rgb(0x6C, 0x71, 0xC4), // solarized violet
218 permission_ask: SOLARIZED_YELLOW,
219 permission_auto_review: SOLARIZED_ORANGE,
220 permission_full_access: SOLARIZED_RED,
221 status_ready: SOLARIZED_CYAN,
222 status_working: SOLARIZED_CYAN,
223 status_warning: SOLARIZED_YELLOW,
224 diff_added_fg: SOLARIZED_GREEN,
225 diff_deleted_fg: SOLARIZED_RED,
226 diff_added_bg: SOLARIZED_DIFF_ADDED_BG,
227 diff_deleted_bg: SOLARIZED_DIFF_DELETED_BG,
228 tool_running: SOLARIZED_BLUE,
229 tool_success: SOLARIZED_GREEN,
230 tool_failed: SOLARIZED_RED,
231 };
232
233 pub const GRAYSCALE_UI_THEME: UiTheme = UiTheme {
234 name: "grayscale",
235 mode: PaletteMode::Grayscale,
236 surface_bg: GRAYSCALE_SURFACE,
237 panel_bg: GRAYSCALE_PANEL,
238 elevated_bg: GRAYSCALE_ELEVATED,
239 composer_bg: GRAYSCALE_PANEL,
240 selection_bg: GRAYSCALE_SELECTION_BG,
241 header_bg: GRAYSCALE_SURFACE,
242 footer_bg: GRAYSCALE_SURFACE,
243 text_dim: GRAYSCALE_TEXT_HINT,
244 text_hint: GRAYSCALE_TEXT_HINT,
245 text_muted: GRAYSCALE_TEXT_MUTED,
246 text_body: GRAYSCALE_TEXT_BODY,
247 text_soft: GRAYSCALE_TEXT_SOFT,
248 border: GRAYSCALE_BORDER,
249 accent_primary: GRAYSCALE_TEXT_SOFT,
250 accent_secondary: GRAYSCALE_TEXT_MUTED,
251 accent_action: Color::Rgb(210, 210, 210),
252 error_fg: GRAYSCALE_TEXT_BODY,
253 error_hover: GRAYSCALE_TEXT_SOFT,
254 error_surface: GRAYSCALE_ERROR,
255 error_border: GRAYSCALE_BORDER,
256 error_text: GRAYSCALE_TEXT_SOFT,
257 warning: GRAYSCALE_TEXT_MUTED,
258 success: GRAYSCALE_TEXT_SOFT,
259 info: GRAYSCALE_TEXT_MUTED,
260 mode_agent: Color::Rgb(200, 200, 200),
261 mode_yolo: Color::Rgb(237, 237, 237),
262 mode_plan: Color::Rgb(181, 181, 181),
263 // Near-white stays unmistakably distinct without claiming arbitrary pure
264 // white content as an already-resolved mode slot.
265 mode_operate: Color::Rgb(250, 250, 250),
266 permission_ask: Color::Rgb(181, 181, 181),
267 permission_auto_review: Color::Rgb(210, 210, 210),
268 permission_full_access: Color::Rgb(237, 237, 237),
269 status_ready: GRAYSCALE_TEXT_MUTED,
270 status_working: GRAYSCALE_TEXT_SOFT,
271 status_warning: GRAYSCALE_TEXT_BODY,
272 diff_added_fg: GRAYSCALE_TEXT_SOFT,
273 diff_deleted_fg: GRAYSCALE_TEXT_BODY,
274 diff_added_bg: GRAYSCALE_SUCCESS,
275 diff_deleted_bg: GRAYSCALE_ERROR,
276 tool_running: GRAYSCALE_TEXT_SOFT,
277 tool_success: GRAYSCALE_TEXT_HINT,
278 tool_failed: GRAYSCALE_TEXT_BODY,
279 };
280
281 pub const CATPPUCCIN_MOCHA_UI_THEME: UiTheme = UiTheme {
282 name: "catppuccin-mocha",
283 mode: PaletteMode::Dark,
284 surface_bg: Color::Rgb(0x1e, 0x1e, 0x2e), // base
285 panel_bg: Color::Rgb(0x18, 0x18, 0x25), // mantle
286 elevated_bg: Color::Rgb(0x31, 0x32, 0x44), // surface0
287 composer_bg: Color::Rgb(0x18, 0x18, 0x25),
288 selection_bg: Color::Rgb(0x45, 0x47, 0x5a), // surface1
289 header_bg: Color::Rgb(0x11, 0x11, 0x1b), // crust
290 footer_bg: Color::Rgb(0x11, 0x11, 0x1b),
291 text_dim: Color::Rgb(0x6c, 0x70, 0x86), // overlay0
292 text_hint: Color::Rgb(0x7f, 0x84, 0x9c), // overlay1
293 text_muted: Color::Rgb(0xa6, 0xad, 0xc8), // subtext0
294 text_body: Color::Rgb(0xcd, 0xd6, 0xf4), // text
295 text_soft: Color::Rgb(0xba, 0xc2, 0xde), // subtext1
296 border: Color::Rgb(0x45, 0x47, 0x5a), // surface1
297 accent_primary: Color::Rgb(0x89, 0xb4, 0xfa), // blue
298 accent_secondary: Color::Rgb(0x74, 0xc7, 0xec), // sapphire
299 accent_action: Color::Rgb(0xfa, 0xb3, 0x87), // peach
300 error_fg: Color::Rgb(0xf3, 0x8b, 0xa8), // red
301 error_hover: Color::Rgb(0xf5, 0xa2, 0xbc),
302 error_surface: Color::Rgb(0x3a, 0x1f, 0x2a),
303 error_border: Color::Rgb(0xf3, 0x8b, 0xa8),
304 error_text: Color::Rgb(0xf5, 0xc2, 0xd0),
305 warning: Color::Rgb(0xf9, 0xe2, 0xaf), // yellow
306 success: Color::Rgb(0xa6, 0xe3, 0xa1), // green
307 info: Color::Rgb(0x89, 0xd9, 0xeb), // sky
308 mode_agent: Color::Rgb(0x8a, 0xb4, 0xfa), // blue
309 mode_yolo: Color::Rgb(0xf3, 0x8c, 0xa8), // red
310 mode_plan: Color::Rgb(0xfa, 0xb4, 0x87), // peach
311 mode_operate: Color::Rgb(0xcb, 0xa6, 0xf7), // mauve
312 permission_ask: Color::Rgb(0xf9, 0xe2, 0xaf),
313 permission_auto_review: Color::Rgb(0xfa, 0xb3, 0x87),
314 permission_full_access: Color::Rgb(0xf3, 0x8b, 0xa8),
315 status_ready: Color::Rgb(0x7f, 0x84, 0x9c), // overlay1
316 status_working: Color::Rgb(0x74, 0xc7, 0xec), // sapphire
317 status_warning: Color::Rgb(0xf9, 0xe2, 0xaf), // yellow
318 diff_added_fg: Color::Rgb(0xa6, 0xe3, 0xa1), // green
319 diff_deleted_fg: Color::Rgb(0xf3, 0x8b, 0xa8), // red
320 diff_added_bg: Color::Rgb(0x1f, 0x33, 0x29),
321 diff_deleted_bg: Color::Rgb(0x3a, 0x1f, 0x2a),
322 tool_running: Color::Rgb(0x74, 0xc7, 0xec), // sapphire
323 tool_success: Color::Rgb(0x7f, 0x84, 0x9c), // overlay1
324 tool_failed: Color::Rgb(0xf3, 0x8b, 0xa8), // red
325 };
326
327 pub const TOKYO_NIGHT_UI_THEME: UiTheme = UiTheme {
328 name: "tokyo-night",
329 mode: PaletteMode::Dark,
330 surface_bg: Color::Rgb(0x1a, 0x1b, 0x26), // bg
331 panel_bg: Color::Rgb(0x16, 0x16, 0x1e), // bg_dark
332 elevated_bg: Color::Rgb(0x29, 0x2e, 0x42), // bg_highlight
333 composer_bg: Color::Rgb(0x16, 0x16, 0x1e),
334 selection_bg: Color::Rgb(0x28, 0x34, 0x57), // visual selection
335 header_bg: Color::Rgb(0x16, 0x16, 0x1e),
336 footer_bg: Color::Rgb(0x16, 0x16, 0x1e),
337 text_dim: Color::Rgb(0x5c, 0x65, 0x8d), // comment, lifted to 3:1 on bg
338 text_hint: Color::Rgb(0x73, 0x7a, 0xa2), // dark5
339 text_muted: Color::Rgb(0xa9, 0xb1, 0xd6), // fg_dark
340 text_body: Color::Rgb(0xc0, 0xca, 0xf5), // fg
341 text_soft: Color::Rgb(0xbb, 0xc2, 0xe0),
342 border: Color::Rgb(0x41, 0x48, 0x68), // terminal_black
343 accent_primary: Color::Rgb(0x7a, 0xa2, 0xf7), // blue
344 accent_secondary: Color::Rgb(0x7d, 0xcf, 0xff), // cyan
345 accent_action: Color::Rgb(0xff, 0x9e, 0x64), // orange
346 error_fg: Color::Rgb(0xf7, 0x76, 0x8e), // red
347 error_hover: Color::Rgb(0xf9, 0x92, 0xa4),
348 error_surface: Color::Rgb(0x33, 0x1c, 0x24),
349 error_border: Color::Rgb(0xf7, 0x76, 0x8e),
350 error_text: Color::Rgb(0xfa, 0xcc, 0xd4),
351 warning: Color::Rgb(0xe0, 0xaf, 0x68), // yellow
352 success: Color::Rgb(0x9e, 0xce, 0x6a), // green
353 info: Color::Rgb(0x7d, 0xcf, 0xff), // cyan
354 mode_agent: Color::Rgb(0x7b, 0xa2, 0xf7), // blue
355 mode_yolo: Color::Rgb(0xf7, 0x77, 0x8e), // red
356 mode_plan: Color::Rgb(0xff, 0x9f, 0x64), // orange
357 mode_operate: Color::Rgb(0xbb, 0x9a, 0xf7), // purple
358 permission_ask: Color::Rgb(0xe0, 0xaf, 0x68),
359 permission_auto_review: Color::Rgb(0xff, 0x9e, 0x64),
360 permission_full_access: Color::Rgb(0xf7, 0x76, 0x8e),
361 status_ready: Color::Rgb(0x5c, 0x65, 0x8d), // comment, lifted to 3:1 on bg
362 status_working: Color::Rgb(0x7d, 0xcf, 0xff), // cyan
363 status_warning: Color::Rgb(0xe0, 0xaf, 0x68), // yellow
364 diff_added_fg: Color::Rgb(0x9e, 0xce, 0x6a), // green
365 diff_deleted_fg: Color::Rgb(0xf7, 0x76, 0x8e), // red
366 diff_added_bg: Color::Rgb(0x1b, 0x2b, 0x1f),
367 diff_deleted_bg: Color::Rgb(0x33, 0x1c, 0x24),
368 tool_running: Color::Rgb(0x7d, 0xcf, 0xff), // cyan
369 tool_success: Color::Rgb(0x56, 0x5f, 0x89), // comment
370 tool_failed: Color::Rgb(0xf7, 0x76, 0x8e), // red
371 };
372
373 pub const DRACULA_UI_THEME: UiTheme = UiTheme {
374 name: "dracula",
375 mode: PaletteMode::Dark,
376 surface_bg: Color::Rgb(0x28, 0x2a, 0x36), // background
377 panel_bg: Color::Rgb(0x21, 0x22, 0x2c),
378 elevated_bg: Color::Rgb(0x34, 0x37, 0x46),
379 composer_bg: Color::Rgb(0x21, 0x22, 0x2c),
380 selection_bg: Color::Rgb(0x44, 0x47, 0x5a), // current line
381 header_bg: Color::Rgb(0x21, 0x22, 0x2c),
382 footer_bg: Color::Rgb(0x21, 0x22, 0x2c),
383 text_dim: Color::Rgb(0x62, 0x72, 0xa4), // comment
384 text_hint: Color::Rgb(0x8a, 0x8e, 0xaa),
385 text_muted: Color::Rgb(0xc0, 0xc4, 0xd6),
386 text_body: Color::Rgb(0xf8, 0xf8, 0xf2), // foreground
387 text_soft: Color::Rgb(0xe2, 0xe2, 0xdc),
388 border: Color::Rgb(0x44, 0x47, 0x5a),
389 accent_primary: Color::Rgb(0xbd, 0x93, 0xf9), // purple
390 accent_secondary: Color::Rgb(0x8b, 0xe9, 0xfd), // cyan
391 accent_action: Color::Rgb(0xff, 0xb8, 0x6c), // orange
392 error_fg: Color::Rgb(0xff, 0x55, 0x55), // red
393 error_hover: Color::Rgb(0xff, 0x7c, 0x7c),
394 error_surface: Color::Rgb(0x3a, 0x1f, 0x22),
395 error_border: Color::Rgb(0xff, 0x55, 0x55),
396 error_text: Color::Rgb(0xff, 0xbb, 0xbb),
397 warning: Color::Rgb(0xf1, 0xfa, 0x8c), // yellow
398 success: Color::Rgb(0x50, 0xfa, 0x7b), // green
399 info: Color::Rgb(0x8b, 0xe9, 0xfd), // cyan
400 mode_agent: Color::Rgb(0xbe, 0x93, 0xf9), // purple
401 mode_yolo: Color::Rgb(0xff, 0x56, 0x55), // red
402 mode_plan: Color::Rgb(0xff, 0xb9, 0x6c), // orange
403 mode_operate: Color::Rgb(0x8c, 0xe9, 0xfd), // cyan
404 permission_ask: Color::Rgb(0xf1, 0xfa, 0x8c),
405 permission_auto_review: Color::Rgb(0xff, 0xb8, 0x6c),
406 permission_full_access: Color::Rgb(0xff, 0x55, 0x55),
407 status_ready: Color::Rgb(0x62, 0x72, 0xa4), // comment
408 status_working: Color::Rgb(0x8b, 0xe9, 0xfd), // cyan
409 status_warning: Color::Rgb(0xf1, 0xfa, 0x8c), // yellow
410 diff_added_fg: Color::Rgb(0x50, 0xfa, 0x7b), // green
411 diff_deleted_fg: Color::Rgb(0xff, 0x55, 0x55), // red
412 diff_added_bg: Color::Rgb(0x21, 0x3a, 0x2a),
413 diff_deleted_bg: Color::Rgb(0x3a, 0x1f, 0x22),
414 tool_running: Color::Rgb(0x8b, 0xe9, 0xfd), // cyan
415 tool_success: Color::Rgb(0x62, 0x72, 0xa4), // comment
416 tool_failed: Color::Rgb(0xff, 0x55, 0x55), // red
417 };
418
419 /// "Terminal" theme: lets the host terminal's color scheme show through
420 /// instead of painting any RGB surface. Backgrounds use `Color::Reset`
421 /// (the terminal's own default bg) and most text uses `Color::Reset`
422 /// (terminal's own default fg). Accents are ANSI named colors so they
423 /// also inherit the user's terminal palette (Solarized, Nord, custom
424 /// schemes, etc.) rather than Codewhale brand RGB.
425 pub const TERMINAL_UI_THEME: UiTheme = UiTheme {
426 name: "terminal",
427 // Mode is reported as Dark to avoid the dark→light cell remap kicking
428 // in; the terminal-theme cell remap already normalizes everything to
429 // `Color::Reset`, and we never want a second pass overwriting that.
430 mode: PaletteMode::Dark,
431 surface_bg: Color::Reset,
432 panel_bg: Color::Reset,
433 elevated_bg: Color::Reset,
434 composer_bg: Color::Reset,
435 selection_bg: Color::Reset,
436 header_bg: Color::Reset,
437 footer_bg: Color::Reset,
438 text_dim: Color::Reset,
439 text_hint: Color::Reset,
440 text_muted: Color::Reset,
441 text_body: Color::Reset,
442 text_soft: Color::Reset,
443 border: Color::Reset,
444 accent_primary: Color::Blue,
445 accent_secondary: Color::Cyan,
446 accent_action: Color::Yellow,
447 error_fg: Color::Red,
448 error_hover: Color::Red,
449 error_surface: Color::Reset,
450 error_border: Color::Red,
451 error_text: Color::Red,
452 warning: Color::Yellow,
453 success: Color::Green,
454 info: Color::Cyan,
455 mode_agent: Color::LightBlue,
456 mode_yolo: Color::LightRed,
457 // Magenta keeps Plan visually distinct from `status_warning` (yellow)
458 // so the mode indicator and warning chip don't collide on themes that
459 // render both in the status row.
460 mode_plan: Color::Magenta,
461 mode_operate: Color::LightCyan,
462 permission_ask: Color::Yellow,
463 permission_auto_review: Color::LightYellow,
464 permission_full_access: Color::LightRed,
465 // DarkGray gives "Ready" a low-contrast but still distinguishable hue
466 // versus default body text (which is `Color::Reset` on this theme).
467 status_ready: Color::DarkGray,
468 status_working: Color::Cyan,
469 status_warning: Color::Yellow,
470 diff_added_fg: Color::Green,
471 diff_deleted_fg: Color::Red,
472 diff_added_bg: Color::Reset,
473 diff_deleted_bg: Color::Reset,
474 tool_running: Color::Cyan,
475 tool_success: Color::Green,
476 tool_failed: Color::Red,
477 };
478
479 pub const GRUVBOX_DARK_UI_THEME: UiTheme = UiTheme {
480 name: "gruvbox-dark",
481 mode: PaletteMode::Dark,
482 surface_bg: Color::Rgb(0x28, 0x28, 0x28), // bg0
483 panel_bg: Color::Rgb(0x3c, 0x38, 0x36), // bg1
484 elevated_bg: Color::Rgb(0x50, 0x49, 0x45), // bg2
485 composer_bg: Color::Rgb(0x3c, 0x38, 0x36),
486 selection_bg: Color::Rgb(0x66, 0x5c, 0x54), // bg3
487 header_bg: Color::Rgb(0x1d, 0x20, 0x21), // bg0_h
488 footer_bg: Color::Rgb(0x1d, 0x20, 0x21),
489 text_dim: Color::Rgb(0x92, 0x83, 0x74), // gray
490 text_hint: Color::Rgb(0xa8, 0x99, 0x84), // fg4
491 text_muted: Color::Rgb(0xc5, 0xb8, 0xa0), // fg3, lifted to 4.5:1 on bg2
492 text_body: Color::Rgb(0xeb, 0xdb, 0xb2), // fg1
493 text_soft: Color::Rgb(0xd5, 0xc4, 0xa1), // fg2
494 border: Color::Rgb(0x66, 0x5c, 0x54), // bg3
495 accent_primary: Color::Rgb(0x83, 0xa5, 0x98), // blue
496 accent_secondary: Color::Rgb(0x8e, 0xc0, 0x7c), // aqua/green
497 accent_action: Color::Rgb(0xfe, 0x80, 0x19), // orange
498 error_fg: Color::Rgb(0xfb, 0x49, 0x34), // red
499 error_hover: Color::Rgb(0xfc, 0x7c, 0x6b),
500 error_surface: Color::Rgb(0x35, 0x1c, 0x18),
501 error_border: Color::Rgb(0xfb, 0x49, 0x34),
502 error_text: Color::Rgb(0xfc, 0xc4, 0xb8),
503 warning: Color::Rgb(0xfa, 0xbd, 0x2f), // yellow
504 success: Color::Rgb(0x8e, 0xc0, 0x7c), // green
505 info: Color::Rgb(0x83, 0xa5, 0x98), // blue
506 mode_agent: Color::Rgb(0x84, 0xa5, 0x98), // blue
507 mode_yolo: Color::Rgb(0xfb, 0x4a, 0x34), // red
508 mode_plan: Color::Rgb(0xfe, 0x81, 0x19), // orange
509 mode_operate: Color::Rgb(0xd3, 0x86, 0x9b), // purple
510 permission_ask: Color::Rgb(0xfa, 0xbd, 0x2f),
511 permission_auto_review: Color::Rgb(0xfe, 0x80, 0x19),
512 permission_full_access: Color::Rgb(0xfb, 0x49, 0x34),
513 status_ready: Color::Rgb(0x92, 0x83, 0x74), // gray
514 status_working: Color::Rgb(0x8e, 0xc0, 0x7c), // aqua
515 status_warning: Color::Rgb(0xfa, 0xbd, 0x2f), // yellow
516 diff_added_fg: Color::Rgb(0x8e, 0xc0, 0x7c), // green
517 diff_deleted_fg: Color::Rgb(0xfb, 0x49, 0x34), // red
518 diff_added_bg: Color::Rgb(0x29, 0x32, 0x16),
519 diff_deleted_bg: Color::Rgb(0x35, 0x1c, 0x18),
520 tool_running: Color::Rgb(0x8e, 0xc0, 0x7c), // aqua
521 tool_success: Color::Rgb(0x92, 0x83, 0x74), // gray
522 tool_failed: Color::Rgb(0xfb, 0x49, 0x34), // red
523 };
524
525 pub const CLAUDE_UI_THEME: UiTheme = UiTheme {
526 name: "claude",
527 mode: PaletteMode::Dark,
528 // Claude Code product surfaces — dark navy with warm undertones
529 surface_bg: Color::Rgb(0x18, 0x17, 0x15), // surface-dark
530 panel_bg: Color::Rgb(0x25, 0x23, 0x20), // surface-dark-elevated
531 elevated_bg: Color::Rgb(0x1f, 0x1e, 0x1b), // surface-dark-soft (code blocks)
532 composer_bg: Color::Rgb(0x25, 0x23, 0x20),
533 selection_bg: Color::Rgb(0x30, 0x2d, 0x28),
534 header_bg: Color::Rgb(0x18, 0x17, 0x15),
535 footer_bg: Color::Rgb(0x18, 0x17, 0x15),
536 // Cream-tinted text hierarchy on dark
537 text_dim: Color::Rgb(0x72, 0x70, 0x6a),
538 text_hint: Color::Rgb(0x7d, 0x7a, 0x73),
539 text_muted: Color::Rgb(0xa0, 0x9d, 0x96), // on-dark-soft
540 text_body: Color::Rgb(0xfa, 0xf9, 0xf5), // on-dark (cream white)
541 text_soft: Color::Rgb(0xd0, 0xcd, 0xc5),
542 border: Color::Rgb(0x30, 0x2d, 0x28),
543 // Coral primary (signature Anthropic accent), teal secondary
544 accent_primary: Color::Rgb(0xcc, 0x78, 0x5c), // coral
545 accent_secondary: Color::Rgb(0x5d, 0xb8, 0xa6), // accent-teal
546 accent_action: Color::Rgb(0xe8, 0xa5, 0x5a), // amber
547 // Error / destructive — warm red
548 error_fg: Color::Rgb(0xe0, 0x60, 0x60),
549 error_hover: Color::Rgb(0xd9, 0x66, 0x66),
550 error_surface: Color::Rgb(0x2a, 0x1c, 0x1c),
551 error_border: Color::Rgb(0xe0, 0x60, 0x60),
552 error_text: Color::Rgb(0xe8, 0xb8, 0xb8),
553 // Status
554 warning: Color::Rgb(0xd4, 0xa0, 0x17), // amber
555 success: Color::Rgb(0x5d, 0xb8, 0x72), // green
556 info: Color::Rgb(0x5d, 0xb8, 0xa6), // teal
557 // Mode badges
558 mode_agent: Color::Rgb(0xcd, 0x78, 0x5c), // coral
559 mode_yolo: Color::Rgb(0xc7, 0x45, 0x45), // red
560 mode_plan: Color::Rgb(0xe8, 0xa6, 0x5a), // amber
561 mode_operate: Color::Rgb(0x8a, 0x63, 0xd2), // violet
562 permission_ask: Color::Rgb(0xd4, 0xa0, 0x17),
563 permission_auto_review: Color::Rgb(0xe8, 0xa5, 0x5a),
564 permission_full_access: Color::Rgb(0xe0, 0x60, 0x60),
565 // Footer statusline
566 status_ready: Color::Rgb(0xa0, 0x9d, 0x96),
567 status_working: Color::Rgb(0x5d, 0xb8, 0xa6),
568 status_warning: Color::Rgb(0xd4, 0xa0, 0x17),
569 // Diff
570 diff_added_fg: Color::Rgb(0x5d, 0xb8, 0x72),
571 diff_deleted_fg: Color::Rgb(0xc6, 0x45, 0x45),
572 diff_added_bg: Color::Rgb(0x1a, 0x24, 0x1d),
573 diff_deleted_bg: Color::Rgb(0x24, 0x1a, 0x1a),
574 // Tool cells
575 tool_running: Color::Rgb(0x5d, 0xb8, 0xa6),
576 tool_success: Color::Rgb(0xa0, 0x9d, 0x96),
577 tool_failed: Color::Rgb(0xc6, 0x45, 0x45),
578 };
579
580 pub const MATRIX_UI_THEME: UiTheme = UiTheme {
581 name: "matrix",
582 mode: PaletteMode::Dark,
583 surface_bg: Color::Rgb(
584 MATRIX_SURFACE_RGB.0,
585 MATRIX_SURFACE_RGB.1,
586 MATRIX_SURFACE_RGB.2,
587 ),
588 panel_bg: Color::Rgb(
589 MATRIX_SURFACE_RGB.0,
590 MATRIX_SURFACE_RGB.1,
591 MATRIX_SURFACE_RGB.2,
592 ),
593 elevated_bg: Color::Rgb(
594 MATRIX_ELEVATED_RGB.0,
595 MATRIX_ELEVATED_RGB.1,
596 MATRIX_ELEVATED_RGB.2,
597 ),
598 composer_bg: Color::Rgb(
599 MATRIX_SURFACE_RGB.0,
600 MATRIX_SURFACE_RGB.1,
601 MATRIX_SURFACE_RGB.2,
602 ),
603 selection_bg: Color::Rgb(
604 MATRIX_SELECTION_RGB.0,
605 MATRIX_SELECTION_RGB.1,
606 MATRIX_SELECTION_RGB.2,
607 ),
608 header_bg: Color::Rgb(
609 MATRIX_SURFACE_RGB.0,
610 MATRIX_SURFACE_RGB.1,
611 MATRIX_SURFACE_RGB.2,
612 ),
613 footer_bg: Color::Rgb(
614 MATRIX_SURFACE_RGB.0,
615 MATRIX_SURFACE_RGB.1,
616 MATRIX_SURFACE_RGB.2,
617 ),
618 text_dim: Color::Rgb(
619 MATRIX_TEXT_DIM_RGB.0,
620 MATRIX_TEXT_DIM_RGB.1,
621 MATRIX_TEXT_DIM_RGB.2,
622 ),
623 text_hint: Color::Rgb(
624 MATRIX_TEXT_HINT_RGB.0,
625 MATRIX_TEXT_HINT_RGB.1,
626 MATRIX_TEXT_HINT_RGB.2,
627 ),
628 text_muted: Color::Rgb(
629 MATRIX_TEXT_MUTED_RGB.0,
630 MATRIX_TEXT_MUTED_RGB.1,
631 MATRIX_TEXT_MUTED_RGB.2,
632 ),
633 text_body: Color::Rgb(
634 MATRIX_TEXT_BODY_RGB.0,
635 MATRIX_TEXT_BODY_RGB.1,
636 MATRIX_TEXT_BODY_RGB.2,
637 ),
638 text_soft: Color::Rgb(
639 MATRIX_TEXT_SOFT_RGB.0,
640 MATRIX_TEXT_SOFT_RGB.1,
641 MATRIX_TEXT_SOFT_RGB.2,
642 ),
643 border: Color::Rgb(
644 MATRIX_BORDER_RGB.0,
645 MATRIX_BORDER_RGB.1,
646 MATRIX_BORDER_RGB.2,
647 ),
648 accent_primary: Color::Rgb(
649 MATRIX_BORDER_RGB.0,
650 MATRIX_BORDER_RGB.1,
651 MATRIX_BORDER_RGB.2,
652 ),
653 accent_secondary: Color::Rgb(0, 153, 0),
654 accent_action: Color::Rgb(0x88, 0xff, 0x88),
655 error_fg: Color::Rgb(0xb4, 0, 0),
656 error_hover: Color::Rgb(0xe0, 0, 0),
657 error_surface: Color::Rgb(0x1a, 0x0d, 0x0d),
658 error_border: Color::Rgb(0xb4, 0, 0),
659 error_text: Color::Rgb(0xff, 0x44, 0x44),
660 warning: Color::Rgb(204, 204, 0),
661 success: Color::Rgb(0x88, 0xff, 0x88),
662 info: Color::Rgb(0, 204, 0),
663 mode_agent: Color::Rgb(0, 154, 0),
664 mode_yolo: Color::Rgb(255, 100, 100),
665 mode_plan: Color::Rgb(255, 170, 60),
666 mode_operate: Color::Rgb(100, 255, 220),
667 permission_ask: Color::Rgb(204, 204, 0),
668 permission_auto_review: Color::Rgb(255, 170, 60),
669 permission_full_access: Color::Rgb(255, 100, 100),
670 status_ready: Color::Rgb(0, 108, 0),
671 status_working: Color::Rgb(
672 MATRIX_TEXT_BODY_RGB.0,
673 MATRIX_TEXT_BODY_RGB.1,
674 MATRIX_TEXT_BODY_RGB.2,
675 ),
676 status_warning: Color::Rgb(204, 204, 0),
677 diff_added_fg: Color::Rgb(0x88, 0xff, 0x88),
678 diff_deleted_fg: Color::Rgb(0xbc, 0x1d, 0x1d), // lifted to 3:1 on diff_deleted_bg
679 diff_added_bg: Color::Rgb(0x0d, 0x1a, 0x0d),
680 diff_deleted_bg: Color::Rgb(0x1a, 0x0d, 0x0d),
681 tool_running: Color::Rgb(0x88, 0xff, 0x88),
682 tool_success: Color::Rgb(0, 102, 0),
683 tool_failed: Color::Rgb(0xb4, 0, 0),
684 };
685
686 pub const UWU_UI_THEME: UiTheme = UiTheme {
687 name: "uwu",
688 mode: PaletteMode::Dark,
689 surface_bg: Color::Rgb(0x16, 0x12, 0x1c),
690 panel_bg: Color::Rgb(0x1f, 0x18, 0x28),
691 elevated_bg: Color::Rgb(0x2c, 0x22, 0x38),
692 composer_bg: Color::Rgb(0x1f, 0x18, 0x28),
693 selection_bg: Color::Rgb(0x3d, 0x2e, 0x4e),
694 header_bg: Color::Rgb(0x12, 0x0e, 0x18),
695 footer_bg: Color::Rgb(0x12, 0x0e, 0x18),
696 text_dim: Color::Rgb(0x6e, 0x60, 0x7a),
697 text_hint: Color::Rgb(0x8a, 0x7c, 0x98),
698 text_muted: Color::Rgb(0xb8, 0xaa, 0xc8),
699 text_body: Color::Rgb(0xf7, 0xf0, 0xf8),
700 text_soft: Color::Rgb(0xe8, 0xdc, 0xee),
701 border: Color::Rgb(0x4a, 0x3a, 0x5c),
702 accent_primary: Color::Rgb(0xff, 0x9e, 0xcd),
703 accent_secondary: Color::Rgb(0x9a, 0xec, 0xe0),
704 accent_action: Color::Rgb(0xff, 0xd6, 0x9a),
705 error_fg: Color::Rgb(0xff, 0x6b, 0x8a),
706 error_hover: Color::Rgb(0xff, 0x8f, 0xa8),
707 error_surface: Color::Rgb(0x3a, 0x18, 0x28),
708 error_border: Color::Rgb(0xff, 0x6b, 0x8a),
709 error_text: Color::Rgb(0xff, 0xc8, 0xd6),
710 warning: Color::Rgb(0xff, 0xe0, 0x8a),
711 success: Color::Rgb(0x9a, 0xef, 0xc0),
712 info: Color::Rgb(0xb8, 0xc8, 0xff),
713 mode_agent: Color::Rgb(0xc4, 0xa8, 0xff),
714 mode_yolo: Color::Rgb(0xff, 0x55, 0x77),
715 mode_plan: Color::Rgb(0xff, 0xb4, 0xe8),
716 mode_operate: Color::Rgb(0x8a, 0xd4, 0xff),
717 permission_ask: Color::Rgb(0xff, 0xe0, 0x8a),
718 permission_auto_review: Color::Rgb(0xff, 0xd6, 0x9a),
719 permission_full_access: Color::Rgb(0xff, 0x6b, 0x8a),
720 status_ready: Color::Rgb(0x8a, 0x7c, 0x98),
721 status_working: Color::Rgb(0x9a, 0xec, 0xe0),
722 status_warning: Color::Rgb(0xff, 0xe0, 0x8a),
723 diff_added_fg: Color::Rgb(0x9a, 0xef, 0xc0),
724 diff_deleted_fg: Color::Rgb(0xff, 0x6b, 0x8a),
725 diff_added_bg: Color::Rgb(0x1a, 0x2a, 0x24),
726 diff_deleted_bg: Color::Rgb(0x3a, 0x18, 0x28),
727 tool_running: Color::Rgb(0x9a, 0xec, 0xe0),
728 tool_success: Color::Rgb(0x8a, 0x7c, 0x98),
729 tool_failed: Color::Rgb(0xff, 0x6b, 0x8a),
730 };
731
732 /// Stable identifiers for the named themes the user can select. `System`
733 /// defers to `PaletteMode::detect()` (terminal-driven dark/light). Each
734 /// dark/light id resolves to a single fixed `UiTheme`.
735 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
736 pub enum ThemeId {
737 System,
738 Terminal,
739 Whale,
740 WhaleLight,
741 Grayscale,
742 CatppuccinMocha,
743 TokyoNight,
744 Dracula,
745 GruvboxDark,
746 Claude,
747 Matrix,
748 SolarizedLight,
749 Uwu,
750 }
751
752 impl ThemeId {
753 /// Parse a settings string (`"system"`, `"dark"`, `"catppuccin-mocha"`, …).
754 /// Accepts a few aliases (`"whale"` for dark, `"light"` for whale-light)
755 /// so existing config files keep working. Case-insensitive.
756 #[must_use]
757 pub fn from_name(value: &str) -> Option<Self> {
758 match normalize_theme_name(value)? {
759 "system" => Some(Self::System),
760 "terminal" => Some(Self::Terminal),
761 "dark" => Some(Self::Whale),
762 "light" => Some(Self::WhaleLight),
763 "grayscale" => Some(Self::Grayscale),
764 "catppuccin-mocha" => Some(Self::CatppuccinMocha),
765 "tokyo-night" => Some(Self::TokyoNight),
766 "dracula" => Some(Self::Dracula),
767 "gruvbox-dark" => Some(Self::GruvboxDark),
768 "claude" => Some(Self::Claude),
769 "matrix" => Some(Self::Matrix),
770 "solarized-light" => Some(Self::SolarizedLight),
771 "uwu" => Some(Self::Uwu),
772 _ => None,
773 }
774 }
775
776 /// Canonical settings string (lowercase, dash-separated). Round-trips
777 /// through `from_name`.
778 #[must_use]
779 pub const fn name(self) -> &'static str {
780 match self {
781 Self::System => "system",
782 Self::Terminal => "terminal",
783 Self::Whale => "dark",
784 Self::WhaleLight => "light",
785 Self::Grayscale => "grayscale",
786 Self::CatppuccinMocha => "catppuccin-mocha",
787 Self::TokyoNight => "tokyo-night",
788 Self::Dracula => "dracula",
789 Self::GruvboxDark => "gruvbox-dark",
790 Self::Claude => "claude",
791 Self::Matrix => "matrix",
792 Self::SolarizedLight => "solarized-light",
793 Self::Uwu => "uwu",
794 }
795 }
796
797 /// Human-readable label for picker rows.
798 #[must_use]
799 pub const fn display_name(self) -> &'static str {
800 match self {
801 Self::System => "System",
802 Self::Terminal => "Terminal",
803 Self::Whale => "Blue Stage",
804 Self::WhaleLight => "Blue Stage Light",
805 Self::Grayscale => "Grayscale",
806 Self::CatppuccinMocha => "Catppuccin Mocha",
807 Self::TokyoNight => "Tokyo Night",
808 Self::Dracula => "Dracula",
809 Self::GruvboxDark => "Gruvbox Dark",
810 Self::Claude => "Claude",
811 Self::Matrix => "Matrix",
812 Self::SolarizedLight => "Solarized Light",
813 Self::Uwu => "Uwu",
814 }
815 }
816
817 /// Short tagline for picker rows.
818 #[must_use]
819 pub const fn tagline(self) -> &'static str {
820 match self {
821 Self::System => "Follow terminal background (COLORFGBG / macOS appearance)",
822 Self::Terminal => "Inherit terminal colors fully (transparent surfaces, ANSI accents)",
823 Self::Whale => "Stage black, action blue, and one Signal Gold human beacon",
824 Self::WhaleLight => "Paper, cobalt action, and one Signal Gold human beacon",
825 Self::Grayscale => "Color-minimal high contrast",
826 Self::CatppuccinMocha => "Soft pastels on warm dark",
827 Self::TokyoNight => "Deep blue/violet night palette",
828 Self::Dracula => "Classic high-contrast purple",
829 Self::GruvboxDark => "Vintage warm earth tones",
830 Self::Claude => "Warm navy & coral",
831 Self::Matrix => "The Matrix films inspired theme",
832 Self::SolarizedLight => {
833 "Solarized light — Light, calming palette on warm ivory — easy on the eyes"
834 }
835 Self::Uwu => "Soft kawaii night — sakura, mint, and peach",
836 }
837 }
838
839 /// Resolve to a concrete `UiTheme`. For `System` this consults
840 /// `PaletteMode::detect()` exactly once and returns the corresponding
841 /// dark/light theme — callers that want to live-track terminal background
842 /// changes need to re-invoke this.
843 #[must_use]
844 pub fn ui_theme(self) -> UiTheme {
845 match self {
846 Self::System => UiTheme::detect(),
847 Self::Terminal => TERMINAL_UI_THEME,
848 Self::Whale => UI_THEME,
849 Self::WhaleLight => LIGHT_UI_THEME,
850 Self::Grayscale => GRAYSCALE_UI_THEME,
851 Self::CatppuccinMocha => CATPPUCCIN_MOCHA_UI_THEME,
852 Self::TokyoNight => TOKYO_NIGHT_UI_THEME,
853 Self::Dracula => DRACULA_UI_THEME,
854 Self::GruvboxDark => GRUVBOX_DARK_UI_THEME,
855 Self::Claude => CLAUDE_UI_THEME,
856 Self::Matrix => MATRIX_UI_THEME,
857 Self::SolarizedLight => SOLARIZED_LIGHT_UI_THEME,
858 Self::Uwu => UWU_UI_THEME,
859 }
860 }
861 }
862
863 /// Themes shown in the `/theme` picker, in display order.
864 pub const SELECTABLE_THEMES: &[ThemeId] = &[
865 ThemeId::System,
866 ThemeId::Terminal,
867 ThemeId::Whale,
868 ThemeId::WhaleLight,
869 ThemeId::Grayscale,
870 ThemeId::CatppuccinMocha,
871 ThemeId::TokyoNight,
872 ThemeId::Dracula,
873 ThemeId::GruvboxDark,
874 ThemeId::Claude,
875 ThemeId::Matrix,
876 ThemeId::SolarizedLight,
877 ThemeId::Uwu,
878 ];
879
880 impl UiTheme {
881 #[must_use]
882 pub fn for_mode(mode: PaletteMode) -> Self {
883 match mode {
884 PaletteMode::Dark => UI_THEME,
885 PaletteMode::Light => LIGHT_UI_THEME,
886 PaletteMode::Grayscale => GRAYSCALE_UI_THEME,
887 PaletteMode::SolarizedLight => SOLARIZED_LIGHT_UI_THEME,
888 }
889 }
890
891 #[must_use]
892 pub fn detect() -> Self {
893 Self::for_mode(PaletteMode::detect())
894 }
895
896 #[must_use]
897 pub fn with_background_color(mut self, color: Color) -> Self {
898 self.surface_bg = color;
899 self.header_bg = color;
900 self.footer_bg = color;
901 self
902 }
903 }
904
905 #[must_use]
906 pub fn normalize_theme_name(value: &str) -> Option<&'static str> {
907 match value.trim().to_ascii_lowercase().as_str() {
908 "" | "auto" | "system" | "default" => Some("system"),
909 "terminal" | "term" | "transparent" | "follow-terminal" | "inherit" => Some("terminal"),
910 "dark" | "whale" | "whale-dark" => Some("dark"),
911 "light" | "whale-light" => Some("light"),
912 "grayscale" | "greyscale" | "gray" | "grey" | "mono" | "monochrome" | "black-white"
913 | "black_and_white" | "blackwhite" | "bw" | "b&w" => Some("grayscale"),
914 "catppuccin-mocha" | "catppuccin" | "mocha" => Some("catppuccin-mocha"),
915 "tokyo-night" | "tokyonight" | "tokyo" => Some("tokyo-night"),
916 "dracula" => Some("dracula"),
917 "gruvbox-dark" | "gruvbox" => Some("gruvbox-dark"),
918 "claude" => Some("claude"),
919 "matrix" | "hacker" => Some("matrix"),
920 "solarized-light" | "solarized" => Some("solarized-light"),
921 "uwu" | "owo" | "kawaii" => Some("uwu"),
922 _ => None,
923 }
924 }
925
926 #[must_use]
927 pub fn theme_label_for_mode(mode: PaletteMode) -> &'static str {
928 match mode {
929 PaletteMode::Dark => "dark",
930 PaletteMode::Light => "light",
931 PaletteMode::Grayscale => "grayscale",
932 PaletteMode::SolarizedLight => "solarized-light",
933 }
934 }
935
936 #[must_use]
937 #[cfg_attr(not(test), allow(dead_code))]
938 pub fn ui_theme_from_settings(theme: &str, background_color: Option<&str>) -> UiTheme {
939 super::resolve_theme_setting(theme, background_color)
940 .map(|(_, _, theme)| theme)
941 .unwrap_or_else(|_| UiTheme::detect())
942 }
943
944 #[must_use]
945 pub fn parse_hex_rgb_color(value: &str) -> Option<Color> {
946 let hex = value.trim().strip_prefix('#').unwrap_or(value.trim());
947 if hex.len() != 6 || !hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
948 return None;
949 }
950
951 let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
952 let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
953 let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
954 Some(Color::Rgb(r, g, b))
955 }
956
957 #[must_use]
958 pub fn normalize_hex_rgb_color(value: &str) -> Option<String> {
959 hex_rgb_string(parse_hex_rgb_color(value)?)
960 }
961
962 #[must_use]
963 pub fn hex_rgb_string(color: Color) -> Option<String> {
964 let Color::Rgb(r, g, b) = color else {
965 return None;
966 };
967 Some(format!("#{r:02x}{g:02x}{b:02x}"))
968 }
969
970 #[cfg(test)]
971 mod tests {
972 use super::*;
973
974 #[test]
975 fn whale_refresh_keeps_theme_ids_and_default_compatibility() {
976 let names = SELECTABLE_THEMES
977 .iter()
978 .map(|theme| theme.name())
979 .collect::<Vec<_>>();
980 assert_eq!(
981 names,
982 [
983 "system",
984 "terminal",
985 "dark",
986 "light",
987 "grayscale",
988 "catppuccin-mocha",
989 "tokyo-night",
990 "dracula",
991 "gruvbox-dark",
992 "claude",
993 "matrix",
994 "solarized-light",
995 "uwu",
996 ]
997 );
998 assert_eq!(normalize_theme_name("default"), Some("system"));
999 assert_eq!(normalize_theme_name("whale"), Some("dark"));
1000 assert_eq!(normalize_theme_name("owo"), Some("uwu"));
1001 assert_eq!(normalize_theme_name("kawaii"), Some("uwu"));
1002 }
1003
1004 #[test]
1005 fn whale_pair_uses_codewhale_semantic_grammar() {
1006 assert_eq!(UI_THEME.accent_primary, WHALE_ACTION);
1007 assert_eq!(UI_THEME.status_working, WHALE_LIVE);
1008 assert_eq!(UI_THEME.accent_action, WHALE_HUMAN);
1009 assert_eq!(UI_THEME.warning, STATUS_WARNING);
1010 assert_eq!(UI_THEME.error_fg, WHALE_ERROR);
1011 assert_eq!(UI_THEME.mode_operate, MODE_OPERATE);
1012 assert_eq!(LIGHT_UI_THEME.accent_primary, LIGHT_ACTION);
1013 assert_eq!(LIGHT_UI_THEME.status_working, LIGHT_LIVE);
1014 assert_eq!(LIGHT_UI_THEME.accent_action, LIGHT_HUMAN);
1015 assert_eq!(LIGHT_UI_THEME.warning, LIGHT_WARNING);
1016 assert_eq!(LIGHT_UI_THEME.error_fg, LIGHT_DANGER);
1017 assert_eq!(LIGHT_UI_THEME.mode_operate, LIGHT_OPERATE);
1018 }
1019
1020 /// Dogfood A7 (#4092): every mode must be tellable apart from the footer
1021 /// badge alone — Operate must never wear the full-access red again. Mode
1022 /// slots also stay distinct from general semantic lanes so limited-color
1023 /// adaptation can identify direct `UiTheme` call sites without guessing.
1024 #[test]
1025 fn every_selectable_theme_keeps_mode_badges_distinct() {
1026 for theme_id in SELECTABLE_THEMES {
1027 let ui = theme_id.ui_theme();
1028 let badges = [
1029 ("act", ui.mode_agent),
1030 ("plan", ui.mode_plan),
1031 ("operate", ui.mode_operate),
1032 ("full access", ui.mode_yolo),
1033 ];
1034 for (i, (name_a, color_a)) in badges.iter().enumerate() {
1035 for (name_b, color_b) in badges.iter().skip(i + 1) {
1036 assert_ne!(
1037 color_a,
1038 color_b,
1039 "theme '{}' renders modes '{name_a}' and '{name_b}' with the same badge color",
1040 theme_id.name(),
1041 );
1042 }
1043 }
1044 let semantic_lanes = [
1045 ("action", ui.accent_primary),
1046 ("live", ui.status_working),
1047 ("human", ui.accent_action),
1048 ("warning", ui.warning),
1049 ("danger", ui.error_fg),
1050 ("success", ui.success),
1051 ];
1052 for (mode_name, mode_color) in badges {
1053 for (lane_name, lane_color) in semantic_lanes {
1054 assert_ne!(
1055 mode_color,
1056 lane_color,
1057 "theme '{}' reuses the {lane_name} color for mode '{mode_name}', erasing render-stage identity",
1058 theme_id.name(),
1059 );
1060 }
1061 }
1062 }
1063 }
1064
1065 #[test]
1066 fn every_selectable_theme_keeps_permission_lanes_distinct() {
1067 for theme_id in SELECTABLE_THEMES {
1068 let ui = theme_id.ui_theme();
1069 let permissions = [
1070 ("ask", ui.permission_ask),
1071 ("auto-review", ui.permission_auto_review),
1072 ("full-access", ui.permission_full_access),
1073 ];
1074 for (index, (name_a, color_a)) in permissions.iter().enumerate() {
1075 for (name_b, color_b) in permissions.iter().skip(index + 1) {
1076 assert_ne!(
1077 color_a,
1078 color_b,
1079 "theme '{}' renders permission lanes '{name_a}' and '{name_b}' identically",
1080 theme_id.name(),
1081 );
1082 }
1083 }
1084 }
1085 }
1086
1087 #[test]
1088 fn every_selectable_theme_separates_live_workers_from_completed_work() {
1089 for theme_id in SELECTABLE_THEMES {
1090 let ui = theme_id.ui_theme();
1091 assert_ne!(
1092 ui.info,
1093 ui.success,
1094 "theme '{}' makes live workers indistinguishable from completed work",
1095 theme_id.name(),
1096 );
1097 }
1098 }
1099 }
1100
1100 lines RUST