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