use std::io::Write;
use crossterm::{queue, style::Color, style::Print};
use crate::app_state::AppState;
use crate::common::config::ThemeConfig;
use crate::device::CpuInfo;
use crate::ui::activity_panel::{self, GRAPH_ROWS, use_multirow_graphs};
use crate::ui::braille::sparkline_braille;
use crate::ui::buffer::BufferWriter;
use crate::ui::scale::{
ANE_SOFT_GRID, ANE_SOFT_MIN_SPAN, PERCENT_DOMAIN, PERCENT_SOFT_GRID, PERCENT_SOFT_MIN_SPAN,
TEMP_SOFT_GRID, TEMP_SOFT_MIN_SPAN, ane_range, power_range, power_soft_grid,
power_soft_min_span, scale_badge, soft_range, temp_range,
};
use crate::ui::text::print_colored_text;
const LABEL_WIDTH: usize = 9;
const VALUE_WIDTH: usize = 7;
const MINMAX_WIDTH: usize = 9;
const SPACING: usize = 5;
pub fn gpu_content_rows(state: &AppState, terminal_rows: u16) -> usize {
if state.gpu_info.is_empty() {
return 0;
}
let ane = show_ane_row(state);
let npu = show_npu_row(state);
let metric_rows = 3 + usize::from(ane) + usize::from(npu) + 1;
if use_multirow_graphs(terminal_rows) {
let secondary = metric_rows - 1;
GRAPH_ROWS + secondary.div_ceil(2)
} else {
metric_rows
}
}
pub fn render_combined_activity_panel<W: Write>(
stdout: &mut W,
state: &AppState,
cpu_info: &[CpuInfo],
width: usize,
terminal_rows: u16,
) {
if cpu_info.is_empty() || cpu_info[0].per_core_utilization.is_empty() {
return;
}
let left_width = width / 2;
let right_width = width - left_width;
let cpu_history: Vec<f64> = state.cpu_utilization_history.iter().copied().collect();
let left_lines = render_cpu_lines(cpu_info, &cpu_history, width, terminal_rows);
let right_lines = render_gpu_lines(state, right_width, terminal_rows);
let total_lines = left_lines.len().max(right_lines.len());
for i in 0..total_lines {
if i < left_lines.len() {
stdout.write_all(left_lines[i].as_bytes()).unwrap();
} else {
print_colored_text(stdout, &" ".repeat(left_width), Color::White, None, None);
}
if i < right_lines.len() {
stdout.write_all(right_lines[i].as_bytes()).unwrap();
}
queue!(stdout, Print("\r\n")).unwrap();
}
}
fn render_cpu_lines(
cpu_info: &[CpuInfo],
cpu_history: &[f64],
width: usize,
terminal_rows: u16,
) -> Vec<String> {
let mut buf = BufferWriter::new();
activity_panel::render_activity_panel(&mut buf, cpu_info, cpu_history, width, terminal_rows);
let raw = buf.get_buffer().to_string();
raw.split("\r\n")
.filter(|line| !line.is_empty())
.map(String::from)
.collect()
}
fn render_gpu_lines(state: &AppState, panel_width: usize, terminal_rows: u16) -> Vec<String> {
if state.gpu_info.is_empty() {
return Vec::new();
}
let is_apple = detect_apple_silicon(state);
let ane = show_ane_row(state);
let npu = show_npu_row(state);
let rows = build_rows(state, is_apple, ane, npu);
let mut lines: Vec<String> = Vec::with_capacity(rows.len() + 2);
lines.push(render_line_to_string(|w| {
draw_top_border(w, panel_width);
}));
if use_multirow_graphs(terminal_rows) {
if let Some(util) = rows.first() {
lines.extend(draw_gpu_util_graph(util, panel_width));
}
let secondary = if rows.is_empty() { &[][..] } else { &rows[1..] };
for pair in secondary.chunks(2) {
lines.push(render_line_to_string(|w| {
draw_compact_pair(w, pair, panel_width);
}));
}
} else {
for row in &rows {
lines.push(render_line_to_string(|w| {
draw_sparkline_row(w, row, panel_width);
}));
}
}
lines.push(render_line_to_string(|w| {
draw_bottom_border(w, panel_width);
}));
lines
}
fn render_line_to_string<F>(f: F) -> String
where
F: FnOnce(&mut BufferWriter),
{
let mut buf = BufferWriter::new();
f(&mut buf);
buf.get_buffer().to_string()
}
struct SparklineRow {
label: &'static str,
short_label: &'static str,
color: Color,
history: Vec<f64>,
latest_str: String,
min_max_str: String,
range: Option<(f64, f64)>,
badge: Option<(String, Color)>,
}
fn build_rows(state: &AppState, is_apple: bool, has_ane: bool, has_npu: bool) -> Vec<SparklineRow> {
let mut rows = Vec::with_capacity(6);
let gpu = state.gpu_info.first();
let gpu_util: Vec<f64> = state.utilization_history.iter().copied().collect();
let latest_util = gpu_util.last().copied().unwrap_or(0.0);
let util_range = soft_range(
&gpu_util,
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
rows.push(SparklineRow {
label: "GPU Util",
short_label: "GPU",
color: ThemeConfig::gpu_color(),
latest_str: format!("{latest_util:.1}%"),
min_max_str: scale_badge(util_range.0, util_range.1),
history: gpu_util,
range: Some(util_range),
badge: None,
});
let gpu_mem: Vec<f64> = state.memory_history.iter().copied().collect();
let latest_mem = gpu_mem.last().copied().unwrap_or(0.0);
let mem_range = soft_range(
&gpu_mem,
PERCENT_SOFT_MIN_SPAN,
PERCENT_SOFT_GRID,
PERCENT_DOMAIN,
);
rows.push(SparklineRow {
label: "GPU Mem",
short_label: "Mem",
color: ThemeConfig::memory_color(),
latest_str: format!("{latest_mem:.1}%"),
min_max_str: scale_badge(mem_range.0, mem_range.1),
history: gpu_mem,
range: Some(mem_range),
badge: None,
});
let gpu_temp: Vec<f64> = state.temperature_history.iter().copied().collect();
let latest_temp = gpu_temp.last().copied().unwrap_or(0.0);
let temp_ceiling = temp_range(gpu).1;
let temp_rng = soft_range(
&gpu_temp,
TEMP_SOFT_MIN_SPAN,
TEMP_SOFT_GRID,
(0.0, temp_ceiling),
);
rows.push(SparklineRow {
label: "GPU Temp",
short_label: "Temp",
color: ThemeConfig::thermal_color(),
latest_str: format!("{latest_temp:.0}\u{00B0}C"),
min_max_str: scale_badge(temp_rng.0, temp_rng.1),
history: gpu_temp,
range: Some(temp_rng),
badge: None,
});
if has_ane {
let ane_w = state.ane_power_history.back().copied().unwrap_or_else(|| {
crate::metrics::gpu_readings::first_ane_power_watts(&state.gpu_info).unwrap_or(0.0)
});
let ane_history: Vec<f64> = if state.ane_power_history.is_empty() {
vec![ane_w]
} else {
state.ane_power_history.iter().copied().collect()
};
let ane_ceiling = ane_range(&ane_history).1;
let ane_rng = soft_range(
&ane_history,
ANE_SOFT_MIN_SPAN,
ANE_SOFT_GRID,
(0.0, ane_ceiling),
);
rows.push(SparklineRow {
label: "ANE",
short_label: "ANE",
color: ThemeConfig::accelerator_color(),
latest_str: format!("{ane_w:.1}W"),
min_max_str: scale_badge(ane_rng.0, ane_rng.1),
history: ane_history,
range: Some(ane_rng),
badge: None,
});
}
if has_npu {
rows.push(SparklineRow {
label: "NPU",
short_label: "NPU",
color: ThemeConfig::accelerator_color(),
latest_str: "0.0W".to_string(),
min_max_str: String::new(),
history: vec![0.0],
range: None,
badge: None,
});
}
let power_w = package_power(state, is_apple);
let power_history: Vec<f64> = if state.package_power_history.is_empty() {
vec![power_w]
} else {
state.package_power_history.iter().copied().collect()
};
let power_ceiling = power_range(&state.gpu_info, &power_history).1;
let power_rng = soft_range(
&power_history,
power_soft_min_span(power_ceiling),
power_soft_grid(power_ceiling),
(0.0, power_ceiling),
);
rows.push(SparklineRow {
label: "Pkg Power",
short_label: "Pkg",
color: ThemeConfig::power_color(),
latest_str: format!("{power_w:.1}W"),
min_max_str: scale_badge(power_rng.0, power_rng.1),
history: power_history,
range: Some(power_rng),
badge: None,
});
rows
}
fn draw_top_border<W: Write>(stdout: &mut W, panel_width: usize) {
let title = "GPU Metrics";
let inner_width = panel_width.saturating_sub(2); let title_space = 1 + title.len() + 1;
let dashes = inner_width.saturating_sub(title_space + 1);
print_colored_text(
stdout,
"\u{256d}\u{2500}",
ThemeConfig::accent_color(),
None,
None,
);
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, title, ThemeConfig::accent_color(), None, None);
print_colored_text(stdout, " ", Color::White, None, None);
for _ in 0..dashes {
print_colored_text(stdout, "\u{2500}", ThemeConfig::accent_color(), None, None);
}
print_colored_text(stdout, "\u{256e}", ThemeConfig::accent_color(), None, None);
}
fn draw_bottom_border<W: Write>(stdout: &mut W, panel_width: usize) {
let inner_width = panel_width.saturating_sub(2);
print_colored_text(stdout, "\u{2570}", ThemeConfig::accent_color(), None, None);
for _ in 0..inner_width {
print_colored_text(stdout, "\u{2500}", ThemeConfig::accent_color(), None, None);
}
print_colored_text(stdout, "\u{256f}", ThemeConfig::accent_color(), None, None);
}
fn draw_sparkline_row<W: Write>(stdout: &mut W, row: &SparklineRow, panel_width: usize) {
let content_width = panel_width.saturating_sub(4);
let badge_len = row.badge.as_ref().map(|(s, _)| s.len() + 1).unwrap_or(0);
let fixed = LABEL_WIDTH + VALUE_WIDTH + MINMAX_WIDTH + SPACING + badge_len;
let sparkline_width = content_width.saturating_sub(fixed).max(4);
let sparkline = sparkline_braille(&row.history, sparkline_width, row.range);
print_colored_text(stdout, "\u{2502} ", ThemeConfig::accent_color(), None, None);
let label_display = format!("{:<LABEL_WIDTH$}", row.label);
print_colored_text(stdout, &label_display, row.color, None, None);
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, &sparkline, row.color, None, None);
print_colored_text(stdout, " ", Color::White, None, None);
let value_display = format!("{:<VALUE_WIDTH$}", row.latest_str);
print_colored_text(stdout, &value_display, Color::White, None, None);
let minmax_display = format!("{:<MINMAX_WIDTH$}", row.min_max_str);
print_colored_text(stdout, &minmax_display, Color::DarkGrey, None, None);
if let Some((ref text, color)) = row.badge {
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, text, color, None, None);
}
let used = 2 + LABEL_WIDTH + 1 + sparkline_width + 1 + VALUE_WIDTH + MINMAX_WIDTH + badge_len;
let pad = panel_width.saturating_sub(used + 2);
if pad > 0 {
print_colored_text(stdout, &" ".repeat(pad), Color::White, None, None);
}
print_colored_text(stdout, " \u{2502}", ThemeConfig::accent_color(), None, None);
}
const COMPACT_LABEL_WIDTH: usize = 4;
const COMPACT_VALUE_WIDTH: usize = 7;
const COMPACT_BADGE_WIDTH: usize = 5;
fn draw_gpu_util_graph(util: &SparklineRow, panel_width: usize) -> Vec<String> {
let content_width = panel_width.saturating_sub(4);
activity_panel::multirow_graph_lines(
&util.history,
(0.0, 100.0),
util.color,
ThemeConfig::accent_color(),
content_width,
&util.latest_str,
"0-100",
)
}
fn draw_compact_pair<W: Write>(stdout: &mut W, pair: &[SparklineRow], panel_width: usize) {
let content_width = panel_width.saturating_sub(4);
let first_budget = content_width / 2;
let second_budget = content_width - first_budget;
print_colored_text(stdout, "\u{2502} ", ThemeConfig::accent_color(), None, None);
let mut used = 0usize;
used += draw_compact_cell(stdout, pair.first(), first_budget);
used += draw_compact_cell(stdout, pair.get(1), second_budget);
let pad = content_width.saturating_sub(used);
if pad > 0 {
print_colored_text(stdout, &" ".repeat(pad), Color::White, None, None);
}
print_colored_text(stdout, " \u{2502}", ThemeConfig::accent_color(), None, None);
}
fn draw_compact_cell<W: Write>(stdout: &mut W, row: Option<&SparklineRow>, budget: usize) -> usize {
let Some(row) = row else {
if budget > 0 {
print_colored_text(stdout, &" ".repeat(budget), Color::White, None, None);
}
return budget;
};
let fixed = COMPACT_LABEL_WIDTH + 1 + 1 + COMPACT_VALUE_WIDTH + COMPACT_BADGE_WIDTH;
let spark_width = budget.saturating_sub(fixed).max(1);
let spark = sparkline_braille(&row.history, spark_width, row.range);
let mut remaining = budget;
let segments: [(&str, usize, Color); 6] = [
(row.short_label, COMPACT_LABEL_WIDTH, row.color),
(" ", 1, Color::White),
(&spark, spark_width, row.color),
(" ", 1, Color::White),
(&row.latest_str, COMPACT_VALUE_WIDTH, Color::White),
(&row.min_max_str, COMPACT_BADGE_WIDTH, Color::DarkGrey),
];
for (text, width, color) in segments {
if remaining == 0 {
break;
}
let w = width.min(remaining);
print_colored_text(stdout, &fit_field(text, w), color, None, None);
remaining -= w;
}
budget - remaining
}
fn fit_field(s: &str, width: usize) -> String {
let count = s.chars().count();
if count > width {
s.chars().take(width).collect()
} else {
format!("{s:<width$}")
}
}
fn detect_apple_silicon(state: &AppState) -> bool {
state.gpu_info.iter().any(|gpu| {
gpu.detail
.get("architecture")
.map(|arch| arch == "Apple Silicon")
.unwrap_or(false)
})
}
fn show_ane_row(state: &AppState) -> bool {
detect_apple_silicon(state)
}
fn show_npu_row(_state: &AppState) -> bool {
false
}
fn package_power(state: &AppState, is_apple: bool) -> f64 {
if is_apple {
let power_mw = state
.gpu_info
.iter()
.filter_map(|gpu| {
gpu.detail
.get("combined_power_mw")
.and_then(|s| s.parse::<f64>().ok())
})
.next()
.unwrap_or(0.0);
power_mw / 1000.0
} else {
crate::metrics::gpu_readings::total_power_watts(&state.gpu_info)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app_state::AppState;
use crate::device::{
AppleSiliconCpuInfo, CoreType, CoreUtilization, CpuInfo, CpuPlatformType, GpuInfo,
};
use std::collections::HashMap;
fn make_nvidia_state() -> AppState {
let mut state = AppState::new();
state.is_local_mode = true;
let mut detail = HashMap::new();
detail.insert("architecture".to_string(), "NVIDIA".to_string());
state.gpu_info.push(GpuInfo {
uuid: "gpu-0".to_string(),
time: String::new(),
name: "RTX 4090".to_string(),
device_type: "GPU".to_string(),
host_id: "localhost".to_string(),
hostname: "localhost".to_string(),
instance: "localhost".to_string(),
utilization: 75.0,
ane_utilization: 0.0,
dla_utilization: None,
tensorcore_utilization: None,
temperature: 72,
used_memory: 8 * 1024 * 1024 * 1024,
total_memory: 24 * 1024 * 1024 * 1024,
frequency: 2100,
power_consumption: 320.0,
gpu_core_count: None,
temperature_threshold_slowdown: None,
temperature_threshold_shutdown: None,
temperature_threshold_max_operating: None,
temperature_threshold_acoustic: None,
performance_state: None,
fan_speed_rpm: None,
numa_node_id: None,
gsp_firmware_mode: None,
gsp_firmware_version: None,
nvlink_remote_devices: Vec::new(),
gpm_metrics: None,
detail,
});
for i in 0..20 {
state.utilization_history.push_back(i as f64 * 5.0);
state.memory_history.push_back(i as f64 * 3.0);
state.temperature_history.push_back(50.0 + i as f64);
state
.package_power_history
.push_back(120.0 + i as f64 * 2.0);
}
state
}
fn make_apple_silicon_state() -> AppState {
let mut state = AppState::new();
state.is_local_mode = true;
let mut detail = HashMap::new();
detail.insert("architecture".to_string(), "Apple Silicon".to_string());
detail.insert("combined_power_mw".to_string(), "12500".to_string());
state.gpu_info.push(GpuInfo {
uuid: "apple-gpu".to_string(),
time: String::new(),
name: "Apple M2 Pro".to_string(),
device_type: "GPU".to_string(),
host_id: "localhost".to_string(),
hostname: "localhost".to_string(),
instance: "localhost".to_string(),
utilization: 45.0,
ane_utilization: 3500.0, dla_utilization: None,
tensorcore_utilization: None,
temperature: 55,
used_memory: 4 * 1024 * 1024 * 1024,
total_memory: 16 * 1024 * 1024 * 1024,
frequency: 1398,
power_consumption: 8.0,
gpu_core_count: Some(16),
temperature_threshold_slowdown: None,
temperature_threshold_shutdown: None,
temperature_threshold_max_operating: None,
temperature_threshold_acoustic: None,
performance_state: None,
fan_speed_rpm: None,
numa_node_id: None,
gsp_firmware_mode: None,
gsp_firmware_version: None,
nvlink_remote_devices: Vec::new(),
gpm_metrics: None,
detail,
});
for i in 0..20 {
state.utilization_history.push_back(i as f64 * 4.0);
state.memory_history.push_back(i as f64 * 2.5);
state.temperature_history.push_back(40.0 + i as f64);
state.package_power_history.push_back(8.0 + i as f64 * 0.5);
state.ane_power_history.push_back(i as f64 * 0.2);
}
state
}
fn make_standard_cpu(core_count: usize) -> CpuInfo {
let per_core: Vec<CoreUtilization> = (0..core_count)
.map(|i| CoreUtilization {
core_id: i as u32,
core_type: CoreType::Standard,
utilization: (i as f64 * 10.0) % 100.0,
})
.collect();
CpuInfo {
index: 0,
host_id: "localhost".to_string(),
hostname: "testhost".to_string(),
instance: "".to_string(),
cpu_model: "Test CPU".to_string(),
architecture: "x86_64".to_string(),
platform_type: CpuPlatformType::Intel,
socket_count: 1,
total_cores: core_count as u32,
total_threads: core_count as u32 * 2,
base_frequency_mhz: 3000,
max_frequency_mhz: 4000,
cache_size_mb: 16,
utilization: 50.0,
temperature: Some(65),
power_consumption: Some(95.0),
per_socket_info: Vec::new(),
apple_silicon_info: None,
per_core_utilization: per_core,
time: String::new(),
}
}
fn make_apple_cpu() -> CpuInfo {
let mut per_core = Vec::new();
for i in 0..4 {
per_core.push(CoreUtilization {
core_id: i as u32,
core_type: CoreType::Efficiency,
utilization: 20.0 + i as f64 * 5.0,
});
}
for i in 0..8 {
per_core.push(CoreUtilization {
core_id: (4 + i) as u32,
core_type: CoreType::Performance,
utilization: 40.0 + i as f64 * 5.0,
});
}
CpuInfo {
index: 0,
host_id: "localhost".to_string(),
hostname: "testhost".to_string(),
instance: "".to_string(),
cpu_model: "Apple M2 Pro".to_string(),
architecture: "arm64".to_string(),
platform_type: CpuPlatformType::AppleSilicon,
socket_count: 1,
total_cores: 12,
total_threads: 12,
base_frequency_mhz: 3490,
max_frequency_mhz: 3490,
cache_size_mb: 16,
utilization: 35.0,
temperature: None,
power_consumption: None,
per_socket_info: Vec::new(),
apple_silicon_info: Some(AppleSiliconCpuInfo {
s_core_count: 0,
p_core_count: 8,
e_core_count: 4,
gpu_core_count: 16,
s_core_utilization: 0.0,
p_core_utilization: 55.0,
e_core_utilization: 25.0,
ane_ops_per_second: None,
s_cluster_frequency_mhz: None,
p_cluster_frequency_mhz: Some(3490),
e_cluster_frequency_mhz: Some(2420),
s_core_l2_cache_mb: None,
p_core_l2_cache_mb: Some(16),
e_core_l2_cache_mb: Some(4),
}),
per_core_utilization: per_core,
time: String::new(),
}
}
fn make_apple_cpu_many_cores() -> CpuInfo {
let mut per_core = Vec::new();
for i in 0..8 {
per_core.push(CoreUtilization {
core_id: i as u32,
core_type: CoreType::Efficiency,
utilization: 20.0 + i as f64 * 3.0,
});
}
for i in 0..16 {
per_core.push(CoreUtilization {
core_id: (8 + i) as u32,
core_type: CoreType::Performance,
utilization: 40.0 + i as f64 * 2.0,
});
}
let mut cpu = make_apple_cpu();
cpu.total_cores = 24;
cpu.total_threads = 24;
if let Some(a) = cpu.apple_silicon_info.as_mut() {
a.p_core_count = 16;
a.e_core_count = 8;
}
cpu.per_core_utilization = per_core;
cpu
}
const SHORT_ROWS: u16 = 24;
const TALL_ROWS: u16 = 50;
#[test]
fn test_gpu_content_rows_empty() {
let state = AppState::new();
assert_eq!(gpu_content_rows(&state, SHORT_ROWS), 0);
assert_eq!(gpu_content_rows(&state, TALL_ROWS), 0);
}
#[test]
fn test_gpu_content_rows_nvidia() {
let state = make_nvidia_state();
assert_eq!(gpu_content_rows(&state, SHORT_ROWS), 4);
assert_eq!(gpu_content_rows(&state, TALL_ROWS), 5);
}
#[test]
fn test_gpu_content_rows_apple_with_ane() {
let state = make_apple_silicon_state();
assert_eq!(gpu_content_rows(&state, SHORT_ROWS), 5);
assert_eq!(gpu_content_rows(&state, TALL_ROWS), 5);
}
#[test]
fn test_gpu_content_rows_nvidia_with_npu_multirow() {
let state = make_nvidia_state();
assert_eq!(build_rows(&state, false, false, true).len(), 5);
assert_eq!(gpu_content_rows(&state, TALL_ROWS), 5);
}
#[test]
fn test_detect_apple_silicon() {
assert!(!detect_apple_silicon(&make_nvidia_state()));
assert!(detect_apple_silicon(&make_apple_silicon_state()));
}
#[test]
fn test_show_ane_row() {
assert!(!show_ane_row(&make_nvidia_state()));
assert!(show_ane_row(&make_apple_silicon_state()));
}
#[test]
fn test_show_ane_row_even_when_ane_idle() {
let mut state = make_apple_silicon_state();
state.gpu_info[0].ane_utilization = 0.0;
assert!(show_ane_row(&state));
}
#[test]
fn test_show_npu_row_returns_false() {
assert!(!show_npu_row(&make_nvidia_state()));
assert!(!show_npu_row(&make_apple_silicon_state()));
}
#[test]
fn test_gpu_content_rows_apple_with_zero_ane_still_shows_row() {
let mut state = make_apple_silicon_state();
state.gpu_info[0].ane_utilization = 0.0;
assert_eq!(gpu_content_rows(&state, SHORT_ROWS), 5);
}
#[test]
fn test_package_power_apple_silicon() {
let state = make_apple_silicon_state();
let watts = package_power(&state, true);
assert!((watts - 12.5).abs() < 0.01);
}
#[test]
fn test_package_power_nvidia() {
let state = make_nvidia_state();
let watts = package_power(&state, false);
assert!((watts - 320.0).abs() < 0.01);
}
#[test]
fn test_build_rows_nvidia() {
let state = make_nvidia_state();
let rows = build_rows(&state, false, false, false);
assert_eq!(rows.len(), 4);
assert_eq!(rows[0].label, "GPU Util");
assert_eq!(rows[1].label, "GPU Mem");
assert_eq!(rows[2].label, "GPU Temp");
assert_eq!(rows[3].label, "Pkg Power");
assert!(rows[2].badge.is_none());
}
#[test]
fn test_build_rows_apple_silicon() {
let state = make_apple_silicon_state();
let rows = build_rows(&state, true, true, false);
assert_eq!(rows.len(), 5);
assert_eq!(rows[0].label, "GPU Util");
assert_eq!(rows[3].label, "ANE");
assert_eq!(rows[4].label, "Pkg Power");
assert!(rows[2].badge.is_none());
assert_eq!(rows[0].min_max_str, "0-80");
assert_eq!(rows[1].min_max_str, "0-50");
assert_eq!(rows[2].min_max_str, "40-60");
assert_eq!(rows[3].min_max_str, "0-4");
assert_eq!(rows[4].min_max_str, "8-18");
}
#[test]
fn test_build_rows_with_npu_scaffolding() {
let state = make_nvidia_state();
let rows = build_rows(&state, false, false, true);
assert_eq!(rows.len(), 5);
assert_eq!(rows[3].label, "NPU");
assert_eq!(rows[4].label, "Pkg Power");
}
#[test]
fn test_render_gpu_lines_nvidia() {
let state = make_nvidia_state();
let lines = render_gpu_lines(&state, 60, SHORT_ROWS);
assert_eq!(lines.len(), 6);
assert!(!lines[0].is_empty()); }
#[test]
fn test_render_gpu_lines_apple_silicon() {
let state = make_apple_silicon_state();
let lines = render_gpu_lines(&state, 60, SHORT_ROWS);
assert_eq!(lines.len(), 7);
}
#[test]
fn test_render_gpu_lines_empty() {
let state = AppState::new();
let lines = render_gpu_lines(&state, 60, SHORT_ROWS);
assert!(lines.is_empty());
assert!(render_gpu_lines(&state, 60, TALL_ROWS).is_empty());
}
#[test]
fn test_render_gpu_lines_multirow_matches_content_rows() {
for state in [make_nvidia_state(), make_apple_silicon_state()] {
for &rows in &[SHORT_ROWS, TALL_ROWS] {
let expected = gpu_content_rows(&state, rows) + 2; let lines = render_gpu_lines(&state, 60, rows);
assert_eq!(
lines.len(),
expected,
"line count must match gpu_content_rows at rows={rows}"
);
}
}
}
#[test]
fn test_render_gpu_lines_apple_multirow_is_seven() {
let state = make_apple_silicon_state();
let lines = render_gpu_lines(&state, 60, TALL_ROWS);
assert_eq!(lines.len(), 7);
}
#[test]
fn test_render_gpu_lines_multirow_no_panic_narrow() {
for state in [make_nvidia_state(), make_apple_silicon_state()] {
for &w in &[40usize, 41, 30, 12] {
let _ = render_gpu_lines(&state, w, TALL_ROWS);
}
}
}
fn visible_width(line: &str) -> usize {
let mut count = 0usize;
let mut in_escape = false;
for c in line.chars() {
if in_escape {
if c == 'm' {
in_escape = false;
}
} else if c == '\u{1b}' {
in_escape = true;
} else {
count += 1;
}
}
count
}
#[test]
fn test_multirow_lines_fit_panel_width_at_narrow_widths() {
for state in [make_nvidia_state(), make_apple_silicon_state()] {
for &w in &[40usize, 41, 42, 60] {
for &rows in &[SHORT_ROWS, TALL_ROWS] {
for (i, line) in render_gpu_lines(&state, w, rows).iter().enumerate() {
assert_eq!(
visible_width(line),
w,
"line {i} must be {w} columns (rows={rows})"
);
}
}
}
}
}
fn combined_line_count(state: &AppState, cpu: &[CpuInfo], width: usize, rows: u16) -> usize {
let mut buf: Vec<u8> = Vec::new();
render_combined_activity_panel(&mut buf, state, cpu, width, rows);
String::from_utf8(buf)
.unwrap()
.split("\r\n")
.filter(|l| !l.is_empty())
.count()
}
#[test]
fn test_render_combined_does_not_panic_nvidia() {
let mut state = make_nvidia_state();
let cpu = vec![make_standard_cpu(8)];
state.cpu_info = cpu.clone();
for &rows in &[SHORT_ROWS, TALL_ROWS] {
let mut buf: Vec<u8> = Vec::new();
render_combined_activity_panel(&mut buf, &state, &cpu, 120, rows);
assert!(!buf.is_empty());
}
}
#[test]
fn test_render_combined_does_not_panic_apple() {
let mut state = make_apple_silicon_state();
let cpu = vec![make_apple_cpu()];
state.cpu_info = cpu.clone();
for &rows in &[SHORT_ROWS, TALL_ROWS] {
let mut buf: Vec<u8> = Vec::new();
render_combined_activity_panel(&mut buf, &state, &cpu, 120, rows);
assert!(!buf.is_empty());
}
}
#[test]
fn test_render_combined_no_gpu() {
let state = AppState::new();
let cpu = vec![make_standard_cpu(4)];
let mut buf: Vec<u8> = Vec::new();
render_combined_activity_panel(&mut buf, &state, &cpu, 120, TALL_ROWS);
assert!(!buf.is_empty());
}
#[test]
fn test_render_combined_no_cpu() {
let state = make_nvidia_state();
let cpu: Vec<CpuInfo> = Vec::new();
let mut buf: Vec<u8> = Vec::new();
render_combined_activity_panel(&mut buf, &state, &cpu, 120, TALL_ROWS);
assert!(buf.is_empty());
}
#[test]
fn test_render_combined_line_count_matches_layout_math() {
let mut nvidia = make_nvidia_state();
let nvidia_cpu = vec![make_standard_cpu(8)];
nvidia.cpu_info = nvidia_cpu.clone();
let mut apple = make_apple_silicon_state();
let apple_cpu = vec![make_apple_cpu()];
apple.cpu_info = apple_cpu.clone();
for &rows in &[SHORT_ROWS, TALL_ROWS] {
for (state, cpu) in [(&nvidia, &nvidia_cpu), (&apple, &apple_cpu)] {
let cpu_h = activity_panel::panel_height(cpu, 120, rows) as usize;
let gpu_content = gpu_content_rows(state, rows);
let gpu_h = if gpu_content > 0 { gpu_content + 2 } else { 0 };
let expected = cpu_h.max(gpu_h);
let actual = combined_line_count(state, cpu, 120, rows);
assert_eq!(actual, expected, "combined mismatch at rows={rows}");
}
}
}
#[test]
fn test_render_combined_apple_multirow_no_growth() {
let mut apple = make_apple_silicon_state();
let cpu = vec![make_apple_cpu_many_cores()];
apple.cpu_info = cpu.clone();
assert_eq!(combined_line_count(&apple, &cpu, 120, SHORT_ROWS), 7);
assert_eq!(combined_line_count(&apple, &cpu, 120, TALL_ROWS), 7);
}
#[test]
fn test_render_combined_no_panic_narrow_short_tall() {
let mut apple = make_apple_silicon_state();
apple.utilization_history.clear();
apple.memory_history.clear();
apple.temperature_history.clear();
apple.ane_power_history.clear();
apple.package_power_history.clear();
apple.cpu_utilization_history.clear();
let cpu = vec![make_apple_cpu()];
let no_gpu = AppState::new();
for &(w, r) in &[(81usize, SHORT_ROWS), (81, TALL_ROWS), (200, TALL_ROWS)] {
let mut buf: Vec<u8> = Vec::new();
render_combined_activity_panel(&mut buf, &apple, &cpu, w, r);
let mut buf2: Vec<u8> = Vec::new();
render_combined_activity_panel(&mut buf2, &no_gpu, &cpu, w, r);
}
}
}