1use std::fmt::Display;
6
7#[derive(Debug, Clone, Copy)]
9pub enum Color {
10 Black,
12 Red,
14 Green,
16 Yellow,
18 Blue,
20 Magenta,
22 Cyan,
24 White,
26 BrightBlack,
28 BrightRed,
30 BrightGreen,
32 BrightYellow,
34 BrightBlue,
36 BrightMagenta,
38 BrightCyan,
40 BrightWhite,
42}
43
44impl Color {
45 pub fn fg_code(&self) -> &'static str {
47 match self {
48 Color::Black => "\x1b[30m",
49 Color::Red => "\x1b[31m",
50 Color::Green => "\x1b[32m",
51 Color::Yellow => "\x1b[33m",
52 Color::Blue => "\x1b[34m",
53 Color::Magenta => "\x1b[35m",
54 Color::Cyan => "\x1b[36m",
55 Color::White => "\x1b[37m",
56 Color::BrightBlack => "\x1b[90m",
57 Color::BrightRed => "\x1b[91m",
58 Color::BrightGreen => "\x1b[92m",
59 Color::BrightYellow => "\x1b[93m",
60 Color::BrightBlue => "\x1b[94m",
61 Color::BrightMagenta => "\x1b[95m",
62 Color::BrightCyan => "\x1b[96m",
63 Color::BrightWhite => "\x1b[97m",
64 }
65 }
66
67 pub fn bg_code(&self) -> &'static str {
69 match self {
70 Color::Black => "\x1b[40m",
71 Color::Red => "\x1b[41m",
72 Color::Green => "\x1b[42m",
73 Color::Yellow => "\x1b[43m",
74 Color::Blue => "\x1b[44m",
75 Color::Magenta => "\x1b[45m",
76 Color::Cyan => "\x1b[46m",
77 Color::White => "\x1b[47m",
78 Color::BrightBlack => "\x1b[100m",
79 Color::BrightRed => "\x1b[101m",
80 Color::BrightGreen => "\x1b[102m",
81 Color::BrightYellow => "\x1b[103m",
82 Color::BrightBlue => "\x1b[104m",
83 Color::BrightMagenta => "\x1b[105m",
84 Color::BrightCyan => "\x1b[106m",
85 Color::BrightWhite => "\x1b[107m",
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy)]
92pub enum Style {
93 Bold,
95 Italic,
97 Underline,
99 Blink,
101 Reverse,
103 Dim,
105}
106
107impl Style {
108 pub fn code(&self) -> &'static str {
110 match self {
111 Style::Bold => "\x1b[1m",
112 Style::Italic => "\x1b[3m",
113 Style::Underline => "\x1b[4m",
114 Style::Blink => "\x1b[5m",
115 Style::Reverse => "\x1b[7m",
116 Style::Dim => "\x1b[2m",
117 }
118 }
119}
120
121pub const RESET: &str = "\x1b[0m";
123
124#[allow(dead_code)]
126pub fn colorize<T: Display>(text: T, color: Color) -> String {
127 format!("{}{}{}", color.fg_code(), text, RESET)
128}
129
130#[allow(dead_code)]
132pub fn colorize_bg<T: Display>(text: T, color: Color) -> String {
133 format!("{}{}{}", color.bg_code(), text, RESET)
134}
135
136#[allow(dead_code)]
138pub fn stylize<T: Display>(text: T, style: Style) -> String {
139 format!("{}{}{}", style.code(), text, RESET)
140}
141
142#[allow(dead_code)]
144pub fn colorize_and_style<T: Display>(
145 text: T,
146 fg_color: Option<Color>,
147 bg_color: Option<Color>,
148 style: Option<Style>,
149) -> String {
150 let mut result = String::new();
151 if let Some(fg) = fg_color {
152 result.push_str(fg.fg_code());
153 }
154 if let Some(bg) = bg_color {
155 result.push_str(bg.bg_code());
156 }
157 if let Some(s) = style {
158 result.push_str(s.code());
159 }
160 result.push_str(&format!("{text}"));
161 result.push_str(RESET);
162 result
163}
164
165#[allow(dead_code)]
169pub fn supports_color() -> bool {
170 if let Ok(term) = std::env::var("TERM") {
171 if term == "dumb" {
172 return false;
173 }
174 }
175 if let Ok(no_color) = std::env::var("NO_COLOR") {
176 if !no_color.is_empty() {
177 return false;
178 }
179 }
180 if let Ok(color) = std::env::var("FORCE_COLOR") {
181 if !color.is_empty() {
182 return true;
183 }
184 }
185 if std::env::var("GITHUB_ACTIONS").is_ok() {
187 return true;
188 }
189 #[cfg(not(target_os = "windows"))]
191 {
192 true
194 }
195 #[cfg(target_os = "windows")]
196 {
197 if let Ok(term) = std::env::var("TERM_PROGRAM") {
199 if term == "vscode" || term == "mintty" || term == "alacritty" {
200 true
201 } else {
202 false
203 }
204 } else {
205 false
206 }
207 }
208}
209
210pub struct ColorOptions {
212 pub enabled: bool,
214 pub use_background: bool,
216 pub use_bright: bool,
218}
219
220impl Default for ColorOptions {
221 fn default() -> Self {
222 Self {
223 enabled: supports_color(),
224 use_background: true,
225 use_bright: true,
226 }
227 }
228}
229
230#[allow(dead_code)]
234pub fn gradient_color(value: f64, options: &ColorOptions) -> Option<Color> {
235 if !options.enabled {
236 return None;
237 }
238 if !(0.0..=1.0).contains(&value) {
239 return None;
240 }
241 if value < 0.5 {
243 if options.use_bright {
245 Some(Color::BrightRed)
246 } else {
247 Some(Color::Red)
248 }
249 } else if value < 0.7 {
250 if options.use_bright {
252 Some(Color::BrightYellow)
253 } else {
254 Some(Color::Yellow)
255 }
256 } else {
257 if options.use_bright {
259 Some(Color::BrightGreen)
260 } else {
261 Some(Color::Green)
262 }
263 }
264}
265
266#[allow(dead_code)]
270pub fn heatmap_gradient_color(value: f64, options: &ColorOptions) -> Option<Color> {
271 if !options.enabled {
272 return None;
273 }
274 if !(0.0..=1.0).contains(&value) {
275 return None;
276 }
277 if value < 0.2 {
279 if options.use_bright {
281 Some(Color::BrightBlue)
282 } else {
283 Some(Color::Blue)
284 }
285 } else if value < 0.4 {
286 if options.use_bright {
288 Some(Color::BrightCyan)
289 } else {
290 Some(Color::Cyan)
291 }
292 } else if value < 0.6 {
293 if options.use_bright {
295 Some(Color::BrightYellow)
296 } else {
297 Some(Color::Yellow)
298 }
299 } else if value < 0.8 {
300 if options.use_bright {
302 Some(Color::BrightRed)
303 } else {
304 Some(Color::Red)
305 }
306 } else {
307 if options.use_bright {
309 Some(Color::BrightMagenta)
310 } else {
311 Some(Color::Magenta)
312 }
313 }
314}
315
316#[allow(dead_code)]
318pub fn colored_metric_cell<T: Display>(
319 value: T,
320 normalized_value: f64,
321 options: &ColorOptions,
322) -> String {
323 if !options.enabled {
324 return format!("{value}");
325 }
326 if let Some(color) = gradient_color(normalized_value, options) {
327 colorize(value, color)
328 } else {
329 format!("{value}")
330 }
331}
332
333#[allow(dead_code)]
335pub fn heatmap_cell<T: Display>(
336 _value: T,
337 normalized_value: f64,
338 options: &ColorOptions,
339) -> String {
340 if !options.enabled {
341 return format!("{_value}");
342 }
343 if let Some(color) = heatmap_gradient_color(normalized_value, options) {
344 if normalized_value > 0.7 {
346 colorize(stylize(_value, Style::Bold), color)
347 } else {
348 colorize(_value, color)
349 }
350 } else {
351 format!("{_value}")
352 }
353}
354
355#[allow(dead_code)]
357pub fn color_legend(options: &ColorOptions) -> Option<String> {
358 if !options.enabled {
359 return None;
360 }
361 let mut legend = String::from("Color Legend: ");
362 let low_color = if options.use_bright {
363 Color::BrightRed
364 } else {
365 Color::Red
366 };
367 let mid_color = if options.use_bright {
368 Color::BrightYellow
369 } else {
370 Color::Yellow
371 };
372 let high_color = if options.use_bright {
373 Color::BrightGreen
374 } else {
375 Color::Green
376 };
377 legend.push_str(&format!("{} Low (0.0-0.5) ", colorize("■", low_color)));
378 legend.push_str(&format!("{} Medium (0.5-0.7) ", colorize("■", mid_color)));
379 legend.push_str(&format!("{} High (0.7-1.0)", colorize("■", high_color)));
380 Some(legend)
381}
382
383#[allow(dead_code)]
385pub fn heatmap_color_legend(options: &ColorOptions) -> Option<String> {
386 if !options.enabled {
387 return None;
388 }
389 let mut legend = String::from("Heatmap Legend: ");
390 let colors = [
391 (
392 if options.use_bright {
393 Color::BrightBlue
394 } else {
395 Color::Blue
396 },
397 "Very Low (0.0-0.2)",
398 ),
399 (
400 if options.use_bright {
401 Color::BrightCyan
402 } else {
403 Color::Cyan
404 },
405 "Low (0.2-0.4)",
406 ),
407 (
408 if options.use_bright {
409 Color::BrightYellow
410 } else {
411 Color::Yellow
412 },
413 "Medium (0.4-0.6)",
414 ),
415 (
416 if options.use_bright {
417 Color::BrightRed
418 } else {
419 Color::Red
420 },
421 "High (0.6-0.8)",
422 ),
423 (
424 if options.use_bright {
425 Color::BrightMagenta
426 } else {
427 Color::Magenta
428 },
429 "Very High (0.8-1.0)",
430 ),
431 ];
432 for (i, (color, label)) in colors.iter().enumerate() {
433 if i > 0 {
434 legend.push(' ');
435 }
436 legend.push_str(&format!("{} {}", colorize("■", *color), label));
437 }
438 Some(legend)
439}
440
441#[allow(dead_code)]
443pub fn colored_string<T: Display>(
444 content: T,
445 color: Option<Color>,
446 style: Option<Style>,
447) -> String {
448 match (color, style) {
449 (Some(c), Some(s)) => colorize_and_style(content, Some(c), None, Some(s)),
450 (Some(c), None) => colorize(content, c),
451 (None, Some(s)) => stylize(content, s),
452 (None, None) => content.to_string(),
453 }
454}