use crate::types::{HealthStatus, TempUnit};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, BorderType, Borders};
pub const TEMP_CPU_WARN: f64 = 70.0;
pub const TEMP_CPU_CRIT: f64 = 85.0;
pub const TEMP_GPU_WARN: f64 = 75.0;
pub const TEMP_GPU_CRIT: f64 = 90.0;
pub const COLOR_GOOD: Color = Color::Rgb(130, 170, 120); pub const COLOR_WARN: Color = Color::Rgb(210, 160, 60); pub const COLOR_CRIT: Color = Color::Rgb(190, 85, 75); pub const COLOR_INFO: Color = Color::Rgb(140, 170, 200); pub const COLOR_ACCENT: Color = Color::Rgb(200, 160, 100); pub const COLOR_DIM: Color = Color::Rgb(110, 105, 100); pub const COLOR_HEADER: Color = Color::Rgb(200, 160, 100); pub const COLOR_TEXT: Color = Color::Rgb(210, 205, 200); pub const COLOR_MUTED: Color = Color::Rgb(150, 145, 140); pub const COLOR_BORDER: Color = Color::Rgb(80, 75, 70); pub const COLOR_HIGHLIGHT_BG: Color = Color::Rgb(50, 48, 45);
pub const SPARK_CPU: Color = Color::Rgb(200, 160, 100); pub const SPARK_MEMORY: Color = Color::Rgb(160, 120, 170); pub const SPARK_NET_DOWN: Color = Color::Rgb(140, 170, 200); pub const SPARK_NET_UP: Color = Color::Rgb(160, 120, 170); pub const SPARK_GPU: Color = Color::Rgb(130, 170, 120); pub const SPARK_TEMP: Color = Color::Rgb(210, 160, 60); pub const SPARK_SWAP: Color = Color::Rgb(210, 160, 60);
pub fn sparkline_bar_set() -> ratatui::symbols::bar::Set<'static> {
if cfg!(windows) {
ratatui::symbols::bar::THREE_LEVELS
} else {
ratatui::symbols::bar::NINE_LEVELS
}
}
pub fn gauge_empty_char() -> &'static str {
if cfg!(windows) {
"\u{2500}"
} else {
"\u{2591}"
}
}
pub fn status_color(status: &HealthStatus) -> Color {
match status {
HealthStatus::Good => COLOR_GOOD,
HealthStatus::Warning => COLOR_WARN,
HealthStatus::Critical => COLOR_CRIT,
HealthStatus::Unknown => COLOR_DIM,
}
}
pub fn content_block(title: &str) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(COLOR_BORDER))
.title(format!(" {} ", title))
.title_style(
Style::default()
.fg(COLOR_ACCENT)
.add_modifier(Modifier::BOLD),
)
}
pub fn sub_block(title: &str) -> Block<'static> {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(COLOR_BORDER))
.title(format!(" {} ", title))
.title_style(Style::default().fg(COLOR_MUTED))
}
pub fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
const TB: u64 = GB * 1024;
if bytes >= TB {
format!("{:.1} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn format_bytes_gib(bytes: u64) -> String {
let gib = bytes as f64 / (1024.0 * 1024.0 * 1024.0);
if gib >= 1024.0 {
format!("{:.1} TiB", gib / 1024.0)
} else if gib >= 1.0 {
format!("{:.2} GiB", gib)
} else {
let mib = bytes as f64 / (1024.0 * 1024.0);
format!("{:.1} MiB", mib)
}
}
pub fn format_throughput(bytes_per_sec: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes_per_sec >= GB {
format!("{:.1} GB/s", bytes_per_sec as f64 / GB as f64)
} else if bytes_per_sec >= MB {
format!("{:.1} MB/s", bytes_per_sec as f64 / MB as f64)
} else if bytes_per_sec >= KB {
format!("{:.1} KB/s", bytes_per_sec as f64 / KB as f64)
} else {
format!("{} B/s", bytes_per_sec)
}
}
pub fn format_uptime(seconds: u64) -> String {
let days = seconds / 86400;
let hours = (seconds % 86400) / 3600;
let minutes = (seconds % 3600) / 60;
if days > 0 {
format!("{}d {}h {}m", days, hours, minutes)
} else if hours > 0 {
format!("{}h {}m", hours, minutes)
} else {
format!("{}m", minutes)
}
}
pub fn plain_language_percent(pct: f64, resource: &str) -> String {
if pct < 30.0 {
format!("Plenty of {} free", resource)
} else if pct < 60.0 {
format!("Moderate {} use", resource)
} else if pct < 80.0 {
format!("Most {} in use", resource)
} else if pct < 95.0 {
format!("{} getting full", resource)
} else {
format!("{} nearly full", resource)
}
}
pub fn plain_language_temp(temp_c: f64) -> &'static str {
if temp_c < 45.0 {
"Cool"
} else if temp_c < 65.0 {
"Normal"
} else if temp_c < 80.0 {
"Warm"
} else if temp_c < 95.0 {
"Hot"
} else {
"Very hot"
}
}
pub fn format_temp(temp_c: f64, unit: TempUnit) -> String {
let value = unit.convert(temp_c);
format!("{:.0}{}", value, unit.suffix())
}
pub fn plain_language_cpu(pct: f32) -> &'static str {
if pct < 25.0 {
"Running quietly"
} else if pct < 50.0 {
"Running normally"
} else if pct < 75.0 {
"Fairly busy right now"
} else if pct < 90.0 {
"Very busy"
} else {
"Extremely busy"
}
}
pub fn plain_language_speed(bytes_per_sec: u64) -> &'static str {
const MB: u64 = 1024 * 1024;
if bytes_per_sec < 100 * 1024 {
"Slow"
} else if bytes_per_sec < MB {
"Moderate"
} else if bytes_per_sec < 10 * MB {
"Fast"
} else {
"Very fast"
}
}
pub fn gauge_bar(percent: f64, width: usize) -> String {
let filled = ((percent / 100.0) * width as f64).round() as usize;
let empty = width.saturating_sub(filled);
format!(
"{}{} {:.0}%",
"\u{2588}".repeat(filled),
gauge_empty_char().repeat(empty),
percent
)
}
pub fn gauge_line<'a>(label: &str, percent: f64, width: usize) -> Line<'a> {
let status = HealthStatus::from_percent(percent);
let color = status_color(&status);
let filled = ((percent / 100.0) * width as f64).round() as usize;
let empty = width.saturating_sub(filled);
Line::from(vec![
Span::styled(format!(" {:<14}", label), Style::default().fg(COLOR_TEXT)),
Span::styled(
format!(
"{}{}",
"\u{2588}".repeat(filled),
gauge_empty_char().repeat(empty)
),
Style::default().fg(color),
),
Span::styled(
format!(" {:.0}%", percent),
Style::default().fg(color).add_modifier(Modifier::BOLD),
),
])
}
pub fn status_line<'a>(status: &HealthStatus, label: &str, description: &str) -> Line<'a> {
let color = status_color(status);
Line::from(vec![
Span::styled(format!(" {} ", status.icon()), Style::default().fg(color)),
Span::styled(format!("{:<16}", label), Style::default().fg(COLOR_TEXT)),
Span::styled(description.to_string(), Style::default().fg(COLOR_DIM)),
])
}
pub fn section_header<'a>(title: &str) -> Line<'a> {
Line::from(Span::styled(
format!(" {}", title),
Style::default()
.fg(COLOR_HEADER)
.add_modifier(Modifier::BOLD),
))
}
pub fn separator(width: usize) -> Line<'static> {
Line::from(Span::styled(
format!(" {}", "\u{2500}".repeat(width.saturating_sub(4))),
Style::default().fg(COLOR_DIM),
))
}
pub fn truncate_str(s: &str, max: usize) -> String {
if max < 3 {
return s.chars().take(max).collect();
}
if s.chars().count() <= max {
s.to_string()
} else {
let truncated: String = s.chars().take(max - 2).collect();
format!("{}..", truncated)
}
}
pub fn health_gauge_line<'a>(
label: &str,
status: &HealthStatus,
description: &str,
percent: f64,
bar_width: usize,
) -> Line<'a> {
let color = status_color(status);
let filled = ((percent / 100.0) * bar_width as f64).round() as usize;
let empty = bar_width.saturating_sub(filled);
let desc_truncated = truncate_str(description, 26);
Line::from(vec![
Span::styled(format!(" {} ", status.icon()), Style::default().fg(color)),
Span::styled(format!("{:<16}", label), Style::default().fg(COLOR_TEXT)),
Span::styled(
format!("{:<28}", desc_truncated),
Style::default().fg(COLOR_DIM),
),
Span::styled(
format!(
"{}{} {:.0}%",
"\u{2588}".repeat(filled),
gauge_empty_char().repeat(empty),
percent
),
Style::default().fg(color),
),
])
}
pub fn health_gauge_line_simple<'a>(label: &str, percent: f64, bar_width: usize) -> Line<'a> {
let status = HealthStatus::from_percent(percent);
let color = status_color(&status);
let filled = ((percent / 100.0) * bar_width as f64).round() as usize;
let empty = bar_width.saturating_sub(filled);
Line::from(vec![
Span::styled(format!(" {:<16}", label), Style::default().fg(COLOR_TEXT)),
Span::styled(
format!(
"{}{} {:.0}% \u{2014} {}",
"\u{2588}".repeat(filled),
gauge_empty_char().repeat(empty),
percent,
plain_language_cpu(percent as f32)
),
Style::default().fg(color),
),
])
}