use std::io::Write;
use crossterm::{queue, style::Color, style::Print};
use crate::device::{CoreUtilization, CpuInfo};
use crate::ui::text::print_colored_text;
use crate::ui::widgets::draw_bar;
use super::widgets::gauges::get_utilization_block;
#[cfg(target_os = "linux")]
static CACHED_CPUSET: std::sync::OnceLock<(bool, Option<String>)> = std::sync::OnceLock::new();
#[cfg(target_os = "linux")]
fn get_cached_cpuset() -> &'static (bool, Option<String>) {
CACHED_CPUSET.get_or_init(|| {
let is_container = std::path::Path::new("/.dockerenv").exists()
|| std::path::Path::new("/proc/self/cgroup").exists();
let cpuset = if is_container {
std::fs::read_to_string("/sys/fs/cgroup/cpuset.cpus.effective")
.or_else(|_| std::fs::read_to_string("/sys/fs/cgroup/cpuset.cpus"))
.or_else(|_| std::fs::read_to_string("/sys/fs/cgroup/cpuset/cpuset.cpus"))
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
} else {
None
};
(is_container, cpuset)
})
}
#[allow(dead_code)]
pub struct CpuRenderer;
impl Default for CpuRenderer {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
impl CpuRenderer {
pub fn new() -> Self {
Self
}
}
fn format_hostname_with_scroll(hostname: &str, scroll_offset: usize) -> String {
if hostname.len() > 9 {
let scroll_len = hostname.len() + 3;
let start_pos = scroll_offset % scroll_len;
let extended_hostname = format!("{hostname} {hostname}");
extended_hostname
.chars()
.skip(start_pos)
.take(9)
.collect::<String>()
} else {
format!("{hostname:<9}")
}
}
fn render_cpu_visualization<W: Write>(
stdout: &mut W,
per_core_utilization: &[CoreUtilization],
cpuset: Option<&str>,
width: usize,
is_container: bool,
) {
if per_core_utilization.is_empty() {
return;
}
let total_cpus = per_core_utilization.len();
let box_width = width.saturating_sub(10);
print_colored_text(stdout, " ", Color::White, None, None);
let title = if is_container {
"Container CPUs"
} else {
"CPU Cores"
};
let title_with_spaces_len = 1 + title.len() + 1;
print_colored_text(stdout, "â•─", Color::Cyan, None, None);
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, title, Color::Cyan, None, None);
print_colored_text(stdout, " ", Color::White, None, None);
let remaining_dashes = box_width.saturating_sub(title_with_spaces_len + 1); for _ in 0..remaining_dashes {
print_colored_text(stdout, "─", Color::Cyan, None, None);
}
print_colored_text(stdout, "â•®", Color::Cyan, None, None);
queue!(stdout, Print("\r\n")).unwrap();
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, "│ ", Color::Cyan, None, None);
let cores_to_show: Vec<&CoreUtilization> = per_core_utilization.iter().collect();
let display_count = cores_to_show.len();
let group_size = if display_count <= 32 {
4
} else if display_count <= 64 {
8
} else {
16
};
let content_str = {
let mut content = String::new();
let mut idx = 0;
for core in &cores_to_show {
let (block, _) = get_utilization_block(core.utilization);
content.push_str(block);
idx += 1;
if (idx % group_size == 0) && (idx < display_count) {
content.push(' ');
}
}
if is_container && cpuset.is_some() {
let summary = format!(" ({display_count} allocated)");
content.push_str(&summary);
} else {
let avg_util = per_core_utilization
.iter()
.map(|c| c.utilization)
.sum::<f64>()
/ total_cpus as f64;
let summary = format!(" ({total_cpus} cores, {avg_util:.1}% avg)");
content.push_str(&summary);
}
content
};
let mut idx = 0;
for core in &cores_to_show {
let (block, color) = get_utilization_block(core.utilization);
print_colored_text(stdout, block, color, None, None);
idx += 1;
if (idx % group_size == 0) && (idx < display_count) {
print_colored_text(stdout, " ", Color::White, None, None);
}
}
if is_container && cpuset.is_some() {
let summary = format!(" ({display_count} allocated)");
print_colored_text(stdout, &summary, Color::Yellow, None, None);
} else {
let avg_util = per_core_utilization
.iter()
.map(|c| c.utilization)
.sum::<f64>()
/ total_cpus as f64;
let summary = format!(" ({total_cpus} cores, {avg_util:.1}% avg)");
print_colored_text(stdout, &summary, Color::Yellow, None, None);
}
let content_display_len = content_str.chars().count();
let inner_width = box_width.saturating_sub(2); let padding_needed = inner_width.saturating_sub(content_display_len);
for _ in 0..padding_needed {
print_colored_text(stdout, " ", Color::White, None, None);
}
print_colored_text(stdout, " │", Color::Cyan, None, None);
queue!(stdout, Print("\r\n")).unwrap();
print_colored_text(stdout, " ", Color::White, None, None);
print_colored_text(stdout, "â•°", Color::Cyan, None, None);
for _ in 0..box_width {
print_colored_text(stdout, "─", Color::Cyan, None, None);
}
print_colored_text(stdout, "╯", Color::Cyan, None, None);
queue!(stdout, Print("\r\n")).unwrap();
}
#[allow(clippy::too_many_arguments)]
pub fn print_cpu_info<W: Write>(
stdout: &mut W,
_index: usize,
info: &CpuInfo,
width: usize,
show_per_core: bool,
cpu_name_scroll_offset: usize,
hostname_scroll_offset: usize,
show_hostname: bool,
) {
let cpu_name = if info.cpu_model.len() > 15 {
let scroll_len = info.cpu_model.len() + 3;
let start_pos = cpu_name_scroll_offset % scroll_len;
let extended_name = format!("{0} {0}", info.cpu_model);
extended_name
.chars()
.skip(start_pos)
.take(15)
.collect::<String>()
} else {
format!("{:<15}", info.cpu_model)
};
print_colored_text(stdout, "CPU ", Color::Cyan, None, None);
print_colored_text(stdout, &cpu_name, Color::White, None, None);
if show_hostname {
let hostname_display = format_hostname_with_scroll(&info.hostname, hostname_scroll_offset);
print_colored_text(stdout, " @ ", Color::DarkGreen, None, None);
print_colored_text(stdout, &hostname_display, Color::White, None, None);
}
print_colored_text(stdout, " Arch:", Color::Yellow, None, None);
print_colored_text(stdout, &info.architecture, Color::White, None, None);
print_colored_text(stdout, " Sockets:", Color::Yellow, None, None);
print_colored_text(
stdout,
&format!("{:>2}", info.socket_count),
Color::White,
None,
None,
);
if let Some(apple_info) = &info.apple_silicon_info {
print_colored_text(stdout, " Cores:", Color::Green, None, None);
if apple_info.s_core_count > 0 {
print_colored_text(
stdout,
&format!("{:>2}S+", apple_info.s_core_count),
Color::White,
None,
None,
);
print_colored_text(
stdout,
&format!("{:>2}P", apple_info.p_core_count),
Color::White,
None,
None,
);
} else {
print_colored_text(
stdout,
&format!("{:>2}P+", apple_info.p_core_count),
Color::White,
None,
None,
);
print_colored_text(
stdout,
&format!("{:>2}E", apple_info.e_core_count),
Color::White,
None,
None,
);
}
} else {
print_colored_text(stdout, " Cores:", Color::Green, None, None);
print_colored_text(
stdout,
&format!("{:>2}", info.total_cores),
Color::White,
None,
None,
);
}
print_colored_text(stdout, " Freq:", Color::Magenta, None, None);
if let Some(apple_info) = &info.apple_silicon_info {
let (high_freq, low_freq) = if apple_info.s_core_count > 0 {
(
apple_info.s_cluster_frequency_mhz,
apple_info.p_cluster_frequency_mhz,
)
} else {
(
apple_info.p_cluster_frequency_mhz,
apple_info.e_cluster_frequency_mhz,
)
};
if let (Some(hi), Some(lo)) = (high_freq, low_freq) {
let freq_display = if hi >= 1000 && lo >= 1000 {
format!("{:.2}+{:.2}GHz", hi as f64 / 1000.0, lo as f64 / 1000.0)
} else if hi >= 1000 {
format!("{:.2}GHz+{lo}MHz", hi as f64 / 1000.0)
} else if lo >= 1000 {
format!("{hi}MHz+{:.2}GHz", lo as f64 / 1000.0)
} else {
format!("{hi}+{lo}MHz")
};
print_colored_text(
stdout,
&format!("{freq_display:>13}"),
Color::White,
None,
None,
);
} else {
let freq_ghz = info.max_frequency_mhz as f64 / 1000.0;
print_colored_text(
stdout,
&format!("{freq_ghz:>6.1}GHz"),
Color::White,
None,
None,
);
}
} else {
let freq_ghz = info.max_frequency_mhz as f64 / 1000.0;
print_colored_text(
stdout,
&format!("{freq_ghz:>6.1}GHz"),
Color::White,
None,
None,
);
}
if let Some(temp) = info.temperature {
print_colored_text(stdout, " Temp:", Color::Magenta, None, None);
print_colored_text(stdout, &format!("{temp:>3}°C"), Color::White, None, None);
}
if let Some(apple_info) = &info.apple_silicon_info {
let (high_cache, low_cache) = if apple_info.s_core_count > 0 {
(apple_info.s_core_l2_cache_mb, apple_info.p_core_l2_cache_mb)
} else {
(apple_info.p_core_l2_cache_mb, apple_info.e_core_l2_cache_mb)
};
if let (Some(hi_cache), Some(lo_cache)) = (high_cache, low_cache) {
print_colored_text(stdout, " L2 Cache:", Color::Red, None, None);
print_colored_text(
stdout,
&format!("{hi_cache}MB+{lo_cache}MB"),
Color::White,
None,
None,
);
} else if info.cache_size_mb > 0 {
print_colored_text(stdout, " L2 Cache:", Color::Red, None, None);
print_colored_text(
stdout,
&format!("{:>5}MB", info.cache_size_mb),
Color::White,
None,
None,
);
}
} else if info.cache_size_mb > 0 {
print_colored_text(stdout, " L3 Cache:", Color::Red, None, None);
print_colored_text(
stdout,
&format!("{:>5}MB", info.cache_size_mb),
Color::White,
None,
None,
);
}
if let Some(power) = info.power_consumption {
print_colored_text(stdout, " Pwr:", Color::Red, None, None);
print_colored_text(stdout, &format!("{power:>4.0}W"), Color::White, None, None);
}
queue!(stdout, Print("\r\n")).unwrap();
let available_width = width.saturating_sub(10);
if let Some(apple_info) = &info.apple_silicon_info {
let num_gauges = 2;
let gauge_width = (available_width - 2) / 2;
let total_gauge_width = gauge_width * num_gauges + (num_gauges - 1) * 2;
let left_padding = 5;
let right_padding = width - left_padding - total_gauge_width;
print_colored_text(stdout, " ", Color::White, None, None);
if apple_info.s_core_count > 0 {
draw_bar(
stdout,
"S-CPU",
apple_info.s_core_utilization,
100.0,
gauge_width,
None,
);
print_colored_text(stdout, " ", Color::White, None, None);
draw_bar(
stdout,
"P-CPU",
apple_info.p_core_utilization,
100.0,
gauge_width,
None,
);
} else {
draw_bar(
stdout,
"P-CPU",
apple_info.p_core_utilization,
100.0,
gauge_width,
None,
);
print_colored_text(stdout, " ", Color::White, None, None);
draw_bar(
stdout,
"E-CPU",
apple_info.e_core_utilization,
100.0,
gauge_width,
None,
);
}
print_colored_text(stdout, &" ".repeat(right_padding), Color::White, None, None);
} else {
let gauge_width = available_width;
let total_gauge_width = gauge_width;
let left_padding = 5;
let right_padding = width - left_padding - total_gauge_width;
print_colored_text(stdout, " ", Color::White, None, None);
draw_bar(stdout, "CPU", info.utilization, 100.0, gauge_width, None);
print_colored_text(stdout, &" ".repeat(right_padding), Color::White, None, None);
}
queue!(stdout, Print("\r\n")).unwrap();
if show_per_core && !info.per_core_utilization.is_empty() {
#[cfg(target_os = "linux")]
{
let &(is_container, ref cpuset) = get_cached_cpuset();
render_cpu_visualization(
stdout,
&info.per_core_utilization,
cpuset.as_deref(),
width,
is_container,
);
}
#[cfg(not(target_os = "linux"))]
{
render_cpu_visualization(stdout, &info.per_core_utilization, None, width, false);
}
let total_cores = info.per_core_utilization.len();
let cores_per_line = if total_cores <= 16 { 4 } else { 8 };
let mut s_cores = Vec::new();
let mut p_cores = Vec::new();
let mut e_cores = Vec::new();
let mut standard_cores = Vec::new();
for core in &info.per_core_utilization {
match core.core_type {
crate::device::CoreType::Super => s_cores.push(core),
crate::device::CoreType::Performance => p_cores.push(core),
crate::device::CoreType::Efficiency => e_cores.push(core),
crate::device::CoreType::Standard => standard_cores.push(core),
}
}
let available_width = width.saturating_sub(10); let spacing_between_cores = 2;
let core_bar_width =
(available_width - (cores_per_line - 1) * spacing_between_cores) / cores_per_line;
let mut cores_displayed = 0;
for (i, core) in s_cores.iter().enumerate() {
if cores_displayed % cores_per_line == 0 && cores_displayed > 0 {
queue!(stdout, Print("\r\n")).unwrap();
}
if cores_displayed % cores_per_line == 0 {
print_colored_text(stdout, " ", Color::White, None, None); }
let label = format!("S{}", i + 1);
draw_bar(
stdout,
&label,
core.utilization,
100.0,
core_bar_width,
None,
);
cores_displayed += 1;
if cores_displayed % cores_per_line != 0 && cores_displayed < total_cores {
print_colored_text(stdout, " ", Color::White, None, None); }
}
for (i, core) in e_cores.iter().enumerate() {
if cores_displayed % cores_per_line == 0 && cores_displayed > 0 {
queue!(stdout, Print("\r\n")).unwrap();
}
if cores_displayed % cores_per_line == 0 {
print_colored_text(stdout, " ", Color::White, None, None); }
let label = format!("E{}", i + 1);
draw_bar(
stdout,
&label,
core.utilization,
100.0,
core_bar_width,
None,
);
cores_displayed += 1;
if cores_displayed % cores_per_line != 0 && cores_displayed < total_cores {
print_colored_text(stdout, " ", Color::White, None, None); }
}
for (i, core) in p_cores.iter().enumerate() {
if cores_displayed % cores_per_line == 0 && cores_displayed > 0 {
queue!(stdout, Print("\r\n")).unwrap();
}
if cores_displayed % cores_per_line == 0 {
print_colored_text(stdout, " ", Color::White, None, None); }
let label = format!("P{}", i + 1);
draw_bar(
stdout,
&label,
core.utilization,
100.0,
core_bar_width,
None,
);
cores_displayed += 1;
if cores_displayed % cores_per_line != 0 && cores_displayed < total_cores {
print_colored_text(stdout, " ", Color::White, None, None); }
}
for (i, core) in standard_cores.iter().enumerate() {
if cores_displayed % cores_per_line == 0 && cores_displayed > 0 {
queue!(stdout, Print("\r\n")).unwrap();
}
if cores_displayed % cores_per_line == 0 {
print_colored_text(stdout, " ", Color::White, None, None); }
let label = format!("C{}", i + 1);
draw_bar(
stdout,
&label,
core.utilization,
100.0,
core_bar_width,
None,
);
cores_displayed += 1;
if cores_displayed % cores_per_line != 0 && cores_displayed < total_cores {
print_colored_text(stdout, " ", Color::White, None, None); }
}
if cores_displayed % cores_per_line != 0 {
let remaining_cores = cores_per_line - (cores_displayed % cores_per_line);
let remaining_width =
remaining_cores * core_bar_width + (remaining_cores - 1) * spacing_between_cores;
print_colored_text(
stdout,
&" ".repeat(remaining_width + spacing_between_cores),
Color::White,
None,
None,
);
}
let total_line_width =
cores_per_line * core_bar_width + (cores_per_line - 1) * spacing_between_cores;
let right_padding = width - 5 - total_line_width;
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::{AppleSiliconCpuInfo, CoreType, CoreUtilization, CpuPlatformType};
fn make_standard_cpu(hostname: &str, 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: hostname.to_string(),
instance: String::new(),
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_silicon_cpu(hostname: &str) -> 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: hostname.to_string(),
instance: String::new(),
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(),
}
}
#[test]
fn test_print_cpu_info_with_hostname() {
let info = make_standard_cpu("myhost", 8);
let mut buf: Vec<u8> = Vec::new();
print_cpu_info(&mut buf, 0, &info, 120, false, 0, 0, true);
let output = String::from_utf8_lossy(&buf);
assert!(output.contains("myhost"));
assert!(!buf.is_empty());
}
#[test]
fn test_print_cpu_info_without_hostname() {
let info = make_standard_cpu("myhost", 8);
let mut buf: Vec<u8> = Vec::new();
print_cpu_info(&mut buf, 0, &info, 120, false, 0, 0, false);
let output = String::from_utf8_lossy(&buf);
assert!(!output.contains("@ myhost"));
assert!(!buf.is_empty());
}
#[test]
fn test_print_cpu_info_apple_silicon_with_hostname() {
let info = make_apple_silicon_cpu("mac-host");
let mut buf: Vec<u8> = Vec::new();
print_cpu_info(&mut buf, 0, &info, 120, false, 0, 0, true);
let output = String::from_utf8_lossy(&buf);
assert!(output.contains("mac-host"));
assert!(!buf.is_empty());
}
#[test]
fn test_print_cpu_info_apple_silicon_without_hostname() {
let info = make_apple_silicon_cpu("mac-host");
let mut buf: Vec<u8> = Vec::new();
print_cpu_info(&mut buf, 0, &info, 120, false, 0, 0, false);
let output = String::from_utf8_lossy(&buf);
assert!(!output.contains("@ mac-host"));
assert!(!buf.is_empty());
}
#[test]
fn test_print_cpu_info_long_hostname_scrolls() {
let info = make_standard_cpu("very-long-hostname-value", 4);
let mut buf: Vec<u8> = Vec::new();
print_cpu_info(&mut buf, 0, &info, 120, false, 0, 5, true);
assert!(!buf.is_empty());
}
#[test]
fn test_format_hostname_with_scroll_short() {
assert_eq!(format_hostname_with_scroll("host", 0), "host ");
assert_eq!(format_hostname_with_scroll("host", 99), "host ");
}
#[test]
fn test_format_hostname_with_scroll_exact_nine() {
assert_eq!(format_hostname_with_scroll("localhost", 0), "localhost");
}
#[test]
fn test_format_hostname_with_scroll_long() {
let long = "very-long-hostname";
let result = format_hostname_with_scroll(long, 0);
assert_eq!(result.len(), 9);
assert_eq!(result, "very-long");
let scroll_len = long.len() + 3;
assert_eq!(
format_hostname_with_scroll(long, scroll_len),
format_hostname_with_scroll(long, 0)
);
}
}