Skip to main content

vtcode_commons/
colors.rs

1//! Color utilities for VT Code
2//!
3//! This module provides color manipulation capabilities using anstyle,
4//! which offers low-level ANSI styling with RGB and 256-color support.
5
6use anstyle::{AnsiColor, Color, Effects, RgbColor, Style};
7
8/// Create an RGB color from hex string
9pub fn color_from_hex(hex: &str) -> Option<Color> {
10    let hex = hex.trim_start_matches('#');
11    if hex.len() != 6 {
12        return None;
13    }
14
15    let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
16    let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
17    let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
18
19    Some(Color::Rgb(RgbColor(r, g, b)))
20}
21
22/// Blend two RGB colors
23pub fn blend_colors(color1: &Color, color2: &Color, ratio: f32) -> Option<Color> {
24    let rgb1 = color_to_rgb(color1)?;
25    let rgb2 = color_to_rgb(color2)?;
26
27    let r = (rgb1.r() as f32 * (1.0 - ratio) + rgb2.r() as f32 * ratio) as u8;
28    let g = (rgb1.g() as f32 * (1.0 - ratio) + rgb2.g() as f32 * ratio) as u8;
29    let b = (rgb1.b() as f32 * (1.0 - ratio) + rgb2.b() as f32 * ratio) as u8;
30
31    Some(Color::Rgb(RgbColor(r, g, b)))
32}
33
34/// Convert an ANSI color to RGB, if possible
35pub fn color_to_rgb(color: &Color) -> Option<RgbColor> {
36    match color {
37        Color::Rgb(rgb) => Some(*rgb),
38        Color::Ansi(ansi_color) => ansi_to_rgb(*ansi_color),
39        Color::Ansi256(ansi256_color) => ansi256_to_rgb(*ansi256_color),
40    }
41}
42
43/// Convert an ANSI color to RGB approximation
44fn ansi_to_rgb(ansi_color: anstyle::AnsiColor) -> Option<RgbColor> {
45    match ansi_color {
46        anstyle::AnsiColor::Black => Some(RgbColor(0, 0, 0)),
47        anstyle::AnsiColor::Red => Some(RgbColor(170, 0, 0)),
48        anstyle::AnsiColor::Green => Some(RgbColor(0, 170, 0)),
49        anstyle::AnsiColor::Yellow => Some(RgbColor(170, 85, 0)),
50        anstyle::AnsiColor::Blue => Some(RgbColor(0, 0, 170)),
51        anstyle::AnsiColor::Magenta => Some(RgbColor(170, 0, 170)),
52        anstyle::AnsiColor::Cyan => Some(RgbColor(0, 170, 170)),
53        anstyle::AnsiColor::White => Some(RgbColor(170, 170, 170)),
54        anstyle::AnsiColor::BrightBlack => Some(RgbColor(85, 85, 85)),
55        anstyle::AnsiColor::BrightRed => Some(RgbColor(255, 85, 85)),
56        anstyle::AnsiColor::BrightGreen => Some(RgbColor(85, 255, 85)),
57        anstyle::AnsiColor::BrightYellow => Some(RgbColor(255, 255, 85)),
58        anstyle::AnsiColor::BrightBlue => Some(RgbColor(85, 85, 255)),
59        anstyle::AnsiColor::BrightMagenta => Some(RgbColor(255, 85, 255)),
60        anstyle::AnsiColor::BrightCyan => Some(RgbColor(85, 255, 255)),
61        anstyle::AnsiColor::BrightWhite => Some(RgbColor(255, 255, 255)),
62    }
63}
64
65/// Convert an ANSI256 color to RGB approximation
66fn ansi256_to_rgb(ansi256_color: anstyle::Ansi256Color) -> Option<RgbColor> {
67    let code = ansi256_color.0;
68    match code {
69        0 => Some(RgbColor(0, 0, 0)),
70        1 => Some(RgbColor(170, 0, 0)),
71        2 => Some(RgbColor(0, 170, 0)),
72        3 => Some(RgbColor(170, 85, 0)),
73        4 => Some(RgbColor(0, 0, 170)),
74        5 => Some(RgbColor(170, 0, 170)),
75        6 => Some(RgbColor(0, 170, 170)),
76        7 => Some(RgbColor(170, 170, 170)),
77        8 => Some(RgbColor(85, 85, 85)),
78        9 => Some(RgbColor(255, 85, 85)),
79        10 => Some(RgbColor(85, 255, 85)),
80        11 => Some(RgbColor(255, 255, 85)),
81        12 => Some(RgbColor(85, 85, 255)),
82        13 => Some(RgbColor(255, 85, 255)),
83        14 => Some(RgbColor(85, 255, 255)),
84        15 => Some(RgbColor(255, 255, 255)),
85        n if (16..=231).contains(&n) => {
86            let adjusted = n - 16;
87            let r = adjusted / 36;
88            let g = (adjusted % 36) / 6;
89            let b = adjusted % 6;
90            let scale = |x: u8| -> u8 { if x == 0 { 0 } else { 55 + x * 40 } };
91            Some(RgbColor(scale(r), scale(g), scale(b)))
92        }
93        n if n >= 232 => {
94            let gray = 8 + (n - 232) * 10;
95            Some(RgbColor(gray, gray, gray))
96        }
97        _ => Some(RgbColor(128, 128, 128)),
98    }
99}
100
101/// Determine if a color is light (for contrast calculations)
102pub fn is_light_color(color: &Color) -> bool {
103    let rgb = color_to_rgb(color);
104    if let Some(RgbColor(r, g, b)) = rgb {
105        let luminance = (0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) / 255.0;
106        luminance > 0.5
107    } else {
108        false
109    }
110}
111
112/// Get a contrasting color (black or white) for better readability
113pub fn contrasting_color(color: &Color) -> Color {
114    if is_light_color(color) {
115        Color::Ansi(anstyle::AnsiColor::Black)
116    } else {
117        Color::Ansi(anstyle::AnsiColor::White)
118    }
119}
120
121/// Create a desaturated version of a color
122pub fn desaturate_color(color: &Color, amount: f32) -> Option<Color> {
123    let rgb = color_to_rgb(color)?;
124    let r = rgb.r() as f32;
125    let g = rgb.g() as f32;
126    let b = rgb.b() as f32;
127    let gray = 0.299 * r + 0.587 * g + 0.114 * b;
128    let r_new = r * (1.0 - amount) + gray * amount;
129    let g_new = g * (1.0 - amount) + gray * amount;
130    let b_new = b * (1.0 - amount) + gray * amount;
131    Some(Color::Rgb(RgbColor(r_new as u8, g_new as u8, b_new as u8)))
132}
133
134fn styled(text: &str, style: Style) -> String {
135    format!("{}{}{}", style.render(), text, style.render_reset())
136}
137
138/// Style wrapper for console::style compatibility
139pub fn style(text: impl std::fmt::Display) -> StyledString {
140    StyledString {
141        text: text.to_string(),
142        style: Style::new(),
143    }
144}
145
146pub struct StyledString {
147    text: String,
148    style: Style,
149}
150
151impl StyledString {
152    pub fn red(mut self) -> Self {
153        self.style = self.style.fg_color(Some(Color::Ansi(AnsiColor::Red)));
154        self
155    }
156
157    pub fn green(mut self) -> Self {
158        self.style = self.style.fg_color(Some(Color::Ansi(AnsiColor::Green)));
159        self
160    }
161
162    pub fn blue(mut self) -> Self {
163        self.style = self.style.fg_color(Some(Color::Ansi(AnsiColor::Blue)));
164        self
165    }
166
167    pub fn yellow(mut self) -> Self {
168        self.style = self.style.fg_color(Some(Color::Ansi(AnsiColor::Yellow)));
169        self
170    }
171
172    pub fn cyan(mut self) -> Self {
173        self.style = self.style.fg_color(Some(Color::Ansi(AnsiColor::Cyan)));
174        self
175    }
176
177    pub fn magenta(mut self) -> Self {
178        self.style = self.style.fg_color(Some(Color::Ansi(AnsiColor::Magenta)));
179        self
180    }
181
182    pub fn bold(mut self) -> Self {
183        self.style = self.style.effects(self.style.get_effects() | Effects::BOLD);
184        self
185    }
186
187    pub fn dimmed(mut self) -> Self {
188        self.style = self
189            .style
190            .effects(self.style.get_effects() | Effects::DIMMED);
191        self
192    }
193
194    pub fn dim(self) -> Self {
195        self.dimmed()
196    }
197
198    pub fn on_black(mut self) -> Self {
199        self.style = self.style.bg_color(Some(Color::Ansi(AnsiColor::Black)));
200        self
201    }
202}
203
204impl std::fmt::Display for StyledString {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        write!(
207            f,
208            "{}{}{}",
209            self.style.render(),
210            self.text,
211            self.style.render_reset()
212        )
213    }
214}
215
216/// Apply red color to text
217pub fn red(text: &str) -> String {
218    styled(
219        text,
220        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))),
221    )
222}
223
224/// Apply green color to text
225pub fn green(text: &str) -> String {
226    styled(
227        text,
228        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green))),
229    )
230}
231
232/// Apply blue color to text
233pub fn blue(text: &str) -> String {
234    styled(
235        text,
236        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Blue))),
237    )
238}
239
240/// Apply yellow color to text
241pub fn yellow(text: &str) -> String {
242    styled(
243        text,
244        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
245    )
246}
247
248/// Apply purple color to text
249pub fn purple(text: &str) -> String {
250    styled(
251        text,
252        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Magenta))),
253    )
254}
255
256/// Apply cyan color to text
257pub fn cyan(text: &str) -> String {
258    styled(
259        text,
260        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
261    )
262}
263
264/// Apply white color to text
265pub fn white(text: &str) -> String {
266    styled(
267        text,
268        Style::new().fg_color(Some(Color::Ansi(AnsiColor::White))),
269    )
270}
271
272/// Apply black color to text
273pub fn black(text: &str) -> String {
274    styled(
275        text,
276        Style::new().fg_color(Some(Color::Ansi(AnsiColor::Black))),
277    )
278}
279
280/// Apply bold styling to text
281pub fn bold(text: &str) -> String {
282    styled(text, Style::new().effects(Effects::BOLD))
283}
284
285/// Apply italic styling to text
286pub fn italic(text: &str) -> String {
287    styled(text, Style::new().effects(Effects::ITALIC))
288}
289
290/// Apply underline styling to text
291pub fn underline(text: &str) -> String {
292    styled(text, Style::new().effects(Effects::UNDERLINE))
293}
294
295/// Apply dimmed styling to text
296pub fn dimmed(text: &str) -> String {
297    styled(text, Style::new().effects(Effects::DIMMED))
298}
299
300/// Apply blinking styling to text
301pub fn blink(text: &str) -> String {
302    styled(text, Style::new().effects(Effects::BLINK))
303}
304
305/// Apply reversed styling to text
306pub fn reversed(text: &str) -> String {
307    styled(text, Style::new().effects(Effects::INVERT))
308}
309
310/// Apply strikethrough styling to text
311pub fn strikethrough(text: &str) -> String {
312    styled(text, Style::new().effects(Effects::STRIKETHROUGH))
313}
314
315/// Apply custom RGB color to text
316pub fn rgb(text: &str, r: u8, g: u8, b: u8) -> String {
317    styled(
318        text,
319        Style::new().fg_color(Some(Color::Rgb(RgbColor(r, g, b)))),
320    )
321}
322
323/// Combine multiple color and style operations
324pub fn custom_style(text: &str, styles: &[&str]) -> String {
325    let mut style = Style::new();
326
327    for style_str in styles {
328        match *style_str {
329            "red" => style = style.fg_color(Some(Color::Ansi(AnsiColor::Red))),
330            "green" => style = style.fg_color(Some(Color::Ansi(AnsiColor::Green))),
331            "blue" => style = style.fg_color(Some(Color::Ansi(AnsiColor::Blue))),
332            "yellow" => style = style.fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
333            "purple" => style = style.fg_color(Some(Color::Ansi(AnsiColor::Magenta))),
334            "cyan" => style = style.fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
335            "white" => style = style.fg_color(Some(Color::Ansi(AnsiColor::White))),
336            "black" => style = style.fg_color(Some(Color::Ansi(AnsiColor::Black))),
337            "bold" => style = style.effects(style.get_effects() | Effects::BOLD),
338            "italic" => style = style.effects(style.get_effects() | Effects::ITALIC),
339            "underline" => style = style.effects(style.get_effects() | Effects::UNDERLINE),
340            "dimmed" => style = style.effects(style.get_effects() | Effects::DIMMED),
341            "blink" => style = style.effects(style.get_effects() | Effects::BLINK),
342            "reversed" => style = style.effects(style.get_effects() | Effects::INVERT),
343            "strikethrough" => style = style.effects(style.get_effects() | Effects::STRIKETHROUGH),
344            _ => {}
345        }
346    }
347
348    styled(text, style)
349}