1use crate::gpu;
10use chrono::TimeZone;
11use sysinfo::{Components, System, Users};
12
13#[derive(Debug, Default, Clone)]
18pub struct CollectOptions {
19 pub long: bool,
21 pub fields: Option<Vec<String>>,
23 pub weather_location: Option<String>,
25}
26
27#[derive(Debug)]
32pub struct SystemInfo {
33 pub os: String,
35 pub kernel: Option<String>,
37 pub hostname: Option<String>,
39 pub arch: String,
41 pub cpu: String,
43 pub cpu_cores: usize,
45 pub cpu_core_info: String,
47 pub memory: String,
49 pub swap: String,
51 pub uptime: String,
53 pub processes: usize,
55 pub load_avg: Option<String>,
57 pub disks: Vec<String>,
59 pub temps: Vec<String>,
61 pub networks: Vec<String>,
63 pub boot_time: String,
65 pub battery: Option<String>,
67 pub shell: Option<String>,
69 pub terminal: Option<String>,
71 pub desktop: Option<String>,
73 pub cpu_freq: Option<String>,
75 pub users: usize,
77 pub gpu: Vec<String>,
79 pub packages: Option<usize>,
81 pub current_user: Option<String>,
83 pub local_ip: Option<String>,
85 pub public_ip: Option<String>,
87 pub active_interface: Option<String>,
89 pub motherboard: Option<String>,
91 pub bios: Option<String>,
93 pub displays: Vec<String>,
95 pub audio: Option<String>,
97 pub wifi: Option<String>,
99 pub bluetooth: Option<String>,
101 pub ui_theme: Option<String>,
103 pub icons: Option<String>,
105 pub cursor: Option<String>,
107 pub font: Option<String>,
109 pub terminal_font: Option<String>,
111 pub camera: Vec<String>,
113 pub gamepad: Vec<String>,
115 pub cpu_cache: Option<String>,
117 pub cpu_usage: Option<String>,
119 pub physical_disks: Vec<String>,
121 pub physical_memory: Option<String>,
123 pub init_system: Option<String>,
125 pub chassis: Option<String>,
127 pub locale: Option<String>,
129 pub bootmgr: Option<String>,
131 pub editor: Option<String>,
133 pub weather: Option<String>,
135}
136
137impl SystemInfo {
138 pub fn collect(opts: CollectOptions) -> anyhow::Result<Self> {
143 let should_collect = |field_name: &str| -> bool {
144 if opts.long {
145 return true;
146 }
147 match &opts.fields {
148 Some(fields) => {
149 let norm_field = field_name.to_lowercase().replace(['-', '_'], " ");
150 let norm_field_no_spaces = norm_field.replace(' ', "");
151 fields.iter().any(|f| {
152 let norm_f = f.to_lowercase().replace(['-', '_'], " ");
153 norm_f == norm_field || norm_f.replace(' ', "") == norm_field_no_spaces
154 })
155 }
156 None => true,
157 }
158 };
159
160 let mut refresh_kind = sysinfo::RefreshKind::nothing();
161 if should_collect("cpu")
162 || should_collect("cpu usage")
163 || should_collect("cpu-usage")
164 || should_collect("cpu cache")
165 || should_collect("cpu-cache")
166 {
167 refresh_kind = refresh_kind.with_cpu(sysinfo::CpuRefreshKind::everything());
168 }
169 if should_collect("memory")
170 || should_collect("swap")
171 || should_collect("phys mem")
172 || should_collect("phys-mem")
173 {
174 refresh_kind = refresh_kind.with_memory(sysinfo::MemoryRefreshKind::everything());
175 }
176 if should_collect("procs") || should_collect("audio") {
177 refresh_kind = refresh_kind.with_processes(sysinfo::ProcessRefreshKind::nothing());
178 }
179
180 let mut sys = System::new_with_specifics(refresh_kind);
181
182 let os = System::long_os_version()
183 .or_else(System::name)
184 .unwrap_or_else(|| "Unknown".to_string());
185
186 let kernel = System::kernel_version();
187 let hostname = System::host_name();
188
189 let cpu = if should_collect("cpu") {
190 sys.cpus()
191 .first()
192 .map(|c| c.brand().to_string())
193 .unwrap_or_else(|| "Unknown CPU".to_string())
194 } else {
195 String::new()
196 };
197
198 let cpu_cores = if should_collect("cpu") {
199 sys.cpus().len()
200 } else {
201 0
202 };
203 let cpu_core_info = if should_collect("cpu") {
204 format_cpu_cores(cpu_cores, System::physical_core_count())
205 } else {
206 String::new()
207 };
208
209 let memory = if should_collect("memory") {
210 let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
211 let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
212 format!("{:.1} / {:.1} GB", used_mem, total_mem)
213 } else {
214 String::new()
215 };
216
217 let swap = if should_collect("swap") {
218 let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
219 let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
220 if total_swap > 0.0 {
221 format!("{:.1} / {:.1} GB", used_swap, total_swap)
222 } else {
223 "No swap".to_string()
224 }
225 } else {
226 String::new()
227 };
228
229 let uptime = format!("{}s", System::uptime());
230
231 let disks: Vec<String> = if should_collect("disk") {
232 let disks_list = crate::disk::detect_logical_disks();
233 let format_disk = |(mount, total, avail, fs): &(String, u64, u64, String)| {
234 let total_gb = *total as f64 / 1024.0 / 1024.0 / 1024.0;
235 let avail_gb = *avail as f64 / 1024.0 / 1024.0 / 1024.0;
236 format!(
237 "{} ({}): {:.1} GB free / {:.1} GB",
238 mount, fs, avail_gb, total_gb
239 )
240 };
241 if !opts.long {
242 let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
243 let home_path = std::path::Path::new(&home);
244 let best = disks_list
245 .iter()
246 .filter(|(mp, ..)| home_path.starts_with(mp))
247 .max_by_key(|(mp, ..)| std::path::Path::new(mp).components().count());
248 if let Some(disk) = best {
249 vec![format_disk(disk)]
250 } else {
251 disks_list.iter().map(format_disk).collect()
252 }
253 } else {
254 disks_list.iter().map(format_disk).collect()
255 }
256 } else {
257 Vec::new()
258 };
259
260 let battery = if should_collect("battery") {
261 crate::battery::get_battery_info().map(|bat| {
262 let pct = bat.percentage;
263 let state = match bat.state {
264 crate::battery::BatteryState::Charging => "charging",
265 crate::battery::BatteryState::Discharging => "discharging",
266 crate::battery::BatteryState::Full => "full",
267 _ => "not charging",
268 };
269 let vendor = bat.vendor;
270 let model = bat.model;
271
272 let time_str = match bat.state {
274 crate::battery::BatteryState::Charging => bat.time_remaining.map(|d| {
275 let total_mins = d.as_secs() / 60;
276 let hours = total_mins / 60;
277 let mins = total_mins % 60;
278 if hours >= 24 {
279 let days = hours / 24;
280 let rem_hours = hours % 24;
281 format!("{}d {}h until full", days, rem_hours)
282 } else if hours > 0 {
283 format!("{}h {}m until full", hours, mins)
284 } else {
285 format!("{}m until full", mins)
286 }
287 }),
288 crate::battery::BatteryState::Discharging => bat.time_remaining.map(|d| {
289 let total_mins = d.as_secs() / 60;
290 let hours = total_mins / 60;
291 let mins = total_mins % 60;
292 if hours >= 24 {
293 let days = hours / 24;
294 let rem_hours = hours % 24;
295 format!("{}d {}h remaining", days, rem_hours)
296 } else if hours > 0 {
297 format!("{}h {}m remaining", hours, mins)
298 } else {
299 format!("{}m remaining", mins)
300 }
301 }),
302 _ => None,
303 };
304
305 let mut parts = vec![state.to_string()];
306 if let Some(t) = time_str {
307 parts.insert(0, t);
308 }
309 if let Some(health) = bat.health {
310 if health < 99.0 {
311 parts.push(format!("{:.0}% health", health));
312 }
313 }
314
315 let base = format!("{:.0}% ({})", pct, parts.join(", "));
316
317 match (vendor, model) {
318 (Some(v), Some(m)) => format!("{} [{} {}]", base, v, m),
319 (Some(v), None) => format!("{} [{}]", base, v),
320 _ => base,
321 }
322 })
323 } else {
324 None
325 };
326
327 let arch = System::cpu_arch();
328
329 let processes = if should_collect("procs") || should_collect("audio") {
330 sys.processes().len()
331 } else {
332 0
333 };
334
335 let load_avg = {
336 let avg = System::load_average();
337 if avg.one > 0.0 || avg.five > 0.0 {
338 Some(format!(
339 "{:.2}, {:.2}, {:.2}",
340 avg.one, avg.five, avg.fifteen
341 ))
342 } else {
343 None
344 }
345 };
346
347 let (
349 gpu,
350 packages,
351 public_ip,
352 (local_ip, active_interface),
353 motherboard,
354 bios,
355 displays,
356 audio,
357 wifi,
358 bluetooth,
359 (ui_theme, icons, cursor, font),
360 camera,
361 gamepad,
362 physical_disks,
363 physical_memory,
364 weather,
365 ) = std::thread::scope(|s| {
366 let gpu_handle = if should_collect("gpu") {
367 Some(s.spawn(|| {
368 gpu::detect_gpus()
369 .into_iter()
370 .map(|g| g.format())
371 .collect::<Vec<String>>()
372 }))
373 } else {
374 None
375 };
376 let packages_handle = if should_collect("packages") {
377 Some(s.spawn(crate::packages::detect_packages))
378 } else {
379 None
380 };
381 let public_ip_handle = if should_collect("public ip") {
382 Some(s.spawn(crate::network::detect_public_ip))
383 } else {
384 None
385 };
386 let network_ips_handle = if should_collect("net") {
387 Some(s.spawn(crate::network::detect_active_interface_and_local_ip))
388 } else {
389 None
390 };
391 let motherboard_handle = if should_collect("motherboard") {
392 Some(s.spawn(crate::motherboard::detect_motherboard))
393 } else {
394 None
395 };
396 let bios_handle = if should_collect("bios") {
397 Some(s.spawn(crate::bios::detect_bios))
398 } else {
399 None
400 };
401 let displays_handle = if should_collect("display") {
402 Some(s.spawn(crate::display::detect_displays))
403 } else {
404 None
405 };
406 let audio_handle = if should_collect("audio") {
407 Some(s.spawn(|| crate::audio::detect_audio(&sys)))
408 } else {
409 None
410 };
411 let wifi_handle = if should_collect("wifi") {
412 Some(s.spawn(crate::network::detect_wifi))
413 } else {
414 None
415 };
416 let bluetooth_handle = if should_collect("bluetooth") {
417 Some(s.spawn(crate::bluetooth::detect_bluetooth))
418 } else {
419 None
420 };
421 let ui_theme_and_fonts_handle = if should_collect("theme")
422 || should_collect("icons")
423 || should_collect("cursor")
424 || should_collect("font")
425 {
426 Some(s.spawn(crate::theme::detect_ui_theme_and_fonts))
427 } else {
428 None
429 };
430 let camera_handle = if should_collect("camera") {
431 Some(s.spawn(crate::camera::detect_camera))
432 } else {
433 None
434 };
435 let gamepad_handle = if should_collect("gamepad") {
436 Some(s.spawn(crate::gamepad::detect_gamepad))
437 } else {
438 None
439 };
440 let physical_disks_handle = if should_collect("phys disk") {
441 Some(s.spawn(crate::disk::detect_physical_disks))
442 } else {
443 None
444 };
445 let physical_memory_handle = if should_collect("phys mem") {
446 Some(s.spawn(crate::memory::detect_physical_memory))
447 } else {
448 None
449 };
450 let weather_location = opts.weather_location.clone();
451 let weather_handle = if should_collect("weather") {
452 Some(s.spawn(move || crate::weather::detect_weather(weather_location.as_deref())))
453 } else {
454 None
455 };
456
457 (
458 gpu_handle
459 .map(|h| h.join().unwrap_or_default())
460 .unwrap_or_default(),
461 packages_handle.and_then(|h| h.join().ok().flatten()),
462 public_ip_handle.and_then(|h| h.join().ok().flatten()),
463 network_ips_handle
464 .map(|h| h.join().unwrap_or((None, None)))
465 .unwrap_or((None, None)),
466 motherboard_handle.and_then(|h| h.join().ok().flatten()),
467 bios_handle.and_then(|h| h.join().ok().flatten()),
468 displays_handle
469 .map(|h| h.join().unwrap_or_default())
470 .unwrap_or_default(),
471 audio_handle.and_then(|h| h.join().ok().flatten()),
472 wifi_handle.and_then(|h| h.join().ok().flatten()),
473 bluetooth_handle.and_then(|h| h.join().ok().flatten()),
474 ui_theme_and_fonts_handle
475 .map(|h| h.join().unwrap_or((None, None, None, None)))
476 .unwrap_or((None, None, None, None)),
477 camera_handle
478 .map(|h| h.join().unwrap_or_default())
479 .unwrap_or_default(),
480 gamepad_handle
481 .map(|h| h.join().unwrap_or_default())
482 .unwrap_or_default(),
483 physical_disks_handle
484 .map(|h| h.join().unwrap_or_default())
485 .unwrap_or_default(),
486 physical_memory_handle.and_then(|h| h.join().ok().flatten()),
487 weather_handle.and_then(|h| h.join().ok().flatten()),
488 )
489 });
490
491 let mut temps: Vec<String> = if should_collect("temp") {
492 Components::new_with_refreshed_list()
493 .iter()
494 .filter_map(|c| {
495 c.temperature().and_then(|t| {
496 if t > 0.0 {
497 Some(format!("{}: {:.0}°C", c.label(), t))
498 } else {
499 None
500 }
501 })
502 })
503 .collect()
504 } else {
505 Vec::new()
506 };
507
508 temps.sort_by(|a, b| {
510 let a_cpu = a.to_lowercase().contains("cpu") || a.to_lowercase().contains("core");
511 let b_cpu = b.to_lowercase().contains("cpu") || b.to_lowercase().contains("core");
512 b_cpu.cmp(&a_cpu)
513 });
514
515 let networks = if should_collect("net") {
516 crate::network::detect_networks(active_interface.as_deref(), local_ip.as_deref())
517 } else {
518 Vec::new()
519 };
520
521 let boot_timestamp = System::boot_time();
522 let boot_dt = chrono::Local
523 .timestamp_opt(boot_timestamp as i64, 0)
524 .single()
525 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
526 .unwrap_or_else(|| boot_timestamp.to_string());
527 let boot_time = boot_dt;
528
529 let shell = if should_collect("shell") {
531 crate::shell::detect_shell(&sys)
532 } else {
533 None
534 };
535 let terminal = if should_collect("terminal") {
536 crate::terminal::detect_terminal(&sys)
537 } else {
538 None
539 };
540 let terminal_font = if should_collect("terminal font")
541 || should_collect("terminal-font")
542 || should_collect("terminal_font")
543 {
544 crate::terminal::detect_terminal_font(terminal.as_deref())
545 } else {
546 None
547 };
548 let desktop = if should_collect("desktop") {
549 std::env::var("XDG_CURRENT_DESKTOP")
550 .or_else(|_| std::env::var("DESKTOP_SESSION"))
551 .ok()
552 } else {
553 None
554 };
555
556 let cpu_freq = if should_collect("cpu-freq")
558 || should_collect("cpu freq")
559 || should_collect("cpu_freq")
560 {
561 sys.cpus().first().map(|c| {
562 let current = format!("{:.2} GHz", c.frequency() as f64 / 1000.0);
563 if let Some((min_khz, max_khz)) = detect_cpu_freq_range() {
564 let min_ghz = min_khz as f64 / 1_000_000.0;
565 let max_ghz = max_khz as f64 / 1_000_000.0;
566 format!("{} ({:.2} \u{2013} {:.2} GHz)", current, min_ghz, max_ghz)
567 } else {
568 current
569 }
570 })
571 } else {
572 None
573 };
574
575 let cpu_cache = if should_collect("cpu-cache")
577 || should_collect("cpu cache")
578 || should_collect("cpu_cache")
579 {
580 detect_cpu_cache()
581 } else {
582 None
583 };
584
585 let cpu_usage = if should_collect("cpu-usage")
587 || should_collect("cpu usage")
588 || should_collect("cpu_usage")
589 {
590 std::thread::sleep(std::time::Duration::from_millis(200));
591 sys.refresh_cpu_usage();
592 let usage: f32 =
593 sys.cpus().iter().map(|c| c.cpu_usage()).sum::<f32>() / sys.cpus().len() as f32;
594 #[cfg(not(target_os = "windows"))]
595 {
596 let avg = System::load_average();
597 let load_str = format!("{:.2}, {:.2}, {:.2}", avg.one, avg.five, avg.fifteen);
598 if usage > 0.0 {
599 Some(format!("{:.1}% (load: {})", usage, load_str))
600 } else if avg.one > 0.0 {
601 Some(format!("load: {}", load_str))
602 } else {
603 None
604 }
605 }
606 #[cfg(target_os = "windows")]
607 {
608 if usage > 0.0 {
609 Some(format!("{:.1}%", usage))
610 } else {
611 None
612 }
613 }
614 } else {
615 None
616 };
617
618 let init_system = if should_collect("init") || should_collect("init system") {
619 detect_init_system()
620 } else {
621 None
622 };
623
624 let chassis = if should_collect("chassis") {
625 detect_chassis()
626 } else {
627 None
628 };
629
630 let locale = if should_collect("locale") {
631 std::env::var("LC_ALL")
632 .ok()
633 .filter(|s| !s.is_empty())
634 .or_else(|| std::env::var("LC_MESSAGES").ok().filter(|s| !s.is_empty()))
635 .or_else(|| std::env::var("LANG").ok().filter(|s| !s.is_empty()))
636 } else {
637 None
638 };
639
640 let bootmgr = if should_collect("bootmgr") || should_collect("boot") {
641 detect_bootmgr()
642 } else {
643 None
644 };
645
646 let editor = if should_collect("editor") {
647 std::env::var("VISUAL")
648 .ok()
649 .filter(|s| !s.is_empty())
650 .or_else(|| std::env::var("EDITOR").ok().filter(|s| !s.is_empty()))
651 } else {
652 None
653 };
654
655 let current_user = std::env::var("USER").ok();
657
658 let users = if should_collect("users") {
660 Users::new_with_refreshed_list()
661 .iter()
662 .filter(|user| {
663 let uid_str = user.id().to_string();
665 if let Ok(uid) = uid_str.parse::<u32>() {
666 uid >= 1000
667 } else {
668 false
669 }
670 })
671 .count()
672 } else {
673 0
674 };
675
676 Ok(Self {
677 os,
678 kernel,
679 hostname,
680 arch,
681 cpu,
682 cpu_cores,
683 cpu_core_info,
684 memory,
685 swap,
686 uptime,
687 processes,
688 load_avg,
689 disks,
690 temps,
691 networks,
692 boot_time,
693 battery,
694 shell,
695 terminal,
696 desktop,
697 cpu_freq,
698 users,
699 gpu,
700 packages,
701 current_user,
702 local_ip,
703 public_ip,
704 active_interface,
705 motherboard,
706 bios,
707 displays,
708 audio,
709 wifi,
710 bluetooth,
711 ui_theme,
712 icons,
713 cursor,
714 font,
715 terminal_font,
716 camera,
717 gamepad,
718 cpu_cache,
719 cpu_usage,
720 physical_disks,
721 physical_memory,
722 init_system,
723 chassis,
724 locale,
725 bootmgr,
726 editor,
727 weather,
728 })
729 }
730}
731
732pub fn detect_cpu_cache() -> Option<String> {
738 #[cfg(target_os = "linux")]
739 {
740 use std::fs;
741 let cache_dir = std::path::Path::new("/sys/devices/system/cpu/cpu0/cache");
742 if !cache_dir.exists() {
743 return None;
744 }
745
746 struct CacheEntry {
747 level: u32,
748 kind: String,
749 size_kb: u64,
750 }
751
752 let mut entries: Vec<CacheEntry> = Vec::new();
753
754 let Ok(indices) = fs::read_dir(cache_dir) else {
755 return None;
756 };
757
758 for entry in indices.flatten() {
759 let path = entry.path();
760 if !path.is_dir() {
762 continue;
763 }
764 let level_str = match fs::read_to_string(path.join("level")) {
765 Ok(s) => s,
766 Err(_) => continue,
767 };
768 let level: u32 = match level_str.trim().parse() {
769 Ok(n) => n,
770 Err(_) => continue,
771 };
772 let kind = match fs::read_to_string(path.join("type")) {
773 Ok(s) => s.trim().to_string(),
774 Err(_) => continue,
775 };
776 let size_str = match fs::read_to_string(path.join("size")) {
777 Ok(s) => s,
778 Err(_) => continue,
779 };
780 let size_raw = size_str.trim();
781 let size_kb: u64 = if let Some(k) = size_raw.strip_suffix('K') {
782 match k.parse() {
783 Ok(n) => n,
784 Err(_) => continue,
785 }
786 } else if let Some(m) = size_raw.strip_suffix('M') {
787 match m.parse::<u64>() {
788 Ok(n) => n * 1024,
789 Err(_) => continue,
790 }
791 } else {
792 match size_raw.parse() {
793 Ok(n) => n,
794 Err(_) => continue,
795 }
796 };
797
798 if kind != "Instruction" && kind != "Data" && kind != "Unified" {
799 continue;
800 }
801
802 entries.push(CacheEntry {
803 level,
804 kind,
805 size_kb,
806 });
807 }
808
809 if entries.is_empty() {
810 return None;
811 }
812
813 entries.sort_by_key(|e| (e.level, e.kind.clone()));
814
815 let fmt_size = |kb: u64| -> String {
816 if kb >= 1024 && kb.is_multiple_of(1024) {
817 format!("{}M", kb / 1024)
818 } else if kb >= 1024 {
819 format!("{:.2}M", kb as f64 / 1024.0)
820 .trim_end_matches('0')
821 .trim_end_matches('.')
822 .to_string()
823 + "M"
824 } else {
825 format!("{}K", kb)
826 }
827 };
828
829 let mut seen = std::collections::HashSet::new();
831 let mut parts: Vec<String> = Vec::new();
832 for e in &entries {
833 let label = match (e.level, e.kind.as_str()) {
834 (1, "Data") => "L1d".to_string(),
835 (1, "Instruction") => "L1i".to_string(),
836 (1, "Unified") => "L1".to_string(),
837 (n, _) => format!("L{}", n),
838 };
839 if seen.insert(label.clone()) {
840 parts.push(format!("{}: {}", label, fmt_size(e.size_kb)));
841 }
842 }
843
844 if parts.is_empty() {
845 None
846 } else {
847 Some(parts.join(", "))
848 }
849 }
850 #[cfg(target_os = "macos")]
851 {
852 extern "C" {
853 fn sysctlbyname(
854 name: *const i8,
855 oldp: *mut std::ffi::c_void,
856 oldlenp: *mut usize,
857 newp: *mut std::ffi::c_void,
858 newlen: usize,
859 ) -> i32;
860 }
861
862 let read_u64 = |key: &str| -> Option<u64> {
863 let name = std::ffi::CString::new(key).ok()?;
864 let mut value: u64 = 0;
865 let mut size = std::mem::size_of::<u64>();
866 let ret = unsafe {
867 sysctlbyname(
868 name.as_ptr(),
869 &mut value as *mut u64 as *mut std::ffi::c_void,
870 &mut size,
871 std::ptr::null_mut(),
872 0,
873 )
874 };
875 if ret == 0 && value > 0 {
876 Some(value)
877 } else {
878 None
879 }
880 };
881
882 let fmt_bytes = |bytes: u64| -> String {
883 if bytes >= 1024 * 1024 {
884 format!("{}M", bytes / (1024 * 1024))
885 } else {
886 format!("{}K", bytes / 1024)
887 }
888 };
889
890 let mut parts = Vec::new();
891 if let Some(v) = read_u64("hw.l1dcachesize") {
892 parts.push(format!("L1d: {}", fmt_bytes(v)));
893 }
894 if let Some(v) = read_u64("hw.l1icachesize") {
895 parts.push(format!("L1i: {}", fmt_bytes(v)));
896 }
897 if let Some(v) = read_u64("hw.l2cachesize") {
898 parts.push(format!("L2: {}", fmt_bytes(v)));
899 }
900 if let Some(v) = read_u64("hw.l3cachesize") {
901 parts.push(format!("L3: {}", fmt_bytes(v)));
902 }
903
904 if parts.is_empty() {
905 None
906 } else {
907 Some(parts.join(", "))
908 }
909 }
910 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
911 {
912 None
913 }
914}
915
916pub fn format_cpu_cores(logical: usize, physical: Option<usize>) -> String {
921 #[cfg(target_os = "linux")]
923 if let Some(hybrid) = detect_hybrid_cores(logical) {
924 return hybrid;
925 }
926
927 #[cfg(target_os = "macos")]
929 if let Some(hybrid) = detect_macos_hybrid_cores(logical) {
930 return hybrid;
931 }
932
933 match physical {
934 Some(p) if p < logical => format!("{}C / {}T", p, logical),
935 _ => format!("{} cores", logical),
936 }
937}
938
939#[cfg(target_os = "linux")]
942fn detect_hybrid_cores(logical: usize) -> Option<String> {
943 use std::collections::HashMap;
944 use std::fs;
945
946 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
947 if !cpufreq.exists() {
948 return None;
949 }
950
951 let mut freq_to_count: HashMap<u64, usize> = HashMap::new();
953 let mut total_accounted = 0usize;
954
955 let Ok(policies) = fs::read_dir(cpufreq) else {
956 return None;
957 };
958
959 for policy in policies.flatten() {
960 let path = policy.path();
961 if !path.is_dir() {
962 continue;
963 }
964 let max_freq_str = fs::read_to_string(path.join("cpuinfo_max_freq")).ok()?;
965 let max_freq: u64 = max_freq_str.trim().parse().ok()?;
966 let affected = fs::read_to_string(path.join("affected_cpus")).ok()?;
967 let count = affected.split_whitespace().count();
968 *freq_to_count.entry(max_freq).or_insert(0) += count;
969 total_accounted += count;
970 }
971
972 if freq_to_count.len() != 2 || total_accounted != logical {
974 return None;
975 }
976
977 let mut tiers: Vec<(u64, usize)> = freq_to_count.into_iter().collect();
978 tiers.sort_by_key(|t| std::cmp::Reverse(t.0)); let (_, p_count) = tiers[0];
980 let (_, e_count) = tiers[1];
981
982 Some(format!("{}P + {}E / {}T", p_count, e_count, logical))
983}
984
985#[cfg(target_os = "macos")]
988fn detect_macos_hybrid_cores(logical: usize) -> Option<String> {
989 extern "C" {
990 fn sysctlbyname(
991 name: *const i8,
992 oldp: *mut std::ffi::c_void,
993 oldlenp: *mut usize,
994 newp: *mut std::ffi::c_void,
995 newlen: usize,
996 ) -> i32;
997 }
998
999 let read_u32 = |key: &str| -> Option<u32> {
1000 let name = std::ffi::CString::new(key).ok()?;
1001 let mut value: u32 = 0;
1002 let mut size = std::mem::size_of::<u32>();
1003 let ret = unsafe {
1004 sysctlbyname(
1005 name.as_ptr(),
1006 &mut value as *mut u32 as *mut std::ffi::c_void,
1007 &mut size,
1008 std::ptr::null_mut(),
1009 0,
1010 )
1011 };
1012 if ret == 0 {
1013 Some(value)
1014 } else {
1015 None
1016 }
1017 };
1018
1019 let nlevels = read_u32("hw.nperflevels")?;
1021 if nlevels != 2 {
1022 return None;
1023 }
1024
1025 let p_cores = read_u32("hw.perflevel0.logicalcpu")? as usize;
1026 let e_cores = read_u32("hw.perflevel1.logicalcpu")? as usize;
1027
1028 if p_cores + e_cores != logical {
1029 return None;
1030 }
1031
1032 Some(format!("{}P + {}E / {}T", p_cores, e_cores, logical))
1033}
1034
1035pub fn detect_cpu_freq_range() -> Option<(u64, u64)> {
1038 #[cfg(target_os = "linux")]
1039 {
1040 use std::fs;
1041 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
1042 if !cpufreq.exists() {
1043 return None;
1044 }
1045 let mut global_min: Option<u64> = None;
1046 let mut global_max: Option<u64> = None;
1047 let Ok(policies) = fs::read_dir(cpufreq) else {
1048 return None;
1049 };
1050 for policy in policies.flatten() {
1051 let path = policy.path();
1052 if !path.is_dir() {
1053 continue;
1054 }
1055 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_min_freq")) {
1056 if let Ok(v) = s.trim().parse::<u64>() {
1057 global_min = Some(global_min.map_or(v, |m: u64| m.min(v)));
1058 }
1059 }
1060 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_max_freq")) {
1061 if let Ok(v) = s.trim().parse::<u64>() {
1062 global_max = Some(global_max.map_or(v, |m: u64| m.max(v)));
1063 }
1064 }
1065 }
1066 match (global_min, global_max) {
1067 (Some(min), Some(max)) => Some((min, max)),
1068 _ => None,
1069 }
1070 }
1071 #[cfg(not(target_os = "linux"))]
1072 {
1073 None
1074 }
1075}
1076
1077fn detect_init_system() -> Option<String> {
1078 #[cfg(target_os = "linux")]
1079 {
1080 let comm = std::fs::read_to_string("/proc/1/comm")
1081 .map(|s| s.trim().to_string())
1082 .ok()
1083 .filter(|s| !s.is_empty());
1084 if let Some(name) = comm {
1085 return Some(name);
1086 }
1087 std::fs::read_link("/proc/1/exe").ok().and_then(|p| {
1088 p.file_name()
1089 .and_then(|n| n.to_str())
1090 .map(|s| s.to_string())
1091 })
1092 }
1093 #[cfg(target_os = "macos")]
1094 {
1095 Some("launchd".to_string())
1096 }
1097 #[cfg(target_os = "windows")]
1098 {
1099 Some("SCM".to_string())
1100 }
1101 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1102 {
1103 None
1104 }
1105}
1106
1107fn detect_chassis() -> Option<String> {
1108 #[cfg(target_os = "linux")]
1109 {
1110 let raw = std::fs::read_to_string("/sys/class/dmi/id/chassis_type").ok()?;
1111 let n: u32 = raw.trim().parse().ok()?;
1112 let label = match n {
1113 3 => "Desktop",
1114 4 => "Low-Profile Desktop",
1115 6 => "Mini Tower",
1116 7 => "Tower",
1117 8 | 9 | 10 | 14 | 31 | 32 => "Laptop",
1118 11 => "Handheld",
1119 13 => "All-in-One",
1120 17 => "Main Server",
1121 23 => "Rack Server",
1122 28 => "Blade",
1123 30 => "Tablet",
1124 35 => "Mini PC",
1125 36 => "Stick PC",
1126 _ => return None,
1127 };
1128 Some(label.to_string())
1129 }
1130 #[cfg(target_os = "macos")]
1131 {
1132 let output = std::process::Command::new("sysctl")
1133 .args(["-n", "hw.model"])
1134 .output()
1135 .ok()?;
1136 let model = String::from_utf8(output.stdout).ok()?;
1137 let model = model.trim();
1138 if model.contains("MacBook") {
1139 Some("Laptop".to_string())
1140 } else if model.contains("MacPro") {
1141 Some("Desktop".to_string())
1142 } else if model.contains("Macmini") || model.contains("Mac mini") {
1143 Some("Mini PC".to_string())
1144 } else if model.contains("iMac") {
1145 Some("All-in-One".to_string())
1146 } else {
1147 Some(model.to_string())
1148 }
1149 }
1150 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1151 {
1152 None
1153 }
1154}
1155
1156fn detect_bootmgr() -> Option<String> {
1157 #[cfg(target_os = "linux")]
1158 {
1159 use std::path::Path;
1160 let is_uefi = Path::new("/sys/firmware/efi").exists();
1161 if Path::new("/boot/loader/entries").exists()
1162 || Path::new("/boot/loader/loader.conf").exists()
1163 || Path::new("/efi/loader/loader.conf").exists()
1164 {
1165 return Some("systemd-boot".to_string());
1166 }
1167 if Path::new("/boot/grub2/grub.cfg").exists() || Path::new("/boot/grub2").exists() {
1168 return Some("GRUB 2".to_string());
1169 }
1170 if Path::new("/boot/grub/grub.cfg").exists() || Path::new("/boot/grub").exists() {
1171 return Some("GRUB".to_string());
1172 }
1173 if is_uefi {
1174 Some("UEFI".to_string())
1175 } else {
1176 Some("BIOS".to_string())
1177 }
1178 }
1179 #[cfg(target_os = "macos")]
1180 {
1181 Some("Apple Boot ROM".to_string())
1182 }
1183 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1184 {
1185 None
1186 }
1187}
1188
1189#[cfg(test)]
1190mod tests {
1191 use super::*;
1192
1193 #[test]
1194 fn test_format_cpu_cores_no_hyperthreading() {
1195 assert_eq!(format_cpu_cores(4, Some(4)), "4 cores");
1197 }
1198
1199 #[test]
1200 fn test_format_cpu_cores_hyperthreaded() {
1201 assert_eq!(format_cpu_cores(16, Some(8)), "8C / 16T");
1203 }
1204
1205 #[test]
1206 fn test_format_cpu_cores_unknown_physical() {
1207 assert_eq!(format_cpu_cores(8, None), "8 cores");
1209 }
1210
1211 #[test]
1212 fn test_format_cpu_cores_physical_equals_zero() {
1213 let result = format_cpu_cores(8, Some(0));
1217 assert!(result.contains("8"), "should mention 8 threads: {}", result);
1218 }
1219
1220 #[cfg(target_os = "linux")]
1221 #[test]
1222 fn test_detect_cpu_cache_returns_some_on_linux() {
1223 if std::path::Path::new("/sys/devices/system/cpu/cpu0/cache").exists() {
1226 let result = detect_cpu_cache();
1227 assert!(result.is_some(), "expected cache info on Linux with sysfs");
1228 let s = result.unwrap();
1229 assert!(
1230 s.contains("L1") || s.contains("L2") || s.contains("L3"),
1231 "expected cache level labels, got: {}",
1232 s
1233 );
1234 }
1235 }
1236
1237 #[cfg(target_os = "linux")]
1238 #[test]
1239 fn test_detect_cpu_freq_range_returns_ordered_pair() {
1240 if std::path::Path::new("/sys/devices/system/cpu/cpufreq").exists() {
1241 if let Some((min, max)) = detect_cpu_freq_range() {
1242 assert!(
1243 min <= max,
1244 "min freq should be <= max freq: {} > {}",
1245 min,
1246 max
1247 );
1248 assert!(min > 0, "min freq should be positive");
1249 }
1250 }
1251 }
1252}