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