use std::io::Write;
use crossterm::{queue, style::Color, style::Print};
use crate::common::config::EnergyConfig;
use crate::device::ChassisInfo;
use crate::metrics::energy::{
EnergyKey, EnergyScope, PowerIntegrator, joules_to_cost, joules_to_kwh,
};
use crate::ui::text::print_colored_text;
use crate::ui::widgets::draw_bar;
use super::gpu_renderer::format_hostname_with_scroll;
#[allow(dead_code)]
pub struct ChassisRenderer;
impl Default for ChassisRenderer {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl ChassisRenderer {
pub fn new() -> Self {
Self
}
}
pub fn print_chassis_energy_row<W: Write>(
stdout: &mut W,
info: &ChassisInfo,
integrator: &PowerIntegrator,
energy_config: &EnergyConfig,
) {
let key = EnergyKey::chassis(info.hostname.clone());
let stats = integrator
.iter_stats()
.find(|s| s.key.scope == EnergyScope::Chassis && s.key.host == info.hostname);
let joules = match stats {
Some(s) if s.session_joules > 0.0 => s.session_joules,
_ => return, };
let _ = key;
let kwh = joules_to_kwh(joules);
let kwh_display = if kwh >= 0.001 {
format!("{kwh:.3} kWh")
} else {
format!("{joules:.1} J")
};
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, "Energy session: ", Color::Yellow, None, None);
print_colored_text(stdout, &kwh_display, Color::White, None, None);
if energy_config.cost_visible() {
let cost = joules_to_cost(joules, energy_config.price_per_kwh);
let cost_display = format_cost(&energy_config.currency, cost);
let price_display = format!(
" (at {}/kWh)",
format_cost(&energy_config.currency, energy_config.price_per_kwh),
);
print_colored_text(stdout, " | ", Color::DarkGrey, None, None);
print_colored_text(stdout, &cost_display, Color::Green, None, None);
print_colored_text(stdout, &price_display, Color::DarkGrey, None, None);
}
queue!(stdout, Print("\r\n")).unwrap();
}
fn format_cost(currency: &str, amount: f64) -> String {
let trimmed = currency.trim();
match trimmed.to_ascii_uppercase().as_str() {
"USD" => format!("${amount:.2}"),
"EUR" => format!("\u{20AC}{amount:.2}"),
"GBP" => format!("\u{00A3}{amount:.2}"),
"JPY" => format!("\u{00A5}{amount:.0}"),
"KRW" => format!("\u{20A9}{amount:.0}"),
_ if trimmed.is_empty() => format!("{amount:.2}"),
_ => format!("{amount:.2} {trimmed}"),
}
}
pub fn print_chassis_info<W: Write>(
stdout: &mut W,
_index: usize,
info: &ChassisInfo,
width: usize,
hostname_scroll_offset: usize,
) {
let hostname_display = format_hostname_with_scroll(&info.hostname, hostname_scroll_offset);
print_colored_text(stdout, "NODE ", Color::Yellow, None, None);
print_colored_text(stdout, &hostname_display, Color::White, None, None);
print_colored_text(stdout, " Pwr:", Color::Red, None, None);
let power_display = if let Some(power) = info.total_power_watts {
format!("{power:>6.1}W")
} else {
format!("{:>7}", "N/A")
};
print_colored_text(stdout, &power_display, Color::White, None, None);
if let Some(ref pressure) = info.thermal_pressure {
print_colored_text(stdout, " Thermal:", Color::Magenta, None, None);
print_colored_text(stdout, &format!("{pressure:>8}"), Color::White, None, None);
} else {
if let Some(inlet) = info.inlet_temperature {
print_colored_text(stdout, " Inlet:", Color::Magenta, None, None);
print_colored_text(stdout, &format!("{inlet:>4.0}°C"), Color::White, None, None);
}
if let Some(outlet) = info.outlet_temperature {
print_colored_text(stdout, " Outlet:", Color::Magenta, None, None);
print_colored_text(
stdout,
&format!("{outlet:>4.0}°C"),
Color::White,
None,
None,
);
}
}
let has_power_breakdown = info.detail.contains_key("cpu_power_watts")
|| info.detail.contains_key("gpu_power_watts")
|| info.detail.contains_key("ane_power_watts");
if has_power_breakdown {
print_colored_text(stdout, " │", Color::DarkGrey, None, None);
if let Some(cpu_power) = info.detail.get("cpu_power_watts")
&& let Ok(power) = cpu_power.parse::<f64>()
{
print_colored_text(stdout, " CPU:", Color::Cyan, None, None);
print_colored_text(stdout, &format!("{power:>5.1}W"), Color::White, None, None);
}
if let Some(gpu_power) = info.detail.get("gpu_power_watts")
&& let Ok(power) = gpu_power.parse::<f64>()
{
print_colored_text(stdout, " GPU:", Color::Green, None, None);
print_colored_text(stdout, &format!("{power:>5.1}W"), Color::White, None, None);
}
if let Some(ane_power) = info.detail.get("ane_power_watts")
&& let Ok(power) = ane_power.parse::<f64>()
{
print_colored_text(stdout, " ANE:", Color::Blue, None, None);
print_colored_text(stdout, &format!("{power:>5.1}W"), Color::White, None, None);
}
}
if !info.fan_speeds.is_empty() {
print_colored_text(stdout, " Fans:", Color::Cyan, None, None);
let avg_rpm: u32 =
info.fan_speeds.iter().map(|f| f.speed_rpm).sum::<u32>() / info.fan_speeds.len() as u32;
print_colored_text(
stdout,
&format!("{avg_rpm:>5}RPM"),
Color::White,
None,
None,
);
}
if !info.psu_status.is_empty() {
let ok_count = info
.psu_status
.iter()
.filter(|p| p.status == crate::device::PsuStatus::Ok)
.count();
let total = info.psu_status.len();
print_colored_text(stdout, " PSU:", Color::Yellow, None, None);
let psu_color = if ok_count == total {
Color::Green
} else {
Color::Red
};
print_colored_text(
stdout,
&format!("{ok_count}/{total}"),
psu_color,
None,
None,
);
}
queue!(stdout, Print("\r\n")).unwrap();
if let Some(power) = info.total_power_watts {
let available_width = width.saturating_sub(10);
let gauge_width = available_width;
let is_apple_silicon = info.detail.get("platform") == Some(&"Apple Silicon".to_string());
let max_power = if is_apple_silicon { 150.0 } else { 1000.0 };
let power_percent = (power / max_power * 100.0).min(100.0);
let left_padding = 5;
let right_padding = width - left_padding - gauge_width;
print_colored_text(stdout, " ", Color::White, None, None);
draw_bar(
stdout,
"Power",
power_percent,
100.0,
gauge_width,
Some(format!("{power:.1}W")),
);
print_colored_text(stdout, &" ".repeat(right_padding), Color::White, None, None);
queue!(stdout, Print("\r\n")).unwrap();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::ChassisInfo;
use crate::metrics::energy::{EnergyKey, PowerIntegrator};
use std::time::{Duration, Instant};
#[test]
fn test_chassis_renderer_new() {
let renderer = ChassisRenderer::new();
let _ = renderer;
}
#[test]
fn test_print_chassis_info_basic() {
let mut buffer = Vec::new();
let chassis = ChassisInfo {
hostname: "test-host".to_string(),
total_power_watts: Some(45.5),
thermal_pressure: Some("Nominal".to_string()),
..Default::default()
};
print_chassis_info(&mut buffer, 0, &chassis, 80, 0);
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("NODE"));
assert!(output.contains("test-host"));
}
#[test]
fn energy_row_emits_nothing_when_no_samples() {
let mut buffer = Vec::new();
let chassis = ChassisInfo {
hostname: "dgx-01".to_string(),
total_power_watts: Some(300.0),
..Default::default()
};
let integrator = PowerIntegrator::default();
let cfg = EnergyConfig::default();
print_chassis_energy_row(&mut buffer, &chassis, &integrator, &cfg);
let output = String::from_utf8(buffer).unwrap();
assert!(output.is_empty(), "expected empty output, got: {output:?}");
}
#[test]
fn energy_row_shows_kwh_and_cost_when_enabled() {
let mut buffer = Vec::new();
let chassis = ChassisInfo {
hostname: "dgx-01".to_string(),
total_power_watts: Some(300.0),
..Default::default()
};
let mut integrator = PowerIntegrator::default();
let key = EnergyKey::chassis("dgx-01");
let origin = Instant::now();
integrator.record_sample(key.clone(), origin, 300.0);
integrator.record_sample(key.clone(), origin + Duration::from_secs(600), 300.0);
let cfg = EnergyConfig::default();
print_chassis_energy_row(&mut buffer, &chassis, &integrator, &cfg);
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("Energy session:"));
assert!(output.contains("0.050 kWh"));
assert!(output.contains("$0.01"));
assert!(output.contains("$0.12/kWh"));
}
#[test]
fn energy_row_hides_cost_when_show_cost_false() {
let mut buffer = Vec::new();
let chassis = ChassisInfo {
hostname: "dgx-01".to_string(),
total_power_watts: Some(300.0),
..Default::default()
};
let mut integrator = PowerIntegrator::default();
let key = EnergyKey::chassis("dgx-01");
let origin = Instant::now();
integrator.record_sample(key.clone(), origin, 300.0);
integrator.record_sample(key, origin + Duration::from_secs(600), 300.0);
let cfg = EnergyConfig {
show_cost: false,
..EnergyConfig::default()
};
print_chassis_energy_row(&mut buffer, &chassis, &integrator, &cfg);
let output = String::from_utf8(buffer).unwrap();
assert!(output.contains("kWh"));
assert!(!output.contains('$'), "cost should be hidden: {output}");
}
#[test]
fn format_cost_respects_currency_code() {
assert_eq!(format_cost("USD", 1.234), "$1.23");
assert_eq!(format_cost("EUR", 1.234), "\u{20AC}1.23");
assert_eq!(format_cost("KRW", 1234.5), "\u{20A9}1234");
assert_eq!(format_cost("UNKNOWN", 1.234), "1.23 UNKNOWN");
assert_eq!(format_cost("", 1.234), "1.23");
}
}