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