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::{
PERCENT_DOMAIN, PERCENT_SOFT_GRID, PERCENT_SOFT_MIN_SPAN, TEMP_SOFT_GRID, TEMP_SOFT_MIN_SPAN,
power_range, power_soft_grid, power_soft_min_span, soft_range, temp_range,
};
use crate::ui::text::print_colored_text;
const SPARKLINE_WIDTH: usize = 8;
const TREND_PERCENT: (f64, f64) = (1.0, 5.0);
const TREND_TEMP: (f64, f64) = (0.5, 2.0);
const TREND_POWER: (f64, f64) = (0.2, 1.0);
const TREND_LOOKBACK: usize = 5;
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) {
let cpu_history: Vec<f64> = state.cpu_utilization_history.iter().copied().collect();
draw_metric_sparkline(
stdout,
"CPU",
&cpu_history,
format_pct(state.cpu_utilization_history.back().copied()),
ThemeConfig::cpu_color(),
Some(soft_range(
&cpu_history,
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
)),
TREND_PERCENT,
);
print_colored_text(stdout, " ", Color::White, None, None);
let gpu_history: Vec<f64> = state.utilization_history.iter().copied().collect();
draw_metric_sparkline(
stdout,
"GPU",
&gpu_history,
format_pct(state.utilization_history.back().copied()),
ThemeConfig::gpu_color(),
Some(soft_range(
&gpu_history,
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
)),
TREND_PERCENT,
);
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);
let temp_history: Vec<f64> = state.cpu_temperature_history.iter().copied().collect();
let temp_ceiling = temp_range(None).1;
draw_metric_sparkline(
stdout,
"Tmp",
&temp_history,
format_temp(state.cpu_temperature_history.back().copied()),
ThemeConfig::thermal_color(),
Some(soft_range(
&temp_history,
TEMP_SOFT_MIN_SPAN,
TEMP_SOFT_GRID,
(0.0, temp_ceiling),
)),
TREND_TEMP,
);
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)>,
trend: (f64, f64),
) {
let sparkline = sparkline_braille(history, SPARKLINE_WIDTH, range);
let glyph = trend_glyph(history, trend.0, trend.1);
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, glyph, color, None, None);
print_colored_text(stdout, " ", Color::DarkGrey, None, None);
print_colored_text(stdout, &sparkline, color, None, None);
}
#[must_use]
fn trend_glyph(history: &[f64], flat: f64, steep: f64) -> &'static str {
if history.len() < 2 {
return " ";
}
let latest = history[history.len() - 1];
let reference = history[history.len().saturating_sub(TREND_LOOKBACK + 1)];
if !latest.is_finite() || !reference.is_finite() {
return "\u{2192}"; }
let delta = latest - reference;
if delta.abs() < flat {
"\u{2192}" } else if delta > 0.0 {
if delta >= steep {
"\u{2191}" } else {
"\u{2197}" }
} else if delta <= -steep {
"\u{2193}" } else {
"\u{2198}" }
}
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 range = soft_range(
&history,
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
let sparkline = sparkline_braille(&history, SPARKLINE_WIDTH, Some(range));
let glyph = trend_glyph(&history, TREND_PERCENT.0, TREND_PERCENT.1);
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, glyph, ThemeConfig::memory_color(), 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(|| crate::metrics::gpu_readings::total_power_watts(&state.gpu_info))
} else {
crate::metrics::gpu_readings::total_power_watts(&state.gpu_info)
};
let value_str = format!("{power_watts:>5.1}W");
let history: Vec<f64> = state.package_power_history.iter().copied().collect();
let ceiling = power_range(&state.gpu_info, &history).1;
let range = soft_range(
&history,
power_soft_min_span(ceiling),
power_soft_grid(ceiling),
(0.0, ceiling),
);
let sparkline = sparkline_braille(&history, SPARKLINE_WIDTH, Some(range));
let glyph = trend_glyph(&history, TREND_POWER.0, TREND_POWER.1);
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, glyph, ThemeConfig::power_color(), 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_trend_glyph_insufficient_history() {
assert_eq!(trend_glyph(&[], 1.0, 5.0), " ");
assert_eq!(trend_glyph(&[42.0], 1.0, 5.0), " ");
}
#[test]
fn test_trend_glyph_flat() {
assert_eq!(trend_glyph(&[50.0, 50.5], 1.0, 5.0), "\u{2192}"); assert_eq!(trend_glyph(&[50.0, 49.5], 1.0, 5.0), "\u{2192}"); }
#[test]
fn test_trend_glyph_gentle_slopes() {
assert_eq!(trend_glyph(&[50.0, 53.0], 1.0, 5.0), "\u{2197}"); assert_eq!(trend_glyph(&[53.0, 50.0], 1.0, 5.0), "\u{2198}"); }
#[test]
fn test_trend_glyph_steep_slopes() {
assert_eq!(trend_glyph(&[50.0, 60.0], 1.0, 5.0), "\u{2191}"); assert_eq!(trend_glyph(&[60.0, 50.0], 1.0, 5.0), "\u{2193}"); }
#[test]
fn test_trend_glyph_uses_sample_lookback_not_oldest() {
let h = [0.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.2];
assert_eq!(trend_glyph(&h, 1.0, 5.0), "\u{2192}"); }
#[test]
fn test_trend_glyph_short_history_uses_oldest() {
let h = [10.0, 20.0, 30.0];
assert_eq!(trend_glyph(&h, 1.0, 5.0), "\u{2191}"); }
#[test]
fn test_trend_glyph_non_finite_reads_level() {
assert_eq!(trend_glyph(&[f64::NAN, 50.0], 1.0, 5.0), "\u{2192}");
assert_eq!(trend_glyph(&[50.0, f64::NAN], 1.0, 5.0), "\u{2192}");
}
#[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());
}
#[test]
fn test_draw_local_header_bar_renders_trend_glyphs() {
use crate::app_state::AppState;
let mut state = AppState::new();
for v in [10.0, 10.0, 10.0, 10.0, 10.0, 60.0] {
state.cpu_utilization_history.push_back(v); }
for v in [80.0, 80.0, 80.0, 80.0, 80.0, 20.0] {
state.utilization_history.push_back(v); }
for v in [50.0, 50.0, 50.0, 50.0, 50.0, 50.5] {
state.system_memory_history.push_back(v); }
for v in [10.0, 10.0, 10.0, 10.0, 10.0, 10.5] {
state.package_power_history.push_back(v); }
for v in [50.0, 50.0, 50.0, 50.0, 50.0, 49.0] {
state.cpu_temperature_history.push_back(v); }
let mut buf: Vec<u8> = Vec::new();
draw_local_header_bar(&mut buf, &state, 120);
let out = String::from_utf8_lossy(&buf);
let cpu = out.find("CPU").expect("CPU label rendered");
let gpu = out.find("GPU").expect("GPU label rendered");
let ram = out.find("RAM").expect("RAM label rendered");
let pwr = out.find("Pwr").expect("Pwr label rendered");
let tmp = out.find("Tmp").expect("Tmp label rendered");
assert!(
cpu < gpu && gpu < ram && ram < pwr && pwr < tmp,
"metric labels rendered out of order: {out:?}"
);
assert!(
out[cpu..gpu].contains('\u{2191}'), "CPU segment should contain the steep-rise glyph: {:?}",
&out[cpu..gpu]
);
assert!(
out[gpu..ram].contains('\u{2193}'), "GPU segment should contain the steep-fall glyph: {:?}",
&out[gpu..ram]
);
assert!(
out[ram..pwr].contains('\u{2192}'), "RAM segment should contain the level glyph: {:?}",
&out[ram..pwr]
);
assert!(
out[pwr..tmp].contains('\u{2197}'), "Pwr segment should contain the gentle-rise glyph: {:?}",
&out[pwr..tmp]
);
assert!(
out[tmp..].contains('\u{2198}'), "Tmp segment should contain the gentle-fall glyph: {:?}",
&out[tmp..]
);
}
}