use super::state::{LossTrend, TrainingStatus};
use std::fmt;
#[inline]
fn clamped_f32_to_u8(value: f32) -> u8 {
let clamped = value.clamp(0.0, 255.0);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let wide = clamped as u16;
u8::try_from(wide).unwrap_or(u8::MAX)
}
#[inline]
fn clamped_f32_to_usize(value: f32) -> usize {
let clamped = value.max(0.0);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let result = clamped as usize;
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorMode {
TrueColor,
Color256,
Color16,
#[default]
Mono,
}
impl ColorMode {
pub fn detect() -> Self {
Self::detect_with_env(
std::env::var("COLORTERM").ok().as_deref(),
std::env::var("TERM").ok().as_deref(),
std::env::var("NO_COLOR").ok().as_deref(),
)
}
pub fn detect_with_env(
colorterm: Option<&str>,
term: Option<&str>,
no_color: Option<&str>,
) -> Self {
if no_color.is_some() {
return Self::Mono;
}
if let Some(ct) = colorterm {
if ct.contains("truecolor") || ct.contains("24bit") {
return Self::TrueColor;
}
}
if let Some(term) = term {
if term.contains("256color") || term.contains("kitty") || term.contains("alacritty") {
return Self::Color256;
}
if term.contains("xterm") || term.contains("screen") || term.contains("tmux") {
return Self::Color16;
}
if term == "dumb" || term.is_empty() {
return Self::Mono;
}
}
Self::Color16
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
}
impl From<(u8, u8, u8)> for Rgb {
fn from((r, g, b): (u8, u8, u8)) -> Self {
Self { r, g, b }
}
}
impl Rgb {
pub fn to_256(self) -> u8 {
let r6 = u8::try_from(u16::from(self.r) * 5 / 255).unwrap_or(u8::MAX);
let g6 = u8::try_from(u16::from(self.g) * 5 / 255).unwrap_or(u8::MAX);
let b6 = u8::try_from(u16::from(self.b) * 5 / 255).unwrap_or(u8::MAX);
16 + 36 * r6 + 6 * g6 + b6
}
pub fn to_16(self) -> u8 {
let max_channel = self.r.max(self.g).max(self.b);
let is_bright = max_channel > 180;
let r_dom = self.r >= self.g && self.r >= self.b;
let g_dom = self.g >= self.r && self.g >= self.b;
let b_dom = self.b >= self.r && self.b >= self.g;
let r_present = self.r > 85;
let g_present = self.g > 85;
let b_present = self.b > 85;
let base = match (r_present, g_present, b_present) {
(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) => {
if r_dom && self.r > 40 {
1
} else if g_dom && self.g > 40 {
2
} else if b_dom && self.b > 40 {
4
} else {
0
}
}
};
if is_bright {
base + 8
} else {
base
}
}
}
pub struct Styled<'a> {
text: &'a str,
fg: Option<Rgb>,
bold: bool,
mode: ColorMode,
}
impl<'a> Styled<'a> {
pub fn new(text: &'a str, mode: ColorMode) -> Self {
Self { text, fg: None, bold: false, mode }
}
pub fn fg(mut self, color: impl Into<Rgb>) -> Self {
self.fg = Some(color.into());
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
}
impl fmt::Display for Styled<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.mode == ColorMode::Mono {
return write!(f, "{}", self.text);
}
let mut has_style = false;
if self.bold {
write!(f, "\x1b[1m")?;
has_style = true;
}
if let Some(rgb) = self.fg {
match self.mode {
ColorMode::TrueColor => {
write!(f, "\x1b[38;2;{};{};{}m", rgb.r, rgb.g, rgb.b)?;
}
ColorMode::Color256 => {
write!(f, "\x1b[38;5;{}m", rgb.to_256())?;
}
ColorMode::Color16 => {
let code = rgb.to_16();
if code >= 8 {
write!(f, "\x1b[9{}m", code - 8)?;
} else {
write!(f, "\x1b[3{code}m")?;
}
}
ColorMode::Mono => {}
}
has_style = true;
}
write!(f, "{}", self.text)?;
if has_style {
write!(f, "\x1b[0m")?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct TrainingPalette {
pub mode: ColorMode,
}
impl Default for TrainingPalette {
fn default() -> Self {
Self { mode: ColorMode::detect() }
}
}
impl TrainingPalette {
pub fn new(mode: ColorMode) -> Self {
Self { mode }
}
pub fn style<'a>(&self, text: &'a str) -> Styled<'a> {
Styled::new(text, self.mode)
}
pub const SUCCESS: Rgb = Rgb::new(80, 200, 120);
pub const WARNING: Rgb = Rgb::new(255, 193, 7);
pub const ERROR: Rgb = Rgb::new(244, 67, 54);
pub const INFO: Rgb = Rgb::new(33, 150, 243);
pub const MUTED: Rgb = Rgb::new(158, 158, 158);
pub const PRIMARY: Rgb = Rgb::new(0, 188, 212);
fn threshold_color(value: f32, thresholds: &[(f32, Rgb)], fallback: Rgb) -> Rgb {
for &(bound, color) in thresholds {
if value <= bound {
return color;
}
}
fallback
}
pub fn gpu_util_color(percent: f32) -> Rgb {
let p = percent.clamp(0.0, 100.0);
Self::threshold_color(
p,
&[(30.0, Self::MUTED), (70.0, Self::SUCCESS), (90.0, Self::INFO)],
Self::PRIMARY,
)
}
pub fn vram_color(percent: f32) -> Rgb {
let p = percent.clamp(0.0, 100.0);
Self::threshold_color(
p,
&[(50.0, Self::SUCCESS), (75.0, Self::INFO), (90.0, Self::WARNING)],
Self::ERROR,
)
}
pub fn temp_color(celsius: f32) -> Rgb {
let t = celsius.clamp(0.0, 200.0);
Self::threshold_color(
t,
&[(50.0, Self::SUCCESS), (70.0, Self::INFO), (80.0, Self::WARNING)],
Self::ERROR,
)
}
pub fn power_color(percent: f32) -> Rgb {
let p = percent.clamp(0.0, 100.0);
Self::threshold_color(
p,
&[(60.0, Self::SUCCESS), (80.0, Self::INFO), (95.0, Self::WARNING)],
Self::ERROR,
)
}
pub fn grad_norm_color(norm: f32) -> Rgb {
Self::threshold_color(
norm,
&[(1.0, Self::SUCCESS), (5.0, Self::INFO), (10.0, Self::WARNING)],
Self::ERROR,
)
}
pub fn loss_color(loss: f32, min_loss: f32, max_loss: f32) -> Rgb {
if max_loss <= min_loss {
return Self::INFO;
}
let normalized = ((loss - min_loss) / (max_loss - min_loss)).clamp(0.0, 1.0);
let (r, g, b) = if normalized < 0.5 {
let t = normalized * 2.0;
(
clamped_f32_to_u8(80.0 + t * 175.0),
clamped_f32_to_u8(200.0 - t * 7.0),
clamped_f32_to_u8(120.0 - t * 113.0),
)
} else {
let t = (normalized - 0.5) * 2.0;
(
clamped_f32_to_u8(255.0 - t * 11.0),
clamped_f32_to_u8(193.0 - t * 126.0),
clamped_f32_to_u8(7.0 + t * 47.0),
)
};
Rgb::new(r, g, b)
}
pub fn status_color(status: &TrainingStatus) -> Rgb {
match status {
TrainingStatus::Running => Self::SUCCESS,
TrainingStatus::Completed => Self::PRIMARY,
TrainingStatus::Paused => Self::WARNING,
TrainingStatus::Failed(_) => Self::ERROR,
TrainingStatus::Initializing => Self::INFO,
}
}
pub fn loss_trend_color(trend: &LossTrend) -> Rgb {
match trend {
LossTrend::Decreasing => Self::SUCCESS, LossTrend::Stable => Self::INFO, LossTrend::Increasing => Self::ERROR, LossTrend::Unknown => Self::MUTED, }
}
pub fn progress_color(percent: f32) -> Rgb {
let p = percent.clamp(0.0, 100.0);
if p <= 75.0 {
Self::INFO } else if p < 100.0 {
Self::SUCCESS } else {
Self::PRIMARY }
}
}
pub fn colored_bar(value: f32, max: f32, width: usize, color: Rgb, mode: ColorMode) -> String {
let percent = if max > 0.0 { value / max } else { 0.0 };
let percent = percent.clamp(0.0, 1.0);
let width_clamped = u16::try_from(width).unwrap_or(u16::MAX);
let filled_f32 = f32::from(width_clamped) * percent;
let filled = clamped_f32_to_usize(filled_f32.clamp(0.0, f32::from(width_clamped))).min(width);
let empty = width.saturating_sub(filled);
let filled_str: String = std::iter::repeat_n('█', filled).collect();
let empty_str: String = std::iter::repeat_n('░', empty).collect();
if mode == ColorMode::Mono {
format!("{filled_str}{empty_str}")
} else {
format!(
"{}{}",
Styled::new(&filled_str, mode).fg(color),
Styled::new(&empty_str, mode).fg(TrainingPalette::MUTED)
)
}
}
pub fn colored_value<T: fmt::Display>(value: T, color: Rgb, mode: ColorMode) -> String {
let text = value.to_string();
Styled::new(&text, mode).fg(color).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_mode_detection() {
assert_eq!(
ColorMode::detect_with_env(Some("truecolor"), Some("xterm-256color"), Some("1")),
ColorMode::Mono
);
assert_eq!(ColorMode::detect_with_env(Some("truecolor"), None, None), ColorMode::TrueColor);
assert_eq!(
ColorMode::detect_with_env(None, Some("xterm-256color"), None),
ColorMode::Color256
);
assert_eq!(ColorMode::detect_with_env(None, Some("xterm"), None), ColorMode::Color16);
assert_eq!(ColorMode::detect_with_env(None, Some("dumb"), None), ColorMode::Mono);
}
#[test]
fn test_rgb_to_256() {
assert_eq!(Rgb::new(0, 0, 0).to_256(), 16);
assert_eq!(Rgb::new(255, 255, 255).to_256(), 231);
assert_eq!(Rgb::new(255, 0, 0).to_256(), 196);
assert_eq!(Rgb::new(0, 255, 0).to_256(), 46);
assert_eq!(Rgb::new(0, 0, 255).to_256(), 21);
}
#[test]
fn test_rgb_to_16() {
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); }
#[test]
fn test_rgb_to_16_all_boolean_combos() {
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); }
#[test]
fn test_styled_display_truecolor() {
let styled = Styled::new("test", ColorMode::TrueColor).fg(Rgb::new(255, 0, 0));
let output = styled.to_string();
assert!(output.contains("\x1b[38;2;255;0;0m"));
assert!(output.contains("test"));
assert!(output.ends_with("\x1b[0m"));
}
#[test]
fn test_styled_display_mono() {
let styled = Styled::new("test", ColorMode::Mono).fg(Rgb::new(255, 0, 0));
let output = styled.to_string();
assert_eq!(output, "test");
}
#[test]
fn test_gpu_util_color() {
assert_eq!(TrainingPalette::gpu_util_color(20.0), TrainingPalette::MUTED);
assert_eq!(TrainingPalette::gpu_util_color(50.0), TrainingPalette::SUCCESS);
assert_eq!(TrainingPalette::gpu_util_color(80.0), TrainingPalette::INFO);
assert_eq!(TrainingPalette::gpu_util_color(95.0), TrainingPalette::PRIMARY);
}
#[test]
fn test_temp_color() {
assert_eq!(TrainingPalette::temp_color(40.0), TrainingPalette::SUCCESS);
assert_eq!(TrainingPalette::temp_color(65.0), TrainingPalette::INFO);
assert_eq!(TrainingPalette::temp_color(75.0), TrainingPalette::WARNING);
assert_eq!(TrainingPalette::temp_color(85.0), TrainingPalette::ERROR);
}
#[test]
fn test_grad_norm_color() {
assert_eq!(TrainingPalette::grad_norm_color(0.5), TrainingPalette::SUCCESS);
assert_eq!(TrainingPalette::grad_norm_color(3.0), TrainingPalette::INFO);
assert_eq!(TrainingPalette::grad_norm_color(8.0), TrainingPalette::WARNING);
assert_eq!(TrainingPalette::grad_norm_color(20.0), TrainingPalette::ERROR);
}
#[test]
fn test_loss_color_gradient() {
let min = 0.0;
let max = 1.0;
let low = TrainingPalette::loss_color(0.1, min, max);
assert!(low.g > low.r);
let high = TrainingPalette::loss_color(0.9, min, max);
assert!(high.r > high.g); }
#[test]
fn test_status_color_all_variants() {
let running = TrainingPalette::status_color(&TrainingStatus::Running);
assert_eq!(running, TrainingPalette::SUCCESS);
let completed = TrainingPalette::status_color(&TrainingStatus::Completed);
assert_eq!(completed, TrainingPalette::PRIMARY);
let paused = TrainingPalette::status_color(&TrainingStatus::Paused);
assert_eq!(paused, TrainingPalette::WARNING);
let failed = TrainingPalette::status_color(&TrainingStatus::Failed("error".to_string()));
assert_eq!(failed, TrainingPalette::ERROR);
let initializing = TrainingPalette::status_color(&TrainingStatus::Initializing);
assert_eq!(initializing, TrainingPalette::INFO);
for status in &[
TrainingStatus::Running,
TrainingStatus::Completed,
TrainingStatus::Paused,
TrainingStatus::Failed("test".to_string()),
TrainingStatus::Initializing,
] {
match status {
TrainingStatus::Running => {
assert_eq!(TrainingPalette::status_color(status), TrainingPalette::SUCCESS);
}
TrainingStatus::Completed => {
assert_eq!(TrainingPalette::status_color(status), TrainingPalette::PRIMARY);
}
TrainingStatus::Paused => {
assert_eq!(TrainingPalette::status_color(status), TrainingPalette::WARNING);
}
TrainingStatus::Failed(_) => {
assert_eq!(TrainingPalette::status_color(status), TrainingPalette::ERROR);
}
TrainingStatus::Initializing => {
assert_eq!(TrainingPalette::status_color(status), TrainingPalette::INFO);
}
}
}
}
#[test]
fn test_colored_bar() {
let bar = colored_bar(50.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
assert!(bar.contains('█'));
assert!(bar.contains('░'));
assert_eq!(bar.chars().filter(|&c| c == '█').count(), 5);
assert_eq!(bar.chars().filter(|&c| c == '░').count(), 5);
}
#[test]
fn test_clamped_f32_to_u8_boundaries() {
assert_eq!(clamped_f32_to_u8(0.0), 0);
assert_eq!(clamped_f32_to_u8(255.0), 255);
assert_eq!(clamped_f32_to_u8(-10.0), 0);
assert_eq!(clamped_f32_to_u8(300.0), 255);
assert_eq!(clamped_f32_to_u8(127.5), 127);
}
#[test]
fn test_clamped_f32_to_usize_boundaries() {
assert_eq!(clamped_f32_to_usize(0.0), 0);
assert_eq!(clamped_f32_to_usize(-5.0), 0);
assert_eq!(clamped_f32_to_usize(10.0), 10);
assert_eq!(clamped_f32_to_usize(100.5), 100);
}
#[test]
fn test_color_mode_default() {
assert_eq!(ColorMode::default(), ColorMode::Mono);
}
#[test]
fn test_color_mode_detect_24bit() {
assert_eq!(ColorMode::detect_with_env(Some("24bit"), None, None), ColorMode::TrueColor);
}
#[test]
fn test_color_mode_detect_kitty() {
assert_eq!(ColorMode::detect_with_env(None, Some("kitty"), None), ColorMode::Color256);
}
#[test]
fn test_color_mode_detect_alacritty() {
assert_eq!(ColorMode::detect_with_env(None, Some("alacritty"), None), ColorMode::Color256);
}
#[test]
fn test_color_mode_detect_screen() {
assert_eq!(ColorMode::detect_with_env(None, Some("screen"), None), ColorMode::Color16);
}
#[test]
fn test_color_mode_detect_tmux() {
assert_eq!(ColorMode::detect_with_env(None, Some("tmux"), None), ColorMode::Color16);
}
#[test]
fn test_color_mode_detect_empty_term() {
assert_eq!(ColorMode::detect_with_env(None, Some(""), None), ColorMode::Mono);
}
#[test]
fn test_color_mode_detect_unknown_term() {
assert_eq!(
ColorMode::detect_with_env(None, Some("something-unknown"), None),
ColorMode::Color16
);
}
#[test]
fn test_color_mode_detect_no_env() {
assert_eq!(ColorMode::detect_with_env(None, None, None), ColorMode::Color16);
}
#[test]
fn test_rgb_new() {
let c = Rgb::new(10, 20, 30);
assert_eq!(c.r, 10);
assert_eq!(c.g, 20);
assert_eq!(c.b, 30);
}
#[test]
fn test_rgb_from_tuple() {
let c: Rgb = (100, 200, 50).into();
assert_eq!(c.r, 100);
assert_eq!(c.g, 200);
assert_eq!(c.b, 50);
}
#[test]
fn test_rgb_to_256_midrange() {
let c = Rgb::new(128, 128, 128);
let idx = c.to_256();
assert!((16..=231).contains(&idx));
}
#[test]
fn test_rgb_to_16_near_black_no_dominant() {
assert_eq!(Rgb::new(10, 10, 10).to_16(), 0);
}
#[test]
fn test_styled_display_256color() {
let styled = Styled::new("hello", ColorMode::Color256).fg(Rgb::new(255, 0, 0));
let output = styled.to_string();
assert!(output.contains("\x1b[38;5;"));
assert!(output.contains("hello"));
assert!(output.ends_with("\x1b[0m"));
}
#[test]
fn test_styled_display_16color_bright() {
let styled = Styled::new("bright", ColorMode::Color16).fg(Rgb::new(255, 50, 50));
let output = styled.to_string();
assert!(output.contains("\x1b[9"));
assert!(output.contains("bright"));
}
#[test]
fn test_styled_display_16color_dark() {
let styled = Styled::new("dark", ColorMode::Color16).fg(Rgb::new(0, 0, 100));
let output = styled.to_string();
assert!(output.contains("\x1b[3"));
assert!(output.contains("dark"));
}
#[test]
fn test_styled_bold() {
let styled = Styled::new("bold", ColorMode::TrueColor).bold();
let output = styled.to_string();
assert!(output.contains("\x1b[1m"));
assert!(output.contains("bold"));
assert!(output.ends_with("\x1b[0m"));
}
#[test]
fn test_styled_bold_and_fg() {
let styled = Styled::new("both", ColorMode::TrueColor).fg(Rgb::new(0, 255, 0)).bold();
let output = styled.to_string();
assert!(output.contains("\x1b[1m"));
assert!(output.contains("\x1b[38;2;0;255;0m"));
}
#[test]
fn test_styled_no_color_no_bold() {
let styled = Styled::new("plain", ColorMode::TrueColor);
let output = styled.to_string();
assert_eq!(output, "plain");
}
#[test]
fn test_training_palette_new() {
let palette = TrainingPalette::new(ColorMode::TrueColor);
assert_eq!(palette.mode, ColorMode::TrueColor);
}
#[test]
fn test_training_palette_style() {
let palette = TrainingPalette::new(ColorMode::Mono);
let styled = palette.style("text").fg(TrainingPalette::SUCCESS);
let output = styled.to_string();
assert_eq!(output, "text"); }
#[test]
fn test_vram_color_thresholds() {
assert_eq!(TrainingPalette::vram_color(30.0), TrainingPalette::SUCCESS);
assert_eq!(TrainingPalette::vram_color(60.0), TrainingPalette::INFO);
assert_eq!(TrainingPalette::vram_color(80.0), TrainingPalette::WARNING);
assert_eq!(TrainingPalette::vram_color(95.0), TrainingPalette::ERROR);
}
#[test]
fn test_power_color_thresholds() {
assert_eq!(TrainingPalette::power_color(40.0), TrainingPalette::SUCCESS);
assert_eq!(TrainingPalette::power_color(70.0), TrainingPalette::INFO);
assert_eq!(TrainingPalette::power_color(90.0), TrainingPalette::WARNING);
assert_eq!(TrainingPalette::power_color(99.0), TrainingPalette::ERROR);
}
#[test]
fn test_loss_color_equal_min_max() {
let color = TrainingPalette::loss_color(0.5, 1.0, 1.0);
assert_eq!(color, TrainingPalette::INFO);
}
#[test]
fn test_loss_color_at_min() {
let color = TrainingPalette::loss_color(0.0, 0.0, 10.0);
assert!(color.g > color.r);
}
#[test]
fn test_loss_color_at_max() {
let color = TrainingPalette::loss_color(10.0, 0.0, 10.0);
assert!(color.r > color.g);
}
#[test]
fn test_loss_color_at_midpoint() {
let color = TrainingPalette::loss_color(5.0, 0.0, 10.0);
assert!(color.r > 200);
assert!(color.g > 150);
}
#[test]
fn test_loss_trend_color_all_variants() {
assert_eq!(
TrainingPalette::loss_trend_color(&LossTrend::Decreasing),
TrainingPalette::SUCCESS
);
assert_eq!(TrainingPalette::loss_trend_color(&LossTrend::Stable), TrainingPalette::INFO);
assert_eq!(
TrainingPalette::loss_trend_color(&LossTrend::Increasing),
TrainingPalette::ERROR
);
assert_eq!(TrainingPalette::loss_trend_color(&LossTrend::Unknown), TrainingPalette::MUTED);
}
#[test]
fn test_progress_color_thresholds() {
assert_eq!(TrainingPalette::progress_color(50.0), TrainingPalette::INFO);
assert_eq!(TrainingPalette::progress_color(75.0), TrainingPalette::INFO);
assert_eq!(TrainingPalette::progress_color(90.0), TrainingPalette::SUCCESS);
assert_eq!(TrainingPalette::progress_color(100.0), TrainingPalette::PRIMARY);
}
#[test]
fn test_progress_color_boundary_at_75() {
assert_eq!(TrainingPalette::progress_color(75.0), TrainingPalette::INFO);
assert_eq!(TrainingPalette::progress_color(75.01), TrainingPalette::SUCCESS);
}
#[test]
fn test_colored_bar_zero_value() {
let bar = colored_bar(0.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
assert_eq!(bar.chars().filter(|&c| c == '░').count(), 10);
assert_eq!(bar.chars().filter(|&c| c == '█').count(), 0);
}
#[test]
fn test_colored_bar_full_value() {
let bar = colored_bar(100.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
assert_eq!(bar.chars().filter(|&c| c == '█').count(), 10);
assert_eq!(bar.chars().filter(|&c| c == '░').count(), 0);
}
#[test]
fn test_colored_bar_zero_max() {
let bar = colored_bar(50.0, 0.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
assert_eq!(bar.chars().filter(|&c| c == '░').count(), 10);
}
#[test]
fn test_colored_bar_truecolor() {
let bar = colored_bar(50.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::TrueColor);
assert!(bar.contains("\x1b["));
}
#[test]
fn test_colored_value_mono() {
let s = colored_value(42, Rgb::new(255, 0, 0), ColorMode::Mono);
assert_eq!(s, "42");
}
#[test]
fn test_colored_value_truecolor() {
let s = colored_value(3.14, Rgb::new(0, 255, 0), ColorMode::TrueColor);
assert!(s.contains("3.14"));
assert!(s.contains("\x1b[38;2;0;255;0m"));
}
#[test]
fn test_threshold_color_below_first() {
let color = TrainingPalette::threshold_color(
5.0,
&[(10.0, Rgb::new(1, 2, 3)), (20.0, Rgb::new(4, 5, 6))],
Rgb::new(7, 8, 9),
);
assert_eq!(color, Rgb::new(1, 2, 3));
}
#[test]
fn test_threshold_color_between_thresholds() {
let color = TrainingPalette::threshold_color(
15.0,
&[(10.0, Rgb::new(1, 2, 3)), (20.0, Rgb::new(4, 5, 6))],
Rgb::new(7, 8, 9),
);
assert_eq!(color, Rgb::new(4, 5, 6));
}
#[test]
fn test_threshold_color_above_all() {
let color = TrainingPalette::threshold_color(
25.0,
&[(10.0, Rgb::new(1, 2, 3)), (20.0, Rgb::new(4, 5, 6))],
Rgb::new(7, 8, 9),
);
assert_eq!(color, Rgb::new(7, 8, 9));
}
#[test]
fn test_threshold_color_empty_thresholds() {
let color = TrainingPalette::threshold_color(5.0, &[], Rgb::new(7, 8, 9));
assert_eq!(color, Rgb::new(7, 8, 9));
}
#[test]
fn test_gpu_util_color_clamping() {
assert_eq!(TrainingPalette::gpu_util_color(-10.0), TrainingPalette::MUTED);
assert_eq!(TrainingPalette::gpu_util_color(150.0), TrainingPalette::PRIMARY);
}
#[test]
fn test_temp_color_clamping() {
assert_eq!(TrainingPalette::temp_color(-20.0), TrainingPalette::SUCCESS);
assert_eq!(TrainingPalette::temp_color(250.0), TrainingPalette::ERROR);
}
#[test]
fn test_colored_bar_overflow_value() {
let bar = colored_bar(200.0, 100.0, 10, TrainingPalette::SUCCESS, ColorMode::Mono);
assert_eq!(bar.chars().filter(|&c| c == '█').count(), 10);
}
#[test]
fn test_styled_display_mono_with_bold_and_fg() {
let styled = Styled::new("mono", ColorMode::Mono).fg(Rgb::new(255, 0, 0)).bold();
let output = styled.to_string();
assert_eq!(output, "mono"); }
#[test]
fn test_semantic_color_constants_distinct() {
let colors = [
TrainingPalette::SUCCESS,
TrainingPalette::WARNING,
TrainingPalette::ERROR,
TrainingPalette::INFO,
TrainingPalette::MUTED,
TrainingPalette::PRIMARY,
];
for i in 0..colors.len() {
for j in (i + 1)..colors.len() {
assert_ne!(colors[i], colors[j], "colors at {i} and {j} should differ");
}
}
}
}