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 full: bool,
23 pub fields: Option<Vec<String>>,
25 pub weather_location: Option<String>,
27 pub weather_unit: crate::weather::WeatherUnit,
29}
30
31#[derive(Debug)]
36pub struct SystemInfo {
37 pub os: String,
39 pub kernel: Option<String>,
41 pub hostname: Option<String>,
43 pub arch: String,
45 pub cpu: String,
47 pub cpu_cores: usize,
49 pub cpu_core_info: String,
51 pub memory: String,
53 pub swap: String,
55 pub uptime: String,
57 pub processes: usize,
59 pub load_avg: Option<String>,
61 pub disks: Vec<String>,
63 pub temps: Vec<String>,
65 pub networks: Vec<String>,
67 pub boot_time: String,
69 pub battery: Option<String>,
71 pub shell: Option<String>,
73 pub terminal: Option<String>,
75 pub desktop: Option<String>,
77 pub cpu_freq: Option<String>,
79 pub users: usize,
81 pub gpu: Vec<String>,
83 pub packages: Option<usize>,
85 pub current_user: Option<String>,
87 pub local_ip: Option<String>,
89 pub public_ip: Option<String>,
91 pub active_interface: Option<String>,
93 pub motherboard: Option<String>,
95 pub bios: Option<String>,
97 pub displays: Vec<String>,
99 pub audio: Option<String>,
101 pub wifi: Option<String>,
103 pub bluetooth: Option<String>,
105 pub ui_theme: Option<String>,
107 pub icons: Option<String>,
109 pub cursor: Option<String>,
111 pub font: Option<String>,
113 pub terminal_font: Option<String>,
115 pub camera: Vec<String>,
117 pub gamepad: Vec<String>,
119 pub cpu_cache: Option<String>,
121 pub cpu_usage: Option<String>,
123 pub physical_disks: Vec<String>,
125 pub physical_memory: Option<String>,
127 pub init_system: Option<String>,
129 pub chassis: Option<String>,
131 pub locale: Option<String>,
133 pub bootmgr: Option<String>,
135 pub editor: Option<String>,
137 pub weather: Option<String>,
139 pub wm: Option<String>,
141 pub dns: Vec<String>,
143 pub domain: Option<String>,
145 pub domain_search: Vec<String>,
147 pub terminal_size: Option<String>,
149 pub btrfs: Vec<String>,
151 pub zpool: Vec<String>,
153 pub login_manager: Option<String>,
155 pub brightness: Option<String>,
157 pub power_adapter: Option<String>,
159}
160
161impl SystemInfo {
162 pub fn collect(opts: CollectOptions) -> anyhow::Result<Self> {
167 let should_collect = |field_name: &str| -> bool {
168 match &opts.fields {
169 Some(fields) => {
170 let norm_field = field_name.to_lowercase().replace(['-', '_'], " ");
171 let norm_field_no_spaces = norm_field.replace(' ', "");
172 fields.iter().any(|f| {
173 let norm_f = f.to_lowercase().replace(['-', '_'], " ");
174 norm_f == norm_field || norm_f.replace(' ', "") == norm_field_no_spaces
175 })
176 }
177 None => true,
178 }
179 };
180
181 let mut refresh_kind = sysinfo::RefreshKind::nothing();
182 if should_collect("cpu")
183 || should_collect("cpu usage")
184 || should_collect("cpu-usage")
185 || should_collect("cpu cache")
186 || should_collect("cpu-cache")
187 {
188 refresh_kind = refresh_kind.with_cpu(sysinfo::CpuRefreshKind::everything());
189 }
190 if should_collect("memory")
191 || should_collect("swap")
192 || should_collect("phys mem")
193 || should_collect("phys-mem")
194 {
195 refresh_kind = refresh_kind.with_memory(sysinfo::MemoryRefreshKind::everything());
196 }
197 if should_collect("procs") || should_collect("audio") {
198 refresh_kind = refresh_kind.with_processes(sysinfo::ProcessRefreshKind::nothing());
199 }
200
201 #[cfg_attr(target_os = "windows", allow(unused_mut))]
204 let mut sys = System::new_with_specifics(refresh_kind);
205
206 let os = System::long_os_version()
207 .or_else(System::name)
208 .unwrap_or_else(|| "Unknown".to_string());
209
210 let kernel = System::kernel_version();
211 let hostname = System::host_name();
212
213 let cpu = if should_collect("cpu") {
214 sys.cpus()
215 .first()
216 .map(|c| c.brand().to_string())
217 .unwrap_or_else(|| "Unknown CPU".to_string())
218 } else {
219 String::new()
220 };
221
222 let cpu_cores = if should_collect("cpu") {
223 sys.cpus().len()
224 } else {
225 0
226 };
227 let cpu_core_info = if should_collect("cpu") {
228 format_cpu_cores(cpu_cores, System::physical_core_count())
229 } else {
230 String::new()
231 };
232
233 let memory = if should_collect("memory") {
234 let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
235 let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
236 format!("{:.1} / {:.1} GB", used_mem, total_mem)
237 } else {
238 String::new()
239 };
240
241 let swap = if should_collect("swap") {
242 let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
243 let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
244 if total_swap > 0.0 {
245 format!("{:.1} / {:.1} GB", used_swap, total_swap)
246 } else {
247 "No swap".to_string()
248 }
249 } else {
250 String::new()
251 };
252
253 let uptime = format!("{}s", System::uptime());
254
255 let disks: Vec<String> = if should_collect("disk") {
256 let disks_list = crate::disk::detect_logical_disks(opts.full);
257 let format_disk = |(mount, total, avail, fs): &(String, u64, u64, String)| {
258 let total_gb = *total as f64 / 1024.0 / 1024.0 / 1024.0;
259 let avail_gb = *avail as f64 / 1024.0 / 1024.0 / 1024.0;
260 format!(
261 "{} ({}): {:.1} GB free / {:.1} GB",
262 mount, fs, avail_gb, total_gb
263 )
264 };
265 if !opts.long {
266 let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
267 let home_path = std::path::Path::new(&home);
268 let best = disks_list
269 .iter()
270 .filter(|(mp, ..)| home_path.starts_with(mp))
271 .max_by_key(|(mp, ..)| std::path::Path::new(mp).components().count());
272 if let Some(disk) = best {
273 vec![format_disk(disk)]
274 } else {
275 disks_list.iter().map(format_disk).collect()
276 }
277 } else {
278 disks_list.iter().map(format_disk).collect()
279 }
280 } else {
281 Vec::new()
282 };
283
284 let battery = if should_collect("battery") {
285 crate::battery::get_battery_info().map(|bat| {
286 let pct = bat.percentage;
287 let state = match bat.state {
288 crate::battery::BatteryState::Charging => "charging",
289 crate::battery::BatteryState::Discharging => "discharging",
290 crate::battery::BatteryState::Full => "full",
291 _ => "not charging",
292 };
293 let vendor = bat.vendor;
294 let model = bat.model;
295
296 let time_str = match bat.state {
298 crate::battery::BatteryState::Charging => bat.time_remaining.map(|d| {
299 let total_mins = d.as_secs() / 60;
300 let hours = total_mins / 60;
301 let mins = total_mins % 60;
302 if hours >= 24 {
303 let days = hours / 24;
304 let rem_hours = hours % 24;
305 format!("{}d {}h until full", days, rem_hours)
306 } else if hours > 0 {
307 format!("{}h {}m until full", hours, mins)
308 } else {
309 format!("{}m until full", mins)
310 }
311 }),
312 crate::battery::BatteryState::Discharging => bat.time_remaining.map(|d| {
313 let total_mins = d.as_secs() / 60;
314 let hours = total_mins / 60;
315 let mins = total_mins % 60;
316 if hours >= 24 {
317 let days = hours / 24;
318 let rem_hours = hours % 24;
319 format!("{}d {}h remaining", days, rem_hours)
320 } else if hours > 0 {
321 format!("{}h {}m remaining", hours, mins)
322 } else {
323 format!("{}m remaining", mins)
324 }
325 }),
326 _ => None,
327 };
328
329 let mut parts = vec![state.to_string()];
330 if let Some(t) = time_str {
331 parts.insert(0, t);
332 }
333 if let Some(health) = bat.health {
334 if health < 99.0 {
335 parts.push(format!("{:.0}% health", health));
336 }
337 }
338
339 let base = format!("{:.0}% ({})", pct, parts.join(", "));
340
341 match (vendor, model) {
342 (Some(v), Some(m)) => format!("{} [{} {}]", base, v, m),
343 (Some(v), None) => format!("{} [{}]", base, v),
344 _ => base,
345 }
346 })
347 } else {
348 None
349 };
350
351 let arch = System::cpu_arch();
352
353 let processes = if should_collect("procs") || should_collect("audio") {
354 sys.processes().len()
355 } else {
356 0
357 };
358
359 let load_avg = {
360 let avg = System::load_average();
361 if avg.one > 0.0 || avg.five > 0.0 {
362 Some(format!(
363 "{:.2}, {:.2}, {:.2}",
364 avg.one, avg.five, avg.fifteen
365 ))
366 } else {
367 None
368 }
369 };
370
371 #[cfg(target_os = "windows")]
375 let cpu_sample0 = win_cpu::sample();
376 #[cfg(target_os = "windows")]
377 let cpu_t0 = std::time::Instant::now();
378
379 let (
381 gpu,
382 packages,
383 public_ip,
384 (local_ip, active_interface),
385 motherboard,
386 bios,
387 displays,
388 audio,
389 wifi,
390 bluetooth,
391 (ui_theme, icons, cursor, font),
392 camera,
393 gamepad,
394 physical_disks,
395 physical_memory,
396 weather,
397 btrfs,
398 zpool,
399 ) = std::thread::scope(|s| {
400 let gpu_handle = if should_collect("gpu") {
401 Some(s.spawn(|| {
402 gpu::detect_gpus()
403 .into_iter()
404 .map(|g| g.format())
405 .collect::<Vec<String>>()
406 }))
407 } else {
408 None
409 };
410 let packages_handle = if should_collect("packages") {
411 Some(s.spawn(crate::packages::detect_packages))
412 } else {
413 None
414 };
415 let public_ip_handle = if should_collect("public ip") {
416 Some(s.spawn(crate::network::detect_public_ip))
417 } else {
418 None
419 };
420 let network_ips_handle = if should_collect("net") {
421 Some(s.spawn(crate::network::detect_active_interface_and_local_ip))
422 } else {
423 None
424 };
425 let motherboard_handle = if should_collect("motherboard") {
426 Some(s.spawn(crate::motherboard::detect_motherboard))
427 } else {
428 None
429 };
430 let bios_handle = if should_collect("bios") {
431 Some(s.spawn(crate::bios::detect_bios))
432 } else {
433 None
434 };
435 let displays_handle = if should_collect("display") {
436 Some(s.spawn(crate::display::detect_displays))
437 } else {
438 None
439 };
440 let audio_handle = if should_collect("audio") {
441 Some(s.spawn(|| crate::audio::detect_audio(&sys)))
442 } else {
443 None
444 };
445 let wifi_handle = if should_collect("wifi") {
446 Some(s.spawn(crate::network::detect_wifi))
447 } else {
448 None
449 };
450 let bluetooth_handle = if should_collect("bluetooth") {
451 Some(s.spawn(crate::bluetooth::detect_bluetooth))
452 } else {
453 None
454 };
455 let ui_theme_and_fonts_handle = if should_collect("theme")
456 || should_collect("icons")
457 || should_collect("cursor")
458 || should_collect("font")
459 {
460 Some(s.spawn(crate::theme::detect_ui_theme_and_fonts))
461 } else {
462 None
463 };
464 let camera_handle = if should_collect("camera") {
465 Some(s.spawn(crate::camera::detect_camera))
466 } else {
467 None
468 };
469 let gamepad_handle = if should_collect("gamepad") {
470 Some(s.spawn(crate::gamepad::detect_gamepad))
471 } else {
472 None
473 };
474 let physical_disks_handle = if should_collect("phys disk") {
475 Some(s.spawn(crate::disk::detect_physical_disks))
476 } else {
477 None
478 };
479 let physical_memory_handle = if should_collect("phys mem") {
480 Some(s.spawn(crate::memory::detect_physical_memory))
481 } else {
482 None
483 };
484 let weather_location = opts.weather_location.clone();
485 let weather_unit = opts.weather_unit;
486 let weather_handle = if should_collect("weather") {
487 Some(s.spawn(move || {
488 crate::weather::detect_weather(weather_location.as_deref(), weather_unit)
489 }))
490 } else {
491 None
492 };
493 let btrfs_handle = if should_collect("btrfs") {
494 Some(s.spawn(crate::btrfs::detect_btrfs))
495 } else {
496 None
497 };
498 let zpool_handle = if should_collect("zpool") {
499 Some(s.spawn(crate::zfs::detect_zpool))
500 } else {
501 None
502 };
503
504 (
505 gpu_handle
506 .map(|h| h.join().unwrap_or_default())
507 .unwrap_or_default(),
508 packages_handle.and_then(|h| h.join().ok().flatten()),
509 public_ip_handle.and_then(|h| h.join().ok().flatten()),
510 network_ips_handle
511 .map(|h| h.join().unwrap_or((None, None)))
512 .unwrap_or((None, None)),
513 motherboard_handle.and_then(|h| h.join().ok().flatten()),
514 bios_handle.and_then(|h| h.join().ok().flatten()),
515 displays_handle
516 .map(|h| h.join().unwrap_or_default())
517 .unwrap_or_default(),
518 audio_handle.and_then(|h| h.join().ok().flatten()),
519 wifi_handle.and_then(|h| h.join().ok().flatten()),
520 bluetooth_handle.and_then(|h| h.join().ok().flatten()),
521 ui_theme_and_fonts_handle
522 .map(|h| h.join().unwrap_or((None, None, None, None)))
523 .unwrap_or((None, None, None, None)),
524 camera_handle
525 .map(|h| h.join().unwrap_or_default())
526 .unwrap_or_default(),
527 gamepad_handle
528 .map(|h| h.join().unwrap_or_default())
529 .unwrap_or_default(),
530 physical_disks_handle
531 .map(|h| h.join().unwrap_or_default())
532 .unwrap_or_default(),
533 physical_memory_handle.and_then(|h| h.join().ok().flatten()),
534 weather_handle.and_then(|h| h.join().ok().flatten()),
535 btrfs_handle
536 .map(|h| h.join().unwrap_or_default())
537 .unwrap_or_default(),
538 zpool_handle
539 .map(|h| h.join().unwrap_or_default())
540 .unwrap_or_default(),
541 )
542 });
543
544 let mut temps: Vec<String> = if should_collect("temp") {
545 Components::new_with_refreshed_list()
546 .iter()
547 .filter_map(|c| {
548 c.temperature().and_then(|t| {
549 if t > 0.0 {
550 Some(format!("{}: {:.0}°C", c.label(), t))
551 } else {
552 None
553 }
554 })
555 })
556 .collect()
557 } else {
558 Vec::new()
559 };
560
561 temps.sort_by(|a, b| {
563 let a_cpu = a.to_lowercase().contains("cpu") || a.to_lowercase().contains("core");
564 let b_cpu = b.to_lowercase().contains("cpu") || b.to_lowercase().contains("core");
565 b_cpu.cmp(&a_cpu)
566 });
567
568 let networks = if should_collect("net") {
569 crate::network::detect_networks(active_interface.as_deref(), local_ip.as_deref())
570 } else {
571 Vec::new()
572 };
573
574 let boot_timestamp = System::boot_time();
575 let boot_dt = chrono::Local
576 .timestamp_opt(boot_timestamp as i64, 0)
577 .single()
578 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
579 .unwrap_or_else(|| boot_timestamp.to_string());
580 let boot_time = boot_dt;
581
582 let shell = if should_collect("shell") {
584 crate::shell::detect_shell(&sys)
585 } else {
586 None
587 };
588 let terminal = if should_collect("terminal") {
589 crate::terminal::detect_terminal(&sys)
590 } else {
591 None
592 };
593 let terminal_font = if should_collect("terminal font")
594 || should_collect("terminal-font")
595 || should_collect("terminal_font")
596 {
597 crate::terminal::detect_terminal_font(terminal.as_deref())
598 } else {
599 None
600 };
601 let desktop = if should_collect("desktop") {
602 std::env::var("XDG_CURRENT_DESKTOP")
603 .or_else(|_| std::env::var("DESKTOP_SESSION"))
604 .or_else(|_| std::env::var("XDG_SESSION_DESKTOP"))
605 .or_else(|_| std::env::var("GDMSESSION"))
606 .ok()
607 .map(|s| normalize_desktop_name(&s))
608 .filter(|s| !s.is_empty())
609 .or_else(detect_desktop_from_proc)
610 } else {
611 None
612 };
613
614 let cpu_freq = if should_collect("cpu-freq")
616 || should_collect("cpu freq")
617 || should_collect("cpu_freq")
618 {
619 sys.cpus().first().map(|c| {
620 let current = format!("{:.2} GHz", c.frequency() as f64 / 1000.0);
621 if let Some((min_khz, max_khz)) = detect_cpu_freq_range() {
622 let min_ghz = min_khz as f64 / 1_000_000.0;
623 let max_ghz = max_khz as f64 / 1_000_000.0;
624 format!("{} ({:.2} \u{2013} {:.2} GHz)", current, min_ghz, max_ghz)
625 } else {
626 current
627 }
628 })
629 } else {
630 None
631 };
632
633 let cpu_cache = if should_collect("cpu-cache")
635 || should_collect("cpu cache")
636 || should_collect("cpu_cache")
637 {
638 detect_cpu_cache()
639 } else {
640 None
641 };
642
643 let cpu_usage = if should_collect("cpu-usage")
648 || should_collect("cpu usage")
649 || should_collect("cpu_usage")
650 {
651 #[cfg(not(target_os = "windows"))]
652 {
653 std::thread::sleep(std::time::Duration::from_millis(200));
654 sys.refresh_cpu_usage();
655 let usage: f32 =
656 sys.cpus().iter().map(|c| c.cpu_usage()).sum::<f32>() / sys.cpus().len() as f32;
657 let avg = System::load_average();
658 let load_str = format!("{:.2}, {:.2}, {:.2}", avg.one, avg.five, avg.fifteen);
659 if usage > 0.0 {
660 Some(format!("{:.1}% (load: {})", usage, load_str))
661 } else if avg.one > 0.0 {
662 Some(format!("load: {}", load_str))
663 } else {
664 None
665 }
666 }
667 #[cfg(target_os = "windows")]
668 {
669 let floor = std::time::Duration::from_millis(100);
673 let elapsed = cpu_t0.elapsed();
674 if elapsed < floor {
675 std::thread::sleep(floor - elapsed);
676 }
677 match (cpu_sample0, win_cpu::sample()) {
678 (Some(s0), Some(s1)) => {
679 let usage = win_cpu::usage_percent(s0, s1);
680 if usage > 0.0 {
681 Some(format!("{:.1}%", usage))
682 } else {
683 None
684 }
685 }
686 _ => None,
687 }
688 }
689 } else {
690 None
691 };
692
693 let init_system = if should_collect("init") || should_collect("init system") {
694 detect_init_system()
695 } else {
696 None
697 };
698
699 let chassis = if should_collect("chassis") {
700 detect_chassis()
701 } else {
702 None
703 };
704
705 let locale = if should_collect("locale") {
706 std::env::var("LC_ALL")
707 .ok()
708 .filter(|s| !s.is_empty())
709 .or_else(|| std::env::var("LC_MESSAGES").ok().filter(|s| !s.is_empty()))
710 .or_else(|| std::env::var("LANG").ok().filter(|s| !s.is_empty()))
711 } else {
712 None
713 };
714
715 let bootmgr = if should_collect("bootmgr") || should_collect("boot") {
716 detect_bootmgr()
717 } else {
718 None
719 };
720
721 let login_manager = if should_collect("login-manager") || should_collect("lm") {
722 detect_login_manager()
723 } else {
724 None
725 };
726
727 let brightness = if should_collect("brightness") {
728 detect_brightness()
729 } else {
730 None
731 };
732
733 let power_adapter = if should_collect("power-adapter") {
734 detect_power_adapter()
735 } else {
736 None
737 };
738
739 let editor = if should_collect("editor") {
740 std::env::var("VISUAL")
741 .ok()
742 .filter(|s| !s.is_empty())
743 .or_else(|| std::env::var("EDITOR").ok().filter(|s| !s.is_empty()))
744 } else {
745 None
746 };
747
748 let wm = if should_collect("wm") || should_collect("window manager") {
749 crate::wm::detect_wm()
750 } else {
751 None
752 };
753
754 let dns = if should_collect("dns") {
755 crate::network::detect_dns()
756 } else {
757 Vec::new()
758 };
759
760 let domain = if should_collect("domain") {
761 crate::network::detect_domain()
762 } else {
763 None
764 };
765
766 let domain_search = if should_collect("domain-search") || should_collect("domain search") {
767 crate::network::detect_domain_search()
768 } else {
769 Vec::new()
770 };
771
772 let terminal_size = if should_collect("terminal size")
773 || should_collect("terminal-size")
774 || should_collect("terminal_size")
775 {
776 crate::terminal::detect_terminal_size()
777 } else {
778 None
779 };
780
781 let current_user = std::env::var("USER").ok();
783
784 let users = if should_collect("users") {
786 Users::new_with_refreshed_list()
787 .iter()
788 .filter(|user| {
789 let uid_str = user.id().to_string();
791 if let Ok(uid) = uid_str.parse::<u32>() {
792 uid >= 1000
793 } else {
794 false
795 }
796 })
797 .count()
798 } else {
799 0
800 };
801
802 Ok(Self {
803 os,
804 kernel,
805 hostname,
806 arch,
807 cpu,
808 cpu_cores,
809 cpu_core_info,
810 memory,
811 swap,
812 uptime,
813 processes,
814 load_avg,
815 disks,
816 temps,
817 networks,
818 boot_time,
819 battery,
820 shell,
821 terminal,
822 desktop,
823 cpu_freq,
824 users,
825 gpu,
826 packages,
827 current_user,
828 local_ip,
829 public_ip,
830 active_interface,
831 motherboard,
832 bios,
833 displays,
834 audio,
835 wifi,
836 bluetooth,
837 ui_theme,
838 icons,
839 cursor,
840 font,
841 terminal_font,
842 camera,
843 gamepad,
844 cpu_cache,
845 cpu_usage,
846 physical_disks,
847 physical_memory,
848 init_system,
849 chassis,
850 locale,
851 bootmgr,
852 editor,
853 weather,
854 wm,
855 dns,
856 domain,
857 domain_search,
858 terminal_size,
859 btrfs,
860 zpool,
861 login_manager,
862 brightness,
863 power_adapter,
864 })
865 }
866}
867
868pub fn detect_cpu_cache() -> Option<String> {
874 #[cfg(target_os = "linux")]
875 {
876 use std::fs;
877 let cache_dir = std::path::Path::new("/sys/devices/system/cpu/cpu0/cache");
878 if !cache_dir.exists() {
879 return None;
880 }
881
882 struct CacheEntry {
883 level: u32,
884 kind: String,
885 size_kb: u64,
886 }
887
888 let mut entries: Vec<CacheEntry> = Vec::new();
889
890 let Ok(indices) = fs::read_dir(cache_dir) else {
891 return None;
892 };
893
894 for entry in indices.flatten() {
895 let path = entry.path();
896 if !path.is_dir() {
898 continue;
899 }
900 let level_str = match fs::read_to_string(path.join("level")) {
901 Ok(s) => s,
902 Err(_) => continue,
903 };
904 let level: u32 = match level_str.trim().parse() {
905 Ok(n) => n,
906 Err(_) => continue,
907 };
908 let kind = match fs::read_to_string(path.join("type")) {
909 Ok(s) => s.trim().to_string(),
910 Err(_) => continue,
911 };
912 let size_str = match fs::read_to_string(path.join("size")) {
913 Ok(s) => s,
914 Err(_) => continue,
915 };
916 let size_raw = size_str.trim();
917 let size_kb: u64 = if let Some(k) = size_raw.strip_suffix('K') {
918 match k.parse() {
919 Ok(n) => n,
920 Err(_) => continue,
921 }
922 } else if let Some(m) = size_raw.strip_suffix('M') {
923 match m.parse::<u64>() {
924 Ok(n) => n * 1024,
925 Err(_) => continue,
926 }
927 } else {
928 match size_raw.parse() {
929 Ok(n) => n,
930 Err(_) => continue,
931 }
932 };
933
934 if kind != "Instruction" && kind != "Data" && kind != "Unified" {
935 continue;
936 }
937
938 entries.push(CacheEntry {
939 level,
940 kind,
941 size_kb,
942 });
943 }
944
945 if entries.is_empty() {
946 return None;
947 }
948
949 entries.sort_by_key(|e| (e.level, e.kind.clone()));
950
951 let fmt_size = |kb: u64| -> String {
952 if kb >= 1024 && kb.is_multiple_of(1024) {
953 format!("{}M", kb / 1024)
954 } else if kb >= 1024 {
955 format!("{:.2}M", kb as f64 / 1024.0)
956 .trim_end_matches('0')
957 .trim_end_matches('.')
958 .to_string()
959 + "M"
960 } else {
961 format!("{}K", kb)
962 }
963 };
964
965 let mut seen = std::collections::HashSet::new();
967 let mut parts: Vec<String> = Vec::new();
968 for e in &entries {
969 let label = match (e.level, e.kind.as_str()) {
970 (1, "Data") => "L1d".to_string(),
971 (1, "Instruction") => "L1i".to_string(),
972 (1, "Unified") => "L1".to_string(),
973 (n, _) => format!("L{}", n),
974 };
975 if seen.insert(label.clone()) {
976 parts.push(format!("{}: {}", label, fmt_size(e.size_kb)));
977 }
978 }
979
980 if parts.is_empty() {
981 None
982 } else {
983 Some(parts.join(", "))
984 }
985 }
986 #[cfg(target_os = "macos")]
987 {
988 extern "C" {
989 fn sysctlbyname(
990 name: *const i8,
991 oldp: *mut std::ffi::c_void,
992 oldlenp: *mut usize,
993 newp: *mut std::ffi::c_void,
994 newlen: usize,
995 ) -> i32;
996 }
997
998 let read_u64 = |key: &str| -> Option<u64> {
999 let name = std::ffi::CString::new(key).ok()?;
1000 let mut value: u64 = 0;
1001 let mut size = std::mem::size_of::<u64>();
1002 let ret = unsafe {
1003 sysctlbyname(
1004 name.as_ptr(),
1005 &mut value as *mut u64 as *mut std::ffi::c_void,
1006 &mut size,
1007 std::ptr::null_mut(),
1008 0,
1009 )
1010 };
1011 if ret == 0 && value > 0 {
1012 Some(value)
1013 } else {
1014 None
1015 }
1016 };
1017
1018 let fmt_bytes = |bytes: u64| -> String {
1019 if bytes >= 1024 * 1024 {
1020 format!("{}M", bytes / (1024 * 1024))
1021 } else {
1022 format!("{}K", bytes / 1024)
1023 }
1024 };
1025
1026 let mut parts = Vec::new();
1027 if let Some(v) = read_u64("hw.l1dcachesize") {
1028 parts.push(format!("L1d: {}", fmt_bytes(v)));
1029 }
1030 if let Some(v) = read_u64("hw.l1icachesize") {
1031 parts.push(format!("L1i: {}", fmt_bytes(v)));
1032 }
1033 if let Some(v) = read_u64("hw.l2cachesize") {
1034 parts.push(format!("L2: {}", fmt_bytes(v)));
1035 }
1036 if let Some(v) = read_u64("hw.l3cachesize") {
1037 parts.push(format!("L3: {}", fmt_bytes(v)));
1038 }
1039
1040 if parts.is_empty() {
1041 None
1042 } else {
1043 Some(parts.join(", "))
1044 }
1045 }
1046 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1047 {
1048 None
1049 }
1050}
1051
1052pub fn format_cpu_cores(logical: usize, physical: Option<usize>) -> String {
1057 #[cfg(target_os = "linux")]
1059 if let Some(hybrid) = detect_hybrid_cores(logical) {
1060 return hybrid;
1061 }
1062
1063 #[cfg(target_os = "macos")]
1065 if let Some(hybrid) = detect_macos_hybrid_cores(logical) {
1066 return hybrid;
1067 }
1068
1069 format_cpu_cores_plain(logical, physical)
1070}
1071
1072fn format_cpu_cores_plain(logical: usize, physical: Option<usize>) -> String {
1082 match physical {
1083 Some(p) if p < logical => format!("{}C / {}T", p, logical),
1084 _ => format!("{} cores", logical),
1085 }
1086}
1087
1088#[cfg(target_os = "linux")]
1091fn detect_hybrid_cores(logical: usize) -> Option<String> {
1092 use std::collections::HashMap;
1093 use std::fs;
1094
1095 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
1096 if !cpufreq.exists() {
1097 return None;
1098 }
1099
1100 let mut freq_to_count: HashMap<u64, usize> = HashMap::new();
1102 let mut total_accounted = 0usize;
1103
1104 let Ok(policies) = fs::read_dir(cpufreq) else {
1105 return None;
1106 };
1107
1108 for policy in policies.flatten() {
1109 let path = policy.path();
1110 if !path.is_dir() {
1111 continue;
1112 }
1113 let max_freq_str = fs::read_to_string(path.join("cpuinfo_max_freq")).ok()?;
1114 let max_freq: u64 = max_freq_str.trim().parse().ok()?;
1115 let affected = fs::read_to_string(path.join("affected_cpus")).ok()?;
1116 let count = affected.split_whitespace().count();
1117 *freq_to_count.entry(max_freq).or_insert(0) += count;
1118 total_accounted += count;
1119 }
1120
1121 if freq_to_count.len() != 2 || total_accounted != logical {
1123 return None;
1124 }
1125
1126 let mut tiers: Vec<(u64, usize)> = freq_to_count.into_iter().collect();
1127 tiers.sort_by_key(|t| std::cmp::Reverse(t.0)); let (_, p_count) = tiers[0];
1129 let (_, e_count) = tiers[1];
1130
1131 Some(format!("{}P + {}E / {}T", p_count, e_count, logical))
1132}
1133
1134#[cfg(target_os = "macos")]
1137fn detect_macos_hybrid_cores(logical: usize) -> Option<String> {
1138 extern "C" {
1139 fn sysctlbyname(
1140 name: *const i8,
1141 oldp: *mut std::ffi::c_void,
1142 oldlenp: *mut usize,
1143 newp: *mut std::ffi::c_void,
1144 newlen: usize,
1145 ) -> i32;
1146 }
1147
1148 let read_u32 = |key: &str| -> Option<u32> {
1149 let name = std::ffi::CString::new(key).ok()?;
1150 let mut value: u32 = 0;
1151 let mut size = std::mem::size_of::<u32>();
1152 let ret = unsafe {
1153 sysctlbyname(
1154 name.as_ptr(),
1155 &mut value as *mut u32 as *mut std::ffi::c_void,
1156 &mut size,
1157 std::ptr::null_mut(),
1158 0,
1159 )
1160 };
1161 if ret == 0 {
1162 Some(value)
1163 } else {
1164 None
1165 }
1166 };
1167
1168 let nlevels = read_u32("hw.nperflevels")?;
1170 if nlevels != 2 {
1171 return None;
1172 }
1173
1174 let p_cores = read_u32("hw.perflevel0.logicalcpu")? as usize;
1175 let e_cores = read_u32("hw.perflevel1.logicalcpu")? as usize;
1176
1177 if p_cores + e_cores != logical {
1178 return None;
1179 }
1180
1181 Some(format!("{}P + {}E / {}T", p_cores, e_cores, logical))
1182}
1183
1184pub fn detect_cpu_freq_range() -> Option<(u64, u64)> {
1187 #[cfg(target_os = "linux")]
1188 {
1189 use std::fs;
1190 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
1191 if !cpufreq.exists() {
1192 return None;
1193 }
1194 let mut global_min: Option<u64> = None;
1195 let mut global_max: Option<u64> = None;
1196 let Ok(policies) = fs::read_dir(cpufreq) else {
1197 return None;
1198 };
1199 for policy in policies.flatten() {
1200 let path = policy.path();
1201 if !path.is_dir() {
1202 continue;
1203 }
1204 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_min_freq")) {
1205 if let Ok(v) = s.trim().parse::<u64>() {
1206 global_min = Some(global_min.map_or(v, |m: u64| m.min(v)));
1207 }
1208 }
1209 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_max_freq")) {
1210 if let Ok(v) = s.trim().parse::<u64>() {
1211 global_max = Some(global_max.map_or(v, |m: u64| m.max(v)));
1212 }
1213 }
1214 }
1215 match (global_min, global_max) {
1216 (Some(min), Some(max)) => Some((min, max)),
1217 _ => None,
1218 }
1219 }
1220 #[cfg(not(target_os = "linux"))]
1221 {
1222 None
1223 }
1224}
1225
1226#[cfg(not(target_os = "linux"))]
1227fn detect_desktop_from_proc() -> Option<String> {
1228 None
1229}
1230
1231#[cfg(target_os = "linux")]
1232fn detect_desktop_from_proc() -> Option<String> {
1233 const DE_PROCS: &[(&str, &str)] = &[
1234 ("gnome-shell", "GNOME"),
1235 ("plasmashell", "KDE Plasma"),
1236 ("xfce4-session", "XFCE"),
1237 ("mate-session", "MATE"),
1238 ("cinnamon", "Cinnamon"),
1239 ("budgie-daemon", "Budgie"),
1240 ("budgie-panel", "Budgie"),
1241 ("lxsession", "LXDE"),
1242 ("lxqt-session", "LXQt"),
1243 ("deepin-session", "Deepin"),
1244 ("dde-session-daemon", "Deepin"),
1245 ("gala", "Pantheon"),
1246 ("enlightenment", "Enlightenment"),
1247 ];
1248 let Ok(entries) = std::fs::read_dir("/proc") else {
1249 return None;
1250 };
1251 for entry in entries.filter_map(|e| e.ok()) {
1252 let path = entry.path();
1253 if !path.is_dir() {
1254 continue;
1255 }
1256 let Ok(comm) = std::fs::read_to_string(path.join("comm")) else {
1257 continue;
1258 };
1259 let comm = comm.trim().to_lowercase();
1260 for (proc_name, de_name) in DE_PROCS {
1261 if comm == *proc_name || comm.starts_with(proc_name) {
1262 return Some(de_name.to_string());
1263 }
1264 }
1265 }
1266 None
1267}
1268
1269fn normalize_desktop_name(raw: &str) -> String {
1270 let s = raw.trim();
1271 match s.to_lowercase().as_str() {
1273 "gnome" => "GNOME".to_string(),
1274 "kde" | "kde plasma" | "plasma" => "KDE Plasma".to_string(),
1275 "xfce" => "XFCE".to_string(),
1276 "lxde" => "LXDE".to_string(),
1277 "lxqt" => "LXQt".to_string(),
1278 "mate" => "MATE".to_string(),
1279 "cinnamon" => "Cinnamon".to_string(),
1280 "budgie" => "Budgie".to_string(),
1281 "deepin" => "Deepin".to_string(),
1282 "pantheon" => "Pantheon".to_string(),
1283 "unity" => "Unity".to_string(),
1284 "enlightenment" | "e" => "Enlightenment".to_string(),
1285 _ => {
1286 if s.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()) {
1288 let mut chars = s.chars();
1289 match chars.next() {
1290 None => String::new(),
1291 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
1292 }
1293 } else {
1294 s.to_string()
1295 }
1296 }
1297 }
1298}
1299
1300fn detect_init_system() -> Option<String> {
1301 #[cfg(target_os = "linux")]
1302 {
1303 let comm = std::fs::read_to_string("/proc/1/comm")
1304 .map(|s| s.trim().to_string())
1305 .ok()
1306 .filter(|s| !s.is_empty());
1307 if let Some(name) = comm {
1308 return Some(name);
1309 }
1310 std::fs::read_link("/proc/1/exe").ok().and_then(|p| {
1311 p.file_name()
1312 .and_then(|n| n.to_str())
1313 .map(|s| s.to_string())
1314 })
1315 }
1316 #[cfg(target_os = "macos")]
1317 {
1318 Some("launchd".to_string())
1319 }
1320 #[cfg(target_os = "windows")]
1321 {
1322 Some("SCM".to_string())
1323 }
1324 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1325 {
1326 None
1327 }
1328}
1329
1330fn detect_chassis() -> Option<String> {
1331 #[cfg(target_os = "linux")]
1332 {
1333 let raw = std::fs::read_to_string("/sys/class/dmi/id/chassis_type").ok()?;
1334 let n: u32 = raw.trim().parse().ok()?;
1335 let label = match n {
1336 3 => "Desktop",
1337 4 => "Low-Profile Desktop",
1338 6 => "Mini Tower",
1339 7 => "Tower",
1340 8 | 9 | 10 | 14 | 31 | 32 => "Laptop",
1341 11 => "Handheld",
1342 13 => "All-in-One",
1343 17 => "Main Server",
1344 23 => "Rack Server",
1345 28 => "Blade",
1346 30 => "Tablet",
1347 35 => "Mini PC",
1348 36 => "Stick PC",
1349 _ => return None,
1350 };
1351 Some(label.to_string())
1352 }
1353 #[cfg(target_os = "macos")]
1354 {
1355 let output = std::process::Command::new("sysctl")
1356 .args(["-n", "hw.model"])
1357 .output()
1358 .ok()?;
1359 let model = String::from_utf8(output.stdout).ok()?;
1360 let model = model.trim();
1361 if model.contains("MacBook") {
1362 Some("Laptop".to_string())
1363 } else if model.contains("MacPro") {
1364 Some("Desktop".to_string())
1365 } else if model.contains("Macmini") || model.contains("Mac mini") {
1366 Some("Mini PC".to_string())
1367 } else if model.contains("iMac") {
1368 Some("All-in-One".to_string())
1369 } else {
1370 Some(model.to_string())
1371 }
1372 }
1373 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1374 {
1375 None
1376 }
1377}
1378
1379fn detect_bootmgr() -> Option<String> {
1380 #[cfg(target_os = "linux")]
1381 {
1382 use std::path::Path;
1383 let is_uefi = Path::new("/sys/firmware/efi").exists();
1384 if Path::new("/boot/loader/entries").exists()
1385 || Path::new("/boot/loader/loader.conf").exists()
1386 || Path::new("/efi/loader/loader.conf").exists()
1387 {
1388 return Some("systemd-boot".to_string());
1389 }
1390 if Path::new("/boot/grub2/grub.cfg").exists() || Path::new("/boot/grub2").exists() {
1391 return Some("GRUB 2".to_string());
1392 }
1393 if Path::new("/boot/grub/grub.cfg").exists() || Path::new("/boot/grub").exists() {
1394 return Some("GRUB".to_string());
1395 }
1396 if is_uefi {
1397 Some("UEFI".to_string())
1398 } else {
1399 Some("BIOS".to_string())
1400 }
1401 }
1402 #[cfg(target_os = "macos")]
1403 {
1404 Some("Apple Boot ROM".to_string())
1405 }
1406 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1407 {
1408 None
1409 }
1410}
1411
1412fn detect_login_manager() -> Option<String> {
1419 #[cfg(target_os = "linux")]
1420 {
1421 let target = std::fs::read_link("/etc/systemd/system/display-manager.service").ok()?;
1422 let unit = target.file_name().and_then(|n| n.to_str())?;
1423 login_manager_from_unit(unit)
1424 }
1425 #[cfg(not(target_os = "linux"))]
1426 {
1427 None
1428 }
1429}
1430
1431#[cfg(target_os = "linux")]
1437fn login_manager_from_unit(unit: &str) -> Option<String> {
1438 let stem = unit.strip_suffix(".service").unwrap_or(unit).trim();
1439 if stem.is_empty() {
1440 return None;
1441 }
1442 let pretty = match stem.to_lowercase().as_str() {
1443 "gdm" | "gdm3" => "GDM",
1444 "sddm" => "SDDM",
1445 "lightdm" => "LightDM",
1446 "lxdm" => "LXDM",
1447 "xdm" => "XDM",
1448 "ly" => "Ly",
1449 "greetd" => "greetd",
1450 "slim" => "SLiM",
1451 "nodm" => "nodm",
1452 "entrance" => "Entrance",
1453 _ => {
1454 let mut chars = stem.chars();
1456 return chars
1457 .next()
1458 .map(|c| c.to_uppercase().collect::<String>() + chars.as_str());
1459 }
1460 };
1461 Some(pretty.to_string())
1462}
1463
1464fn detect_brightness() -> Option<String> {
1471 #[cfg(target_os = "linux")]
1472 {
1473 use std::path::Path;
1474 let dir = Path::new("/sys/class/backlight");
1475 if !dir.exists() {
1476 return None;
1477 }
1478 let mut devices: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
1481 .ok()?
1482 .flatten()
1483 .map(|e| e.path())
1484 .collect();
1485 devices.sort_by_key(|p| {
1486 let name = p
1487 .file_name()
1488 .and_then(|n| n.to_str())
1489 .unwrap_or("")
1490 .to_lowercase();
1491 if name.contains("acpi") || name.contains("video") {
1493 1
1494 } else {
1495 0
1496 }
1497 });
1498 for dev in devices {
1499 let cur = std::fs::read_to_string(dev.join("brightness"))
1500 .ok()
1501 .and_then(|s| s.trim().parse::<u64>().ok());
1502 let max = std::fs::read_to_string(dev.join("max_brightness"))
1503 .ok()
1504 .and_then(|s| s.trim().parse::<u64>().ok());
1505 if let (Some(cur), Some(max)) = (cur, max) {
1506 if let Some(pct) = brightness_percent(cur, max) {
1507 return Some(pct);
1508 }
1509 }
1510 }
1511 None
1512 }
1513 #[cfg(not(target_os = "linux"))]
1514 {
1515 None
1516 }
1517}
1518
1519#[cfg(target_os = "linux")]
1524fn brightness_percent(cur: u64, max: u64) -> Option<String> {
1525 if max == 0 {
1526 return None;
1527 }
1528 let pct = (cur as f64 / max as f64 * 100.0).round() as u64;
1529 Some(format!("{}%", pct))
1530}
1531
1532fn detect_power_adapter() -> Option<String> {
1540 #[cfg(target_os = "linux")]
1541 {
1542 use std::path::Path;
1543 let dir = Path::new("/sys/class/power_supply");
1544 if !dir.exists() {
1545 return None;
1546 }
1547 for entry in std::fs::read_dir(dir).ok()?.flatten() {
1548 let path = entry.path();
1549 let supply_type = std::fs::read_to_string(path.join("type"))
1550 .map(|s| s.trim().to_string())
1551 .unwrap_or_default();
1552 if supply_type != "Mains" {
1553 continue;
1554 }
1555 let name = path
1556 .file_name()
1557 .and_then(|n| n.to_str())
1558 .unwrap_or("AC")
1559 .to_string();
1560 let online = std::fs::read_to_string(path.join("online"))
1561 .map(|s| s.trim().to_string())
1562 .unwrap_or_default();
1563 return Some(format_power_adapter(&name, &online));
1564 }
1565 None
1566 }
1567 #[cfg(not(target_os = "linux"))]
1568 {
1569 None
1570 }
1571}
1572
1573#[cfg(target_os = "linux")]
1577fn format_power_adapter(name: &str, online: &str) -> String {
1578 let state = match online.trim() {
1579 "1" => "connected",
1580 "0" => "not connected",
1581 _ => "unknown",
1582 };
1583 format!("{} ({})", name, state)
1584}
1585
1586#[cfg(target_os = "windows")]
1591mod win_cpu {
1592 #[repr(C)]
1593 struct FileTime {
1594 low: u32,
1595 high: u32,
1596 }
1597
1598 impl FileTime {
1599 fn ticks(&self) -> u64 {
1600 ((self.high as u64) << 32) | self.low as u64
1601 }
1602 }
1603
1604 extern "system" {
1605 fn GetSystemTimes(idle: *mut FileTime, kernel: *mut FileTime, user: *mut FileTime) -> i32;
1606 }
1607
1608 pub fn sample() -> Option<(u64, u64, u64)> {
1611 let mut idle = FileTime { low: 0, high: 0 };
1612 let mut kernel = FileTime { low: 0, high: 0 };
1613 let mut user = FileTime { low: 0, high: 0 };
1614 let ok = unsafe { GetSystemTimes(&mut idle, &mut kernel, &mut user) };
1616 if ok == 0 {
1617 None
1618 } else {
1619 Some((idle.ticks(), kernel.ticks(), user.ticks()))
1620 }
1621 }
1622
1623 pub fn usage_percent(s0: (u64, u64, u64), s1: (u64, u64, u64)) -> f32 {
1626 let idle = s1.0.saturating_sub(s0.0);
1627 let kernel = s1.1.saturating_sub(s0.1);
1628 let user = s1.2.saturating_sub(s0.2);
1629 let total = kernel + user;
1630 if total == 0 {
1631 0.0
1632 } else {
1633 (100.0 * total.saturating_sub(idle) as f64 / total as f64) as f32
1634 }
1635 }
1636
1637 #[cfg(test)]
1638 mod layout {
1639 use std::mem::size_of;
1640
1641 #[test]
1643 fn filetime_size() {
1644 assert_eq!(size_of::<super::FileTime>(), 8);
1645 }
1646 }
1647}
1648
1649#[cfg(test)]
1650mod tests {
1651 use super::*;
1652
1653 #[cfg(target_os = "linux")]
1654 #[test]
1655 fn test_login_manager_from_unit() {
1656 assert_eq!(
1657 login_manager_from_unit("gdm.service").as_deref(),
1658 Some("GDM")
1659 );
1660 assert_eq!(
1661 login_manager_from_unit("gdm3.service").as_deref(),
1662 Some("GDM")
1663 );
1664 assert_eq!(
1665 login_manager_from_unit("sddm.service").as_deref(),
1666 Some("SDDM")
1667 );
1668 assert_eq!(
1669 login_manager_from_unit("lightdm.service").as_deref(),
1670 Some("LightDM")
1671 );
1672 assert_eq!(
1674 login_manager_from_unit("emptty.service").as_deref(),
1675 Some("Emptty")
1676 );
1677 assert_eq!(login_manager_from_unit("ly").as_deref(), Some("Ly"));
1679 assert_eq!(login_manager_from_unit("").as_deref(), None);
1681 assert_eq!(login_manager_from_unit(".service").as_deref(), None);
1682 }
1683
1684 #[cfg(target_os = "linux")]
1685 #[test]
1686 fn test_brightness_percent() {
1687 assert_eq!(brightness_percent(50, 100).as_deref(), Some("50%"));
1688 assert_eq!(brightness_percent(100, 100).as_deref(), Some("100%"));
1689 assert_eq!(brightness_percent(0, 100).as_deref(), Some("0%"));
1690 assert_eq!(brightness_percent(133, 255).as_deref(), Some("52%"));
1692 assert_eq!(brightness_percent(10, 0), None);
1694 }
1695
1696 #[cfg(target_os = "linux")]
1697 #[test]
1698 fn test_format_power_adapter() {
1699 assert_eq!(format_power_adapter("AC", "1"), "AC (connected)");
1700 assert_eq!(format_power_adapter("ADP1", "0"), "ADP1 (not connected)");
1701 assert_eq!(format_power_adapter("AC", ""), "AC (unknown)");
1703 }
1704
1705 #[cfg(target_os = "windows")]
1706 #[test]
1707 fn test_win_cpu_usage_percent() {
1708 use super::win_cpu::usage_percent;
1709 let u = usage_percent((0, 0, 0), (50, 100, 50));
1712 assert!((u - 66.6667).abs() < 0.01, "got {}", u);
1713
1714 assert_eq!(usage_percent((0, 0, 0), (100, 100, 0)), 0.0);
1716
1717 assert_eq!(usage_percent((0, 0, 0), (0, 100, 100)), 100.0);
1719
1720 assert_eq!(usage_percent((5, 10, 10), (5, 10, 10)), 0.0);
1722 }
1723
1724 #[test]
1730 fn test_format_cpu_cores_no_hyperthreading() {
1731 assert_eq!(format_cpu_cores_plain(4, Some(4)), "4 cores");
1733 }
1734
1735 #[test]
1736 fn test_format_cpu_cores_hyperthreaded() {
1737 assert_eq!(format_cpu_cores_plain(16, Some(8)), "8C / 16T");
1739 }
1740
1741 #[test]
1742 fn test_format_cpu_cores_unknown_physical() {
1743 assert_eq!(format_cpu_cores_plain(8, None), "8 cores");
1745 }
1746
1747 #[test]
1748 fn test_format_cpu_cores_physical_equals_zero() {
1749 let result = format_cpu_cores_plain(8, Some(0));
1753 assert!(result.contains("8"), "should mention 8 threads: {}", result);
1754 }
1755
1756 #[cfg(target_os = "linux")]
1757 #[test]
1758 fn test_detect_cpu_cache_returns_some_on_linux() {
1759 if std::path::Path::new("/sys/devices/system/cpu/cpu0/cache").exists() {
1762 let result = detect_cpu_cache();
1763 assert!(result.is_some(), "expected cache info on Linux with sysfs");
1764 let s = result.unwrap();
1765 assert!(
1766 s.contains("L1") || s.contains("L2") || s.contains("L3"),
1767 "expected cache level labels, got: {}",
1768 s
1769 );
1770 }
1771 }
1772
1773 #[test]
1774 fn test_normalize_desktop_name_known() {
1775 assert_eq!(normalize_desktop_name("gnome"), "GNOME");
1776 assert_eq!(normalize_desktop_name("GNOME"), "GNOME");
1777 assert_eq!(normalize_desktop_name("kde"), "KDE Plasma");
1778 assert_eq!(normalize_desktop_name("plasma"), "KDE Plasma");
1779 assert_eq!(normalize_desktop_name("KDE Plasma"), "KDE Plasma");
1780 assert_eq!(normalize_desktop_name("xfce"), "XFCE");
1781 assert_eq!(normalize_desktop_name("lxqt"), "LXQt");
1782 assert_eq!(normalize_desktop_name("mate"), "MATE");
1783 assert_eq!(normalize_desktop_name("cinnamon"), "Cinnamon");
1784 assert_eq!(normalize_desktop_name("e"), "Enlightenment");
1785 }
1786
1787 #[test]
1788 fn test_normalize_desktop_name_unknown_lowercase() {
1789 assert_eq!(normalize_desktop_name("budgie"), "Budgie");
1791 assert_eq!(normalize_desktop_name("niri"), "Niri");
1792 }
1793
1794 #[test]
1795 fn test_normalize_desktop_name_unknown_mixed() {
1796 assert_eq!(normalize_desktop_name("MyDE"), "MyDE");
1798 }
1799
1800 #[test]
1801 fn test_normalize_desktop_name_trims_whitespace() {
1802 assert_eq!(normalize_desktop_name(" gnome "), "GNOME");
1803 assert_eq!(normalize_desktop_name(" niri "), "Niri");
1804 }
1805
1806 #[cfg(target_os = "linux")]
1807 #[test]
1808 fn test_detect_desktop_from_proc_returns_option() {
1809 let result = detect_desktop_from_proc();
1811 if let Some(ref de) = result {
1812 assert!(!de.is_empty(), "desktop name should not be empty");
1813 }
1814 }
1815
1816 #[cfg(target_os = "linux")]
1817 #[test]
1818 fn test_detect_cpu_freq_range_returns_ordered_pair() {
1819 if std::path::Path::new("/sys/devices/system/cpu/cpufreq").exists() {
1820 if let Some((min, max)) = detect_cpu_freq_range() {
1821 assert!(
1822 min <= max,
1823 "min freq should be <= max freq: {} > {}",
1824 min,
1825 max
1826 );
1827 assert!(min > 0, "min freq should be positive");
1828 }
1829 }
1830 }
1831}