1use super::state::{LossTrend, TrainingStatus};
7use std::fmt;
8
9#[inline]
12fn clamped_f32_to_u8(value: f32) -> u8 {
13 let clamped = value.clamp(0.0, 255.0);
14 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
16 let wide = clamped as u16;
17 u8::try_from(wide).unwrap_or(u8::MAX)
19}
20
21#[inline]
23fn clamped_f32_to_usize(value: f32) -> usize {
24 let clamped = value.max(0.0);
25 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
27 let result = clamped as usize;
28 result
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum ColorMode {
34 TrueColor,
36 Color256,
38 Color16,
40 #[default]
42 Mono,
43}
44
45impl ColorMode {
46 pub fn detect() -> Self {
48 Self::detect_with_env(
49 std::env::var("COLORTERM").ok().as_deref(),
50 std::env::var("TERM").ok().as_deref(),
51 std::env::var("NO_COLOR").ok().as_deref(),
52 )
53 }
54
55 pub fn detect_with_env(
57 colorterm: Option<&str>,
58 term: Option<&str>,
59 no_color: Option<&str>,
60 ) -> Self {
61 if no_color.is_some() {
63 return Self::Mono;
64 }
65
66 if let Some(ct) = colorterm {
68 if ct.contains("truecolor") || ct.contains("24bit") {
69 return Self::TrueColor;
70 }
71 }
72
73 if let Some(term) = term {
75 if term.contains("256color") || term.contains("kitty") || term.contains("alacritty") {
76 return Self::Color256;
77 }
78 if term.contains("xterm") || term.contains("screen") || term.contains("tmux") {
79 return Self::Color16;
80 }
81 if term == "dumb" || term.is_empty() {
82 return Self::Mono;
83 }
84 }
85
86 Self::Color16
88 }
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub struct Rgb {
94 pub r: u8,
95 pub g: u8,
96 pub b: u8,
97}
98
99impl Rgb {
100 pub const fn new(r: u8, g: u8, b: u8) -> Self {
101 Self { r, g, b }
102 }
103}
104
105impl From<(u8, u8, u8)> for Rgb {
106 fn from((r, g, b): (u8, u8, u8)) -> Self {
107 Self { r, g, b }
108 }
109}
110
111impl Rgb {
112 pub fn to_256(self) -> u8 {
114 let r6 = u8::try_from(u16::from(self.r) * 5 / 255).unwrap_or(u8::MAX);
117 let g6 = u8::try_from(u16::from(self.g) * 5 / 255).unwrap_or(u8::MAX);
118 let b6 = u8::try_from(u16::from(self.b) * 5 / 255).unwrap_or(u8::MAX);
119 16 + 36 * r6 + 6 * g6 + b6
120 }
121
122 pub fn to_16(self) -> u8 {
124 let max_channel = self.r.max(self.g).max(self.b);
126 let is_bright = max_channel > 180;
127
128 let r_dom = self.r >= self.g && self.r >= self.b;
130 let g_dom = self.g >= self.r && self.g >= self.b;
131 let b_dom = self.b >= self.r && self.b >= self.g;
132
133 let r_present = self.r > 85;
135 let g_present = self.g > 85;
136 let b_present = self.b > 85;
137
138 let base = match (r_present, g_present, b_present) {
139 (true, true, true) => 7, (true, true, false) => 3, (true, false, true) => 5, (false, true, true) => 6, (true, false, false) => 1, (false, true, false) => 2, (false, false, true) => 4, (false, false, false) => {
147 if r_dom && self.r > 40 {
149 1
150 } else if g_dom && self.g > 40 {
151 2
152 } else if b_dom && self.b > 40 {
153 4
154 } else {
155 0
156 }
157 }
158 };
159
160 if is_bright {
161 base + 8
162 } else {
163 base
164 }
165 }
166}
167
168pub struct Styled<'a> {
170 text: &'a str,
171 fg: Option<Rgb>,
172 bold: bool,
173 mode: ColorMode,
174}
175
176impl<'a> Styled<'a> {
177 pub fn new(text: &'a str, mode: ColorMode) -> Self {
178 Self { text, fg: None, bold: false, mode }
179 }
180
181 pub fn fg(mut self, color: impl Into<Rgb>) -> Self {
182 self.fg = Some(color.into());
183 self
184 }
185
186 pub fn bold(mut self) -> Self {
187 self.bold = true;
188 self
189 }
190}
191
192impl fmt::Display for Styled<'_> {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 if self.mode == ColorMode::Mono {
195 return write!(f, "{}", self.text);
196 }
197
198 let mut has_style = false;
199
200 if self.bold {
202 write!(f, "\x1b[1m")?;
203 has_style = true;
204 }
205
206 if let Some(rgb) = self.fg {
208 match self.mode {
209 ColorMode::TrueColor => {
210 write!(f, "\x1b[38;2;{};{};{}m", rgb.r, rgb.g, rgb.b)?;
211 }
212 ColorMode::Color256 => {
213 write!(f, "\x1b[38;5;{}m", rgb.to_256())?;
214 }
215 ColorMode::Color16 => {
216 let code = rgb.to_16();
217 if code >= 8 {
218 write!(f, "\x1b[9{}m", code - 8)?;
219 } else {
220 write!(f, "\x1b[3{code}m")?;
221 }
222 }
223 ColorMode::Mono => {}
224 }
225 has_style = true;
226 }
227
228 write!(f, "{}", self.text)?;
229
230 if has_style {
231 write!(f, "\x1b[0m")?;
232 }
233
234 Ok(())
235 }
236}
237
238#[derive(Debug, Clone)]
240pub struct TrainingPalette {
241 pub mode: ColorMode,
242}
243
244impl Default for TrainingPalette {
245 fn default() -> Self {
246 Self { mode: ColorMode::detect() }
247 }
248}
249
250impl TrainingPalette {
251 pub fn new(mode: ColorMode) -> Self {
252 Self { mode }
253 }
254
255 pub fn style<'a>(&self, text: &'a str) -> Styled<'a> {
257 Styled::new(text, self.mode)
258 }
259
260 pub const SUCCESS: Rgb = Rgb::new(80, 200, 120);
266
267 pub const WARNING: Rgb = Rgb::new(255, 193, 7);
269
270 pub const ERROR: Rgb = Rgb::new(244, 67, 54);
272
273 pub const INFO: Rgb = Rgb::new(33, 150, 243);
275
276 pub const MUTED: Rgb = Rgb::new(158, 158, 158);
278
279 pub const PRIMARY: Rgb = Rgb::new(0, 188, 212);
281
282 fn threshold_color(value: f32, thresholds: &[(f32, Rgb)], fallback: Rgb) -> Rgb {
291 for &(bound, color) in thresholds {
292 if value <= bound {
293 return color;
294 }
295 }
296 fallback
297 }
298
299 pub fn gpu_util_color(percent: f32) -> Rgb {
305 let p = percent.clamp(0.0, 100.0);
306 Self::threshold_color(
307 p,
308 &[(30.0, Self::MUTED), (70.0, Self::SUCCESS), (90.0, Self::INFO)],
309 Self::PRIMARY,
310 )
311 }
312
313 pub fn vram_color(percent: f32) -> Rgb {
315 let p = percent.clamp(0.0, 100.0);
316 Self::threshold_color(
317 p,
318 &[(50.0, Self::SUCCESS), (75.0, Self::INFO), (90.0, Self::WARNING)],
319 Self::ERROR,
320 )
321 }
322
323 pub fn temp_color(celsius: f32) -> Rgb {
325 let t = celsius.clamp(0.0, 200.0);
326 Self::threshold_color(
327 t,
328 &[(50.0, Self::SUCCESS), (70.0, Self::INFO), (80.0, Self::WARNING)],
329 Self::ERROR,
330 )
331 }
332
333 pub fn power_color(percent: f32) -> Rgb {
335 let p = percent.clamp(0.0, 100.0);
336 Self::threshold_color(
337 p,
338 &[(60.0, Self::SUCCESS), (80.0, Self::INFO), (95.0, Self::WARNING)],
339 Self::ERROR,
340 )
341 }
342
343 pub fn grad_norm_color(norm: f32) -> Rgb {
349 Self::threshold_color(
350 norm,
351 &[(1.0, Self::SUCCESS), (5.0, Self::INFO), (10.0, Self::WARNING)],
352 Self::ERROR,
353 )
354 }
355
356 pub fn loss_color(loss: f32, min_loss: f32, max_loss: f32) -> Rgb {
359 if max_loss <= min_loss {
360 return Self::INFO;
361 }
362
363 let normalized = ((loss - min_loss) / (max_loss - min_loss)).clamp(0.0, 1.0);
364
365 let (r, g, b) = if normalized < 0.5 {
367 let t = normalized * 2.0;
369 (
370 clamped_f32_to_u8(80.0 + t * 175.0),
371 clamped_f32_to_u8(200.0 - t * 7.0),
372 clamped_f32_to_u8(120.0 - t * 113.0),
373 )
374 } else {
375 let t = (normalized - 0.5) * 2.0;
377 (
378 clamped_f32_to_u8(255.0 - t * 11.0),
379 clamped_f32_to_u8(193.0 - t * 126.0),
380 clamped_f32_to_u8(7.0 + t * 47.0),
381 )
382 };
383
384 Rgb::new(r, g, b)
385 }
386
387 pub fn status_color(status: &TrainingStatus) -> Rgb {
389 match status {
390 TrainingStatus::Running => Self::SUCCESS,
391 TrainingStatus::Completed => Self::PRIMARY,
392 TrainingStatus::Paused => Self::WARNING,
393 TrainingStatus::Failed(_) => Self::ERROR,
394 TrainingStatus::Initializing => Self::INFO,
395 }
396 }
397
398 pub fn loss_trend_color(trend: &LossTrend) -> Rgb {
400 match trend {
401 LossTrend::Decreasing => Self::SUCCESS, LossTrend::Stable => Self::INFO, LossTrend::Increasing => Self::ERROR, LossTrend::Unknown => Self::MUTED, }
406 }
407
408 pub fn progress_color(percent: f32) -> Rgb {
414 let p = percent.clamp(0.0, 100.0);
415 if p <= 75.0 {
416 Self::INFO } else if p < 100.0 {
418 Self::SUCCESS } else {
420 Self::PRIMARY }
422 }
423}
424
425pub fn colored_bar(value: f32, max: f32, width: usize, color: Rgb, mode: ColorMode) -> String {
431 let percent = if max > 0.0 { value / max } else { 0.0 };
432 let percent = percent.clamp(0.0, 1.0);
433 let width_clamped = u16::try_from(width).unwrap_or(u16::MAX);
435 let filled_f32 = f32::from(width_clamped) * percent;
436 let filled = clamped_f32_to_usize(filled_f32.clamp(0.0, f32::from(width_clamped))).min(width);
437 let empty = width.saturating_sub(filled);
438
439 let filled_str: String = std::iter::repeat_n('█', filled).collect();
440 let empty_str: String = std::iter::repeat_n('░', empty).collect();
441
442 if mode == ColorMode::Mono {
443 format!("{filled_str}{empty_str}")
444 } else {
445 format!(
446 "{}{}",
447 Styled::new(&filled_str, mode).fg(color),
448 Styled::new(&empty_str, mode).fg(TrainingPalette::MUTED)
449 )
450 }
451}
452
453pub fn colored_value<T: fmt::Display>(value: T, color: Rgb, mode: ColorMode) -> String {
455 let text = value.to_string();
456 Styled::new(&text, mode).fg(color).to_string()
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn test_color_mode_detection() {
465 assert_eq!(
467 ColorMode::detect_with_env(Some("truecolor"), Some("xterm-256color"), Some("1")),
468 ColorMode::Mono
469 );
470
471 assert_eq!(ColorMode::detect_with_env(Some("truecolor"), None, None), ColorMode::TrueColor);
473
474 assert_eq!(
476 ColorMode::detect_with_env(None, Some("xterm-256color"), None),
477 ColorMode::Color256
478 );
479
480 assert_eq!(ColorMode::detect_with_env(None, Some("xterm"), None), ColorMode::Color16);
482
483 assert_eq!(ColorMode::detect_with_env(None, Some("dumb"), None), ColorMode::Mono);
485 }
486
487 #[test]
488 fn test_rgb_to_256() {
489 assert_eq!(Rgb::new(0, 0, 0).to_256(), 16);
491 assert_eq!(Rgb::new(255, 255, 255).to_256(), 231);
493 assert_eq!(Rgb::new(255, 0, 0).to_256(), 196);
495 assert_eq!(Rgb::new(0, 255, 0).to_256(), 46);
497 assert_eq!(Rgb::new(0, 0, 255).to_256(), 21);
499 }
500
501 #[test]
502 fn test_rgb_to_16() {
503 assert_eq!(Rgb::new(255, 50, 50).to_16(), 9); assert_eq!(Rgb::new(50, 255, 50).to_16(), 10); assert_eq!(Rgb::new(0, 0, 100).to_16(), 4); }
510
511 #[test]
512 fn test_rgb_to_16_all_boolean_combos() {
513 assert_eq!(Rgb::new(200, 200, 200).to_16(), 15); assert_eq!(Rgb::new(200, 200, 50).to_16(), 11); assert_eq!(Rgb::new(200, 50, 200).to_16(), 13); assert_eq!(Rgb::new(50, 200, 200).to_16(), 14); assert_eq!(Rgb::new(100, 50, 50).to_16(), 1); assert_eq!(Rgb::new(50, 100, 50).to_16(), 2); assert_eq!(Rgb::new(50, 50, 100).to_16(), 4); assert_eq!(Rgb::new(60, 20, 20).to_16(), 1); assert_eq!(Rgb::new(20, 60, 20).to_16(), 2); assert_eq!(Rgb::new(20, 20, 60).to_16(), 4); assert_eq!(Rgb::new(20, 20, 20).to_16(), 0); }
540
541 #[test]
542 fn test_styled_display_truecolor() {
543 let styled = Styled::new("test", ColorMode::TrueColor).fg(Rgb::new(255, 0, 0));
544 let output = styled.to_string();
545 assert!(output.contains("\x1b[38;2;255;0;0m"));
546 assert!(output.contains("test"));
547 assert!(output.ends_with("\x1b[0m"));
548 }
549
550 #[test]
551 fn test_styled_display_mono() {
552 let styled = Styled::new("test", ColorMode::Mono).fg(Rgb::new(255, 0, 0));
553 let output = styled.to_string();
554 assert_eq!(output, "test");
555 }
556
557 #[test]
558 fn test_gpu_util_color() {
559 assert_eq!(TrainingPalette::gpu_util_color(20.0), TrainingPalette::MUTED);
560 assert_eq!(TrainingPalette::gpu_util_color(50.0), TrainingPalette::SUCCESS);
561 assert_eq!(TrainingPalette::gpu_util_color(80.0), TrainingPalette::INFO);
562 assert_eq!(TrainingPalette::gpu_util_color(95.0), TrainingPalette::PRIMARY);
563 }
564
565 #[test]
566 fn test_temp_color() {
567 assert_eq!(TrainingPalette::temp_color(40.0), TrainingPalette::SUCCESS);
568 assert_eq!(TrainingPalette::temp_color(65.0), TrainingPalette::INFO);
569 assert_eq!(TrainingPalette::temp_color(75.0), TrainingPalette::WARNING);
570 assert_eq!(TrainingPalette::temp_color(85.0), TrainingPalette::ERROR);
571 }
572
573 #[test]
574 fn test_grad_norm_color() {
575 assert_eq!(TrainingPalette::grad_norm_color(0.5), TrainingPalette::SUCCESS);
576 assert_eq!(TrainingPalette::grad_norm_color(3.0), TrainingPalette::INFO);
577 assert_eq!(TrainingPalette::grad_norm_color(8.0), TrainingPalette::WARNING);
578 assert_eq!(TrainingPalette::grad_norm_color(20.0), TrainingPalette::ERROR);
579 }
580
581 #[test]
582 fn test_loss_color_gradient() {
583 let min = 0.0;
584 let max = 1.0;
585
586 let low = TrainingPalette::loss_color(0.1, min, max);
588 assert!(low.g > low.r); let high = TrainingPalette::loss_color(0.9, min, max);
592 assert!(high.r > high.g); }
594
595 #[test]
596 fn test_status_color_all_variants() {
597 let running = TrainingPalette::status_color(&TrainingStatus::Running);
599 assert_eq!(running, TrainingPalette::SUCCESS);
600
601 let completed = TrainingPalette::status_color(&TrainingStatus::Completed);
602 assert_eq!(completed, TrainingPalette::PRIMARY);
603
604 let paused = TrainingPalette::status_color(&TrainingStatus::Paused);
605 assert_eq!(paused, TrainingPalette::WARNING);
606
607 let failed = TrainingPalette::status_color(&TrainingStatus::Failed("error".to_string()));
608 assert_eq!(failed, TrainingPalette::ERROR);
609
610 let initializing = TrainingPalette::status_color(&TrainingStatus::Initializing);
611 assert_eq!(initializing, TrainingPalette::INFO);
612
613 for status in &[
615 TrainingStatus::Running,
616 TrainingStatus::Completed,
617 TrainingStatus::Paused,
618 TrainingStatus::Failed("test".to_string()),
619 TrainingStatus::Initializing,
620 ] {
621 match status {
622 TrainingStatus::Running => {
623 assert_eq!(TrainingPalette::status_color(status), TrainingPalette::SUCCESS);
624 }
625 TrainingStatus::Completed => {
626 assert_eq!(TrainingPalette::status_color(status), TrainingPalette::PRIMARY);
627 }
628 TrainingStatus::Paused => {
629 assert_eq!(TrainingPalette::status_color(status), TrainingPalette::WARNING);
630 }
631 TrainingStatus::Failed(_) => {
632 assert_eq!(TrainingPalette::status_color(status), TrainingPalette::ERROR);
633 }
634 TrainingStatus::Initializing => {
635 assert_eq!(TrainingPalette::status_color(status), TrainingPalette::INFO);
636 }
637 }
638 }
639 }
640
641 #[test]
642 fn test_colored_bar() {
643 let bar = colored_bar(50.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
644 assert!(bar.contains('█'));
645 assert!(bar.contains('░'));
646 assert_eq!(bar.chars().filter(|&c| c == '█').count(), 5);
647 assert_eq!(bar.chars().filter(|&c| c == '░').count(), 5);
648 }
649
650 #[test]
653 fn test_clamped_f32_to_u8_boundaries() {
654 assert_eq!(clamped_f32_to_u8(0.0), 0);
655 assert_eq!(clamped_f32_to_u8(255.0), 255);
656 assert_eq!(clamped_f32_to_u8(-10.0), 0);
657 assert_eq!(clamped_f32_to_u8(300.0), 255);
658 assert_eq!(clamped_f32_to_u8(127.5), 127);
659 }
660
661 #[test]
662 fn test_clamped_f32_to_usize_boundaries() {
663 assert_eq!(clamped_f32_to_usize(0.0), 0);
664 assert_eq!(clamped_f32_to_usize(-5.0), 0);
665 assert_eq!(clamped_f32_to_usize(10.0), 10);
666 assert_eq!(clamped_f32_to_usize(100.5), 100);
667 }
668
669 #[test]
670 fn test_color_mode_default() {
671 assert_eq!(ColorMode::default(), ColorMode::Mono);
672 }
673
674 #[test]
675 fn test_color_mode_detect_24bit() {
676 assert_eq!(ColorMode::detect_with_env(Some("24bit"), None, None), ColorMode::TrueColor);
677 }
678
679 #[test]
680 fn test_color_mode_detect_kitty() {
681 assert_eq!(ColorMode::detect_with_env(None, Some("kitty"), None), ColorMode::Color256);
682 }
683
684 #[test]
685 fn test_color_mode_detect_alacritty() {
686 assert_eq!(ColorMode::detect_with_env(None, Some("alacritty"), None), ColorMode::Color256);
687 }
688
689 #[test]
690 fn test_color_mode_detect_screen() {
691 assert_eq!(ColorMode::detect_with_env(None, Some("screen"), None), ColorMode::Color16);
692 }
693
694 #[test]
695 fn test_color_mode_detect_tmux() {
696 assert_eq!(ColorMode::detect_with_env(None, Some("tmux"), None), ColorMode::Color16);
697 }
698
699 #[test]
700 fn test_color_mode_detect_empty_term() {
701 assert_eq!(ColorMode::detect_with_env(None, Some(""), None), ColorMode::Mono);
702 }
703
704 #[test]
705 fn test_color_mode_detect_unknown_term() {
706 assert_eq!(
707 ColorMode::detect_with_env(None, Some("something-unknown"), None),
708 ColorMode::Color16
709 );
710 }
711
712 #[test]
713 fn test_color_mode_detect_no_env() {
714 assert_eq!(ColorMode::detect_with_env(None, None, None), ColorMode::Color16);
715 }
716
717 #[test]
718 fn test_rgb_new() {
719 let c = Rgb::new(10, 20, 30);
720 assert_eq!(c.r, 10);
721 assert_eq!(c.g, 20);
722 assert_eq!(c.b, 30);
723 }
724
725 #[test]
726 fn test_rgb_from_tuple() {
727 let c: Rgb = (100, 200, 50).into();
728 assert_eq!(c.r, 100);
729 assert_eq!(c.g, 200);
730 assert_eq!(c.b, 50);
731 }
732
733 #[test]
734 fn test_rgb_to_256_midrange() {
735 let c = Rgb::new(128, 128, 128);
736 let idx = c.to_256();
737 assert!((16..=231).contains(&idx));
739 }
740
741 #[test]
742 fn test_rgb_to_16_near_black_no_dominant() {
743 assert_eq!(Rgb::new(10, 10, 10).to_16(), 0);
745 }
746
747 #[test]
748 fn test_styled_display_256color() {
749 let styled = Styled::new("hello", ColorMode::Color256).fg(Rgb::new(255, 0, 0));
750 let output = styled.to_string();
751 assert!(output.contains("\x1b[38;5;"));
752 assert!(output.contains("hello"));
753 assert!(output.ends_with("\x1b[0m"));
754 }
755
756 #[test]
757 fn test_styled_display_16color_bright() {
758 let styled = Styled::new("bright", ColorMode::Color16).fg(Rgb::new(255, 50, 50));
759 let output = styled.to_string();
760 assert!(output.contains("\x1b[9"));
762 assert!(output.contains("bright"));
763 }
764
765 #[test]
766 fn test_styled_display_16color_dark() {
767 let styled = Styled::new("dark", ColorMode::Color16).fg(Rgb::new(0, 0, 100));
768 let output = styled.to_string();
769 assert!(output.contains("\x1b[3"));
771 assert!(output.contains("dark"));
772 }
773
774 #[test]
775 fn test_styled_bold() {
776 let styled = Styled::new("bold", ColorMode::TrueColor).bold();
777 let output = styled.to_string();
778 assert!(output.contains("\x1b[1m"));
779 assert!(output.contains("bold"));
780 assert!(output.ends_with("\x1b[0m"));
781 }
782
783 #[test]
784 fn test_styled_bold_and_fg() {
785 let styled = Styled::new("both", ColorMode::TrueColor).fg(Rgb::new(0, 255, 0)).bold();
786 let output = styled.to_string();
787 assert!(output.contains("\x1b[1m"));
788 assert!(output.contains("\x1b[38;2;0;255;0m"));
789 }
790
791 #[test]
792 fn test_styled_no_color_no_bold() {
793 let styled = Styled::new("plain", ColorMode::TrueColor);
794 let output = styled.to_string();
795 assert_eq!(output, "plain");
797 }
798
799 #[test]
800 fn test_training_palette_new() {
801 let palette = TrainingPalette::new(ColorMode::TrueColor);
802 assert_eq!(palette.mode, ColorMode::TrueColor);
803 }
804
805 #[test]
806 fn test_training_palette_style() {
807 let palette = TrainingPalette::new(ColorMode::Mono);
808 let styled = palette.style("text").fg(TrainingPalette::SUCCESS);
809 let output = styled.to_string();
810 assert_eq!(output, "text"); }
812
813 #[test]
814 fn test_vram_color_thresholds() {
815 assert_eq!(TrainingPalette::vram_color(30.0), TrainingPalette::SUCCESS);
816 assert_eq!(TrainingPalette::vram_color(60.0), TrainingPalette::INFO);
817 assert_eq!(TrainingPalette::vram_color(80.0), TrainingPalette::WARNING);
818 assert_eq!(TrainingPalette::vram_color(95.0), TrainingPalette::ERROR);
819 }
820
821 #[test]
822 fn test_power_color_thresholds() {
823 assert_eq!(TrainingPalette::power_color(40.0), TrainingPalette::SUCCESS);
824 assert_eq!(TrainingPalette::power_color(70.0), TrainingPalette::INFO);
825 assert_eq!(TrainingPalette::power_color(90.0), TrainingPalette::WARNING);
826 assert_eq!(TrainingPalette::power_color(99.0), TrainingPalette::ERROR);
827 }
828
829 #[test]
830 fn test_loss_color_equal_min_max() {
831 let color = TrainingPalette::loss_color(0.5, 1.0, 1.0);
833 assert_eq!(color, TrainingPalette::INFO);
834 }
835
836 #[test]
837 fn test_loss_color_at_min() {
838 let color = TrainingPalette::loss_color(0.0, 0.0, 10.0);
839 assert!(color.g > color.r);
841 }
842
843 #[test]
844 fn test_loss_color_at_max() {
845 let color = TrainingPalette::loss_color(10.0, 0.0, 10.0);
846 assert!(color.r > color.g);
848 }
849
850 #[test]
851 fn test_loss_color_at_midpoint() {
852 let color = TrainingPalette::loss_color(5.0, 0.0, 10.0);
853 assert!(color.r > 200);
855 assert!(color.g > 150);
856 }
857
858 #[test]
859 fn test_loss_trend_color_all_variants() {
860 assert_eq!(
861 TrainingPalette::loss_trend_color(&LossTrend::Decreasing),
862 TrainingPalette::SUCCESS
863 );
864 assert_eq!(TrainingPalette::loss_trend_color(&LossTrend::Stable), TrainingPalette::INFO);
865 assert_eq!(
866 TrainingPalette::loss_trend_color(&LossTrend::Increasing),
867 TrainingPalette::ERROR
868 );
869 assert_eq!(TrainingPalette::loss_trend_color(&LossTrend::Unknown), TrainingPalette::MUTED);
870 }
871
872 #[test]
873 fn test_progress_color_thresholds() {
874 assert_eq!(TrainingPalette::progress_color(50.0), TrainingPalette::INFO);
875 assert_eq!(TrainingPalette::progress_color(75.0), TrainingPalette::INFO);
876 assert_eq!(TrainingPalette::progress_color(90.0), TrainingPalette::SUCCESS);
877 assert_eq!(TrainingPalette::progress_color(100.0), TrainingPalette::PRIMARY);
878 }
879
880 #[test]
881 fn test_progress_color_boundary_at_75() {
882 assert_eq!(TrainingPalette::progress_color(75.0), TrainingPalette::INFO);
883 assert_eq!(TrainingPalette::progress_color(75.01), TrainingPalette::SUCCESS);
884 }
885
886 #[test]
887 fn test_colored_bar_zero_value() {
888 let bar = colored_bar(0.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
889 assert_eq!(bar.chars().filter(|&c| c == '░').count(), 10);
890 assert_eq!(bar.chars().filter(|&c| c == '█').count(), 0);
891 }
892
893 #[test]
894 fn test_colored_bar_full_value() {
895 let bar = colored_bar(100.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
896 assert_eq!(bar.chars().filter(|&c| c == '█').count(), 10);
897 assert_eq!(bar.chars().filter(|&c| c == '░').count(), 0);
898 }
899
900 #[test]
901 fn test_colored_bar_zero_max() {
902 let bar = colored_bar(50.0, 0.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
903 assert_eq!(bar.chars().filter(|&c| c == '░').count(), 10);
905 }
906
907 #[test]
908 fn test_colored_bar_truecolor() {
909 let bar = colored_bar(50.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::TrueColor);
910 assert!(bar.contains("\x1b["));
912 }
913
914 #[test]
915 fn test_colored_value_mono() {
916 let s = colored_value(42, Rgb::new(255, 0, 0), ColorMode::Mono);
917 assert_eq!(s, "42");
918 }
919
920 #[test]
921 fn test_colored_value_truecolor() {
922 let s = colored_value(3.14, Rgb::new(0, 255, 0), ColorMode::TrueColor);
923 assert!(s.contains("3.14"));
924 assert!(s.contains("\x1b[38;2;0;255;0m"));
925 }
926
927 #[test]
928 fn test_threshold_color_below_first() {
929 let color = TrainingPalette::threshold_color(
930 5.0,
931 &[(10.0, Rgb::new(1, 2, 3)), (20.0, Rgb::new(4, 5, 6))],
932 Rgb::new(7, 8, 9),
933 );
934 assert_eq!(color, Rgb::new(1, 2, 3));
935 }
936
937 #[test]
938 fn test_threshold_color_between_thresholds() {
939 let color = TrainingPalette::threshold_color(
940 15.0,
941 &[(10.0, Rgb::new(1, 2, 3)), (20.0, Rgb::new(4, 5, 6))],
942 Rgb::new(7, 8, 9),
943 );
944 assert_eq!(color, Rgb::new(4, 5, 6));
945 }
946
947 #[test]
948 fn test_threshold_color_above_all() {
949 let color = TrainingPalette::threshold_color(
950 25.0,
951 &[(10.0, Rgb::new(1, 2, 3)), (20.0, Rgb::new(4, 5, 6))],
952 Rgb::new(7, 8, 9),
953 );
954 assert_eq!(color, Rgb::new(7, 8, 9));
955 }
956
957 #[test]
958 fn test_threshold_color_empty_thresholds() {
959 let color = TrainingPalette::threshold_color(5.0, &[], Rgb::new(7, 8, 9));
960 assert_eq!(color, Rgb::new(7, 8, 9));
961 }
962
963 #[test]
964 fn test_gpu_util_color_clamping() {
965 assert_eq!(TrainingPalette::gpu_util_color(-10.0), TrainingPalette::MUTED);
967 assert_eq!(TrainingPalette::gpu_util_color(150.0), TrainingPalette::PRIMARY);
969 }
970
971 #[test]
972 fn test_temp_color_clamping() {
973 assert_eq!(TrainingPalette::temp_color(-20.0), TrainingPalette::SUCCESS);
974 assert_eq!(TrainingPalette::temp_color(250.0), TrainingPalette::ERROR);
975 }
976
977 #[test]
978 fn test_colored_bar_overflow_value() {
979 let bar = colored_bar(200.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
981 assert_eq!(bar.chars().filter(|&c| c == '█').count(), 10);
982 }
983
984 #[test]
985 fn test_styled_display_mono_with_bold_and_fg() {
986 let styled = Styled::new("mono", ColorMode::Mono).fg(Rgb::new(255, 0, 0)).bold();
988 let output = styled.to_string();
989 assert_eq!(output, "mono"); }
991
992 #[test]
993 fn test_semantic_color_constants_distinct() {
994 let colors = [
996 TrainingPalette::SUCCESS,
997 TrainingPalette::WARNING,
998 TrainingPalette::ERROR,
999 TrainingPalette::INFO,
1000 TrainingPalette::MUTED,
1001 TrainingPalette::PRIMARY,
1002 ];
1003 for i in 0..colors.len() {
1004 for j in (i + 1)..colors.len() {
1005 assert_ne!(colors[i], colors[j], "colors at {i} and {j} should differ");
1006 }
1007 }
1008 }
1009}