use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub enum Widget {
Gauge(GaugeWidget),
Sparkline(SparklineWidget),
ProgressBar(ProgressBarWidget),
Table(TableWidget),
Text(TextWidget),
}
#[derive(Debug, Clone)]
pub struct GaugeWidget {
pub label: String,
pub value_pct: f64,
pub warning_threshold: f64,
pub critical_threshold: f64,
pub max_value: f64,
pub suffix: String,
}
impl GaugeWidget {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
value_pct: 0.0,
warning_threshold: 70.0,
critical_threshold: 90.0,
max_value: 100.0,
suffix: "%".to_string(),
}
}
#[must_use]
pub fn with_value(mut self, value: f64) -> Self {
self.value_pct = value;
self
}
#[must_use]
pub fn with_thresholds(mut self, warning: f64, critical: f64) -> Self {
self.warning_threshold = warning;
self.critical_threshold = critical;
self
}
#[must_use]
pub fn color(&self) -> GaugeColor {
if self.value_pct >= self.critical_threshold {
GaugeColor::Critical
} else if self.value_pct >= self.warning_threshold {
GaugeColor::Warning
} else {
GaugeColor::Ok
}
}
#[must_use]
pub fn render_bar(&self, width: usize) -> String {
let ratio = (self.value_pct / self.max_value).min(1.0);
let filled = (ratio * width as f64).round() as usize;
let empty = width.saturating_sub(filled);
format!(
"{}: [{}{}] {:.1}{}",
self.label,
"\u{2588}".repeat(filled),
"\u{2591}".repeat(empty),
self.value_pct,
self.suffix
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GaugeColor {
Ok,
Warning,
Critical,
}
#[derive(Debug, Clone)]
pub struct SparklineWidget {
pub data: VecDeque<f64>,
pub label: String,
pub baseline: Option<f64>,
pub auto_scale: bool,
}
impl SparklineWidget {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self {
data: VecDeque::with_capacity(60),
label: label.into(),
baseline: None,
auto_scale: true,
}
}
#[must_use]
pub fn with_data(mut self, data: VecDeque<f64>) -> Self {
self.data = data;
self
}
#[must_use]
pub fn render(&self) -> String {
if self.data.is_empty() {
return String::new();
}
let blocks = [
'\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
'\u{2588}',
];
let (min, max) = if self.auto_scale {
let min = self.data.iter().copied().fold(f64::INFINITY, f64::min);
let max = self.data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
(min, max)
} else {
(0.0, 100.0)
};
let range = (max - min).max(0.001);
self.data
.iter()
.map(|&v| {
let normalized = ((v - min) / range).clamp(0.0, 1.0);
let idx = (normalized * 7.0).round() as usize;
blocks[idx.min(7)]
})
.collect()
}
}
#[derive(Debug, Clone)]
pub struct ProgressBarWidget {
pub label: String,
pub progress: f64,
pub total_desc: String,
}
impl ProgressBarWidget {
#[must_use]
pub fn new(label: impl Into<String>) -> Self {
Self { label: label.into(), progress: 0.0, total_desc: String::new() }
}
#[must_use]
pub fn with_progress(mut self, progress: f64) -> Self {
self.progress = progress.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn with_total(mut self, desc: impl Into<String>) -> Self {
self.total_desc = desc.into();
self
}
#[must_use]
pub fn render(&self, width: usize) -> String {
let filled = (self.progress * width as f64).round() as usize;
let empty = width.saturating_sub(filled);
format!(
"{}: [{}{}] {}",
self.label,
"\u{2588}".repeat(filled),
"\u{2591}".repeat(empty),
self.total_desc
)
}
}
#[derive(Debug, Clone)]
pub struct TableWidget {
pub headers: Vec<String>,
pub rows: Vec<Vec<String>>,
pub highlight_row: Option<usize>,
pub column_widths: Vec<usize>,
}
impl TableWidget {
#[must_use]
pub fn new(headers: Vec<String>) -> Self {
Self { headers, rows: Vec::new(), highlight_row: None, column_widths: Vec::new() }
}
pub fn add_row(&mut self, row: Vec<String>) {
self.rows.push(row);
}
pub fn highlight(&mut self, row: usize) {
self.highlight_row = Some(row);
}
#[must_use]
pub fn calculate_widths(&self) -> Vec<usize> {
let mut widths: Vec<usize> = self.headers.iter().map(|h| h.len()).collect();
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(cell.len());
}
}
}
widths
}
}
#[derive(Debug, Clone)]
pub struct TextWidget {
pub content: String,
pub style: TextStyle,
}
impl TextWidget {
#[must_use]
pub fn new(content: impl Into<String>) -> Self {
Self { content: content.into(), style: TextStyle::Normal }
}
#[must_use]
pub fn with_style(mut self, style: TextStyle) -> Self {
self.style = style;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextStyle {
Normal,
Bold,
Dim,
Italic,
Header,
Error,
Warning,
Success,
}