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