Skip to main content

vtcode_ui/tui/core_tui/
style.rs

1use anstyle::{Color as AnsiColorEnum, Style as AnsiStyle};
2use ratatui::style::{Color, Modifier, Style};
3use unicode_width::UnicodeWidthStr;
4
5use crate::tui::ui::theme;
6
7// Re-export from commons so existing consumers don't break.
8pub use vtcode_commons::ui_protocol::{convert_style, theme_from_color_fields};
9
10use super::types::{InlineTextStyle, InlineTheme};
11
12pub fn theme_from_styles(styles: &theme::ThemeStyles) -> InlineTheme {
13    theme_from_color_fields(
14        styles.foreground,
15        styles.background,
16        styles.primary,
17        styles.secondary,
18        styles.tool,
19        styles.tool_detail,
20        styles.pty_output,
21        styles.error,
22        styles.warning,
23    )
24}
25
26pub(crate) fn measure_text_width(text: &str) -> u16 {
27    UnicodeWidthStr::width(text) as u16
28}
29
30/// Convert anstyle Color to ratatui Color.
31///
32/// Delegates to `crate::design::color::anstyle_to_ratatui_color` which
33/// provides the correct mapping (fixing the Magenta bug).
34pub(crate) fn ratatui_color_from_ansi(color: AnsiColorEnum) -> Color {
35    crate::design::color::anstyle_to_ratatui_color(color)
36}
37
38/// Parse a hex color string (e.g., "#D99A4E") to a ratatui Color.
39/// Returns None if the string is invalid or cannot be parsed.
40pub(crate) use crate::design::color::hex_to_ratatui_color;
41
42/// Get the agent color style from an optional color token.
43///
44/// The token may be a primary-agent mode name (`"build"`), a standard ANSI hue
45/// name (`"green"`), or a `#rrggbb` hex string. It is resolved theme-aware via
46/// the design system so the badge stays legible on both dark and light
47/// terminals, with `fallback_color` used when the token is empty or unknown.
48pub(crate) fn agent_color_style(color: Option<&str>, fallback_color: Color) -> Style {
49    let light = matches!(
50        vtcode_commons::ansi_capabilities::detect_color_scheme(),
51        vtcode_commons::ansi_capabilities::ColorScheme::Light
52    );
53    let color = color
54        .map(|c| crate::design::color::resolve_agent_color(c, fallback_color, light))
55        .unwrap_or(fallback_color);
56    Style::default().fg(color).add_modifier(Modifier::BOLD)
57}
58
59pub(crate) fn ratatui_style_from_inline(style: &InlineTextStyle, fallback: Option<AnsiColorEnum>) -> Style {
60    crate::design::style::inline_text_style_to_ratatui(style.color, style.bg_color, style.effects, fallback)
61}
62
63/// PTY output style helper: keep configured colors and suppress bold.
64pub(crate) fn ratatui_pty_style_from_inline(style: &InlineTextStyle, fallback: Option<AnsiColorEnum>) -> Style {
65    ratatui_style_from_inline(style, fallback).remove_modifier(Modifier::BOLD)
66}
67
68/// Convert an `anstyle::Style` directly to a `ratatui::style::Style`.
69pub(crate) fn ratatui_style_from_ansi(style: AnsiStyle) -> Style {
70    crate::design::style::anstyle_to_ratatui_style(style)
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::design::color::resolve_agent_color;
77    use vtcode_config::constants::ui::{AGENT_COLOR_AUTO, AGENT_COLOR_BUILD, AGENT_COLOR_DUCK, AGENT_COLOR_PLAN};
78
79    #[test]
80    fn agent_color_style_applies_mode_color_with_bold() {
81        let fallback = Color::LightMagenta;
82        let style = agent_color_style(Some(AGENT_COLOR_BUILD), fallback);
83        // The exact variant depends on the detected terminal scheme, but it must
84        // be a concrete (non-fallback) standard color and always bold.
85        assert_ne!(style.fg, Some(fallback));
86        assert!(style.add_modifier.contains(Modifier::BOLD));
87
88        for hue in [AGENT_COLOR_BUILD, AGENT_COLOR_AUTO, AGENT_COLOR_PLAN, AGENT_COLOR_DUCK] {
89            let s = agent_color_style(Some(hue), fallback);
90            assert!(s.add_modifier.contains(Modifier::BOLD));
91            assert!(s.fg.is_some());
92        }
93    }
94
95    #[test]
96    fn agent_color_style_is_theme_aware_and_distinct_per_mode() {
97        let fallback = Color::LightMagenta;
98        // Build is a fixed hex token: identical on both appearances.
99        let build = Color::Rgb(0x73, 0xA1, 0x8E);
100        // On a dark terminal each hue resolves to its bright variant.
101        let dark = [
102            resolve_agent_color(AGENT_COLOR_BUILD, fallback, false),
103            resolve_agent_color(AGENT_COLOR_AUTO, fallback, false),
104            resolve_agent_color(AGENT_COLOR_PLAN, fallback, false),
105            resolve_agent_color(AGENT_COLOR_DUCK, fallback, false),
106        ];
107        assert_eq!(dark, [build, Color::LightGreen, Color::LightBlue, Color::LightMagenta]);
108        // On a light terminal each hue resolves to its base variant.
109        let light = [
110            resolve_agent_color(AGENT_COLOR_BUILD, fallback, true),
111            resolve_agent_color(AGENT_COLOR_AUTO, fallback, true),
112            resolve_agent_color(AGENT_COLOR_PLAN, fallback, true),
113            resolve_agent_color(AGENT_COLOR_DUCK, fallback, true),
114        ];
115        assert_eq!(light, [build, Color::Green, Color::Blue, Color::Magenta]);
116        // The four modes must remain visually distinct in both appearances.
117        assert_eq!(dark.iter().collect::<std::collections::HashSet<_>>().len(), 4);
118        assert_eq!(light.iter().collect::<std::collections::HashSet<_>>().len(), 4);
119    }
120
121    #[test]
122    fn agent_color_style_accepts_raw_hue_names_and_hex() {
123        let fallback = Color::LightMagenta;
124        // Raw standard ANSI hue name (as emitted by the plan-approval overlay).
125        assert_eq!(resolve_agent_color("green", fallback, false), Color::LightGreen);
126        assert_eq!(resolve_agent_color("blue", fallback, true), Color::Blue);
127        // Legacy hex still resolves.
128        assert_eq!(resolve_agent_color("#FF0000", fallback, false), Color::Rgb(255, 0, 0));
129    }
130
131    #[test]
132    fn agent_color_style_falls_back_when_missing_or_invalid() {
133        let fallback = Color::LightMagenta;
134        let missing = agent_color_style(None, fallback);
135        assert_eq!(missing.fg, Some(fallback));
136        assert!(missing.add_modifier.contains(Modifier::BOLD));
137
138        let invalid = agent_color_style(Some("not-a-color"), fallback);
139        assert_eq!(invalid.fg, Some(fallback));
140    }
141}