use ftui::Style;
use ftui::render::cell::PackedRgba;
pub mod colors {
use ftui::render::cell::PackedRgba as Color;
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); }
#[derive(Clone, Copy)]
pub struct RoleTheme {
pub fg: PackedRgba,
pub bg: PackedRgba,
pub border: PackedRgba,
pub badge: PackedRgba,
}
#[derive(Clone, Copy)]
pub struct GradientShades {
pub dark: PackedRgba,
pub mid: PackedRgba,
pub light: PackedRgba,
}
impl GradientShades {
pub fn header() -> Self {
Self {
dark: colors::GRADIENT_HEADER_TOP,
mid: colors::GRADIENT_HEADER_MID,
light: colors::GRADIENT_HEADER_BOT,
}
}
pub fn pill() -> Self {
Self {
dark: colors::GRADIENT_PILL_LEFT,
mid: colors::GRADIENT_PILL_CENTER,
light: colors::GRADIENT_PILL_RIGHT,
}
}
pub fn styles(&self) -> (Style, Style, Style) {
(
Style::new().bg(self.dark),
Style::new().bg(self.mid),
Style::new().bg(self.light),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TerminalWidth {
Narrow,
Normal,
Wide,
}
impl TerminalWidth {
pub fn from_cols(cols: u16) -> Self {
if cols < 80 {
Self::Narrow
} else if cols <= 120 {
Self::Normal
} else {
Self::Wide
}
}
pub fn border_color(self) -> PackedRgba {
match self {
Self::Narrow => colors::BORDER_MINIMAL,
Self::Normal => colors::BORDER_STANDARD,
Self::Wide => colors::BORDER_EMPHASIZED,
}
}
pub fn border_style(self) -> Style {
Style::new().fg(self.border_color())
}
pub fn show_decorations(self) -> bool {
!matches!(self, Self::Narrow)
}
pub fn show_extended_info(self) -> bool {
matches!(self, Self::Wide)
}
}
#[derive(Clone, Copy)]
pub struct AdaptiveBorders {
pub width_class: TerminalWidth,
pub color: PackedRgba,
pub style: Style,
pub use_double: bool,
pub show_corners: bool,
}
impl AdaptiveBorders {
pub fn for_width(cols: u16) -> Self {
let width_class = TerminalWidth::from_cols(cols);
let color = width_class.border_color();
Self {
width_class,
color,
style: Style::new().fg(color),
use_double: matches!(width_class, TerminalWidth::Wide),
show_corners: width_class.show_decorations(),
}
}
pub fn focused(cols: u16) -> Self {
let mut borders = Self::for_width(cols);
borders.color = colors::BORDER_FOCUS;
borders.style = Style::new().fg(colors::BORDER_FOCUS);
borders
}
}
#[derive(Clone, Copy)]
pub struct PaneTheme {
pub bg: PackedRgba,
pub fg: PackedRgba,
pub accent: PackedRgba,
}
#[derive(Clone, Copy)]
pub struct ThemePalette {
pub accent: PackedRgba,
pub accent_alt: PackedRgba,
pub bg: PackedRgba,
pub fg: PackedRgba,
pub surface: PackedRgba,
pub hint: PackedRgba,
pub border: PackedRgba,
pub user: PackedRgba,
pub agent: PackedRgba,
pub tool: PackedRgba,
pub system: PackedRgba,
pub stripe_even: PackedRgba,
pub stripe_odd: PackedRgba,
}
impl ThemePalette {
pub fn light() -> Self {
Self {
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), }
}
pub fn dark() -> Self {
Self {
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: colors::ROLE_USER,
agent: colors::ROLE_AGENT,
tool: colors::ROLE_TOOL,
system: colors::ROLE_SYSTEM,
stripe_even: colors::BG_DEEP, stripe_odd: PackedRgba::rgb(30, 32, 48), }
}
pub fn title(self) -> Style {
Style::new().fg(self.accent).bold()
}
pub fn title_subtle(self) -> Style {
Style::new().fg(self.fg).bold()
}
pub fn hint_style(self) -> Style {
Style::new().fg(self.hint)
}
pub fn border_style(self) -> Style {
Style::new().fg(self.border)
}
pub fn border_focus_style(self) -> Style {
Style::new().fg(self.accent)
}
pub fn surface_style(self) -> Style {
Style::new().bg(self.surface)
}
pub fn agent_pane(agent: &str) -> PaneTheme {
let slug = agent.to_lowercase().replace('-', "_");
let (bg, accent) = match slug.as_str() {
"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),
};
PaneTheme {
bg,
fg: colors::TEXT_PRIMARY, accent,
}
}
pub fn agent_icon(agent: &str) -> &'static str {
let slug = agent.to_lowercase().replace('-', "_");
match slug.as_str() {
"codex" => "◆",
"claude_code" | "claude" => "●",
"gemini" | "gemini_cli" => "◇",
"cline" => "■",
"amp" => "▲",
"aider" => "▼",
"cursor" => "◈",
"chatgpt" => "○",
"opencode" => "□",
"pi_agent" => "△",
"factory" | "droid" => "▣",
"clawdbot" => "⬢",
"vibe" | "mistral" => "✦",
"openclaw" => "⬡",
"copilot" => "◐",
"copilot_cli" => "◑",
"crush" => "✚",
"hermes" => "▽",
"kimi" => "✧",
"qwen" => "◒",
_ => "•",
}
}
pub fn role_style(self, role: &str) -> Style {
let color = match role.to_lowercase().as_str() {
"user" => self.user,
"assistant" | "agent" => self.agent,
"tool" => self.tool,
"system" => self.system,
_ => self.hint,
};
Style::new().fg(color)
}
pub fn role_theme(self, role: &str) -> RoleTheme {
match role.to_lowercase().as_str() {
"user" => RoleTheme {
fg: self.user,
bg: colors::ROLE_USER_BG,
border: self.user,
badge: colors::STATUS_SUCCESS,
},
"assistant" | "agent" => RoleTheme {
fg: self.agent,
bg: colors::ROLE_AGENT_BG,
border: self.agent,
badge: colors::ACCENT_PRIMARY,
},
"tool" => RoleTheme {
fg: self.tool,
bg: colors::ROLE_TOOL_BG,
border: self.tool,
badge: colors::ROLE_TOOL,
},
"system" => RoleTheme {
fg: self.system,
bg: colors::ROLE_SYSTEM_BG,
border: self.system,
badge: colors::STATUS_WARNING,
},
_ => RoleTheme {
fg: self.hint,
bg: self.bg,
border: self.border,
badge: self.hint,
},
}
}
pub fn header_gradient(&self) -> GradientShades {
GradientShades::header()
}
pub fn pill_gradient(&self) -> GradientShades {
GradientShades::pill()
}
pub fn adaptive_borders(&self, cols: u16) -> AdaptiveBorders {
AdaptiveBorders::for_width(cols)
}
pub fn adaptive_borders_focused(&self, cols: u16) -> AdaptiveBorders {
AdaptiveBorders::focused(cols)
}
pub fn highlight_style(self) -> Style {
Style::new()
.fg(self.bg) .bg(self.accent) .bold()
}
pub fn selected_style(self) -> Style {
Style::new().bg(self.surface).bold()
}
pub fn code_style(self) -> Style {
Style::new().bg(self.surface).fg(self.hint)
}
}
pub fn chip_style(palette: ThemePalette) -> Style {
Style::new().fg(palette.accent_alt).bold()
}
pub fn kbd_style(palette: ThemePalette) -> Style {
Style::new().fg(palette.accent).bold()
}
pub fn score_style(score: f32, palette: ThemePalette) -> Style {
let color = if score >= 8.0 {
colors::STATUS_SUCCESS
} else if score >= 5.0 {
palette.accent
} else {
palette.hint
};
let base = Style::new().fg(color);
if score >= 8.0 {
base.bold()
} else if score < 5.0 {
base.dim()
} else {
base
}
}
pub fn relative_luminance(color: PackedRgba) -> f64 {
let (r, g, b) = (color.r(), color.g(), color.b());
fn linearize(c: u8) -> f64 {
let c = f64::from(c) / 255.0;
if c <= 0.04045 {
c / 12.92
} else {
((c + 0.055) / 1.055).powf(2.4)
}
}
let r_lin = linearize(r);
let g_lin = linearize(g);
let b_lin = linearize(b);
0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin
}
pub fn contrast_ratio(fg: PackedRgba, bg: PackedRgba) -> f64 {
let lum_fg = relative_luminance(fg);
let lum_bg = relative_luminance(bg);
let (lighter, darker) = if lum_fg > lum_bg {
(lum_fg, lum_bg)
} else {
(lum_bg, lum_fg)
};
(lighter + 0.05) / (darker + 0.05)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ContrastLevel {
Fail,
AALarge,
AA,
AAALarge,
AAA,
}
impl ContrastLevel {
pub fn from_ratio(ratio: f64) -> Self {
if ratio >= 7.0 {
Self::AAA
} else if ratio >= 4.5 {
Self::AA
} else if ratio >= 3.0 {
Self::AALarge
} else {
Self::Fail
}
}
pub fn meets(self, required: ContrastLevel) -> bool {
match required {
Self::Fail => true,
Self::AALarge => !matches!(self, Self::Fail),
Self::AA | Self::AAALarge => matches!(self, Self::AA | Self::AAALarge | Self::AAA),
Self::AAA => matches!(self, Self::AAA),
}
}
pub fn name(self) -> &'static str {
match self {
Self::Fail => "Fail",
Self::AALarge => "AA (large text)",
Self::AA => "AA",
Self::AAALarge => "AAA (large text)",
Self::AAA => "AAA",
}
}
}
pub fn check_contrast(fg: PackedRgba, bg: PackedRgba) -> ContrastLevel {
ContrastLevel::from_ratio(contrast_ratio(fg, bg))
}
pub fn ensure_contrast(fg: PackedRgba, bg: PackedRgba, min_level: ContrastLevel) -> PackedRgba {
let level = check_contrast(fg, bg);
if level.meets(min_level) {
return fg;
}
let bg_lum = relative_luminance(bg);
if bg_lum > 0.5 {
PackedRgba::BLACK
} else {
PackedRgba::WHITE
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ThemePreset {
#[default]
TokyoNight,
Daylight,
Catppuccin,
Dracula,
Nord,
SolarizedDark,
SolarizedLight,
Monokai,
GruvboxDark,
OneDark,
RosePine,
Everforest,
Kanagawa,
AyuMirage,
Nightfox,
CyberpunkAurora,
Synthwave84,
HighContrast,
Colorblind,
}
impl ThemePreset {
const ALL: [Self; 19] = [
Self::TokyoNight,
Self::Daylight,
Self::Catppuccin,
Self::Dracula,
Self::Nord,
Self::SolarizedDark,
Self::SolarizedLight,
Self::Monokai,
Self::GruvboxDark,
Self::OneDark,
Self::RosePine,
Self::Everforest,
Self::Kanagawa,
Self::AyuMirage,
Self::Nightfox,
Self::CyberpunkAurora,
Self::Synthwave84,
Self::HighContrast,
Self::Colorblind,
];
pub fn name(self) -> &'static str {
match self {
Self::TokyoNight => "Tokyo Night",
Self::Daylight => "Daylight",
Self::Catppuccin => "Catppuccin Mocha",
Self::Dracula => "Dracula",
Self::Nord => "Nord",
Self::SolarizedDark => "Solarized Dark",
Self::SolarizedLight => "Solarized Light",
Self::Monokai => "Monokai",
Self::GruvboxDark => "Gruvbox Dark",
Self::OneDark => "One Dark",
Self::RosePine => "Ros\u{e9} Pine",
Self::Everforest => "Everforest",
Self::Kanagawa => "Kanagawa",
Self::AyuMirage => "Ayu Mirage",
Self::Nightfox => "Nightfox",
Self::CyberpunkAurora => "Cyberpunk Aurora",
Self::Synthwave84 => "Synthwave '84",
Self::HighContrast => "High Contrast",
Self::Colorblind => "Colorblind",
}
}
pub fn next(self) -> Self {
let idx = Self::ALL.iter().position(|p| *p == self).unwrap_or(0);
Self::ALL[(idx + 1) % Self::ALL.len()]
}
pub fn prev(self) -> Self {
let idx = Self::ALL.iter().position(|p| *p == self).unwrap_or(0);
Self::ALL[(idx + Self::ALL.len() - 1) % Self::ALL.len()]
}
pub fn to_palette(self) -> ThemePalette {
match self {
Self::TokyoNight => ThemePalette::dark(),
Self::Daylight => ThemePalette::light(),
Self::Catppuccin => ThemePalette::catppuccin(),
Self::Dracula => ThemePalette::dracula(),
Self::Nord => ThemePalette::nord(),
Self::SolarizedDark => ThemePalette::solarized_dark(),
Self::SolarizedLight => ThemePalette::solarized_light(),
Self::Monokai => ThemePalette::monokai(),
Self::GruvboxDark => ThemePalette::gruvbox_dark(),
Self::OneDark => ThemePalette::one_dark(),
Self::RosePine => ThemePalette::rose_pine(),
Self::Everforest => ThemePalette::everforest(),
Self::Kanagawa => ThemePalette::kanagawa(),
Self::AyuMirage => ThemePalette::ayu_mirage(),
Self::Nightfox => ThemePalette::nightfox(),
Self::CyberpunkAurora => ThemePalette::cyberpunk_aurora(),
Self::Synthwave84 => ThemePalette::synthwave_84(),
Self::HighContrast => ThemePalette::high_contrast(),
Self::Colorblind => ThemePalette::colorblind(),
}
}
pub fn all() -> &'static [Self] {
&Self::ALL
}
}
impl ThemePalette {
pub fn catppuccin() -> Self {
Self {
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), }
}
pub fn dracula() -> Self {
Self {
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), }
}
pub fn nord() -> Self {
Self {
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), }
}
pub fn high_contrast() -> Self {
Self {
accent: PackedRgba::rgb(0, 191, 255),
accent_alt: PackedRgba::rgb(255, 105, 180),
bg: PackedRgba::BLACK,
fg: PackedRgba::WHITE,
surface: PackedRgba::rgb(28, 28, 28),
hint: PackedRgba::rgb(180, 180, 180),
border: PackedRgba::WHITE,
user: PackedRgba::rgb(0, 255, 127),
agent: PackedRgba::rgb(0, 191, 255),
tool: PackedRgba::rgb(255, 165, 0),
system: PackedRgba::rgb(255, 255, 0),
stripe_even: PackedRgba::BLACK,
stripe_odd: PackedRgba::rgb(24, 24, 24),
}
}
pub fn colorblind() -> Self {
Self {
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), }
}
pub fn solarized_dark() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(7, 54, 66),
}
}
pub fn solarized_light() -> Self {
Self {
accent: PackedRgba::rgb(38, 139, 210),
accent_alt: PackedRgba::rgb(108, 113, 196),
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),
tool: PackedRgba::rgb(203, 75, 22),
system: PackedRgba::rgb(177, 133, 0), stripe_even: PackedRgba::rgb(253, 246, 227),
stripe_odd: PackedRgba::rgb(238, 232, 213),
}
}
pub fn monokai() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(48, 49, 42),
}
}
pub fn gruvbox_dark() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(50, 48, 47),
}
}
pub fn one_dark() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(49, 53, 63),
}
}
pub fn rose_pine() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(33, 30, 46),
}
}
pub fn everforest() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(47, 55, 42),
}
}
pub fn kanagawa() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(42, 42, 54),
}
}
pub fn ayu_mirage() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(44, 51, 64),
}
}
pub fn nightfox() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(29, 33, 46),
}
}
pub fn cyberpunk_aurora() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(22, 10, 48),
}
}
pub fn synthwave_84() -> Self {
Self {
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),
stripe_odd: PackedRgba::rgb(44, 28, 68),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_terminal_width_from_cols_narrow() {
assert_eq!(TerminalWidth::from_cols(40), TerminalWidth::Narrow);
assert_eq!(TerminalWidth::from_cols(79), TerminalWidth::Narrow);
}
#[test]
fn test_terminal_width_from_cols_normal() {
assert_eq!(TerminalWidth::from_cols(80), TerminalWidth::Normal);
assert_eq!(TerminalWidth::from_cols(100), TerminalWidth::Normal);
assert_eq!(TerminalWidth::from_cols(120), TerminalWidth::Normal);
}
#[test]
fn test_terminal_width_from_cols_wide() {
assert_eq!(TerminalWidth::from_cols(121), TerminalWidth::Wide);
assert_eq!(TerminalWidth::from_cols(200), TerminalWidth::Wide);
}
#[test]
fn test_terminal_width_border_color() {
assert_eq!(TerminalWidth::Narrow.border_color(), colors::BORDER_MINIMAL);
assert_eq!(
TerminalWidth::Normal.border_color(),
colors::BORDER_STANDARD
);
assert_eq!(
TerminalWidth::Wide.border_color(),
colors::BORDER_EMPHASIZED
);
}
#[test]
fn test_terminal_width_show_decorations() {
assert!(!TerminalWidth::Narrow.show_decorations());
assert!(TerminalWidth::Normal.show_decorations());
assert!(TerminalWidth::Wide.show_decorations());
}
#[test]
fn test_terminal_width_show_extended_info() {
assert!(!TerminalWidth::Narrow.show_extended_info());
assert!(!TerminalWidth::Normal.show_extended_info());
assert!(TerminalWidth::Wide.show_extended_info());
}
#[test]
fn test_gradient_shades_header() {
let shades = GradientShades::header();
assert_eq!(shades.dark, colors::GRADIENT_HEADER_TOP);
assert_eq!(shades.mid, colors::GRADIENT_HEADER_MID);
assert_eq!(shades.light, colors::GRADIENT_HEADER_BOT);
}
#[test]
fn test_gradient_shades_pill() {
let shades = GradientShades::pill();
assert_eq!(shades.dark, colors::GRADIENT_PILL_LEFT);
assert_eq!(shades.mid, colors::GRADIENT_PILL_CENTER);
assert_eq!(shades.light, colors::GRADIENT_PILL_RIGHT);
}
#[test]
fn test_gradient_shades_styles() {
let shades = GradientShades::header();
let (dark, mid, light) = shades.styles();
assert_eq!(dark.bg, Some(shades.dark));
assert_eq!(mid.bg, Some(shades.mid));
assert_eq!(light.bg, Some(shades.light));
}
#[test]
fn test_adaptive_borders_for_width_narrow() {
let borders = AdaptiveBorders::for_width(60);
assert_eq!(borders.width_class, TerminalWidth::Narrow);
assert!(!borders.use_double);
assert!(!borders.show_corners);
}
#[test]
fn test_adaptive_borders_for_width_normal() {
let borders = AdaptiveBorders::for_width(100);
assert_eq!(borders.width_class, TerminalWidth::Normal);
assert!(!borders.use_double);
assert!(borders.show_corners);
}
#[test]
fn test_adaptive_borders_for_width_wide() {
let borders = AdaptiveBorders::for_width(150);
assert_eq!(borders.width_class, TerminalWidth::Wide);
assert!(borders.use_double);
assert!(borders.show_corners);
}
#[test]
fn test_adaptive_borders_focused() {
let borders = AdaptiveBorders::focused(100);
assert_eq!(borders.color, colors::BORDER_FOCUS);
}
#[test]
fn test_theme_palette_light() {
let palette = ThemePalette::light();
assert_eq!(palette.bg, PackedRgba::rgb(250, 250, 252));
assert_eq!(palette.fg, PackedRgba::rgb(36, 41, 46));
}
#[test]
fn test_theme_palette_dark() {
let palette = ThemePalette::dark();
assert_eq!(palette.bg, colors::BG_DEEP);
assert_eq!(palette.fg, colors::TEXT_PRIMARY);
}
#[test]
fn test_theme_palette_catppuccin() {
let palette = ThemePalette::catppuccin();
assert_eq!(palette.bg, PackedRgba::rgb(30, 30, 46));
}
#[test]
fn test_theme_palette_dracula() {
let palette = ThemePalette::dracula();
assert_eq!(palette.bg, PackedRgba::rgb(40, 42, 54));
}
#[test]
fn test_theme_palette_nord() {
let palette = ThemePalette::nord();
assert_eq!(palette.bg, PackedRgba::rgb(46, 52, 64));
}
#[test]
fn test_theme_palette_high_contrast() {
let palette = ThemePalette::high_contrast();
assert_eq!(palette.bg, PackedRgba::rgb(0, 0, 0));
assert_eq!(palette.fg, PackedRgba::rgb(255, 255, 255));
}
#[test]
fn test_theme_palette_agent_pane_known_agents() {
let claude = ThemePalette::agent_pane("claude_code");
assert_eq!(claude.bg, colors::AGENT_CLAUDE_BG);
let codex = ThemePalette::agent_pane("codex");
assert_eq!(codex.bg, colors::AGENT_CODEX_BG);
let gemini = ThemePalette::agent_pane("gemini_cli");
assert_eq!(gemini.bg, colors::AGENT_GEMINI_BG);
let chatgpt = ThemePalette::agent_pane("chatgpt");
assert_eq!(chatgpt.bg, colors::AGENT_CHATGPT_BG);
}
#[test]
fn test_theme_palette_agent_pane_unknown_agent() {
let unknown = ThemePalette::agent_pane("unknown_agent");
assert_eq!(unknown.bg, colors::BG_DEEP);
}
#[test]
fn test_theme_palette_agent_icon() {
assert_eq!(ThemePalette::agent_icon("codex"), "◆");
assert_eq!(ThemePalette::agent_icon("claude_code"), "●");
assert_eq!(ThemePalette::agent_icon("gemini"), "◇");
assert_eq!(ThemePalette::agent_icon("chatgpt"), "○");
assert_eq!(ThemePalette::agent_icon("unknown"), "•");
}
#[test]
fn test_theme_palette_role_theme() {
let palette = ThemePalette::dark();
let user_theme = palette.role_theme("user");
assert_eq!(user_theme.fg, palette.user);
let agent_theme = palette.role_theme("assistant");
assert_eq!(agent_theme.fg, palette.agent);
let tool_theme = palette.role_theme("tool");
assert_eq!(tool_theme.fg, palette.tool);
let system_theme = palette.role_theme("system");
assert_eq!(system_theme.fg, palette.system);
}
#[test]
fn test_contrast_level_from_ratio() {
assert_eq!(ContrastLevel::from_ratio(2.0), ContrastLevel::Fail);
assert_eq!(ContrastLevel::from_ratio(3.5), ContrastLevel::AALarge);
assert_eq!(ContrastLevel::from_ratio(5.0), ContrastLevel::AA);
assert_eq!(ContrastLevel::from_ratio(8.0), ContrastLevel::AAA);
}
#[test]
fn test_contrast_level_meets() {
assert!(ContrastLevel::AAA.meets(ContrastLevel::AA));
assert!(ContrastLevel::AA.meets(ContrastLevel::AALarge));
assert!(!ContrastLevel::Fail.meets(ContrastLevel::AA));
}
#[test]
fn test_contrast_level_name() {
assert_eq!(ContrastLevel::AAA.name(), "AAA");
assert_eq!(ContrastLevel::AA.name(), "AA");
assert_eq!(ContrastLevel::Fail.name(), "Fail");
}
#[test]
fn test_relative_luminance_black() {
let lum = relative_luminance(PackedRgba::rgb(0, 0, 0));
assert!((lum - 0.0).abs() < 0.001);
}
#[test]
fn test_relative_luminance_white() {
let lum = relative_luminance(PackedRgba::rgb(255, 255, 255));
assert!((lum - 1.0).abs() < 0.001);
}
#[test]
fn test_relative_luminance_named_colors() {
let black_lum = relative_luminance(PackedRgba::BLACK);
assert!(black_lum < 0.01);
let white_lum = relative_luminance(PackedRgba::WHITE);
assert!(white_lum > 0.99);
}
#[test]
fn test_contrast_ratio_black_white() {
let ratio = contrast_ratio(PackedRgba::rgb(255, 255, 255), PackedRgba::rgb(0, 0, 0));
assert!(ratio > 20.0);
}
#[test]
fn test_contrast_ratio_same_color() {
let ratio = contrast_ratio(
PackedRgba::rgb(128, 128, 128),
PackedRgba::rgb(128, 128, 128),
);
assert!((ratio - 1.0).abs() < 0.001);
}
#[test]
fn test_check_contrast() {
let level = check_contrast(PackedRgba::rgb(255, 255, 255), PackedRgba::rgb(0, 0, 0));
assert_eq!(level, ContrastLevel::AAA);
let level = check_contrast(
PackedRgba::rgb(100, 100, 100),
PackedRgba::rgb(120, 120, 120),
);
assert_eq!(level, ContrastLevel::Fail);
}
#[test]
fn test_ensure_contrast_already_sufficient() {
let bg = PackedRgba::rgb(0, 0, 0);
let fg = PackedRgba::rgb(255, 255, 255);
let result = ensure_contrast(fg, bg, ContrastLevel::AA);
assert_eq!(result, fg);
}
#[test]
fn test_theme_preset_default() {
let preset = ThemePreset::default();
assert_eq!(preset, ThemePreset::TokyoNight);
}
#[test]
fn test_theme_preset_name() {
assert_eq!(ThemePreset::TokyoNight.name(), "Tokyo Night");
assert_eq!(ThemePreset::Daylight.name(), "Daylight");
assert_eq!(ThemePreset::Catppuccin.name(), "Catppuccin Mocha");
assert_eq!(ThemePreset::Dracula.name(), "Dracula");
assert_eq!(ThemePreset::Nord.name(), "Nord");
assert_eq!(ThemePreset::HighContrast.name(), "High Contrast");
}
#[test]
fn test_theme_preset_next_cycles() {
let mut preset = ThemePreset::TokyoNight;
preset = preset.next();
assert_eq!(preset, ThemePreset::Daylight);
preset = preset.next();
assert_eq!(preset, ThemePreset::Catppuccin);
let mut p = ThemePreset::Colorblind;
p = p.next();
assert_eq!(p, ThemePreset::TokyoNight);
}
#[test]
fn test_theme_preset_prev_cycles() {
let mut preset = ThemePreset::TokyoNight;
preset = preset.prev();
assert_eq!(preset, ThemePreset::Colorblind);
preset = preset.prev();
assert_eq!(preset, ThemePreset::HighContrast);
}
#[test]
fn test_theme_preset_to_palette() {
let palette = ThemePreset::TokyoNight.to_palette();
assert_eq!(palette.bg, ThemePalette::dark().bg);
let palette = ThemePreset::Daylight.to_palette();
assert_eq!(palette.bg, ThemePalette::light().bg);
}
#[test]
fn test_theme_preset_all() {
let all = ThemePreset::all();
assert_eq!(all.len(), 19);
assert!(all.contains(&ThemePreset::TokyoNight));
assert!(all.contains(&ThemePreset::Daylight));
}
#[test]
fn test_chip_style() {
let palette = ThemePalette::dark();
let style = chip_style(palette);
assert_eq!(style.fg, Some(palette.accent_alt));
}
#[test]
fn test_kbd_style() {
let palette = ThemePalette::dark();
let style = kbd_style(palette);
assert_eq!(style.fg, Some(palette.accent));
}
#[test]
fn test_score_style_high() {
let palette = ThemePalette::dark();
let style = score_style(9.0, palette);
assert_eq!(style.fg, Some(colors::STATUS_SUCCESS));
}
#[test]
fn test_score_style_medium() {
let palette = ThemePalette::dark();
let style = score_style(6.0, palette);
assert_eq!(style.fg, Some(palette.accent));
}
#[test]
fn test_score_style_low() {
let palette = ThemePalette::dark();
let style = score_style(3.0, palette);
assert_eq!(style.fg, Some(palette.hint));
}
#[test]
fn test_role_theme_has_all_fields() {
let palette = ThemePalette::dark();
let theme = palette.role_theme("user");
assert_ne!(theme.fg, PackedRgba::TRANSPARENT);
assert_ne!(theme.bg, PackedRgba::TRANSPARENT);
assert_ne!(theme.border, PackedRgba::TRANSPARENT);
assert_ne!(theme.badge, PackedRgba::TRANSPARENT);
}
#[test]
fn test_pane_theme_has_all_fields() {
let pane = ThemePalette::agent_pane("claude");
assert_ne!(pane.fg, PackedRgba::TRANSPARENT);
assert_ne!(pane.bg, PackedRgba::TRANSPARENT);
assert_ne!(pane.accent, PackedRgba::TRANSPARENT);
}
const KNOWN_AGENTS: &[&str] = &[
"claude_code",
"codex",
"cline",
"gemini",
"amp",
"aider",
"cursor",
"chatgpt",
"opencode",
"pi_agent",
"factory",
"clawdbot",
"vibe",
"openclaw",
"copilot",
"copilot_cli",
"crush",
"hermes",
"kimi",
"qwen",
];
#[test]
fn agent_accent_colors_are_pairwise_distinct() {
let accents: Vec<(&str, PackedRgba)> = KNOWN_AGENTS
.iter()
.map(|a| (*a, ThemePalette::agent_pane(a).accent))
.collect();
for i in 0..accents.len() {
for j in (i + 1)..accents.len() {
let (name_a, color_a) = accents[i];
let (name_b, color_b) = accents[j];
assert_ne!(
color_a, color_b,
"Agents {name_a} and {name_b} have identical accent colors — \
users cannot distinguish them"
);
}
}
}
#[test]
fn known_agents_do_not_use_unknown_fallback_background() {
for agent in KNOWN_AGENTS {
let pane = ThemePalette::agent_pane(agent);
assert_ne!(
pane.bg,
colors::BG_DEEP,
"known agent {agent} should have a provider-specific background"
);
}
}
#[test]
fn agent_background_colors_are_pairwise_distinct() {
let bgs: Vec<(&str, PackedRgba)> = KNOWN_AGENTS
.iter()
.map(|a| (*a, ThemePalette::agent_pane(a).bg))
.collect();
for i in 0..bgs.len() {
for j in (i + 1)..bgs.len() {
let (name_a, bg_a) = bgs[i];
let (name_b, bg_b) = bgs[j];
if (name_a == "codex" && name_b == "pi_agent")
|| (name_a == "pi_agent" && name_b == "codex")
{
continue;
}
assert_ne!(
bg_a, bg_b,
"Agents {name_a} and {name_b} have identical background colors"
);
}
}
}
#[test]
fn agent_icons_are_pairwise_distinct() {
let icons: Vec<(&str, &str)> = KNOWN_AGENTS
.iter()
.map(|a| (*a, ThemePalette::agent_icon(a)))
.collect();
for i in 0..icons.len() {
for j in (i + 1)..icons.len() {
let (name_a, icon_a) = icons[i];
let (name_b, icon_b) = icons[j];
assert_ne!(
icon_a, icon_b,
"Agents {name_a} and {name_b} have identical icons"
);
}
}
}
#[test]
fn agent_icons_are_single_char_glyphs() {
for agent in KNOWN_AGENTS {
let icon = ThemePalette::agent_icon(agent);
assert_eq!(
icon.chars().count(),
1,
"Agent {agent} icon should be a single-width glyph for layout stability"
);
}
}
#[test]
fn unknown_agent_falls_back_gracefully() {
let pane = ThemePalette::agent_pane("nonexistent_agent");
assert_ne!(pane.fg, PackedRgba::TRANSPARENT);
assert_ne!(pane.bg, PackedRgba::TRANSPARENT);
assert_ne!(pane.accent, PackedRgba::TRANSPARENT);
let icon = ThemePalette::agent_icon("nonexistent_agent");
assert!(!icon.is_empty(), "unknown agent should get a fallback icon");
}
#[test]
fn role_colors_are_pairwise_distinct_in_palette() {
let palette = ThemePalette::dark();
let roles = [
("user", palette.user),
("agent", palette.agent),
("tool", palette.tool),
("system", palette.system),
];
for i in 0..roles.len() {
for j in (i + 1)..roles.len() {
let (name_a, color_a) = roles[i];
let (name_b, color_b) = roles[j];
assert_ne!(
color_a, color_b,
"ThemePalette::dark() role {name_a} and {name_b} have identical colors"
);
}
}
}
}