pub struct ProgressBar {
width: usize,
fill_char: char,
empty_char: char,
use_color: bool,
}
impl Default for ProgressBar {
fn default() -> Self {
ProgressBar {
width: 20,
fill_char: '█',
empty_char: '░',
use_color: false,
}
}
}
impl ProgressBar {
pub fn new(width: usize) -> Self {
ProgressBar {
width,
..Default::default()
}
}
pub fn with_color(mut self) -> Self {
self.use_color = true;
self
}
pub fn render(&self, value: f64) -> String {
let clamped = value.clamp(0.0, 1.0);
let filled = (clamped * self.width as f64).round() as usize;
let empty = self.width.saturating_sub(filled);
format!(
"[{}{}]",
self.fill_char.to_string().repeat(filled),
self.empty_char.to_string().repeat(empty)
)
}
pub fn render_with_percent(&self, value: f64) -> String {
let rendered = self.render(value);
format!("{} {:>3.0}%", rendered, value * 100.0)
}
pub fn render_segmented(&self, value: f64, thresholds: &[(f64, Severity)]) -> String {
let clamped = value.clamp(0.0, 1.0);
let filled = (clamped * self.width as f64).round() as usize;
let mut result = String::with_capacity(self.width + 10);
result.push('[');
for i in 0..self.width {
let pos = i as f64 / self.width as f64;
let char_to_use = if i < filled {
self.fill_char
} else {
self.empty_char
};
if self.use_color && i < filled {
let severity = thresholds
.iter()
.filter(|(t, _)| pos < *t)
.map(|(_, s)| s)
.next()
.unwrap_or(&Severity::Low);
result.push_str(severity.color_code());
result.push(char_to_use);
result.push_str("\x1b[0m");
} else {
result.push(char_to_use);
}
}
result.push(']');
result
}
}
pub struct Sparkline {
chars: [char; 8],
}
impl Default for Sparkline {
fn default() -> Self {
Sparkline {
chars: ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'],
}
}
}
impl Sparkline {
pub fn render(&self, values: &[u8]) -> String {
values
.iter()
.map(|&v| self.chars[(v.min(7)) as usize])
.collect()
}
pub fn render_auto(&self, values: &[f64]) -> String {
if values.is_empty() {
return String::new();
}
let min = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let range = max - min;
if range == 0.0 {
return self.chars[4].to_string().repeat(values.len());
}
let normalized: Vec<u8> = values
.iter()
.map(|&v| ((v - min) / range * 7.0).round() as u8)
.collect();
self.render(&normalized)
}
pub fn render_with_trend(&self, values: &[f64]) -> String {
let sparkline = self.render_auto(values);
let direction = Self::detect_trend(values);
format!("{} {}", sparkline, direction.arrow())
}
fn detect_trend(values: &[f64]) -> TrendDirection {
if values.len() < 2 {
return TrendDirection::Stable;
}
let n = values.len() as f64;
let x_mean = (n - 1.0) / 2.0;
let y_mean: f64 = values.iter().sum::<f64>() / n;
let mut numerator = 0.0;
let mut denominator = 0.0;
for (i, &y) in values.iter().enumerate() {
let x = i as f64;
numerator += (x - x_mean) * (y - y_mean);
denominator += (x - x_mean) * (x - x_mean);
}
if denominator == 0.0 {
return TrendDirection::Stable;
}
let slope = numerator / denominator;
let threshold = y_mean.abs() * 0.05;
if slope > threshold {
TrendDirection::Degrading
} else if slope < -threshold {
TrendDirection::Improving
} else {
TrendDirection::Stable
}
}
}
pub struct StatusIndicator;
impl StatusIndicator {
pub fn pass() -> &'static str {
"✓"
}
pub fn fail() -> &'static str {
"✗"
}
pub fn warning() -> &'static str {
"⚠"
}
pub fn pending() -> &'static str {
"◷"
}
pub fn info() -> &'static str {
"ℹ"
}
}