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
249impl SystemInfo {
250 pub fn collect(opts: CollectOptions) -> anyhow::Result<Self> {
255 let should_collect = |field_name: &str| -> bool {
256 match &opts.fields {
257 Some(fields) => {
258 let norm_field = field_name.to_lowercase().replace(['-', '_'], " ");
259 let norm_field_no_spaces = norm_field.replace(' ', "");
260 fields.iter().any(|f| {
261 let norm_f = f.to_lowercase().replace(['-', '_'], " ");
262 norm_f == norm_field || norm_f.replace(' ', "") == norm_field_no_spaces
263 })
264 }
265 None => true,
266 }
267 };
268
269 let mut refresh_kind = sysinfo::RefreshKind::nothing();
270 if let Some(cpu_kind) = cpu_refresh_kind(
273 should_collect("cpu"),
274 should_collect("cpu-freq"),
275 should_collect("cpu-usage"),
276 ) {
277 refresh_kind = refresh_kind.with_cpu(cpu_kind);
278 }
279 if should_collect("memory")
280 || should_collect("swap")
281 || should_collect("phys mem")
282 || should_collect("phys-mem")
283 {
284 refresh_kind = refresh_kind.with_memory(sysinfo::MemoryRefreshKind::everything());
285 }
286 if should_collect("procs") || should_collect("audio") {
287 refresh_kind = refresh_kind.with_processes(sysinfo::ProcessRefreshKind::nothing());
288 }
289
290 #[cfg_attr(target_os = "windows", allow(unused_mut))]
293 let mut sys = System::new_with_specifics(refresh_kind);
294
295 let os = System::long_os_version()
296 .or_else(System::name)
297 .unwrap_or_else(|| "Unknown".to_string());
298
299 let kernel = System::kernel_version();
300 let hostname = System::host_name();
301
302 let cpu = if should_collect("cpu") {
303 sys.cpus()
304 .first()
305 .map(|c| c.brand().to_string())
306 .unwrap_or_else(|| "Unknown CPU".to_string())
307 } else {
308 String::new()
309 };
310
311 let cpu_cores = if should_collect("cpu") {
312 sys.cpus().len()
313 } else {
314 0
315 };
316 let cpu_core_info = if should_collect("cpu") {
317 format_cpu_cores(cpu_cores, System::physical_core_count())
318 } else {
319 String::new()
320 };
321
322 let memory = if should_collect("memory") {
323 let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
324 let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
325 format!("{:.1} / {:.1} GB", used_mem, total_mem)
326 } else {
327 String::new()
328 };
329
330 let swap = if should_collect("swap") {
331 let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
332 let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
333 if total_swap > 0.0 {
334 format!("{:.1} / {:.1} GB", used_swap, total_swap)
335 } else {
336 "No swap".to_string()
337 }
338 } else {
339 String::new()
340 };
341
342 let uptime = format!("{}s", System::uptime());
343
344 let disks: Vec<String> = if should_collect("disk") {
345 let disks_list = crate::disk::detect_logical_disks(opts.full);
346 let format_disk = |(mount, total, avail, fs): &(String, u64, u64, String)| {
347 let total_gb = *total as f64 / 1024.0 / 1024.0 / 1024.0;
348 let avail_gb = *avail as f64 / 1024.0 / 1024.0 / 1024.0;
349 format!(
350 "{} ({}): {:.1} GB free / {:.1} GB",
351 mount, fs, avail_gb, total_gb
352 )
353 };
354 if !opts.long {
355 let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
356 let home_path = std::path::Path::new(&home);
357 let best = disks_list
358 .iter()
359 .filter(|(mp, ..)| home_path.starts_with(mp))
360 .max_by_key(|(mp, ..)| std::path::Path::new(mp).components().count());
361 if let Some(disk) = best {
362 vec![format_disk(disk)]
363 } else {
364 disks_list.iter().map(format_disk).collect()
365 }
366 } else {
367 disks_list.iter().map(format_disk).collect()
368 }
369 } else {
370 Vec::new()
371 };
372
373 let battery = if should_collect("battery") {
374 crate::battery::get_battery_info().map(|bat| {
375 let pct = bat.percentage;
376 let state = match bat.state {
377 crate::battery::BatteryState::Charging => "charging",
378 crate::battery::BatteryState::Discharging => "discharging",
379 crate::battery::BatteryState::Full => "full",
380 _ => "not charging",
381 };
382 let vendor = bat.vendor;
383 let model = bat.model;
384
385 let time_str = match bat.state {
387 crate::battery::BatteryState::Charging => bat.time_remaining.map(|d| {
388 let total_mins = d.as_secs() / 60;
389 let hours = total_mins / 60;
390 let mins = total_mins % 60;
391 if hours >= 24 {
392 let days = hours / 24;
393 let rem_hours = hours % 24;
394 format!("{}d {}h until full", days, rem_hours)
395 } else if hours > 0 {
396 format!("{}h {}m until full", hours, mins)
397 } else {
398 format!("{}m until full", mins)
399 }
400 }),
401 crate::battery::BatteryState::Discharging => bat.time_remaining.map(|d| {
402 let total_mins = d.as_secs() / 60;
403 let hours = total_mins / 60;
404 let mins = total_mins % 60;
405 if hours >= 24 {
406 let days = hours / 24;
407 let rem_hours = hours % 24;
408 format!("{}d {}h remaining", days, rem_hours)
409 } else if hours > 0 {
410 format!("{}h {}m remaining", hours, mins)
411 } else {
412 format!("{}m remaining", mins)
413 }
414 }),
415 _ => None,
416 };
417
418 let mut parts = vec![state.to_string()];
419 if let Some(t) = time_str {
420 parts.insert(0, t);
421 }
422 if let Some(health) = bat.health {
423 if health < 99.0 {
424 parts.push(format!("{:.0}% health", health));
425 }
426 }
427
428 let base = format!("{:.0}% ({})", pct, parts.join(", "));
429
430 match (vendor, model) {
431 (Some(v), Some(m)) => format!("{} [{} {}]", base, v, m),
432 (Some(v), None) => format!("{} [{}]", base, v),
433 _ => base,
434 }
435 })
436 } else {
437 None
438 };
439
440 let arch = System::cpu_arch();
441
442 let processes = if should_collect("procs") || should_collect("audio") {
443 sys.processes().len()
444 } else {
445 0
446 };
447
448 let load_avg = if should_probe_load(should_collect("load")) {
449 let avg = System::load_average();
450 if avg.one > 0.0 || avg.five > 0.0 {
451 Some(format!(
452 "{:.2}, {:.2}, {:.2}",
453 avg.one, avg.five, avg.fifteen
454 ))
455 } else {
456 None
457 }
458 } else {
459 None
460 };
461
462 #[cfg(target_os = "windows")]
466 let cpu_sample0 = win_cpu::sample();
467 #[cfg(target_os = "windows")]
468 let cpu_t0 = std::time::Instant::now();
469
470 let want_disk_io = should_collect("disk-io") || should_collect("disk io");
476 let want_net_io = should_collect("net-io") || should_collect("net io");
477 let disk_io_sample0 = if want_disk_io {
478 crate::io::sample_disk_io()
479 } else {
480 Vec::new()
481 };
482 let net_io_sample0 = if want_net_io {
483 crate::io::sample_net_io()
484 } else {
485 Vec::new()
486 };
487 let io_t0 = std::time::Instant::now();
488
489 let (
491 gpu,
492 packages,
493 public_ip,
494 (local_ip, active_interface),
495 motherboard,
496 bios,
497 displays,
498 audio,
499 wifi,
500 bluetooth,
501 (ui_theme, icons, cursor, font),
502 camera,
503 gamepad,
504 physical_disks,
505 physical_memory,
506 weather,
507 btrfs,
508 zpool,
509 (media, player),
510 gpu_apis,
511 ) = std::thread::scope(|s| {
512 let gpu_handle = if should_collect("gpu") {
513 Some(s.spawn(|| {
514 gpu::detect_gpus()
515 .into_iter()
516 .map(|g| g.format())
517 .collect::<Vec<String>>()
518 }))
519 } else {
520 None
521 };
522 let packages_handle = if should_collect("packages") {
523 Some(s.spawn(crate::packages::detect_packages))
524 } else {
525 None
526 };
527 let public_ip_handle = if should_collect("public ip") {
528 Some(s.spawn(crate::network::detect_public_ip))
529 } else {
530 None
531 };
532 let network_ips_handle = if should_collect("net") {
533 Some(s.spawn(crate::network::detect_active_interface_and_local_ip))
534 } else {
535 None
536 };
537 let motherboard_handle = if should_collect("motherboard") {
538 Some(s.spawn(crate::motherboard::detect_motherboard))
539 } else {
540 None
541 };
542 let bios_handle = if should_collect("bios") {
543 Some(s.spawn(crate::bios::detect_bios))
544 } else {
545 None
546 };
547 let displays_handle = if should_collect("display") {
548 Some(s.spawn(crate::display::detect_displays))
549 } else {
550 None
551 };
552 let audio_handle = if should_collect("audio") {
553 Some(s.spawn(|| crate::audio::detect_audio(&sys)))
554 } else {
555 None
556 };
557 let wifi_handle = if should_collect("wifi") {
558 Some(s.spawn(crate::network::detect_wifi))
559 } else {
560 None
561 };
562 let bluetooth_handle = if should_collect("bluetooth") {
563 Some(s.spawn(crate::bluetooth::detect_bluetooth))
564 } else {
565 None
566 };
567 let ui_theme_and_fonts_handle = if should_collect("theme")
568 || should_collect("icons")
569 || should_collect("cursor")
570 || should_collect("font")
571 {
572 Some(s.spawn(crate::theme::detect_ui_theme_and_fonts))
573 } else {
574 None
575 };
576 let camera_handle = if should_collect("camera") {
577 Some(s.spawn(crate::camera::detect_camera))
578 } else {
579 None
580 };
581 let gamepad_handle = if should_collect("gamepad") {
582 Some(s.spawn(crate::gamepad::detect_gamepad))
583 } else {
584 None
585 };
586 let physical_disks_handle = if should_collect("phys disk") {
587 Some(s.spawn(crate::disk::detect_physical_disks))
588 } else {
589 None
590 };
591 let physical_memory_handle = if should_collect("phys mem") {
592 Some(s.spawn(crate::memory::detect_physical_memory))
593 } else {
594 None
595 };
596 let weather_location = opts.weather_location.clone();
597 let weather_unit = opts.weather_unit;
598 let weather_handle = if should_collect("weather") {
599 Some(s.spawn(move || {
600 crate::weather::detect_weather(weather_location.as_deref(), weather_unit)
601 }))
602 } else {
603 None
604 };
605 let btrfs_handle = if should_collect("btrfs") {
606 Some(s.spawn(crate::btrfs::detect_btrfs))
607 } else {
608 None
609 };
610 let zpool_handle = if should_collect("zpool") {
611 Some(s.spawn(crate::zfs::detect_zpool))
612 } else {
613 None
614 };
615 let media_handle = if should_collect("media") || should_collect("player") {
616 Some(s.spawn(crate::media::detect_media))
617 } else {
618 None
619 };
620 let gpu_apis_handle =
624 if should_collect("vulkan") || should_collect("opengl") || should_collect("opencl")
625 {
626 Some(s.spawn(crate::gpu_api::detect_gpu_apis))
627 } else {
628 None
629 };
630
631 (
632 gpu_handle
633 .map(|h| h.join().unwrap_or_default())
634 .unwrap_or_default(),
635 packages_handle.and_then(|h| h.join().ok().flatten()),
636 public_ip_handle.and_then(|h| h.join().ok().flatten()),
637 network_ips_handle
638 .map(|h| h.join().unwrap_or((None, None)))
639 .unwrap_or((None, None)),
640 motherboard_handle.and_then(|h| h.join().ok().flatten()),
641 bios_handle.and_then(|h| h.join().ok().flatten()),
642 displays_handle
643 .map(|h| h.join().unwrap_or_default())
644 .unwrap_or_default(),
645 audio_handle.and_then(|h| h.join().ok().flatten()),
646 wifi_handle.and_then(|h| h.join().ok().flatten()),
647 bluetooth_handle.and_then(|h| h.join().ok().flatten()),
648 ui_theme_and_fonts_handle
649 .map(|h| h.join().unwrap_or((None, None, None, None)))
650 .unwrap_or((None, None, None, None)),
651 camera_handle
652 .map(|h| h.join().unwrap_or_default())
653 .unwrap_or_default(),
654 gamepad_handle
655 .map(|h| h.join().unwrap_or_default())
656 .unwrap_or_default(),
657 physical_disks_handle
658 .map(|h| h.join().unwrap_or_default())
659 .unwrap_or_default(),
660 physical_memory_handle.and_then(|h| h.join().ok().flatten()),
661 weather_handle.and_then(|h| h.join().ok().flatten()),
662 btrfs_handle
663 .map(|h| h.join().unwrap_or_default())
664 .unwrap_or_default(),
665 zpool_handle
666 .map(|h| h.join().unwrap_or_default())
667 .unwrap_or_default(),
668 media_handle
669 .map(|h| h.join().unwrap_or((None, None)))
670 .unwrap_or((None, None)),
671 gpu_apis_handle
672 .map(|h| h.join().unwrap_or_default())
673 .unwrap_or_default(),
674 )
675 });
676
677 let mut temps: Vec<String> = if should_collect("temp") {
678 Components::new_with_refreshed_list()
679 .iter()
680 .filter_map(|c| {
681 c.temperature().and_then(|t| {
682 if t > 0.0 {
683 Some(format!("{}: {:.0}°C", c.label(), t))
684 } else {
685 None
686 }
687 })
688 })
689 .collect()
690 } else {
691 Vec::new()
692 };
693
694 temps.sort_by(|a, b| {
696 let a_cpu = a.to_lowercase().contains("cpu") || a.to_lowercase().contains("core");
697 let b_cpu = b.to_lowercase().contains("cpu") || b.to_lowercase().contains("core");
698 b_cpu.cmp(&a_cpu)
699 });
700
701 let networks = if should_collect("net") {
702 crate::network::detect_networks(active_interface.as_deref(), local_ip.as_deref())
703 } else {
704 Vec::new()
705 };
706
707 let boot_timestamp = System::boot_time();
708 let boot_dt = chrono::Local
709 .timestamp_opt(boot_timestamp as i64, 0)
710 .single()
711 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
712 .unwrap_or_else(|| boot_timestamp.to_string());
713 let boot_time = boot_dt;
714
715 let shell = if should_collect("shell") {
717 crate::shell::detect_shell(&sys)
718 } else {
719 None
720 };
721 let terminal = if should_collect("terminal") {
722 crate::terminal::detect_terminal(&sys)
723 } else {
724 None
725 };
726 let terminal_font = if should_collect("terminal font")
727 || should_collect("terminal-font")
728 || should_collect("terminal_font")
729 {
730 crate::terminal::detect_terminal_font(terminal.as_deref())
731 } else {
732 None
733 };
734 let desktop = if should_collect("desktop") {
735 std::env::var("XDG_CURRENT_DESKTOP")
736 .or_else(|_| std::env::var("DESKTOP_SESSION"))
737 .or_else(|_| std::env::var("XDG_SESSION_DESKTOP"))
738 .or_else(|_| std::env::var("GDMSESSION"))
739 .ok()
740 .map(|s| normalize_desktop_name(&s))
741 .filter(|s| !s.is_empty())
742 .or_else(detect_desktop_from_proc)
743 } else {
744 None
745 };
746
747 let cpu_freq = if should_collect("cpu-freq")
749 || should_collect("cpu freq")
750 || should_collect("cpu_freq")
751 {
752 sys.cpus().first().map(|c| {
753 let current = format!("{:.2} GHz", c.frequency() as f64 / 1000.0);
754 if let Some((min_khz, max_khz)) = detect_cpu_freq_range() {
755 let min_ghz = min_khz as f64 / 1_000_000.0;
756 let max_ghz = max_khz as f64 / 1_000_000.0;
757 format!("{} ({:.2} \u{2013} {:.2} GHz)", current, min_ghz, max_ghz)
758 } else {
759 current
760 }
761 })
762 } else {
763 None
764 };
765
766 let cpu_cache = if should_collect("cpu-cache")
768 || should_collect("cpu cache")
769 || should_collect("cpu_cache")
770 {
771 detect_cpu_cache()
772 } else {
773 None
774 };
775
776 let cpu_usage = if should_collect("cpu-usage")
781 || should_collect("cpu usage")
782 || should_collect("cpu_usage")
783 {
784 #[cfg(not(target_os = "windows"))]
785 {
786 std::thread::sleep(std::time::Duration::from_millis(200));
787 sys.refresh_cpu_usage();
788 let usage: f32 =
789 sys.cpus().iter().map(|c| c.cpu_usage()).sum::<f32>() / sys.cpus().len() as f32;
790 let avg = System::load_average();
791 let load_str = format!("{:.2}, {:.2}, {:.2}", avg.one, avg.five, avg.fifteen);
792 if usage > 0.0 {
793 Some(format!("{:.1}% (load: {})", usage, load_str))
794 } else if avg.one > 0.0 {
795 Some(format!("load: {}", load_str))
796 } else {
797 None
798 }
799 }
800 #[cfg(target_os = "windows")]
801 {
802 let floor = std::time::Duration::from_millis(100);
806 let elapsed = cpu_t0.elapsed();
807 if elapsed < floor {
808 std::thread::sleep(floor - elapsed);
809 }
810 match (cpu_sample0, win_cpu::sample()) {
811 (Some(s0), Some(s1)) => {
812 let usage = win_cpu::usage_percent(s0, s1);
813 if usage > 0.0 {
814 Some(format!("{:.1}%", usage))
815 } else {
816 None
817 }
818 }
819 _ => None,
820 }
821 }
822 } else {
823 None
824 };
825
826 let (disk_io, net_io) = if want_disk_io || want_net_io {
832 let floor = std::time::Duration::from_millis(100);
833 let elapsed = io_t0.elapsed();
834 if elapsed < floor {
835 std::thread::sleep(floor - elapsed);
836 }
837 let elapsed_secs = io_t0.elapsed().as_secs_f64();
838 let disk_io = if want_disk_io {
839 crate::io::compute_rates(
840 &disk_io_sample0,
841 &crate::io::sample_disk_io(),
842 elapsed_secs,
843 )
844 .iter()
845 .map(|r| crate::io::format_io_line(r, "R", "W"))
846 .collect()
847 } else {
848 Vec::new()
849 };
850 let net_io = if want_net_io {
851 let rates = crate::io::compute_rates(
852 &net_io_sample0,
853 &crate::io::sample_net_io(),
854 elapsed_secs,
855 );
856 crate::io::select_net_rates(rates, active_interface.as_deref())
857 .iter()
858 .map(|r| crate::io::format_io_line(r, "RX", "TX"))
859 .collect()
860 } else {
861 Vec::new()
862 };
863 (disk_io, net_io)
864 } else {
865 (Vec::new(), Vec::new())
866 };
867
868 let init_system = if should_collect("init") || should_collect("init system") {
869 detect_init_system()
870 } else {
871 None
872 };
873
874 let chassis = if should_collect("chassis") {
875 detect_chassis()
876 } else {
877 None
878 };
879
880 let locale = if should_collect("locale") {
881 std::env::var("LC_ALL")
882 .ok()
883 .filter(|s| !s.is_empty())
884 .or_else(|| std::env::var("LC_MESSAGES").ok().filter(|s| !s.is_empty()))
885 .or_else(|| std::env::var("LANG").ok().filter(|s| !s.is_empty()))
886 } else {
887 None
888 };
889
890 let bootmgr = if should_collect("bootmgr") || should_collect("boot") {
891 detect_bootmgr()
892 } else {
893 None
894 };
895
896 let login_manager = if should_collect("login-manager") || should_collect("lm") {
897 detect_login_manager()
898 } else {
899 None
900 };
901
902 let brightness = if should_collect("brightness") {
903 detect_brightness()
904 } else {
905 None
906 };
907
908 let power_adapter = if should_collect("power-adapter") {
909 detect_power_adapter()
910 } else {
911 None
912 };
913
914 let (keyboard, mouse) = if should_collect("keyboard") || should_collect("mouse") {
917 let (kbds, mice) = crate::input::detect_input_devices();
918 (
919 if should_collect("keyboard") {
920 kbds
921 } else {
922 Vec::new()
923 },
924 if should_collect("mouse") {
925 mice
926 } else {
927 Vec::new()
928 },
929 )
930 } else {
931 (Vec::new(), Vec::new())
932 };
933
934 let tpm = if should_collect("tpm") {
935 detect_tpm()
936 } else {
937 None
938 };
939
940 let editor = if should_collect("editor") {
941 std::env::var("VISUAL")
942 .ok()
943 .filter(|s| !s.is_empty())
944 .or_else(|| std::env::var("EDITOR").ok().filter(|s| !s.is_empty()))
945 } else {
946 None
947 };
948
949 let wm = if should_collect("wm") || should_collect("window manager") {
950 crate::wm::detect_wm()
951 } else {
952 None
953 };
954
955 let dns = if should_collect("dns") {
956 crate::network::detect_dns()
957 } else {
958 Vec::new()
959 };
960
961 let domain = if should_collect("domain") {
962 crate::network::detect_domain()
963 } else {
964 None
965 };
966
967 let domain_search = if should_collect("domain-search") || should_collect("domain search") {
968 crate::network::detect_domain_search()
969 } else {
970 Vec::new()
971 };
972
973 let terminal_size = if should_collect("terminal size")
974 || should_collect("terminal-size")
975 || should_collect("terminal_size")
976 {
977 crate::terminal::detect_terminal_size()
978 } else {
979 None
980 };
981
982 let current_user = std::env::var("USER").ok();
984
985 let users = if should_collect("users") {
990 #[cfg(target_os = "windows")]
991 {
992 crate::win_users::active_user_session_count()
993 }
994 #[cfg(not(target_os = "windows"))]
995 {
996 Users::new_with_refreshed_list()
997 .iter()
998 .filter(|user| {
999 user.id()
1001 .to_string()
1002 .parse::<u32>()
1003 .map(|uid| uid >= 1000)
1004 .unwrap_or(false)
1005 })
1006 .count()
1007 }
1008 } else {
1009 0
1010 };
1011
1012 let wm_theme = if should_collect("wm-theme")
1013 || should_collect("wm theme")
1014 || should_collect("wm_theme")
1015 {
1016 crate::theme::detect_wm_theme(wm.as_deref(), desktop.as_deref())
1017 } else {
1018 None
1019 };
1020
1021 let wallpaper = if should_collect("wallpaper") {
1022 crate::theme::detect_wallpaper(desktop.as_deref(), wm.as_deref())
1023 } else {
1024 None
1025 };
1026
1027 let terminal_theme = if should_collect("terminal-theme")
1028 || should_collect("terminal theme")
1029 || should_collect("terminal_theme")
1030 {
1031 crate::terminal::detect_terminal_theme(terminal.as_deref())
1032 } else {
1033 None
1034 };
1035
1036 Ok(Self {
1037 os,
1038 kernel,
1039 hostname,
1040 arch,
1041 cpu,
1042 cpu_cores,
1043 cpu_core_info,
1044 memory,
1045 swap,
1046 uptime,
1047 processes,
1048 load_avg,
1049 disks,
1050 temps,
1051 networks,
1052 boot_time,
1053 battery,
1054 shell,
1055 terminal,
1056 desktop,
1057 cpu_freq,
1058 users,
1059 gpu,
1060 packages,
1061 current_user,
1062 local_ip,
1063 public_ip,
1064 active_interface,
1065 motherboard,
1066 bios,
1067 displays,
1068 audio,
1069 wifi,
1070 bluetooth,
1071 ui_theme,
1072 icons,
1073 cursor,
1074 font,
1075 terminal_font,
1076 camera,
1077 gamepad,
1078 cpu_cache,
1079 cpu_usage,
1080 physical_disks,
1081 vulkan: gpu_apis.vulkan,
1082 opengl: gpu_apis.opengl,
1083 opencl: gpu_apis.opencl,
1084 disk_io,
1085 net_io,
1086 physical_memory,
1087 init_system,
1088 chassis,
1089 locale,
1090 bootmgr,
1091 editor,
1092 weather,
1093 wm,
1094 dns,
1095 domain,
1096 domain_search,
1097 terminal_size,
1098 btrfs,
1099 zpool,
1100 login_manager,
1101 brightness,
1102 power_adapter,
1103 keyboard,
1104 mouse,
1105 tpm,
1106 media,
1107 player,
1108 wm_theme,
1109 wallpaper,
1110 terminal_theme,
1111 })
1112 }
1113}
1114
1115pub fn detect_cpu_cache() -> Option<String> {
1121 #[cfg(target_os = "linux")]
1122 {
1123 use std::fs;
1124 let cache_dir = std::path::Path::new("/sys/devices/system/cpu/cpu0/cache");
1125 if !cache_dir.exists() {
1126 return None;
1127 }
1128
1129 struct CacheEntry {
1130 level: u32,
1131 kind: String,
1132 size_kb: u64,
1133 }
1134
1135 let mut entries: Vec<CacheEntry> = Vec::new();
1136
1137 let Ok(indices) = fs::read_dir(cache_dir) else {
1138 return None;
1139 };
1140
1141 for entry in indices.flatten() {
1142 let path = entry.path();
1143 if !path.is_dir() {
1145 continue;
1146 }
1147 let level_str = match fs::read_to_string(path.join("level")) {
1148 Ok(s) => s,
1149 Err(_) => continue,
1150 };
1151 let level: u32 = match level_str.trim().parse() {
1152 Ok(n) => n,
1153 Err(_) => continue,
1154 };
1155 let kind = match fs::read_to_string(path.join("type")) {
1156 Ok(s) => s.trim().to_string(),
1157 Err(_) => continue,
1158 };
1159 let size_str = match fs::read_to_string(path.join("size")) {
1160 Ok(s) => s,
1161 Err(_) => continue,
1162 };
1163 let size_raw = size_str.trim();
1164 let size_kb: u64 = if let Some(k) = size_raw.strip_suffix('K') {
1165 match k.parse() {
1166 Ok(n) => n,
1167 Err(_) => continue,
1168 }
1169 } else if let Some(m) = size_raw.strip_suffix('M') {
1170 match m.parse::<u64>() {
1171 Ok(n) => n * 1024,
1172 Err(_) => continue,
1173 }
1174 } else {
1175 match size_raw.parse() {
1176 Ok(n) => n,
1177 Err(_) => continue,
1178 }
1179 };
1180
1181 if kind != "Instruction" && kind != "Data" && kind != "Unified" {
1182 continue;
1183 }
1184
1185 entries.push(CacheEntry {
1186 level,
1187 kind,
1188 size_kb,
1189 });
1190 }
1191
1192 if entries.is_empty() {
1193 return None;
1194 }
1195
1196 entries.sort_by_key(|e| (e.level, e.kind.clone()));
1197
1198 let fmt_size = |kb: u64| -> String {
1199 if kb >= 1024 && kb.is_multiple_of(1024) {
1200 format!("{}M", kb / 1024)
1201 } else if kb >= 1024 {
1202 format!("{:.2}M", kb as f64 / 1024.0)
1203 .trim_end_matches('0')
1204 .trim_end_matches('.')
1205 .to_string()
1206 + "M"
1207 } else {
1208 format!("{}K", kb)
1209 }
1210 };
1211
1212 let mut seen = std::collections::HashSet::new();
1214 let mut parts: Vec<String> = Vec::new();
1215 for e in &entries {
1216 let label = match (e.level, e.kind.as_str()) {
1217 (1, "Data") => "L1d".to_string(),
1218 (1, "Instruction") => "L1i".to_string(),
1219 (1, "Unified") => "L1".to_string(),
1220 (n, _) => format!("L{}", n),
1221 };
1222 if seen.insert(label.clone()) {
1223 parts.push(format!("{}: {}", label, fmt_size(e.size_kb)));
1224 }
1225 }
1226
1227 if parts.is_empty() {
1228 None
1229 } else {
1230 Some(parts.join(", "))
1231 }
1232 }
1233 #[cfg(target_os = "macos")]
1234 {
1235 extern "C" {
1236 fn sysctlbyname(
1237 name: *const i8,
1238 oldp: *mut std::ffi::c_void,
1239 oldlenp: *mut usize,
1240 newp: *mut std::ffi::c_void,
1241 newlen: usize,
1242 ) -> i32;
1243 }
1244
1245 let read_u64 = |key: &str| -> Option<u64> {
1246 let name = std::ffi::CString::new(key).ok()?;
1247 let mut value: u64 = 0;
1248 let mut size = std::mem::size_of::<u64>();
1249 let ret = unsafe {
1250 sysctlbyname(
1251 name.as_ptr(),
1252 &mut value as *mut u64 as *mut std::ffi::c_void,
1253 &mut size,
1254 std::ptr::null_mut(),
1255 0,
1256 )
1257 };
1258 if ret == 0 && value > 0 {
1259 Some(value)
1260 } else {
1261 None
1262 }
1263 };
1264
1265 let fmt_bytes = |bytes: u64| -> String {
1266 if bytes >= 1024 * 1024 {
1267 format!("{}M", bytes / (1024 * 1024))
1268 } else {
1269 format!("{}K", bytes / 1024)
1270 }
1271 };
1272
1273 let mut parts = Vec::new();
1274 if let Some(v) = read_u64("hw.l1dcachesize") {
1275 parts.push(format!("L1d: {}", fmt_bytes(v)));
1276 }
1277 if let Some(v) = read_u64("hw.l1icachesize") {
1278 parts.push(format!("L1i: {}", fmt_bytes(v)));
1279 }
1280 if let Some(v) = read_u64("hw.l2cachesize") {
1281 parts.push(format!("L2: {}", fmt_bytes(v)));
1282 }
1283 if let Some(v) = read_u64("hw.l3cachesize") {
1284 parts.push(format!("L3: {}", fmt_bytes(v)));
1285 }
1286
1287 if parts.is_empty() {
1288 None
1289 } else {
1290 Some(parts.join(", "))
1291 }
1292 }
1293 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1294 {
1295 None
1296 }
1297}
1298
1299pub fn format_cpu_cores(logical: usize, physical: Option<usize>) -> String {
1304 #[cfg(target_os = "linux")]
1306 if let Some(hybrid) = detect_hybrid_cores(logical) {
1307 return hybrid;
1308 }
1309
1310 #[cfg(target_os = "macos")]
1312 if let Some(hybrid) = detect_macos_hybrid_cores(logical) {
1313 return hybrid;
1314 }
1315
1316 format_cpu_cores_plain(logical, physical)
1317}
1318
1319fn format_cpu_cores_plain(logical: usize, physical: Option<usize>) -> String {
1329 match physical {
1330 Some(p) if p < logical => format!("{}C / {}T", p, logical),
1331 _ => format!("{} cores", logical),
1332 }
1333}
1334
1335#[cfg(target_os = "linux")]
1338fn detect_hybrid_cores(logical: usize) -> Option<String> {
1339 use std::collections::HashMap;
1340 use std::fs;
1341
1342 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
1343 if !cpufreq.exists() {
1344 return None;
1345 }
1346
1347 let mut freq_to_count: HashMap<u64, usize> = HashMap::new();
1349 let mut total_accounted = 0usize;
1350
1351 let Ok(policies) = fs::read_dir(cpufreq) else {
1352 return None;
1353 };
1354
1355 for policy in policies.flatten() {
1356 let path = policy.path();
1357 if !path.is_dir() {
1358 continue;
1359 }
1360 let max_freq_str = fs::read_to_string(path.join("cpuinfo_max_freq")).ok()?;
1361 let max_freq: u64 = max_freq_str.trim().parse().ok()?;
1362 let affected = fs::read_to_string(path.join("affected_cpus")).ok()?;
1363 let count = affected.split_whitespace().count();
1364 *freq_to_count.entry(max_freq).or_insert(0) += count;
1365 total_accounted += count;
1366 }
1367
1368 if freq_to_count.len() != 2 || total_accounted != logical {
1370 return None;
1371 }
1372
1373 let mut tiers: Vec<(u64, usize)> = freq_to_count.into_iter().collect();
1374 tiers.sort_by_key(|t| std::cmp::Reverse(t.0)); let (_, p_count) = tiers[0];
1376 let (_, e_count) = tiers[1];
1377
1378 Some(format!("{}P + {}E / {}T", p_count, e_count, logical))
1379}
1380
1381#[cfg(target_os = "macos")]
1384fn detect_macos_hybrid_cores(logical: usize) -> Option<String> {
1385 extern "C" {
1386 fn sysctlbyname(
1387 name: *const i8,
1388 oldp: *mut std::ffi::c_void,
1389 oldlenp: *mut usize,
1390 newp: *mut std::ffi::c_void,
1391 newlen: usize,
1392 ) -> i32;
1393 }
1394
1395 let read_u32 = |key: &str| -> Option<u32> {
1396 let name = std::ffi::CString::new(key).ok()?;
1397 let mut value: u32 = 0;
1398 let mut size = std::mem::size_of::<u32>();
1399 let ret = unsafe {
1400 sysctlbyname(
1401 name.as_ptr(),
1402 &mut value as *mut u32 as *mut std::ffi::c_void,
1403 &mut size,
1404 std::ptr::null_mut(),
1405 0,
1406 )
1407 };
1408 if ret == 0 {
1409 Some(value)
1410 } else {
1411 None
1412 }
1413 };
1414
1415 let nlevels = read_u32("hw.nperflevels")?;
1417 if nlevels != 2 {
1418 return None;
1419 }
1420
1421 let p_cores = read_u32("hw.perflevel0.logicalcpu")? as usize;
1422 let e_cores = read_u32("hw.perflevel1.logicalcpu")? as usize;
1423
1424 if p_cores + e_cores != logical {
1425 return None;
1426 }
1427
1428 Some(format!("{}P + {}E / {}T", p_cores, e_cores, logical))
1429}
1430
1431pub fn detect_cpu_freq_range() -> Option<(u64, u64)> {
1434 #[cfg(target_os = "linux")]
1435 {
1436 use std::fs;
1437 let cpufreq = std::path::Path::new("/sys/devices/system/cpu/cpufreq");
1438 if !cpufreq.exists() {
1439 return None;
1440 }
1441 let mut global_min: Option<u64> = None;
1442 let mut global_max: Option<u64> = None;
1443 let Ok(policies) = fs::read_dir(cpufreq) else {
1444 return None;
1445 };
1446 for policy in policies.flatten() {
1447 let path = policy.path();
1448 if !path.is_dir() {
1449 continue;
1450 }
1451 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_min_freq")) {
1452 if let Ok(v) = s.trim().parse::<u64>() {
1453 global_min = Some(global_min.map_or(v, |m: u64| m.min(v)));
1454 }
1455 }
1456 if let Ok(s) = fs::read_to_string(path.join("cpuinfo_max_freq")) {
1457 if let Ok(v) = s.trim().parse::<u64>() {
1458 global_max = Some(global_max.map_or(v, |m: u64| m.max(v)));
1459 }
1460 }
1461 }
1462 match (global_min, global_max) {
1463 (Some(min), Some(max)) => Some((min, max)),
1464 _ => None,
1465 }
1466 }
1467 #[cfg(not(target_os = "linux"))]
1468 {
1469 None
1470 }
1471}
1472
1473#[cfg(not(target_os = "linux"))]
1474fn detect_desktop_from_proc() -> Option<String> {
1475 None
1476}
1477
1478#[cfg(target_os = "linux")]
1479fn detect_desktop_from_proc() -> Option<String> {
1480 const DE_PROCS: &[(&str, &str)] = &[
1481 ("gnome-shell", "GNOME"),
1482 ("plasmashell", "KDE Plasma"),
1483 ("xfce4-session", "XFCE"),
1484 ("mate-session", "MATE"),
1485 ("cinnamon", "Cinnamon"),
1486 ("budgie-daemon", "Budgie"),
1487 ("budgie-panel", "Budgie"),
1488 ("lxsession", "LXDE"),
1489 ("lxqt-session", "LXQt"),
1490 ("deepin-session", "Deepin"),
1491 ("dde-session-daemon", "Deepin"),
1492 ("gala", "Pantheon"),
1493 ("enlightenment", "Enlightenment"),
1494 ];
1495 let Ok(entries) = std::fs::read_dir("/proc") else {
1496 return None;
1497 };
1498 for entry in entries.filter_map(|e| e.ok()) {
1499 let path = entry.path();
1500 if !path.is_dir() {
1501 continue;
1502 }
1503 let Ok(comm) = std::fs::read_to_string(path.join("comm")) else {
1504 continue;
1505 };
1506 let comm = comm.trim().to_lowercase();
1507 for (proc_name, de_name) in DE_PROCS {
1508 if comm == *proc_name || comm.starts_with(proc_name) {
1509 return Some(de_name.to_string());
1510 }
1511 }
1512 }
1513 None
1514}
1515
1516fn normalize_desktop_name(raw: &str) -> String {
1517 let s = raw.trim();
1518 match s.to_lowercase().as_str() {
1520 "gnome" => "GNOME".to_string(),
1521 "kde" | "kde plasma" | "plasma" => "KDE Plasma".to_string(),
1522 "xfce" => "XFCE".to_string(),
1523 "lxde" => "LXDE".to_string(),
1524 "lxqt" => "LXQt".to_string(),
1525 "mate" => "MATE".to_string(),
1526 "cinnamon" => "Cinnamon".to_string(),
1527 "budgie" => "Budgie".to_string(),
1528 "deepin" => "Deepin".to_string(),
1529 "pantheon" => "Pantheon".to_string(),
1530 "unity" => "Unity".to_string(),
1531 "enlightenment" | "e" => "Enlightenment".to_string(),
1532 _ => {
1533 if s.chars().all(|c| c.is_lowercase() || !c.is_alphabetic()) {
1535 let mut chars = s.chars();
1536 match chars.next() {
1537 None => String::new(),
1538 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
1539 }
1540 } else {
1541 s.to_string()
1542 }
1543 }
1544 }
1545}
1546
1547fn detect_init_system() -> Option<String> {
1548 #[cfg(target_os = "linux")]
1549 {
1550 let comm = std::fs::read_to_string("/proc/1/comm")
1551 .map(|s| s.trim().to_string())
1552 .ok()
1553 .filter(|s| !s.is_empty());
1554 if let Some(name) = comm {
1555 return Some(name);
1556 }
1557 std::fs::read_link("/proc/1/exe").ok().and_then(|p| {
1558 p.file_name()
1559 .and_then(|n| n.to_str())
1560 .map(|s| s.to_string())
1561 })
1562 }
1563 #[cfg(target_os = "macos")]
1564 {
1565 Some("launchd".to_string())
1566 }
1567 #[cfg(target_os = "windows")]
1568 {
1569 Some("SCM".to_string())
1570 }
1571 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1572 {
1573 None
1574 }
1575}
1576
1577fn detect_chassis() -> Option<String> {
1578 #[cfg(target_os = "linux")]
1579 {
1580 let raw = std::fs::read_to_string("/sys/class/dmi/id/chassis_type").ok()?;
1581 let n: u32 = raw.trim().parse().ok()?;
1582 let label = match n {
1583 3 => "Desktop",
1584 4 => "Low-Profile Desktop",
1585 6 => "Mini Tower",
1586 7 => "Tower",
1587 8 | 9 | 10 | 14 | 31 | 32 => "Laptop",
1588 11 => "Handheld",
1589 13 => "All-in-One",
1590 17 => "Main Server",
1591 23 => "Rack Server",
1592 28 => "Blade",
1593 30 => "Tablet",
1594 35 => "Mini PC",
1595 36 => "Stick PC",
1596 _ => return None,
1597 };
1598 Some(label.to_string())
1599 }
1600 #[cfg(target_os = "macos")]
1601 {
1602 let output = std::process::Command::new("sysctl")
1603 .args(["-n", "hw.model"])
1604 .output()
1605 .ok()?;
1606 let model = String::from_utf8(output.stdout).ok()?;
1607 let model = model.trim();
1608 if model.contains("MacBook") {
1609 Some("Laptop".to_string())
1610 } else if model.contains("MacPro") {
1611 Some("Desktop".to_string())
1612 } else if model.contains("Macmini") || model.contains("Mac mini") {
1613 Some("Mini PC".to_string())
1614 } else if model.contains("iMac") {
1615 Some("All-in-One".to_string())
1616 } else {
1617 Some(model.to_string())
1618 }
1619 }
1620 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1621 {
1622 None
1623 }
1624}
1625
1626fn detect_bootmgr() -> Option<String> {
1627 #[cfg(target_os = "linux")]
1628 {
1629 use std::path::Path;
1630 let is_uefi = Path::new("/sys/firmware/efi").exists();
1631 if Path::new("/boot/loader/entries").exists()
1632 || Path::new("/boot/loader/loader.conf").exists()
1633 || Path::new("/efi/loader/loader.conf").exists()
1634 {
1635 return Some("systemd-boot".to_string());
1636 }
1637 if Path::new("/boot/grub2/grub.cfg").exists() || Path::new("/boot/grub2").exists() {
1638 return Some("GRUB 2".to_string());
1639 }
1640 if Path::new("/boot/grub/grub.cfg").exists() || Path::new("/boot/grub").exists() {
1641 return Some("GRUB".to_string());
1642 }
1643 if is_uefi {
1644 Some("UEFI".to_string())
1645 } else {
1646 Some("BIOS".to_string())
1647 }
1648 }
1649 #[cfg(target_os = "macos")]
1650 {
1651 Some("Apple Boot ROM".to_string())
1652 }
1653 #[cfg(not(any(target_os = "linux", target_os = "macos")))]
1654 {
1655 None
1656 }
1657}
1658
1659fn detect_login_manager() -> Option<String> {
1666 #[cfg(target_os = "linux")]
1667 {
1668 let target = std::fs::read_link("/etc/systemd/system/display-manager.service").ok()?;
1669 let unit = target.file_name().and_then(|n| n.to_str())?;
1670 login_manager_from_unit(unit)
1671 }
1672 #[cfg(not(target_os = "linux"))]
1673 {
1674 None
1675 }
1676}
1677
1678#[cfg(target_os = "linux")]
1684fn login_manager_from_unit(unit: &str) -> Option<String> {
1685 let stem = unit.strip_suffix(".service").unwrap_or(unit).trim();
1686 if stem.is_empty() {
1687 return None;
1688 }
1689 let pretty = match stem.to_lowercase().as_str() {
1690 "gdm" | "gdm3" => "GDM",
1691 "sddm" => "SDDM",
1692 "lightdm" => "LightDM",
1693 "lxdm" => "LXDM",
1694 "xdm" => "XDM",
1695 "ly" => "Ly",
1696 "greetd" => "greetd",
1697 "slim" => "SLiM",
1698 "nodm" => "nodm",
1699 "entrance" => "Entrance",
1700 _ => {
1701 let mut chars = stem.chars();
1703 return chars
1704 .next()
1705 .map(|c| c.to_uppercase().collect::<String>() + chars.as_str());
1706 }
1707 };
1708 Some(pretty.to_string())
1709}
1710
1711fn detect_brightness() -> Option<String> {
1718 #[cfg(target_os = "linux")]
1719 {
1720 use std::path::Path;
1721 let dir = Path::new("/sys/class/backlight");
1722 if !dir.exists() {
1723 return None;
1724 }
1725 let mut devices: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
1728 .ok()?
1729 .flatten()
1730 .map(|e| e.path())
1731 .collect();
1732 devices.sort_by_key(|p| {
1733 let name = p
1734 .file_name()
1735 .and_then(|n| n.to_str())
1736 .unwrap_or("")
1737 .to_lowercase();
1738 if name.contains("acpi") || name.contains("video") {
1740 1
1741 } else {
1742 0
1743 }
1744 });
1745 for dev in devices {
1746 let cur = std::fs::read_to_string(dev.join("brightness"))
1747 .ok()
1748 .and_then(|s| s.trim().parse::<u64>().ok());
1749 let max = std::fs::read_to_string(dev.join("max_brightness"))
1750 .ok()
1751 .and_then(|s| s.trim().parse::<u64>().ok());
1752 if let (Some(cur), Some(max)) = (cur, max) {
1753 if let Some(pct) = brightness_percent(cur, max) {
1754 return Some(pct);
1755 }
1756 }
1757 }
1758 None
1759 }
1760 #[cfg(not(target_os = "linux"))]
1761 {
1762 None
1763 }
1764}
1765
1766#[cfg(target_os = "linux")]
1771fn brightness_percent(cur: u64, max: u64) -> Option<String> {
1772 if max == 0 {
1773 return None;
1774 }
1775 let pct = (cur as f64 / max as f64 * 100.0).round() as u64;
1776 Some(format!("{}%", pct))
1777}
1778
1779fn detect_power_adapter() -> Option<String> {
1787 #[cfg(target_os = "linux")]
1788 {
1789 use std::path::Path;
1790 let dir = Path::new("/sys/class/power_supply");
1791 if !dir.exists() {
1792 return None;
1793 }
1794 for entry in std::fs::read_dir(dir).ok()?.flatten() {
1795 let path = entry.path();
1796 let supply_type = std::fs::read_to_string(path.join("type"))
1797 .map(|s| s.trim().to_string())
1798 .unwrap_or_default();
1799 if supply_type != "Mains" {
1800 continue;
1801 }
1802 let name = path
1803 .file_name()
1804 .and_then(|n| n.to_str())
1805 .unwrap_or("AC")
1806 .to_string();
1807 let online = std::fs::read_to_string(path.join("online"))
1808 .map(|s| s.trim().to_string())
1809 .unwrap_or_default();
1810 return Some(format_power_adapter(&name, &online));
1811 }
1812 None
1813 }
1814 #[cfg(not(target_os = "linux"))]
1815 {
1816 None
1817 }
1818}
1819
1820#[cfg(target_os = "linux")]
1824fn format_power_adapter(name: &str, online: &str) -> String {
1825 let state = match online.trim() {
1826 "1" => "connected",
1827 "0" => "not connected",
1828 _ => "unknown",
1829 };
1830 format!("{} ({})", name, state)
1831}
1832
1833fn detect_tpm() -> Option<String> {
1841 #[cfg(target_os = "linux")]
1842 {
1843 use std::path::Path;
1844 let dir = Path::new("/sys/class/tpm");
1845 if !dir.exists() {
1846 return None;
1847 }
1848 let mut devices: Vec<std::path::PathBuf> = std::fs::read_dir(dir)
1849 .ok()?
1850 .flatten()
1851 .map(|e| e.path())
1852 .collect();
1853 devices.sort();
1855 for dev in devices {
1856 if let Ok(major) = std::fs::read_to_string(dev.join("tpm_version_major")) {
1857 if let Some(v) = format_tpm_version(major.trim()) {
1858 return Some(v);
1859 }
1860 }
1861 }
1862 None
1863 }
1864 #[cfg(not(target_os = "linux"))]
1865 {
1866 None
1867 }
1868}
1869
1870#[cfg(target_os = "linux")]
1877fn format_tpm_version(major: &str) -> Option<String> {
1878 match major.trim() {
1879 "1" => Some("1.2".to_string()),
1880 "2" => Some("2.0".to_string()),
1881 _ => None,
1882 }
1883}
1884
1885#[cfg(target_os = "windows")]
1890mod win_cpu {
1891 #[repr(C)]
1892 struct FileTime {
1893 low: u32,
1894 high: u32,
1895 }
1896
1897 impl FileTime {
1898 fn ticks(&self) -> u64 {
1899 ((self.high as u64) << 32) | self.low as u64
1900 }
1901 }
1902
1903 extern "system" {
1904 fn GetSystemTimes(idle: *mut FileTime, kernel: *mut FileTime, user: *mut FileTime) -> i32;
1905 }
1906
1907 pub fn sample() -> Option<(u64, u64, u64)> {
1910 let mut idle = FileTime { low: 0, high: 0 };
1911 let mut kernel = FileTime { low: 0, high: 0 };
1912 let mut user = FileTime { low: 0, high: 0 };
1913 let ok = unsafe { GetSystemTimes(&mut idle, &mut kernel, &mut user) };
1915 if ok == 0 {
1916 None
1917 } else {
1918 Some((idle.ticks(), kernel.ticks(), user.ticks()))
1919 }
1920 }
1921
1922 pub fn usage_percent(s0: (u64, u64, u64), s1: (u64, u64, u64)) -> f32 {
1925 let idle = s1.0.saturating_sub(s0.0);
1926 let kernel = s1.1.saturating_sub(s0.1);
1927 let user = s1.2.saturating_sub(s0.2);
1928 let total = kernel + user;
1929 if total == 0 {
1930 0.0
1931 } else {
1932 (100.0 * total.saturating_sub(idle) as f64 / total as f64) as f32
1933 }
1934 }
1935
1936 #[cfg(test)]
1937 mod layout {
1938 use std::mem::size_of;
1939
1940 #[test]
1942 fn filetime_size() {
1943 assert_eq!(size_of::<super::FileTime>(), 8);
1944 }
1945 }
1946}
1947
1948#[cfg(test)]
1949mod tests {
1950
1951 #[test]
1952 fn cpu_refresh_kind_is_none_when_no_cpu_field_is_selected() {
1953 assert!(cpu_refresh_kind(false, false, false).is_none());
1954 }
1955
1956 #[test]
1957 fn cpu_refresh_kind_for_plain_cpu_asks_for_neither_flag() {
1958 let kind = cpu_refresh_kind(true, false, false).expect("cpu selected");
1962 assert!(!kind.frequency(), "plain `cpu` must not request frequency");
1963 assert!(!kind.cpu_usage(), "plain `cpu` must not request cpu usage");
1964 }
1965
1966 #[test]
1967 fn cpu_refresh_kind_asks_for_frequency_only_for_cpu_freq() {
1968 let kind = cpu_refresh_kind(false, true, false).expect("cpu-freq selected");
1969 assert!(
1970 kind.frequency(),
1971 "`cpu-freq` reads Cpu::frequency() and must request it"
1972 );
1973 }
1974
1975 #[test]
1976 fn cpu_refresh_kind_asks_for_usage_only_off_windows() {
1977 let kind = cpu_refresh_kind(false, false, true).expect("cpu-usage selected");
1978 if cfg!(target_os = "windows") {
1979 assert!(!kind.cpu_usage());
1982 } else {
1983 assert!(kind.cpu_usage());
1985 }
1986 assert!(!kind.frequency(), "cpu-usage must not drag in frequency");
1987 }
1988
1989 #[test]
1990 fn load_is_probed_only_when_selected_and_never_on_windows() {
1991 assert!(
1992 !should_probe_load(false),
1993 "an unselected `load` must not be probed on any platform"
1994 );
1995 if cfg!(target_os = "windows") {
1996 assert!(
1997 !should_probe_load(true),
1998 "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"
1999 );
2000 } else {
2001 assert!(should_probe_load(true));
2002 }
2003 }
2004
2005 use super::*;
2006
2007 #[cfg(target_os = "linux")]
2008 #[test]
2009 fn test_login_manager_from_unit() {
2010 assert_eq!(
2011 login_manager_from_unit("gdm.service").as_deref(),
2012 Some("GDM")
2013 );
2014 assert_eq!(
2015 login_manager_from_unit("gdm3.service").as_deref(),
2016 Some("GDM")
2017 );
2018 assert_eq!(
2019 login_manager_from_unit("sddm.service").as_deref(),
2020 Some("SDDM")
2021 );
2022 assert_eq!(
2023 login_manager_from_unit("lightdm.service").as_deref(),
2024 Some("LightDM")
2025 );
2026 assert_eq!(
2028 login_manager_from_unit("emptty.service").as_deref(),
2029 Some("Emptty")
2030 );
2031 assert_eq!(login_manager_from_unit("ly").as_deref(), Some("Ly"));
2033 assert_eq!(login_manager_from_unit("").as_deref(), None);
2035 assert_eq!(login_manager_from_unit(".service").as_deref(), None);
2036 }
2037
2038 #[cfg(target_os = "linux")]
2039 #[test]
2040 fn test_brightness_percent() {
2041 assert_eq!(brightness_percent(50, 100).as_deref(), Some("50%"));
2042 assert_eq!(brightness_percent(100, 100).as_deref(), Some("100%"));
2043 assert_eq!(brightness_percent(0, 100).as_deref(), Some("0%"));
2044 assert_eq!(brightness_percent(133, 255).as_deref(), Some("52%"));
2046 assert_eq!(brightness_percent(10, 0), None);
2048 }
2049
2050 #[cfg(target_os = "linux")]
2051 #[test]
2052 fn test_format_power_adapter() {
2053 assert_eq!(format_power_adapter("AC", "1"), "AC (connected)");
2054 assert_eq!(format_power_adapter("ADP1", "0"), "ADP1 (not connected)");
2055 assert_eq!(format_power_adapter("AC", ""), "AC (unknown)");
2057 }
2058
2059 #[cfg(target_os = "linux")]
2060 #[test]
2061 fn test_format_tpm_version() {
2062 assert_eq!(format_tpm_version("2").as_deref(), Some("2.0"));
2064 assert_eq!(format_tpm_version("1").as_deref(), Some("1.2"));
2065 assert_eq!(format_tpm_version("2\n").as_deref(), Some("2.0"));
2067 assert_eq!(format_tpm_version("3"), None);
2069 assert_eq!(format_tpm_version(""), None);
2070 assert_eq!(format_tpm_version("garbage"), None);
2071 }
2072
2073 #[cfg(target_os = "windows")]
2074 #[test]
2075 fn test_win_cpu_usage_percent() {
2076 use super::win_cpu::usage_percent;
2077 let u = usage_percent((0, 0, 0), (50, 100, 50));
2080 assert!((u - 66.6667).abs() < 0.01, "got {}", u);
2081
2082 assert_eq!(usage_percent((0, 0, 0), (100, 100, 0)), 0.0);
2084
2085 assert_eq!(usage_percent((0, 0, 0), (0, 100, 100)), 100.0);
2087
2088 assert_eq!(usage_percent((5, 10, 10), (5, 10, 10)), 0.0);
2090 }
2091
2092 #[test]
2098 fn test_format_cpu_cores_no_hyperthreading() {
2099 assert_eq!(format_cpu_cores_plain(4, Some(4)), "4 cores");
2101 }
2102
2103 #[test]
2104 fn test_format_cpu_cores_hyperthreaded() {
2105 assert_eq!(format_cpu_cores_plain(16, Some(8)), "8C / 16T");
2107 }
2108
2109 #[test]
2110 fn test_format_cpu_cores_unknown_physical() {
2111 assert_eq!(format_cpu_cores_plain(8, None), "8 cores");
2113 }
2114
2115 #[test]
2116 fn test_format_cpu_cores_physical_equals_zero() {
2117 let result = format_cpu_cores_plain(8, Some(0));
2121 assert!(result.contains("8"), "should mention 8 threads: {}", result);
2122 }
2123
2124 #[cfg(target_os = "linux")]
2125 #[test]
2126 fn test_detect_cpu_cache_returns_some_on_linux() {
2127 if std::path::Path::new("/sys/devices/system/cpu/cpu0/cache").exists() {
2130 let result = detect_cpu_cache();
2131 assert!(result.is_some(), "expected cache info on Linux with sysfs");
2132 let s = result.unwrap();
2133 assert!(
2134 s.contains("L1") || s.contains("L2") || s.contains("L3"),
2135 "expected cache level labels, got: {}",
2136 s
2137 );
2138 }
2139 }
2140
2141 #[test]
2142 fn test_normalize_desktop_name_known() {
2143 assert_eq!(normalize_desktop_name("gnome"), "GNOME");
2144 assert_eq!(normalize_desktop_name("GNOME"), "GNOME");
2145 assert_eq!(normalize_desktop_name("kde"), "KDE Plasma");
2146 assert_eq!(normalize_desktop_name("plasma"), "KDE Plasma");
2147 assert_eq!(normalize_desktop_name("KDE Plasma"), "KDE Plasma");
2148 assert_eq!(normalize_desktop_name("xfce"), "XFCE");
2149 assert_eq!(normalize_desktop_name("lxqt"), "LXQt");
2150 assert_eq!(normalize_desktop_name("mate"), "MATE");
2151 assert_eq!(normalize_desktop_name("cinnamon"), "Cinnamon");
2152 assert_eq!(normalize_desktop_name("e"), "Enlightenment");
2153 }
2154
2155 #[test]
2156 fn test_normalize_desktop_name_unknown_lowercase() {
2157 assert_eq!(normalize_desktop_name("budgie"), "Budgie");
2159 assert_eq!(normalize_desktop_name("niri"), "Niri");
2160 }
2161
2162 #[test]
2163 fn test_normalize_desktop_name_unknown_mixed() {
2164 assert_eq!(normalize_desktop_name("MyDE"), "MyDE");
2166 }
2167
2168 #[test]
2169 fn test_normalize_desktop_name_trims_whitespace() {
2170 assert_eq!(normalize_desktop_name(" gnome "), "GNOME");
2171 assert_eq!(normalize_desktop_name(" niri "), "Niri");
2172 }
2173
2174 #[cfg(target_os = "linux")]
2175 #[test]
2176 fn test_detect_desktop_from_proc_returns_option() {
2177 let result = detect_desktop_from_proc();
2179 if let Some(ref de) = result {
2180 assert!(!de.is_empty(), "desktop name should not be empty");
2181 }
2182 }
2183
2184 #[cfg(target_os = "linux")]
2185 #[test]
2186 fn test_detect_cpu_freq_range_returns_ordered_pair() {
2187 if std::path::Path::new("/sys/devices/system/cpu/cpufreq").exists() {
2188 if let Some((min, max)) = detect_cpu_freq_range() {
2189 assert!(
2190 min <= max,
2191 "min freq should be <= max freq: {} > {}",
2192 min,
2193 max
2194 );
2195 assert!(min > 0, "min freq should be positive");
2196 }
2197 }
2198 }
2199}