1use crate::gpu;
10use chrono::TimeZone;
11use sysinfo::{Components, System};
12#[cfg(not(target_os = "windows"))]
15use sysinfo::Users;
16
17#[derive(Debug, Default, Clone)]
22pub struct CollectOptions {
23 pub long: bool,
25 pub full: bool,
27 pub fields: Option<Vec<String>>,
29 pub weather_location: Option<String>,
31 pub weather_unit: crate::weather::WeatherUnit,
33}
34
35#[derive(Debug)]
40pub struct SystemInfo {
41 pub os: String,
43 pub kernel: Option<String>,
45 pub hostname: Option<String>,
47 pub arch: String,
49 pub cpu: String,
51 pub cpu_cores: usize,
53 pub cpu_core_info: String,
55 pub memory: String,
57 pub swap: String,
59 pub uptime: String,
61 pub processes: usize,
63 pub load_avg: Option<String>,
65 pub disks: Vec<String>,
67 pub temps: Vec<String>,
69 pub networks: Vec<crate::network::NetworkInterface>,
71 pub boot_time: String,
73 pub battery: Option<String>,
75 pub shell: Option<String>,
77 pub terminal: Option<String>,
79 pub desktop: Option<String>,
81 pub cpu_freq: Option<String>,
83 pub users: usize,
85 pub gpu: Vec<String>,
87 pub packages: Option<usize>,
89 pub current_user: Option<String>,
91 pub local_ip: Option<String>,
93 pub public_ip: Option<String>,
95 pub active_interface: Option<String>,
97 pub motherboard: Option<String>,
99 pub bios: Option<String>,
101 pub displays: Vec<String>,
103 pub audio: Option<String>,
105 pub wifi: Option<String>,
107 pub bluetooth: Option<String>,
109 pub ui_theme: Option<String>,
111 pub icons: Option<String>,
113 pub cursor: Option<String>,
115 pub font: Option<String>,
117 pub terminal_font: Option<String>,
119 pub camera: Vec<String>,
121 pub gamepad: Vec<String>,
123 pub cpu_cache: Option<String>,
125 pub cpu_usage: Option<String>,
127 pub physical_disks: Vec<String>,
129 pub vulkan: Option<String>,
131 pub opengl: Option<String>,
133 pub opencl: Option<String>,
135 pub disk_io: Vec<String>,
137 pub net_io: Vec<String>,
139 pub physical_memory: Option<String>,
141 pub init_system: Option<String>,
143 pub chassis: Option<String>,
145 pub locale: Option<String>,
147 pub bootmgr: Option<String>,
149 pub editor: Option<String>,
151 pub weather: Option<String>,
153 pub wm: Option<String>,
155 pub dns: Vec<String>,
157 pub domain: Option<String>,
160 pub domain_search: Vec<String>,
163 pub terminal_size: Option<String>,
165 pub btrfs: Vec<String>,
167 pub zpool: Vec<String>,
169 pub login_manager: Option<String>,
171 pub brightness: Option<String>,
173 pub power_adapter: Option<String>,
175 pub keyboard: Vec<String>,
178 pub mouse: Vec<String>,
180 pub tpm: Option<String>,
182 pub media: Option<String>,
184 pub player: Option<String>,
186 pub wm_theme: Option<String>,
188 pub wallpaper: Option<String>,
190 pub terminal_theme: Option<String>,
192}
193
194fn cpu_refresh_kind(want_freq: bool, want_usage: bool) -> Option<sysinfo::CpuRefreshKind> {
215 if !(want_freq || want_usage) {
216 return None;
217 }
218 let mut kind = sysinfo::CpuRefreshKind::nothing();
219 if want_freq {
220 kind = kind.with_frequency();
221 }
222 if want_usage && !cfg!(target_os = "windows") {
226 kind = kind.with_cpu_usage();
227 }
228 Some(kind)
229}
230
231fn own_cpu_refresh_kind() -> sysinfo::CpuRefreshKind {
235 sysinfo::CpuRefreshKind::nothing()
236}
237
238fn cpu_brand(cpus: &[sysinfo::Cpu]) -> String {
244 let brand = cpus
245 .first()
246 .map(|c| c.brand().to_string())
247 .unwrap_or_else(|| "Unknown CPU".to_string());
248 #[cfg(target_os = "windows")]
249 if brand.trim().is_empty() {
250 if let Some(name) = windows_cpu_name() {
251 return name;
252 }
253 }
254 brand
255}
256
257#[cfg(target_os = "windows")]
260fn windows_cpu_name() -> Option<String> {
261 crate::win_reg::get_reg_string(
262 crate::win_reg::HKEY_LOCAL_MACHINE,
263 r"HARDWARE\DESCRIPTION\System\CentralProcessor\0",
264 "ProcessorNameString",
265 )
266 .map(|s| s.trim().to_string())
267 .filter(|s| !s.is_empty())
268}
269
270#[cfg_attr(target_os = "windows", allow(dead_code))]
274fn remaining_wait(
275 interval: std::time::Duration,
276 elapsed: std::time::Duration,
277) -> std::time::Duration {
278 interval.saturating_sub(elapsed)
279}
280
281#[cfg_attr(target_os = "windows", allow(dead_code))]
289fn exclude_own_usage(
290 usage: f32,
291 own: std::time::Duration,
292 window: std::time::Duration,
293 cpus: usize,
294) -> f32 {
295 if cpus == 0 || window.is_zero() {
296 return usage;
297 }
298 let own_pct = 100.0 * own.as_secs_f64() / (window.as_secs_f64() * cpus as f64);
299 (f64::from(usage) - own_pct).max(0.0) as f32
300}
301
302#[cfg(not(target_os = "windows"))]
305fn own_cpu_time() -> std::time::Duration {
306 fn to_duration(t: libc::timeval) -> std::time::Duration {
307 std::time::Duration::from_secs(t.tv_sec.max(0) as u64)
308 + std::time::Duration::from_micros(t.tv_usec.max(0) as u64)
309 }
310 fn usage(who: libc::c_int) -> std::time::Duration {
311 let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
314 if unsafe { libc::getrusage(who, &mut ru) } != 0 {
317 return std::time::Duration::ZERO;
318 }
319 to_duration(ru.ru_utime) + to_duration(ru.ru_stime)
320 }
321 usage(libc::RUSAGE_SELF) + usage(libc::RUSAGE_CHILDREN)
322}
323
324fn should_probe_load(want_load: bool) -> bool {
340 want_load && !cfg!(target_os = "windows")
341}
342
343fn needs_process_list(procs: bool, audio: bool, shell: bool, terminal: bool) -> bool {
357 !cfg!(target_os = "linux") && (procs || audio || shell || terminal)
358}
359
360impl SystemInfo {
361 pub fn collect(opts: CollectOptions) -> anyhow::Result<Self> {
366 crate::timing::start();
367 let should_collect = |field_name: &str| -> bool {
368 match &opts.fields {
369 Some(fields) => {
370 let norm_field = field_name.to_lowercase().replace(['-', '_'], " ");
371 let norm_field_no_spaces = norm_field.replace(' ', "");
372 fields.iter().any(|f| {
373 let norm_f = f.to_lowercase().replace(['-', '_'], " ");
374 norm_f == norm_field || norm_f.replace(' ', "") == norm_field_no_spaces
375 })
376 }
377 None => true,
378 }
379 };
380
381 #[cfg(target_os = "windows")]
385 let cpu_sample0 = win_cpu::sample();
386 #[cfg(target_os = "windows")]
387 let cpu_t0 = std::time::Instant::now();
388
389 let want_disk_io = should_collect("disk-io") || should_collect("disk io");
395 let want_net_io = should_collect("net-io") || should_collect("net io");
396 let disk_io_sample0 = if want_disk_io {
397 crate::io::sample_disk_io()
398 } else {
399 Vec::new()
400 };
401 let net_io_sample0 = if want_net_io {
402 crate::io::sample_net_io()
403 } else {
404 Vec::new()
405 };
406 let io_t0 = std::time::Instant::now();
407
408 let sys_needs_cpus =
414 cpu_refresh_kind(should_collect("cpu-freq"), should_collect("cpu-usage")).is_some();
415 let cpu_early_handle = (should_collect("cpu") && !sys_needs_cpus).then(|| {
418 std::thread::spawn(move || {
419 crate::timing::timed("cpu", || {
420 let own = System::new_with_specifics(
421 sysinfo::RefreshKind::nothing().with_cpu(own_cpu_refresh_kind()),
422 );
423 let (brand, logical) = (cpu_brand(own.cpus()), own.cpus().len());
424 (brand, logical, hybrid_cores(logical))
425 })
426 })
427 });
428 let cpu_physical_handle = if should_collect("cpu") {
434 Some(std::thread::spawn(move || {
435 crate::timing::timed("cpu-physical", System::physical_core_count)
436 }))
437 } else {
438 None
439 };
440 let gpu_handle = if should_collect("gpu") {
441 Some(std::thread::spawn(move || {
442 crate::timing::timed("gpu", || {
443 gpu::detect_gpus()
444 .into_iter()
445 .map(|g| g.format())
446 .collect::<Vec<String>>()
447 })
448 }))
449 } else {
450 None
451 };
452 let packages_handle = if should_collect("packages") {
453 Some(std::thread::spawn(move || {
454 crate::timing::timed("packages", crate::packages::detect_packages)
455 }))
456 } else {
457 None
458 };
459 let public_ip_handle = if should_collect("public ip") {
460 Some(std::thread::spawn(move || {
461 crate::timing::timed("public-ip", crate::network::detect_public_ip)
462 }))
463 } else {
464 None
465 };
466 let network_ips_handle = if should_collect("net") {
469 Some(std::thread::spawn(move || {
470 let (local_ip, active_interface) = crate::timing::timed(
471 "net",
472 crate::network::detect_active_interface_and_local_ip,
473 );
474 let networks = crate::timing::timed("net-detail", || {
475 crate::network::detect_networks(
476 active_interface.as_deref(),
477 local_ip.as_deref(),
478 )
479 });
480 ((local_ip, active_interface), networks)
481 }))
482 } else {
483 None
484 };
485 let motherboard_handle = if should_collect("motherboard") {
486 Some(std::thread::spawn(move || {
487 crate::timing::timed("motherboard", crate::motherboard::detect_motherboard)
488 }))
489 } else {
490 None
491 };
492 let bios_handle = if should_collect("bios") {
493 Some(std::thread::spawn(move || {
494 crate::timing::timed("bios", crate::bios::detect_bios)
495 }))
496 } else {
497 None
498 };
499 let displays_handle = if should_collect("display") {
500 Some(std::thread::spawn(move || {
501 crate::timing::timed("display", crate::display::detect_displays)
502 }))
503 } else {
504 None
505 };
506 let wifi_handle = if should_collect("wifi") {
507 Some(std::thread::spawn(move || {
508 crate::timing::timed("wifi", crate::network::detect_wifi)
509 }))
510 } else {
511 None
512 };
513 let bluetooth_handle = if should_collect("bluetooth") {
514 Some(std::thread::spawn(move || {
515 crate::timing::timed("bluetooth", crate::bluetooth::detect_bluetooth)
516 }))
517 } else {
518 None
519 };
520 let ui_theme_and_fonts_handle = if should_collect("theme")
521 || should_collect("icons")
522 || should_collect("cursor")
523 || should_collect("font")
524 {
525 Some(std::thread::spawn(move || {
526 crate::timing::timed("theme+fonts", crate::theme::detect_ui_theme_and_fonts)
527 }))
528 } else {
529 None
530 };
531 let camera_handle = if should_collect("camera") {
532 Some(std::thread::spawn(move || {
533 crate::timing::timed("camera", crate::camera::detect_camera)
534 }))
535 } else {
536 None
537 };
538 let gamepad_handle = if should_collect("gamepad") {
539 Some(std::thread::spawn(move || {
540 crate::timing::timed("gamepad", crate::gamepad::detect_gamepad)
541 }))
542 } else {
543 None
544 };
545 let physical_disks_handle = if should_collect("phys disk") {
546 Some(std::thread::spawn(move || {
547 crate::timing::timed("phys-disk", crate::disk::detect_physical_disks)
548 }))
549 } else {
550 None
551 };
552 let physical_memory_handle = if should_collect("phys mem") {
553 Some(std::thread::spawn(move || {
554 crate::timing::timed("phys-mem", crate::memory::detect_physical_memory)
555 }))
556 } else {
557 None
558 };
559 let weather_location = opts.weather_location.clone();
560 let weather_unit = opts.weather_unit;
561 let weather_handle = if should_collect("weather") {
562 Some(std::thread::spawn(move || {
563 crate::timing::timed("weather", || {
564 crate::weather::detect_weather(weather_location.as_deref(), weather_unit)
565 })
566 }))
567 } else {
568 None
569 };
570 let btrfs_handle = if should_collect("btrfs") {
571 Some(std::thread::spawn(move || {
572 crate::timing::timed("btrfs", crate::btrfs::detect_btrfs)
573 }))
574 } else {
575 None
576 };
577 let zpool_handle = if should_collect("zpool") {
578 Some(std::thread::spawn(move || {
579 crate::timing::timed("zpool", crate::zfs::detect_zpool)
580 }))
581 } else {
582 None
583 };
584 let media_handle = if should_collect("media") || should_collect("player") {
585 Some(std::thread::spawn(move || {
586 crate::timing::timed("media", crate::media::detect_media)
587 }))
588 } else {
589 None
590 };
591 let gpu_apis_handle =
595 if should_collect("vulkan") || should_collect("opengl") || should_collect("opencl") {
596 Some(std::thread::spawn(move || {
597 crate::timing::timed("gpu-apis", crate::gpu_api::detect_gpu_apis)
598 }))
599 } else {
600 None
601 };
602
603 let mut refresh_kind = sysinfo::RefreshKind::nothing();
604 let sys_has_cpus =
607 match cpu_refresh_kind(should_collect("cpu-freq"), should_collect("cpu-usage")) {
608 Some(cpu_kind) => {
609 refresh_kind = refresh_kind.with_cpu(cpu_kind);
610 true
611 }
612 None => false,
613 };
614 if should_collect("memory")
615 || should_collect("swap")
616 || should_collect("phys mem")
617 || should_collect("phys-mem")
618 {
619 refresh_kind = refresh_kind.with_memory(sysinfo::MemoryRefreshKind::everything());
620 }
621 if needs_process_list(
622 should_collect("procs"),
623 should_collect("audio"),
624 should_collect("shell"),
625 should_collect("terminal"),
626 ) {
627 refresh_kind = refresh_kind.with_processes(sysinfo::ProcessRefreshKind::nothing());
628 }
629
630 #[cfg_attr(target_os = "windows", allow(unused_mut))]
633 let mut sys = crate::timing::timed("sys-init", || System::new_with_specifics(refresh_kind));
634 #[cfg(not(target_os = "windows"))]
638 let cpu_usage_t0 = std::time::Instant::now();
639 #[cfg(not(target_os = "windows"))]
642 let cpu_usage_own0 = own_cpu_time();
643
644 let os = crate::timing::timed("os", || {
645 System::long_os_version()
646 .or_else(System::name)
647 .unwrap_or_else(|| "Unknown".to_string())
648 });
649
650 let kernel = crate::timing::timed("kernel", System::kernel_version);
651 let hostname = crate::timing::timed("host", System::host_name);
652
653 let cpu_from_sys: Option<(String, usize)> = (should_collect("cpu") && sys_has_cpus)
657 .then(|| (cpu_brand(sys.cpus()), sys.cpus().len()));
658 let memory = if should_collect("memory") {
659 let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
660 let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
661 format!("{:.1} / {:.1} GB", used_mem, total_mem)
662 } else {
663 String::new()
664 };
665
666 let swap = if should_collect("swap") {
667 let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
668 let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
669 if total_swap > 0.0 {
670 format!("{:.1} / {:.1} GB", used_swap, total_swap)
671 } else {
672 "No swap".to_string()
673 }
674 } else {
675 String::new()
676 };
677
678 let uptime = format!("{}s", crate::timing::timed("uptime", System::uptime));
679
680 let disks: Vec<String> = if should_collect("disk") {
681 let disks_list =
682 crate::timing::timed("disk", || crate::disk::detect_logical_disks(opts.full));
683 let format_disk = |(mount, total, avail, fs): &(String, u64, u64, String)| {
684 let total_gb = *total as f64 / 1024.0 / 1024.0 / 1024.0;
685 let avail_gb = *avail as f64 / 1024.0 / 1024.0 / 1024.0;
686 format!(
687 "{} ({}): {:.1} GB free / {:.1} GB",
688 mount, fs, avail_gb, total_gb
689 )
690 };
691 if !opts.long {
692 let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
693 let home_path = std::path::Path::new(&home);
694 let best = disks_list
695 .iter()
696 .filter(|(mp, ..)| home_path.starts_with(mp))
697 .max_by_key(|(mp, ..)| std::path::Path::new(mp).components().count());
698 if let Some(disk) = best {
699 vec![format_disk(disk)]
700 } else {
701 disks_list.iter().map(format_disk).collect()
702 }
703 } else {
704 disks_list.iter().map(format_disk).collect()
705 }
706 } else {
707 Vec::new()
708 };
709
710 let battery = if should_collect("battery") {
711 crate::timing::timed("battery", crate::battery::get_battery_info).map(|bat| {
712 let pct = bat.percentage;
713 let state = match bat.state {
714 crate::battery::BatteryState::Charging => "charging",
715 crate::battery::BatteryState::Discharging => "discharging",
716 crate::battery::BatteryState::Full => "full",
717 _ => "not charging",
718 };
719 let vendor = bat.vendor;
720 let model = bat.model;
721
722 let time_str = match bat.state {
724 crate::battery::BatteryState::Charging => bat.time_remaining.map(|d| {
725 let total_mins = d.as_secs() / 60;
726 let hours = total_mins / 60;
727 let mins = total_mins % 60;
728 if hours >= 24 {
729 let days = hours / 24;
730 let rem_hours = hours % 24;
731 format!("{}d {}h until full", days, rem_hours)
732 } else if hours > 0 {
733 format!("{}h {}m until full", hours, mins)
734 } else {
735 format!("{}m until full", mins)
736 }
737 }),
738 crate::battery::BatteryState::Discharging => bat.time_remaining.map(|d| {
739 let total_mins = d.as_secs() / 60;
740 let hours = total_mins / 60;
741 let mins = total_mins % 60;
742 if hours >= 24 {
743 let days = hours / 24;
744 let rem_hours = hours % 24;
745 format!("{}d {}h remaining", days, rem_hours)
746 } else if hours > 0 {
747 format!("{}h {}m remaining", hours, mins)
748 } else {
749 format!("{}m remaining", mins)
750 }
751 }),
752 _ => None,
753 };
754
755 let mut parts = vec![state.to_string()];
756 if let Some(t) = time_str {
757 parts.insert(0, t);
758 }
759 if let Some(health) = bat.health {
760 if health < 99.0 {
761 parts.push(format!("{:.0}% health", health));
762 }
763 }
764
765 let base = format!("{:.0}% ({})", pct, parts.join(", "));
766
767 match (vendor, model) {
768 (Some(v), Some(m)) => format!("{} [{} {}]", base, v, m),
769 (Some(v), None) => format!("{} [{}]", base, v),
770 _ => base,
771 }
772 })
773 } else {
774 None
775 };
776
777 let arch = crate::timing::timed("arch", System::cpu_arch);
778
779 let processes = if should_collect("procs") || should_collect("audio") {
780 #[cfg(target_os = "linux")]
781 {
782 crate::timing::timed("procs", crate::proc_tree::task_count)
783 }
784 #[cfg(not(target_os = "linux"))]
785 {
786 sys.processes().len()
787 }
788 } else {
789 0
790 };
791
792 let load_avg = if should_probe_load(should_collect("load")) {
793 let avg = System::load_average();
794 if avg.one > 0.0 || avg.five > 0.0 {
795 Some(format!(
796 "{:.2}, {:.2}, {:.2}",
797 avg.one, avg.five, avg.fifteen
798 ))
799 } else {
800 None
801 }
802 } else {
803 None
804 };
805
806 let (
808 gpu,
809 packages,
810 public_ip,
811 ((local_ip, active_interface), networks),
812 motherboard,
813 bios,
814 displays,
815 audio,
816 wifi,
817 bluetooth,
818 (ui_theme, icons, cursor, font),
819 camera,
820 gamepad,
821 physical_disks,
822 physical_memory,
823 weather,
824 btrfs,
825 zpool,
826 (media, player),
827 gpu_apis,
828 shell,
829 (cpu, cpu_cores, cpu_hybrid),
830 cpu_physical,
831 ) = crate::timing::timed("scope", || {
832 std::thread::scope(|s| {
833 let cpu_cores_handle = if should_collect("cpu") && sys_has_cpus {
836 let known = cpu_from_sys.clone();
837 Some(s.spawn(move || {
838 crate::timing::timed("cpu", || {
839 let (brand, logical) = known.unwrap_or_else(|| {
840 let own = System::new_with_specifics(
841 sysinfo::RefreshKind::nothing()
842 .with_cpu(own_cpu_refresh_kind()),
843 );
844 (cpu_brand(own.cpus()), own.cpus().len())
845 });
846 (brand, logical, hybrid_cores(logical))
847 })
848 }))
849 } else {
850 None
851 };
852 let audio_handle = if should_collect("audio") {
853 Some(s.spawn(|| {
854 crate::timing::timed("audio", || crate::audio::detect_audio(&sys))
855 }))
856 } else {
857 None
858 };
859 let shell_handle = if should_collect("shell") {
865 Some(s.spawn(|| {
866 crate::timing::timed("shell", || crate::shell::detect_shell(&sys))
867 }))
868 } else {
869 None
870 };
871 (
872 gpu_handle
873 .map(|h| h.join().unwrap_or_default())
874 .unwrap_or_default(),
875 packages_handle.and_then(|h| h.join().ok().flatten()),
876 public_ip_handle.and_then(|h| h.join().ok().flatten()),
877 network_ips_handle
878 .map(|h| h.join().unwrap_or(((None, None), Vec::new())))
879 .unwrap_or(((None, None), Vec::new())),
880 motherboard_handle.and_then(|h| h.join().ok().flatten()),
881 bios_handle.and_then(|h| h.join().ok().flatten()),
882 displays_handle
883 .map(|h| h.join().unwrap_or_default())
884 .unwrap_or_default(),
885 audio_handle.and_then(|h| h.join().ok().flatten()),
886 wifi_handle.and_then(|h| h.join().ok().flatten()),
887 bluetooth_handle.and_then(|h| h.join().ok().flatten()),
888 ui_theme_and_fonts_handle
889 .map(|h| h.join().unwrap_or((None, None, None, None)))
890 .unwrap_or((None, None, None, None)),
891 camera_handle
892 .map(|h| h.join().unwrap_or_default())
893 .unwrap_or_default(),
894 gamepad_handle
895 .map(|h| h.join().unwrap_or_default())
896 .unwrap_or_default(),
897 physical_disks_handle
898 .map(|h| h.join().unwrap_or_default())
899 .unwrap_or_default(),
900 physical_memory_handle.and_then(|h| h.join().ok().flatten()),
901 weather_handle.and_then(|h| h.join().ok().flatten()),
902 btrfs_handle
903 .map(|h| h.join().unwrap_or_default())
904 .unwrap_or_default(),
905 zpool_handle
906 .map(|h| h.join().unwrap_or_default())
907 .unwrap_or_default(),
908 media_handle
909 .map(|h| h.join().unwrap_or((None, None)))
910 .unwrap_or((None, None)),
911 gpu_apis_handle
912 .map(|h| h.join().unwrap_or_default())
913 .unwrap_or_default(),
914 shell_handle.and_then(|h| h.join().ok().flatten()),
915 cpu_early_handle
916 .map(|h| h.join().unwrap_or_default())
917 .or_else(|| cpu_cores_handle.map(|h| h.join().unwrap_or_default()))
918 .unwrap_or_default(),
919 cpu_physical_handle.and_then(|h| h.join().ok().flatten()),
920 )
921 })
922 });
923
924 let cpu_core_info = if should_collect("cpu") {
925 cpu_hybrid.unwrap_or_else(|| format_cpu_cores_plain(cpu_cores, cpu_physical))
926 } else {
927 String::new()
928 };
929
930 let mut temps: Vec<String> = if should_collect("temp") {
931 crate::timing::timed("temp", Components::new_with_refreshed_list)
932 .iter()
933 .filter_map(|c| {
934 c.temperature().and_then(|t| {
935 if t > 0.0 {
936 Some(format!("{}: {:.0}°C", c.label(), t))
937 } else {
938 None
939 }
940 })
941 })
942 .collect()
943 } else {
944 Vec::new()
945 };
946
947 temps.sort_by(|a, b| {
949 let a_cpu = a.to_lowercase().contains("cpu") || a.to_lowercase().contains("core");
950 let b_cpu = b.to_lowercase().contains("cpu") || b.to_lowercase().contains("core");
951 b_cpu.cmp(&a_cpu)
952 });
953
954 let boot_timestamp = crate::timing::timed("boot-time", System::boot_time);
955 let boot_dt = chrono::Local
956 .timestamp_opt(boot_timestamp as i64, 0)
957 .single()
958 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
959 .unwrap_or_else(|| boot_timestamp.to_string());
960 let boot_time = boot_dt;
961
962 let terminal = if should_collect("terminal") {
964 crate::timing::timed("terminal", || crate::terminal::detect_terminal(&sys))
965 } else {
966 None
967 };
968 let terminal_font = if should_collect("terminal font")
969 || should_collect("terminal-font")
970 || should_collect("terminal_font")
971 {
972 crate::timing::timed("terminal-font", || {
973 crate::terminal::detect_terminal_font(terminal.as_deref())
974 })
975 } else {
976 None
977 };
978 let desktop = if should_collect("desktop") {
979 std::env::var("XDG_CURRENT_DESKTOP")
980 .or_else(|_| std::env::var("DESKTOP_SESSION"))
981 .or_else(|_| std::env::var("XDG_SESSION_DESKTOP"))
982 .or_else(|_| std::env::var("GDMSESSION"))
983 .ok()
984 .map(|s| normalize_desktop_name(&s))
985 .filter(|s| !s.is_empty())
986 .or_else(|| crate::timing::timed("desktop-proc", detect_desktop_from_proc))
987 } else {
988 None
989 };
990
991 let cpu_freq = if should_collect("cpu-freq")
993 || should_collect("cpu freq")
994 || should_collect("cpu_freq")
995 {
996 sys.cpus().first().map(|c| {
997 let current = format!("{:.2} GHz", c.frequency() as f64 / 1000.0);
998 if let Some((min_khz, max_khz)) =
999 crate::timing::timed("cpu-freq", detect_cpu_freq_range)
1000 {
1001 let min_ghz = min_khz as f64 / 1_000_000.0;
1002 let max_ghz = max_khz as f64 / 1_000_000.0;
1003 format!("{} ({:.2} \u{2013} {:.2} GHz)", current, min_ghz, max_ghz)
1004 } else {
1005 current
1006 }
1007 })
1008 } else {
1009 None
1010 };
1011
1012 let cpu_cache = if should_collect("cpu-cache")
1014 || should_collect("cpu cache")
1015 || should_collect("cpu_cache")
1016 {
1017 crate::timing::timed("cpu-cache", detect_cpu_cache)
1018 } else {
1019 None
1020 };
1021
1022 let cpu_usage = if should_collect("cpu-usage")
1031 || should_collect("cpu usage")
1032 || should_collect("cpu_usage")
1033 {
1034 #[cfg(not(target_os = "windows"))]
1035 {
1036 let wait =
1037 remaining_wait(sysinfo::MINIMUM_CPU_UPDATE_INTERVAL, cpu_usage_t0.elapsed());
1038 crate::timing::timed("cpu-usage-wait", || std::thread::sleep(wait));
1039 sys.refresh_cpu_usage();
1040 let window = cpu_usage_t0.elapsed();
1041 let own = own_cpu_time().saturating_sub(cpu_usage_own0);
1042 let raw: f32 =
1043 sys.cpus().iter().map(|c| c.cpu_usage()).sum::<f32>() / sys.cpus().len() as f32;
1044 let usage = exclude_own_usage(raw, own, window, sys.cpus().len());
1049 let avg = System::load_average();
1050 let load_str = format!("{:.2}, {:.2}, {:.2}", avg.one, avg.five, avg.fifteen);
1051 if raw > 0.0 {
1052 Some(format!("{:.1}% (load: {})", usage, load_str))
1053 } else if avg.one > 0.0 {
1054 Some(format!("load: {}", load_str))
1055 } else {
1056 None
1057 }
1058 }
1059 #[cfg(target_os = "windows")]
1060 {
1061 let floor = std::time::Duration::from_millis(100);
1065 let elapsed = cpu_t0.elapsed();
1066 if elapsed < floor {
1067 std::thread::sleep(floor - elapsed);
1068 }
1069 match (cpu_sample0, win_cpu::sample()) {
1070 (Some(s0), Some(s1)) => {
1071 let usage = win_cpu::usage_percent(s0, s1);
1072 if usage > 0.0 {
1073 Some(format!("{:.1}%", usage))
1074 } else {
1075 None
1076 }
1077 }
1078 _ => None,
1079 }
1080 }
1081 } else {
1082 None
1083 };
1084
1085 let (disk_io, net_io) = if want_disk_io || want_net_io {
1091 let floor = std::time::Duration::from_millis(100);
1092 let elapsed = io_t0.elapsed();
1093 if elapsed < floor {
1094 std::thread::sleep(floor - elapsed);
1095 }
1096 let elapsed_secs = io_t0.elapsed().as_secs_f64();
1097 let disk_io = if want_disk_io {
1098 crate::io::compute_rates(
1099 &disk_io_sample0,
1100 &crate::io::sample_disk_io(),
1101 elapsed_secs,
1102 )
1103 .iter()
1104 .map(|r| crate::io::format_io_line(r, "R", "W"))
1105 .collect()
1106 } else {
1107 Vec::new()
1108 };
1109 let net_io = if want_net_io {
1110 let rates = crate::io::compute_rates(
1111 &net_io_sample0,
1112 &crate::io::sample_net_io(),
1113 elapsed_secs,
1114 );
1115 crate::io::select_net_rates(rates, active_interface.as_deref())
1116 .iter()
1117 .map(|r| crate::io::format_io_line(r, "RX", "TX"))
1118 .collect()
1119 } else {
1120 Vec::new()
1121 };
1122 (disk_io, net_io)
1123 } else {
1124 (Vec::new(), Vec::new())
1125 };
1126
1127 let init_system = if should_collect("init") || should_collect("init system") {
1128 crate::timing::timed("init", detect_init_system)
1129 } else {
1130 None
1131 };
1132
1133 let chassis = if should_collect("chassis") {
1134 crate::timing::timed("chassis", detect_chassis)
1135 } else {
1136 None
1137 };
1138
1139 let locale = if should_collect("locale") {
1140 std::env::var("LC_ALL")
1141 .ok()
1142 .filter(|s| !s.is_empty())
1143 .or_else(|| std::env::var("LC_MESSAGES").ok().filter(|s| !s.is_empty()))
1144 .or_else(|| std::env::var("LANG").ok().filter(|s| !s.is_empty()))
1145 } else {
1146 None
1147 };
1148
1149 let bootmgr = if should_collect("bootmgr") || should_collect("boot") {
1150 crate::timing::timed("bootmgr", detect_bootmgr)
1151 } else {
1152 None
1153 };
1154
1155 let login_manager = if should_collect("login-manager") || should_collect("lm") {
1156 crate::timing::timed("login-manager", detect_login_manager)
1157 } else {
1158 None
1159 };
1160
1161 let brightness = if should_collect("brightness") {
1162 crate::timing::timed("brightness", detect_brightness)
1163 } else {
1164 None
1165 };
1166
1167 let power_adapter = if should_collect("power-adapter") {
1168 crate::timing::timed("power-adapter", detect_power_adapter)
1169 } else {
1170 None
1171 };
1172
1173 let (keyboard, mouse) = if should_collect("keyboard") || should_collect("mouse") {
1176 let (kbds, mice) = crate::timing::timed("input", crate::input::detect_input_devices);
1177 (
1178 if should_collect("keyboard") {
1179 kbds
1180 } else {
1181 Vec::new()
1182 },
1183 if should_collect("mouse") {
1184 mice
1185 } else {
1186 Vec::new()
1187 },
1188 )
1189 } else {
1190 (Vec::new(), Vec::new())
1191 };
1192
1193 let tpm = if should_collect("tpm") {
1194 crate::timing::timed("tpm", detect_tpm)
1195 } else {
1196 None
1197 };
1198
1199 let editor = if should_collect("editor") {
1200 std::env::var("VISUAL")
1201 .ok()
1202 .filter(|s| !s.is_empty())
1203 .or_else(|| std::env::var("EDITOR").ok().filter(|s| !s.is_empty()))
1204 } else {
1205 None
1206 };
1207
1208 let wm = if should_collect("wm") || should_collect("window manager") {
1209 crate::timing::timed("wm", crate::wm::detect_wm)
1210 } else {
1211 None
1212 };
1213
1214 let dns = if should_collect("dns") {
1215 crate::timing::timed("dns", crate::network::detect_dns)
1216 } else {
1217 Vec::new()
1218 };
1219
1220 let domain = if should_collect("domain") {
1221 crate::timing::timed("domain", crate::network::detect_domain)
1222 } else {
1223 None
1224 };
1225
1226 let domain_search = if should_collect("domain-search") || should_collect("domain search") {
1227 crate::timing::timed("domain-search", crate::network::detect_domain_search)
1228 } else {
1229 Vec::new()
1230 };
1231
1232 let terminal_size = if should_collect("terminal size")
1233 || should_collect("terminal-size")
1234 || should_collect("terminal_size")
1235 {
1236 crate::timing::timed("terminal-size", crate::terminal::detect_terminal_size)
1237 } else {
1238 None
1239 };
1240
1241 let current_user = std::env::var("USER").ok();
1243
1244 let users = if should_collect("users") {
1249 #[cfg(target_os = "windows")]
1250 {
1251 crate::win_users::active_user_session_count()
1252 }
1253 #[cfg(not(target_os = "windows"))]
1254 {
1255 crate::timing::timed("users", Users::new_with_refreshed_list)
1256 .iter()
1257 .filter(|user| {
1258 user.id()
1260 .to_string()
1261 .parse::<u32>()
1262 .map(|uid| uid >= 1000)
1263 .unwrap_or(false)
1264 })
1265 .count()
1266 }
1267 } else {
1268 0
1269 };
1270
1271 let wm_theme = if should_collect("wm-theme")
1272 || should_collect("wm theme")
1273 || should_collect("wm_theme")
1274 {
1275 crate::timing::timed("wm-theme", || {
1276 crate::theme::detect_wm_theme(wm.as_deref(), desktop.as_deref())
1277 })
1278 } else {
1279 None
1280 };
1281
1282 let wallpaper = if should_collect("wallpaper") {
1283 crate::timing::timed("wallpaper", || {
1284 crate::theme::detect_wallpaper(desktop.as_deref(), wm.as_deref())
1285 })
1286 } else {
1287 None
1288 };
1289
1290 let terminal_theme = if should_collect("terminal-theme")
1291 || should_collect("terminal theme")
1292 || should_collect("terminal_theme")
1293 {
1294 crate::timing::timed("terminal-theme", || {
1295 crate::terminal::detect_terminal_theme(terminal.as_deref())
1296 })
1297 } else {
1298 None
1299 };
1300
1301 crate::timing::mark("collect-done");
1302
1303 Ok(Self {
1304 os,
1305 kernel,
1306 hostname,
1307 arch,
1308 cpu,
1309 cpu_cores,
1310 cpu_core_info,
1311 memory,
1312 swap,
1313 uptime,
1314 processes,
1315 load_avg,
1316 disks,
1317 temps,
1318 networks,
1319 boot_time,
1320 battery,
1321 shell,
1322 terminal,
1323 desktop,
1324 cpu_freq,
1325 users,
1326 gpu,
1327 packages,
1328 current_user,
1329 local_ip,
1330 public_ip,
1331 active_interface,
1332 motherboard,
1333 bios,
1334 displays,
1335 audio,
1336 wifi,
1337 bluetooth,
1338 ui_theme,
1339 icons,
1340 cursor,
1341 font,
1342 terminal_font,
1343 camera,
1344 gamepad,
1345 cpu_cache,
1346 cpu_usage,
1347 physical_disks,
1348 vulkan: gpu_apis.vulkan,
1349 opengl: gpu_apis.opengl,
1350 opencl: gpu_apis.opencl,
1351 disk_io,
1352 net_io,
1353 physical_memory,
1354 init_system,
1355 chassis,
1356 locale,
1357 bootmgr,
1358 editor,
1359 weather,
1360 wm,
1361 dns,
1362 domain,
1363 domain_search,
1364 terminal_size,
1365 btrfs,
1366 zpool,
1367 login_manager,
1368 brightness,
1369 power_adapter,
1370 keyboard,
1371 mouse,
1372 tpm,
1373 media,
1374 player,
1375 wm_theme,
1376 wallpaper,
1377 terminal_theme,
1378 })
1379 }
1380}
1381
1382pub fn detect_cpu_cache() -> Option<String> {
1388 #[cfg(target_os = "linux")]
1389 {
1390 use std::fs;
1391 let cache_dir = std::path::Path::new("/sys/devices/system/cpu/cpu0/cache");
1392 if !cache_dir.exists() {
1393 return None;
1394 }
1395
1396 struct CacheEntry {
1397 level: u32,
1398 kind: String,
1399 size_kb: u64,
1400 }
1401
1402 let mut entries: Vec<CacheEntry> = Vec::new();
1403
1404 let Ok(indices) = fs::read_dir(cache_dir) else {
1405 return None;
1406 };
1407
1408 for entry in indices.flatten() {
1409 let path = entry.path();
1410 if !path.is_dir() {
1412 continue;
1413 }
1414 let level_str = match fs::read_to_string(path.join("level")) {
1415 Ok(s) => s,
1416 Err(_) => continue,
1417 };
1418 let level: u32 = match level_str.trim().parse() {
1419 Ok(n) => n,
1420 Err(_) => continue,
1421 };
1422 let kind = match fs::read_to_string(path.join("type")) {
1423 Ok(s) => s.trim().to_string(),
1424 Err(_) => continue,
1425 };
1426 let size_str = match fs::read_to_string(path.join("size")) {
1427 Ok(s) => s,
1428 Err(_) => continue,
1429 };
1430 let size_raw = size_str.trim();
1431 let size_kb: u64 = if let Some(k) = size_raw.strip_suffix('K') {
1432 match k.parse() {
1433 Ok(n) => n,
1434 Err(_) => continue,
1435 }
1436 } else if let Some(m) = size_raw.strip_suffix('M') {
1437 match m.parse::<u64>() {
1438 Ok(n) => n * 1024,
1439 Err(_) => continue,
1440 }
1441 } else {
1442 match size_raw.parse() {
1443 Ok(n) => n,
1444 Err(_) => continue,
1445 }
1446 };
1447
1448 if kind != "Instruction" && kind != "Data" && kind != "Unified" {
1449 continue;
1450 }
1451
1452 entries.push(CacheEntry {
1453 level,
1454 kind,
1455 size_kb,
1456 });
1457 }
1458
1459 if entries.is_empty() {
1460 return None;
1461 }
1462
1463 entries.sort_by_key(|e| (e.level, e.kind.clone()));
1464
1465 let fmt_size = |kb: u64| -> String {
1466 if kb >= 1024 && kb.is_multiple_of(1024) {
1467 format!("{}M", kb / 1024)
1468 } else if kb >= 1024 {
1469 format!("{:.2}M", kb as f64 / 1024.0)
1470 .trim_end_matches('0')
1471 .trim_end_matches('.')
1472 .to_string()
1473 + "M"
1474 } else {
1475 format!("{}K", kb)
1476 }
1477 };
1478
1479 let mut seen = std::collections::HashSet::new();
1481 let mut parts: Vec<String> = Vec::new();
1482 for e in &entries {
1483 let label = match (e.level, e.kind.as_str()) {
1484 (1, "Data") => "L1d".to_string(),
1485 (1, "Instruction") => "L1i".to_string(),
1486 (1, "Unified") => "L1".to_string(),
1487 (n, _) => format!("L{}", n),
1488 };
1489 if seen.insert(label.clone()) {
1490 parts.push(format!("{}: {}", label, fmt_size(e.size_kb)));
1491 }
1492 }
1493
1494 if parts.is_empty() {
1495 None
1496 } else {
1497 Some(parts.join(", "))
1498 }
1499 }
1500 #[cfg(target_os = "macos")]
1501 {
1502 extern "C" {
1503 fn sysctlbyname(
1504 name: *const i8,
1505 oldp: *mut std::ffi::c_void,
1506 oldlenp: *mut usize,
1507 newp: *mut std::ffi::c_void,
1508 newlen: usize,
1509 ) -> i32;
1510 }
1511
1512 let read_u64 = |key: &str| -> Option<u64> {
1513 let name = std::ffi::CString::new(key).ok()?;
1514 let mut value: u64 = 0;
1515 let mut size = std::mem::size_of::<u64>();
1516 let ret = unsafe {
1517 sysctlbyname(
1518 name.as_ptr(),
1519 &mut value as *mut u64 as *mut std::ffi::c_void,
1520 &mut size,
1521 std::ptr::null_mut(),
1522 0,
1523 )
1524 };
1525 if ret == 0 && value > 0 {
1526 Some(value)
1527 } else {
1528 None
1529 }
1530 };
1531
1532 let fmt_bytes = |bytes: u64| -> String {
1533 if bytes >= 1024 * 1024 {
1534 format!("{}M", bytes / (1024 * 1024))
1535 } else {
1536 format!("{}K", bytes / 1024)
1537 }
1538 };
1539
1540 let mut parts = Vec::new();
1541 if let Some(v) = read_u64("hw.l1dcachesize") {
1542 parts.push(format!("L1d: {}", fmt_bytes(v)));
1543 }
1544 if let Some(v) = read_u64("hw.l1icachesize") {
1545 parts.push(format!("L1i: {}", fmt_bytes(v)));
1546 }
1547 if let Some(v) = read_u64("hw.l2cachesize") {
1548 parts.push(format!("L2: {}", fmt_bytes(v)));
1549 }
1550 if let Some(v) = read_u64("hw.l3cachesize") {
1551 parts.push(format!("L3: {}", fmt_bytes(v)));
1552 }
1553
1554 if parts.is_empty() {
1555 None
1556 } else {
1557 Some(parts.join(", "))
1558 }
1559 }
1560 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1561 {
1562 None
1563 }
1564}
1565
1566pub fn format_cpu_cores(logical: usize, physical: Option<usize>) -> String {
1571 hybrid_cores(logical).unwrap_or_else(|| format_cpu_cores_plain(logical, physical))
1572}
1573
1574fn hybrid_cores(logical: usize) -> Option<String> {
1579 #[cfg(target_os = "linux")]
1580 {
1581 detect_hybrid_cores(logical)
1582 }
1583 #[cfg(target_os = "macos")]
1584 {
1585 detect_macos_hybrid_cores(logical)
1586 }
1587 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1588 {
1589 let _ = logical;
1590 None
1591 }
1592}
1593
1594fn format_cpu_cores_plain(logical: usize, physical: Option<usize>) -> String {
1604 match physical {
1605 Some(p) if p < logical => format!("{}C / {}T", p, logical),
1606 _ => format!("{} cores", logical),
1607 }
1608}
1609
1610#[cfg(target_os = "linux")]
1613fn detect_hybrid_cores(logical: usize) -> Option<String> {
1614 hybrid_cores_in(
1615 std::path::Path::new("/sys/devices/system/cpu/cpufreq"),
1616 logical,
1617 )
1618}
1619
1620#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
1630fn hybrid_cores_in(cpufreq: &std::path::Path, logical: usize) -> Option<String> {
1631 use std::collections::HashMap;
1632 use std::fs;
1633
1634 let policies: Vec<std::path::PathBuf> = fs::read_dir(cpufreq)
1635 .ok()?
1636 .flatten()
1637 .map(|e| e.path())
1638 .filter(|p| p.is_dir())
1639 .collect();
1640 let one_cpu_each = policies.len() == logical;
1641
1642 let mut freq_to_count: HashMap<u64, usize> = HashMap::new();
1644 let mut total_accounted = 0usize;
1645 for path in &policies {
1646 let max_freq: u64 = fs::read_to_string(path.join("cpuinfo_max_freq"))
1647 .ok()?
1648 .trim()
1649 .parse()
1650 .ok()?;
1651 let count = if one_cpu_each {
1652 1
1653 } else {
1654 fs::read_to_string(path.join("affected_cpus"))
1655 .ok()?
1656 .split_whitespace()
1657 .count()
1658 };
1659 *freq_to_count.entry(max_freq).or_insert(0) += count;
1660 total_accounted += count;
1661 if freq_to_count.len() > 2 {
1662 return None;
1663 }
1664 }
1665 hybrid_label(&freq_to_count, total_accounted, logical)
1666}
1667
1668#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
1671fn hybrid_label(
1672 freq_to_count: &std::collections::HashMap<u64, usize>,
1673 total_accounted: usize,
1674 logical: usize,
1675) -> Option<String> {
1676 if freq_to_count.len() != 2 || total_accounted != logical {
1677 return None;
1678 }
1679 let mut tiers: Vec<(u64, usize)> = freq_to_count.iter().map(|(f, c)| (*f, *c)).collect();
1680 tiers.sort_by_key(|t| std::cmp::Reverse(t.0)); Some(format!("{}P + {}E / {}T", tiers[0].1, tiers[1].1, logical))
1682}
1683
1684#[cfg(target_os = "macos")]
1687fn detect_macos_hybrid_cores(logical: usize) -> Option<String> {
1688 extern "C" {
1689 fn sysctlbyname(
1690 name: *const i8,
1691 oldp: *mut std::ffi::c_void,
1692 oldlenp: *mut usize,
1693 newp: *mut std::ffi::c_void,
1694 newlen: usize,
1695 ) -> i32;
1696 }
1697
1698 let read_u32 = |key: &str| -> Option<u32> {
1699 let name = std::ffi::CString::new(key).ok()?;
1700 let mut value: u32 = 0;
1701 let mut size = std::mem::size_of::<u32>();
1702 let ret = unsafe {
1703 sysctlbyname(
1704 name.as_ptr(),
1705 &mut value as *mut u32 as *mut std::ffi::c_void,
1706 &mut size,
1707 std::ptr::null_mut(),
1708 0,
1709 )
1710 };
1711 if ret == 0 {
1712 Some(value)
1713 } else {
1714 None
1715 }
1716 };
1717
1718 let nlevels = read_u32("hw.nperflevels")?;
1720 if nlevels != 2 {
1721 return None;
1722 }
1723
1724 let p_cores = read_u32("hw.perflevel0.logicalcpu")? as usize;
1725 let e_cores = read_u32("hw.perflevel1.logicalcpu")? as usize;
1726
1727 if p_cores + e_cores != logical {
1728 return None;
1729 }
1730
1731 Some(format!("{}P + {}E / {}T", p_cores, e_cores, logical))
1732}
1733
1734pub fn detect_cpu_freq_range() -> Option<(u64, u64)> {
1737 #[cfg(target_os = "linux")]
1738 {
1739 use std::fs;
1740 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
1741 if !cpufreq.exists() {
1742 return None;
1743 }
1744 let mut global_min: Option<u64> = None;
1745 let mut global_max: Option<u64> = None;
1746 let Ok(policies) = fs::read_dir(cpufreq) else {
1747 return None;
1748 };
1749 for policy in policies.flatten() {
1750 let path = policy.path();
1751 if !path.is_dir() {
1752 continue;
1753 }
1754 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_min_freq")) {
1755 if let Ok(v) = s.trim().parse::<u64>() {
1756 global_min = Some(global_min.map_or(v, |m: u64| m.min(v)));
1757 }
1758 }
1759 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_max_freq")) {
1760 if let Ok(v) = s.trim().parse::<u64>() {
1761 global_max = Some(global_max.map_or(v, |m: u64| m.max(v)));
1762 }
1763 }
1764 }
1765 match (global_min, global_max) {
1766 (Some(min), Some(max)) => Some((min, max)),
1767 _ => None,
1768 }
1769 }
1770 #[cfg(not(target_os = "linux"))]
1771 {
1772 None
1773 }
1774}
1775
1776#[cfg(not(target_os = "linux"))]
1777fn detect_desktop_from_proc() -> Option<String> {
1778 None
1779}
1780
1781#[cfg(target_os = "linux")]
1782fn detect_desktop_from_proc() -> Option<String> {
1783 const DE_PROCS: &[(&str, &str)] = &[
1784 ("gnome-shell", "GNOME"),
1785 ("plasmashell", "KDE Plasma"),
1786 ("xfce4-session", "XFCE"),
1787 ("mate-session", "MATE"),
1788 ("cinnamon", "Cinnamon"),
1789 ("budgie-daemon", "Budgie"),
1790 ("budgie-panel", "Budgie"),
1791 ("lxsession", "LXDE"),
1792 ("lxqt-session", "LXQt"),
1793 ("deepin-session", "Deepin"),
1794 ("dde-session-daemon", "Deepin"),
1795 ("gala", "Pantheon"),
1796 ("enlightenment", "Enlightenment"),
1797 ];
1798 let Ok(entries) = std::fs::read_dir("/proc") else {
1799 return None;
1800 };
1801 for entry in entries.filter_map(|e| e.ok()) {
1802 let path = entry.path();
1803 if !path.is_dir() {
1804 continue;
1805 }
1806 let Ok(comm) = std::fs::read_to_string(path.join("comm")) else {
1807 continue;
1808 };
1809 let comm = comm.trim().to_lowercase();
1810 for (proc_name, de_name) in DE_PROCS {
1811 if comm == *proc_name || comm.starts_with(proc_name) {
1812 return Some(de_name.to_string());
1813 }
1814 }
1815 }
1816 None
1817}
1818
1819fn normalize_desktop_name(raw: &str) -> String {
1820 let s = raw.trim();
1821 match s.to_lowercase().as_str() {
1823 "gnome" => "GNOME".to_string(),
1824 "kde" | "kde plasma" | "plasma" => "KDE Plasma".to_string(),
1825 "xfce" => "XFCE".to_string(),
1826 "lxde" => "LXDE".to_string(),
1827 "lxqt" => "LXQt".to_string(),
1828 "mate" => "MATE".to_string(),
1829 "cinnamon" => "Cinnamon".to_string(),
1830 "budgie" => "Budgie".to_string(),
1831 "deepin" => "Deepin".to_string(),
1832 "pantheon" => "Pantheon".to_string(),
1833 "unity" => "Unity".to_string(),
1834 "enlightenment" | "e" => "Enlightenment".to_string(),
1835 _ => {
1836 if s.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()) {
1838 let mut chars = s.chars();
1839 match chars.next() {
1840 None => String::new(),
1841 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
1842 }
1843 } else {
1844 s.to_string()
1845 }
1846 }
1847 }
1848}
1849
1850fn detect_init_system() -> Option<String> {
1851 #[cfg(target_os = "linux")]
1852 {
1853 let comm = std::fs::read_to_string("/proc/1/comm")
1854 .map(|s| s.trim().to_string())
1855 .ok()
1856 .filter(|s| !s.is_empty());
1857 if let Some(name) = comm {
1858 return Some(name);
1859 }
1860 std::fs::read_link("/proc/1/exe").ok().and_then(|p| {
1861 p.file_name()
1862 .and_then(|n| n.to_str())
1863 .map(|s| s.to_string())
1864 })
1865 }
1866 #[cfg(target_os = "macos")]
1867 {
1868 Some("launchd".to_string())
1869 }
1870 #[cfg(target_os = "windows")]
1871 {
1872 Some("SCM".to_string())
1873 }
1874 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1875 {
1876 None
1877 }
1878}
1879
1880fn detect_chassis() -> Option<String> {
1881 #[cfg(target_os = "linux")]
1882 {
1883 let raw = std::fs::read_to_string("/sys/class/dmi/id/chassis_type").ok()?;
1884 let n: u32 = raw.trim().parse().ok()?;
1885 let label = match n {
1886 3 => "Desktop",
1887 4 => "Low-Profile Desktop",
1888 6 => "Mini Tower",
1889 7 => "Tower",
1890 8 | 9 | 10 | 14 | 31 | 32 => "Laptop",
1891 11 => "Handheld",
1892 13 => "All-in-One",
1893 17 => "Main Server",
1894 23 => "Rack Server",
1895 28 => "Blade",
1896 30 => "Tablet",
1897 35 => "Mini PC",
1898 36 => "Stick PC",
1899 _ => return None,
1900 };
1901 Some(label.to_string())
1902 }
1903 #[cfg(target_os = "macos")]
1904 {
1905 let output = std::process::Command::new("sysctl")
1906 .args(["-n", "hw.model"])
1907 .output()
1908 .ok()?;
1909 let model = String::from_utf8(output.stdout).ok()?;
1910 let model = model.trim();
1911 if model.contains("MacBook") {
1912 Some("Laptop".to_string())
1913 } else if model.contains("MacPro") {
1914 Some("Desktop".to_string())
1915 } else if model.contains("Macmini") || model.contains("Mac mini") {
1916 Some("Mini PC".to_string())
1917 } else if model.contains("iMac") {
1918 Some("All-in-One".to_string())
1919 } else {
1920 Some(model.to_string())
1921 }
1922 }
1923 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1924 {
1925 None
1926 }
1927}
1928
1929fn detect_bootmgr() -> Option<String> {
1930 #[cfg(target_os = "linux")]
1931 {
1932 use std::path::Path;
1933 let is_uefi = Path::new("/sys/firmware/efi").exists();
1934 if Path::new("/boot/loader/entries").exists()
1935 || Path::new("/boot/loader/loader.conf").exists()
1936 || Path::new("/efi/loader/loader.conf").exists()
1937 {
1938 return Some("systemd-boot".to_string());
1939 }
1940 if Path::new("/boot/grub2/grub.cfg").exists() || Path::new("/boot/grub2").exists() {
1941 return Some("GRUB 2".to_string());
1942 }
1943 if Path::new("/boot/grub/grub.cfg").exists() || Path::new("/boot/grub").exists() {
1944 return Some("GRUB".to_string());
1945 }
1946 if is_uefi {
1947 Some("UEFI".to_string())
1948 } else {
1949 Some("BIOS".to_string())
1950 }
1951 }
1952 #[cfg(target_os = "macos")]
1953 {
1954 Some("Apple Boot ROM".to_string())
1955 }
1956 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1957 {
1958 None
1959 }
1960}
1961
1962fn detect_login_manager() -> Option<String> {
1969 #[cfg(target_os = "linux")]
1970 {
1971 let target = std::fs::read_link("/etc/systemd/system/display-manager.service").ok()?;
1972 let unit = target.file_name().and_then(|n| n.to_str())?;
1973 login_manager_from_unit(unit)
1974 }
1975 #[cfg(target_os = "macos")]
1976 {
1977 let plist = std::fs::read_to_string(LOGINWINDOW_PLIST).ok()?;
1981 Some(format_login_window(parse_plist_string(
1982 &plist,
1983 "CFBundleShortVersionString",
1984 )))
1985 }
1986 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1987 {
1988 None
1989 }
1990}
1991
1992#[cfg(target_os = "macos")]
1994const LOGINWINDOW_PLIST: &str = "/System/Library/CoreServices/loginwindow.app/Contents/Info.plist";
1995
1996#[cfg(any(target_os = "macos", test))]
2007fn parse_plist_string(xml: &str, key: &str) -> Option<String> {
2008 let needle = format!("<key>{key}</key>");
2009 let rest = xml.split_once(&needle)?.1;
2010 let open = rest.find("<string>")?;
2011 if let Some(next_key) = rest.find("<key>") {
2014 if next_key < open {
2015 return None;
2016 }
2017 }
2018 let after = &rest[open + "<string>".len()..];
2019 let end = after.find("</string>")?;
2020 let value = after[..end].trim();
2021 (!value.is_empty()).then(|| value.to_string())
2022}
2023
2024#[cfg(any(target_os = "macos", test))]
2028fn format_login_window(version: Option<String>) -> String {
2029 match version {
2030 Some(v) => format!("Login Window {v}"),
2031 None => "Login Window".to_string(),
2032 }
2033}
2034
2035#[cfg(target_os = "linux")]
2041fn login_manager_from_unit(unit: &str) -> Option<String> {
2042 let stem = unit.strip_suffix(".service").unwrap_or(unit).trim();
2043 if stem.is_empty() {
2044 return None;
2045 }
2046 let pretty = match stem.to_lowercase().as_str() {
2047 "gdm" | "gdm3" => "GDM",
2048 "sddm" => "SDDM",
2049 "lightdm" => "LightDM",
2050 "lxdm" => "LXDM",
2051 "xdm" => "XDM",
2052 "ly" => "Ly",
2053 "greetd" => "greetd",
2054 "slim" => "SLiM",
2055 "nodm" => "nodm",
2056 "entrance" => "Entrance",
2057 _ => {
2058 let mut chars = stem.chars();
2060 return chars
2061 .next()
2062 .map(|c| c.to_uppercase().collect::<String>() + chars.as_str());
2063 }
2064 };
2065 Some(pretty.to_string())
2066}
2067
2068fn detect_brightness() -> Option<String> {
2075 #[cfg(target_os = "linux")]
2076 {
2077 use std::path::Path;
2078 let dir = Path::new("/sys/class/backlight");
2079 if !dir.exists() {
2080 return None;
2081 }
2082 let mut devices: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
2085 .ok()?
2086 .flatten()
2087 .map(|e| e.path())
2088 .collect();
2089 devices.sort_by_key(|p| {
2090 let name = p
2091 .file_name()
2092 .and_then(|n| n.to_str())
2093 .unwrap_or("")
2094 .to_lowercase();
2095 if name.contains("acpi") || name.contains("video") {
2097 1
2098 } else {
2099 0
2100 }
2101 });
2102 for dev in devices {
2103 let cur = std::fs::read_to_string(dev.join("brightness"))
2104 .ok()
2105 .and_then(|s| s.trim().parse::<u64>().ok());
2106 let max = std::fs::read_to_string(dev.join("max_brightness"))
2107 .ok()
2108 .and_then(|s| s.trim().parse::<u64>().ok());
2109 if let (Some(cur), Some(max)) = (cur, max) {
2110 if let Some(pct) = brightness_percent(cur, max) {
2111 return Some(pct);
2112 }
2113 }
2114 }
2115 None
2116 }
2117 #[cfg(target_os = "macos")]
2118 {
2119 let (value, min, max) = crate::macos_ffi::get_backlight_brightness()?;
2120 let span = max.checked_sub(min)?;
2125 let level = value.checked_sub(min)?;
2126 if span <= 0 || level < 0 {
2127 return None;
2128 }
2129 brightness_percent(level as u64, span as u64)
2130 }
2131 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
2132 {
2133 None
2134 }
2135}
2136
2137#[cfg(any(target_os = "linux", target_os = "macos"))]
2146fn brightness_percent(cur: u64, max: u64) -> Option<String> {
2147 if max == 0 {
2148 return None;
2149 }
2150 let pct = (cur as f64 / max as f64 * 100.0).round() as u64;
2151 Some(format!("{}%", pct))
2152}
2153
2154fn detect_power_adapter() -> Option<String> {
2162 #[cfg(target_os = "linux")]
2163 {
2164 use std::path::Path;
2165 let dir = Path::new("/sys/class/power_supply");
2166 if !dir.exists() {
2167 return None;
2168 }
2169 for entry in std::fs::read_dir(dir).ok()?.flatten() {
2170 let path = entry.path();
2171 let supply_type = std::fs::read_to_string(path.join("type"))
2172 .map(|s| s.trim().to_string())
2173 .unwrap_or_default();
2174 if supply_type != "Mains" {
2175 continue;
2176 }
2177 let name = path
2178 .file_name()
2179 .and_then(|n| n.to_str())
2180 .unwrap_or("AC")
2181 .to_string();
2182 let online = std::fs::read_to_string(path.join("online"))
2183 .map(|s| s.trim().to_string())
2184 .unwrap_or_default();
2185 return Some(format_power_adapter(&name, &online));
2186 }
2187 None
2188 }
2189 #[cfg(target_os = "macos")]
2190 {
2191 let watts = crate::macos_ffi::get_power_adapter_watts()?;
2196 Some(format_power_adapter_watts(watts))
2197 }
2198 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
2199 {
2200 None
2201 }
2202}
2203
2204#[cfg(any(target_os = "macos", test))]
2211fn format_power_adapter_watts(watts: i64) -> String {
2212 if watts > 0 {
2213 format!("{watts}W (connected)")
2214 } else {
2215 "connected".to_string()
2216 }
2217}
2218
2219#[cfg(target_os = "linux")]
2223fn format_power_adapter(name: &str, online: &str) -> String {
2224 let state = match online.trim() {
2225 "1" => "connected",
2226 "0" => "not connected",
2227 _ => "unknown",
2228 };
2229 format!("{} ({})", name, state)
2230}
2231
2232fn detect_tpm() -> Option<String> {
2240 #[cfg(target_os = "linux")]
2241 {
2242 use std::path::Path;
2243 let dir = Path::new("/sys/class/tpm");
2244 if !dir.exists() {
2245 return None;
2246 }
2247 let mut devices: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
2248 .ok()?
2249 .flatten()
2250 .map(|e| e.path())
2251 .collect();
2252 devices.sort();
2254 for dev in devices {
2255 if let Ok(major) = std::fs::read_to_string(dev.join("tpm_version_major")) {
2256 if let Some(v) = format_tpm_version(major.trim()) {
2257 return Some(v);
2258 }
2259 }
2260 }
2261 None
2262 }
2263 #[cfg(not(target_os = "linux"))]
2264 {
2265 None
2266 }
2267}
2268
2269#[cfg(target_os = "linux")]
2276fn format_tpm_version(major: &str) -> Option<String> {
2277 match major.trim() {
2278 "1" => Some("1.2".to_string()),
2279 "2" => Some("2.0".to_string()),
2280 _ => None,
2281 }
2282}
2283
2284#[cfg(target_os = "windows")]
2289mod win_cpu {
2290 #[repr(C)]
2291 struct FileTime {
2292 low: u32,
2293 high: u32,
2294 }
2295
2296 impl FileTime {
2297 fn ticks(&self) -> u64 {
2298 ((self.high as u64) << 32) | self.low as u64
2299 }
2300 }
2301
2302 extern "system" {
2303 fn GetSystemTimes(idle: *mut FileTime, kernel: *mut FileTime, user: *mut FileTime) -> i32;
2304 }
2305
2306 pub fn sample() -> Option<(u64, u64, u64)> {
2309 let mut idle = FileTime { low: 0, high: 0 };
2310 let mut kernel = FileTime { low: 0, high: 0 };
2311 let mut user = FileTime { low: 0, high: 0 };
2312 let ok = unsafe { GetSystemTimes(&mut idle, &mut kernel, &mut user) };
2314 if ok == 0 {
2315 None
2316 } else {
2317 Some((idle.ticks(), kernel.ticks(), user.ticks()))
2318 }
2319 }
2320
2321 pub fn usage_percent(s0: (u64, u64, u64), s1: (u64, u64, u64)) -> f32 {
2324 let idle = s1.0.saturating_sub(s0.0);
2325 let kernel = s1.1.saturating_sub(s0.1);
2326 let user = s1.2.saturating_sub(s0.2);
2327 let total = kernel + user;
2328 if total == 0 {
2329 0.0
2330 } else {
2331 (100.0 * total.saturating_sub(idle) as f64 / total as f64) as f32
2332 }
2333 }
2334
2335 #[cfg(test)]
2336 mod layout {
2337 use std::mem::size_of;
2338
2339 #[test]
2341 fn filetime_size() {
2342 assert_eq!(size_of::<super::FileTime>(), 8);
2343 }
2344 }
2345}
2346
2347#[cfg(test)]
2348mod tests {
2349
2350 fn fake_cpufreq(name: &str, policies: &[(u64, Option<&str>)]) -> std::path::PathBuf {
2353 let root =
2354 std::env::temp_dir().join(format!("retch-cpufreq-{name}-{}", std::process::id()));
2355 let _ = std::fs::remove_dir_all(&root);
2356 for (i, (freq, affected)) in policies.iter().enumerate() {
2357 let dir = root.join(format!("policy{i}"));
2358 std::fs::create_dir_all(&dir).unwrap();
2359 std::fs::write(dir.join("cpuinfo_max_freq"), format!("{freq}\n")).unwrap();
2360 if let Some(a) = affected {
2361 std::fs::write(dir.join("affected_cpus"), a).unwrap();
2362 }
2363 }
2364 root
2365 }
2366
2367 #[test]
2368 fn hybrid_per_cpu_policies_need_no_affected_cpus() {
2369 let mut p: Vec<(u64, Option<&str>)> = vec![(5_000_000, None); 4];
2372 p.extend(vec![(3_800_000, None); 8]);
2373 let root = fake_cpufreq("percpu", &p);
2374 assert_eq!(hybrid_cores_in(&root, 12).as_deref(), Some("4P + 8E / 12T"));
2375 let _ = std::fs::remove_dir_all(&root);
2376 }
2377
2378 #[test]
2379 fn hybrid_cluster_policies_still_read_affected_cpus() {
2380 let root = fake_cpufreq(
2383 "cluster",
2384 &[
2385 (2_400_000, Some("0 1\n")),
2386 (1_800_000, Some("2 3 4 5 6 7\n")),
2387 ],
2388 );
2389 assert_eq!(hybrid_cores_in(&root, 8).as_deref(), Some("2P + 6E / 8T"));
2390 let _ = std::fs::remove_dir_all(&root);
2391 }
2392
2393 #[test]
2394 fn hybrid_is_none_for_one_or_three_tiers() {
2395 let one = fake_cpufreq("one", &vec![(3_000_000, None); 8]);
2396 assert_eq!(
2397 hybrid_cores_in(&one, 8),
2398 None,
2399 "a single tier is not hybrid"
2400 );
2401 let _ = std::fs::remove_dir_all(&one);
2402 let three = fake_cpufreq(
2403 "three",
2404 &[(3_000_000, None), (2_000_000, None), (1_000_000, None)],
2405 );
2406 assert_eq!(hybrid_cores_in(&three, 3), None, "three tiers are not P/E");
2407 let _ = std::fs::remove_dir_all(&three);
2408 }
2409
2410 #[test]
2411 fn hybrid_is_none_when_policies_do_not_cover_every_cpu() {
2412 let root = fake_cpufreq(
2413 "partial",
2414 &[(5_000_000, Some("0 1\n")), (3_000_000, Some("2\n"))],
2415 );
2416 assert_eq!(hybrid_cores_in(&root, 8), None);
2417 let _ = std::fs::remove_dir_all(&root);
2418 }
2419
2420 #[test]
2421 fn remaining_wait_covers_only_what_the_work_in_between_did_not() {
2422 use std::time::Duration;
2423 let ms = Duration::from_millis;
2424 assert_eq!(remaining_wait(ms(200), ms(0)), ms(200), "nothing elapsed");
2425 assert_eq!(remaining_wait(ms(200), ms(157)), ms(43), "the remainder");
2426 assert_eq!(remaining_wait(ms(200), ms(200)), ms(0), "exactly spent");
2427 assert_eq!(
2430 remaining_wait(ms(200), ms(237)),
2431 ms(0),
2432 "overspent, no wait"
2433 );
2434 }
2435
2436 #[test]
2437 fn exclude_own_usage_removes_retchs_share_of_the_machine() {
2438 use std::time::Duration;
2439 let ms = Duration::from_millis;
2440 assert!((exclude_own_usage(20.0, ms(100), ms(200), 4) - 7.5).abs() < 1e-4);
2442 assert!((exclude_own_usage(3.0, ms(100), ms(200), 32) - 1.4375).abs() < 1e-4);
2444 assert_eq!(exclude_own_usage(5.0, ms(0), ms(200), 8), 5.0);
2446 }
2447
2448 #[test]
2449 fn exclude_own_usage_never_goes_negative_or_divides_by_zero() {
2450 use std::time::Duration;
2451 let ms = Duration::from_millis;
2452 assert_eq!(exclude_own_usage(1.0, ms(100), ms(200), 4), 0.0, "clamped");
2453 assert_eq!(
2454 exclude_own_usage(5.0, ms(10), ms(0), 4),
2455 5.0,
2456 "empty window"
2457 );
2458 assert_eq!(exclude_own_usage(5.0, ms(10), ms(200), 0), 5.0, "no CPUs");
2459 }
2460
2461 #[cfg(not(target_os = "windows"))]
2464 #[test]
2465 fn own_cpu_time_counts_work_done_by_this_process() {
2466 let before = own_cpu_time();
2467 let t = std::time::Instant::now();
2468 let mut x: u64 = 0;
2469 while t.elapsed() < std::time::Duration::from_millis(50) {
2470 x = std::hint::black_box(x.wrapping_mul(6364136223846793005).wrapping_add(1));
2471 }
2472 let used = own_cpu_time().saturating_sub(before);
2473 assert!(
2474 used >= std::time::Duration::from_millis(20),
2475 "50 ms of busy work registered as {used:?}"
2476 );
2477 }
2478
2479 #[cfg(not(target_os = "windows"))]
2484 #[test]
2485 fn cpu_usage_alone_still_waits_out_the_minimum_interval() {
2486 let opts = CollectOptions {
2487 fields: Some(vec!["cpu-usage".to_string()]),
2488 ..Default::default()
2489 };
2490 let t0 = std::time::Instant::now();
2491 let _ = SystemInfo::collect(opts).expect("collect");
2492 let elapsed = t0.elapsed();
2493 assert!(
2494 elapsed >= sysinfo::MINIMUM_CPU_UPDATE_INTERVAL,
2495 "cpu-usage alone took {elapsed:?}, under sysinfo's {:?} minimum",
2496 sysinfo::MINIMUM_CPU_UPDATE_INTERVAL
2497 );
2498 }
2499
2500 #[test]
2501 fn cpu_refresh_kind_is_none_unless_freq_or_usage_needs_sys() {
2502 assert!(cpu_refresh_kind(false, false).is_none());
2504 }
2505
2506 #[test]
2507 fn cpu_refresh_kind_for_plain_cpu_asks_for_neither_flag() {
2508 let kind = own_cpu_refresh_kind();
2512 assert!(!kind.frequency(), "plain `cpu` must not request frequency");
2513 assert!(!kind.cpu_usage(), "plain `cpu` must not request cpu usage");
2514 }
2515
2516 #[test]
2517 fn cpu_refresh_kind_asks_for_frequency_only_for_cpu_freq() {
2518 let kind = cpu_refresh_kind(true, false).expect("cpu-freq selected");
2519 assert!(
2520 kind.frequency(),
2521 "`cpu-freq` reads Cpu::frequency() and must request it"
2522 );
2523 }
2524
2525 #[test]
2526 fn cpu_refresh_kind_asks_for_usage_only_off_windows() {
2527 let kind = cpu_refresh_kind(false, true).expect("cpu-usage selected");
2528 if cfg!(target_os = "windows") {
2529 assert!(!kind.cpu_usage());
2532 } else {
2533 assert!(kind.cpu_usage());
2535 }
2536 assert!(!kind.frequency(), "cpu-usage must not drag in frequency");
2537 }
2538
2539 fn collect_fields(fields: &[&str]) -> SystemInfo {
2540 SystemInfo::collect(CollectOptions {
2541 fields: Some(fields.iter().map(|f| f.to_string()).collect()),
2542 ..Default::default()
2543 })
2544 .expect("collect")
2545 }
2546
2547 #[test]
2552 fn cpu_fields_are_the_same_whichever_path_fills_them() {
2553 let alone = collect_fields(&["cpu"]);
2554 let with_freq = collect_fields(&["cpu", "cpu-freq"]);
2555 assert!(!alone.cpu.is_empty(), "name read by the probe itself");
2556 assert!(
2557 alone.cpu_cores > 0,
2558 "logical count read by the probe itself"
2559 );
2560 assert!(!alone.cpu_core_info.is_empty());
2561 assert_eq!(alone.cpu, with_freq.cpu, "same name from either path");
2562 assert_eq!(alone.cpu_cores, with_freq.cpu_cores);
2563 assert_eq!(alone.cpu_core_info, with_freq.cpu_core_info);
2564 }
2565
2566 #[test]
2569 fn cpu_core_string_matches_format_cpu_cores() {
2570 let info = collect_fields(&["cpu"]);
2571 assert_eq!(
2572 info.cpu_core_info,
2573 format_cpu_cores(info.cpu_cores, System::physical_core_count())
2574 );
2575 }
2576
2577 #[cfg(target_os = "windows")]
2580 #[test]
2581 fn windows_registry_has_a_cpu_name() {
2582 let name = windows_cpu_name();
2583 assert!(
2584 name.as_deref().is_some_and(|n| !n.is_empty()),
2585 "ProcessorNameString missing: {name:?}"
2586 );
2587 }
2588
2589 #[test]
2590 fn cpu_fields_stay_empty_when_cpu_is_not_requested() {
2591 let info = collect_fields(&["os"]);
2592 assert!(info.cpu.is_empty());
2593 assert_eq!(info.cpu_cores, 0);
2594 assert!(info.cpu_core_info.is_empty());
2595 }
2596
2597 #[test]
2598 fn load_is_probed_only_when_selected_and_never_on_windows() {
2599 assert!(
2600 !should_probe_load(false),
2601 "an unselected `load` must not be probed on any platform"
2602 );
2603 if cfg!(target_os = "windows") {
2604 assert!(
2605 !should_probe_load(true),
2606 "sysinfo's Windows load average samples every 5 s from a zeroed static, so it can only ever report 0.00 in a process this short-lived"
2607 );
2608 } else {
2609 assert!(should_probe_load(true));
2610 }
2611 }
2612
2613 #[test]
2614 fn shell_and_terminal_each_load_the_process_list_on_their_own() {
2615 let expected = !cfg!(target_os = "linux");
2622 assert_eq!(
2623 needs_process_list(false, false, true, false),
2624 expected,
2625 "shell alone"
2626 );
2627 assert_eq!(
2628 needs_process_list(false, false, false, true),
2629 expected,
2630 "terminal alone"
2631 );
2632 assert_eq!(
2633 needs_process_list(true, false, false, false),
2634 expected,
2635 "procs alone"
2636 );
2637 assert_eq!(
2638 needs_process_list(false, true, false, false),
2639 expected,
2640 "audio alone"
2641 );
2642 assert!(
2643 !needs_process_list(false, false, false, false),
2644 "no consumer selected: the list must not be loaded"
2645 );
2646 }
2647
2648 use super::*;
2649
2650 #[cfg(target_os = "linux")]
2651 #[test]
2652 fn test_login_manager_from_unit() {
2653 assert_eq!(
2654 login_manager_from_unit("gdm.service").as_deref(),
2655 Some("GDM")
2656 );
2657 assert_eq!(
2658 login_manager_from_unit("gdm3.service").as_deref(),
2659 Some("GDM")
2660 );
2661 assert_eq!(
2662 login_manager_from_unit("sddm.service").as_deref(),
2663 Some("SDDM")
2664 );
2665 assert_eq!(
2666 login_manager_from_unit("lightdm.service").as_deref(),
2667 Some("LightDM")
2668 );
2669 assert_eq!(
2671 login_manager_from_unit("emptty.service").as_deref(),
2672 Some("Emptty")
2673 );
2674 assert_eq!(login_manager_from_unit("ly").as_deref(), Some("Ly"));
2676 assert_eq!(login_manager_from_unit("").as_deref(), None);
2678 assert_eq!(login_manager_from_unit(".service").as_deref(), None);
2679 }
2680
2681 #[cfg(any(target_os = "linux", target_os = "macos"))]
2682 #[test]
2683 fn test_brightness_percent() {
2684 assert_eq!(brightness_percent(50, 100).as_deref(), Some("50%"));
2685 assert_eq!(brightness_percent(100, 100).as_deref(), Some("100%"));
2686 assert_eq!(brightness_percent(0, 100).as_deref(), Some("0%"));
2687 assert_eq!(brightness_percent(133, 255).as_deref(), Some("52%"));
2689 assert_eq!(brightness_percent(10, 0), None);
2691 }
2692
2693 #[test]
2697 fn test_parse_plist_string() {
2698 const PLIST: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
2699<plist version="1.0">
2700<dict>
2701 <key>CFBundleName</key>
2702 <string>loginwindow</string>
2703 <key>CFBundleShortVersionString</key>
2704 <string>9.0</string>
2705 <key>CFBundleVersion</key>
2706 <string>3085.6.3</string>
2707</dict>
2708</plist>"#;
2709 assert_eq!(
2710 parse_plist_string(PLIST, "CFBundleShortVersionString").as_deref(),
2711 Some("9.0")
2712 );
2713 assert_eq!(
2714 parse_plist_string(PLIST, "CFBundleVersion").as_deref(),
2715 Some("3085.6.3")
2716 );
2717 assert_eq!(parse_plist_string(PLIST, "NoSuchKey"), None);
2720 }
2721
2722 #[test]
2727 fn test_parse_plist_string_rejects_a_non_string_value() {
2728 const PLIST: &str = r#"<dict>
2729 <key>Flag</key>
2730 <true/>
2731 <key>Other</key>
2732 <string>unrelated</string>
2733</dict>"#;
2734 assert_eq!(parse_plist_string(PLIST, "Flag"), None);
2735 assert_eq!(
2736 parse_plist_string(PLIST, "Other").as_deref(),
2737 Some("unrelated")
2738 );
2739 }
2740
2741 #[test]
2742 fn test_format_login_window() {
2743 assert_eq!(format_login_window(Some("9.0".into())), "Login Window 9.0");
2744 assert_eq!(format_login_window(None), "Login Window");
2747 }
2748
2749 #[test]
2750 fn test_format_power_adapter_watts() {
2751 assert_eq!(format_power_adapter_watts(96), "96W (connected)");
2752 assert_eq!(format_power_adapter_watts(0), "connected");
2755 assert_eq!(format_power_adapter_watts(-1), "connected");
2756 }
2757
2758 #[cfg(any(target_os = "linux", target_os = "macos"))]
2762 #[test]
2763 fn test_backlight_triple_rebasing() {
2764 assert_eq!(
2766 brightness_percent((32768i64 - 0) as u64, (65536i64 - 0) as u64).as_deref(),
2767 Some("50%")
2768 );
2769 assert_eq!(
2772 brightness_percent((200i64 - 100) as u64, (300i64 - 100) as u64).as_deref(),
2773 Some("50%")
2774 );
2775 }
2776
2777 #[cfg(target_os = "linux")]
2778 #[test]
2779 fn test_format_power_adapter() {
2780 assert_eq!(format_power_adapter("AC", "1"), "AC (connected)");
2781 assert_eq!(format_power_adapter("ADP1", "0"), "ADP1 (not connected)");
2782 assert_eq!(format_power_adapter("AC", ""), "AC (unknown)");
2784 }
2785
2786 #[cfg(target_os = "linux")]
2787 #[test]
2788 fn test_format_tpm_version() {
2789 assert_eq!(format_tpm_version("2").as_deref(), Some("2.0"));
2791 assert_eq!(format_tpm_version("1").as_deref(), Some("1.2"));
2792 assert_eq!(format_tpm_version("2\n").as_deref(), Some("2.0"));
2794 assert_eq!(format_tpm_version("3"), None);
2796 assert_eq!(format_tpm_version(""), None);
2797 assert_eq!(format_tpm_version("garbage"), None);
2798 }
2799
2800 #[cfg(target_os = "windows")]
2801 #[test]
2802 fn test_win_cpu_usage_percent() {
2803 use super::win_cpu::usage_percent;
2804 let u = usage_percent((0, 0, 0), (50, 100, 50));
2807 assert!((u - 66.6667).abs() < 0.01, "got {}", u);
2808
2809 assert_eq!(usage_percent((0, 0, 0), (100, 100, 0)), 0.0);
2811
2812 assert_eq!(usage_percent((0, 0, 0), (0, 100, 100)), 100.0);
2814
2815 assert_eq!(usage_percent((5, 10, 10), (5, 10, 10)), 0.0);
2817 }
2818
2819 #[test]
2825 fn test_format_cpu_cores_no_hyperthreading() {
2826 assert_eq!(format_cpu_cores_plain(4, Some(4)), "4 cores");
2828 }
2829
2830 #[test]
2831 fn test_format_cpu_cores_hyperthreaded() {
2832 assert_eq!(format_cpu_cores_plain(16, Some(8)), "8C / 16T");
2834 }
2835
2836 #[test]
2837 fn test_format_cpu_cores_unknown_physical() {
2838 assert_eq!(format_cpu_cores_plain(8, None), "8 cores");
2840 }
2841
2842 #[test]
2843 fn test_format_cpu_cores_physical_equals_zero() {
2844 let result = format_cpu_cores_plain(8, Some(0));
2848 assert!(result.contains("8"), "should mention 8 threads: {}", result);
2849 }
2850
2851 #[cfg(target_os = "linux")]
2852 #[test]
2853 fn test_detect_cpu_cache_returns_some_on_linux() {
2854 if std::path::Path::new("/sys/devices/system/cpu/cpu0/cache").exists() {
2857 let result = detect_cpu_cache();
2858 assert!(result.is_some(), "expected cache info on Linux with sysfs");
2859 let s = result.unwrap();
2860 assert!(
2861 s.contains("L1") || s.contains("L2") || s.contains("L3"),
2862 "expected cache level labels, got: {}",
2863 s
2864 );
2865 }
2866 }
2867
2868 #[test]
2869 fn test_normalize_desktop_name_known() {
2870 assert_eq!(normalize_desktop_name("gnome"), "GNOME");
2871 assert_eq!(normalize_desktop_name("GNOME"), "GNOME");
2872 assert_eq!(normalize_desktop_name("kde"), "KDE Plasma");
2873 assert_eq!(normalize_desktop_name("plasma"), "KDE Plasma");
2874 assert_eq!(normalize_desktop_name("KDE Plasma"), "KDE Plasma");
2875 assert_eq!(normalize_desktop_name("xfce"), "XFCE");
2876 assert_eq!(normalize_desktop_name("lxqt"), "LXQt");
2877 assert_eq!(normalize_desktop_name("mate"), "MATE");
2878 assert_eq!(normalize_desktop_name("cinnamon"), "Cinnamon");
2879 assert_eq!(normalize_desktop_name("e"), "Enlightenment");
2880 }
2881
2882 #[test]
2883 fn test_normalize_desktop_name_unknown_lowercase() {
2884 assert_eq!(normalize_desktop_name("budgie"), "Budgie");
2886 assert_eq!(normalize_desktop_name("niri"), "Niri");
2887 }
2888
2889 #[test]
2890 fn test_normalize_desktop_name_unknown_mixed() {
2891 assert_eq!(normalize_desktop_name("MyDE"), "MyDE");
2893 }
2894
2895 #[test]
2896 fn test_normalize_desktop_name_trims_whitespace() {
2897 assert_eq!(normalize_desktop_name(" gnome "), "GNOME");
2898 assert_eq!(normalize_desktop_name(" niri "), "Niri");
2899 }
2900
2901 #[cfg(target_os = "linux")]
2902 #[test]
2903 fn test_detect_desktop_from_proc_returns_option() {
2904 let result = detect_desktop_from_proc();
2906 if let Some(ref de) = result {
2907 assert!(!de.is_empty(), "desktop name should not be empty");
2908 }
2909 }
2910
2911 #[cfg(target_os = "linux")]
2912 #[test]
2913 fn test_detect_cpu_freq_range_returns_ordered_pair() {
2914 if std::path::Path::new("/sys/devices/system/cpu/cpufreq").exists() {
2915 if let Some((min, max)) = detect_cpu_freq_range() {
2916 assert!(
2917 min <= max,
2918 "min freq should be <= max freq: {} > {}",
2919 min,
2920 max
2921 );
2922 assert!(min > 0, "min freq should be positive");
2923 }
2924 }
2925 }
2926}