use std::sync::{Arc, OnceLock};
use sysinfo::{Disks, System};
use crate::payload::{Bar, Body, Entry, Payload, TextData};
use super::{Fetcher, RealtimeFetcher};
pub mod dmi;
#[cfg(test)]
pub(crate) mod test_helpers;
mod info_bios;
mod info_board;
mod info_cpu;
mod info_desktop;
mod info_env;
mod info_host;
mod info_kernel;
mod info_locale;
mod info_machine;
mod info_memory;
mod info_timezone;
mod monitor_battery;
mod monitor_cpu;
mod monitor_disk;
mod monitor_host;
mod monitor_load;
mod monitor_memory;
mod monitor_processes;
mod monitor_uptime;
pub(crate) mod sprites;
pub use info_bios::SystemInfoBios;
pub use info_board::SystemInfoBoard;
pub use info_cpu::SystemInfoCpu;
pub use info_desktop::SystemInfoDesktop;
pub use info_env::SystemInfoEnv;
pub use info_host::SystemInfoHost;
pub use info_kernel::SystemInfoKernel;
pub use info_locale::SystemInfoLocale;
pub use info_machine::SystemInfoMachine;
pub use info_memory::SystemInfoMemory;
pub use info_timezone::SystemInfoTimezone;
pub use monitor_battery::SystemMonitorBattery;
pub use monitor_cpu::SystemMonitorCpu;
pub use monitor_disk::SystemMonitorDisk;
pub use monitor_host::SystemMonitorHost;
pub use monitor_load::SystemMonitorLoad;
pub use monitor_memory::SystemMonitorMemory;
pub use monitor_processes::SystemMonitorProcesses;
pub use monitor_uptime::SystemMonitorUptime;
pub fn realtime_fetchers() -> Vec<Arc<dyn RealtimeFetcher>> {
vec![
Arc::new(SystemMonitorHost::new()),
Arc::new(SystemInfoHost),
Arc::new(SystemInfoCpu),
Arc::new(SystemInfoMemory),
Arc::new(SystemInfoKernel),
Arc::new(SystemInfoMachine),
Arc::new(SystemInfoBoard),
Arc::new(SystemInfoBios),
Arc::new(SystemInfoLocale),
Arc::new(SystemInfoTimezone),
Arc::new(SystemInfoEnv),
Arc::new(SystemInfoDesktop),
Arc::new(SystemMonitorCpu::new()),
Arc::new(SystemMonitorMemory::new()),
Arc::new(SystemMonitorUptime),
Arc::new(SystemMonitorLoad),
Arc::new(SystemMonitorProcesses::new()),
Arc::new(SystemMonitorBattery::new()),
]
}
pub fn cached_fetchers() -> Vec<Arc<dyn Fetcher>> {
vec![Arc::new(SystemMonitorDisk)]
}
#[derive(Debug, Clone)]
struct CpuInfo {
model: String,
vendor: String,
frequency_mhz: u64,
}
fn cached_cpu_info() -> &'static CpuInfo {
static CACHE: OnceLock<CpuInfo> = OnceLock::new();
CACHE.get_or_init(|| {
let mut sys = System::new();
sys.refresh_cpu_usage();
let cpu = sys.cpus().first();
let frequency_mhz = cpu
.map(|c| c.frequency())
.filter(|f| *f > 0)
.or_else(cpu_mhz_from_proc_cpuinfo)
.unwrap_or(0);
CpuInfo {
model: cpu
.map(|c| c.brand().trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".into()),
vendor: cpu
.map(|c| c.vendor_id().trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "unknown".into()),
frequency_mhz,
}
})
}
fn cpu_mhz_from_proc_cpuinfo() -> Option<u64> {
if !cfg!(target_os = "linux") {
return None;
}
let text = std::fs::read_to_string("/proc/cpuinfo").ok()?;
text.lines()
.find_map(|line| {
line.strip_prefix("cpu MHz")
.and_then(|rest| rest.split(':').nth(1))
})
.and_then(|val| val.trim().parse::<f64>().ok())
.map(|mhz| mhz.round() as u64)
.filter(|m| *m > 0)
}
fn format_cpu_cores() -> String {
let physical = System::physical_core_count().unwrap_or(0);
let mut sys = System::new();
sys.refresh_cpu_usage();
let logical = sys.cpus().len();
match (physical, logical) {
(0, 0) => "unknown".into(),
(0, l) => format!("{l} threads"),
(p, l) if p == l => format!("{p} cores"),
(p, l) => format!("{p} cores / {l} threads"),
}
}
fn format_cpu_frequency(mhz: u64) -> String {
match mhz {
0 => "unknown".into(),
m if m >= 1000 => format!("{:.2} GHz", m as f64 / 1000.0),
m => format!("{m} MHz"),
}
}
#[derive(Debug, Clone, Copy)]
struct MemoryTotals {
memory: u64,
swap: u64,
}
fn cached_memory_totals() -> MemoryTotals {
static CACHE: OnceLock<MemoryTotals> = OnceLock::new();
*CACHE.get_or_init(|| {
let mut sys = System::new();
sys.refresh_memory();
MemoryTotals {
memory: sys.total_memory(),
swap: sys.total_swap(),
}
})
}
fn kernel_name() -> String {
match std::env::consts::OS {
"linux" => "Linux".into(),
"macos" => "Darwin".into(),
"windows" => "Windows NT".into(),
"freebsd" => "FreeBSD".into(),
"openbsd" => "OpenBSD".into(),
"netbsd" => "NetBSD".into(),
other if !other.is_empty() => {
let mut chars = other.chars();
chars
.next()
.map(|c| c.to_uppercase().chain(chars).collect())
.unwrap_or_else(|| other.into())
}
_ => "unknown".into(),
}
}
fn dmi_or_na(value: Option<String>) -> String {
value.unwrap_or_else(|| "n/a".into())
}
fn detect_locale() -> String {
["LC_ALL", "LC_CTYPE", "LANG"]
.iter()
.find_map(|k| std::env::var(k).ok().filter(|v| !v.is_empty()))
.unwrap_or_else(|| "C".into())
}
fn detect_timezone() -> String {
if let Some(tz) = std::env::var("TZ").ok().filter(|v| !v.is_empty()) {
return tz;
}
if let Ok(path) = std::fs::read_link("/etc/localtime") {
let s = path.to_string_lossy();
if let Some(idx) = s.find("/zoneinfo/") {
return s[idx + "/zoneinfo/".len()..].to_string();
}
if let Some(name) = path.file_name() {
return name.to_string_lossy().into_owned();
}
}
"unknown".into()
}
fn env_basename(key: &str, fallback: &str) -> String {
std::env::var(key)
.ok()
.and_then(|s| {
let trimmed = s.trim();
(!trimmed.is_empty()).then(|| {
std::path::Path::new(trimmed)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| trimmed.to_string())
})
})
.unwrap_or_else(|| fallback.into())
}
fn detect_desktop_environment() -> String {
if cfg!(target_os = "macos") {
return "Aqua".into();
}
if cfg!(target_os = "windows") {
return "Windows".into();
}
["XDG_CURRENT_DESKTOP", "DESKTOP_SESSION", "GDMSESSION"]
.iter()
.find_map(|k| std::env::var(k).ok().filter(|v| !v.is_empty()))
.map(|s| s.split(':').next().unwrap_or("").to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "tty".into())
}
fn detect_window_manager() -> String {
if cfg!(target_os = "macos") {
return "Quartz".into();
}
if cfg!(target_os = "windows") {
return "DWM".into();
}
if std::env::var("WAYLAND_DISPLAY").is_ok() {
return std::env::var("XDG_SESSION_DESKTOP")
.ok()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "wayland".into());
}
if std::env::var("DISPLAY").is_ok() {
return std::env::var("XDG_SESSION_DESKTOP")
.ok()
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "x11".into());
}
"tty".into()
}
fn detect_init_system() -> String {
if cfg!(target_os = "macos") {
return "launchd".into();
}
if cfg!(target_os = "windows") {
return "wininit".into();
}
if cfg!(target_os = "linux")
&& let Ok(comm) = std::fs::read_to_string("/proc/1/comm")
{
let name = comm.trim();
if !name.is_empty() {
return name.into();
}
}
"unknown".into()
}
fn detect_terminal() -> String {
let env = |k: &str| std::env::var(k).ok();
if env("WT_SESSION").is_some() {
return "Windows Terminal".into();
}
if env("GHOSTTY_RESOURCES_DIR").is_some() {
return "Ghostty".into();
}
if env("KITTY_WINDOW_ID").is_some() || env("TERM").as_deref() == Some("xterm-kitty") {
return "Kitty".into();
}
if env("ALACRITTY_WINDOW_ID").is_some() || env("ALACRITTY_LOG").is_some() {
return "Alacritty".into();
}
if env("WEZTERM_PANE").is_some() {
return "WezTerm".into();
}
match env("TERM_PROGRAM").as_deref() {
Some("iTerm.app") => "iTerm2".into(),
Some("Apple_Terminal") => "Terminal".into(),
Some("ghostty") => "Ghostty".into(),
Some("WezTerm") => "WezTerm".into(),
Some("Hyper") => "Hyper".into(),
Some("vscode") => "VS Code".into(),
Some(other) if !other.is_empty() => other.into(),
_ => "(unset)".into(),
}
}
fn detect_shell() -> String {
std::env::var("SHELL")
.ok()
.and_then(|s| {
std::path::Path::new(&s)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
})
.unwrap_or_else(|| "(unset)".into())
}
fn parse_options<T: serde::de::DeserializeOwned + Default>(
raw: Option<&toml::Value>,
) -> Result<T, String> {
match raw {
None => Ok(T::default()),
Some(value) => value
.clone()
.try_into::<T>()
.map_err(|e| format!("invalid options: {e}")),
}
}
fn options_placeholder(msg: &str) -> Payload {
payload(Body::Text(TextData {
value: format!("⚠ {msg}"),
}))
}
fn payload(body: Body) -> Payload {
Payload {
icon: None,
status: None,
format: None,
body,
}
}
fn entry(key: &str, value: &str) -> Entry {
Entry {
key: key.into(),
value: Some(value.into()),
status: None,
}
}
fn os_label() -> String {
System::long_os_version()
.or_else(System::name)
.unwrap_or_else(|| "unknown".into())
}
fn memory_ratio(sys: &System) -> f64 {
ratio_of(sys.used_memory(), sys.total_memory())
}
fn ratio_of(numer: u64, denom: u64) -> f64 {
if denom == 0 {
0.0
} else {
(numer as f64 / denom as f64).clamp(0.0, 1.0)
}
}
fn top_processes(sys: &System, count: usize) -> Vec<(String, f32)> {
let mut rows: Vec<(String, f32)> = sys
.processes()
.values()
.map(|p| (p.name().to_string_lossy().into_owned(), p.cpu_usage()))
.collect();
rows.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
rows.truncate(count);
rows
}
fn primary_disk(disks: &Disks) -> Option<(u64, u64)> {
disks
.iter()
.filter(|d| d.total_space() > 0)
.max_by_key(|d| d.total_space())
.map(|d| (d.total_space(), d.available_space()))
}
fn disk_bars(disks: &Disks) -> Vec<Bar> {
disks
.iter()
.filter(|d| d.total_space() > 0)
.map(|d| Bar {
label: d.mount_point().to_string_lossy().into_owned(),
value: d.total_space().saturating_sub(d.available_space()),
value_label: None,
})
.collect()
}
fn disk_label(total: u64, available: u64) -> String {
let used = total.saturating_sub(available);
format!(
"{:.0}% of {}",
ratio_of(used, total) * 100.0,
format_bytes(total)
)
}
fn format_uptime(secs: u64) -> String {
let days = secs / 86_400;
let hours = (secs % 86_400) / 3600;
let minutes = (secs % 3600) / 60;
match (days, hours, minutes) {
(0, 0, m) => format!("{m}m"),
(0, h, m) => format!("{h}h {m}m"),
(d, h, _) => format!("{d}d {h}h"),
}
}
fn format_load(v: f64) -> String {
if cfg!(windows) {
"n/a".into()
} else {
format!("{v:.2}")
}
}
#[cfg(windows)]
fn load_line(_: f64, _: f64, _: f64) -> String {
"n/a (windows)".into()
}
#[cfg(not(windows))]
fn load_line(one: f64, five: f64, fifteen: f64) -> String {
format!("{one:.2} {five:.2} {fifteen:.2}")
}
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
const TB: u64 = 1024 * GB;
fn format_bytes(bytes: u64) -> String {
if bytes >= TB {
format!("{:.1} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.0} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.0} KB", bytes as f64 / KB as f64)
} else {
format!("{bytes} B")
}
}
#[cfg(test)]
mod tests {
use super::super::Safety;
use super::info_host::HostInfoOptions as SystemOptions;
use super::monitor_battery::{BatteryOptions, BatteryTextKind, format_percent};
use super::monitor_processes::PROCESS_TOP_COUNT;
use super::test_helpers::{EnvGuard, ctx_text, detect_terminal_with};
use super::*;
use crate::render::Shape;
fn assert_realtime_contract(
fetcher: &dyn RealtimeFetcher,
expected_name: &str,
expected_shapes: &[Shape],
default_shape: Shape,
unsupported_shape: Shape,
option_count: usize,
) {
assert_eq!(fetcher.name(), expected_name);
assert_eq!(fetcher.safety(), Safety::Safe);
assert!(!fetcher.description().is_empty());
assert_eq!(fetcher.shapes(), expected_shapes);
assert_eq!(fetcher.default_shape(), default_shape);
assert_eq!(fetcher.option_schemas().len(), option_count);
expected_shapes
.iter()
.for_each(|shape| assert!(fetcher.sample_body(*shape).is_some()));
assert!(fetcher.sample_body(unsupported_shape).is_none());
}
fn assert_cached_contract(
fetcher: &dyn Fetcher,
expected_name: &str,
expected_shapes: &[Shape],
unsupported_shape: Shape,
) {
assert_eq!(fetcher.name(), expected_name);
assert_eq!(fetcher.safety(), Safety::Safe);
assert!(!fetcher.description().is_empty());
assert_eq!(fetcher.shapes(), expected_shapes);
assert_eq!(fetcher.default_shape(), expected_shapes[0]);
expected_shapes
.iter()
.for_each(|shape| assert!(fetcher.sample_body(*shape).is_some()));
assert!(fetcher.sample_body(unsupported_shape).is_none());
}
#[test]
fn system_family_registers_builtin_fetchers() {
let realtime_names: Vec<_> = realtime_fetchers()
.into_iter()
.map(|fetcher| fetcher.name().to_string())
.collect();
let cached_names: Vec<_> = cached_fetchers()
.into_iter()
.map(|fetcher| fetcher.name().to_string())
.collect();
assert_eq!(
realtime_names,
vec![
"system_monitor_host",
"system_info_host",
"system_info_cpu",
"system_info_memory",
"system_info_kernel",
"system_info_machine",
"system_info_board",
"system_info_bios",
"system_info_locale",
"system_info_timezone",
"system_info_env",
"system_info_desktop",
"system_monitor_cpu",
"system_monitor_memory",
"system_monitor_uptime",
"system_monitor_load",
"system_monitor_processes",
"system_monitor_battery",
]
);
assert_eq!(cached_names, vec!["system_monitor_disk"]);
}
#[test]
fn fetcher_contracts_cover_supported_shapes_and_samples() {
assert_realtime_contract(
&SystemMonitorHost::default(),
"system_monitor_host",
&[Shape::Entries, Shape::TextBlock],
Shape::Entries,
Shape::Ratio,
0,
);
assert_realtime_contract(
&SystemInfoHost,
"system_info_host",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemInfoCpu,
"system_info_cpu",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemInfoMemory,
"system_info_memory",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemInfoKernel,
"system_info_kernel",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemInfoMachine,
"system_info_machine",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemInfoBoard,
"system_info_board",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemInfoBios,
"system_info_bios",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemInfoLocale,
"system_info_locale",
&[Shape::Text],
Shape::Text,
Shape::Entries,
0,
);
assert_realtime_contract(
&SystemInfoTimezone,
"system_info_timezone",
&[Shape::Text],
Shape::Text,
Shape::Entries,
0,
);
assert_realtime_contract(
&SystemInfoEnv,
"system_info_env",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemInfoDesktop,
"system_info_desktop",
&[Shape::Text],
Shape::Text,
Shape::Entries,
1,
);
assert_realtime_contract(
&SystemMonitorCpu::default(),
"system_monitor_cpu",
&[Shape::Ratio, Shape::Text, Shape::PixelArt],
Shape::Ratio,
Shape::Entries,
0,
);
assert_realtime_contract(
&SystemMonitorMemory::default(),
"system_monitor_memory",
&[Shape::Ratio, Shape::Text, Shape::Entries],
Shape::Ratio,
Shape::Bars,
0,
);
assert_realtime_contract(
&SystemMonitorUptime,
"system_monitor_uptime",
&[Shape::Text],
Shape::Text,
Shape::Entries,
0,
);
assert_realtime_contract(
&SystemMonitorLoad,
"system_monitor_load",
&[Shape::Text, Shape::Entries],
Shape::Text,
Shape::Badge,
0,
);
assert_realtime_contract(
&SystemMonitorProcesses::default(),
"system_monitor_processes",
&[Shape::Entries, Shape::TextBlock],
Shape::Entries,
Shape::Ratio,
0,
);
assert_realtime_contract(
&SystemMonitorBattery::default(),
"system_monitor_battery",
&[
Shape::Ratio,
Shape::Text,
Shape::Entries,
Shape::Badge,
Shape::PixelArt,
],
Shape::Ratio,
Shape::Bars,
2,
);
assert_cached_contract(
&SystemMonitorDisk,
"system_monitor_disk",
&[Shape::Ratio, Shape::Text, Shape::Bars],
Shape::Entries,
);
}
#[test]
fn system_sample_body_matches_documented_copy() {
let fetcher = SystemMonitorHost::default();
assert!(matches!(
fetcher.sample_body(Shape::Entries),
Some(Body::Entries(entries))
if entries.items.len() == 6
&& entries.items[0].key == "os"
&& entries.items[5].value.as_deref() == Some("67%")
));
assert!(matches!(
fetcher.sample_body(Shape::TextBlock),
Some(Body::TextBlock(block))
if block.lines[0] == "os: linux" && block.lines[5] == "memory: 67%"
));
assert!(fetcher.sample_body(Shape::Text).is_none());
assert!(matches!(
SystemInfoHost.sample_body(Shape::Text),
Some(Body::Text(text)) if text.value == "iTerm2"
));
}
#[test]
fn sample_bodies_return_none_for_unsupported_shapes() {
let unsupported = Shape::Image;
assert!(SystemMonitorHost::new().sample_body(unsupported).is_none());
assert!(SystemMonitorCpu::new().sample_body(unsupported).is_none());
assert!(
SystemMonitorMemory::new()
.sample_body(unsupported)
.is_none()
);
assert!(SystemMonitorUptime.sample_body(unsupported).is_none());
assert!(SystemMonitorLoad.sample_body(unsupported).is_none());
assert!(
SystemMonitorProcesses::new()
.sample_body(unsupported)
.is_none()
);
assert!(SystemMonitorDisk.sample_body(unsupported).is_none());
assert!(
SystemMonitorBattery::new()
.sample_body(unsupported)
.is_none()
);
}
#[test]
fn parse_options_defaults_and_surfaces_invalid_input() {
let system: SystemOptions = parse_options(None).unwrap();
assert!(system.kind.is_none());
let battery_raw: toml::Value = toml::from_str("kind = \"percent\"\nindex = 2").unwrap();
let battery: BatteryOptions = parse_options(Some(&battery_raw)).unwrap();
assert!(matches!(battery.kind, Some(BatteryTextKind::Percent)));
assert_eq!(battery.index, Some(2));
let invalid: toml::Value = toml::from_str("bogus = true").unwrap();
let err = parse_options::<BatteryOptions>(Some(&invalid)).unwrap_err();
assert!(err.starts_with("invalid options:"));
}
#[test]
fn options_placeholder_wraps_the_message_in_warning_text() {
let Body::Text(text) = options_placeholder("bad config").body else {
panic!("expected text body");
};
assert_eq!(text.value, "⚠ bad config");
}
#[test]
fn detect_terminal_prefers_known_env_markers_and_fallbacks() {
assert_eq!(
detect_terminal_with(&[("WT_SESSION", "1"), ("TERM_PROGRAM", "Hyper")]),
"Windows Terminal"
);
assert_eq!(
detect_terminal_with(&[("GHOSTTY_RESOURCES_DIR", "/tmp/resources")]),
"Ghostty"
);
assert_eq!(detect_terminal_with(&[("TERM", "xterm-kitty")]), "Kitty");
assert_eq!(
detect_terminal_with(&[("ALACRITTY_LOG", "/tmp/alacritty.log")]),
"Alacritty"
);
assert_eq!(detect_terminal_with(&[("WEZTERM_PANE", "pane")]), "WezTerm");
assert_eq!(
detect_terminal_with(&[("TERM_PROGRAM", "vscode")]),
"VS Code"
);
assert_eq!(
detect_terminal_with(&[("TERM_PROGRAM", "CustomTerm")]),
"CustomTerm"
);
assert_eq!(detect_terminal_with(&[]), "(unset)");
}
#[test]
fn detect_terminal_covers_remaining_aliases_and_priorities() {
assert_eq!(
detect_terminal_with(&[("KITTY_WINDOW_ID", "7"), ("TERM_PROGRAM", "iTerm.app")]),
"Kitty"
);
assert_eq!(
detect_terminal_with(&[("ALACRITTY_WINDOW_ID", "9")]),
"Alacritty"
);
assert_eq!(
detect_terminal_with(&[("TERM_PROGRAM", "iTerm.app")]),
"iTerm2"
);
assert_eq!(
detect_terminal_with(&[("TERM_PROGRAM", "Apple_Terminal")]),
"Terminal"
);
assert_eq!(
detect_terminal_with(&[("TERM_PROGRAM", "ghostty")]),
"Ghostty"
);
assert_eq!(
detect_terminal_with(&[("TERM_PROGRAM", "WezTerm")]),
"WezTerm"
);
}
#[test]
fn detect_shell_uses_basename_and_fallback() {
let _guard = EnvGuard::set(&[("SHELL", Some("/usr/local/bin/fish"))]);
assert_eq!(detect_shell(), "fish");
drop(_guard);
let _guard = EnvGuard::set(&[("SHELL", None)]);
assert_eq!(detect_shell(), "(unset)");
}
#[test]
fn format_uptime_covers_minute_hour_day_ranges() {
assert_eq!(format_uptime(0), "0m");
assert_eq!(format_uptime(45 * 60), "45m");
assert_eq!(format_uptime(2 * 3600 + 15 * 60), "2h 15m");
assert_eq!(format_uptime(3 * 86_400 + 4 * 3600 + 30 * 60), "3d 4h");
}
#[test]
fn format_bytes_buckets_by_unit() {
assert_eq!(format_bytes(0), "0 B");
assert_eq!(format_bytes(512), "512 B");
assert_eq!(format_bytes(4 * KB), "4 KB");
assert_eq!(format_bytes(250 * MB), "250 MB");
assert_eq!(format_bytes(2 * GB + GB / 2), "2.5 GB");
assert_eq!(format_bytes(3 * TB), "3.0 TB");
}
#[test]
fn ratio_of_handles_zero_denominator() {
assert_eq!(ratio_of(10, 0), 0.0);
assert_eq!(ratio_of(5, 10), 0.5);
assert_eq!(ratio_of(20, 10), 1.0);
}
#[test]
fn empty_system_and_disk_snapshots_use_safe_fallbacks() {
let sys = System::new();
assert_eq!(memory_ratio(&sys), 0.0);
assert!(top_processes(&sys, PROCESS_TOP_COUNT).is_empty());
let disks = Disks::new();
assert_eq!(primary_disk(&disks), None);
assert!(disk_bars(&disks).is_empty());
assert_eq!(disk_label(10, 20), "0% of 10 B");
}
#[test]
fn helper_formatters_and_builders_cover_remaining_branches() {
assert_eq!(format_percent(-1.0), "0%");
assert_eq!(format_percent(0.5), "50%");
assert_eq!(format_percent(2.0), "100%");
assert_eq!(disk_label(200, 50), "75% of 200 B");
#[cfg(not(windows))]
{
assert_eq!(format_load(0.42), "0.42");
assert_eq!(load_line(0.42, 0.38, 0.31), "0.42 0.38 0.31");
}
#[cfg(windows)]
{
assert_eq!(format_load(0.42), "n/a");
assert_eq!(load_line(0.42, 0.38, 0.31), "n/a (windows)");
}
let row = entry("cpu", "18%");
assert_eq!(row.key, "cpu");
assert_eq!(row.value.as_deref(), Some("18%"));
assert!(row.status.is_none());
let wrapped = payload(Body::Text(TextData {
value: "hello".into(),
}));
assert!(wrapped.icon.is_none());
assert!(wrapped.status.is_none());
assert!(wrapped.format.is_none());
assert!(matches!(wrapped.body, Body::Text(TextData { value }) if value == "hello"));
}
#[test]
fn detect_terminal_returns_non_empty_label() {
assert!(!detect_terminal().is_empty());
}
#[test]
#[ignore]
fn live_system_text_all_kinds() {
let cases: Vec<(&str, &dyn RealtimeFetcher, Option<&str>)> = vec![
(
"host:terminal",
&SystemInfoHost,
Some("kind = \"terminal\""),
),
("host:os", &SystemInfoHost, Some("kind = \"os\"")),
(
"host:os_version",
&SystemInfoHost,
Some("kind = \"os_version\""),
),
(
"host:hostname",
&SystemInfoHost,
Some("kind = \"hostname\""),
),
("host:shell", &SystemInfoHost, Some("kind = \"shell\"")),
("host:arch", &SystemInfoHost, Some("kind = \"arch\"")),
("cpu:model", &SystemInfoCpu, Some("kind = \"model\"")),
("cpu:cores", &SystemInfoCpu, Some("kind = \"cores\"")),
(
"cpu:frequency",
&SystemInfoCpu,
Some("kind = \"frequency\""),
),
("cpu:vendor", &SystemInfoCpu, Some("kind = \"vendor\"")),
("memory:total", &SystemInfoMemory, Some("kind = \"total\"")),
(
"memory:swap_total",
&SystemInfoMemory,
Some("kind = \"swap_total\""),
),
("kernel:name", &SystemInfoKernel, Some("kind = \"name\"")),
(
"kernel:version",
&SystemInfoKernel,
Some("kind = \"version\""),
),
(
"machine:model",
&SystemInfoMachine,
Some("kind = \"model\""),
),
(
"machine:vendor",
&SystemInfoMachine,
Some("kind = \"vendor\""),
),
(
"machine:serial",
&SystemInfoMachine,
Some("kind = \"serial\""),
),
(
"machine:chassis",
&SystemInfoMachine,
Some("kind = \"chassis\""),
),
("board:vendor", &SystemInfoBoard, Some("kind = \"vendor\"")),
("board:model", &SystemInfoBoard, Some("kind = \"model\"")),
("bios:vendor", &SystemInfoBios, Some("kind = \"vendor\"")),
("bios:version", &SystemInfoBios, Some("kind = \"version\"")),
("bios:date", &SystemInfoBios, Some("kind = \"date\"")),
("locale", &SystemInfoLocale, None),
("timezone", &SystemInfoTimezone, None),
("env:editor", &SystemInfoEnv, Some("kind = \"editor\"")),
("env:visual", &SystemInfoEnv, Some("kind = \"visual\"")),
("env:pager", &SystemInfoEnv, Some("kind = \"pager\"")),
("desktop:de", &SystemInfoDesktop, Some("kind = \"de\"")),
("desktop:wm", &SystemInfoDesktop, Some("kind = \"wm\"")),
("desktop:init", &SystemInfoDesktop, Some("kind = \"init\"")),
];
for (label, fetcher, opts) in cases {
let p = fetcher.compute(&ctx_text(opts));
assert!(matches!(p.body, Body::Text(_)), "expected text for {label}");
if let Body::Text(t) = p.body {
eprintln!("{label:<18} → {}", t.value);
assert!(!t.value.is_empty());
}
}
}
#[test]
fn kernel_name_maps_consts_os() {
let name = kernel_name();
assert!(!name.is_empty());
match std::env::consts::OS {
"linux" => assert_eq!(name, "Linux"),
"macos" => assert_eq!(name, "Darwin"),
"windows" => assert_eq!(name, "Windows NT"),
_ => {}
}
}
#[test]
fn format_cpu_frequency_buckets_units() {
assert_eq!(format_cpu_frequency(0), "unknown");
assert_eq!(format_cpu_frequency(800), "800 MHz");
assert_eq!(format_cpu_frequency(2400), "2.40 GHz");
assert_eq!(format_cpu_frequency(4250), "4.25 GHz");
}
#[test]
fn dmi_or_na_falls_back_when_unavailable() {
assert_eq!(dmi_or_na(None), "n/a");
assert_eq!(dmi_or_na(Some("Dell Inc.".into())), "Dell Inc.");
}
#[test]
fn env_basename_uses_filename_then_fallback() {
let _guard = EnvGuard::set(&[("EDITOR", Some("/usr/bin/nvim"))]);
assert_eq!(env_basename("EDITOR", "(unset)"), "nvim");
drop(_guard);
let _guard = EnvGuard::set(&[("EDITOR", None)]);
assert_eq!(env_basename("EDITOR", "(unset)"), "(unset)");
}
#[test]
#[cfg(unix)]
fn env_basename_returns_trimmed_path_when_file_name_is_empty() {
let _guard = EnvGuard::set(&[("EDITOR", Some("/"))]);
assert_eq!(env_basename("EDITOR", "(unset)"), "/");
}
#[test]
fn detect_terminal_recognises_hyper_term_program() {
assert_eq!(detect_terminal_with(&[("TERM_PROGRAM", "Hyper")]), "Hyper");
}
#[test]
#[cfg(target_os = "linux")]
fn cpu_mhz_from_proc_cpuinfo_returns_some_on_linux() {
let _ = cpu_mhz_from_proc_cpuinfo();
}
#[test]
#[cfg(not(target_os = "linux"))]
fn cpu_mhz_from_proc_cpuinfo_is_none_off_linux() {
assert!(cpu_mhz_from_proc_cpuinfo().is_none());
}
#[test]
fn detect_locale_prefers_lc_all_then_lang() {
let _guard = EnvGuard::set(&[
("LC_ALL", Some("en_US.UTF-8")),
("LC_CTYPE", None),
("LANG", Some("ja_JP.UTF-8")),
]);
assert_eq!(detect_locale(), "en_US.UTF-8");
drop(_guard);
let _guard = EnvGuard::set(&[
("LC_ALL", None),
("LC_CTYPE", None),
("LANG", Some("ja_JP.UTF-8")),
]);
assert_eq!(detect_locale(), "ja_JP.UTF-8");
drop(_guard);
let _guard = EnvGuard::set(&[("LC_ALL", None), ("LC_CTYPE", None), ("LANG", None)]);
assert_eq!(detect_locale(), "C");
}
#[test]
fn detect_timezone_honors_tz_env_override() {
let _guard = EnvGuard::set(&[("TZ", Some("Asia/Tokyo"))]);
assert_eq!(detect_timezone(), "Asia/Tokyo");
}
#[test]
fn detect_timezone_falls_back_without_tz_env() {
let _guard = EnvGuard::set(&[("TZ", None)]);
assert!(!detect_timezone().is_empty());
}
#[test]
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn detect_desktop_environment_strips_colon_and_falls_through() {
let _guard = EnvGuard::set(&[
("XDG_CURRENT_DESKTOP", Some("GNOME:Unity")),
("DESKTOP_SESSION", Some("kde")),
("GDMSESSION", None),
]);
assert_eq!(detect_desktop_environment(), "GNOME");
drop(_guard);
let _guard = EnvGuard::set(&[
("XDG_CURRENT_DESKTOP", Some("")),
("DESKTOP_SESSION", Some("plasma")),
("GDMSESSION", None),
]);
assert_eq!(detect_desktop_environment(), "plasma");
drop(_guard);
let _guard = EnvGuard::set(&[
("XDG_CURRENT_DESKTOP", None),
("DESKTOP_SESSION", None),
("GDMSESSION", None),
]);
assert_eq!(detect_desktop_environment(), "tty");
}
#[test]
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn detect_window_manager_separates_wayland_x11_and_tty() {
let _guard = EnvGuard::set(&[
("WAYLAND_DISPLAY", Some("wayland-0")),
("DISPLAY", None),
("XDG_SESSION_DESKTOP", Some("sway")),
]);
assert_eq!(detect_window_manager(), "sway");
drop(_guard);
let _guard = EnvGuard::set(&[
("WAYLAND_DISPLAY", Some("wayland-0")),
("DISPLAY", None),
("XDG_SESSION_DESKTOP", None),
]);
assert_eq!(detect_window_manager(), "wayland");
drop(_guard);
let _guard = EnvGuard::set(&[
("WAYLAND_DISPLAY", None),
("DISPLAY", Some(":0")),
("XDG_SESSION_DESKTOP", Some("i3")),
]);
assert_eq!(detect_window_manager(), "i3");
drop(_guard);
let _guard = EnvGuard::set(&[
("WAYLAND_DISPLAY", None),
("DISPLAY", Some(":0")),
("XDG_SESSION_DESKTOP", None),
]);
assert_eq!(detect_window_manager(), "x11");
drop(_guard);
let _guard = EnvGuard::set(&[
("WAYLAND_DISPLAY", None),
("DISPLAY", None),
("XDG_SESSION_DESKTOP", None),
]);
assert_eq!(detect_window_manager(), "tty");
}
#[test]
#[cfg(target_os = "linux")]
fn detect_init_system_reads_proc_one_comm_on_linux() {
assert!(!detect_init_system().is_empty());
}
#[test]
#[cfg(any(target_os = "macos", target_os = "windows"))]
fn detect_init_system_returns_platform_constant() {
let name = detect_init_system();
assert!(matches!(name.as_str(), "launchd" | "wininit"));
}
#[test]
fn cpu_info_cache_returns_non_empty_fields() {
let info = cached_cpu_info();
assert!(!info.model.is_empty());
assert!(!info.vendor.is_empty());
}
#[test]
fn memory_totals_cache_reports_nonzero_memory() {
let totals = cached_memory_totals();
assert!(totals.memory > 0);
}
}