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::buffer::BufferWriter;
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::{ansi_display_width, print_colored_text, truncate_to_width};
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;
#[derive(Debug, Clone, Copy)]
struct MetricPresentation {
color: Color,
range: Option<(f64, f64)>,
trend: (f64, f64),
sparkline_width: Option<usize>,
}
pub fn draw_local_header_bar<W: Write>(stdout: &mut W, state: &AppState, cols: u16) {
draw_identity_line(stdout, state, cols);
draw_metrics_line(stdout, state, cols);
}
#[must_use]
pub fn local_header_line_count(cols: u16) -> u16 {
if cols < 40 { 3 } else { 2 }
}
fn draw_identity_line<W: Write>(stdout: &mut W, state: &AppState, cols: u16) {
let hostname = state
.cpu_info
.first()
.map(|c| c.hostname.as_str())
.unwrap_or("localhost");
let uptime_secs = sysinfo::System::uptime();
let uptime_str = format_uptime(uptime_secs);
let width = cols as usize;
let live_width = 6; let host_prefix_width = 5; let uptime_suffix = format!(" · up {uptime_str}");
let show_uptime = width >= host_prefix_width + 4 + uptime_suffix.len() + 2 + live_width;
let suffix_width = if show_uptime { uptime_suffix.len() } else { 0 };
let hostname_budget = width
.saturating_sub(host_prefix_width + suffix_width + 2 + live_width)
.max(1);
let hostname_display = truncate_to_width(hostname, hostname_budget);
print_colored_text(stdout, "Host ", Color::DarkGrey, None, None);
print_colored_text(stdout, &hostname_display, Color::White, None, None);
if show_uptime {
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
};
let used = host_prefix_width + hostname_display.chars().count() + suffix_width;
let gap = width.saturating_sub(used + live_width).max(1);
print_colored_text(stdout, &" ".repeat(gap), 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, cols: u16) {
let width = cols as usize;
let detailed = render_metrics_line(state, Some(SPARKLINE_WIDTH));
if ansi_display_width(&detailed) <= width {
stdout.write_all(detailed.as_bytes()).unwrap();
queue!(stdout, Print("\r\n")).unwrap();
return;
}
let values_only = render_metrics_line(state, None);
if ansi_display_width(&values_only) <= width {
stdout.write_all(values_only.as_bytes()).unwrap();
queue!(stdout, Print("\r\n")).unwrap();
return;
}
draw_tiny_metrics(stdout, state, width);
}
fn render_metrics_line(state: &AppState, sparkline_width: Option<usize>) -> String {
let mut stdout = BufferWriter::new();
let cpu_history: Vec<f64> = state.cpu_utilization_history.iter().copied().collect();
draw_metric_sparkline(
&mut stdout,
"CPU",
&cpu_history,
format_pct(state.cpu_utilization_history.back().copied()),
MetricPresentation {
color: ThemeConfig::cpu_color(),
range: Some(soft_range(
&cpu_history,
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
)),
trend: TREND_PERCENT,
sparkline_width,
},
);
print_colored_text(&mut stdout, " ", Color::White, None, None);
let gpu_history: Vec<f64> = state.utilization_history.iter().copied().collect();
draw_metric_sparkline(
&mut stdout,
"GPU",
&gpu_history,
format_pct(state.utilization_history.back().copied()),
MetricPresentation {
color: ThemeConfig::gpu_color(),
range: Some(soft_range(
&gpu_history,
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
)),
trend: TREND_PERCENT,
sparkline_width,
},
);
print_colored_text(&mut stdout, " ", Color::White, None, None);
draw_ram_sparkline(&mut stdout, state, sparkline_width);
print_colored_text(&mut stdout, " ", Color::White, None, None);
draw_power_sparkline(&mut stdout, state, sparkline_width);
print_colored_text(&mut 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(
&mut stdout,
"Tmp",
&temp_history,
format_temp(state.cpu_temperature_history.back().copied()),
MetricPresentation {
color: ThemeConfig::thermal_color(),
range: Some(soft_range(
&temp_history,
TEMP_SOFT_MIN_SPAN,
TEMP_SOFT_GRID,
(0.0, temp_ceiling),
)),
trend: TREND_TEMP,
sparkline_width,
},
);
stdout.get_buffer().to_string()
}
fn draw_tiny_metrics<W: Write>(stdout: &mut W, state: &AppState, width: usize) {
let cpu = compact_pct(state.cpu_utilization_history.back().copied());
let gpu = compact_pct(state.utilization_history.back().copied());
let memory = compact_memory_pct(state);
let power = format!("{:.0}W", current_power_watts(state));
let temp = state
.cpu_temperature_history
.back()
.map_or_else(|| "N/A".to_string(), |value| format!("{value:.0}°"));
draw_tiny_metric(stdout, "C", &cpu, ThemeConfig::cpu_color());
print_colored_text(stdout, " ", Color::White, None, None);
draw_tiny_metric(stdout, "G", &gpu, ThemeConfig::gpu_color());
if width < 40 {
print_colored_text(stdout, " ", Color::White, None, None);
draw_tiny_metric(stdout, "M", &memory, ThemeConfig::memory_color());
queue!(stdout, Print("\r\n")).unwrap();
draw_tiny_metric(stdout, "P", &power, ThemeConfig::power_color());
print_colored_text(stdout, " ", Color::White, None, None);
draw_tiny_metric(stdout, "T", &temp, ThemeConfig::thermal_color());
} else {
print_colored_text(stdout, " ", Color::White, None, None);
draw_tiny_metric(stdout, "M", &memory, ThemeConfig::memory_color());
print_colored_text(stdout, " ", Color::White, None, None);
draw_tiny_metric(stdout, "P", &power, ThemeConfig::power_color());
print_colored_text(stdout, " ", Color::White, None, None);
draw_tiny_metric(stdout, "T", &temp, ThemeConfig::thermal_color());
}
queue!(stdout, Print("\r\n")).unwrap();
}
fn draw_tiny_metric<W: Write>(stdout: &mut W, label: &str, value: &str, color: Color) {
print_colored_text(stdout, label, color, None, None);
print_colored_text(stdout, value, Color::White, None, None);
}
fn compact_pct(value: Option<f64>) -> String {
value.map_or_else(|| "N/A".to_string(), |value| format!("{value:.0}%"))
}
fn compact_memory_pct(state: &AppState) -> String {
let total = state.memory_info.iter().map(|m| m.total_bytes).sum::<u64>();
let used = state.memory_info.iter().map(|m| m.used_bytes).sum::<u64>();
if total == 0 {
"N/A".to_string()
} else {
format!("{:.0}%", used as f64 / total as f64 * 100.0)
}
}
fn draw_metric_sparkline<W: Write>(
stdout: &mut W,
label: &str,
history: &[f64],
value_str: String,
presentation: MetricPresentation,
) {
let MetricPresentation {
color,
range,
trend,
sparkline_width,
} = presentation;
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);
if let Some(width) = sparkline_width {
let sparkline = sparkline_braille(history, width, range);
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, sparkline_width: Option<usize>) {
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 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);
if let Some(width) = sparkline_width {
let sparkline = sparkline_braille(&history, width, Some(range));
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,
sparkline_width: Option<usize>,
) {
let power_watts = current_power_watts(state);
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 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);
if let Some(width) = sparkline_width {
let sparkline = sparkline_braille(&history, width, Some(range));
print_colored_text(stdout, " ", Color::DarkGrey, None, None);
print_colored_text(stdout, &sparkline, ThemeConfig::power_color(), None, None);
}
}
fn current_power_watts(state: &AppState) -> f64 {
let is_apple_silicon = state.gpu_info.iter().any(|gpu| {
gpu.detail
.get("architecture")
.map(|arch| arch == "Apple Silicon")
.unwrap_or(false)
});
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)
}
}
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 local_header_uses_only_intentional_rows_and_never_overflows() {
use crate::app_state::AppState;
use crate::ui::text::ansi_display_width;
let mut state = AppState::new();
for value in [10.0, 20.0, 30.0] {
state.cpu_utilization_history.push_back(value);
state.utilization_history.push_back(value);
state.system_memory_history.push_back(value);
state.package_power_history.push_back(value);
state.cpu_temperature_history.push_back(40.0 + value);
}
for cols in [20, 39, 40, 64, 80, 110, 160] {
let mut buffer = Vec::new();
draw_local_header_bar(&mut buffer, &state, cols);
let rendered = String::from_utf8(buffer).unwrap();
let lines: Vec<_> = rendered
.split("\r\n")
.filter(|line| !line.is_empty())
.collect();
assert_eq!(
lines.len(),
local_header_line_count(cols) as usize,
"header row accounting drifted at {cols} columns"
);
for line in lines {
assert!(
ansi_display_width(line) <= cols as usize,
"header overflowed {cols} columns: {line:?}"
);
}
}
}
#[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..]
);
}
}