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