use ratatui::style::{Color, Modifier, Style};
use super::icons::verb as icons_verb;
use super::tokens::TokenResolver;
#[allow(dead_code)]
pub mod solarized {
use ratatui::style::Color;
pub const BASE03: Color = Color::Rgb(0, 43, 54); pub const BASE02: Color = Color::Rgb(7, 54, 66); pub const BASE01: Color = Color::Rgb(88, 110, 117); pub const BASE00: Color = Color::Rgb(101, 123, 131); pub const BASE0: Color = Color::Rgb(131, 148, 150); pub const BASE1: Color = Color::Rgb(147, 161, 161); pub const BASE2: Color = Color::Rgb(238, 232, 213); pub const BASE3: Color = Color::Rgb(253, 246, 227);
pub const YELLOW: Color = Color::Rgb(181, 137, 0); pub const ORANGE: Color = Color::Rgb(203, 75, 22); pub const RED: Color = Color::Rgb(220, 50, 47); pub const MAGENTA: Color = Color::Rgb(211, 54, 130); pub const VIOLET: Color = Color::Rgb(108, 113, 196); pub const BLUE: Color = Color::Rgb(38, 139, 210); pub const CYAN: Color = Color::Rgb(42, 161, 152); pub const GREEN: Color = Color::Rgb(133, 153, 0);
pub fn accent_at(frame: u8) -> Color {
let idx = (frame / 32) % 8;
match idx {
0 => YELLOW,
1 => ORANGE,
2 => RED,
3 => MAGENTA,
4 => VIOLET,
5 => BLUE,
6 => CYAN,
_ => GREEN,
}
}
pub fn pulse_intensity(frame: u8) -> f32 {
let t = (frame as f32 / 255.0) * std::f32::consts::PI * 2.0;
(t.sin() + 1.0) / 2.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorMode {
#[default]
TrueColor,
Color256,
Color16,
}
impl ColorMode {
pub fn detect() -> Self {
if let Ok(colorterm) = std::env::var("COLORTERM") {
let ct = colorterm.to_lowercase();
if ct == "truecolor" || ct == "24bit" {
return Self::TrueColor;
}
}
if let Ok(term) = std::env::var("TERM") {
if term.contains("256color") || term.contains("24bit") {
return Self::Color256;
}
if term.contains("truecolor") {
return Self::TrueColor;
}
}
Self::Color16
}
pub fn supports_rgb(&self) -> bool {
matches!(self, Self::TrueColor)
}
pub fn supports_256(&self) -> bool {
matches!(self, Self::TrueColor | Self::Color256)
}
pub fn adapt_color(&self, color: Color) -> Color {
match self {
Self::TrueColor => color,
Self::Color256 => Self::rgb_to_256(color),
Self::Color16 => Self::rgb_to_16(color),
}
}
fn rgb_to_256(color: Color) -> Color {
match color {
Color::Rgb(r, g, b) => {
let r_idx = (r as u16 * 5 / 255) as u8;
let g_idx = (g as u16 * 5 / 255) as u8;
let b_idx = (b as u16 * 5 / 255) as u8;
let idx = 16 + 36 * r_idx + 6 * g_idx + b_idx;
Color::Indexed(idx)
}
other => other,
}
}
fn rgb_to_16(color: Color) -> Color {
match color {
Color::Rgb(r, g, b) => {
let luma = (r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000;
let bright = luma > 127;
let max = r.max(g).max(b);
let min = r.min(g).min(b);
let saturation = if max == 0 {
0
} else {
(max - min) as u16 * 255 / max as u16
};
if saturation < 50 {
if bright {
Color::White
} else {
Color::DarkGray
}
} else if r >= g && r >= b {
if g > 100 && g > r / 2 {
if bright {
Color::Yellow
} else {
Color::LightYellow
}
} else if bright {
Color::LightRed
} else {
Color::Red
}
} else if g >= r && g >= b {
if b > 100 && b > g / 2 {
if bright {
Color::Cyan
} else {
Color::LightCyan
}
} else if bright {
Color::LightGreen
} else {
Color::Green
}
} else {
if r > 100 && r > b / 2 {
if bright {
Color::LightMagenta
} else {
Color::Magenta
}
} else if bright {
Color::LightBlue
} else {
Color::Blue
}
}
}
other => other,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum VerbColor {
Infer, Exec, Fetch, Invoke, Agent, Spawn, User, }
impl VerbColor {
pub fn rgb(&self) -> Color {
match self {
Self::Infer => Color::Rgb(139, 92, 246), Self::Exec => Color::Rgb(245, 158, 11), Self::Fetch => Color::Rgb(6, 182, 212), Self::Invoke => Color::Rgb(16, 185, 129), Self::Agent => Color::Rgb(244, 63, 94), Self::Spawn => Color::Rgb(253, 164, 175), Self::User => Color::Rgb(14, 165, 233), }
}
pub fn glow(&self) -> Color {
match self {
Self::Infer => Color::Rgb(167, 139, 250), Self::Exec => Color::Rgb(251, 191, 36), Self::Fetch => Color::Rgb(34, 211, 238), Self::Invoke => Color::Rgb(52, 211, 153), Self::Agent => Color::Rgb(251, 113, 133), Self::Spawn => Color::Rgb(254, 205, 211), Self::User => Color::Rgb(56, 189, 248), }
}
pub fn muted(&self) -> Color {
match self {
Self::Infer => Color::Rgb(97, 64, 171),
Self::Exec => Color::Rgb(171, 110, 8),
Self::Fetch => Color::Rgb(4, 127, 148),
Self::Invoke => Color::Rgb(11, 129, 90),
Self::Agent => Color::Rgb(170, 44, 66),
Self::Spawn => Color::Rgb(177, 115, 122), Self::User => Color::Rgb(10, 115, 163), }
}
pub fn subtle(&self) -> Color {
match self {
Self::Infer => Color::Rgb(55, 48, 83), Self::Exec => Color::Rgb(69, 53, 18), Self::Fetch => Color::Rgb(22, 57, 67), Self::Invoke => Color::Rgb(20, 61, 47), Self::Agent => Color::Rgb(68, 32, 41), Self::Spawn => Color::Rgb(70, 45, 48), Self::User => Color::Rgb(12, 51, 70), }
}
pub fn icon(&self) -> &'static str {
match self {
Self::Infer => icons_verb::INFER,
Self::Exec => icons_verb::EXEC,
Self::Fetch => icons_verb::FETCH,
Self::Invoke => icons_verb::INVOKE,
Self::Agent => icons_verb::AGENT,
Self::Spawn => icons_verb::SUBAGENT,
Self::User => "👤",
}
}
pub fn subagent_icon() -> &'static str {
icons_verb::SUBAGENT
}
pub fn rgb_tuple(&self) -> (u8, u8, u8) {
match self {
Self::Infer => (139, 92, 246), Self::Exec => (245, 158, 11), Self::Fetch => (6, 182, 212), Self::Invoke => (16, 185, 129), Self::Agent => (244, 63, 94), Self::Spawn => (253, 164, 175), Self::User => (14, 165, 233), }
}
pub fn glow_tuple(&self) -> (u8, u8, u8) {
match self {
Self::Infer => (167, 139, 250), Self::Exec => (251, 191, 36), Self::Fetch => (34, 211, 238), Self::Invoke => (52, 211, 153), Self::Agent => (251, 113, 133), Self::Spawn => (254, 205, 211), Self::User => (56, 189, 248), }
}
pub fn muted_tuple(&self) -> (u8, u8, u8) {
match self {
Self::Infer => (97, 64, 171), Self::Exec => (171, 110, 8), Self::Fetch => (4, 127, 148), Self::Invoke => (11, 129, 90), Self::Agent => (170, 44, 66), Self::Spawn => (177, 115, 122), Self::User => (10, 115, 163), }
}
pub fn icon_ascii(&self) -> &'static str {
match self {
Self::Infer => icons_verb::INFER_ASCII,
Self::Exec => icons_verb::EXEC_ASCII,
Self::Fetch => icons_verb::FETCH_ASCII,
Self::Invoke => icons_verb::INVOKE_ASCII,
Self::Agent => icons_verb::AGENT_ASCII,
Self::Spawn => icons_verb::SUBAGENT_ASCII,
Self::User => "[U]",
}
}
pub fn hex(&self) -> &'static str {
match self {
Self::Infer => "#8b5cf6",
Self::Exec => "#f59e0b",
Self::Fetch => "#06b6d4",
Self::Invoke => "#10b981",
Self::Agent => "#f43f5e",
Self::Spawn => "#fda4af",
Self::User => "#0ea5e9",
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Infer => "INFER",
Self::Exec => "EXEC",
Self::Fetch => "FETCH",
Self::Invoke => "INVOKE",
Self::Agent => "AGENT",
Self::Spawn => "SPAWN",
Self::User => "USER",
}
}
pub fn icon_label(&self) -> String {
format!("{} {}", self.icon(), self.label())
}
pub fn border_rgb(&self) -> Color {
match self {
Self::Infer => Color::Rgb(124, 58, 237), Self::Exec => Color::Rgb(217, 119, 6), Self::Fetch => Color::Rgb(8, 145, 178), Self::Invoke => Color::Rgb(5, 150, 105), Self::Agent => Color::Rgb(225, 29, 72), Self::Spawn => Color::Rgb(251, 113, 133), Self::User => Color::Rgb(2, 132, 199), }
}
pub fn color_from_resolver(&self, resolver: &TokenResolver) -> Color {
match self {
Self::Infer => resolver.verb_infer(),
Self::Exec => resolver.verb_exec(),
Self::Fetch => resolver.verb_fetch(),
Self::Invoke => resolver.verb_invoke(),
Self::Agent => resolver.verb_agent(),
Self::Spawn => self.rgb(),
Self::User => self.rgb(),
}
}
pub fn from_verb(verb: &str) -> Self {
match verb.to_lowercase().as_str() {
"infer" => Self::Infer,
"exec" => Self::Exec,
"fetch" => Self::Fetch,
"invoke" => Self::Invoke,
"agent" => Self::Agent,
"spawn" | "subagent" => Self::Spawn,
_ => Self::Infer, }
}
pub fn animated(&self, frame: u8) -> Color {
if (frame / 8) % 2 == 0 {
self.rgb()
} else {
self.glow()
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemeMode {
#[default]
Dark,
Light,
Solarized,
}
impl ThemeMode {
pub fn toggle(&self) -> Self {
match self {
Self::Dark => Self::Light,
Self::Light => Self::Dark,
Self::Solarized => Self::Dark,
}
}
pub fn cycle(&self) -> Self {
match self {
Self::Dark => Self::Light,
Self::Light => Self::Solarized,
Self::Solarized => Self::Dark,
}
}
pub fn theme(&self) -> Theme {
match self {
Self::Dark => Theme::dark(),
Self::Light => Theme::light(),
Self::Solarized => Theme::solarized(),
}
}
pub fn label(&self) -> &'static str {
match self {
Self::Dark => "Dark",
Self::Light => "Light",
Self::Solarized => "Solarized",
}
}
}
#[derive(Debug, Clone)]
pub struct Theme {
pub realm_shared: Color,
pub realm_org: Color,
pub trait_defined: Color,
pub trait_authored: Color,
pub trait_imported: Color,
pub trait_generated: Color,
pub trait_retrieved: Color,
pub status_pending: Color,
pub status_running: Color,
pub status_success: Color,
pub status_failed: Color,
pub status_paused: Color,
pub phase_launch: Color, pub phase_orbital: Color, pub phase_rendezvous: Color,
pub mcp_describe: Color,
pub mcp_context: Color,
pub mcp_search: Color,
pub mcp_audit: Color,
pub mcp_write: Color,
pub border_normal: Color,
pub border_focused: Color,
pub text: Color,
pub text_primary: Color,
pub text_secondary: Color,
pub text_muted: Color,
pub background: Color,
pub highlight: Color,
pub selection: Color,
pub git_added: Color, pub git_modified: Color, pub git_deleted: Color,
pub scrollbar_thumb: Color, pub scrollbar_track: Color, pub scrollbar_arrows: Color, }
impl Default for Theme {
fn default() -> Self {
Self::cosmic_dark()
}
}
impl Theme {
pub fn novanet() -> Self {
Self::dark()
}
pub fn dark() -> Self {
Self::default()
}
pub fn from_name(name: &super::config::ThemeName) -> Self {
match name {
super::config::ThemeName::Dark => Self::dark(),
super::config::ThemeName::Light => Self::light(),
super::config::ThemeName::Solarized => Self::solarized(),
}
}
pub fn light() -> Self {
Self::cosmic_light()
}
pub fn solarized() -> Self {
Self::cosmic_dark()
}
pub fn mcp_tool_color(&self, tool: &str) -> Color {
match tool {
t if t.contains("describe") => self.mcp_describe,
t if t.contains("search") => self.mcp_search,
t if t.contains("introspect") => self.mcp_describe, t if t.contains("context") => self.mcp_context,
t if t.contains("write") => self.mcp_write,
t if t.contains("audit") => self.mcp_audit,
t if t.contains("batch") => self.mcp_search, _ => self.text_secondary,
}
}
pub fn status_style(&self, status: TaskStatus) -> Style {
let color = match status {
TaskStatus::Queued => self.text_muted,
TaskStatus::Pending => self.status_pending,
TaskStatus::Running => self.status_running,
TaskStatus::Success => self.status_success,
TaskStatus::Failed => self.status_failed,
TaskStatus::Paused => self.status_paused,
TaskStatus::Skipped => self.text_muted,
};
Style::default().fg(color)
}
pub fn border_style(&self, focused: bool) -> Style {
if focused {
Style::default()
.fg(self.border_focused)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(self.border_normal)
}
}
pub fn text_style(&self) -> Style {
Style::default().fg(self.text_primary)
}
pub fn text_secondary_style(&self) -> Style {
Style::default().fg(self.text_secondary)
}
pub fn text_muted_style(&self) -> Style {
Style::default().fg(self.text_muted)
}
pub fn highlight_style(&self) -> Style {
Style::default()
.fg(self.highlight)
.add_modifier(Modifier::BOLD)
}
pub fn verb_color(&self, verb: VerbColor) -> Color {
verb.rgb()
}
pub fn verb_color_muted(&self, verb: VerbColor) -> Color {
verb.muted()
}
pub fn animated_highlight(&self, frame: u8) -> Style {
let color = solarized::accent_at(frame);
Style::default().fg(color).add_modifier(Modifier::BOLD)
}
pub fn border_glow(&self, frame: u8) -> Style {
let intensity = solarized::pulse_intensity(frame);
if intensity > 0.5 {
Style::default()
.fg(self.border_focused)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(self.highlight)
}
}
pub fn success_pulse(&self, frame: u8) -> Style {
let intensity = solarized::pulse_intensity(frame);
let color = if intensity > 0.7 {
solarized::GREEN
} else {
self.status_success
};
Style::default().fg(color).add_modifier(Modifier::BOLD)
}
pub fn running_pulse(&self, frame: u8) -> Style {
let intensity = solarized::pulse_intensity(frame);
let color = if intensity > 0.5 {
solarized::ORANGE
} else {
solarized::YELLOW
};
Style::default().fg(color).add_modifier(Modifier::BOLD)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskStatus {
Queued,
Pending,
Running,
Success,
Failed,
Paused,
Skipped,
}
impl TaskStatus {
pub fn color(&self, theme: &Theme) -> Color {
match self {
Self::Queued => theme.text_muted,
Self::Pending => theme.status_pending,
Self::Running => theme.status_running,
Self::Success => theme.status_success,
Self::Failed => theme.status_failed,
Self::Paused => theme.status_paused,
Self::Skipped => theme.text_muted,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MissionPhase {
Preflight,
Countdown,
Launch,
Orbital,
Rendezvous,
MissionSuccess,
Abort,
Pause,
}
impl MissionPhase {
pub fn icon(&self) -> &'static str {
match self {
Self::Preflight => "◦",
Self::Countdown => "⊙",
Self::Launch => "⊛",
Self::Orbital => "◉",
Self::Rendezvous => "◈",
Self::MissionSuccess => "✦",
Self::Abort => "⊗",
Self::Pause => "⏸",
}
}
pub fn name(&self) -> &'static str {
match self {
Self::Preflight => "PREFLIGHT",
Self::Countdown => "COUNTDOWN",
Self::Launch => "LAUNCH",
Self::Orbital => "ORBITAL",
Self::Rendezvous => "RENDEZVOUS",
Self::MissionSuccess => "MISSION SUCCESS",
Self::Abort => "ABORT",
Self::Pause => "PAUSED",
}
}
pub fn color(&self, theme: &Theme) -> Color {
match self {
Self::Preflight => theme.status_pending,
Self::Countdown => theme.status_running,
Self::Launch => theme.phase_launch,
Self::Orbital => theme.phase_orbital,
Self::Rendezvous => theme.phase_rendezvous,
Self::MissionSuccess => theme.status_success,
Self::Abort => theme.status_failed,
Self::Pause => theme.status_paused,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_theme_default_creates_valid_colors() {
let theme = Theme::default();
assert_ne!(theme.realm_shared, Color::Reset);
assert_ne!(theme.status_running, Color::Reset);
assert_ne!(theme.mcp_describe, Color::Reset);
}
#[test]
fn test_mcp_tool_color_matches_tool_name() {
let theme = Theme::novanet();
assert_eq!(theme.mcp_tool_color("novanet_describe"), theme.mcp_describe);
assert_eq!(theme.mcp_tool_color("novanet_search"), theme.mcp_search);
assert_eq!(
theme.mcp_tool_color("novanet_introspect"),
theme.mcp_describe
);
assert_eq!(theme.mcp_tool_color("novanet_context"), theme.mcp_context);
assert_eq!(theme.mcp_tool_color("novanet_write"), theme.mcp_write);
assert_eq!(theme.mcp_tool_color("novanet_audit"), theme.mcp_audit);
assert_eq!(theme.mcp_tool_color("novanet_batch"), theme.mcp_search);
}
#[test]
fn test_status_style_returns_correct_color() {
let theme = Theme::novanet();
let style = theme.status_style(TaskStatus::Running);
assert_eq!(style.fg, Some(theme.status_running));
}
#[test]
fn test_border_style_focused_vs_unfocused() {
let theme = Theme::novanet();
let focused = theme.border_style(true);
let unfocused = theme.border_style(false);
assert_ne!(focused.fg, unfocused.fg);
}
#[test]
fn test_mission_phase_icons() {
assert_eq!(MissionPhase::Preflight.icon(), "◦");
assert_eq!(MissionPhase::Orbital.icon(), "◉");
assert_eq!(MissionPhase::MissionSuccess.icon(), "✦");
assert_eq!(MissionPhase::Abort.icon(), "⊗");
}
#[test]
fn test_mission_phase_names() {
assert_eq!(MissionPhase::Countdown.name(), "COUNTDOWN");
assert_eq!(MissionPhase::MissionSuccess.name(), "MISSION SUCCESS");
}
#[test]
fn test_theme_mode_default_is_dark() {
let mode = ThemeMode::default();
assert_eq!(mode, ThemeMode::Dark);
}
#[test]
fn test_theme_mode_toggle() {
let mode = ThemeMode::Dark;
assert_eq!(mode.toggle(), ThemeMode::Light);
let mode = ThemeMode::Light;
assert_eq!(mode.toggle(), ThemeMode::Dark);
}
#[test]
fn test_theme_mode_theme_returns_correct_theme() {
let dark_theme = ThemeMode::Dark.theme();
let light_theme = ThemeMode::Light.theme();
assert_eq!(dark_theme.background, Color::Rgb(15, 23, 42));
assert_eq!(light_theme.background, Color::Rgb(248, 250, 252)); }
#[test]
fn test_light_theme_colors_differ_from_dark() {
let dark = Theme::dark();
let light = Theme::light();
assert_ne!(dark.text_primary, light.text_primary);
assert_ne!(dark.background, light.background);
}
#[test]
fn test_verb_color_rgb_returns_correct_colors() {
assert_eq!(VerbColor::Infer.rgb(), Color::Rgb(139, 92, 246)); assert_eq!(VerbColor::Exec.rgb(), Color::Rgb(245, 158, 11)); assert_eq!(VerbColor::Fetch.rgb(), Color::Rgb(6, 182, 212)); assert_eq!(VerbColor::Invoke.rgb(), Color::Rgb(16, 185, 129)); assert_eq!(VerbColor::Agent.rgb(), Color::Rgb(244, 63, 94)); }
#[test]
fn test_verb_color_muted_returns_darker_colors() {
assert_ne!(VerbColor::Infer.muted(), VerbColor::Infer.rgb());
assert_ne!(VerbColor::Exec.muted(), VerbColor::Exec.rgb());
assert_ne!(VerbColor::Fetch.muted(), VerbColor::Fetch.rgb());
assert_ne!(VerbColor::Invoke.muted(), VerbColor::Invoke.rgb());
assert_ne!(VerbColor::Agent.muted(), VerbColor::Agent.rgb());
assert_eq!(VerbColor::Infer.muted(), Color::Rgb(97, 64, 171));
assert_eq!(VerbColor::Agent.muted(), Color::Rgb(170, 44, 66));
}
#[test]
fn test_verb_color_icons() {
assert_eq!(VerbColor::Infer.icon(), "⚡"); assert_eq!(VerbColor::Exec.icon(), "📟"); assert_eq!(VerbColor::Fetch.icon(), "🛰️"); assert_eq!(VerbColor::Invoke.icon(), "🔌"); assert_eq!(VerbColor::Agent.icon(), "🐔"); assert_eq!(VerbColor::subagent_icon(), "🐤"); }
#[test]
fn test_verb_color_from_verb_string() {
assert_eq!(VerbColor::from_verb("infer"), VerbColor::Infer);
assert_eq!(VerbColor::from_verb("exec"), VerbColor::Exec);
assert_eq!(VerbColor::from_verb("fetch"), VerbColor::Fetch);
assert_eq!(VerbColor::from_verb("invoke"), VerbColor::Invoke);
assert_eq!(VerbColor::from_verb("agent"), VerbColor::Agent);
}
#[test]
fn test_verb_color_from_verb_case_insensitive() {
assert_eq!(VerbColor::from_verb("INFER"), VerbColor::Infer);
assert_eq!(VerbColor::from_verb("Exec"), VerbColor::Exec);
assert_eq!(VerbColor::from_verb("FeTcH"), VerbColor::Fetch);
}
#[test]
fn test_verb_color_from_verb_unknown_defaults_to_infer() {
assert_eq!(VerbColor::from_verb("unknown"), VerbColor::Infer);
assert_eq!(VerbColor::from_verb(""), VerbColor::Infer);
assert_eq!(VerbColor::from_verb("transform"), VerbColor::Infer);
}
#[test]
fn test_theme_verb_color_methods() {
let theme = Theme::novanet();
assert_eq!(theme.verb_color(VerbColor::Infer), Color::Rgb(139, 92, 246));
assert_eq!(theme.verb_color(VerbColor::Agent), Color::Rgb(244, 63, 94));
assert_eq!(
theme.verb_color_muted(VerbColor::Infer),
Color::Rgb(97, 64, 171)
);
assert_eq!(
theme.verb_color_muted(VerbColor::Agent),
Color::Rgb(170, 44, 66)
);
}
#[test]
fn test_verb_color_all_variants_have_distinct_colors() {
let colors = [
VerbColor::Infer.rgb(),
VerbColor::Exec.rgb(),
VerbColor::Fetch.rgb(),
VerbColor::Invoke.rgb(),
VerbColor::Agent.rgb(),
];
for i in 0..colors.len() {
for j in (i + 1)..colors.len() {
assert_ne!(
colors[i], colors[j],
"Verb colors {} and {} are identical",
i, j
);
}
}
}
#[test]
fn test_verb_color_glow_is_brighter() {
assert_ne!(VerbColor::Infer.glow(), VerbColor::Infer.rgb());
assert_ne!(VerbColor::Exec.glow(), VerbColor::Exec.rgb());
assert_ne!(VerbColor::Fetch.glow(), VerbColor::Fetch.rgb());
assert_ne!(VerbColor::Invoke.glow(), VerbColor::Invoke.rgb());
assert_ne!(VerbColor::Agent.glow(), VerbColor::Agent.rgb());
assert_eq!(VerbColor::Infer.glow(), Color::Rgb(167, 139, 250)); assert_eq!(VerbColor::Exec.glow(), Color::Rgb(251, 191, 36)); }
#[test]
fn test_verb_color_subtle_is_darker() {
assert_ne!(VerbColor::Infer.subtle(), VerbColor::Infer.rgb());
assert_ne!(VerbColor::Infer.subtle(), VerbColor::Infer.muted());
assert_ne!(VerbColor::Agent.subtle(), VerbColor::Agent.rgb());
}
#[test]
fn test_verb_color_animated_alternates() {
assert_eq!(VerbColor::Infer.animated(0), VerbColor::Infer.rgb());
assert_eq!(VerbColor::Infer.animated(7), VerbColor::Infer.rgb());
assert_eq!(VerbColor::Infer.animated(8), VerbColor::Infer.glow());
assert_eq!(VerbColor::Infer.animated(15), VerbColor::Infer.glow());
assert_eq!(VerbColor::Infer.animated(16), VerbColor::Infer.rgb());
}
#[test]
fn test_verb_color_icon_ascii() {
assert_eq!(VerbColor::Infer.icon_ascii(), "[I]");
assert_eq!(VerbColor::Exec.icon_ascii(), "[X]");
assert_eq!(VerbColor::Fetch.icon_ascii(), "[F]");
assert_eq!(VerbColor::Invoke.icon_ascii(), "[V]");
assert_eq!(VerbColor::Agent.icon_ascii(), "[A]");
}
#[test]
fn test_color_mode_default_is_truecolor() {
let mode = ColorMode::default();
assert_eq!(mode, ColorMode::TrueColor);
}
#[test]
fn test_color_mode_supports_rgb_truecolor() {
assert!(ColorMode::TrueColor.supports_rgb());
assert!(!ColorMode::Color256.supports_rgb());
assert!(!ColorMode::Color16.supports_rgb());
}
#[test]
fn test_color_mode_supports_256() {
assert!(ColorMode::TrueColor.supports_256());
assert!(ColorMode::Color256.supports_256());
assert!(!ColorMode::Color16.supports_256());
}
#[test]
fn test_color_mode_adapt_color_truecolor_unchanged() {
let color = Color::Rgb(139, 92, 246);
assert_eq!(ColorMode::TrueColor.adapt_color(color), color);
}
#[test]
fn test_color_mode_adapt_color_256_converts_to_indexed() {
let color = Color::Rgb(139, 92, 246);
let adapted = ColorMode::Color256.adapt_color(color);
match adapted {
Color::Indexed(_) => (), _ => panic!("Expected Indexed color, got {:?}", adapted),
}
}
#[test]
fn test_color_mode_adapt_color_16_converts_to_ansi() {
let color = Color::Rgb(139, 92, 246);
let adapted = ColorMode::Color16.adapt_color(color);
if let Color::Rgb(_, _, _) = adapted {
panic!("Should not be RGB for Color16 mode")
}
}
#[test]
fn test_color_mode_256_adapt_converts_to_indexed() {
let mode = ColorMode::Color256;
let red = mode.adapt_color(Color::Rgb(255, 0, 0));
let green = mode.adapt_color(Color::Rgb(0, 255, 0));
let blue = mode.adapt_color(Color::Rgb(0, 0, 255));
match (red, green, blue) {
(Color::Indexed(r), Color::Indexed(g), Color::Indexed(b)) => {
assert!((16..=231).contains(&r));
assert!((16..=231).contains(&g));
assert!((16..=231).contains(&b));
}
_ => panic!("Expected Indexed colors"),
}
}
#[test]
fn test_color_mode_256_adapt_preserves_non_rgb() {
let mode = ColorMode::Color256;
let indexed = mode.adapt_color(Color::Indexed(100));
assert_eq!(indexed, Color::Indexed(100));
let white = mode.adapt_color(Color::White);
assert_eq!(white, Color::White);
}
#[test]
fn test_color_mode_16_adapt_grayscale() {
let mode = ColorMode::Color16;
let light_gray = mode.adapt_color(Color::Rgb(200, 200, 200));
assert_eq!(light_gray, Color::White);
let dark_gray = mode.adapt_color(Color::Rgb(50, 50, 50));
assert_eq!(dark_gray, Color::DarkGray);
}
#[test]
fn test_color_mode_16_adapt_red_dominant() {
let mode = ColorMode::Color16;
let red = mode.adapt_color(Color::Rgb(255, 80, 60));
assert_eq!(red, Color::LightRed);
}
#[test]
fn test_color_mode_16_adapt_green_dominant() {
let mode = ColorMode::Color16;
let green = mode.adapt_color(Color::Rgb(30, 200, 30));
assert_eq!(green, Color::LightGreen);
}
#[test]
fn test_color_mode_16_adapt_blue_dominant() {
let mode = ColorMode::Color16;
let blue = mode.adapt_color(Color::Rgb(60, 150, 200));
assert_eq!(blue, Color::LightBlue);
}
#[test]
fn test_color_mode_16_adapt_preserves_non_rgb() {
let mode = ColorMode::Color16;
let white = mode.adapt_color(Color::White);
assert_eq!(white, Color::White);
let indexed = mode.adapt_color(Color::Indexed(50));
assert_eq!(indexed, Color::Indexed(50));
}
#[test]
fn test_theme_dark_is_same_as_default() {
let dark = Theme::dark();
let default = Theme::default();
assert_eq!(dark.realm_shared, default.realm_shared);
assert_eq!(dark.text_primary, default.text_primary);
assert_eq!(dark.background, default.background);
}
#[test]
fn test_theme_novanet_is_dark() {
let novanet = Theme::novanet();
let dark = Theme::dark();
assert_eq!(novanet.background, dark.background);
assert_eq!(novanet.text_primary, dark.text_primary);
}
#[test]
fn test_theme_light_has_inverted_colors() {
let light = Theme::light();
assert_eq!(light.background, Color::Rgb(248, 250, 252));
assert_eq!(light.text_primary, Color::Rgb(15, 23, 42)); }
#[test]
fn test_theme_solarized_maps_to_cosmic_dark() {
let theme = Theme::solarized();
assert_eq!(theme.background, Color::Rgb(15, 23, 42)); assert_eq!(theme.text_primary, Color::Rgb(248, 250, 252)); }
#[test]
fn test_theme_solarized_has_cosmic_status_colors() {
let theme = Theme::solarized();
assert_eq!(theme.status_running, Color::Rgb(245, 158, 11)); assert_eq!(theme.status_success, Color::Rgb(16, 185, 129)); assert_eq!(theme.status_failed, Color::Rgb(239, 68, 68)); }
#[test]
fn test_theme_solarized_equals_dark() {
let dark = Theme::dark();
let solarized = Theme::solarized();
assert_eq!(dark.background, solarized.background);
assert_eq!(dark.text_primary, solarized.text_primary);
}
#[test]
fn test_theme_solarized_differs_from_light() {
let light = Theme::light();
let solarized = Theme::solarized();
assert_ne!(light.background, solarized.background);
assert_ne!(light.text_primary, solarized.text_primary);
}
#[test]
fn test_theme_from_name_dark() {
use super::super::config::ThemeName;
let theme = Theme::from_name(&ThemeName::Dark);
let dark = Theme::dark();
assert_eq!(theme.background, dark.background);
}
#[test]
fn test_theme_from_name_light() {
use super::super::config::ThemeName;
let theme = Theme::from_name(&ThemeName::Light);
let light = Theme::light();
assert_eq!(theme.background, light.background);
}
#[test]
fn test_theme_from_name_solarized() {
use super::super::config::ThemeName;
let theme = Theme::from_name(&ThemeName::Solarized);
let solarized = Theme::solarized();
assert_eq!(theme.background, solarized.background);
}
#[test]
fn test_theme_mcp_tool_color_describe() {
let theme = Theme::default();
assert_eq!(theme.mcp_tool_color("novanet_describe"), theme.mcp_describe);
assert_eq!(theme.mcp_tool_color("describe"), theme.mcp_describe);
}
#[test]
fn test_theme_mcp_tool_color_context() {
let theme = Theme::default();
assert_eq!(theme.mcp_tool_color("novanet_context"), theme.mcp_context);
}
#[test]
fn test_theme_mcp_tool_color_unknown_defaults_to_secondary() {
let theme = Theme::default();
assert_eq!(theme.mcp_tool_color("unknown_tool"), theme.text_secondary);
assert_eq!(theme.mcp_tool_color(""), theme.text_secondary);
}
#[test]
fn test_theme_status_style_pending() {
let theme = Theme::novanet();
let style = theme.status_style(TaskStatus::Pending);
assert_eq!(style.fg, Some(theme.status_pending));
}
#[test]
fn test_theme_status_style_all_variants() {
let theme = Theme::novanet();
let pending = theme.status_style(TaskStatus::Pending);
assert_eq!(pending.fg, Some(theme.status_pending));
let running = theme.status_style(TaskStatus::Running);
assert_eq!(running.fg, Some(theme.status_running));
let success = theme.status_style(TaskStatus::Success);
assert_eq!(success.fg, Some(theme.status_success));
let failed = theme.status_style(TaskStatus::Failed);
assert_eq!(failed.fg, Some(theme.status_failed));
let paused = theme.status_style(TaskStatus::Paused);
assert_eq!(paused.fg, Some(theme.status_paused));
}
#[test]
fn test_theme_border_style_focused_has_bold() {
let theme = Theme::novanet();
let focused = theme.border_style(true);
assert!(focused.add_modifier.contains(Modifier::BOLD));
assert_eq!(focused.fg, Some(theme.border_focused));
}
#[test]
fn test_theme_border_style_unfocused_no_bold() {
let theme = Theme::novanet();
let unfocused = theme.border_style(false);
assert!(!unfocused.add_modifier.contains(Modifier::BOLD));
assert_eq!(unfocused.fg, Some(theme.border_normal));
}
#[test]
fn test_theme_text_style_primary() {
let theme = Theme::novanet();
let style = theme.text_style();
assert_eq!(style.fg, Some(theme.text_primary));
}
#[test]
fn test_theme_text_secondary_style() {
let theme = Theme::novanet();
let style = theme.text_secondary_style();
assert_eq!(style.fg, Some(theme.text_secondary));
}
#[test]
fn test_theme_text_muted_style() {
let theme = Theme::novanet();
let style = theme.text_muted_style();
assert_eq!(style.fg, Some(theme.text_muted));
}
#[test]
fn test_theme_highlight_style_has_bold() {
let theme = Theme::novanet();
let style = theme.highlight_style();
assert!(style.add_modifier.contains(Modifier::BOLD));
assert_eq!(style.fg, Some(theme.highlight));
}
#[test]
fn test_theme_trait_colors_are_distinct() {
let theme = Theme::default();
let traits = [
theme.trait_defined,
theme.trait_authored,
theme.trait_imported,
theme.trait_generated,
theme.trait_retrieved,
];
for i in 0..traits.len() {
for j in (i + 1)..traits.len() {
assert_ne!(
traits[i], traits[j],
"Trait color {} and {} are identical",
i, j
);
}
}
}
#[test]
fn test_theme_realm_colors_are_distinct() {
let theme = Theme::default();
assert_ne!(theme.realm_shared, theme.realm_org);
}
#[test]
fn test_task_status_can_be_created() {
let _ = TaskStatus::Pending;
let _ = TaskStatus::Running;
let _ = TaskStatus::Success;
let _ = TaskStatus::Failed;
let _ = TaskStatus::Paused;
}
#[test]
fn test_task_status_equality() {
assert_eq!(TaskStatus::Pending, TaskStatus::Pending);
assert_ne!(TaskStatus::Pending, TaskStatus::Running);
assert_ne!(TaskStatus::Success, TaskStatus::Failed);
}
#[test]
fn test_task_status_copy_clone() {
let status = TaskStatus::Running;
let copied = status;
assert_eq!(status, copied);
}
#[test]
fn test_mission_phase_preflight_icon_and_name() {
let phase = MissionPhase::Preflight;
assert_eq!(phase.icon(), "◦");
assert_eq!(phase.name(), "PREFLIGHT");
}
#[test]
fn test_mission_phase_countdown_icon_and_name() {
let phase = MissionPhase::Countdown;
assert_eq!(phase.icon(), "⊙");
assert_eq!(phase.name(), "COUNTDOWN");
}
#[test]
fn test_mission_phase_launch_icon_and_name() {
let phase = MissionPhase::Launch;
assert_eq!(phase.icon(), "⊛");
assert_eq!(phase.name(), "LAUNCH");
}
#[test]
fn test_mission_phase_orbital_icon_and_name() {
let phase = MissionPhase::Orbital;
assert_eq!(phase.icon(), "◉");
assert_eq!(phase.name(), "ORBITAL");
}
#[test]
fn test_mission_phase_rendezvous_icon_and_name() {
let phase = MissionPhase::Rendezvous;
assert_eq!(phase.icon(), "◈");
assert_eq!(phase.name(), "RENDEZVOUS");
}
#[test]
fn test_mission_phase_success_icon_and_name() {
let phase = MissionPhase::MissionSuccess;
assert_eq!(phase.icon(), "✦");
assert_eq!(phase.name(), "MISSION SUCCESS");
}
#[test]
fn test_mission_phase_abort_icon_and_name() {
let phase = MissionPhase::Abort;
assert_eq!(phase.icon(), "⊗");
assert_eq!(phase.name(), "ABORT");
}
#[test]
fn test_mission_phase_pause_icon_and_name() {
let phase = MissionPhase::Pause;
assert_eq!(phase.icon(), "⏸");
assert_eq!(phase.name(), "PAUSED");
}
#[test]
fn test_mission_phase_all_icons_unique() {
let icons = [
MissionPhase::Preflight.icon(),
MissionPhase::Countdown.icon(),
MissionPhase::Launch.icon(),
MissionPhase::Orbital.icon(),
MissionPhase::Rendezvous.icon(),
MissionPhase::MissionSuccess.icon(),
MissionPhase::Abort.icon(),
MissionPhase::Pause.icon(),
];
for i in 0..icons.len() {
for j in (i + 1)..icons.len() {
assert_ne!(
icons[i], icons[j],
"Icons at positions {} and {} are identical: {}",
i, j, icons[i]
);
}
}
}
#[test]
fn test_mission_phase_equality() {
assert_eq!(MissionPhase::Preflight, MissionPhase::Preflight);
assert_ne!(MissionPhase::Preflight, MissionPhase::Countdown);
assert_ne!(MissionPhase::MissionSuccess, MissionPhase::Abort);
}
#[test]
fn test_mission_phase_copy_clone() {
let phase = MissionPhase::Orbital;
let copied = phase;
assert_eq!(phase, copied);
}
#[test]
fn test_theme_mode_equality() {
assert_eq!(ThemeMode::Dark, ThemeMode::Dark);
assert_eq!(ThemeMode::Light, ThemeMode::Light);
assert_ne!(ThemeMode::Dark, ThemeMode::Light);
}
#[test]
fn test_theme_mode_copy_clone() {
let mode = ThemeMode::Light;
let copied = mode;
assert_eq!(mode, copied);
}
#[test]
fn test_theme_mode_toggle_bidirectional() {
let dark = ThemeMode::Dark;
assert_eq!(dark.toggle().toggle(), dark);
let light = ThemeMode::Light;
assert_eq!(light.toggle().toggle(), light);
}
#[test]
fn test_theme_mode_cycle() {
let dark = ThemeMode::Dark;
assert_eq!(dark.cycle(), ThemeMode::Light);
let light = ThemeMode::Light;
assert_eq!(light.cycle(), ThemeMode::Solarized);
let solarized = ThemeMode::Solarized;
assert_eq!(solarized.cycle(), ThemeMode::Dark);
}
#[test]
fn test_theme_mode_cycle_full_loop() {
let start = ThemeMode::Dark;
let after_3_cycles = start.cycle().cycle().cycle();
assert_eq!(start, after_3_cycles);
}
#[test]
fn test_theme_mode_label() {
assert_eq!(ThemeMode::Dark.label(), "Dark");
assert_eq!(ThemeMode::Light.label(), "Light");
assert_eq!(ThemeMode::Solarized.label(), "Solarized");
}
#[test]
fn test_theme_mode_solarized_toggle_goes_to_dark() {
let solarized = ThemeMode::Solarized;
assert_eq!(solarized.toggle(), ThemeMode::Dark);
}
#[test]
fn test_theme_mode_solarized_theme_returns_cosmic_dark() {
let solarized_theme = ThemeMode::Solarized.theme();
assert_eq!(solarized_theme.background, Color::Rgb(15, 23, 42)); }
#[test]
fn test_verb_color_all_methods_return_rgb() {
let verbs = [
VerbColor::Infer,
VerbColor::Exec,
VerbColor::Fetch,
VerbColor::Invoke,
VerbColor::Agent,
];
for verb in verbs {
match verb.rgb() {
Color::Rgb(_, _, _) => (),
_ => panic!("Expected RGB color for {:?}", verb),
}
match verb.glow() {
Color::Rgb(_, _, _) => (),
_ => panic!("Expected RGB color for glow: {:?}", verb),
}
match verb.muted() {
Color::Rgb(_, _, _) => (),
_ => panic!("Expected RGB color for muted: {:?}", verb),
}
match verb.subtle() {
Color::Rgb(_, _, _) => (),
_ => panic!("Expected RGB color for subtle: {:?}", verb),
}
}
}
#[test]
fn test_color_mode_detect_consistency() {
let mode1 = ColorMode::detect();
let mode2 = ColorMode::detect();
assert_eq!(mode1, mode2);
}
#[test]
fn test_verbcolor_spawn_variant_rgb() {
let color = VerbColor::Spawn;
assert_eq!(color.rgb(), Color::Rgb(253, 164, 175)); }
#[test]
fn test_verbcolor_spawn_variant_glow() {
let color = VerbColor::Spawn;
assert_eq!(color.glow(), Color::Rgb(254, 205, 211)); }
#[test]
fn test_verbcolor_spawn_hex() {
assert_eq!(VerbColor::Spawn.hex(), "#fda4af");
}
#[test]
fn test_verbcolor_spawn_label() {
assert_eq!(VerbColor::Spawn.label(), "SPAWN");
}
#[test]
fn test_verbcolor_spawn_border_rgb() {
let color = VerbColor::Spawn;
assert_eq!(color.border_rgb(), Color::Rgb(251, 113, 133)); }
#[test]
fn test_verbcolor_spawn_icon() {
assert_eq!(VerbColor::Spawn.icon(), "🐤"); }
#[test]
fn test_verbcolor_has_all_methods_for_spawn() {
let spawn = VerbColor::Spawn;
let _ = spawn.rgb();
let _ = spawn.glow();
let _ = spawn.muted();
let _ = spawn.subtle();
let _ = spawn.hex();
let _ = spawn.icon();
let _ = spawn.label();
let _ = spawn.border_rgb();
let _ = spawn.rgb_tuple();
let _ = spawn.glow_tuple();
let _ = spawn.muted_tuple();
let _ = spawn.icon_ascii();
let _ = spawn.animated(0);
}
#[test]
fn test_verbcolor_from_verb_spawn() {
assert_eq!(VerbColor::from_verb("spawn"), VerbColor::Spawn);
assert_eq!(VerbColor::from_verb("SPAWN"), VerbColor::Spawn);
assert_eq!(VerbColor::from_verb("Spawn"), VerbColor::Spawn);
}
#[test]
fn test_verbcolor_all_six_variants_distinct() {
let colors = [
VerbColor::Infer.rgb(),
VerbColor::Exec.rgb(),
VerbColor::Fetch.rgb(),
VerbColor::Invoke.rgb(),
VerbColor::Agent.rgb(),
VerbColor::Spawn.rgb(),
];
for i in 0..colors.len() {
for j in (i + 1)..colors.len() {
assert_ne!(
colors[i], colors[j],
"Verb colors {} and {} are identical",
i, j
);
}
}
}
#[test]
fn test_theme_dark_vs_light_status_colors_differ() {
let dark = Theme::dark();
let light = Theme::light();
assert_ne!(dark.status_running, light.status_running);
assert_ne!(dark.status_success, light.status_success);
assert_ne!(dark.status_failed, light.status_failed);
}
#[test]
fn test_theme_all_realm_colors_present() {
let theme = Theme::default();
assert_ne!(theme.realm_shared, Color::Reset);
assert_ne!(theme.realm_org, Color::Reset);
}
#[test]
fn test_theme_all_ui_element_colors_present() {
let theme = Theme::default();
assert_ne!(theme.border_normal, Color::Reset);
assert_ne!(theme.border_focused, Color::Reset);
assert_ne!(theme.text_primary, Color::Reset);
assert_ne!(theme.text_secondary, Color::Reset);
assert_ne!(theme.text_muted, Color::Reset);
assert_ne!(theme.background, Color::Reset);
assert_ne!(theme.highlight, Color::Reset);
}
}