use crate::art::logos;
use crate::cla::{Args, Style};
use crate::style;
#[derive(Debug, Default, Clone)]
pub struct CpuReport {
pub model: String,
pub vendor: String,
pub uarch: Option<String>,
pub soc: Option<String>,
pub technology_nm: Option<u32>,
pub architecture: String,
pub physical_cores: u32,
pub logical_cores: u32,
pub max_ghz: Option<f32>,
pub base_ghz: Option<f32>,
pub l1i_kb: Option<(u32, u32)>,
pub l1d_kb: Option<(u32, u32)>,
pub l2_kb: Option<(u32, u32)>,
pub l3_kb: Option<(u32, u32)>,
pub flags: Vec<String>,
}
fn format_cache(size_kb: u32) -> String {
if size_kb >= 1000 {
format!("{:.1} MiB", size_kb as f32 / 1024.0)
} else {
format!("{} KiB", size_kb)
}
}
fn abbreviate_name(model: &str) -> String {
let without_marks = model
.replace("(R)", "")
.replace("(TM)", "")
.replace("(tm)", "");
let without_freq = match without_marks.rfind('@') {
Some(at) => without_marks[..at].to_string(),
None => without_marks,
};
without_freq
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
fn info_lines(r: &CpuReport, full_name: bool) -> Vec<(String, String)> {
let mut lines = Vec::new();
let name = if full_name {
r.model.clone()
} else {
abbreviate_name(&r.model)
};
lines.push(("Name".to_string(), name));
lines.push(("Vendor".to_string(), r.vendor.clone()));
if let Some(soc) = &r.soc {
lines.push(("SoC".to_string(), soc.clone()));
}
if let Some(uarch) = &r.uarch {
lines.push(("Microarchitecture".to_string(), uarch.clone()));
}
if let Some(nm) = r.technology_nm {
lines.push(("Technology".to_string(), format!("{}nm", nm)));
}
lines.push((
"Cores".to_string(),
format!("{} physical, {} logical", r.physical_cores, r.logical_cores),
));
if let Some(ghz) = r.max_ghz {
lines.push(("Max Frequency".to_string(), format!("{:.2} GHz", ghz)));
}
if let Some(ghz) = r.base_ghz {
lines.push(("Base Frequency".to_string(), format!("{:.2} GHz", ghz)));
}
if let Some((_, total)) = r.l1i_kb {
lines.push(("L1i Size".to_string(), format_cache(total)));
}
if let Some((_, total)) = r.l1d_kb {
lines.push(("L1d Size".to_string(), format_cache(total)));
}
if let Some((_, total)) = r.l2_kb {
lines.push(("L2 Size".to_string(), format_cache(total)));
}
if let Some((_, total)) = r.l3_kb {
lines.push(("L3 Size".to_string(), format_cache(total)));
}
if !r.flags.is_empty() {
lines.push(("Flags".to_string(), r.flags.join(" ")));
}
lines
}
fn terminal_width() -> usize {
std::env::var("COLUMNS")
.ok()
.and_then(|c| c.parse().ok())
.unwrap_or(80)
}
fn wrap_value(value: &str, avail: usize) -> Vec<String> {
if avail == 0 || value.len() <= avail {
return vec![value.to_string()];
}
let mut lines = Vec::new();
let mut current = String::new();
for word in value.split_whitespace() {
if current.is_empty() {
current.push_str(word);
} else if current.len() + 1 + word.len() <= avail {
current.push(' ');
current.push_str(word);
} else {
lines.push(std::mem::take(&mut current));
current = word.to_string();
}
}
if !current.is_empty() {
lines.push(current);
}
lines
}
fn logo_vendor_id(r: &CpuReport, logo_override: Option<&str>) -> String {
if let Some(v) = logo_override {
return v.to_string();
}
match r.vendor.as_str() {
v if v.eq_ignore_ascii_case("Intel") => "GenuineIntel".to_string(),
v if v.eq_ignore_ascii_case("AMD") => "AuthenticAMD".to_string(),
v if v.eq_ignore_ascii_case("Apple") => "Apple".to_string(),
v if v.eq_ignore_ascii_case("IBM") => "PowerPC".to_string(),
_ if r.architecture.contains("arm") || r.architecture.contains("aarch64") => {
"ARM".to_string()
}
other => other.to_string(),
}
}
pub fn print(r: &CpuReport, args: &Args, logo_override: Option<&str>) {
let (style, colors, colored) =
match style::resolve(args.style, args.color.as_deref(), &r.vendor) {
Ok(v) => v,
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
};
let lines = info_lines(r, args.full_cpu_name);
let label_color = if colored {
colors.label.fg()
} else {
String::new()
};
let value_color = if colored {
colors.value.fg()
} else {
String::new()
};
let reset = if colored { style::RESET } else { "" };
let vendor_id = logo_vendor_id(r, logo_override);
let ansi_logo_colors: Vec<String> = colors.logo.iter().map(|c| c.fg()).collect();
let logo_lines = if args.no_logo {
None
} else if style == Style::Legacy {
logos::get_logo_lines_styled(&vendor_id, &ansi_logo_colors, false)
} else {
logos::get_logo_lines_styled(&vendor_id, &ansi_logo_colors, true)
};
let logo_width = logo_lines
.as_ref()
.map(|l| l.iter().map(|s| visible_width(s)).max().unwrap_or(0))
.unwrap_or(0);
let avail = terminal_width().saturating_sub(logo_width + 3);
let rendered: Vec<String> = lines
.iter()
.flat_map(|(label, value)| {
let prefix_len = label.len() + 2; let wrapped = wrap_value(value, avail.saturating_sub(prefix_len));
wrapped
.into_iter()
.enumerate()
.map(|(i, part)| {
if i == 0 {
format!("{}{}: {}{}{}", label_color, label, value_color, part, reset)
} else {
format!(
"{}{}{}{}",
" ".repeat(prefix_len),
value_color,
part,
reset
)
}
})
.collect::<Vec<_>>()
})
.collect();
if args.no_logo {
for line in &rendered {
println!("{}", line);
}
return;
}
match logo_lines {
Some(logo) => print_side_by_side(&logo, &rendered),
None => {
for line in &rendered {
println!("{}", line);
}
}
}
}
fn print_side_by_side(logo: &[String], info: &[String]) {
let logo_height = logo.len();
let info_height = info.len();
let total = logo_height.max(info_height);
let logo_pad_top = total.saturating_sub(logo_height) / 2;
let info_pad_top = total.saturating_sub(info_height) / 2;
let logo_width = logo.iter().map(|l| visible_width(l)).max().unwrap_or(0);
for i in 0..total {
let logo_line = if i >= logo_pad_top && i - logo_pad_top < logo_height {
&logo[i - logo_pad_top]
} else {
""
};
let pad = logo_width.saturating_sub(visible_width(logo_line));
let info_line = if i >= info_pad_top && i - info_pad_top < info_height {
&info[i - info_pad_top]
} else {
""
};
println!("{}{} {}", logo_line, " ".repeat(pad), info_line);
}
}
fn visible_width(s: &str) -> usize {
let mut width = 0;
let mut in_escape = false;
for ch in s.chars() {
if in_escape {
if ch == 'm' {
in_escape = false;
}
continue;
}
if ch == '\x1b' {
in_escape = true;
continue;
}
width += 1;
}
width
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_cache_uses_kib_below_1000() {
assert_eq!(format_cache(64), "64 KiB");
assert_eq!(format_cache(999), "999 KiB");
}
#[test]
fn format_cache_switches_to_mib_at_1000() {
assert_eq!(format_cache(1024), "1.0 MiB");
assert_eq!(format_cache(12288), "12.0 MiB");
}
#[test]
fn visible_width_ignores_ansi_escapes() {
assert_eq!(visible_width("\x1b[38;2;1;2;3mhello\x1b[0m"), 5);
assert_eq!(visible_width("plain"), 5);
}
}