Skip to main content

vtcode_commons/
styling.rs

1//! Unified message styles and their logical mappings
2
3use crate::ansi_codes::RESET;
4use crate::color_policy;
5use anstyle::{AnsiColor, Color, Effects, RgbColor, Style};
6
7/// Standard color palette with semantic names
8#[derive(Debug, Clone, Copy)]
9pub struct ColorPalette {
10    pub success: Color, // Green
11    pub error: Color,   // Red
12    pub warning: Color, // Red
13    pub info: Color,    // Cyan
14    pub accent: Color,  // Magenta
15    pub primary: Color, // Cyan
16    pub muted: Color,   // Gray/Dim
17}
18
19impl Default for ColorPalette {
20    fn default() -> Self {
21        Self {
22            success: Color::Ansi(AnsiColor::Green),
23            error: Color::Ansi(AnsiColor::Red),
24            warning: Color::Ansi(AnsiColor::Red),
25            info: Color::Ansi(AnsiColor::Cyan),
26            accent: Color::Ansi(AnsiColor::Magenta),
27            primary: Color::Ansi(AnsiColor::Cyan),
28            muted: Color::Ansi(AnsiColor::BrightBlack),
29        }
30    }
31}
32
33/// Render text with a single color and optional effects
34pub fn render_styled(text: &str, color: Color, effects: Option<String>) -> String {
35    let mut style = Style::new();
36    if color_policy::color_output_enabled() {
37        style = style.fg_color(Some(color));
38    }
39
40    if let Some(effects_str) = effects {
41        let mut ansi_effects = Effects::new();
42
43        for effect in effects_str.split(',') {
44            let effect = effect.trim().to_lowercase();
45            match effect.as_str() {
46                "bold" => ansi_effects |= Effects::BOLD,
47                "dim" | "dimmed" => ansi_effects |= Effects::DIMMED,
48                "italic" => ansi_effects |= Effects::ITALIC,
49                "underline" => ansi_effects |= Effects::UNDERLINE,
50                "blink" => ansi_effects |= Effects::BLINK,
51                "invert" | "reversed" => ansi_effects |= Effects::INVERT,
52                "hidden" => ansi_effects |= Effects::HIDDEN,
53                "strikethrough" => ansi_effects |= Effects::STRIKETHROUGH,
54                _ => {}
55            }
56        }
57
58        style = style.effects(ansi_effects);
59    }
60
61    let prefix = style.to_string();
62    if prefix.is_empty() {
63        text.to_string()
64    } else {
65        format!("{prefix}{text}{RESET}")
66    }
67}
68
69/// Build style from CSS/terminal color name
70pub fn style_from_color_name(name: &str) -> Style {
71    let (color_name, dimmed) = if let Some(idx) = name.find(':') {
72        let (color, modifier) = name.split_at(idx);
73        (color, modifier.strip_prefix(':').unwrap_or(""))
74    } else {
75        (name, "")
76    };
77
78    let color = match color_name.to_lowercase().as_str() {
79        "red" => Color::Ansi(AnsiColor::Red),
80        "green" => Color::Ansi(AnsiColor::Green),
81        "blue" => Color::Ansi(AnsiColor::Blue),
82        "yellow" => Color::Ansi(AnsiColor::Yellow),
83        "cyan" => Color::Ansi(AnsiColor::Cyan),
84        "magenta" | "purple" => Color::Ansi(AnsiColor::Magenta),
85        "white" => Color::Ansi(AnsiColor::White),
86        "black" => Color::Ansi(AnsiColor::Black),
87        _ => return Style::new(),
88    };
89
90    let mut style = Style::new().fg_color(Some(color));
91    if dimmed.eq_ignore_ascii_case("dimmed") {
92        style = style.dimmed();
93    }
94    style
95}
96
97/// Create a bold colored style from AnsiColor
98pub fn bold_color(color: AnsiColor) -> Style {
99    Style::new().bold().fg_color(Some(Color::Ansi(color)))
100}
101
102/// Create a dimmed colored style from AnsiColor
103pub fn dimmed_color(color: AnsiColor) -> Style {
104    Style::new().dimmed().fg_color(Some(Color::Ansi(color)))
105}
106
107/// Diff color palette for consistent git diff styling
108/// Uses standard ANSI colors without bold for accessibility and consistency.
109#[derive(Debug, Clone, Copy)]
110pub struct DiffColorPalette {
111    pub added_fg: Color,
112    pub added_bg: Color,
113    pub removed_fg: Color,
114    pub removed_bg: Color,
115    pub header_fg: Color,
116    pub header_bg: Color,
117}
118
119impl Default for DiffColorPalette {
120    fn default() -> Self {
121        Self {
122            added_fg: Color::Ansi(AnsiColor::Green),
123            added_bg: Color::Rgb(RgbColor(10, 24, 10)),
124            removed_fg: Color::Ansi(AnsiColor::Red),
125            removed_bg: Color::Rgb(RgbColor(24, 10, 10)),
126            header_fg: Color::Ansi(AnsiColor::Cyan),
127            header_bg: Color::Rgb(RgbColor(10, 16, 20)),
128        }
129    }
130}
131
132impl DiffColorPalette {
133    pub fn added_style(&self) -> Style {
134        Style::new().fg_color(Some(self.added_fg))
135    }
136
137    pub fn removed_style(&self) -> Style {
138        Style::new().fg_color(Some(self.removed_fg))
139    }
140
141    pub fn header_style(&self) -> Style {
142        Style::new().fg_color(Some(self.header_fg))
143    }
144}
145
146// Re-export diff theme configuration
147pub use crate::diff_theme::{
148    DiffColorLevel, DiffTheme, diff_add_bg, diff_del_bg, diff_gutter_bg_add_light,
149    diff_gutter_bg_del_light, diff_gutter_fg_light,
150};