use anstyle::{Color, Effects, RgbColor, Style};
use vtcode_config::constants::{defaults, ui};
use crate::theme::color_math::{balance_text_luminance, ensure_contrast, lighten, mix};
pub const DEFAULT_THEME_ID: &str = defaults::DEFAULT_THEME;
const DEFAULT_MIN_CONTRAST: f32 = ui::THEME_MIN_CONTRAST_RATIO;
#[derive(Clone, Debug)]
pub struct ColorAccessibilityConfig {
pub minimum_contrast: f32,
pub bold_is_bright: bool,
pub safe_colors_only: bool,
}
impl Default for ColorAccessibilityConfig {
fn default() -> Self {
Self {
minimum_contrast: DEFAULT_MIN_CONTRAST,
bold_is_bright: false,
safe_colors_only: false,
}
}
}
#[derive(Clone, Debug)]
pub struct ThemePalette {
pub primary_accent: RgbColor,
pub background: RgbColor,
pub foreground: RgbColor,
pub secondary_accent: RgbColor,
pub alert: RgbColor,
pub logo_accent: RgbColor,
}
#[derive(Clone, Debug)]
pub(crate) struct ColorContext {
pub background: RgbColor,
pub min_contrast: f32,
pub fallback_light: RgbColor,
}
impl ColorContext {
pub(crate) fn new(background: RgbColor, min_contrast: f32) -> Self {
Self {
background,
min_contrast,
fallback_light: RgbColor(
ui::THEME_COLOR_WHITE_RED,
ui::THEME_COLOR_WHITE_GREEN,
ui::THEME_COLOR_WHITE_BLUE,
),
}
}
pub(crate) fn guaranteed_text_color(
&self,
candidate: RgbColor,
fallbacks: &[RgbColor],
) -> RgbColor {
let color = ensure_contrast(candidate, self.background, self.min_contrast, fallbacks);
balance_text_luminance(color, self.background, self.min_contrast)
}
pub(crate) fn guaranteed_accent_color(
&self,
candidate: RgbColor,
fallbacks: &[RgbColor],
) -> RgbColor {
ensure_contrast(candidate, self.background, self.min_contrast, fallbacks)
}
pub(crate) fn compute_text_color(&self, foreground: RgbColor, secondary: RgbColor) -> RgbColor {
self.guaranteed_text_color(
foreground,
&[
lighten(foreground, ui::THEME_FOREGROUND_LIGHTEN_RATIO),
lighten(secondary, ui::THEME_SECONDARY_LIGHTEN_RATIO),
self.fallback_light,
],
)
}
pub(crate) fn compute_info_color(&self, secondary: RgbColor, text_color: RgbColor) -> RgbColor {
self.guaranteed_text_color(
secondary,
&[
lighten(secondary, ui::THEME_SECONDARY_LIGHTEN_RATIO),
text_color,
self.fallback_light,
],
)
}
pub(crate) fn compute_tool_color(&self, text_color: RgbColor) -> RgbColor {
self.guaranteed_accent_color(
lighten(text_color, ui::THEME_MIX_RATIO),
&[
lighten(
lighten(text_color, ui::THEME_MIX_RATIO),
ui::THEME_TOOL_BODY_LIGHTEN_RATIO,
),
text_color,
self.fallback_light,
],
)
}
pub(crate) fn compute_tool_body_color(&self, text_color: RgbColor) -> RgbColor {
let candidate = mix(
lighten(text_color, ui::THEME_MIX_RATIO),
text_color,
ui::THEME_TOOL_BODY_MIX_RATIO,
);
self.guaranteed_accent_color(
candidate,
&[
lighten(
lighten(text_color, ui::THEME_MIX_RATIO),
ui::THEME_TOOL_BODY_LIGHTEN_RATIO,
),
text_color,
self.fallback_light,
],
)
}
pub(crate) fn compute_pty_output_color(
&self,
tool_body_color: RgbColor,
text_color: RgbColor,
) -> RgbColor {
let candidate = mix(
tool_body_color,
self.background,
ui::THEME_PTY_OUTPUT_MIX_RATIO,
);
self.guaranteed_text_color(candidate, &[tool_body_color, text_color])
}
pub(crate) fn compute_response_color(&self, text_color: RgbColor) -> RgbColor {
self.guaranteed_text_color(
text_color,
&[
lighten(text_color, ui::THEME_RESPONSE_COLOR_LIGHTEN_RATIO),
self.fallback_light,
],
)
}
pub(crate) fn compute_reasoning_color(&self, text_color: RgbColor) -> RgbColor {
self.guaranteed_text_color(
lighten(text_color, 0.25),
&[lighten(text_color, 0.15), text_color, self.fallback_light],
)
}
pub(crate) fn compute_user_color(
&self,
secondary: RgbColor,
info_color: RgbColor,
text_color: RgbColor,
) -> RgbColor {
self.guaranteed_text_color(
lighten(secondary, ui::THEME_USER_COLOR_LIGHTEN_RATIO),
&[
lighten(secondary, ui::THEME_SECONDARY_USER_COLOR_LIGHTEN_RATIO),
info_color,
text_color,
],
)
}
pub(crate) fn compute_alert_color(&self, alert: RgbColor, text_color: RgbColor) -> RgbColor {
self.guaranteed_text_color(
alert,
&[
lighten(alert, ui::THEME_LUMINANCE_LIGHTEN_RATIO),
self.fallback_light,
text_color,
],
)
}
pub(crate) fn compute_primary_color(
&self,
primary: RgbColor,
text_color: RgbColor,
) -> RgbColor {
self.guaranteed_text_color(
ensure_contrast(primary, self.background, self.min_contrast, &[text_color]),
&[text_color],
)
}
pub(crate) fn compute_secondary_color(
&self,
secondary: RgbColor,
info_color: RgbColor,
text_color: RgbColor,
) -> RgbColor {
self.guaranteed_text_color(
ensure_contrast(
secondary,
self.background,
self.min_contrast,
&[info_color, text_color],
),
&[info_color, text_color],
)
}
pub(crate) fn compute_logo_color(
&self,
logo_accent: RgbColor,
secondary_color: RgbColor,
text_color: RgbColor,
) -> RgbColor {
self.guaranteed_text_color(
ensure_contrast(
logo_accent,
self.background,
self.min_contrast,
&[secondary_color, text_color],
),
&[secondary_color, text_color],
)
}
pub(crate) fn compute_status_color(
&self,
primary_color: RgbColor,
info_color: RgbColor,
text_color: RgbColor,
) -> RgbColor {
self.guaranteed_accent_color(
lighten(primary_color, ui::THEME_PRIMARY_STATUS_LIGHTEN_RATIO),
&[
lighten(
primary_color,
ui::THEME_PRIMARY_STATUS_SECONDARY_LIGHTEN_RATIO,
),
info_color,
text_color,
],
)
}
pub(crate) fn compute_mcp_color(&self, logo_color: RgbColor, info_color: RgbColor) -> RgbColor {
self.guaranteed_accent_color(
lighten(logo_color, ui::THEME_SECONDARY_LIGHTEN_RATIO),
&[
lighten(logo_color, ui::THEME_LOGO_ACCENT_BANNER_LIGHTEN_RATIO),
info_color,
self.fallback_light,
],
)
}
}
impl ThemePalette {
fn style_from(color: RgbColor, bold: bool, bold_is_bright: bool) -> Style {
let mut style = Style::new().fg_color(Some(Color::Rgb(color)));
if bold && !bold_is_bright {
style = style.bold();
}
style
}
pub(crate) fn build_styles_with_accessibility(
&self,
accessibility: &ColorAccessibilityConfig,
) -> ThemeStyles {
let ctx = ColorContext::new(self.background, accessibility.minimum_contrast);
let bold_is_bright = accessibility.bold_is_bright;
let text = ctx.compute_text_color(self.foreground, self.secondary_accent);
let info = ctx.compute_info_color(self.secondary_accent, text);
let tool_body = ctx.compute_tool_body_color(text);
let pty = ctx.compute_pty_output_color(tool_body, text);
let primary = ctx.compute_primary_color(self.primary_accent, text);
let secondary = ctx.compute_secondary_color(self.secondary_accent, info, text);
let logo = ctx.compute_logo_color(self.logo_accent, secondary, text);
ThemeStyles {
info: Self::style_from(info, true, bold_is_bright),
error: Self::style_from(
ctx.compute_alert_color(self.alert, text),
true,
bold_is_bright,
),
output: Self::style_from(text, false, bold_is_bright),
response: Self::style_from(ctx.compute_response_color(text), false, bold_is_bright),
reasoning: Self::style_from(ctx.compute_reasoning_color(text), false, bold_is_bright)
.effects(Effects::DIMMED | Effects::ITALIC),
tool: Style::new().fg_color(Some(Color::Rgb(ctx.compute_tool_color(text)))),
tool_detail: Style::new().fg_color(Some(Color::Rgb(tool_body))),
tool_output: Style::new(),
pty_output: Style::new().fg_color(Some(Color::Rgb(pty))),
status: Self::style_from(
ctx.compute_status_color(primary, info, text),
true,
bold_is_bright,
),
mcp: Self::style_from(ctx.compute_mcp_color(logo, info), true, bold_is_bright),
user: Self::style_from(
ctx.compute_user_color(self.secondary_accent, info, text),
false,
bold_is_bright,
),
primary: Self::style_from(primary, false, bold_is_bright),
secondary: Self::style_from(secondary, false, bold_is_bright),
background: Color::Rgb(self.background),
foreground: Color::Rgb(text),
}
}
}
#[derive(Clone, Debug)]
pub struct ThemeStyles {
pub info: Style,
pub error: Style,
pub output: Style,
pub response: Style,
pub reasoning: Style,
pub tool: Style,
pub tool_detail: Style,
pub tool_output: Style,
pub pty_output: Style,
pub status: Style,
pub mcp: Style,
pub user: Style,
pub primary: Style,
pub secondary: Style,
pub background: Color,
pub foreground: Color,
}
#[derive(Clone, Debug)]
pub struct ThemeDefinition {
pub id: &'static str,
pub label: &'static str,
pub palette: ThemePalette,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ThemeSuite {
pub id: &'static str,
pub label: &'static str,
pub theme_ids: Vec<&'static str>,
}
#[derive(Debug, Clone)]
pub struct ThemeValidationResult {
pub is_valid: bool,
pub warnings: Vec<String>,
pub errors: Vec<String>,
}