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