1use ftui::Style;
11use ftui::render::cell::PackedRgba;
12
13pub mod colors {
16 use ftui::render::cell::PackedRgba as Color;
17
18 pub const BG_DEEP: Color = Color::rgb(26, 27, 38); pub const BG_SURFACE: Color = Color::rgb(36, 40, 59); pub const BG_HIGHLIGHT: Color = Color::rgb(41, 46, 66); pub const BORDER: Color = Color::rgb(59, 66, 97); pub const BORDER_FOCUS: Color = Color::rgb(125, 145, 200); pub const TEXT_PRIMARY: Color = Color::rgb(192, 202, 245); pub const TEXT_SECONDARY: Color = Color::rgb(169, 177, 214); pub const TEXT_MUTED: Color = Color::rgb(105, 114, 158); pub const TEXT_DISABLED: Color = Color::rgb(68, 75, 106); pub const ACCENT_PRIMARY: Color = Color::rgb(122, 162, 247); pub const ACCENT_SECONDARY: Color = Color::rgb(187, 154, 247); pub const ACCENT_TERTIARY: Color = Color::rgb(125, 207, 255); pub const ROLE_USER: Color = Color::rgb(158, 206, 106); pub const ROLE_AGENT: Color = Color::rgb(122, 162, 247); pub const ROLE_TOOL: Color = Color::rgb(255, 158, 100); pub const ROLE_SYSTEM: Color = Color::rgb(224, 175, 104); pub const STATUS_SUCCESS: Color = Color::rgb(115, 218, 202); pub const STATUS_WARNING: Color = Color::rgb(224, 175, 104); pub const STATUS_ERROR: Color = Color::rgb(247, 118, 142); pub const STATUS_INFO: Color = Color::rgb(125, 207, 255); pub const AGENT_CLAUDE_BG: Color = Color::rgb(24, 30, 52); pub const AGENT_CODEX_BG: Color = Color::rgb(22, 38, 32); pub const AGENT_CLINE_BG: Color = Color::rgb(20, 34, 42); pub const AGENT_GEMINI_BG: Color = Color::rgb(34, 24, 48); pub const AGENT_AMP_BG: Color = Color::rgb(42, 28, 24); pub const AGENT_AIDER_BG: Color = Color::rgb(20, 36, 36); pub const AGENT_CURSOR_BG: Color = Color::rgb(38, 24, 38); pub const AGENT_CHATGPT_BG: Color = Color::rgb(20, 38, 28); pub const AGENT_OPENCODE_BG: Color = Color::rgb(32, 32, 36); pub const AGENT_FACTORY_BG: Color = Color::rgb(36, 30, 20); pub const AGENT_CLAWDBOT_BG: Color = Color::rgb(26, 24, 44); pub const AGENT_VIBE_BG: Color = Color::rgb(36, 22, 30); pub const AGENT_OPENCLAW_BG: Color = Color::rgb(24, 30, 34); pub const AGENT_COPILOT_BG: Color = Color::rgb(18, 38, 34); pub const AGENT_COPILOT_CLI_BG: Color = Color::rgb(20, 32, 44); pub const AGENT_CRUSH_BG: Color = Color::rgb(42, 22, 32); pub const AGENT_KIMI_BG: Color = Color::rgb(30, 24, 50); pub const AGENT_QWEN_BG: Color = Color::rgb(24, 36, 24); pub const AGENT_HERMES_BG: Color = Color::rgb(40, 34, 18); pub const ROLE_USER_BG: Color = Color::rgb(26, 32, 30); pub const ROLE_AGENT_BG: Color = Color::rgb(26, 28, 36); pub const ROLE_TOOL_BG: Color = Color::rgb(32, 28, 26); pub const ROLE_SYSTEM_BG: Color = Color::rgb(32, 30, 26); pub const GRADIENT_HEADER_TOP: Color = Color::rgb(22, 24, 32); pub const GRADIENT_HEADER_MID: Color = Color::rgb(30, 32, 44); pub const GRADIENT_HEADER_BOT: Color = Color::rgb(36, 40, 54); pub const GRADIENT_PILL_LEFT: Color = Color::rgb(50, 56, 80); pub const GRADIENT_PILL_CENTER: Color = Color::rgb(60, 68, 96); pub const GRADIENT_PILL_RIGHT: Color = Color::rgb(50, 56, 80); pub const BORDER_MINIMAL: Color = Color::rgb(45, 50, 72); pub const BORDER_STANDARD: Color = Color::rgb(59, 66, 97); pub const BORDER_EMPHASIZED: Color = Color::rgb(75, 85, 120); }
211
212#[derive(Clone, Copy)]
214pub struct RoleTheme {
215 pub fg: PackedRgba,
217 pub bg: PackedRgba,
219 pub border: PackedRgba,
221 pub badge: PackedRgba,
223}
224
225#[derive(Clone, Copy)]
227pub struct GradientShades {
228 pub dark: PackedRgba,
230 pub mid: PackedRgba,
232 pub light: PackedRgba,
234}
235
236impl GradientShades {
237 pub fn header() -> Self {
239 Self {
240 dark: colors::GRADIENT_HEADER_TOP,
241 mid: colors::GRADIENT_HEADER_MID,
242 light: colors::GRADIENT_HEADER_BOT,
243 }
244 }
245
246 pub fn pill() -> Self {
248 Self {
249 dark: colors::GRADIENT_PILL_LEFT,
250 mid: colors::GRADIENT_PILL_CENTER,
251 light: colors::GRADIENT_PILL_RIGHT,
252 }
253 }
254
255 pub fn styles(&self) -> (Style, Style, Style) {
257 (
258 Style::new().bg(self.dark),
259 Style::new().bg(self.mid),
260 Style::new().bg(self.light),
261 )
262 }
263}
264
265#[derive(Clone, Copy, Debug, PartialEq, Eq)]
267pub enum TerminalWidth {
268 Narrow,
270 Normal,
272 Wide,
274}
275
276impl TerminalWidth {
277 pub fn from_cols(cols: u16) -> Self {
279 if cols < 80 {
280 Self::Narrow
281 } else if cols <= 120 {
282 Self::Normal
283 } else {
284 Self::Wide
285 }
286 }
287
288 pub fn border_color(self) -> PackedRgba {
290 match self {
291 Self::Narrow => colors::BORDER_MINIMAL,
292 Self::Normal => colors::BORDER_STANDARD,
293 Self::Wide => colors::BORDER_EMPHASIZED,
294 }
295 }
296
297 pub fn border_style(self) -> Style {
299 Style::new().fg(self.border_color())
300 }
301
302 pub fn show_decorations(self) -> bool {
304 !matches!(self, Self::Narrow)
305 }
306
307 pub fn show_extended_info(self) -> bool {
309 matches!(self, Self::Wide)
310 }
311}
312
313#[derive(Clone, Copy)]
315pub struct AdaptiveBorders {
316 pub width_class: TerminalWidth,
318 pub color: PackedRgba,
320 pub style: Style,
322 pub use_double: bool,
324 pub show_corners: bool,
326}
327
328impl AdaptiveBorders {
329 pub fn for_width(cols: u16) -> Self {
331 let width_class = TerminalWidth::from_cols(cols);
332 let color = width_class.border_color();
333 Self {
334 width_class,
335 color,
336 style: Style::new().fg(color),
337 use_double: matches!(width_class, TerminalWidth::Wide),
338 show_corners: width_class.show_decorations(),
339 }
340 }
341
342 pub fn focused(cols: u16) -> Self {
344 let mut borders = Self::for_width(cols);
345 borders.color = colors::BORDER_FOCUS;
346 borders.style = Style::new().fg(colors::BORDER_FOCUS);
347 borders
348 }
349}
350
351#[derive(Clone, Copy)]
352pub struct PaneTheme {
353 pub bg: PackedRgba,
354 pub fg: PackedRgba,
355 pub accent: PackedRgba,
356}
357
358#[derive(Clone, Copy)]
359pub struct ThemePalette {
360 pub accent: PackedRgba,
361 pub accent_alt: PackedRgba,
362 pub bg: PackedRgba,
363 pub fg: PackedRgba,
364 pub surface: PackedRgba,
365 pub hint: PackedRgba,
366 pub border: PackedRgba,
367 pub user: PackedRgba,
368 pub agent: PackedRgba,
369 pub tool: PackedRgba,
370 pub system: PackedRgba,
371 pub stripe_even: PackedRgba,
373 pub stripe_odd: PackedRgba,
374}
375
376impl ThemePalette {
377 pub fn light() -> Self {
379 Self {
380 accent: PackedRgba::rgb(47, 107, 231), accent_alt: PackedRgba::rgb(124, 93, 198), bg: PackedRgba::rgb(250, 250, 252), fg: PackedRgba::rgb(36, 41, 46), surface: PackedRgba::rgb(240, 241, 245), hint: PackedRgba::rgb(125, 134, 144), border: PackedRgba::rgb(216, 222, 228), user: PackedRgba::rgb(45, 138, 72), agent: PackedRgba::rgb(47, 107, 231), tool: PackedRgba::rgb(207, 107, 44), system: PackedRgba::rgb(177, 133, 41), stripe_even: PackedRgba::rgb(250, 250, 252), stripe_odd: PackedRgba::rgb(240, 241, 245), }
394 }
395
396 pub fn dark() -> Self {
398 Self {
399 accent: colors::ACCENT_PRIMARY,
400 accent_alt: colors::ACCENT_SECONDARY,
401 bg: colors::BG_DEEP,
402 fg: colors::TEXT_PRIMARY,
403 surface: colors::BG_SURFACE,
404 hint: colors::TEXT_MUTED,
405 border: colors::BORDER,
406 user: colors::ROLE_USER,
407 agent: colors::ROLE_AGENT,
408 tool: colors::ROLE_TOOL,
409 system: colors::ROLE_SYSTEM,
410 stripe_even: colors::BG_DEEP, stripe_odd: PackedRgba::rgb(30, 32, 48), }
413 }
414
415 pub fn title(self) -> Style {
417 Style::new().fg(self.accent).bold()
418 }
419
420 pub fn title_subtle(self) -> Style {
422 Style::new().fg(self.fg).bold()
423 }
424
425 pub fn hint_style(self) -> Style {
427 Style::new().fg(self.hint)
428 }
429
430 pub fn border_style(self) -> Style {
432 Style::new().fg(self.border)
433 }
434
435 pub fn border_focus_style(self) -> Style {
437 Style::new().fg(self.accent)
438 }
439
440 pub fn surface_style(self) -> Style {
442 Style::new().bg(self.surface)
443 }
444
445 pub fn agent_pane(agent: &str) -> PaneTheme {
451 let slug = agent.to_lowercase().replace('-', "_");
452
453 let (bg, accent) = match slug.as_str() {
454 "claude_code" | "claude" => (colors::AGENT_CLAUDE_BG, colors::ACCENT_PRIMARY), "codex" => (colors::AGENT_CODEX_BG, colors::STATUS_SUCCESS), "cline" => (colors::AGENT_CLINE_BG, colors::ACCENT_TERTIARY), "gemini" | "gemini_cli" => (colors::AGENT_GEMINI_BG, colors::ACCENT_SECONDARY), "amp" => (colors::AGENT_AMP_BG, colors::STATUS_ERROR), "aider" => (colors::AGENT_AIDER_BG, PackedRgba::rgb(64, 224, 208)), "cursor" => (colors::AGENT_CURSOR_BG, PackedRgba::rgb(236, 72, 153)), "chatgpt" => (colors::AGENT_CHATGPT_BG, PackedRgba::rgb(16, 163, 127)), "opencode" => (colors::AGENT_OPENCODE_BG, colors::ROLE_USER), "pi_agent" => (colors::AGENT_CODEX_BG, PackedRgba::rgb(255, 140, 0)), "factory" | "droid" => (colors::AGENT_FACTORY_BG, PackedRgba::rgb(230, 176, 60)), "clawdbot" => (colors::AGENT_CLAWDBOT_BG, PackedRgba::rgb(140, 130, 240)), "vibe" | "mistral" => (colors::AGENT_VIBE_BG, PackedRgba::rgb(220, 100, 160)), "openclaw" => (colors::AGENT_OPENCLAW_BG, PackedRgba::rgb(130, 190, 210)), "copilot" => (colors::AGENT_COPILOT_BG, PackedRgba::rgb(92, 200, 120)), "copilot_cli" => (colors::AGENT_COPILOT_CLI_BG, PackedRgba::rgb(80, 170, 230)), "crush" => (colors::AGENT_CRUSH_BG, PackedRgba::rgb(255, 120, 80)), "hermes" => (colors::AGENT_HERMES_BG, PackedRgba::rgb(240, 200, 100)), "kimi" => (colors::AGENT_KIMI_BG, PackedRgba::rgb(190, 220, 80)), "qwen" => (colors::AGENT_QWEN_BG, PackedRgba::rgb(80, 210, 180)), _ => (colors::BG_DEEP, colors::ACCENT_PRIMARY),
476 };
477
478 PaneTheme {
479 bg,
480 fg: colors::TEXT_PRIMARY, accent,
482 }
483 }
484
485 pub fn agent_icon(agent: &str) -> &'static str {
489 let slug = agent.to_lowercase().replace('-', "_");
490 match slug.as_str() {
491 "codex" => "◆",
492 "claude_code" | "claude" => "●",
493 "gemini" | "gemini_cli" => "◇",
494 "cline" => "■",
495 "amp" => "▲",
496 "aider" => "▼",
497 "cursor" => "◈",
498 "chatgpt" => "○",
499 "opencode" => "□",
500 "pi_agent" => "△",
501 "factory" | "droid" => "▣",
502 "clawdbot" => "⬢",
503 "vibe" | "mistral" => "✦",
504 "openclaw" => "⬡",
505 "copilot" => "◐",
506 "copilot_cli" => "◑",
507 "crush" => "✚",
508 "hermes" => "▽",
509 "kimi" => "✧",
510 "qwen" => "◒",
511 _ => "•",
512 }
513 }
514
515 pub fn role_style(self, role: &str) -> Style {
517 let color = match role.to_lowercase().as_str() {
518 "user" => self.user,
519 "assistant" | "agent" => self.agent,
520 "tool" => self.tool,
521 "system" => self.system,
522 _ => self.hint,
523 };
524 Style::new().fg(color)
525 }
526
527 pub fn role_theme(self, role: &str) -> RoleTheme {
532 match role.to_lowercase().as_str() {
533 "user" => RoleTheme {
534 fg: self.user,
535 bg: colors::ROLE_USER_BG,
536 border: self.user,
537 badge: colors::STATUS_SUCCESS,
538 },
539 "assistant" | "agent" => RoleTheme {
540 fg: self.agent,
541 bg: colors::ROLE_AGENT_BG,
542 border: self.agent,
543 badge: colors::ACCENT_PRIMARY,
544 },
545 "tool" => RoleTheme {
546 fg: self.tool,
547 bg: colors::ROLE_TOOL_BG,
548 border: self.tool,
549 badge: colors::ROLE_TOOL,
550 },
551 "system" => RoleTheme {
552 fg: self.system,
553 bg: colors::ROLE_SYSTEM_BG,
554 border: self.system,
555 badge: colors::STATUS_WARNING,
556 },
557 _ => RoleTheme {
558 fg: self.hint,
559 bg: self.bg,
560 border: self.border,
561 badge: self.hint,
562 },
563 }
564 }
565
566 pub fn header_gradient(&self) -> GradientShades {
568 GradientShades::header()
569 }
570
571 pub fn pill_gradient(&self) -> GradientShades {
573 GradientShades::pill()
574 }
575
576 pub fn adaptive_borders(&self, cols: u16) -> AdaptiveBorders {
578 AdaptiveBorders::for_width(cols)
579 }
580
581 pub fn adaptive_borders_focused(&self, cols: u16) -> AdaptiveBorders {
583 AdaptiveBorders::focused(cols)
584 }
585
586 pub fn highlight_style(self) -> Style {
589 Style::new()
590 .fg(self.bg) .bg(self.accent) .bold()
593 }
594
595 pub fn selected_style(self) -> Style {
597 Style::new().bg(self.surface).bold()
598 }
599
600 pub fn code_style(self) -> Style {
602 Style::new().bg(self.surface).fg(self.hint)
603 }
604}
605
606pub fn chip_style(palette: ThemePalette) -> Style {
612 Style::new().fg(palette.accent_alt).bold()
613}
614
615pub fn kbd_style(palette: ThemePalette) -> Style {
617 Style::new().fg(palette.accent).bold()
618}
619
620pub fn score_style(score: f32, palette: ThemePalette) -> Style {
622 let color = if score >= 8.0 {
623 colors::STATUS_SUCCESS
624 } else if score >= 5.0 {
625 palette.accent
626 } else {
627 palette.hint
628 };
629
630 let base = Style::new().fg(color);
631 if score >= 8.0 {
632 base.bold()
633 } else if score < 5.0 {
634 base.dim()
635 } else {
636 base
637 }
638}
639
640pub fn relative_luminance(color: PackedRgba) -> f64 {
647 let (r, g, b) = (color.r(), color.g(), color.b());
648
649 fn linearize(c: u8) -> f64 {
650 let c = f64::from(c) / 255.0;
651 if c <= 0.04045 {
652 c / 12.92
653 } else {
654 ((c + 0.055) / 1.055).powf(2.4)
655 }
656 }
657
658 let r_lin = linearize(r);
659 let g_lin = linearize(g);
660 let b_lin = linearize(b);
661
662 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin
663}
664
665pub fn contrast_ratio(fg: PackedRgba, bg: PackedRgba) -> f64 {
668 let lum_fg = relative_luminance(fg);
669 let lum_bg = relative_luminance(bg);
670 let (lighter, darker) = if lum_fg > lum_bg {
671 (lum_fg, lum_bg)
672 } else {
673 (lum_bg, lum_fg)
674 };
675 (lighter + 0.05) / (darker + 0.05)
676}
677
678#[derive(Clone, Copy, Debug, PartialEq, Eq)]
680pub enum ContrastLevel {
681 Fail,
683 AALarge,
685 AA,
687 AAALarge,
689 AAA,
691}
692
693impl ContrastLevel {
694 pub fn from_ratio(ratio: f64) -> Self {
696 if ratio >= 7.0 {
697 Self::AAA
698 } else if ratio >= 4.5 {
699 Self::AA
700 } else if ratio >= 3.0 {
701 Self::AALarge
702 } else {
703 Self::Fail
704 }
705 }
706
707 pub fn meets(self, required: ContrastLevel) -> bool {
709 match required {
710 Self::Fail => true,
711 Self::AALarge => !matches!(self, Self::Fail),
712 Self::AA | Self::AAALarge => matches!(self, Self::AA | Self::AAALarge | Self::AAA),
713 Self::AAA => matches!(self, Self::AAA),
714 }
715 }
716
717 pub fn name(self) -> &'static str {
719 match self {
720 Self::Fail => "Fail",
721 Self::AALarge => "AA (large text)",
722 Self::AA => "AA",
723 Self::AAALarge => "AAA (large text)",
724 Self::AAA => "AAA",
725 }
726 }
727}
728
729pub fn check_contrast(fg: PackedRgba, bg: PackedRgba) -> ContrastLevel {
731 ContrastLevel::from_ratio(contrast_ratio(fg, bg))
732}
733
734pub fn ensure_contrast(fg: PackedRgba, bg: PackedRgba, min_level: ContrastLevel) -> PackedRgba {
737 let level = check_contrast(fg, bg);
738 if level.meets(min_level) {
739 return fg;
740 }
741
742 let bg_lum = relative_luminance(bg);
744 if bg_lum > 0.5 {
745 PackedRgba::BLACK
747 } else {
748 PackedRgba::WHITE
750 }
751}
752
753#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
759pub enum ThemePreset {
760 #[default]
762 TokyoNight,
763 Daylight,
765 Catppuccin,
767 Dracula,
769 Nord,
771 SolarizedDark,
773 SolarizedLight,
775 Monokai,
777 GruvboxDark,
779 OneDark,
781 RosePine,
783 Everforest,
785 Kanagawa,
787 AyuMirage,
789 Nightfox,
791 CyberpunkAurora,
793 Synthwave84,
795 HighContrast,
797 Colorblind,
800}
801
802impl ThemePreset {
803 const ALL: [Self; 19] = [
804 Self::TokyoNight,
805 Self::Daylight,
806 Self::Catppuccin,
807 Self::Dracula,
808 Self::Nord,
809 Self::SolarizedDark,
810 Self::SolarizedLight,
811 Self::Monokai,
812 Self::GruvboxDark,
813 Self::OneDark,
814 Self::RosePine,
815 Self::Everforest,
816 Self::Kanagawa,
817 Self::AyuMirage,
818 Self::Nightfox,
819 Self::CyberpunkAurora,
820 Self::Synthwave84,
821 Self::HighContrast,
822 Self::Colorblind,
823 ];
824
825 pub fn name(self) -> &'static str {
827 match self {
828 Self::TokyoNight => "Tokyo Night",
829 Self::Daylight => "Daylight",
830 Self::Catppuccin => "Catppuccin Mocha",
831 Self::Dracula => "Dracula",
832 Self::Nord => "Nord",
833 Self::SolarizedDark => "Solarized Dark",
834 Self::SolarizedLight => "Solarized Light",
835 Self::Monokai => "Monokai",
836 Self::GruvboxDark => "Gruvbox Dark",
837 Self::OneDark => "One Dark",
838 Self::RosePine => "Ros\u{e9} Pine",
839 Self::Everforest => "Everforest",
840 Self::Kanagawa => "Kanagawa",
841 Self::AyuMirage => "Ayu Mirage",
842 Self::Nightfox => "Nightfox",
843 Self::CyberpunkAurora => "Cyberpunk Aurora",
844 Self::Synthwave84 => "Synthwave '84",
845 Self::HighContrast => "High Contrast",
846 Self::Colorblind => "Colorblind",
847 }
848 }
849
850 pub fn next(self) -> Self {
852 let idx = Self::ALL.iter().position(|p| *p == self).unwrap_or(0);
853 Self::ALL[(idx + 1) % Self::ALL.len()]
854 }
855
856 pub fn prev(self) -> Self {
858 let idx = Self::ALL.iter().position(|p| *p == self).unwrap_or(0);
859 Self::ALL[(idx + Self::ALL.len() - 1) % Self::ALL.len()]
860 }
861
862 pub fn to_palette(self) -> ThemePalette {
864 match self {
865 Self::TokyoNight => ThemePalette::dark(),
866 Self::Daylight => ThemePalette::light(),
867 Self::Catppuccin => ThemePalette::catppuccin(),
868 Self::Dracula => ThemePalette::dracula(),
869 Self::Nord => ThemePalette::nord(),
870 Self::SolarizedDark => ThemePalette::solarized_dark(),
871 Self::SolarizedLight => ThemePalette::solarized_light(),
872 Self::Monokai => ThemePalette::monokai(),
873 Self::GruvboxDark => ThemePalette::gruvbox_dark(),
874 Self::OneDark => ThemePalette::one_dark(),
875 Self::RosePine => ThemePalette::rose_pine(),
876 Self::Everforest => ThemePalette::everforest(),
877 Self::Kanagawa => ThemePalette::kanagawa(),
878 Self::AyuMirage => ThemePalette::ayu_mirage(),
879 Self::Nightfox => ThemePalette::nightfox(),
880 Self::CyberpunkAurora => ThemePalette::cyberpunk_aurora(),
881 Self::Synthwave84 => ThemePalette::synthwave_84(),
882 Self::HighContrast => ThemePalette::high_contrast(),
883 Self::Colorblind => ThemePalette::colorblind(),
884 }
885 }
886
887 pub fn all() -> &'static [Self] {
889 &Self::ALL
890 }
891}
892
893impl ThemePalette {
894 pub fn catppuccin() -> Self {
897 Self {
898 accent: PackedRgba::rgb(137, 180, 250), accent_alt: PackedRgba::rgb(203, 166, 247), bg: PackedRgba::rgb(30, 30, 46), fg: PackedRgba::rgb(205, 214, 244), surface: PackedRgba::rgb(49, 50, 68), hint: PackedRgba::rgb(127, 132, 156), border: PackedRgba::rgb(69, 71, 90), user: PackedRgba::rgb(166, 227, 161), agent: PackedRgba::rgb(137, 180, 250), tool: PackedRgba::rgb(250, 179, 135), system: PackedRgba::rgb(249, 226, 175), stripe_even: PackedRgba::rgb(30, 30, 46), stripe_odd: PackedRgba::rgb(36, 36, 54), }
913 }
914
915 pub fn dracula() -> Self {
918 Self {
919 accent: PackedRgba::rgb(189, 147, 249), accent_alt: PackedRgba::rgb(255, 121, 198), bg: PackedRgba::rgb(40, 42, 54), fg: PackedRgba::rgb(248, 248, 242), surface: PackedRgba::rgb(68, 71, 90), hint: PackedRgba::rgb(155, 165, 200), border: PackedRgba::rgb(68, 71, 90), user: PackedRgba::rgb(80, 250, 123), agent: PackedRgba::rgb(189, 147, 249), tool: PackedRgba::rgb(255, 184, 108), system: PackedRgba::rgb(241, 250, 140), stripe_even: PackedRgba::rgb(40, 42, 54), stripe_odd: PackedRgba::rgb(48, 50, 64), }
934 }
935
936 pub fn nord() -> Self {
939 Self {
940 accent: PackedRgba::rgb(136, 192, 208), accent_alt: PackedRgba::rgb(180, 142, 173), bg: PackedRgba::rgb(46, 52, 64), fg: PackedRgba::rgb(236, 239, 244), surface: PackedRgba::rgb(59, 66, 82), hint: PackedRgba::rgb(145, 155, 180), border: PackedRgba::rgb(67, 76, 94), user: PackedRgba::rgb(163, 190, 140), agent: PackedRgba::rgb(136, 192, 208), tool: PackedRgba::rgb(208, 135, 112), system: PackedRgba::rgb(235, 203, 139), stripe_even: PackedRgba::rgb(46, 52, 64), stripe_odd: PackedRgba::rgb(52, 58, 72), }
955 }
956
957 pub fn high_contrast() -> Self {
962 Self {
963 accent: PackedRgba::rgb(0, 191, 255),
964 accent_alt: PackedRgba::rgb(255, 105, 180),
965 bg: PackedRgba::BLACK,
966 fg: PackedRgba::WHITE,
967 surface: PackedRgba::rgb(28, 28, 28),
968 hint: PackedRgba::rgb(180, 180, 180),
969 border: PackedRgba::WHITE,
970 user: PackedRgba::rgb(0, 255, 127),
971 agent: PackedRgba::rgb(0, 191, 255),
972 tool: PackedRgba::rgb(255, 165, 0),
973 system: PackedRgba::rgb(255, 255, 0),
974 stripe_even: PackedRgba::BLACK,
975 stripe_odd: PackedRgba::rgb(24, 24, 24),
976 }
977 }
978
979 pub fn colorblind() -> Self {
986 Self {
987 accent: colors::ACCENT_PRIMARY, accent_alt: colors::ACCENT_SECONDARY, bg: colors::BG_DEEP, fg: colors::TEXT_PRIMARY, surface: colors::BG_SURFACE, hint: colors::TEXT_MUTED, border: colors::BORDER, user: PackedRgba::rgb(125, 207, 255), agent: colors::ROLE_AGENT, tool: PackedRgba::rgb(224, 175, 104), system: PackedRgba::rgb(208, 154, 247), stripe_even: colors::BG_DEEP, stripe_odd: PackedRgba::rgb(30, 32, 48), }
1001 }
1002
1003 pub fn solarized_dark() -> Self {
1004 Self {
1005 accent: PackedRgba::rgb(38, 139, 210), accent_alt: PackedRgba::rgb(108, 113, 196), bg: PackedRgba::rgb(0, 43, 54), fg: PackedRgba::rgb(147, 161, 161), surface: PackedRgba::rgb(7, 54, 66), hint: PackedRgba::rgb(105, 127, 134), border: PackedRgba::rgb(88, 110, 117), user: PackedRgba::rgb(133, 153, 0), agent: PackedRgba::rgb(38, 139, 210), tool: PackedRgba::rgb(203, 75, 22), system: PackedRgba::rgb(181, 137, 0), stripe_even: PackedRgba::rgb(0, 43, 54),
1017 stripe_odd: PackedRgba::rgb(7, 54, 66),
1018 }
1019 }
1020
1021 pub fn solarized_light() -> Self {
1022 Self {
1023 accent: PackedRgba::rgb(38, 139, 210),
1024 accent_alt: PackedRgba::rgb(108, 113, 196),
1025 bg: PackedRgba::rgb(253, 246, 227), fg: PackedRgba::rgb(86, 108, 116), surface: PackedRgba::rgb(238, 232, 213), hint: PackedRgba::rgb(115, 132, 134), border: PackedRgba::rgb(147, 161, 161), user: PackedRgba::rgb(128, 148, 0), agent: PackedRgba::rgb(38, 139, 210),
1032 tool: PackedRgba::rgb(203, 75, 22),
1033 system: PackedRgba::rgb(177, 133, 0), stripe_even: PackedRgba::rgb(253, 246, 227),
1035 stripe_odd: PackedRgba::rgb(238, 232, 213),
1036 }
1037 }
1038
1039 pub fn monokai() -> Self {
1040 Self {
1041 accent: PackedRgba::rgb(166, 226, 46), accent_alt: PackedRgba::rgb(174, 129, 255), bg: PackedRgba::rgb(39, 40, 34), fg: PackedRgba::rgb(248, 248, 242), surface: PackedRgba::rgb(53, 54, 45), hint: PackedRgba::rgb(150, 155, 140), border: PackedRgba::rgb(73, 72, 62), user: PackedRgba::rgb(166, 226, 46), agent: PackedRgba::rgb(102, 217, 239), tool: PackedRgba::rgb(253, 151, 31), system: PackedRgba::rgb(230, 219, 116), stripe_even: PackedRgba::rgb(39, 40, 34),
1053 stripe_odd: PackedRgba::rgb(48, 49, 42),
1054 }
1055 }
1056
1057 pub fn gruvbox_dark() -> Self {
1058 Self {
1059 accent: PackedRgba::rgb(250, 189, 47), accent_alt: PackedRgba::rgb(211, 134, 155), bg: PackedRgba::rgb(40, 40, 40), fg: PackedRgba::rgb(235, 219, 178), surface: PackedRgba::rgb(50, 48, 47), hint: PackedRgba::rgb(146, 131, 116), border: PackedRgba::rgb(80, 73, 69), user: PackedRgba::rgb(184, 187, 38), agent: PackedRgba::rgb(131, 165, 152), tool: PackedRgba::rgb(254, 128, 25), system: PackedRgba::rgb(250, 189, 47), stripe_even: PackedRgba::rgb(40, 40, 40),
1071 stripe_odd: PackedRgba::rgb(50, 48, 47),
1072 }
1073 }
1074
1075 pub fn one_dark() -> Self {
1076 Self {
1077 accent: PackedRgba::rgb(97, 175, 239), accent_alt: PackedRgba::rgb(198, 120, 221), bg: PackedRgba::rgb(40, 44, 52), fg: PackedRgba::rgb(171, 178, 191), surface: PackedRgba::rgb(49, 53, 63), hint: PackedRgba::rgb(118, 128, 150), border: PackedRgba::rgb(62, 68, 81), user: PackedRgba::rgb(152, 195, 121), agent: PackedRgba::rgb(97, 175, 239), tool: PackedRgba::rgb(229, 192, 123), system: PackedRgba::rgb(224, 108, 117), stripe_even: PackedRgba::rgb(40, 44, 52),
1089 stripe_odd: PackedRgba::rgb(49, 53, 63),
1090 }
1091 }
1092
1093 pub fn rose_pine() -> Self {
1094 Self {
1095 accent: PackedRgba::rgb(235, 188, 186), accent_alt: PackedRgba::rgb(196, 167, 231), bg: PackedRgba::rgb(25, 23, 36), fg: PackedRgba::rgb(224, 222, 244), surface: PackedRgba::rgb(38, 35, 53), hint: PackedRgba::rgb(114, 110, 138), border: PackedRgba::rgb(57, 53, 82), user: PackedRgba::rgb(156, 207, 216), agent: PackedRgba::rgb(196, 167, 231), tool: PackedRgba::rgb(246, 193, 119), system: PackedRgba::rgb(235, 111, 146), stripe_even: PackedRgba::rgb(25, 23, 36),
1107 stripe_odd: PackedRgba::rgb(33, 30, 46),
1108 }
1109 }
1110
1111 pub fn everforest() -> Self {
1112 Self {
1113 accent: PackedRgba::rgb(167, 192, 128), accent_alt: PackedRgba::rgb(214, 153, 182), bg: PackedRgba::rgb(39, 46, 34), fg: PackedRgba::rgb(211, 198, 170), surface: PackedRgba::rgb(47, 55, 42), hint: PackedRgba::rgb(135, 127, 110), border: PackedRgba::rgb(68, 77, 60), user: PackedRgba::rgb(131, 192, 146), agent: PackedRgba::rgb(124, 195, 210), tool: PackedRgba::rgb(219, 188, 127), system: PackedRgba::rgb(230, 126, 128), stripe_even: PackedRgba::rgb(39, 46, 34),
1125 stripe_odd: PackedRgba::rgb(47, 55, 42),
1126 }
1127 }
1128
1129 pub fn kanagawa() -> Self {
1130 Self {
1131 accent: PackedRgba::rgb(126, 156, 216), accent_alt: PackedRgba::rgb(149, 127, 184), bg: PackedRgba::rgb(31, 31, 40), fg: PackedRgba::rgb(220, 215, 186), surface: PackedRgba::rgb(42, 42, 54), hint: PackedRgba::rgb(119, 118, 110), border: PackedRgba::rgb(84, 84, 109), user: PackedRgba::rgb(152, 187, 108), agent: PackedRgba::rgb(127, 180, 202), tool: PackedRgba::rgb(255, 169, 98), system: PackedRgba::rgb(195, 64, 67), stripe_even: PackedRgba::rgb(31, 31, 40),
1143 stripe_odd: PackedRgba::rgb(42, 42, 54),
1144 }
1145 }
1146
1147 pub fn ayu_mirage() -> Self {
1148 Self {
1149 accent: PackedRgba::rgb(115, 210, 222), accent_alt: PackedRgba::rgb(217, 155, 243), bg: PackedRgba::rgb(36, 42, 54), fg: PackedRgba::rgb(204, 204, 194), surface: PackedRgba::rgb(44, 51, 64), hint: PackedRgba::rgb(119, 126, 140), border: PackedRgba::rgb(60, 68, 82), user: PackedRgba::rgb(135, 213, 134), agent: PackedRgba::rgb(115, 210, 222), tool: PackedRgba::rgb(255, 213, 109), system: PackedRgba::rgb(240, 113, 120), stripe_even: PackedRgba::rgb(36, 42, 54),
1161 stripe_odd: PackedRgba::rgb(44, 51, 64),
1162 }
1163 }
1164
1165 pub fn nightfox() -> Self {
1166 Self {
1167 accent: PackedRgba::rgb(129, 180, 243), accent_alt: PackedRgba::rgb(174, 140, 211), bg: PackedRgba::rgb(18, 21, 31), fg: PackedRgba::rgb(205, 207, 216), surface: PackedRgba::rgb(29, 33, 46), hint: PackedRgba::rgb(106, 108, 122), border: PackedRgba::rgb(48, 54, 71), user: PackedRgba::rgb(129, 200, 152), agent: PackedRgba::rgb(129, 180, 243), tool: PackedRgba::rgb(218, 167, 89), system: PackedRgba::rgb(201, 101, 120), stripe_even: PackedRgba::rgb(18, 21, 31),
1179 stripe_odd: PackedRgba::rgb(29, 33, 46),
1180 }
1181 }
1182
1183 pub fn cyberpunk_aurora() -> Self {
1184 Self {
1185 accent: PackedRgba::rgb(255, 0, 128), accent_alt: PackedRgba::rgb(0, 255, 255), bg: PackedRgba::rgb(13, 2, 33), fg: PackedRgba::rgb(224, 210, 255), surface: PackedRgba::rgb(22, 10, 48), hint: PackedRgba::rgb(120, 100, 160), border: PackedRgba::rgb(60, 30, 100), user: PackedRgba::rgb(0, 255, 163), agent: PackedRgba::rgb(0, 200, 255), tool: PackedRgba::rgb(255, 213, 0), system: PackedRgba::rgb(255, 51, 102), stripe_even: PackedRgba::rgb(13, 2, 33),
1197 stripe_odd: PackedRgba::rgb(22, 10, 48),
1198 }
1199 }
1200
1201 pub fn synthwave_84() -> Self {
1202 Self {
1203 accent: PackedRgba::rgb(255, 123, 213), accent_alt: PackedRgba::rgb(114, 241, 223), bg: PackedRgba::rgb(34, 20, 54), fg: PackedRgba::rgb(241, 233, 255), surface: PackedRgba::rgb(44, 28, 68), hint: PackedRgba::rgb(130, 115, 165), border: PackedRgba::rgb(70, 45, 100), user: PackedRgba::rgb(114, 241, 223), agent: PackedRgba::rgb(54, 245, 253), tool: PackedRgba::rgb(254, 215, 102), system: PackedRgba::rgb(254, 73, 99), stripe_even: PackedRgba::rgb(34, 20, 54),
1215 stripe_odd: PackedRgba::rgb(44, 28, 68),
1216 }
1217 }
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223
1224 #[test]
1227 fn test_terminal_width_from_cols_narrow() {
1228 assert_eq!(TerminalWidth::from_cols(40), TerminalWidth::Narrow);
1229 assert_eq!(TerminalWidth::from_cols(79), TerminalWidth::Narrow);
1230 }
1231
1232 #[test]
1233 fn test_terminal_width_from_cols_normal() {
1234 assert_eq!(TerminalWidth::from_cols(80), TerminalWidth::Normal);
1235 assert_eq!(TerminalWidth::from_cols(100), TerminalWidth::Normal);
1236 assert_eq!(TerminalWidth::from_cols(120), TerminalWidth::Normal);
1237 }
1238
1239 #[test]
1240 fn test_terminal_width_from_cols_wide() {
1241 assert_eq!(TerminalWidth::from_cols(121), TerminalWidth::Wide);
1242 assert_eq!(TerminalWidth::from_cols(200), TerminalWidth::Wide);
1243 }
1244
1245 #[test]
1246 fn test_terminal_width_border_color() {
1247 assert_eq!(TerminalWidth::Narrow.border_color(), colors::BORDER_MINIMAL);
1248 assert_eq!(
1249 TerminalWidth::Normal.border_color(),
1250 colors::BORDER_STANDARD
1251 );
1252 assert_eq!(
1253 TerminalWidth::Wide.border_color(),
1254 colors::BORDER_EMPHASIZED
1255 );
1256 }
1257
1258 #[test]
1259 fn test_terminal_width_show_decorations() {
1260 assert!(!TerminalWidth::Narrow.show_decorations());
1261 assert!(TerminalWidth::Normal.show_decorations());
1262 assert!(TerminalWidth::Wide.show_decorations());
1263 }
1264
1265 #[test]
1266 fn test_terminal_width_show_extended_info() {
1267 assert!(!TerminalWidth::Narrow.show_extended_info());
1268 assert!(!TerminalWidth::Normal.show_extended_info());
1269 assert!(TerminalWidth::Wide.show_extended_info());
1270 }
1271
1272 #[test]
1275 fn test_gradient_shades_header() {
1276 let shades = GradientShades::header();
1277 assert_eq!(shades.dark, colors::GRADIENT_HEADER_TOP);
1278 assert_eq!(shades.mid, colors::GRADIENT_HEADER_MID);
1279 assert_eq!(shades.light, colors::GRADIENT_HEADER_BOT);
1280 }
1281
1282 #[test]
1283 fn test_gradient_shades_pill() {
1284 let shades = GradientShades::pill();
1285 assert_eq!(shades.dark, colors::GRADIENT_PILL_LEFT);
1286 assert_eq!(shades.mid, colors::GRADIENT_PILL_CENTER);
1287 assert_eq!(shades.light, colors::GRADIENT_PILL_RIGHT);
1288 }
1289
1290 #[test]
1291 fn test_gradient_shades_styles() {
1292 let shades = GradientShades::header();
1293 let (dark, mid, light) = shades.styles();
1294 assert_eq!(dark.bg, Some(shades.dark));
1295 assert_eq!(mid.bg, Some(shades.mid));
1296 assert_eq!(light.bg, Some(shades.light));
1297 }
1298
1299 #[test]
1302 fn test_adaptive_borders_for_width_narrow() {
1303 let borders = AdaptiveBorders::for_width(60);
1304 assert_eq!(borders.width_class, TerminalWidth::Narrow);
1305 assert!(!borders.use_double);
1306 assert!(!borders.show_corners);
1307 }
1308
1309 #[test]
1310 fn test_adaptive_borders_for_width_normal() {
1311 let borders = AdaptiveBorders::for_width(100);
1312 assert_eq!(borders.width_class, TerminalWidth::Normal);
1313 assert!(!borders.use_double);
1314 assert!(borders.show_corners);
1315 }
1316
1317 #[test]
1318 fn test_adaptive_borders_for_width_wide() {
1319 let borders = AdaptiveBorders::for_width(150);
1320 assert_eq!(borders.width_class, TerminalWidth::Wide);
1321 assert!(borders.use_double);
1322 assert!(borders.show_corners);
1323 }
1324
1325 #[test]
1326 fn test_adaptive_borders_focused() {
1327 let borders = AdaptiveBorders::focused(100);
1328 assert_eq!(borders.color, colors::BORDER_FOCUS);
1329 }
1330
1331 #[test]
1334 fn test_theme_palette_light() {
1335 let palette = ThemePalette::light();
1336 assert_eq!(palette.bg, PackedRgba::rgb(250, 250, 252));
1338 assert_eq!(palette.fg, PackedRgba::rgb(36, 41, 46));
1340 }
1341
1342 #[test]
1343 fn test_theme_palette_dark() {
1344 let palette = ThemePalette::dark();
1345 assert_eq!(palette.bg, colors::BG_DEEP);
1347 assert_eq!(palette.fg, colors::TEXT_PRIMARY);
1349 }
1350
1351 #[test]
1352 fn test_theme_palette_catppuccin() {
1353 let palette = ThemePalette::catppuccin();
1354 assert_eq!(palette.bg, PackedRgba::rgb(30, 30, 46));
1356 }
1357
1358 #[test]
1359 fn test_theme_palette_dracula() {
1360 let palette = ThemePalette::dracula();
1361 assert_eq!(palette.bg, PackedRgba::rgb(40, 42, 54));
1362 }
1363
1364 #[test]
1365 fn test_theme_palette_nord() {
1366 let palette = ThemePalette::nord();
1367 assert_eq!(palette.bg, PackedRgba::rgb(46, 52, 64));
1368 }
1369
1370 #[test]
1371 fn test_theme_palette_high_contrast() {
1372 let palette = ThemePalette::high_contrast();
1373 assert_eq!(palette.bg, PackedRgba::rgb(0, 0, 0));
1375 assert_eq!(palette.fg, PackedRgba::rgb(255, 255, 255));
1376 }
1377
1378 #[test]
1379 fn test_theme_palette_agent_pane_known_agents() {
1380 let claude = ThemePalette::agent_pane("claude_code");
1382 assert_eq!(claude.bg, colors::AGENT_CLAUDE_BG);
1383
1384 let codex = ThemePalette::agent_pane("codex");
1385 assert_eq!(codex.bg, colors::AGENT_CODEX_BG);
1386
1387 let gemini = ThemePalette::agent_pane("gemini_cli");
1388 assert_eq!(gemini.bg, colors::AGENT_GEMINI_BG);
1389
1390 let chatgpt = ThemePalette::agent_pane("chatgpt");
1391 assert_eq!(chatgpt.bg, colors::AGENT_CHATGPT_BG);
1392 }
1393
1394 #[test]
1395 fn test_theme_palette_agent_pane_unknown_agent() {
1396 let unknown = ThemePalette::agent_pane("unknown_agent");
1397 assert_eq!(unknown.bg, colors::BG_DEEP);
1398 }
1399
1400 #[test]
1401 fn test_theme_palette_agent_icon() {
1402 assert_eq!(ThemePalette::agent_icon("codex"), "◆");
1403 assert_eq!(ThemePalette::agent_icon("claude_code"), "●");
1404 assert_eq!(ThemePalette::agent_icon("gemini"), "◇");
1405 assert_eq!(ThemePalette::agent_icon("chatgpt"), "○");
1406 assert_eq!(ThemePalette::agent_icon("unknown"), "•");
1407 }
1408
1409 #[test]
1410 fn test_theme_palette_role_theme() {
1411 let palette = ThemePalette::dark();
1412
1413 let user_theme = palette.role_theme("user");
1414 assert_eq!(user_theme.fg, palette.user);
1415
1416 let agent_theme = palette.role_theme("assistant");
1417 assert_eq!(agent_theme.fg, palette.agent);
1418
1419 let tool_theme = palette.role_theme("tool");
1420 assert_eq!(tool_theme.fg, palette.tool);
1421
1422 let system_theme = palette.role_theme("system");
1423 assert_eq!(system_theme.fg, palette.system);
1424 }
1425
1426 #[test]
1429 fn test_contrast_level_from_ratio() {
1430 assert_eq!(ContrastLevel::from_ratio(2.0), ContrastLevel::Fail);
1431 assert_eq!(ContrastLevel::from_ratio(3.5), ContrastLevel::AALarge);
1432 assert_eq!(ContrastLevel::from_ratio(5.0), ContrastLevel::AA);
1433 assert_eq!(ContrastLevel::from_ratio(8.0), ContrastLevel::AAA);
1434 }
1435
1436 #[test]
1437 fn test_contrast_level_meets() {
1438 assert!(ContrastLevel::AAA.meets(ContrastLevel::AA));
1439 assert!(ContrastLevel::AA.meets(ContrastLevel::AALarge));
1440 assert!(!ContrastLevel::Fail.meets(ContrastLevel::AA));
1441 }
1442
1443 #[test]
1444 fn test_contrast_level_name() {
1445 assert_eq!(ContrastLevel::AAA.name(), "AAA");
1446 assert_eq!(ContrastLevel::AA.name(), "AA");
1447 assert_eq!(ContrastLevel::Fail.name(), "Fail");
1448 }
1449
1450 #[test]
1453 fn test_relative_luminance_black() {
1454 let lum = relative_luminance(PackedRgba::rgb(0, 0, 0));
1455 assert!((lum - 0.0).abs() < 0.001);
1456 }
1457
1458 #[test]
1459 fn test_relative_luminance_white() {
1460 let lum = relative_luminance(PackedRgba::rgb(255, 255, 255));
1461 assert!((lum - 1.0).abs() < 0.001);
1462 }
1463
1464 #[test]
1465 fn test_relative_luminance_named_colors() {
1466 let black_lum = relative_luminance(PackedRgba::BLACK);
1468 assert!(black_lum < 0.01);
1469
1470 let white_lum = relative_luminance(PackedRgba::WHITE);
1472 assert!(white_lum > 0.99);
1473 }
1474
1475 #[test]
1476 fn test_contrast_ratio_black_white() {
1477 let ratio = contrast_ratio(PackedRgba::rgb(255, 255, 255), PackedRgba::rgb(0, 0, 0));
1478 assert!(ratio > 20.0);
1480 }
1481
1482 #[test]
1483 fn test_contrast_ratio_same_color() {
1484 let ratio = contrast_ratio(
1485 PackedRgba::rgb(128, 128, 128),
1486 PackedRgba::rgb(128, 128, 128),
1487 );
1488 assert!((ratio - 1.0).abs() < 0.001);
1490 }
1491
1492 #[test]
1493 fn test_check_contrast() {
1494 let level = check_contrast(PackedRgba::rgb(255, 255, 255), PackedRgba::rgb(0, 0, 0));
1496 assert_eq!(level, ContrastLevel::AAA);
1497
1498 let level = check_contrast(
1500 PackedRgba::rgb(100, 100, 100),
1501 PackedRgba::rgb(120, 120, 120),
1502 );
1503 assert_eq!(level, ContrastLevel::Fail);
1504 }
1505
1506 #[test]
1507 fn test_ensure_contrast_already_sufficient() {
1508 let bg = PackedRgba::rgb(0, 0, 0);
1509 let fg = PackedRgba::rgb(255, 255, 255);
1510 let result = ensure_contrast(fg, bg, ContrastLevel::AA);
1511 assert_eq!(result, fg);
1513 }
1514
1515 #[test]
1518 fn test_theme_preset_default() {
1519 let preset = ThemePreset::default();
1520 assert_eq!(preset, ThemePreset::TokyoNight);
1521 }
1522
1523 #[test]
1524 fn test_theme_preset_name() {
1525 assert_eq!(ThemePreset::TokyoNight.name(), "Tokyo Night");
1526 assert_eq!(ThemePreset::Daylight.name(), "Daylight");
1527 assert_eq!(ThemePreset::Catppuccin.name(), "Catppuccin Mocha");
1528 assert_eq!(ThemePreset::Dracula.name(), "Dracula");
1529 assert_eq!(ThemePreset::Nord.name(), "Nord");
1530 assert_eq!(ThemePreset::HighContrast.name(), "High Contrast");
1531 }
1532
1533 #[test]
1534 fn test_theme_preset_next_cycles() {
1535 let mut preset = ThemePreset::TokyoNight;
1536 preset = preset.next();
1537 assert_eq!(preset, ThemePreset::Daylight);
1538 preset = preset.next();
1539 assert_eq!(preset, ThemePreset::Catppuccin);
1540 let mut p = ThemePreset::Colorblind;
1542 p = p.next();
1543 assert_eq!(p, ThemePreset::TokyoNight);
1544 }
1545
1546 #[test]
1547 fn test_theme_preset_prev_cycles() {
1548 let mut preset = ThemePreset::TokyoNight;
1549 preset = preset.prev();
1550 assert_eq!(preset, ThemePreset::Colorblind);
1551 preset = preset.prev();
1552 assert_eq!(preset, ThemePreset::HighContrast);
1553 }
1554
1555 #[test]
1556 fn test_theme_preset_to_palette() {
1557 let palette = ThemePreset::TokyoNight.to_palette();
1558 assert_eq!(palette.bg, ThemePalette::dark().bg);
1559
1560 let palette = ThemePreset::Daylight.to_palette();
1561 assert_eq!(palette.bg, ThemePalette::light().bg);
1562 }
1563
1564 #[test]
1565 fn test_theme_preset_all() {
1566 let all = ThemePreset::all();
1567 assert_eq!(all.len(), 19);
1568 assert!(all.contains(&ThemePreset::TokyoNight));
1569 assert!(all.contains(&ThemePreset::Daylight));
1570 }
1571
1572 #[test]
1575 fn test_chip_style() {
1576 let palette = ThemePalette::dark();
1577 let style = chip_style(palette);
1578 assert_eq!(style.fg, Some(palette.accent_alt));
1579 }
1580
1581 #[test]
1582 fn test_kbd_style() {
1583 let palette = ThemePalette::dark();
1584 let style = kbd_style(palette);
1585 assert_eq!(style.fg, Some(palette.accent));
1586 }
1587
1588 #[test]
1589 fn test_score_style_high() {
1590 let palette = ThemePalette::dark();
1591 let style = score_style(9.0, palette);
1592 assert_eq!(style.fg, Some(colors::STATUS_SUCCESS));
1593 }
1594
1595 #[test]
1596 fn test_score_style_medium() {
1597 let palette = ThemePalette::dark();
1598 let style = score_style(6.0, palette);
1599 assert_eq!(style.fg, Some(palette.accent));
1600 }
1601
1602 #[test]
1603 fn test_score_style_low() {
1604 let palette = ThemePalette::dark();
1605 let style = score_style(3.0, palette);
1606 assert_eq!(style.fg, Some(palette.hint));
1607 }
1608
1609 #[test]
1612 fn test_role_theme_has_all_fields() {
1613 let palette = ThemePalette::dark();
1614 let theme = palette.role_theme("user");
1615 assert_ne!(theme.fg, PackedRgba::TRANSPARENT);
1617 assert_ne!(theme.bg, PackedRgba::TRANSPARENT);
1618 assert_ne!(theme.border, PackedRgba::TRANSPARENT);
1619 assert_ne!(theme.badge, PackedRgba::TRANSPARENT);
1620 }
1621
1622 #[test]
1625 fn test_pane_theme_has_all_fields() {
1626 let pane = ThemePalette::agent_pane("claude");
1627 assert_ne!(pane.fg, PackedRgba::TRANSPARENT);
1628 assert_ne!(pane.bg, PackedRgba::TRANSPARENT);
1629 assert_ne!(pane.accent, PackedRgba::TRANSPARENT);
1630 }
1631
1632 const KNOWN_AGENTS: &[&str] = &[
1635 "claude_code",
1636 "codex",
1637 "cline",
1638 "gemini",
1639 "amp",
1640 "aider",
1641 "cursor",
1642 "chatgpt",
1643 "opencode",
1644 "pi_agent",
1645 "factory",
1646 "clawdbot",
1647 "vibe",
1648 "openclaw",
1649 "copilot",
1650 "copilot_cli",
1651 "crush",
1652 "hermes",
1653 "kimi",
1654 "qwen",
1655 ];
1656
1657 #[test]
1658 fn agent_accent_colors_are_pairwise_distinct() {
1659 let accents: Vec<(&str, PackedRgba)> = KNOWN_AGENTS
1660 .iter()
1661 .map(|a| (*a, ThemePalette::agent_pane(a).accent))
1662 .collect();
1663
1664 for i in 0..accents.len() {
1665 for j in (i + 1)..accents.len() {
1666 let (name_a, color_a) = accents[i];
1667 let (name_b, color_b) = accents[j];
1668 assert_ne!(
1669 color_a, color_b,
1670 "Agents {name_a} and {name_b} have identical accent colors — \
1671 users cannot distinguish them"
1672 );
1673 }
1674 }
1675 }
1676
1677 #[test]
1678 fn known_agents_do_not_use_unknown_fallback_background() {
1679 for agent in KNOWN_AGENTS {
1680 let pane = ThemePalette::agent_pane(agent);
1681 assert_ne!(
1682 pane.bg,
1683 colors::BG_DEEP,
1684 "known agent {agent} should have a provider-specific background"
1685 );
1686 }
1687 }
1688
1689 #[test]
1690 fn agent_background_colors_are_pairwise_distinct() {
1691 let bgs: Vec<(&str, PackedRgba)> = KNOWN_AGENTS
1692 .iter()
1693 .map(|a| (*a, ThemePalette::agent_pane(a).bg))
1694 .collect();
1695
1696 for i in 0..bgs.len() {
1697 for j in (i + 1)..bgs.len() {
1698 let (name_a, bg_a) = bgs[i];
1699 let (name_b, bg_b) = bgs[j];
1700 if (name_a == "codex" && name_b == "pi_agent")
1702 || (name_a == "pi_agent" && name_b == "codex")
1703 {
1704 continue;
1705 }
1706 assert_ne!(
1707 bg_a, bg_b,
1708 "Agents {name_a} and {name_b} have identical background colors"
1709 );
1710 }
1711 }
1712 }
1713
1714 #[test]
1715 fn agent_icons_are_pairwise_distinct() {
1716 let icons: Vec<(&str, &str)> = KNOWN_AGENTS
1717 .iter()
1718 .map(|a| (*a, ThemePalette::agent_icon(a)))
1719 .collect();
1720
1721 for i in 0..icons.len() {
1722 for j in (i + 1)..icons.len() {
1723 let (name_a, icon_a) = icons[i];
1724 let (name_b, icon_b) = icons[j];
1725 assert_ne!(
1726 icon_a, icon_b,
1727 "Agents {name_a} and {name_b} have identical icons"
1728 );
1729 }
1730 }
1731 }
1732
1733 #[test]
1734 fn agent_icons_are_single_char_glyphs() {
1735 for agent in KNOWN_AGENTS {
1736 let icon = ThemePalette::agent_icon(agent);
1737 assert_eq!(
1738 icon.chars().count(),
1739 1,
1740 "Agent {agent} icon should be a single-width glyph for layout stability"
1741 );
1742 }
1743 }
1744
1745 #[test]
1746 fn unknown_agent_falls_back_gracefully() {
1747 let pane = ThemePalette::agent_pane("nonexistent_agent");
1748 assert_ne!(pane.fg, PackedRgba::TRANSPARENT);
1750 assert_ne!(pane.bg, PackedRgba::TRANSPARENT);
1751 assert_ne!(pane.accent, PackedRgba::TRANSPARENT);
1752
1753 let icon = ThemePalette::agent_icon("nonexistent_agent");
1754 assert!(!icon.is_empty(), "unknown agent should get a fallback icon");
1755 }
1756
1757 #[test]
1758 fn role_colors_are_pairwise_distinct_in_palette() {
1759 let palette = ThemePalette::dark();
1760 let roles = [
1761 ("user", palette.user),
1762 ("agent", palette.agent),
1763 ("tool", palette.tool),
1764 ("system", palette.system),
1765 ];
1766 for i in 0..roles.len() {
1767 for j in (i + 1)..roles.len() {
1768 let (name_a, color_a) = roles[i];
1769 let (name_b, color_b) = roles[j];
1770 assert_ne!(
1771 color_a, color_b,
1772 "ThemePalette::dark() role {name_a} and {name_b} have identical colors"
1773 );
1774 }
1775 }
1776 }
1777}