1use crate::gpu;
10use chrono::TimeZone;
11use sysinfo::{Components, System};
12#[cfg(not(target_os = "windows"))]
15use sysinfo::Users;
16
17#[derive(Debug, Default, Clone)]
22pub struct CollectOptions {
23 pub long: bool,
25 pub full: bool,
27 pub fields: Option<Vec<String>>,
29 pub weather_location: Option<String>,
31 pub weather_unit: crate::weather::WeatherUnit,
33}
34
35#[derive(Debug)]
40pub struct SystemInfo {
41 pub os: String,
43 pub kernel: Option<String>,
45 pub hostname: Option<String>,
47 pub arch: String,
49 pub cpu: String,
51 pub cpu_cores: usize,
53 pub cpu_core_info: String,
55 pub memory: String,
57 pub swap: String,
59 pub uptime: String,
61 pub processes: usize,
63 pub load_avg: Option<String>,
65 pub disks: Vec<String>,
67 pub temps: Vec<String>,
69 pub networks: Vec<crate::network::NetworkInterface>,
71 pub boot_time: String,
73 pub battery: Option<String>,
75 pub shell: Option<String>,
77 pub terminal: Option<String>,
79 pub desktop: Option<String>,
81 pub cpu_freq: Option<String>,
83 pub users: usize,
85 pub gpu: Vec<String>,
87 pub packages: Option<usize>,
89 pub current_user: Option<String>,
91 pub local_ip: Option<String>,
93 pub public_ip: Option<String>,
95 pub active_interface: Option<String>,
97 pub motherboard: Option<String>,
99 pub bios: Option<String>,
101 pub displays: Vec<String>,
103 pub audio: Option<String>,
105 pub wifi: Option<String>,
107 pub bluetooth: Option<String>,
109 pub ui_theme: Option<String>,
111 pub icons: Option<String>,
113 pub cursor: Option<String>,
115 pub font: Option<String>,
117 pub terminal_font: Option<String>,
119 pub camera: Vec<String>,
121 pub gamepad: Vec<String>,
123 pub cpu_cache: Option<String>,
125 pub cpu_usage: Option<String>,
127 pub physical_disks: Vec<String>,
129 pub vulkan: Option<String>,
131 pub opengl: Option<String>,
133 pub opencl: Option<String>,
135 pub disk_io: Vec<String>,
137 pub net_io: Vec<String>,
139 pub physical_memory: Option<String>,
141 pub init_system: Option<String>,
143 pub chassis: Option<String>,
145 pub locale: Option<String>,
147 pub bootmgr: Option<String>,
149 pub editor: Option<String>,
151 pub weather: Option<String>,
153 pub wm: Option<String>,
155 pub dns: Vec<String>,
157 pub domain: Option<String>,
160 pub domain_search: Vec<String>,
163 pub terminal_size: Option<String>,
165 pub btrfs: Vec<String>,
167 pub zpool: Vec<String>,
169 pub login_manager: Option<String>,
171 pub brightness: Option<String>,
173 pub power_adapter: Option<String>,
175 pub keyboard: Vec<String>,
178 pub mouse: Vec<String>,
180 pub tpm: Option<String>,
182 pub media: Option<String>,
184 pub player: Option<String>,
186 pub wm_theme: Option<String>,
188 pub wallpaper: Option<String>,
190 pub terminal_theme: Option<String>,
192}
193
194fn cpu_refresh_kind(
210 want_cpu: bool,
211 want_freq: bool,
212 want_usage: bool,
213) -> Option<sysinfo::CpuRefreshKind> {
214 if !(want_cpu || want_freq || want_usage) {
215 return None;
216 }
217 let mut kind = sysinfo::CpuRefreshKind::nothing();
218 if want_freq {
219 kind = kind.with_frequency();
220 }
221 if want_usage && !cfg!(target_os = "windows") {
225 kind = kind.with_cpu_usage();
226 }
227 Some(kind)
228}
229
230fn should_probe_load(want_load: bool) -> bool {
246 want_load && !cfg!(target_os = "windows")
247}
248
249fn needs_process_list(procs: bool, audio: bool, shell: bool, terminal: bool) -> bool {
259 procs || audio || shell || terminal
260}
261
262impl SystemInfo {
263 pub fn collect(opts: CollectOptions) -> anyhow::Result<Self> {
268 let should_collect = |field_name: &str| -> bool {
269 match &opts.fields {
270 Some(fields) => {
271 let norm_field = field_name.to_lowercase().replace(['-', '_'], " ");
272 let norm_field_no_spaces = norm_field.replace(' ', "");
273 fields.iter().any(|f| {
274 let norm_f = f.to_lowercase().replace(['-', '_'], " ");
275 norm_f == norm_field || norm_f.replace(' ', "") == norm_field_no_spaces
276 })
277 }
278 None => true,
279 }
280 };
281
282 let mut refresh_kind = sysinfo::RefreshKind::nothing();
283 if let Some(cpu_kind) = cpu_refresh_kind(
286 should_collect("cpu"),
287 should_collect("cpu-freq"),
288 should_collect("cpu-usage"),
289 ) {
290 refresh_kind = refresh_kind.with_cpu(cpu_kind);
291 }
292 if should_collect("memory")
293 || should_collect("swap")
294 || should_collect("phys mem")
295 || should_collect("phys-mem")
296 {
297 refresh_kind = refresh_kind.with_memory(sysinfo::MemoryRefreshKind::everything());
298 }
299 if needs_process_list(
300 should_collect("procs"),
301 should_collect("audio"),
302 should_collect("shell"),
303 should_collect("terminal"),
304 ) {
305 refresh_kind = refresh_kind.with_processes(sysinfo::ProcessRefreshKind::nothing());
306 }
307
308 #[cfg_attr(target_os = "windows", allow(unused_mut))]
311 let mut sys = System::new_with_specifics(refresh_kind);
312
313 let os = System::long_os_version()
314 .or_else(System::name)
315 .unwrap_or_else(|| "Unknown".to_string());
316
317 let kernel = System::kernel_version();
318 let hostname = System::host_name();
319
320 let cpu = if should_collect("cpu") {
321 sys.cpus()
322 .first()
323 .map(|c| c.brand().to_string())
324 .unwrap_or_else(|| "Unknown CPU".to_string())
325 } else {
326 String::new()
327 };
328
329 let cpu_cores = if should_collect("cpu") {
330 sys.cpus().len()
331 } else {
332 0
333 };
334 let cpu_core_info = if should_collect("cpu") {
335 format_cpu_cores(cpu_cores, System::physical_core_count())
336 } else {
337 String::new()
338 };
339
340 let memory = if should_collect("memory") {
341 let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
342 let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
343 format!("{:.1} / {:.1} GB", used_mem, total_mem)
344 } else {
345 String::new()
346 };
347
348 let swap = if should_collect("swap") {
349 let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
350 let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
351 if total_swap > 0.0 {
352 format!("{:.1} / {:.1} GB", used_swap, total_swap)
353 } else {
354 "No swap".to_string()
355 }
356 } else {
357 String::new()
358 };
359
360 let uptime = format!("{}s", System::uptime());
361
362 let disks: Vec<String> = if should_collect("disk") {
363 let disks_list = crate::disk::detect_logical_disks(opts.full);
364 let format_disk = |(mount, total, avail, fs): &(String, u64, u64, String)| {
365 let total_gb = *total as f64 / 1024.0 / 1024.0 / 1024.0;
366 let avail_gb = *avail as f64 / 1024.0 / 1024.0 / 1024.0;
367 format!(
368 "{} ({}): {:.1} GB free / {:.1} GB",
369 mount, fs, avail_gb, total_gb
370 )
371 };
372 if !opts.long {
373 let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
374 let home_path = std::path::Path::new(&home);
375 let best = disks_list
376 .iter()
377 .filter(|(mp, ..)| home_path.starts_with(mp))
378 .max_by_key(|(mp, ..)| std::path::Path::new(mp).components().count());
379 if let Some(disk) = best {
380 vec![format_disk(disk)]
381 } else {
382 disks_list.iter().map(format_disk).collect()
383 }
384 } else {
385 disks_list.iter().map(format_disk).collect()
386 }
387 } else {
388 Vec::new()
389 };
390
391 let battery = if should_collect("battery") {
392 crate::battery::get_battery_info().map(|bat| {
393 let pct = bat.percentage;
394 let state = match bat.state {
395 crate::battery::BatteryState::Charging => "charging",
396 crate::battery::BatteryState::Discharging => "discharging",
397 crate::battery::BatteryState::Full => "full",
398 _ => "not charging",
399 };
400 let vendor = bat.vendor;
401 let model = bat.model;
402
403 let time_str = match bat.state {
405 crate::battery::BatteryState::Charging => bat.time_remaining.map(|d| {
406 let total_mins = d.as_secs() / 60;
407 let hours = total_mins / 60;
408 let mins = total_mins % 60;
409 if hours >= 24 {
410 let days = hours / 24;
411 let rem_hours = hours % 24;
412 format!("{}d {}h until full", days, rem_hours)
413 } else if hours > 0 {
414 format!("{}h {}m until full", hours, mins)
415 } else {
416 format!("{}m until full", mins)
417 }
418 }),
419 crate::battery::BatteryState::Discharging => bat.time_remaining.map(|d| {
420 let total_mins = d.as_secs() / 60;
421 let hours = total_mins / 60;
422 let mins = total_mins % 60;
423 if hours >= 24 {
424 let days = hours / 24;
425 let rem_hours = hours % 24;
426 format!("{}d {}h remaining", days, rem_hours)
427 } else if hours > 0 {
428 format!("{}h {}m remaining", hours, mins)
429 } else {
430 format!("{}m remaining", mins)
431 }
432 }),
433 _ => None,
434 };
435
436 let mut parts = vec![state.to_string()];
437 if let Some(t) = time_str {
438 parts.insert(0, t);
439 }
440 if let Some(health) = bat.health {
441 if health < 99.0 {
442 parts.push(format!("{:.0}% health", health));
443 }
444 }
445
446 let base = format!("{:.0}% ({})", pct, parts.join(", "));
447
448 match (vendor, model) {
449 (Some(v), Some(m)) => format!("{} [{} {}]", base, v, m),
450 (Some(v), None) => format!("{} [{}]", base, v),
451 _ => base,
452 }
453 })
454 } else {
455 None
456 };
457
458 let arch = System::cpu_arch();
459
460 let processes = if should_collect("procs") || should_collect("audio") {
461 sys.processes().len()
462 } else {
463 0
464 };
465
466 let load_avg = if should_probe_load(should_collect("load")) {
467 let avg = System::load_average();
468 if avg.one > 0.0 || avg.five > 0.0 {
469 Some(format!(
470 "{:.2}, {:.2}, {:.2}",
471 avg.one, avg.five, avg.fifteen
472 ))
473 } else {
474 None
475 }
476 } else {
477 None
478 };
479
480 #[cfg(target_os = "windows")]
484 let cpu_sample0 = win_cpu::sample();
485 #[cfg(target_os = "windows")]
486 let cpu_t0 = std::time::Instant::now();
487
488 let want_disk_io = should_collect("disk-io") || should_collect("disk io");
494 let want_net_io = should_collect("net-io") || should_collect("net io");
495 let disk_io_sample0 = if want_disk_io {
496 crate::io::sample_disk_io()
497 } else {
498 Vec::new()
499 };
500 let net_io_sample0 = if want_net_io {
501 crate::io::sample_net_io()
502 } else {
503 Vec::new()
504 };
505 let io_t0 = std::time::Instant::now();
506
507 let (
509 gpu,
510 packages,
511 public_ip,
512 (local_ip, active_interface),
513 motherboard,
514 bios,
515 displays,
516 audio,
517 wifi,
518 bluetooth,
519 (ui_theme, icons, cursor, font),
520 camera,
521 gamepad,
522 physical_disks,
523 physical_memory,
524 weather,
525 btrfs,
526 zpool,
527 (media, player),
528 gpu_apis,
529 shell,
530 ) = std::thread::scope(|s| {
531 let gpu_handle = if should_collect("gpu") {
532 Some(s.spawn(|| {
533 gpu::detect_gpus()
534 .into_iter()
535 .map(|g| g.format())
536 .collect::<Vec<String>>()
537 }))
538 } else {
539 None
540 };
541 let packages_handle = if should_collect("packages") {
542 Some(s.spawn(crate::packages::detect_packages))
543 } else {
544 None
545 };
546 let public_ip_handle = if should_collect("public ip") {
547 Some(s.spawn(crate::network::detect_public_ip))
548 } else {
549 None
550 };
551 let network_ips_handle = if should_collect("net") {
552 Some(s.spawn(crate::network::detect_active_interface_and_local_ip))
553 } else {
554 None
555 };
556 let motherboard_handle = if should_collect("motherboard") {
557 Some(s.spawn(crate::motherboard::detect_motherboard))
558 } else {
559 None
560 };
561 let bios_handle = if should_collect("bios") {
562 Some(s.spawn(crate::bios::detect_bios))
563 } else {
564 None
565 };
566 let displays_handle = if should_collect("display") {
567 Some(s.spawn(crate::display::detect_displays))
568 } else {
569 None
570 };
571 let audio_handle = if should_collect("audio") {
572 Some(s.spawn(|| crate::audio::detect_audio(&sys)))
573 } else {
574 None
575 };
576 let wifi_handle = if should_collect("wifi") {
577 Some(s.spawn(crate::network::detect_wifi))
578 } else {
579 None
580 };
581 let bluetooth_handle = if should_collect("bluetooth") {
582 Some(s.spawn(crate::bluetooth::detect_bluetooth))
583 } else {
584 None
585 };
586 let ui_theme_and_fonts_handle = if should_collect("theme")
587 || should_collect("icons")
588 || should_collect("cursor")
589 || should_collect("font")
590 {
591 Some(s.spawn(crate::theme::detect_ui_theme_and_fonts))
592 } else {
593 None
594 };
595 let camera_handle = if should_collect("camera") {
596 Some(s.spawn(crate::camera::detect_camera))
597 } else {
598 None
599 };
600 let gamepad_handle = if should_collect("gamepad") {
601 Some(s.spawn(crate::gamepad::detect_gamepad))
602 } else {
603 None
604 };
605 let physical_disks_handle = if should_collect("phys disk") {
606 Some(s.spawn(crate::disk::detect_physical_disks))
607 } else {
608 None
609 };
610 let physical_memory_handle = if should_collect("phys mem") {
611 Some(s.spawn(crate::memory::detect_physical_memory))
612 } else {
613 None
614 };
615 let weather_location = opts.weather_location.clone();
616 let weather_unit = opts.weather_unit;
617 let weather_handle = if should_collect("weather") {
618 Some(s.spawn(move || {
619 crate::weather::detect_weather(weather_location.as_deref(), weather_unit)
620 }))
621 } else {
622 None
623 };
624 let btrfs_handle = if should_collect("btrfs") {
625 Some(s.spawn(crate::btrfs::detect_btrfs))
626 } else {
627 None
628 };
629 let zpool_handle = if should_collect("zpool") {
630 Some(s.spawn(crate::zfs::detect_zpool))
631 } else {
632 None
633 };
634 let media_handle = if should_collect("media") || should_collect("player") {
635 Some(s.spawn(crate::media::detect_media))
636 } else {
637 None
638 };
639 let gpu_apis_handle =
643 if should_collect("vulkan") || should_collect("opengl") || should_collect("opencl")
644 {
645 Some(s.spawn(crate::gpu_api::detect_gpu_apis))
646 } else {
647 None
648 };
649 let shell_handle = if should_collect("shell") {
655 Some(s.spawn(|| crate::shell::detect_shell(&sys)))
656 } else {
657 None
658 };
659
660 (
661 gpu_handle
662 .map(|h| h.join().unwrap_or_default())
663 .unwrap_or_default(),
664 packages_handle.and_then(|h| h.join().ok().flatten()),
665 public_ip_handle.and_then(|h| h.join().ok().flatten()),
666 network_ips_handle
667 .map(|h| h.join().unwrap_or((None, None)))
668 .unwrap_or((None, None)),
669 motherboard_handle.and_then(|h| h.join().ok().flatten()),
670 bios_handle.and_then(|h| h.join().ok().flatten()),
671 displays_handle
672 .map(|h| h.join().unwrap_or_default())
673 .unwrap_or_default(),
674 audio_handle.and_then(|h| h.join().ok().flatten()),
675 wifi_handle.and_then(|h| h.join().ok().flatten()),
676 bluetooth_handle.and_then(|h| h.join().ok().flatten()),
677 ui_theme_and_fonts_handle
678 .map(|h| h.join().unwrap_or((None, None, None, None)))
679 .unwrap_or((None, None, None, None)),
680 camera_handle
681 .map(|h| h.join().unwrap_or_default())
682 .unwrap_or_default(),
683 gamepad_handle
684 .map(|h| h.join().unwrap_or_default())
685 .unwrap_or_default(),
686 physical_disks_handle
687 .map(|h| h.join().unwrap_or_default())
688 .unwrap_or_default(),
689 physical_memory_handle.and_then(|h| h.join().ok().flatten()),
690 weather_handle.and_then(|h| h.join().ok().flatten()),
691 btrfs_handle
692 .map(|h| h.join().unwrap_or_default())
693 .unwrap_or_default(),
694 zpool_handle
695 .map(|h| h.join().unwrap_or_default())
696 .unwrap_or_default(),
697 media_handle
698 .map(|h| h.join().unwrap_or((None, None)))
699 .unwrap_or((None, None)),
700 gpu_apis_handle
701 .map(|h| h.join().unwrap_or_default())
702 .unwrap_or_default(),
703 shell_handle.and_then(|h| h.join().ok().flatten()),
704 )
705 });
706
707 let mut temps: Vec<String> = if should_collect("temp") {
708 Components::new_with_refreshed_list()
709 .iter()
710 .filter_map(|c| {
711 c.temperature().and_then(|t| {
712 if t > 0.0 {
713 Some(format!("{}: {:.0}°C", c.label(), t))
714 } else {
715 None
716 }
717 })
718 })
719 .collect()
720 } else {
721 Vec::new()
722 };
723
724 temps.sort_by(|a, b| {
726 let a_cpu = a.to_lowercase().contains("cpu") || a.to_lowercase().contains("core");
727 let b_cpu = b.to_lowercase().contains("cpu") || b.to_lowercase().contains("core");
728 b_cpu.cmp(&a_cpu)
729 });
730
731 let networks = if should_collect("net") {
732 crate::network::detect_networks(active_interface.as_deref(), local_ip.as_deref())
733 } else {
734 Vec::new()
735 };
736
737 let boot_timestamp = System::boot_time();
738 let boot_dt = chrono::Local
739 .timestamp_opt(boot_timestamp as i64, 0)
740 .single()
741 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
742 .unwrap_or_else(|| boot_timestamp.to_string());
743 let boot_time = boot_dt;
744
745 let terminal = if should_collect("terminal") {
747 crate::terminal::detect_terminal(&sys)
748 } else {
749 None
750 };
751 let terminal_font = if should_collect("terminal font")
752 || should_collect("terminal-font")
753 || should_collect("terminal_font")
754 {
755 crate::terminal::detect_terminal_font(terminal.as_deref())
756 } else {
757 None
758 };
759 let desktop = if should_collect("desktop") {
760 std::env::var("XDG_CURRENT_DESKTOP")
761 .or_else(|_| std::env::var("DESKTOP_SESSION"))
762 .or_else(|_| std::env::var("XDG_SESSION_DESKTOP"))
763 .or_else(|_| std::env::var("GDMSESSION"))
764 .ok()
765 .map(|s| normalize_desktop_name(&s))
766 .filter(|s| !s.is_empty())
767 .or_else(detect_desktop_from_proc)
768 } else {
769 None
770 };
771
772 let cpu_freq = if should_collect("cpu-freq")
774 || should_collect("cpu freq")
775 || should_collect("cpu_freq")
776 {
777 sys.cpus().first().map(|c| {
778 let current = format!("{:.2} GHz", c.frequency() as f64 / 1000.0);
779 if let Some((min_khz, max_khz)) = detect_cpu_freq_range() {
780 let min_ghz = min_khz as f64 / 1_000_000.0;
781 let max_ghz = max_khz as f64 / 1_000_000.0;
782 format!("{} ({:.2} \u{2013} {:.2} GHz)", current, min_ghz, max_ghz)
783 } else {
784 current
785 }
786 })
787 } else {
788 None
789 };
790
791 let cpu_cache = if should_collect("cpu-cache")
793 || should_collect("cpu cache")
794 || should_collect("cpu_cache")
795 {
796 detect_cpu_cache()
797 } else {
798 None
799 };
800
801 let cpu_usage = if should_collect("cpu-usage")
806 || should_collect("cpu usage")
807 || should_collect("cpu_usage")
808 {
809 #[cfg(not(target_os = "windows"))]
810 {
811 std::thread::sleep(std::time::Duration::from_millis(200));
812 sys.refresh_cpu_usage();
813 let usage: f32 =
814 sys.cpus().iter().map(|c| c.cpu_usage()).sum::<f32>() / sys.cpus().len() as f32;
815 let avg = System::load_average();
816 let load_str = format!("{:.2}, {:.2}, {:.2}", avg.one, avg.five, avg.fifteen);
817 if usage > 0.0 {
818 Some(format!("{:.1}% (load: {})", usage, load_str))
819 } else if avg.one > 0.0 {
820 Some(format!("load: {}", load_str))
821 } else {
822 None
823 }
824 }
825 #[cfg(target_os = "windows")]
826 {
827 let floor = std::time::Duration::from_millis(100);
831 let elapsed = cpu_t0.elapsed();
832 if elapsed < floor {
833 std::thread::sleep(floor - elapsed);
834 }
835 match (cpu_sample0, win_cpu::sample()) {
836 (Some(s0), Some(s1)) => {
837 let usage = win_cpu::usage_percent(s0, s1);
838 if usage > 0.0 {
839 Some(format!("{:.1}%", usage))
840 } else {
841 None
842 }
843 }
844 _ => None,
845 }
846 }
847 } else {
848 None
849 };
850
851 let (disk_io, net_io) = if want_disk_io || want_net_io {
857 let floor = std::time::Duration::from_millis(100);
858 let elapsed = io_t0.elapsed();
859 if elapsed < floor {
860 std::thread::sleep(floor - elapsed);
861 }
862 let elapsed_secs = io_t0.elapsed().as_secs_f64();
863 let disk_io = if want_disk_io {
864 crate::io::compute_rates(
865 &disk_io_sample0,
866 &crate::io::sample_disk_io(),
867 elapsed_secs,
868 )
869 .iter()
870 .map(|r| crate::io::format_io_line(r, "R", "W"))
871 .collect()
872 } else {
873 Vec::new()
874 };
875 let net_io = if want_net_io {
876 let rates = crate::io::compute_rates(
877 &net_io_sample0,
878 &crate::io::sample_net_io(),
879 elapsed_secs,
880 );
881 crate::io::select_net_rates(rates, active_interface.as_deref())
882 .iter()
883 .map(|r| crate::io::format_io_line(r, "RX", "TX"))
884 .collect()
885 } else {
886 Vec::new()
887 };
888 (disk_io, net_io)
889 } else {
890 (Vec::new(), Vec::new())
891 };
892
893 let init_system = if should_collect("init") || should_collect("init system") {
894 detect_init_system()
895 } else {
896 None
897 };
898
899 let chassis = if should_collect("chassis") {
900 detect_chassis()
901 } else {
902 None
903 };
904
905 let locale = if should_collect("locale") {
906 std::env::var("LC_ALL")
907 .ok()
908 .filter(|s| !s.is_empty())
909 .or_else(|| std::env::var("LC_MESSAGES").ok().filter(|s| !s.is_empty()))
910 .or_else(|| std::env::var("LANG").ok().filter(|s| !s.is_empty()))
911 } else {
912 None
913 };
914
915 let bootmgr = if should_collect("bootmgr") || should_collect("boot") {
916 detect_bootmgr()
917 } else {
918 None
919 };
920
921 let login_manager = if should_collect("login-manager") || should_collect("lm") {
922 detect_login_manager()
923 } else {
924 None
925 };
926
927 let brightness = if should_collect("brightness") {
928 detect_brightness()
929 } else {
930 None
931 };
932
933 let power_adapter = if should_collect("power-adapter") {
934 detect_power_adapter()
935 } else {
936 None
937 };
938
939 let (keyboard, mouse) = if should_collect("keyboard") || should_collect("mouse") {
942 let (kbds, mice) = crate::input::detect_input_devices();
943 (
944 if should_collect("keyboard") {
945 kbds
946 } else {
947 Vec::new()
948 },
949 if should_collect("mouse") {
950 mice
951 } else {
952 Vec::new()
953 },
954 )
955 } else {
956 (Vec::new(), Vec::new())
957 };
958
959 let tpm = if should_collect("tpm") {
960 detect_tpm()
961 } else {
962 None
963 };
964
965 let editor = if should_collect("editor") {
966 std::env::var("VISUAL")
967 .ok()
968 .filter(|s| !s.is_empty())
969 .or_else(|| std::env::var("EDITOR").ok().filter(|s| !s.is_empty()))
970 } else {
971 None
972 };
973
974 let wm = if should_collect("wm") || should_collect("window manager") {
975 crate::wm::detect_wm()
976 } else {
977 None
978 };
979
980 let dns = if should_collect("dns") {
981 crate::network::detect_dns()
982 } else {
983 Vec::new()
984 };
985
986 let domain = if should_collect("domain") {
987 crate::network::detect_domain()
988 } else {
989 None
990 };
991
992 let domain_search = if should_collect("domain-search") || should_collect("domain search") {
993 crate::network::detect_domain_search()
994 } else {
995 Vec::new()
996 };
997
998 let terminal_size = if should_collect("terminal size")
999 || should_collect("terminal-size")
1000 || should_collect("terminal_size")
1001 {
1002 crate::terminal::detect_terminal_size()
1003 } else {
1004 None
1005 };
1006
1007 let current_user = std::env::var("USER").ok();
1009
1010 let users = if should_collect("users") {
1015 #[cfg(target_os = "windows")]
1016 {
1017 crate::win_users::active_user_session_count()
1018 }
1019 #[cfg(not(target_os = "windows"))]
1020 {
1021 Users::new_with_refreshed_list()
1022 .iter()
1023 .filter(|user| {
1024 user.id()
1026 .to_string()
1027 .parse::<u32>()
1028 .map(|uid| uid >= 1000)
1029 .unwrap_or(false)
1030 })
1031 .count()
1032 }
1033 } else {
1034 0
1035 };
1036
1037 let wm_theme = if should_collect("wm-theme")
1038 || should_collect("wm theme")
1039 || should_collect("wm_theme")
1040 {
1041 crate::theme::detect_wm_theme(wm.as_deref(), desktop.as_deref())
1042 } else {
1043 None
1044 };
1045
1046 let wallpaper = if should_collect("wallpaper") {
1047 crate::theme::detect_wallpaper(desktop.as_deref(), wm.as_deref())
1048 } else {
1049 None
1050 };
1051
1052 let terminal_theme = if should_collect("terminal-theme")
1053 || should_collect("terminal theme")
1054 || should_collect("terminal_theme")
1055 {
1056 crate::terminal::detect_terminal_theme(terminal.as_deref())
1057 } else {
1058 None
1059 };
1060
1061 Ok(Self {
1062 os,
1063 kernel,
1064 hostname,
1065 arch,
1066 cpu,
1067 cpu_cores,
1068 cpu_core_info,
1069 memory,
1070 swap,
1071 uptime,
1072 processes,
1073 load_avg,
1074 disks,
1075 temps,
1076 networks,
1077 boot_time,
1078 battery,
1079 shell,
1080 terminal,
1081 desktop,
1082 cpu_freq,
1083 users,
1084 gpu,
1085 packages,
1086 current_user,
1087 local_ip,
1088 public_ip,
1089 active_interface,
1090 motherboard,
1091 bios,
1092 displays,
1093 audio,
1094 wifi,
1095 bluetooth,
1096 ui_theme,
1097 icons,
1098 cursor,
1099 font,
1100 terminal_font,
1101 camera,
1102 gamepad,
1103 cpu_cache,
1104 cpu_usage,
1105 physical_disks,
1106 vulkan: gpu_apis.vulkan,
1107 opengl: gpu_apis.opengl,
1108 opencl: gpu_apis.opencl,
1109 disk_io,
1110 net_io,
1111 physical_memory,
1112 init_system,
1113 chassis,
1114 locale,
1115 bootmgr,
1116 editor,
1117 weather,
1118 wm,
1119 dns,
1120 domain,
1121 domain_search,
1122 terminal_size,
1123 btrfs,
1124 zpool,
1125 login_manager,
1126 brightness,
1127 power_adapter,
1128 keyboard,
1129 mouse,
1130 tpm,
1131 media,
1132 player,
1133 wm_theme,
1134 wallpaper,
1135 terminal_theme,
1136 })
1137 }
1138}
1139
1140pub fn detect_cpu_cache() -> Option<String> {
1146 #[cfg(target_os = "linux")]
1147 {
1148 use std::fs;
1149 let cache_dir = std::path::Path::new("/sys/devices/system/cpu/cpu0/cache");
1150 if !cache_dir.exists() {
1151 return None;
1152 }
1153
1154 struct CacheEntry {
1155 level: u32,
1156 kind: String,
1157 size_kb: u64,
1158 }
1159
1160 let mut entries: Vec<CacheEntry> = Vec::new();
1161
1162 let Ok(indices) = fs::read_dir(cache_dir) else {
1163 return None;
1164 };
1165
1166 for entry in indices.flatten() {
1167 let path = entry.path();
1168 if !path.is_dir() {
1170 continue;
1171 }
1172 let level_str = match fs::read_to_string(path.join("level")) {
1173 Ok(s) => s,
1174 Err(_) => continue,
1175 };
1176 let level: u32 = match level_str.trim().parse() {
1177 Ok(n) => n,
1178 Err(_) => continue,
1179 };
1180 let kind = match fs::read_to_string(path.join("type")) {
1181 Ok(s) => s.trim().to_string(),
1182 Err(_) => continue,
1183 };
1184 let size_str = match fs::read_to_string(path.join("size")) {
1185 Ok(s) => s,
1186 Err(_) => continue,
1187 };
1188 let size_raw = size_str.trim();
1189 let size_kb: u64 = if let Some(k) = size_raw.strip_suffix('K') {
1190 match k.parse() {
1191 Ok(n) => n,
1192 Err(_) => continue,
1193 }
1194 } else if let Some(m) = size_raw.strip_suffix('M') {
1195 match m.parse::<u64>() {
1196 Ok(n) => n * 1024,
1197 Err(_) => continue,
1198 }
1199 } else {
1200 match size_raw.parse() {
1201 Ok(n) => n,
1202 Err(_) => continue,
1203 }
1204 };
1205
1206 if kind != "Instruction" && kind != "Data" && kind != "Unified" {
1207 continue;
1208 }
1209
1210 entries.push(CacheEntry {
1211 level,
1212 kind,
1213 size_kb,
1214 });
1215 }
1216
1217 if entries.is_empty() {
1218 return None;
1219 }
1220
1221 entries.sort_by_key(|e| (e.level, e.kind.clone()));
1222
1223 let fmt_size = |kb: u64| -> String {
1224 if kb >= 1024 && kb.is_multiple_of(1024) {
1225 format!("{}M", kb / 1024)
1226 } else if kb >= 1024 {
1227 format!("{:.2}M", kb as f64 / 1024.0)
1228 .trim_end_matches('0')
1229 .trim_end_matches('.')
1230 .to_string()
1231 + "M"
1232 } else {
1233 format!("{}K", kb)
1234 }
1235 };
1236
1237 let mut seen = std::collections::HashSet::new();
1239 let mut parts: Vec<String> = Vec::new();
1240 for e in &entries {
1241 let label = match (e.level, e.kind.as_str()) {
1242 (1, "Data") => "L1d".to_string(),
1243 (1, "Instruction") => "L1i".to_string(),
1244 (1, "Unified") => "L1".to_string(),
1245 (n, _) => format!("L{}", n),
1246 };
1247 if seen.insert(label.clone()) {
1248 parts.push(format!("{}: {}", label, fmt_size(e.size_kb)));
1249 }
1250 }
1251
1252 if parts.is_empty() {
1253 None
1254 } else {
1255 Some(parts.join(", "))
1256 }
1257 }
1258 #[cfg(target_os = "macos")]
1259 {
1260 extern "C" {
1261 fn sysctlbyname(
1262 name: *const i8,
1263 oldp: *mut std::ffi::c_void,
1264 oldlenp: *mut usize,
1265 newp: *mut std::ffi::c_void,
1266 newlen: usize,
1267 ) -> i32;
1268 }
1269
1270 let read_u64 = |key: &str| -> Option<u64> {
1271 let name = std::ffi::CString::new(key).ok()?;
1272 let mut value: u64 = 0;
1273 let mut size = std::mem::size_of::<u64>();
1274 let ret = unsafe {
1275 sysctlbyname(
1276 name.as_ptr(),
1277 &mut value as *mut u64 as *mut std::ffi::c_void,
1278 &mut size,
1279 std::ptr::null_mut(),
1280 0,
1281 )
1282 };
1283 if ret == 0 && value > 0 {
1284 Some(value)
1285 } else {
1286 None
1287 }
1288 };
1289
1290 let fmt_bytes = |bytes: u64| -> String {
1291 if bytes >= 1024 * 1024 {
1292 format!("{}M", bytes / (1024 * 1024))
1293 } else {
1294 format!("{}K", bytes / 1024)
1295 }
1296 };
1297
1298 let mut parts = Vec::new();
1299 if let Some(v) = read_u64("hw.l1dcachesize") {
1300 parts.push(format!("L1d: {}", fmt_bytes(v)));
1301 }
1302 if let Some(v) = read_u64("hw.l1icachesize") {
1303 parts.push(format!("L1i: {}", fmt_bytes(v)));
1304 }
1305 if let Some(v) = read_u64("hw.l2cachesize") {
1306 parts.push(format!("L2: {}", fmt_bytes(v)));
1307 }
1308 if let Some(v) = read_u64("hw.l3cachesize") {
1309 parts.push(format!("L3: {}", fmt_bytes(v)));
1310 }
1311
1312 if parts.is_empty() {
1313 None
1314 } else {
1315 Some(parts.join(", "))
1316 }
1317 }
1318 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1319 {
1320 None
1321 }
1322}
1323
1324pub fn format_cpu_cores(logical: usize, physical: Option<usize>) -> String {
1329 #[cfg(target_os = "linux")]
1331 if let Some(hybrid) = detect_hybrid_cores(logical) {
1332 return hybrid;
1333 }
1334
1335 #[cfg(target_os = "macos")]
1337 if let Some(hybrid) = detect_macos_hybrid_cores(logical) {
1338 return hybrid;
1339 }
1340
1341 format_cpu_cores_plain(logical, physical)
1342}
1343
1344fn format_cpu_cores_plain(logical: usize, physical: Option<usize>) -> String {
1354 match physical {
1355 Some(p) if p < logical => format!("{}C / {}T", p, logical),
1356 _ => format!("{} cores", logical),
1357 }
1358}
1359
1360#[cfg(target_os = "linux")]
1363fn detect_hybrid_cores(logical: usize) -> Option<String> {
1364 use std::collections::HashMap;
1365 use std::fs;
1366
1367 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
1368 if !cpufreq.exists() {
1369 return None;
1370 }
1371
1372 let mut freq_to_count: HashMap<u64, usize> = HashMap::new();
1374 let mut total_accounted = 0usize;
1375
1376 let Ok(policies) = fs::read_dir(cpufreq) else {
1377 return None;
1378 };
1379
1380 for policy in policies.flatten() {
1381 let path = policy.path();
1382 if !path.is_dir() {
1383 continue;
1384 }
1385 let max_freq_str = fs::read_to_string(path.join("cpuinfo_max_freq")).ok()?;
1386 let max_freq: u64 = max_freq_str.trim().parse().ok()?;
1387 let affected = fs::read_to_string(path.join("affected_cpus")).ok()?;
1388 let count = affected.split_whitespace().count();
1389 *freq_to_count.entry(max_freq).or_insert(0) += count;
1390 total_accounted += count;
1391 }
1392
1393 if freq_to_count.len() != 2 || total_accounted != logical {
1395 return None;
1396 }
1397
1398 let mut tiers: Vec<(u64, usize)> = freq_to_count.into_iter().collect();
1399 tiers.sort_by_key(|t| std::cmp::Reverse(t.0)); let (_, p_count) = tiers[0];
1401 let (_, e_count) = tiers[1];
1402
1403 Some(format!("{}P + {}E / {}T", p_count, e_count, logical))
1404}
1405
1406#[cfg(target_os = "macos")]
1409fn detect_macos_hybrid_cores(logical: usize) -> Option<String> {
1410 extern "C" {
1411 fn sysctlbyname(
1412 name: *const i8,
1413 oldp: *mut std::ffi::c_void,
1414 oldlenp: *mut usize,
1415 newp: *mut std::ffi::c_void,
1416 newlen: usize,
1417 ) -> i32;
1418 }
1419
1420 let read_u32 = |key: &str| -> Option<u32> {
1421 let name = std::ffi::CString::new(key).ok()?;
1422 let mut value: u32 = 0;
1423 let mut size = std::mem::size_of::<u32>();
1424 let ret = unsafe {
1425 sysctlbyname(
1426 name.as_ptr(),
1427 &mut value as *mut u32 as *mut std::ffi::c_void,
1428 &mut size,
1429 std::ptr::null_mut(),
1430 0,
1431 )
1432 };
1433 if ret == 0 {
1434 Some(value)
1435 } else {
1436 None
1437 }
1438 };
1439
1440 let nlevels = read_u32("hw.nperflevels")?;
1442 if nlevels != 2 {
1443 return None;
1444 }
1445
1446 let p_cores = read_u32("hw.perflevel0.logicalcpu")? as usize;
1447 let e_cores = read_u32("hw.perflevel1.logicalcpu")? as usize;
1448
1449 if p_cores + e_cores != logical {
1450 return None;
1451 }
1452
1453 Some(format!("{}P + {}E / {}T", p_cores, e_cores, logical))
1454}
1455
1456pub fn detect_cpu_freq_range() -> Option<(u64, u64)> {
1459 #[cfg(target_os = "linux")]
1460 {
1461 use std::fs;
1462 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
1463 if !cpufreq.exists() {
1464 return None;
1465 }
1466 let mut global_min: Option<u64> = None;
1467 let mut global_max: Option<u64> = None;
1468 let Ok(policies) = fs::read_dir(cpufreq) else {
1469 return None;
1470 };
1471 for policy in policies.flatten() {
1472 let path = policy.path();
1473 if !path.is_dir() {
1474 continue;
1475 }
1476 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_min_freq")) {
1477 if let Ok(v) = s.trim().parse::<u64>() {
1478 global_min = Some(global_min.map_or(v, |m: u64| m.min(v)));
1479 }
1480 }
1481 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_max_freq")) {
1482 if let Ok(v) = s.trim().parse::<u64>() {
1483 global_max = Some(global_max.map_or(v, |m: u64| m.max(v)));
1484 }
1485 }
1486 }
1487 match (global_min, global_max) {
1488 (Some(min), Some(max)) => Some((min, max)),
1489 _ => None,
1490 }
1491 }
1492 #[cfg(not(target_os = "linux"))]
1493 {
1494 None
1495 }
1496}
1497
1498#[cfg(not(target_os = "linux"))]
1499fn detect_desktop_from_proc() -> Option<String> {
1500 None
1501}
1502
1503#[cfg(target_os = "linux")]
1504fn detect_desktop_from_proc() -> Option<String> {
1505 const DE_PROCS: &[(&str, &str)] = &[
1506 ("gnome-shell", "GNOME"),
1507 ("plasmashell", "KDE Plasma"),
1508 ("xfce4-session", "XFCE"),
1509 ("mate-session", "MATE"),
1510 ("cinnamon", "Cinnamon"),
1511 ("budgie-daemon", "Budgie"),
1512 ("budgie-panel", "Budgie"),
1513 ("lxsession", "LXDE"),
1514 ("lxqt-session", "LXQt"),
1515 ("deepin-session", "Deepin"),
1516 ("dde-session-daemon", "Deepin"),
1517 ("gala", "Pantheon"),
1518 ("enlightenment", "Enlightenment"),
1519 ];
1520 let Ok(entries) = std::fs::read_dir("/proc") else {
1521 return None;
1522 };
1523 for entry in entries.filter_map(|e| e.ok()) {
1524 let path = entry.path();
1525 if !path.is_dir() {
1526 continue;
1527 }
1528 let Ok(comm) = std::fs::read_to_string(path.join("comm")) else {
1529 continue;
1530 };
1531 let comm = comm.trim().to_lowercase();
1532 for (proc_name, de_name) in DE_PROCS {
1533 if comm == *proc_name || comm.starts_with(proc_name) {
1534 return Some(de_name.to_string());
1535 }
1536 }
1537 }
1538 None
1539}
1540
1541fn normalize_desktop_name(raw: &str) -> String {
1542 let s = raw.trim();
1543 match s.to_lowercase().as_str() {
1545 "gnome" => "GNOME".to_string(),
1546 "kde" | "kde plasma" | "plasma" => "KDE Plasma".to_string(),
1547 "xfce" => "XFCE".to_string(),
1548 "lxde" => "LXDE".to_string(),
1549 "lxqt" => "LXQt".to_string(),
1550 "mate" => "MATE".to_string(),
1551 "cinnamon" => "Cinnamon".to_string(),
1552 "budgie" => "Budgie".to_string(),
1553 "deepin" => "Deepin".to_string(),
1554 "pantheon" => "Pantheon".to_string(),
1555 "unity" => "Unity".to_string(),
1556 "enlightenment" | "e" => "Enlightenment".to_string(),
1557 _ => {
1558 if s.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()) {
1560 let mut chars = s.chars();
1561 match chars.next() {
1562 None => String::new(),
1563 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
1564 }
1565 } else {
1566 s.to_string()
1567 }
1568 }
1569 }
1570}
1571
1572fn detect_init_system() -> Option<String> {
1573 #[cfg(target_os = "linux")]
1574 {
1575 let comm = std::fs::read_to_string("/proc/1/comm")
1576 .map(|s| s.trim().to_string())
1577 .ok()
1578 .filter(|s| !s.is_empty());
1579 if let Some(name) = comm {
1580 return Some(name);
1581 }
1582 std::fs::read_link("/proc/1/exe").ok().and_then(|p| {
1583 p.file_name()
1584 .and_then(|n| n.to_str())
1585 .map(|s| s.to_string())
1586 })
1587 }
1588 #[cfg(target_os = "macos")]
1589 {
1590 Some("launchd".to_string())
1591 }
1592 #[cfg(target_os = "windows")]
1593 {
1594 Some("SCM".to_string())
1595 }
1596 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1597 {
1598 None
1599 }
1600}
1601
1602fn detect_chassis() -> Option<String> {
1603 #[cfg(target_os = "linux")]
1604 {
1605 let raw = std::fs::read_to_string("/sys/class/dmi/id/chassis_type").ok()?;
1606 let n: u32 = raw.trim().parse().ok()?;
1607 let label = match n {
1608 3 => "Desktop",
1609 4 => "Low-Profile Desktop",
1610 6 => "Mini Tower",
1611 7 => "Tower",
1612 8 | 9 | 10 | 14 | 31 | 32 => "Laptop",
1613 11 => "Handheld",
1614 13 => "All-in-One",
1615 17 => "Main Server",
1616 23 => "Rack Server",
1617 28 => "Blade",
1618 30 => "Tablet",
1619 35 => "Mini PC",
1620 36 => "Stick PC",
1621 _ => return None,
1622 };
1623 Some(label.to_string())
1624 }
1625 #[cfg(target_os = "macos")]
1626 {
1627 let output = std::process::Command::new("sysctl")
1628 .args(["-n", "hw.model"])
1629 .output()
1630 .ok()?;
1631 let model = String::from_utf8(output.stdout).ok()?;
1632 let model = model.trim();
1633 if model.contains("MacBook") {
1634 Some("Laptop".to_string())
1635 } else if model.contains("MacPro") {
1636 Some("Desktop".to_string())
1637 } else if model.contains("Macmini") || model.contains("Mac mini") {
1638 Some("Mini PC".to_string())
1639 } else if model.contains("iMac") {
1640 Some("All-in-One".to_string())
1641 } else {
1642 Some(model.to_string())
1643 }
1644 }
1645 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1646 {
1647 None
1648 }
1649}
1650
1651fn detect_bootmgr() -> Option<String> {
1652 #[cfg(target_os = "linux")]
1653 {
1654 use std::path::Path;
1655 let is_uefi = Path::new("/sys/firmware/efi").exists();
1656 if Path::new("/boot/loader/entries").exists()
1657 || Path::new("/boot/loader/loader.conf").exists()
1658 || Path::new("/efi/loader/loader.conf").exists()
1659 {
1660 return Some("systemd-boot".to_string());
1661 }
1662 if Path::new("/boot/grub2/grub.cfg").exists() || Path::new("/boot/grub2").exists() {
1663 return Some("GRUB 2".to_string());
1664 }
1665 if Path::new("/boot/grub/grub.cfg").exists() || Path::new("/boot/grub").exists() {
1666 return Some("GRUB".to_string());
1667 }
1668 if is_uefi {
1669 Some("UEFI".to_string())
1670 } else {
1671 Some("BIOS".to_string())
1672 }
1673 }
1674 #[cfg(target_os = "macos")]
1675 {
1676 Some("Apple Boot ROM".to_string())
1677 }
1678 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1679 {
1680 None
1681 }
1682}
1683
1684fn detect_login_manager() -> Option<String> {
1691 #[cfg(target_os = "linux")]
1692 {
1693 let target = std::fs::read_link("/etc/systemd/system/display-manager.service").ok()?;
1694 let unit = target.file_name().and_then(|n| n.to_str())?;
1695 login_manager_from_unit(unit)
1696 }
1697 #[cfg(target_os = "macos")]
1698 {
1699 let plist = std::fs::read_to_string(LOGINWINDOW_PLIST).ok()?;
1703 Some(format_login_window(parse_plist_string(
1704 &plist,
1705 "CFBundleShortVersionString",
1706 )))
1707 }
1708 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1709 {
1710 None
1711 }
1712}
1713
1714#[cfg(target_os = "macos")]
1716const LOGINWINDOW_PLIST: &str = "/System/Library/CoreServices/loginwindow.app/Contents/Info.plist";
1717
1718#[cfg(any(target_os = "macos", test))]
1729fn parse_plist_string(xml: &str, key: &str) -> Option<String> {
1730 let needle = format!("<key>{key}</key>");
1731 let rest = xml.split_once(&needle)?.1;
1732 let open = rest.find("<string>")?;
1733 if let Some(next_key) = rest.find("<key>") {
1736 if next_key < open {
1737 return None;
1738 }
1739 }
1740 let after = &rest[open + "<string>".len()..];
1741 let end = after.find("</string>")?;
1742 let value = after[..end].trim();
1743 (!value.is_empty()).then(|| value.to_string())
1744}
1745
1746#[cfg(any(target_os = "macos", test))]
1750fn format_login_window(version: Option<String>) -> String {
1751 match version {
1752 Some(v) => format!("Login Window {v}"),
1753 None => "Login Window".to_string(),
1754 }
1755}
1756
1757#[cfg(target_os = "linux")]
1763fn login_manager_from_unit(unit: &str) -> Option<String> {
1764 let stem = unit.strip_suffix(".service").unwrap_or(unit).trim();
1765 if stem.is_empty() {
1766 return None;
1767 }
1768 let pretty = match stem.to_lowercase().as_str() {
1769 "gdm" | "gdm3" => "GDM",
1770 "sddm" => "SDDM",
1771 "lightdm" => "LightDM",
1772 "lxdm" => "LXDM",
1773 "xdm" => "XDM",
1774 "ly" => "Ly",
1775 "greetd" => "greetd",
1776 "slim" => "SLiM",
1777 "nodm" => "nodm",
1778 "entrance" => "Entrance",
1779 _ => {
1780 let mut chars = stem.chars();
1782 return chars
1783 .next()
1784 .map(|c| c.to_uppercase().collect::<String>() + chars.as_str());
1785 }
1786 };
1787 Some(pretty.to_string())
1788}
1789
1790fn detect_brightness() -> Option<String> {
1797 #[cfg(target_os = "linux")]
1798 {
1799 use std::path::Path;
1800 let dir = Path::new("/sys/class/backlight");
1801 if !dir.exists() {
1802 return None;
1803 }
1804 let mut devices: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
1807 .ok()?
1808 .flatten()
1809 .map(|e| e.path())
1810 .collect();
1811 devices.sort_by_key(|p| {
1812 let name = p
1813 .file_name()
1814 .and_then(|n| n.to_str())
1815 .unwrap_or("")
1816 .to_lowercase();
1817 if name.contains("acpi") || name.contains("video") {
1819 1
1820 } else {
1821 0
1822 }
1823 });
1824 for dev in devices {
1825 let cur = std::fs::read_to_string(dev.join("brightness"))
1826 .ok()
1827 .and_then(|s| s.trim().parse::<u64>().ok());
1828 let max = std::fs::read_to_string(dev.join("max_brightness"))
1829 .ok()
1830 .and_then(|s| s.trim().parse::<u64>().ok());
1831 if let (Some(cur), Some(max)) = (cur, max) {
1832 if let Some(pct) = brightness_percent(cur, max) {
1833 return Some(pct);
1834 }
1835 }
1836 }
1837 None
1838 }
1839 #[cfg(target_os = "macos")]
1840 {
1841 let (value, min, max) = crate::macos_ffi::get_backlight_brightness()?;
1842 let span = max.checked_sub(min)?;
1847 let level = value.checked_sub(min)?;
1848 if span <= 0 || level < 0 {
1849 return None;
1850 }
1851 brightness_percent(level as u64, span as u64)
1852 }
1853 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1854 {
1855 None
1856 }
1857}
1858
1859#[cfg(any(target_os = "linux", target_os = "macos"))]
1868fn brightness_percent(cur: u64, max: u64) -> Option<String> {
1869 if max == 0 {
1870 return None;
1871 }
1872 let pct = (cur as f64 / max as f64 * 100.0).round() as u64;
1873 Some(format!("{}%", pct))
1874}
1875
1876fn detect_power_adapter() -> Option<String> {
1884 #[cfg(target_os = "linux")]
1885 {
1886 use std::path::Path;
1887 let dir = Path::new("/sys/class/power_supply");
1888 if !dir.exists() {
1889 return None;
1890 }
1891 for entry in std::fs::read_dir(dir).ok()?.flatten() {
1892 let path = entry.path();
1893 let supply_type = std::fs::read_to_string(path.join("type"))
1894 .map(|s| s.trim().to_string())
1895 .unwrap_or_default();
1896 if supply_type != "Mains" {
1897 continue;
1898 }
1899 let name = path
1900 .file_name()
1901 .and_then(|n| n.to_str())
1902 .unwrap_or("AC")
1903 .to_string();
1904 let online = std::fs::read_to_string(path.join("online"))
1905 .map(|s| s.trim().to_string())
1906 .unwrap_or_default();
1907 return Some(format_power_adapter(&name, &online));
1908 }
1909 None
1910 }
1911 #[cfg(target_os = "macos")]
1912 {
1913 let watts = crate::macos_ffi::get_power_adapter_watts()?;
1918 Some(format_power_adapter_watts(watts))
1919 }
1920 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1921 {
1922 None
1923 }
1924}
1925
1926#[cfg(any(target_os = "macos", test))]
1933fn format_power_adapter_watts(watts: i64) -> String {
1934 if watts > 0 {
1935 format!("{watts}W (connected)")
1936 } else {
1937 "connected".to_string()
1938 }
1939}
1940
1941#[cfg(target_os = "linux")]
1945fn format_power_adapter(name: &str, online: &str) -> String {
1946 let state = match online.trim() {
1947 "1" => "connected",
1948 "0" => "not connected",
1949 _ => "unknown",
1950 };
1951 format!("{} ({})", name, state)
1952}
1953
1954fn detect_tpm() -> Option<String> {
1962 #[cfg(target_os = "linux")]
1963 {
1964 use std::path::Path;
1965 let dir = Path::new("/sys/class/tpm");
1966 if !dir.exists() {
1967 return None;
1968 }
1969 let mut devices: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
1970 .ok()?
1971 .flatten()
1972 .map(|e| e.path())
1973 .collect();
1974 devices.sort();
1976 for dev in devices {
1977 if let Ok(major) = std::fs::read_to_string(dev.join("tpm_version_major")) {
1978 if let Some(v) = format_tpm_version(major.trim()) {
1979 return Some(v);
1980 }
1981 }
1982 }
1983 None
1984 }
1985 #[cfg(not(target_os = "linux"))]
1986 {
1987 None
1988 }
1989}
1990
1991#[cfg(target_os = "linux")]
1998fn format_tpm_version(major: &str) -> Option<String> {
1999 match major.trim() {
2000 "1" => Some("1.2".to_string()),
2001 "2" => Some("2.0".to_string()),
2002 _ => None,
2003 }
2004}
2005
2006#[cfg(target_os = "windows")]
2011mod win_cpu {
2012 #[repr(C)]
2013 struct FileTime {
2014 low: u32,
2015 high: u32,
2016 }
2017
2018 impl FileTime {
2019 fn ticks(&self) -> u64 {
2020 ((self.high as u64) << 32) | self.low as u64
2021 }
2022 }
2023
2024 extern "system" {
2025 fn GetSystemTimes(idle: *mut FileTime, kernel: *mut FileTime, user: *mut FileTime) -> i32;
2026 }
2027
2028 pub fn sample() -> Option<(u64, u64, u64)> {
2031 let mut idle = FileTime { low: 0, high: 0 };
2032 let mut kernel = FileTime { low: 0, high: 0 };
2033 let mut user = FileTime { low: 0, high: 0 };
2034 let ok = unsafe { GetSystemTimes(&mut idle, &mut kernel, &mut user) };
2036 if ok == 0 {
2037 None
2038 } else {
2039 Some((idle.ticks(), kernel.ticks(), user.ticks()))
2040 }
2041 }
2042
2043 pub fn usage_percent(s0: (u64, u64, u64), s1: (u64, u64, u64)) -> f32 {
2046 let idle = s1.0.saturating_sub(s0.0);
2047 let kernel = s1.1.saturating_sub(s0.1);
2048 let user = s1.2.saturating_sub(s0.2);
2049 let total = kernel + user;
2050 if total == 0 {
2051 0.0
2052 } else {
2053 (100.0 * total.saturating_sub(idle) as f64 / total as f64) as f32
2054 }
2055 }
2056
2057 #[cfg(test)]
2058 mod layout {
2059 use std::mem::size_of;
2060
2061 #[test]
2063 fn filetime_size() {
2064 assert_eq!(size_of::<super::FileTime>(), 8);
2065 }
2066 }
2067}
2068
2069#[cfg(test)]
2070mod tests {
2071
2072 #[test]
2073 fn cpu_refresh_kind_is_none_when_no_cpu_field_is_selected() {
2074 assert!(cpu_refresh_kind(false, false, false).is_none());
2075 }
2076
2077 #[test]
2078 fn cpu_refresh_kind_for_plain_cpu_asks_for_neither_flag() {
2079 let kind = cpu_refresh_kind(true, false, false).expect("cpu selected");
2083 assert!(!kind.frequency(), "plain `cpu` must not request frequency");
2084 assert!(!kind.cpu_usage(), "plain `cpu` must not request cpu usage");
2085 }
2086
2087 #[test]
2088 fn cpu_refresh_kind_asks_for_frequency_only_for_cpu_freq() {
2089 let kind = cpu_refresh_kind(false, true, false).expect("cpu-freq selected");
2090 assert!(
2091 kind.frequency(),
2092 "`cpu-freq` reads Cpu::frequency() and must request it"
2093 );
2094 }
2095
2096 #[test]
2097 fn cpu_refresh_kind_asks_for_usage_only_off_windows() {
2098 let kind = cpu_refresh_kind(false, false, true).expect("cpu-usage selected");
2099 if cfg!(target_os = "windows") {
2100 assert!(!kind.cpu_usage());
2103 } else {
2104 assert!(kind.cpu_usage());
2106 }
2107 assert!(!kind.frequency(), "cpu-usage must not drag in frequency");
2108 }
2109
2110 #[test]
2111 fn load_is_probed_only_when_selected_and_never_on_windows() {
2112 assert!(
2113 !should_probe_load(false),
2114 "an unselected `load` must not be probed on any platform"
2115 );
2116 if cfg!(target_os = "windows") {
2117 assert!(
2118 !should_probe_load(true),
2119 "sysinfo's Windows load average samples every 5 s from a zeroed static, so it can only ever report 0.00 in a process this short-lived"
2120 );
2121 } else {
2122 assert!(should_probe_load(true));
2123 }
2124 }
2125
2126 #[test]
2127 fn shell_and_terminal_each_load_the_process_list_on_their_own() {
2128 assert!(needs_process_list(false, false, true, false), "shell alone");
2132 assert!(
2133 needs_process_list(false, false, false, true),
2134 "terminal alone"
2135 );
2136 assert!(needs_process_list(true, false, false, false), "procs alone");
2137 assert!(needs_process_list(false, true, false, false), "audio alone");
2138 assert!(
2139 !needs_process_list(false, false, false, false),
2140 "no consumer selected: the list must not be loaded"
2141 );
2142 }
2143
2144 use super::*;
2145
2146 #[cfg(target_os = "linux")]
2147 #[test]
2148 fn test_login_manager_from_unit() {
2149 assert_eq!(
2150 login_manager_from_unit("gdm.service").as_deref(),
2151 Some("GDM")
2152 );
2153 assert_eq!(
2154 login_manager_from_unit("gdm3.service").as_deref(),
2155 Some("GDM")
2156 );
2157 assert_eq!(
2158 login_manager_from_unit("sddm.service").as_deref(),
2159 Some("SDDM")
2160 );
2161 assert_eq!(
2162 login_manager_from_unit("lightdm.service").as_deref(),
2163 Some("LightDM")
2164 );
2165 assert_eq!(
2167 login_manager_from_unit("emptty.service").as_deref(),
2168 Some("Emptty")
2169 );
2170 assert_eq!(login_manager_from_unit("ly").as_deref(), Some("Ly"));
2172 assert_eq!(login_manager_from_unit("").as_deref(), None);
2174 assert_eq!(login_manager_from_unit(".service").as_deref(), None);
2175 }
2176
2177 #[cfg(any(target_os = "linux", target_os = "macos"))]
2178 #[test]
2179 fn test_brightness_percent() {
2180 assert_eq!(brightness_percent(50, 100).as_deref(), Some("50%"));
2181 assert_eq!(brightness_percent(100, 100).as_deref(), Some("100%"));
2182 assert_eq!(brightness_percent(0, 100).as_deref(), Some("0%"));
2183 assert_eq!(brightness_percent(133, 255).as_deref(), Some("52%"));
2185 assert_eq!(brightness_percent(10, 0), None);
2187 }
2188
2189 #[test]
2193 fn test_parse_plist_string() {
2194 const PLIST: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
2195<plist version="1.0">
2196<dict>
2197 <key>CFBundleName</key>
2198 <string>loginwindow</string>
2199 <key>CFBundleShortVersionString</key>
2200 <string>9.0</string>
2201 <key>CFBundleVersion</key>
2202 <string>3085.6.3</string>
2203</dict>
2204</plist>"#;
2205 assert_eq!(
2206 parse_plist_string(PLIST, "CFBundleShortVersionString").as_deref(),
2207 Some("9.0")
2208 );
2209 assert_eq!(
2210 parse_plist_string(PLIST, "CFBundleVersion").as_deref(),
2211 Some("3085.6.3")
2212 );
2213 assert_eq!(parse_plist_string(PLIST, "NoSuchKey"), None);
2216 }
2217
2218 #[test]
2223 fn test_parse_plist_string_rejects_a_non_string_value() {
2224 const PLIST: &str = r#"<dict>
2225 <key>Flag</key>
2226 <true/>
2227 <key>Other</key>
2228 <string>unrelated</string>
2229</dict>"#;
2230 assert_eq!(parse_plist_string(PLIST, "Flag"), None);
2231 assert_eq!(
2232 parse_plist_string(PLIST, "Other").as_deref(),
2233 Some("unrelated")
2234 );
2235 }
2236
2237 #[test]
2238 fn test_format_login_window() {
2239 assert_eq!(format_login_window(Some("9.0".into())), "Login Window 9.0");
2240 assert_eq!(format_login_window(None), "Login Window");
2243 }
2244
2245 #[test]
2246 fn test_format_power_adapter_watts() {
2247 assert_eq!(format_power_adapter_watts(96), "96W (connected)");
2248 assert_eq!(format_power_adapter_watts(0), "connected");
2251 assert_eq!(format_power_adapter_watts(-1), "connected");
2252 }
2253
2254 #[cfg(any(target_os = "linux", target_os = "macos"))]
2258 #[test]
2259 fn test_backlight_triple_rebasing() {
2260 assert_eq!(
2262 brightness_percent((32768i64 - 0) as u64, (65536i64 - 0) as u64).as_deref(),
2263 Some("50%")
2264 );
2265 assert_eq!(
2268 brightness_percent((200i64 - 100) as u64, (300i64 - 100) as u64).as_deref(),
2269 Some("50%")
2270 );
2271 }
2272
2273 #[cfg(target_os = "linux")]
2274 #[test]
2275 fn test_format_power_adapter() {
2276 assert_eq!(format_power_adapter("AC", "1"), "AC (connected)");
2277 assert_eq!(format_power_adapter("ADP1", "0"), "ADP1 (not connected)");
2278 assert_eq!(format_power_adapter("AC", ""), "AC (unknown)");
2280 }
2281
2282 #[cfg(target_os = "linux")]
2283 #[test]
2284 fn test_format_tpm_version() {
2285 assert_eq!(format_tpm_version("2").as_deref(), Some("2.0"));
2287 assert_eq!(format_tpm_version("1").as_deref(), Some("1.2"));
2288 assert_eq!(format_tpm_version("2\n").as_deref(), Some("2.0"));
2290 assert_eq!(format_tpm_version("3"), None);
2292 assert_eq!(format_tpm_version(""), None);
2293 assert_eq!(format_tpm_version("garbage"), None);
2294 }
2295
2296 #[cfg(target_os = "windows")]
2297 #[test]
2298 fn test_win_cpu_usage_percent() {
2299 use super::win_cpu::usage_percent;
2300 let u = usage_percent((0, 0, 0), (50, 100, 50));
2303 assert!((u - 66.6667).abs() < 0.01, "got {}", u);
2304
2305 assert_eq!(usage_percent((0, 0, 0), (100, 100, 0)), 0.0);
2307
2308 assert_eq!(usage_percent((0, 0, 0), (0, 100, 100)), 100.0);
2310
2311 assert_eq!(usage_percent((5, 10, 10), (5, 10, 10)), 0.0);
2313 }
2314
2315 #[test]
2321 fn test_format_cpu_cores_no_hyperthreading() {
2322 assert_eq!(format_cpu_cores_plain(4, Some(4)), "4 cores");
2324 }
2325
2326 #[test]
2327 fn test_format_cpu_cores_hyperthreaded() {
2328 assert_eq!(format_cpu_cores_plain(16, Some(8)), "8C / 16T");
2330 }
2331
2332 #[test]
2333 fn test_format_cpu_cores_unknown_physical() {
2334 assert_eq!(format_cpu_cores_plain(8, None), "8 cores");
2336 }
2337
2338 #[test]
2339 fn test_format_cpu_cores_physical_equals_zero() {
2340 let result = format_cpu_cores_plain(8, Some(0));
2344 assert!(result.contains("8"), "should mention 8 threads: {}", result);
2345 }
2346
2347 #[cfg(target_os = "linux")]
2348 #[test]
2349 fn test_detect_cpu_cache_returns_some_on_linux() {
2350 if std::path::Path::new("/sys/devices/system/cpu/cpu0/cache").exists() {
2353 let result = detect_cpu_cache();
2354 assert!(result.is_some(), "expected cache info on Linux with sysfs");
2355 let s = result.unwrap();
2356 assert!(
2357 s.contains("L1") || s.contains("L2") || s.contains("L3"),
2358 "expected cache level labels, got: {}",
2359 s
2360 );
2361 }
2362 }
2363
2364 #[test]
2365 fn test_normalize_desktop_name_known() {
2366 assert_eq!(normalize_desktop_name("gnome"), "GNOME");
2367 assert_eq!(normalize_desktop_name("GNOME"), "GNOME");
2368 assert_eq!(normalize_desktop_name("kde"), "KDE Plasma");
2369 assert_eq!(normalize_desktop_name("plasma"), "KDE Plasma");
2370 assert_eq!(normalize_desktop_name("KDE Plasma"), "KDE Plasma");
2371 assert_eq!(normalize_desktop_name("xfce"), "XFCE");
2372 assert_eq!(normalize_desktop_name("lxqt"), "LXQt");
2373 assert_eq!(normalize_desktop_name("mate"), "MATE");
2374 assert_eq!(normalize_desktop_name("cinnamon"), "Cinnamon");
2375 assert_eq!(normalize_desktop_name("e"), "Enlightenment");
2376 }
2377
2378 #[test]
2379 fn test_normalize_desktop_name_unknown_lowercase() {
2380 assert_eq!(normalize_desktop_name("budgie"), "Budgie");
2382 assert_eq!(normalize_desktop_name("niri"), "Niri");
2383 }
2384
2385 #[test]
2386 fn test_normalize_desktop_name_unknown_mixed() {
2387 assert_eq!(normalize_desktop_name("MyDE"), "MyDE");
2389 }
2390
2391 #[test]
2392 fn test_normalize_desktop_name_trims_whitespace() {
2393 assert_eq!(normalize_desktop_name(" gnome "), "GNOME");
2394 assert_eq!(normalize_desktop_name(" niri "), "Niri");
2395 }
2396
2397 #[cfg(target_os = "linux")]
2398 #[test]
2399 fn test_detect_desktop_from_proc_returns_option() {
2400 let result = detect_desktop_from_proc();
2402 if let Some(ref de) = result {
2403 assert!(!de.is_empty(), "desktop name should not be empty");
2404 }
2405 }
2406
2407 #[cfg(target_os = "linux")]
2408 #[test]
2409 fn test_detect_cpu_freq_range_returns_ordered_pair() {
2410 if std::path::Path::new("/sys/devices/system/cpu/cpufreq").exists() {
2411 if let Some((min, max)) = detect_cpu_freq_range() {
2412 assert!(
2413 min <= max,
2414 "min freq should be <= max freq: {} > {}",
2415 min,
2416 max
2417 );
2418 assert!(min > 0, "min freq should be positive");
2419 }
2420 }
2421 }
2422}