1use anstyle::{AnsiColor, Color, Effects, RgbColor, Style};
7
8pub 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#[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
35pub 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
44fn 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
66fn 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
102pub 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
113pub 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#[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
140pub 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
218pub fn red(text: &str) -> String {
220 styled(
221 text,
222 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red))),
223 )
224}
225
226pub fn green(text: &str) -> String {
228 styled(
229 text,
230 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green))),
231 )
232}
233
234pub fn blue(text: &str) -> String {
236 styled(
237 text,
238 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Blue))),
239 )
240}
241
242pub fn yellow(text: &str) -> String {
244 styled(
245 text,
246 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))),
247 )
248}
249
250pub fn purple(text: &str) -> String {
252 styled(
253 text,
254 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Magenta))),
255 )
256}
257
258pub fn cyan(text: &str) -> String {
260 styled(
261 text,
262 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Cyan))),
263 )
264}
265
266pub fn white(text: &str) -> String {
268 styled(
269 text,
270 Style::new().fg_color(Some(Color::Ansi(AnsiColor::White))),
271 )
272}
273
274pub fn black(text: &str) -> String {
276 styled(
277 text,
278 Style::new().fg_color(Some(Color::Ansi(AnsiColor::Black))),
279 )
280}
281
282pub fn bold(text: &str) -> String {
284 styled(text, Style::new().effects(Effects::BOLD))
285}
286
287pub fn italic(text: &str) -> String {
289 styled(text, Style::new().effects(Effects::ITALIC))
290}
291
292pub fn underline(text: &str) -> String {
294 styled(text, Style::new().effects(Effects::UNDERLINE))
295}
296
297pub fn dimmed(text: &str) -> String {
299 styled(text, Style::new().effects(Effects::DIMMED))
300}
301
302pub fn blink(text: &str) -> String {
304 styled(text, Style::new().effects(Effects::BLINK))
305}
306
307pub fn reversed(text: &str) -> String {
309 styled(text, Style::new().effects(Effects::INVERT))
310}
311
312pub fn strikethrough(text: &str) -> String {
314 styled(text, Style::new().effects(Effects::STRIKETHROUGH))
315}
316
317pub 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
325pub 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}