use std::io::Write;
use crossterm::{queue, style::Color, style::Print};
use crate::app_state::AppState;
use crate::common::config::ThemeConfig;
use crate::ui::braille::sparkline_braille;
use crate::ui::scale::{power_range, temp_range};
use crate::ui::text::print_colored_text;
const SPARKLINE_WIDTH: usize = 8;
pub fn draw_local_header_bar<W: Write>(stdout: &mut W, state: &AppState, _cols: u16) {
draw_identity_line(stdout, state);
draw_metrics_line(stdout, state);
}
fn draw_identity_line<W: Write>(stdout: &mut W, state: &AppState) {
let hostname = state
.cpu_info
.first()
.map(|c| c.hostname.as_str())
.unwrap_or("localhost");
let cpu_model = state
.cpu_info
.first()
.map(|c| c.cpu_model.as_str())
.unwrap_or("unknown");
let arch = state
.cpu_info
.first()
.map(|c| c.architecture.as_str())
.unwrap_or("unknown");
let uptime_secs = sysinfo::System::uptime();
let uptime_str = format_uptime(uptime_secs);
print_colored_text(stdout, "Host ", Color::DarkGrey, None, None);
print_colored_text(stdout, hostname, Color::White, None, None);
print_colored_text(stdout, " · ", Color::DarkGrey, None, None);
print_colored_text(stdout, cpu_model, Color::White, None, None);
print_colored_text(stdout, " · arch ", Color::DarkGrey, None, None);
print_colored_text(stdout, arch, ThemeConfig::accent_color(), None, None);
print_colored_text(stdout, " · up ", Color::DarkGrey, None, None);
print_colored_text(stdout, &uptime_str, ThemeConfig::memory_color(), None, None);
let live_color = if state.frame_counter.is_multiple_of(2) {
Color::Green
} else {
Color::DarkGreen
};
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, "●", live_color, None, None);
print_colored_text(stdout, " Live", Color::DarkGrey, None, None);
queue!(stdout, Print("\r\n")).unwrap();
}
fn draw_metrics_line<W: Write>(stdout: &mut W, state: &AppState) {
draw_metric_sparkline(
stdout,
"CPU",
&state
.cpu_utilization_history
.iter()
.copied()
.collect::<Vec<_>>(),
format_pct(state.cpu_utilization_history.back().copied()),
ThemeConfig::cpu_color(),
Some((0.0, 100.0)),
);
print_colored_text(stdout, " ", Color::White, None, None);
draw_metric_sparkline(
stdout,
"GPU",
&state
.utilization_history
.iter()
.copied()
.collect::<Vec<_>>(),
format_pct(state.utilization_history.back().copied()),
ThemeConfig::gpu_color(),
Some((0.0, 100.0)),
);
print_colored_text(stdout, " ", Color::White, None, None);
draw_ram_sparkline(stdout, state);
print_colored_text(stdout, " ", Color::White, None, None);
draw_power_sparkline(stdout, state);
print_colored_text(stdout, " ", Color::White, None, None);
draw_metric_sparkline(
stdout,
"Tmp",
&state
.cpu_temperature_history
.iter()
.copied()
.collect::<Vec<_>>(),
format_temp(state.cpu_temperature_history.back().copied()),
ThemeConfig::thermal_color(),
Some(temp_range(None)),
);
queue!(stdout, Print("\r\n")).unwrap();
}
fn draw_metric_sparkline<W: Write>(
stdout: &mut W,
label: &str,
history: &[f64],
value_str: String,
color: Color,
range: Option<(f64, f64)>,
) {
let sparkline = sparkline_braille(history, SPARKLINE_WIDTH, range);
print_colored_text(stdout, label, color, None, None);
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, &value_str, Color::White, None, None);
print_colored_text(stdout, " ", Color::DarkGrey, None, None);
print_colored_text(stdout, &sparkline, color, None, None);
}
fn draw_ram_sparkline<W: Write>(stdout: &mut W, state: &AppState) {
let total_gb = state.memory_info.iter().map(|m| m.total_bytes).sum::<u64>() as f64
/ (1024.0 * 1024.0 * 1024.0);
let used_gb = state.memory_info.iter().map(|m| m.used_bytes).sum::<u64>() as f64
/ (1024.0 * 1024.0 * 1024.0);
let total_str = format!("{total_gb:.0}");
let value_str = format!("{used_gb:>width$.0}/{total_str}GB", width = total_str.len());
let history: Vec<f64> = state.system_memory_history.iter().copied().collect();
let sparkline = sparkline_braille(&history, SPARKLINE_WIDTH, Some((0.0, 100.0)));
print_colored_text(stdout, "RAM", ThemeConfig::memory_color(), None, None);
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, &value_str, Color::White, None, None);
print_colored_text(stdout, " ", Color::DarkGrey, None, None);
print_colored_text(stdout, &sparkline, ThemeConfig::memory_color(), None, None);
}
fn draw_power_sparkline<W: Write>(stdout: &mut W, state: &AppState) {
let is_apple_silicon = state.gpu_info.iter().any(|gpu| {
gpu.detail
.get("architecture")
.map(|arch| arch == "Apple Silicon")
.unwrap_or(false)
});
let power_watts = if is_apple_silicon {
state
.gpu_info
.iter()
.filter_map(|gpu| {
gpu.detail
.get("combined_power_mw")
.and_then(|s| s.parse::<f64>().ok())
.map(|mw| mw / 1000.0)
})
.next()
.unwrap_or_else(|| state.gpu_info.iter().map(|g| g.power_consumption).sum())
} else {
state.gpu_info.iter().map(|g| g.power_consumption).sum()
};
let value_str = format!("{power_watts:>5.1}W");
let history: Vec<f64> = state.package_power_history.iter().copied().collect();
let range = Some(power_range(&state.gpu_info, &history));
let sparkline = sparkline_braille(&history, SPARKLINE_WIDTH, range);
print_colored_text(stdout, "Pwr", ThemeConfig::power_color(), None, None);
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, &value_str, Color::White, None, None);
print_colored_text(stdout, " ", Color::DarkGrey, None, None);
print_colored_text(stdout, &sparkline, ThemeConfig::power_color(), None, None);
}
fn format_pct(value: Option<f64>) -> String {
match value {
Some(v) => format!("{v:>5.1}%"),
None => format!("{:>6}", "N/A"),
}
}
fn format_temp(value: Option<f64>) -> String {
match value {
Some(v) => format!("{v:>3.0}°C"),
None => format!("{:>5}", "N/A"),
}
}
fn format_uptime(secs: u64) -> String {
let days = secs / 86400;
let hours = (secs % 86400) / 3600;
let mins = (secs % 3600) / 60;
let remaining_secs = secs % 60;
if days > 0 {
format!("{days}d {hours}h {mins}m")
} else if hours > 0 {
format!("{hours}h {mins}m")
} else {
format!("{mins}m {remaining_secs}s")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_uptime_seconds_only() {
assert_eq!(format_uptime(45), "0m 45s");
assert_eq!(format_uptime(0), "0m 0s");
}
#[test]
fn test_format_uptime_minutes() {
assert_eq!(format_uptime(90), "1m 30s");
assert_eq!(format_uptime(3599), "59m 59s");
}
#[test]
fn test_format_uptime_hours() {
assert_eq!(format_uptime(3600), "1h 0m");
assert_eq!(format_uptime(7384), "2h 3m");
assert_eq!(format_uptime(86399), "23h 59m");
}
#[test]
fn test_format_uptime_days() {
assert_eq!(format_uptime(86400), "1d 0h 0m");
assert_eq!(format_uptime(172861), "2d 0h 1m");
assert_eq!(format_uptime(263845), "3d 1h 17m");
}
#[test]
fn test_format_pct_some() {
assert_eq!(format_pct(Some(0.0)), " 0.0%");
assert_eq!(format_pct(Some(75.5)), " 75.5%");
assert_eq!(format_pct(Some(100.0)), "100.0%");
}
#[test]
fn test_format_pct_none() {
assert_eq!(format_pct(None), " N/A");
}
#[test]
fn test_format_temp_some() {
assert_eq!(format_temp(Some(72.0)), " 72°C");
assert_eq!(format_temp(Some(72.9)), " 73°C"); }
#[test]
fn test_format_temp_none() {
assert_eq!(format_temp(None), " N/A");
}
#[test]
fn test_format_pct_fixed_width() {
let values = [0.0, 9.9, 10.0, 50.0, 99.9, 100.0];
let widths: Vec<usize> = values.iter().map(|&v| format_pct(Some(v)).len()).collect();
assert!(
widths.windows(2).all(|w| w[0] == w[1]),
"all pct widths should be equal: {widths:?}"
);
}
#[test]
fn test_format_temp_fixed_display_width() {
assert_eq!(format_temp(Some(9.0)), " 9°C");
assert_eq!(format_temp(Some(10.0)), " 10°C");
assert_eq!(format_temp(Some(99.0)), " 99°C");
assert_eq!(format_temp(Some(100.0)), "100°C");
}
#[test]
fn test_format_power_fixed_width() {
let values = [0.0_f64, 9.9, 10.0, 99.9, 100.0, 999.9];
for &w in &values {
let s = format!("{w:>5.1}W");
assert_eq!(
s.len(),
6,
"power format for {w} should be 6 chars, got {s:?}"
);
}
assert_eq!(format!("{:>5.1}W", 0.0_f64), " 0.0W");
assert_eq!(format!("{:>5.1}W", 10.5_f64), " 10.5W");
assert_eq!(format!("{:>5.1}W", 999.9_f64), "999.9W");
}
#[test]
fn test_format_ram_fixed_separator_position() {
let cases: &[(f64, f64, &str)] = &[
(0.0, 16.0, " 0/16GB"),
(8.0, 16.0, " 8/16GB"),
(16.0, 16.0, "16/16GB"),
(0.0, 128.0, " 0/128GB"),
(64.0, 128.0, " 64/128GB"),
(128.0, 128.0, "128/128GB"),
];
for &(used, total, expected) in cases {
let total_str = format!("{total:.0}");
let value_str = format!("{used:>width$.0}/{total_str}GB", width = total_str.len());
assert_eq!(
value_str, expected,
"RAM format for {used}/{total} GB should be {expected:?}, got {value_str:?}"
);
}
let totals = [16.0_f64, 128.0];
for total in totals {
let total_str = format!("{total:.0}");
let w = total_str.len();
let zero = 0.0_f64;
let len_0 = format!("{zero:>w$.0}/{total_str}GB").len();
let len_total = format!("{total:>w$.0}/{total_str}GB").len();
assert_eq!(
len_0, len_total,
"RAM format width should be stable for total={total}"
);
}
}
#[test]
fn test_draw_local_header_bar_does_not_panic_empty_state() {
use crate::app_state::AppState;
let state = AppState::new();
let mut buf: Vec<u8> = Vec::new();
draw_local_header_bar(&mut buf, &state, 80);
}
#[test]
fn test_draw_local_header_bar_with_history() {
use crate::app_state::AppState;
let mut state = AppState::new();
for i in 0..10 {
state.cpu_utilization_history.push_back(i as f64 * 10.0);
state.utilization_history.push_back(i as f64 * 8.0);
state.system_memory_history.push_back(i as f64 * 5.0);
state
.cpu_temperature_history
.push_back(40.0 + i as f64 * 3.0);
}
let mut buf: Vec<u8> = Vec::new();
draw_local_header_bar(&mut buf, &state, 120);
assert!(!buf.is_empty());
}
}