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