Skip to main content

retch_sysinfo/
fetch.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright (C) 2026 l1a
3
4//! System information gathering.
5//!
6//! Uses the `sysinfo` crate and other heuristics to collect details
7//! about the OS, hardware, and environment.
8
9use crate::gpu;
10use chrono::TimeZone;
11use sysinfo::{Components, Disks, System, Users};
12
13/// Options for controlling what system information is gathered.
14///
15/// This decouples the collection logic from the CLI argument parser,
16/// allowing `retch-sysinfo` to be used as a standalone library.
17#[derive(Debug, Default, Clone)]
18pub struct CollectOptions {
19    /// Whether to collect all fields (long mode) or only the primary/default ones.
20    pub long: bool,
21}
22
23/// Comprehensive system information data structure.
24///
25/// This struct holds all the metrics collected from the system,
26/// ranging from OS details to hardware specs and network status.
27#[derive(Debug)]
28pub struct SystemInfo {
29    /// Operating system name and version.
30    pub os: String,
31    /// Kernel version.
32    pub kernel: Option<String>,
33    /// System hostname.
34    pub hostname: Option<String>,
35    /// CPU architecture (e.g., x86_64).
36    pub arch: String,
37    /// CPU model brand string.
38    pub cpu: String,
39    /// Total number of logical CPU cores.
40    pub cpu_cores: usize,
41    /// Formatted memory usage (Used / Total).
42    pub memory: String,
43    /// Formatted swap usage (Used / Total).
44    pub swap: String,
45    /// System uptime formatted as a duration.
46    pub uptime: String,
47    /// Number of currently running processes.
48    pub processes: usize,
49    /// Load average (1, 5, 15 minutes).
50    pub load_avg: Option<String>,
51    /// List of mounted disks with usage information.
52    pub disks: Vec<String>,
53    /// Hardware component temperatures.
54    pub temps: Vec<String>,
55    /// Network interface statistics and status.
56    pub networks: Vec<String>,
57    /// System boot time in ISO 8601 format.
58    pub boot_time: String,
59    /// Battery status (currently placeholder for future feature).
60    pub battery: Option<String>,
61    /// Path to the current user's shell.
62    pub shell: Option<String>,
63    /// Name of the terminal emulator in use.
64    pub terminal: Option<String>,
65    /// Detected desktop environment or window manager.
66    pub desktop: Option<String>,
67    /// Current CPU frequency (formatted).
68    pub cpu_freq: Option<String>,
69    /// Number of interactive users (UID >= 1000).
70    pub users: usize,
71    /// List of detected GPUs with model names.
72    pub gpu: Vec<String>,
73    /// Total count of installed packages across supported managers.
74    pub packages: Option<usize>,
75    /// Name of the user running the process.
76    pub current_user: Option<String>,
77    /// Primary local IP address.
78    pub local_ip: Option<String>,
79    /// Public IP address (best effort).
80    pub public_ip: Option<String>,
81    /// Name of the active/default network interface.
82    pub active_interface: Option<String>,
83    /// Detected motherboard name and manufacturer.
84    pub motherboard: Option<String>,
85    /// Detected BIOS details.
86    pub bios: Option<String>,
87    /// List of connected display resolutions and refresh rates.
88    pub displays: Vec<String>,
89    /// Detected active audio driver/server and devices.
90    pub audio: Option<String>,
91    /// Connected Wi-Fi SSID and speed.
92    pub wifi: Option<String>,
93    /// Bluetooth power status.
94    pub bluetooth: Option<String>,
95    /// UI Theme (GTK, Qt, macOS, Windows).
96    pub ui_theme: Option<String>,
97    /// Icon theme (GTK/Qt).
98    pub icons: Option<String>,
99    /// Cursor theme (GTK/Qt).
100    pub cursor: Option<String>,
101    /// System Font.
102    pub font: Option<String>,
103    /// Terminal Font (configured in terminal emulator).
104    pub terminal_font: Option<String>,
105    /// Connected camera/webcam names.
106    pub camera: Vec<String>,
107    /// Connected gamepad/controller names.
108    pub gamepad: Vec<String>,
109}
110
111impl SystemInfo {
112    /// Collects system information using sysinfo and environment probes.
113    ///
114    /// This method aggregates data from the operating system, hardware,
115    /// and current user environment into a `SystemInfo` struct.
116    pub fn collect(opts: CollectOptions) -> anyhow::Result<Self> {
117        let mut sys = System::new_all();
118        sys.refresh_all();
119
120        let os = System::long_os_version()
121            .or_else(System::name)
122            .unwrap_or_else(|| "Unknown".to_string());
123
124        let kernel = System::kernel_version();
125        let hostname = System::host_name();
126
127        let cpu = sys
128            .cpus()
129            .first()
130            .map(|c| c.brand().to_string())
131            .unwrap_or_else(|| "Unknown CPU".to_string());
132
133        let cpu_cores = sys.cpus().len();
134
135        let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
136        let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
137        let memory = format!("{:.1} / {:.1} GB", used_mem, total_mem);
138
139        let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
140        let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
141        let swap = if total_swap > 0.0 {
142            format!("{:.1} / {:.1} GB", used_swap, total_swap)
143        } else {
144            "No swap".to_string()
145        };
146
147        let uptime = format!("{}s", System::uptime());
148
149        let disks_list = Disks::new_with_refreshed_list();
150        let disks: Vec<String> = if !opts.long {
151            let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
152            let mut best_match: Option<&sysinfo::Disk> = None;
153            for disk in disks_list.iter() {
154                if home.starts_with(disk.mount_point()) {
155                    if let Some(best) = best_match {
156                        if disk.mount_point().components().count()
157                            > best.mount_point().components().count()
158                        {
159                            best_match = Some(disk);
160                        }
161                    } else {
162                        best_match = Some(disk);
163                    }
164                }
165            }
166            let selected_disks = if let Some(best) = best_match {
167                vec![best]
168            } else {
169                disks_list.iter().collect::<Vec<_>>()
170            };
171
172            selected_disks
173                .iter()
174                .map(|d| {
175                    let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
176                    let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
177                    let fs = d.file_system().to_string_lossy();
178                    format!(
179                        "{} ({}): {:.1} GB free / {:.1} GB",
180                        d.mount_point().display(),
181                        fs,
182                        avail,
183                        total
184                    )
185                })
186                .collect()
187        } else {
188            disks_list
189                .iter()
190                .map(|d| {
191                    let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
192                    let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
193                    let fs = d.file_system().to_string_lossy();
194                    format!(
195                        "{} ({}): {:.1} GB free / {:.1} GB",
196                        d.mount_point().display(),
197                        fs,
198                        avail,
199                        total
200                    )
201                })
202                .collect()
203        };
204
205        let battery = crate::battery::get_battery_info().map(|bat| {
206            let pct = bat.percentage;
207            let state = match bat.state {
208                crate::battery::BatteryState::Charging => "charging",
209                crate::battery::BatteryState::Discharging => "discharging",
210                crate::battery::BatteryState::Full => "full",
211                _ => "not charging",
212            };
213            let vendor = bat.vendor;
214            let model = bat.model;
215
216            // Format time remaining as "Xh Ym" or "Xd Yh"
217            let time_str = match bat.state {
218                crate::battery::BatteryState::Charging => bat.time_remaining.map(|d| {
219                    let total_mins = d.as_secs() / 60;
220                    let hours = total_mins / 60;
221                    let mins = total_mins % 60;
222                    if hours >= 24 {
223                        let days = hours / 24;
224                        let rem_hours = hours % 24;
225                        format!("{}d {}h until full", days, rem_hours)
226                    } else if hours > 0 {
227                        format!("{}h {}m until full", hours, mins)
228                    } else {
229                        format!("{}m until full", mins)
230                    }
231                }),
232                crate::battery::BatteryState::Discharging => bat.time_remaining.map(|d| {
233                    let total_mins = d.as_secs() / 60;
234                    let hours = total_mins / 60;
235                    let mins = total_mins % 60;
236                    if hours >= 24 {
237                        let days = hours / 24;
238                        let rem_hours = hours % 24;
239                        format!("{}d {}h remaining", days, rem_hours)
240                    } else if hours > 0 {
241                        format!("{}h {}m remaining", hours, mins)
242                    } else {
243                        format!("{}m remaining", mins)
244                    }
245                }),
246                _ => None,
247            };
248
249            let mut parts = vec![state.to_string()];
250            if let Some(t) = time_str {
251                parts.insert(0, t);
252            }
253            if let Some(health) = bat.health {
254                if health < 99.0 {
255                    parts.push(format!("{:.0}% health", health));
256                }
257            }
258
259            let base = format!("{:.0}% ({})", pct, parts.join(", "));
260
261            match (vendor, model) {
262                (Some(v), Some(m)) => format!("{} [{} {}]", base, v, m),
263                (Some(v), None) => format!("{} [{}]", base, v),
264                _ => base,
265            }
266        });
267
268        let arch = System::cpu_arch();
269
270        let processes = sys.processes().len();
271
272        let load_avg = {
273            let avg = System::load_average();
274            if avg.one > 0.0 || avg.five > 0.0 {
275                Some(format!(
276                    "{:.2}, {:.2}, {:.2}",
277                    avg.one, avg.five, avg.fifteen
278                ))
279            } else {
280                None
281            }
282        };
283
284        // Compute slow system queries concurrently in parallel threads
285        let (
286            gpu,
287            packages,
288            public_ip,
289            (local_ip, active_interface),
290            motherboard,
291            bios,
292            displays,
293            audio,
294            wifi,
295            bluetooth,
296            (ui_theme, icons, cursor, font),
297            camera,
298            gamepad,
299        ) = std::thread::scope(|s| {
300            let gpu_handle = s.spawn(|| {
301                gpu::detect_gpus()
302                    .into_iter()
303                    .map(|g| g.format())
304                    .collect::<Vec<String>>()
305            });
306            let packages_handle = s.spawn(detect_packages);
307            let public_ip_handle = s.spawn(crate::network::detect_public_ip);
308            let network_ips_handle = s.spawn(crate::network::detect_active_interface_and_local_ip);
309            let motherboard_handle = s.spawn(detect_motherboard);
310            let bios_handle = s.spawn(detect_bios);
311            let displays_handle = s.spawn(crate::display::detect_displays);
312            let audio_handle = s.spawn(|| crate::audio::detect_audio(&sys));
313            let wifi_handle = s.spawn(crate::network::detect_wifi);
314            let bluetooth_handle = s.spawn(crate::bluetooth::detect_bluetooth);
315            let ui_theme_and_fonts_handle = s.spawn(detect_ui_theme_and_fonts);
316            let camera_handle = s.spawn(detect_camera);
317            let gamepad_handle = s.spawn(detect_gamepad);
318
319            (
320                gpu_handle.join().unwrap_or_default(),
321                packages_handle.join().ok().flatten(),
322                public_ip_handle.join().ok().flatten(),
323                network_ips_handle.join().unwrap_or((None, None)),
324                motherboard_handle.join().ok().flatten(),
325                bios_handle.join().ok().flatten(),
326                displays_handle.join().unwrap_or_default(),
327                audio_handle.join().ok().flatten(),
328                wifi_handle.join().ok().flatten(),
329                bluetooth_handle.join().ok().flatten(),
330                ui_theme_and_fonts_handle
331                    .join()
332                    .unwrap_or((None, None, None, None)),
333                camera_handle.join().unwrap_or_default(),
334                gamepad_handle.join().unwrap_or_default(),
335            )
336        });
337
338        let mut temps: Vec<String> = Components::new_with_refreshed_list()
339            .iter()
340            .filter_map(|c| {
341                c.temperature().and_then(|t| {
342                    if t > 0.0 {
343                        Some(format!("{}: {:.0}°C", c.label(), t))
344                    } else {
345                        None
346                    }
347                })
348            })
349            .collect();
350
351        // Sort so CPU temperatures appear first
352        temps.sort_by(|a, b| {
353            let a_cpu = a.to_lowercase().contains("cpu") || a.to_lowercase().contains("core");
354            let b_cpu = b.to_lowercase().contains("cpu") || b.to_lowercase().contains("core");
355            b_cpu.cmp(&a_cpu)
356        });
357
358        let networks =
359            crate::network::detect_networks(active_interface.as_deref(), local_ip.as_deref());
360
361        let boot_timestamp = System::boot_time();
362        let boot_dt = chrono::Local
363            .timestamp_opt(boot_timestamp as i64, 0)
364            .single()
365            .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
366            .unwrap_or_else(|| boot_timestamp.to_string());
367        let boot_time = boot_dt;
368
369        // Environment-based info
370        let shell = detect_shell(&sys);
371        let terminal = detect_terminal(&sys);
372        let terminal_font = detect_terminal_font(terminal.as_deref());
373        let desktop = std::env::var("XDG_CURRENT_DESKTOP")
374            .or_else(|_| std::env::var("DESKTOP_SESSION"))
375            .ok();
376
377        // CPU frequency (first CPU)
378        let cpu_freq = sys
379            .cpus()
380            .first()
381            .map(|c| format!("{:.2} GHz", c.frequency() as f64 / 1000.0));
382
383        // Current logged in user
384        let current_user = std::env::var("USER").ok();
385
386        // Number of interactive users (UID >= 1000, excluding system accounts)
387        let users = Users::new_with_refreshed_list()
388            .iter()
389            .filter(|user| {
390                // UID is exposed via Display
391                let uid_str = user.id().to_string();
392                if let Ok(uid) = uid_str.parse::<u32>() {
393                    uid >= 1000
394                } else {
395                    false
396                }
397            })
398            .count();
399
400        Ok(Self {
401            os,
402            kernel,
403            hostname,
404            arch,
405            cpu,
406            cpu_cores,
407            memory,
408            swap,
409            uptime,
410            processes,
411            load_avg,
412            disks,
413            temps,
414            networks,
415            boot_time,
416            battery,
417            shell,
418            terminal,
419            desktop,
420            cpu_freq,
421            users,
422            gpu,
423            packages,
424            current_user,
425            local_ip,
426            public_ip,
427            active_interface,
428            motherboard,
429            bios,
430            displays,
431            audio,
432            wifi,
433            bluetooth,
434            ui_theme,
435            icons,
436            cursor,
437            font,
438            terminal_font,
439            camera,
440            gamepad,
441        })
442    }
443}
444
445/// Count installed packages by inspecting package manager databases directly.
446///
447/// Supports Pacman (Arch), Dpkg (Debian), XBPS (Void), RPM (Fedora/RHEL) on Linux,
448/// and Homebrew (Formulae and Casks) and MacPorts on macOS.
449fn detect_packages() -> Option<usize> {
450    #[cfg(target_os = "macos")]
451    {
452        let mut count = 0;
453
454        // Homebrew Cellar (Formulae)
455        for cellar_path in &["/opt/homebrew/Cellar", "/usr/local/Cellar"] {
456            if let Ok(entries) = std::fs::read_dir(cellar_path) {
457                count += entries.filter_map(|e| e.ok()).count();
458            }
459        }
460
461        // Homebrew Caskroom (Casks)
462        for cask_path in &["/opt/homebrew/Caskroom", "/usr/local/Caskroom"] {
463            if let Ok(entries) = std::fs::read_dir(cask_path) {
464                count += entries.filter_map(|e| e.ok()).count();
465            }
466        }
467
468        // MacPorts
469        if let Ok(entries) = std::fs::read_dir("/opt/local/var/macports/software") {
470            count += entries.filter_map(|e| e.ok()).count();
471        }
472
473        if count > 0 {
474            Some(count)
475        } else {
476            None
477        }
478    }
479
480    #[cfg(target_os = "windows")]
481    {
482        let mut count = 0;
483
484        // Scoop
485        if let Some(home) = dirs::home_dir() {
486            let scoop_dir = std::env::var("SCOOP")
487                .map(std::path::PathBuf::from)
488                .unwrap_or_else(|_| home.join("scoop"));
489            let scoop_apps = scoop_dir.join("apps");
490            if let Ok(entries) = std::fs::read_dir(scoop_apps) {
491                count += entries.filter_map(|e| e.ok()).count();
492            }
493        }
494
495        // Chocolatey
496        let choco_install = std::env::var("ChocolateyInstall")
497            .unwrap_or_else(|_| "C:\\ProgramData\\chocolatey".to_string());
498        let choco_lib = std::path::Path::new(&choco_install).join("lib");
499        if let Ok(entries) = std::fs::read_dir(choco_lib) {
500            count += entries.filter_map(|e| e.ok()).count();
501        }
502
503        if count > 0 {
504            Some(count)
505        } else {
506            None
507        }
508    }
509
510    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
511    {
512        // Arch / Manjaro
513        if let Ok(entries) = std::fs::read_dir("/var/lib/pacman/local") {
514            let count = entries.filter_map(|e| e.ok()).count();
515            if count > 0 {
516                return Some(count);
517            }
518        }
519
520        // Debian / Ubuntu
521        if let Ok(entries) = std::fs::read_dir("/var/lib/dpkg/info") {
522            let count = entries
523                .filter_map(|e| e.ok())
524                .filter(|e| e.path().extension().is_some_and(|ext| ext == "list"))
525                .count();
526            if count > 0 {
527                return Some(count);
528            }
529        }
530
531        // Gentoo
532        if let Ok(entries) = std::fs::read_dir("/var/db/pkg") {
533            let count: usize = entries
534                .filter_map(|e| e.ok())
535                .map(|e| {
536                    std::fs::read_dir(e.path())
537                        .map(|d| d.filter(|_| true).count())
538                        .unwrap_or(0)
539                })
540                .sum();
541            if count > 0 {
542                return Some(count);
543            }
544        }
545
546        // Void Linux
547        if let Ok(entries) = std::fs::read_dir("/var/db/xbps") {
548            let count = entries
549                .filter_map(|e| e.ok())
550                .filter(|e| e.path().extension().is_some_and(|ext| ext == "plist"))
551                .count();
552            if count > 0 {
553                return Some(count);
554            }
555        }
556
557        // Fedora / RHEL / openSUSE - read from RPM SQLite database
558        let rpm_db = "/var/lib/rpm/rpmdb.sqlite";
559        if std::path::Path::new(rpm_db).exists() {
560            match rusqlite::Connection::open(rpm_db) {
561                Ok(conn) => {
562                    if let Ok(count) = conn.query_row("SELECT COUNT(*) FROM Packages", [], |row| {
563                        row.get::<_, i64>(0)
564                    }) {
565                        if count > 0 {
566                            return Some(count as usize);
567                        }
568                    }
569                }
570                Err(e) => {
571                    eprintln!("warning: failed to open RPM database at {}: {}", rpm_db, e);
572                }
573            }
574        }
575
576        None
577    }
578}
579
580#[allow(dead_code)]
581pub fn is_real_camera(name: &str) -> bool {
582    let name_lower = name.to_lowercase();
583    !name_lower.contains("infrared")
584        && !name_lower.contains("ir camera")
585        && !name_lower.contains("integrated i")
586        && !name_lower.contains("integrated ir")
587        && !name_lower.contains("depth camera")
588}
589
590#[allow(dead_code)]
591pub fn clean_camera_name(name: &str) -> String {
592    let trimmed = name.trim();
593    if trimmed.starts_with("Integrated Camera:") {
594        return "Integrated Camera".to_string();
595    }
596    if trimmed.starts_with("Integrated Webcam:") {
597        return "Integrated Webcam".to_string();
598    }
599    trimmed.to_string()
600}
601
602#[allow(dead_code)]
603pub fn parse_macos_camera(stdout: &str) -> Vec<String> {
604    let mut devices = Vec::new();
605    let mut in_cameras = false;
606    for line in stdout.lines() {
607        let trimmed = line.trim();
608        let indent = line.len() - line.trim_start().len();
609        if trimmed.starts_with("Video Support:")
610            || trimmed.starts_with("Camera:")
611            || trimmed.starts_with("Cameras:")
612        {
613            in_cameras = true;
614            continue;
615        }
616        if in_cameras {
617            if indent < 4
618                && !trimmed.is_empty()
619                && !trimmed.starts_with("Camera")
620                && !trimmed.starts_with("Video Support")
621            {
622                in_cameras = false;
623                continue;
624            }
625            if (indent == 4 || indent == 6 || indent == 8) && trimmed.ends_with(':') {
626                let name = trimmed.trim_end_matches(':').trim().to_string();
627                if !name.is_empty() && is_real_camera(&name) {
628                    let cleaned = clean_camera_name(&name);
629                    if !devices.contains(&cleaned) {
630                        devices.push(cleaned);
631                    }
632                }
633            }
634        }
635    }
636    devices
637}
638
639#[allow(dead_code)]
640pub fn parse_macos_gamepad(usb_stdout: &str, bt_stdout: &str) -> Vec<String> {
641    let mut gamepads = Vec::new();
642    let keywords = [
643        "controller",
644        "gamepad",
645        "joystick",
646        "xbox",
647        "playstation",
648        "dualshock",
649        "dualsense",
650        "nintendo",
651        "joy-con",
652        "joycon",
653    ];
654
655    let is_gamepad = |name: &str| -> bool {
656        let name_lower = name.to_lowercase();
657        keywords.iter().any(|&kw| name_lower.contains(kw))
658    };
659
660    // Parse USB
661    for line in usb_stdout.lines() {
662        let trimmed = line.trim();
663        let indent = line.len() - line.trim_start().len();
664        if (indent == 4 || indent == 6 || indent == 8) && trimmed.ends_with(':') {
665            let name = trimmed.trim_end_matches(':').trim().to_string();
666            if is_gamepad(&name) && !gamepads.contains(&name) {
667                gamepads.push(name);
668            }
669        }
670    }
671
672    // Parse Bluetooth
673    let mut current_device = None;
674    for line in bt_stdout.lines() {
675        let trimmed = line.trim();
676        let indent = line.len() - line.trim_start().len();
677        if indent >= 8 && trimmed.ends_with(':') {
678            current_device = Some(trimmed.trim_end_matches(':').trim().to_string());
679        } else if trimmed.starts_with("Connected: Yes") || trimmed.starts_with("Connection: Yes") {
680            if let Some(ref dev) = current_device {
681                if is_gamepad(dev) && !gamepads.contains(dev) {
682                    gamepads.push(dev.clone());
683                }
684            }
685        }
686    }
687
688    gamepads
689}
690
691fn detect_camera() -> Vec<String> {
692    #[cfg(target_os = "linux")]
693    {
694        let mut cameras = Vec::new();
695        if let Ok(entries) = std::fs::read_dir("/sys/class/video4linux") {
696            for entry in entries.filter_map(|e| e.ok()) {
697                let path = entry.path().join("name");
698                if path.exists() {
699                    if let Ok(name) = std::fs::read_to_string(path) {
700                        let trimmed = name.trim().to_string();
701                        if !trimmed.is_empty() && is_real_camera(&trimmed) {
702                            let cleaned = clean_camera_name(&trimmed);
703                            if !cameras.contains(&cleaned) {
704                                cameras.push(cleaned);
705                            }
706                        }
707                    }
708                }
709            }
710        }
711        cameras
712    }
713
714    #[cfg(target_os = "macos")]
715    {
716        if let Ok(output) = std::process::Command::new("system_profiler")
717            .arg("SPCameraDataType")
718            .output()
719        {
720            if let Ok(stdout) = String::from_utf8(output.stdout) {
721                return parse_macos_camera(&stdout);
722            }
723        }
724        Vec::new()
725    }
726
727    #[cfg(target_os = "windows")]
728    {
729        let cmd = "Get-PnpDevice -Class Camera,Image -PresentOnly -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'OK' } | Select-Object -ExpandProperty FriendlyName";
730        if let Ok(output) = std::process::Command::new("powershell")
731            .args(["-Command", cmd])
732            .output()
733        {
734            if let Ok(stdout) = String::from_utf8(output.stdout) {
735                let mut cameras = Vec::new();
736                for line in stdout.lines() {
737                    let trimmed = line.trim().to_string();
738                    if !trimmed.is_empty() && is_real_camera(&trimmed) {
739                        let cleaned = clean_camera_name(&trimmed);
740                        if !cameras.contains(&cleaned) {
741                            cameras.push(cleaned);
742                        }
743                    }
744                }
745                return cameras;
746            }
747        }
748        Vec::new()
749    }
750
751    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
752    {
753        Vec::new()
754    }
755}
756
757fn detect_gamepad() -> Vec<String> {
758    #[cfg(target_os = "linux")]
759    {
760        let mut gamepads = Vec::new();
761        if let Ok(entries) = std::fs::read_dir("/sys/class/input") {
762            for entry in entries.filter_map(|e| e.ok()) {
763                let name = entry.file_name().to_string_lossy().to_string();
764                if name.starts_with("js") {
765                    let path = entry.path().join("device/name");
766                    if path.exists() {
767                        if let Ok(dev_name) = std::fs::read_to_string(path) {
768                            let trimmed = dev_name.trim().to_string();
769                            if !trimmed.is_empty() && !gamepads.contains(&trimmed) {
770                                gamepads.push(trimmed);
771                            }
772                        }
773                    }
774                }
775            }
776        }
777        gamepads
778    }
779
780    #[cfg(target_os = "macos")]
781    {
782        let usb_stdout = std::process::Command::new("system_profiler")
783            .arg("SPUSBDataType")
784            .output()
785            .ok()
786            .and_then(|o| String::from_utf8(o.stdout).ok())
787            .unwrap_or_default();
788
789        let bt_stdout = std::process::Command::new("system_profiler")
790            .arg("SPBluetoothDataType")
791            .output()
792            .ok()
793            .and_then(|o| String::from_utf8(o.stdout).ok())
794            .unwrap_or_default();
795
796        parse_macos_gamepad(&usb_stdout, &bt_stdout)
797    }
798
799    #[cfg(target_os = "windows")]
800    {
801        let cmd = "Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | Where-Object { $_.Class -eq 'HIDClass' -and ($_.HardwareID -match 'HID_DEVICE_SYSTEM_GAME' -or $_.HardwareID -match 'HID_DEVICE_GAME') -or $_.FriendlyName -match 'Xbox Controller|Gamepad|Joystick' } | Select-Object -ExpandProperty FriendlyName";
802        if let Ok(output) = std::process::Command::new("powershell")
803            .args(["-Command", cmd])
804            .output()
805        {
806            if let Ok(stdout) = String::from_utf8(output.stdout) {
807                let mut gamepads = Vec::new();
808                for line in stdout.lines() {
809                    let trimmed = line.trim().to_string();
810                    if !trimmed.is_empty() && !gamepads.contains(&trimmed) {
811                        gamepads.push(trimmed);
812                    }
813                }
814                return gamepads;
815            }
816        }
817        Vec::new()
818    }
819
820    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
821    {
822        Vec::new()
823    }
824}
825
826fn detect_motherboard() -> Option<String> {
827    #[cfg(target_os = "macos")]
828    {
829        extern "C" {
830            fn sysctlbyname(
831                name: *const i8,
832                oldp: *mut std::ffi::c_void,
833                oldlenp: *mut usize,
834                newp: *mut std::ffi::c_void,
835                newlen: usize,
836            ) -> i32;
837        }
838
839        let name_c = std::ffi::CString::new("hw.model").ok();
840        let mut model_str = None;
841
842        if let Some(name) = name_c {
843            let mut size: usize = 0;
844            unsafe {
845                sysctlbyname(
846                    name.as_ptr(),
847                    std::ptr::null_mut(),
848                    &mut size,
849                    std::ptr::null_mut(),
850                    0,
851                );
852            }
853            if size > 0 {
854                let mut buf = vec![0u8; size];
855                let res = unsafe {
856                    sysctlbyname(
857                        name.as_ptr(),
858                        buf.as_mut_ptr() as *mut std::ffi::c_void,
859                        &mut size,
860                        std::ptr::null_mut(),
861                        0,
862                    )
863                };
864                if res == 0 {
865                    if let Some(pos) = buf.iter().position(|&x| x == 0) {
866                        buf.truncate(pos);
867                    }
868                    if let Ok(model) = String::from_utf8(buf) {
869                        let trimmed = model.trim();
870                        if !trimmed.is_empty() {
871                            model_str = Some(trimmed.to_string());
872                        }
873                    }
874                }
875            }
876        }
877
878        if model_str.is_some() {
879            return model_str;
880        }
881
882        if let Ok(output) = std::process::Command::new("sysctl")
883            .args(["-n", "hw.model"])
884            .output()
885        {
886            if let Ok(model) = String::from_utf8(output.stdout) {
887                let trimmed = model.trim();
888                if !trimmed.is_empty() {
889                    return Some(trimmed.to_string());
890                }
891            }
892        }
893        None
894    }
895
896    #[cfg(target_os = "windows")]
897    {
898        let manufacturer = win_reg::get_reg_string(
899            win_reg::HKEY_LOCAL_MACHINE,
900            "HARDWARE\\DESCRIPTION\\System\\BIOS",
901            "BaseBoardManufacturer",
902        )
903        .unwrap_or_default();
904        let product = win_reg::get_reg_string(
905            win_reg::HKEY_LOCAL_MACHINE,
906            "HARDWARE\\DESCRIPTION\\System\\BIOS",
907            "BaseBoardProduct",
908        )
909        .unwrap_or_default();
910
911        let manufacturer = manufacturer.trim();
912        let product = product.trim();
913
914        if !manufacturer.is_empty() && !product.is_empty() {
915            return Some(format!("{} {}", manufacturer, product));
916        } else if !product.is_empty() {
917            return Some(product.to_string());
918        } else if !manufacturer.is_empty() {
919            return Some(manufacturer.to_string());
920        }
921
922        if let Ok(output) = std::process::Command::new("wmic")
923            .args(["baseboard", "get", "manufacturer,product", "/value"])
924            .output()
925        {
926            if let Ok(stdout) = String::from_utf8(output.stdout) {
927                let mut manufacturer = String::new();
928                let mut product = String::new();
929                for line in stdout.lines() {
930                    let line = line.trim();
931                    if line.starts_with("Manufacturer=") {
932                        manufacturer = line
933                            .strip_prefix("Manufacturer=")
934                            .unwrap_or("")
935                            .trim()
936                            .to_string();
937                    } else if line.starts_with("Product=") {
938                        product = line
939                            .strip_prefix("Product=")
940                            .unwrap_or("")
941                            .trim()
942                            .to_string();
943                    }
944                }
945                if !manufacturer.is_empty() && !product.is_empty() {
946                    return Some(format!("{} {}", manufacturer, product));
947                } else if !product.is_empty() {
948                    return Some(product);
949                } else if !manufacturer.is_empty() {
950                    return Some(manufacturer);
951                }
952            }
953        }
954        None
955    }
956
957    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
958    {
959        let vendor = std::fs::read_to_string("/sys/class/dmi/id/board_vendor")
960            .map(|s| s.trim().to_string())
961            .ok();
962        let name = std::fs::read_to_string("/sys/class/dmi/id/board_name")
963            .map(|s| s.trim().to_string())
964            .ok();
965
966        match (vendor, name) {
967            (Some(v), Some(n)) if !v.is_empty() && !n.is_empty() => {
968                if n.starts_with(&v) {
969                    Some(n)
970                } else {
971                    Some(format!("{} {}", v, n))
972                }
973            }
974            (Some(v), _) if !v.is_empty() => Some(v),
975            (_, Some(n)) if !n.is_empty() => Some(n),
976            _ => None,
977        }
978    }
979}
980
981fn detect_bios() -> Option<String> {
982    #[cfg(target_os = "macos")]
983    {
984        if let Ok(output) = std::process::Command::new("system_profiler")
985            .arg("SPHardwareDataType")
986            .output()
987        {
988            if let Ok(stdout) = String::from_utf8(output.stdout) {
989                for line in stdout.lines() {
990                    let line = line.trim();
991                    if line.starts_with("System Firmware Version:")
992                        || line.starts_with("Boot ROM Version:")
993                    {
994                        if let Some(val) = line.split(':').nth(1) {
995                            return Some(val.trim().to_string());
996                        }
997                    }
998                }
999            }
1000        }
1001        None
1002    }
1003
1004    #[cfg(target_os = "windows")]
1005    {
1006        let manufacturer = win_reg::get_reg_string(
1007            win_reg::HKEY_LOCAL_MACHINE,
1008            "HARDWARE\\DESCRIPTION\\System\\BIOS",
1009            "BIOSVendor",
1010        )
1011        .unwrap_or_default();
1012        let version = win_reg::get_reg_string(
1013            win_reg::HKEY_LOCAL_MACHINE,
1014            "HARDWARE\\DESCRIPTION\\System\\BIOS",
1015            "BIOSVersion",
1016        )
1017        .unwrap_or_default();
1018        let raw_date = win_reg::get_reg_string(
1019            win_reg::HKEY_LOCAL_MACHINE,
1020            "HARDWARE\\DESCRIPTION\\System\\BIOS",
1021            "BIOSReleaseDate",
1022        )
1023        .unwrap_or_default();
1024
1025        let manufacturer = manufacturer.trim();
1026        let version = version.trim();
1027        let raw_date = raw_date.trim();
1028
1029        let date = if raw_date.len() >= 8 {
1030            let year = &raw_date[0..4];
1031            let month = &raw_date[4..6];
1032            let day = &raw_date[6..8];
1033            format!("{}/{}/{}", month, day, year)
1034        } else {
1035            raw_date.to_string()
1036        };
1037
1038        let mut parts = Vec::new();
1039        if !manufacturer.is_empty() {
1040            parts.push(manufacturer.to_string());
1041        }
1042        if !version.is_empty() {
1043            parts.push(version.to_string());
1044        }
1045        let mut res = parts.join(" ");
1046        if !date.is_empty() {
1047            if res.is_empty() {
1048                res = date;
1049            } else {
1050                res = format!("{} ({})", res, date);
1051            }
1052        }
1053        if !res.is_empty() {
1054            return Some(res);
1055        }
1056
1057        if let Ok(output) = std::process::Command::new("wmic")
1058            .args([
1059                "bios",
1060                "get",
1061                "manufacturer,smbiosbiosversion,releasedate",
1062                "/value",
1063            ])
1064            .output()
1065        {
1066            if let Ok(stdout) = String::from_utf8(output.stdout) {
1067                let mut manufacturer = String::new();
1068                let mut version = String::new();
1069                let mut date = String::new();
1070                for line in stdout.lines() {
1071                    let line = line.trim();
1072                    if line.starts_with("Manufacturer=") {
1073                        manufacturer = line
1074                            .strip_prefix("Manufacturer=")
1075                            .unwrap_or("")
1076                            .trim()
1077                            .to_string();
1078                    } else if line.starts_with("SMBIOSBIOSVersion=") {
1079                        version = line
1080                            .strip_prefix("SMBIOSBIOSVersion=")
1081                            .unwrap_or("")
1082                            .trim()
1083                            .to_string();
1084                    } else if line.starts_with("ReleaseDate=") {
1085                        let raw_date = line.strip_prefix("ReleaseDate=").unwrap_or("").trim();
1086                        if raw_date.len() >= 8 {
1087                            let year = &raw_date[0..4];
1088                            let month = &raw_date[4..6];
1089                            let day = &raw_date[6..8];
1090                            date = format!("{}/{}/{}", month, day, year);
1091                        } else {
1092                            date = raw_date.to_string();
1093                        }
1094                    }
1095                }
1096
1097                let mut parts = Vec::new();
1098                if !manufacturer.is_empty() {
1099                    parts.push(manufacturer);
1100                }
1101                if !version.is_empty() {
1102                    parts.push(version);
1103                }
1104                let mut res = parts.join(" ");
1105                if !date.is_empty() {
1106                    if res.is_empty() {
1107                        res = date;
1108                    } else {
1109                        res = format!("{} ({})", res, date);
1110                    }
1111                }
1112                if !res.is_empty() {
1113                    return Some(res);
1114                }
1115            }
1116        }
1117        None
1118    }
1119
1120    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
1121    {
1122        let vendor = std::fs::read_to_string("/sys/class/dmi/id/bios_vendor")
1123            .map(|s| s.trim().to_string())
1124            .ok();
1125        let version = std::fs::read_to_string("/sys/class/dmi/id/bios_version")
1126            .map(|s| s.trim().to_string())
1127            .ok();
1128        let date = std::fs::read_to_string("/sys/class/dmi/id/bios_date")
1129            .map(|s| s.trim().to_string())
1130            .ok();
1131
1132        let mut parts = Vec::new();
1133        if let Some(v) = vendor {
1134            if !v.is_empty() {
1135                parts.push(v);
1136            }
1137        }
1138        if let Some(ver) = version {
1139            let mut ver_cleaned = ver;
1140            while ver_cleaned.contains(" )") {
1141                ver_cleaned = ver_cleaned.replace(" )", ")");
1142            }
1143            let ver_cleaned = ver_cleaned.trim().to_string();
1144            if !ver_cleaned.is_empty() {
1145                parts.push(ver_cleaned);
1146            }
1147        }
1148        let mut res = parts.join(" ");
1149        if let Some(d) = date {
1150            if !d.is_empty() {
1151                if res.is_empty() {
1152                    res = d;
1153                } else {
1154                    res = format!("{} ({})", res, d);
1155                }
1156            }
1157        }
1158        if !res.is_empty() {
1159            Some(res)
1160        } else {
1161            None
1162        }
1163    }
1164}
1165
1166// Display detection logic has been extracted to `crate::display`.
1167
1168/// Detects the active shell and retrieves its version.
1169fn detect_shell(sys: &sysinfo::System) -> Option<String> {
1170    // 1. Try to get shell from the SHELL env var
1171    let shell_env = std::env::var("SHELL").ok();
1172
1173    // Extract shell path and basename
1174    let (shell_path, shell_name) = if let Some(ref path_str) = shell_env {
1175        let path = std::path::Path::new(path_str);
1176        let name = path
1177            .file_name()
1178            .and_then(|n| n.to_str())
1179            .unwrap_or(path_str)
1180            .to_string();
1181        (path_str.clone(), name)
1182    } else {
1183        // 2. Fallback to process tree climbing
1184        let mut current_pid = sysinfo::get_current_pid().ok();
1185        let mut detected: Option<(String, String)> = None;
1186        let known_shells = [
1187            "bash",
1188            "zsh",
1189            "fish",
1190            "sh",
1191            "dash",
1192            "nu",
1193            "elvish",
1194            "tcsh",
1195            "csh",
1196            "ksh",
1197            "powershell",
1198            "pwsh",
1199            "cmd",
1200        ];
1201
1202        while let Some(pid) = current_pid {
1203            if let Some(process) = sys.process(pid) {
1204                let proc_name = process.name().to_string_lossy().to_string();
1205                let proc_name_lower = proc_name.to_lowercase();
1206
1207                // On Windows, the process name might be "powershell.exe" or "pwsh.exe"
1208                let clean_name = proc_name_lower
1209                    .strip_suffix(".exe")
1210                    .unwrap_or(&proc_name_lower)
1211                    .to_string();
1212
1213                if known_shells.contains(&clean_name.as_str()) {
1214                    detected = Some((proc_name.clone(), clean_name));
1215                    break;
1216                }
1217                current_pid = process.parent();
1218            } else {
1219                break;
1220            }
1221        }
1222
1223        if let Some((orig_name, clean_name)) = detected {
1224            (orig_name, clean_name)
1225        } else {
1226            // Default fallbacks
1227            #[cfg(target_os = "windows")]
1228            {
1229                if std::env::var("PSModulePath").is_ok() {
1230                    ("powershell.exe".to_string(), "powershell".to_string())
1231                } else {
1232                    ("cmd.exe".to_string(), "cmd".to_string())
1233                }
1234            }
1235            #[cfg(not(target_os = "windows"))]
1236            {
1237                ("sh".to_string(), "sh".to_string())
1238            }
1239        }
1240    };
1241
1242    // Now that we have the shell_path and shell_name, query its version
1243    let shell_name_lower = shell_name.to_lowercase();
1244    let shell_name_clean = shell_name_lower
1245        .strip_suffix(".exe")
1246        .unwrap_or(&shell_name_lower);
1247
1248    let version = detect_shell_version(&shell_path, shell_name_clean);
1249
1250    if let Some(ver) = version {
1251        Some(format!("{} {}", shell_name_clean, ver))
1252    } else {
1253        Some(shell_name_clean.to_string())
1254    }
1255}
1256
1257/// Helper to query standard shell executables for their versions.
1258fn detect_shell_version(shell_path: &str, shell_name: &str) -> Option<String> {
1259    // Select arguments based on shell
1260    let args = match shell_name {
1261        "powershell" => vec![
1262            "-NoProfile",
1263            "-Command",
1264            "$PSVersionTable.PSVersion.ToString()",
1265        ],
1266        "elvish" => vec!["-version"],
1267        _ => vec!["--version"],
1268    };
1269
1270    let output = std::process::Command::new(shell_path)
1271        .args(&args)
1272        .output()
1273        .or_else(|_| {
1274            // If the full path failed (e.g. SHELL was set to an invalid path),
1275            // try running by the shell name (searching the system PATH)
1276            std::process::Command::new(shell_name).args(&args).output()
1277        })
1278        .ok()?;
1279
1280    let stdout = String::from_utf8_lossy(&output.stdout).to_string();
1281    let stderr = String::from_utf8_lossy(&output.stderr).to_string();
1282
1283    // Some shells or errors might output to stderr
1284    let full_output = format!("{}\n{}", stdout, stderr);
1285
1286    parse_shell_version(shell_name, &full_output)
1287}
1288
1289/// Parses the output of a shell's version flag to find the version string.
1290fn parse_shell_version(shell_name: &str, output: &str) -> Option<String> {
1291    let output_trimmed = output.trim();
1292    if output_trimmed.is_empty() {
1293        return None;
1294    }
1295
1296    match shell_name {
1297        "bash" => {
1298            if let Some(pos) = output.find("version ") {
1299                let rest = &output[pos + 8..];
1300                let ver = rest
1301                    .split(|c: char| c.is_whitespace() || c == '(' || c == ',' || c == '-')
1302                    .next()
1303                    .unwrap_or("");
1304                if !ver.is_empty() {
1305                    return Some(ver.to_string());
1306                }
1307            }
1308        }
1309        "zsh" => {
1310            if let Some(pos) = output.find("zsh ") {
1311                let rest = &output[pos + 4..];
1312                let ver = rest
1313                    .split(|c: char| c.is_whitespace() || c == '(')
1314                    .next()
1315                    .unwrap_or("");
1316                if !ver.is_empty() {
1317                    return Some(ver.to_string());
1318                }
1319            }
1320        }
1321        "fish" => {
1322            if let Some(pos) = output.find("version ") {
1323                let rest = &output[pos + 8..];
1324                let ver = rest
1325                    .split(|c: char| c.is_whitespace() || c == '(' || c == ',')
1326                    .next()
1327                    .unwrap_or("");
1328                if !ver.is_empty() {
1329                    return Some(ver.to_string());
1330                }
1331            }
1332        }
1333        "nu" => {
1334            let ver = output_trimmed.split_whitespace().next().unwrap_or("");
1335            if !ver.is_empty() {
1336                return Some(ver.to_string());
1337            }
1338        }
1339        "pwsh" => {
1340            if let Some(pos) = output.find("PowerShell ") {
1341                let rest = &output[pos + 11..];
1342                let ver = rest.split_whitespace().next().unwrap_or("");
1343                if !ver.is_empty() {
1344                    return Some(ver.to_string());
1345                }
1346            }
1347        }
1348        "powershell" => {
1349            let ver = output_trimmed.split_whitespace().next().unwrap_or("");
1350            if !ver.is_empty() {
1351                return Some(ver.to_string());
1352            }
1353        }
1354        "elvish" => {
1355            let ver = output_trimmed.split_whitespace().next().unwrap_or("");
1356            if !ver.is_empty() {
1357                return Some(ver.to_string());
1358            }
1359        }
1360        "tcsh" => {
1361            if let Some(pos) = output.find("tcsh ") {
1362                let rest = &output[pos + 5..];
1363                let ver = rest.split_whitespace().next().unwrap_or("");
1364                if !ver.is_empty() {
1365                    return Some(ver.to_string());
1366                }
1367            }
1368        }
1369        _ => {
1370            // General generic version finder for "version " or any number pattern
1371            if let Some(pos) = output.to_lowercase().find("version ") {
1372                let rest = &output[pos + 8..];
1373                let ver = rest
1374                    .split(|c: char| c.is_whitespace() || c == '(' || c == ',' || c == '-')
1375                    .next()
1376                    .unwrap_or("");
1377                if !ver.is_empty() {
1378                    return Some(ver.to_string());
1379                }
1380            }
1381        }
1382    }
1383
1384    None
1385}
1386
1387#[allow(dead_code)]
1388fn parse_ini_key(content: &str, key: &str) -> Option<String> {
1389    for line in content.lines() {
1390        let line = line.trim();
1391        if line.starts_with('#') || line.starts_with(';') {
1392            continue;
1393        }
1394        if let Some(pos) = line.find('=') {
1395            let k = line[..pos].trim();
1396            if k == key {
1397                let v = line[pos + 1..].trim();
1398                let v = if (v.starts_with('"') && v.ends_with('"'))
1399                    || (v.starts_with('\'') && v.ends_with('\''))
1400                {
1401                    if v.len() >= 2 {
1402                        v[1..v.len() - 1].to_string()
1403                    } else {
1404                        v.to_string()
1405                    }
1406                } else {
1407                    v.to_string()
1408                };
1409                if !v.is_empty() {
1410                    return Some(v);
1411                }
1412            }
1413        }
1414    }
1415    None
1416}
1417
1418#[cfg(target_os = "linux")]
1419fn get_gtk_setting(key: &str) -> Option<String> {
1420    let home = dirs::home_dir()?;
1421    let paths = [
1422        home.join(".config/gtk-4.0/settings.ini"),
1423        home.join(".config/gtk-3.0/settings.ini"),
1424        home.join(".config/gtk-2.0/settings.ini"),
1425        home.join(".gtkrc-2.0"),
1426    ];
1427
1428    for path in &paths {
1429        if path.exists() {
1430            if let Ok(contents) = std::fs::read_to_string(path) {
1431                if let Some(val) = parse_ini_key(&contents, key) {
1432                    return Some(val);
1433                }
1434            }
1435        }
1436    }
1437    None
1438}
1439
1440#[cfg(target_os = "linux")]
1441fn query_gsettings(schema: &str, key: &str) -> Option<String> {
1442    let output = std::process::Command::new("gsettings")
1443        .args(["get", schema, key])
1444        .output()
1445        .ok()?;
1446    if output.status.success() {
1447        let val = String::from_utf8_lossy(&output.stdout).trim().to_string();
1448        let val = val.trim_matches('\'').trim_matches('"').to_string();
1449        if !val.is_empty() && val != "''" && val != "\"\"" {
1450            return Some(val);
1451        }
1452    }
1453    None
1454}
1455
1456#[cfg(target_os = "linux")]
1457fn get_kde_setting(key: &str) -> Option<String> {
1458    let home = dirs::home_dir()?;
1459    let path = home.join(".config/kdeglobals");
1460    if path.exists() {
1461        if let Ok(contents) = std::fs::read_to_string(path) {
1462            return parse_ini_key(&contents, key);
1463        }
1464    }
1465    None
1466}
1467
1468#[cfg(target_os = "linux")]
1469fn detect_ui_theme_and_fonts() -> (
1470    Option<String>,
1471    Option<String>,
1472    Option<String>,
1473    Option<String>,
1474) {
1475    // GTK Settings
1476    let gtk_theme = get_gtk_setting("gtk-theme-name")
1477        .or_else(|| query_gsettings("org.gnome.desktop.interface", "gtk-theme"));
1478    let gtk_icons = get_gtk_setting("gtk-icon-theme-name")
1479        .or_else(|| query_gsettings("org.gnome.desktop.interface", "icon-theme"));
1480    let gtk_cursor = get_gtk_setting("gtk-cursor-theme-name")
1481        .or_else(|| query_gsettings("org.gnome.desktop.interface", "cursor-theme"));
1482    let gtk_font = get_gtk_setting("gtk-font-name")
1483        .or_else(|| query_gsettings("org.gnome.desktop.interface", "font-name"));
1484
1485    // KDE/Qt Settings
1486    let qt_theme = get_kde_setting("widgetStyle").or_else(|| get_kde_setting("ColorScheme"));
1487    let qt_icons = get_kde_setting("iconTheme");
1488    let qt_cursor = {
1489        let home = dirs::home_dir();
1490        home.and_then(|h| {
1491            let path = h.join(".config/kcminputrc");
1492            if path.exists() {
1493                std::fs::read_to_string(path)
1494                    .ok()
1495                    .and_then(|contents| parse_ini_key(&contents, "theme"))
1496            } else {
1497                None
1498            }
1499        })
1500    };
1501    let qt_font = get_kde_setting("font").map(|f| {
1502        let parts: Vec<&str> = f.split(',').collect();
1503        if parts.len() >= 2 {
1504            let name = parts[0].trim();
1505            let size = parts[1].trim();
1506            format!("{} ({}pt)", name, size)
1507        } else {
1508            f
1509        }
1510    });
1511
1512    // Check which desktop environment is in use to prioritize formatting
1513    let de = std::env::var("XDG_CURRENT_DESKTOP")
1514        .or_else(|_| std::env::var("DESKTOP_SESSION"))
1515        .unwrap_or_default()
1516        .to_lowercase();
1517
1518    let is_kde = de.contains("kde") || de.contains("plasma");
1519
1520    let theme = if is_kde {
1521        match (qt_theme, gtk_theme) {
1522            (Some(qt), Some(gt)) => Some(format!("{} [Qt], {} [GTK]", qt, gt)),
1523            (Some(qt), None) => Some(format!("{} [Qt]", qt)),
1524            (None, Some(gt)) => Some(format!("{} [GTK]", gt)),
1525            (None, None) => None,
1526        }
1527    } else {
1528        match (gtk_theme, qt_theme) {
1529            (Some(gt), Some(qt)) => Some(format!("{} [GTK], {} [Qt]", gt, qt)),
1530            (Some(gt), None) => Some(format!("{} [GTK]", gt)),
1531            (None, Some(qt)) => Some(format!("{} [Qt]", qt)),
1532            (None, None) => None,
1533        }
1534    };
1535
1536    let icons = if is_kde {
1537        match (qt_icons, gtk_icons) {
1538            (Some(qi), Some(gi)) => Some(format!("{} [Qt], {} [GTK]", qi, gi)),
1539            (Some(qi), None) => Some(format!("{} [Qt]", qi)),
1540            (None, Some(gi)) => Some(format!("{} [GTK]", gi)),
1541            (None, None) => None,
1542        }
1543    } else {
1544        match (gtk_icons, qt_icons) {
1545            (Some(gi), Some(qi)) => Some(format!("{} [GTK], {} [Qt]", gi, qi)),
1546            (Some(gi), None) => Some(format!("{} [GTK]", gi)),
1547            (None, Some(qi)) => Some(format!("{} [Qt]", qi)),
1548            (None, None) => None,
1549        }
1550    };
1551
1552    let cursor = if is_kde {
1553        match (qt_cursor, gtk_cursor) {
1554            (Some(qc), Some(gc)) => Some(format!("{} [Qt], {} [GTK]", qc, gc)),
1555            (Some(qc), None) => Some(format!("{} [Qt]", qc)),
1556            (None, Some(gc)) => Some(format!("{} [GTK]", gc)),
1557            (None, None) => None,
1558        }
1559    } else {
1560        match (gtk_cursor, qt_cursor) {
1561            (Some(gc), Some(qc)) => Some(format!("{} [GTK], {} [Qt]", gc, qc)),
1562            (Some(gc), None) => Some(format!("{} [GTK]", gc)),
1563            (None, Some(qc)) => Some(format!("{} [Qt]", qc)),
1564            (None, None) => None,
1565        }
1566    };
1567
1568    let font = if is_kde {
1569        match (qt_font, gtk_font) {
1570            (Some(qf), Some(gf)) => Some(format!("{} [Qt], {} [GTK]", qf, gf)),
1571            (Some(qf), None) => Some(format!("{} [Qt]", qf)),
1572            (None, Some(gf)) => Some(format!("{} [GTK]", gf)),
1573            (None, None) => None,
1574        }
1575    } else {
1576        match (gtk_font, qt_font) {
1577            (Some(gf), Some(qf)) => Some(format!("{} [GTK], {} [Qt]", gf, qf)),
1578            (Some(gf), None) => Some(format!("{} [GTK]", gf)),
1579            (None, Some(qf)) => Some(format!("{} [Qt]", qf)),
1580            (None, None) => None,
1581        }
1582    };
1583
1584    (theme, icons, cursor, font)
1585}
1586
1587#[cfg(target_os = "macos")]
1588fn detect_ui_theme_and_fonts() -> (
1589    Option<String>,
1590    Option<String>,
1591    Option<String>,
1592    Option<String>,
1593) {
1594    let interface_style = std::process::Command::new("defaults")
1595        .args(["read", "-g", "AppleInterfaceStyle"])
1596        .output()
1597        .ok()
1598        .and_then(|o| {
1599            if o.status.success() {
1600                Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
1601            } else {
1602                None
1603            }
1604        });
1605
1606    let theme = match interface_style {
1607        Some(style) => Some(format!("Aqua ({})", style)),
1608        None => Some("Aqua (Light)".to_string()),
1609    };
1610
1611    (theme, None, None, Some("San Francisco".to_string()))
1612}
1613
1614#[cfg(target_os = "windows")]
1615fn detect_ui_theme_and_fonts() -> (
1616    Option<String>,
1617    Option<String>,
1618    Option<String>,
1619    Option<String>,
1620) {
1621    let theme = {
1622        let apps_light = win_reg::get_reg_u32(
1623            win_reg::HKEY_CURRENT_USER,
1624            "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
1625            "AppsUseLightTheme",
1626        );
1627
1628        let apps_dark = apps_light.map(|val| val == 0);
1629
1630        match apps_dark {
1631            Some(true) => Some("Dark".to_string()),
1632            Some(false) => Some("Light".to_string()),
1633            None => {
1634                let output = std::process::Command::new("reg")
1635                    .args([
1636                        "query",
1637                        r"HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
1638                        "/v",
1639                        "AppsUseLightTheme",
1640                    ])
1641                    .output()
1642                    .ok();
1643
1644                let cmd_dark = output.and_then(|o| {
1645                    if o.status.success() {
1646                        let s = String::from_utf8_lossy(&o.stdout);
1647                        if s.contains("0x0") {
1648                            Some(true)
1649                        } else if s.contains("0x1") {
1650                            Some(false)
1651                        } else {
1652                            None
1653                        }
1654                    } else {
1655                        None
1656                    }
1657                });
1658
1659                match cmd_dark {
1660                    Some(true) => Some("Dark".to_string()),
1661                    Some(false) => Some("Light".to_string()),
1662                    None => Some("Unknown".to_string()),
1663                }
1664            }
1665        }
1666    };
1667
1668    (theme, None, None, Some("Segoe UI".to_string()))
1669}
1670
1671#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1672fn detect_ui_theme_and_fonts() -> (
1673    Option<String>,
1674    Option<String>,
1675    Option<String>,
1676    Option<String>,
1677) {
1678    (None, None, None, None)
1679}
1680
1681fn detect_terminal(sys: &System) -> Option<String> {
1682    if let Ok(prog) = std::env::var("TERM_PROGRAM") {
1683        if !prog.is_empty() {
1684            return Some(prog);
1685        }
1686    }
1687    if let Ok(prog) = std::env::var("TERMINAL_EMULATOR") {
1688        if !prog.is_empty() {
1689            return Some(prog);
1690        }
1691    }
1692    if std::env::var("ALACRITTY_LOG").is_ok() || std::env::var("ALACRITTY_WINDOW_ID").is_ok() {
1693        return Some("alacritty".to_string());
1694    }
1695
1696    let current_pid = sysinfo::Pid::from_u32(std::process::id());
1697    let mut current_proc = sys.process(current_pid);
1698
1699    let known_terms = [
1700        "kitty",
1701        "alacritty",
1702        "wezterm",
1703        "gnome-terminal",
1704        "konsole",
1705        "iterm2",
1706        "Terminal",
1707        "rio",
1708        "foot",
1709        "tilix",
1710        "xfce4-terminal",
1711        "terminator",
1712        "st",
1713        "urxvt",
1714        "ptyxis",
1715    ];
1716
1717    let mut depth = 0;
1718    while let Some(proc) = current_proc {
1719        if depth > 5 {
1720            break;
1721        }
1722        let name = proc.name().to_string_lossy().to_lowercase();
1723
1724        for term in &known_terms {
1725            if name == *term || name.ends_with(term) || (name.contains(term) && term.len() > 3) {
1726                return Some(term.to_string());
1727            }
1728        }
1729
1730        if let Some(parent_pid) = proc.parent() {
1731            current_proc = sys.process(parent_pid);
1732        } else {
1733            break;
1734        }
1735        depth += 1;
1736    }
1737
1738    if let Ok(term) = std::env::var("TERM") {
1739        if term != "xterm-256color" && term != "xterm" && term != "linux" && term != "cygwin" {
1740            if let Some(stripped) = term.strip_prefix("xterm-") {
1741                return Some(stripped.to_string());
1742            }
1743            return Some(term);
1744        }
1745    }
1746
1747    None
1748}
1749
1750fn get_default_monospace_font() -> Option<String> {
1751    #[cfg(target_os = "linux")]
1752    {
1753        if let Ok(output) = std::process::Command::new("fc-match")
1754            .arg("monospace")
1755            .output()
1756        {
1757            if output.status.success() {
1758                let s = String::from_utf8_lossy(&output.stdout);
1759                if let Some(start) = s.find('"') {
1760                    if let Some(end) = s[start + 1..].find('"') {
1761                        return Some(s[start + 1..start + 1 + end].to_string());
1762                    }
1763                }
1764            }
1765        }
1766        return None;
1767    }
1768
1769    #[cfg(target_os = "macos")]
1770    {
1771        return Some("SF Mono".to_string());
1772    }
1773
1774    #[cfg(target_os = "windows")]
1775    {
1776        return Some("Consolas".to_string());
1777    }
1778
1779    #[allow(unreachable_code)]
1780    None
1781}
1782
1783fn detect_terminal_font(terminal: Option<&str>) -> Option<String> {
1784    let term = terminal?;
1785    let term_lower = term.to_lowercase();
1786    let home = dirs::home_dir()?;
1787
1788    if term_lower.contains("kitty") {
1789        let conf_path = home.join(".config/kitty/kitty.conf");
1790        if let Ok(content) = std::fs::read_to_string(&conf_path) {
1791            let mut family = None;
1792            let mut size = None;
1793            for line in content.lines() {
1794                let line = line.trim();
1795                if line.starts_with("font_family") {
1796                    let parts: Vec<&str> = line.split_whitespace().collect();
1797                    if parts.len() >= 2 {
1798                        family = Some(parts[1..].join(" "));
1799                    }
1800                } else if line.starts_with("font_size") {
1801                    let parts: Vec<&str> = line.split_whitespace().collect();
1802                    if parts.len() >= 2 {
1803                        size = Some(parts[1].to_string());
1804                    }
1805                }
1806            }
1807            match (family, size) {
1808                (Some(f), Some(s)) => return Some(format!("{} ({})", f, s)),
1809                (Some(f), None) => return Some(f),
1810                (None, Some(s)) => {
1811                    let fallback =
1812                        get_default_monospace_font().unwrap_or_else(|| "Default".to_string());
1813                    return Some(format!("{} ({})", fallback, s));
1814                }
1815                (None, None) => {}
1816            }
1817        }
1818    } else if term_lower.contains("alacritty") {
1819        let paths = [
1820            home.join(".config/alacritty/alacritty.toml"),
1821            home.join(".config/alacritty/alacritty.yml"),
1822            home.join(".alacritty.toml"),
1823            home.join(".alacritty.yml"),
1824        ];
1825        for path in paths {
1826            if let Ok(content) = std::fs::read_to_string(&path) {
1827                let mut family = None;
1828                let mut size = None;
1829                for line in content.lines() {
1830                    let line = line.trim();
1831                    if line.starts_with("family") {
1832                        if let Some(idx) = line.find('=') {
1833                            let val = line[idx + 1..].trim().trim_matches('"').trim_matches('\'');
1834                            family = Some(val.to_string());
1835                        } else if let Some(idx) = line.find(':') {
1836                            let val = line[idx + 1..].trim().trim_matches('"').trim_matches('\'');
1837                            family = Some(val.to_string());
1838                        }
1839                    } else if line.starts_with("size") {
1840                        if let Some(idx) = line.find('=') {
1841                            size = Some(line[idx + 1..].trim().to_string());
1842                        } else if let Some(idx) = line.find(':') {
1843                            size = Some(line[idx + 1..].trim().to_string());
1844                        }
1845                    }
1846                }
1847                match (family, size) {
1848                    (Some(f), Some(s)) => return Some(format!("{} ({})", f, s)),
1849                    (Some(f), None) => return Some(f),
1850                    (None, Some(s)) => {
1851                        let fallback =
1852                            get_default_monospace_font().unwrap_or_else(|| "Default".to_string());
1853                        return Some(format!("{} ({})", fallback, s));
1854                    }
1855                    (None, None) => {}
1856                }
1857            }
1858        }
1859    } else if term_lower.contains("wezterm") {
1860        let paths = [
1861            home.join(".wezterm.lua"),
1862            home.join(".config/wezterm/wezterm.lua"),
1863        ];
1864        for path in paths {
1865            if let Ok(content) = std::fs::read_to_string(&path) {
1866                let mut family = None;
1867                let mut size = None;
1868                for line in content.lines() {
1869                    if line.contains("wezterm.font") {
1870                        if let Some(start) = line.find("wezterm.font") {
1871                            let rest = &line[start..];
1872                            if let Some(quote1) = rest.find('\'').or(rest.find('"')) {
1873                                let quote_char = rest.chars().nth(quote1).unwrap();
1874                                if let Some(quote2) = rest[quote1 + 1..].find(quote_char) {
1875                                    family =
1876                                        Some(rest[quote1 + 1..quote1 + 1 + quote2].to_string());
1877                                }
1878                            }
1879                        }
1880                    }
1881                    if line.contains("font_size") {
1882                        if let Some(idx) = line.find('=') {
1883                            let val = line[idx + 1..].trim().trim_end_matches(',');
1884                            size = Some(val.to_string());
1885                        }
1886                    }
1887                }
1888                match (family, size) {
1889                    (Some(f), Some(s)) => return Some(format!("{} ({})", f, s)),
1890                    (Some(f), None) => return Some(f),
1891                    (None, Some(s)) => {
1892                        let fallback =
1893                            get_default_monospace_font().unwrap_or_else(|| "Default".to_string());
1894                        return Some(format!("{} ({})", fallback, s));
1895                    }
1896                    (None, None) => {}
1897                }
1898            }
1899        }
1900    } else if term_lower.contains("foot") {
1901        let conf_path = home.join(".config/foot/foot.ini");
1902        if let Ok(content) = std::fs::read_to_string(&conf_path) {
1903            for line in content.lines() {
1904                let line = line.trim();
1905                if line.starts_with("font=") {
1906                    let val = line.trim_start_matches("font=");
1907                    let parts: Vec<&str> = val.split(':').collect();
1908                    let family = parts[0].trim();
1909                    let mut size = None;
1910                    for part in &parts[1..] {
1911                        if part.starts_with("size=") {
1912                            size = Some(part.trim_start_matches("size=").trim());
1913                        }
1914                    }
1915                    if let Some(s) = size {
1916                        return Some(format!("{} ({})", family, s));
1917                    } else {
1918                        return Some(family.to_string());
1919                    }
1920                }
1921            }
1922        }
1923    } else if term_lower.contains("ptyxis") {
1924        #[cfg(target_os = "linux")]
1925        if let Ok(output) = std::process::Command::new("gsettings")
1926            .args(["get", "org.gnome.Ptyxis", "use-system-font"])
1927            .output()
1928        {
1929            if output.status.success() {
1930                let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
1931                if s == "false" {
1932                    if let Ok(font_out) = std::process::Command::new("gsettings")
1933                        .args(["get", "org.gnome.Ptyxis", "font-name"])
1934                        .output()
1935                    {
1936                        if font_out.status.success() {
1937                            let mut font_str =
1938                                String::from_utf8_lossy(&font_out.stdout).trim().to_string();
1939                            font_str = font_str.trim_matches('\'').to_string();
1940                            if !font_str.is_empty() {
1941                                if let Some(last_space) = font_str.rfind(' ') {
1942                                    let family = &font_str[..last_space];
1943                                    let size = &font_str[last_space + 1..];
1944                                    if size.chars().all(|c| c.is_ascii_digit() || c == '.') {
1945                                        return Some(format!("{} ({})", family, size));
1946                                    }
1947                                }
1948                                return Some(font_str);
1949                            }
1950                        }
1951                    }
1952                } else {
1953                    if let Ok(font_out) = std::process::Command::new("gsettings")
1954                        .args(["get", "org.gnome.desktop.interface", "monospace-font-name"])
1955                        .output()
1956                    {
1957                        if font_out.status.success() {
1958                            let mut font_str =
1959                                String::from_utf8_lossy(&font_out.stdout).trim().to_string();
1960                            font_str = font_str.trim_matches('\'').to_string();
1961                            if !font_str.is_empty() {
1962                                if let Some(last_space) = font_str.rfind(' ') {
1963                                    let family = &font_str[..last_space];
1964                                    let size = &font_str[last_space + 1..];
1965                                    if size.chars().all(|c| c.is_ascii_digit() || c == '.') {
1966                                        return Some(format!("{} ({})", family, size));
1967                                    }
1968                                }
1969                                return Some(font_str);
1970                            }
1971                        }
1972                    }
1973                    return get_default_monospace_font();
1974                }
1975            }
1976        }
1977    } else if term_lower.contains("konsole") {
1978        let rc_path = home.join(".config/konsolerc");
1979        let mut profile_name = "Default.profile".to_string();
1980        if let Ok(content) = std::fs::read_to_string(&rc_path) {
1981            for line in content.lines() {
1982                let line = line.trim();
1983                if line.starts_with("DefaultProfile=") {
1984                    profile_name = line.trim_start_matches("DefaultProfile=").to_string();
1985                    break;
1986                }
1987            }
1988        }
1989        let profile_path = home.join(".local/share/konsole").join(profile_name);
1990        if let Ok(content) = std::fs::read_to_string(&profile_path) {
1991            for line in content.lines() {
1992                let line = line.trim();
1993                if line.starts_with("Font=") {
1994                    let val = line.trim_start_matches("Font=");
1995                    let parts: Vec<&str> = val.split(',').collect();
1996                    if !parts.is_empty() {
1997                        let family = parts[0];
1998                        if parts.len() > 1 {
1999                            let size = parts[1];
2000                            return Some(format!("{} ({})", family, size));
2001                        }
2002                        return Some(family.to_string());
2003                    }
2004                }
2005            }
2006        }
2007        return get_default_monospace_font();
2008    }
2009
2010    #[cfg(target_os = "macos")]
2011    if term_lower == "iterm.app" || term_lower.contains("iterm2") {
2012        if let Ok(output) = std::process::Command::new("defaults")
2013            .args(["read", "com.googlecode.iterm2", "Normal Font"])
2014            .output()
2015        {
2016            if let Ok(s) = String::from_utf8(output.stdout) {
2017                let font = s.trim();
2018                if !font.is_empty() {
2019                    return Some(font.to_string());
2020                }
2021            }
2022        }
2023    }
2024
2025    None
2026}
2027
2028#[cfg(test)]
2029mod tests {
2030    use super::*;
2031
2032    #[test]
2033    fn test_parse_shell_version() {
2034        // Bash
2035        let bash_out = "GNU bash, version 5.2.15(1)-release (x86_64-pc-linux-gnu)\nCopyright (C) 2022 Free Software Foundation, Inc.";
2036        assert_eq!(
2037            parse_shell_version("bash", bash_out),
2038            Some("5.2.15".to_string())
2039        );
2040
2041        // Zsh
2042        let zsh_out = "zsh 5.9 (x86_64-pc-linux-gnu)";
2043        assert_eq!(parse_shell_version("zsh", zsh_out), Some("5.9".to_string()));
2044
2045        // Fish
2046        let fish_out = "fish, version 3.6.0";
2047        assert_eq!(
2048            parse_shell_version("fish", fish_out),
2049            Some("3.6.0".to_string())
2050        );
2051
2052        // Nushell
2053        let nu_out = "0.93.0";
2054        assert_eq!(
2055            parse_shell_version("nu", nu_out),
2056            Some("0.93.0".to_string())
2057        );
2058
2059        // PowerShell Core
2060        let pwsh_out = "PowerShell 7.4.1";
2061        assert_eq!(
2062            parse_shell_version("pwsh", pwsh_out),
2063            Some("7.4.1".to_string())
2064        );
2065
2066        // Windows PowerShell
2067        let powershell_out = "5.1.22621.2428";
2068        assert_eq!(
2069            parse_shell_version("powershell", powershell_out),
2070            Some("5.1.22621.2428".to_string())
2071        );
2072
2073        // Elvish
2074        let elvish_out = "0.20.1";
2075        assert_eq!(
2076            parse_shell_version("elvish", elvish_out),
2077            Some("0.20.1".to_string())
2078        );
2079
2080        // Tcsh
2081        let tcsh_out = "tcsh 6.24.10 (Astron) 2023-04-20 (x86_64-amd-linux) options wide,nls,dl,al,kan,sm,color,filec";
2082        assert_eq!(
2083            parse_shell_version("tcsh", tcsh_out),
2084            Some("6.24.10".to_string())
2085        );
2086
2087        // Generic fallback
2088        let custom_out = "CustomShell version 1.2.3-patch4";
2089        assert_eq!(
2090            parse_shell_version("custom", custom_out),
2091            Some("1.2.3".to_string())
2092        );
2093    }
2094
2095    #[test]
2096    fn test_is_real_camera() {
2097        assert!(!is_real_camera("Infrared Camera"));
2098        assert!(!is_real_camera("IR Camera"));
2099        assert!(!is_real_camera("Integrated IR Camera"));
2100        assert!(!is_real_camera("Depth Camera"));
2101        assert!(is_real_camera("FaceTime HD Camera"));
2102        assert!(is_real_camera("Integrated Camera"));
2103        assert!(is_real_camera("HD Webcam C920"));
2104    }
2105
2106    #[test]
2107    fn test_clean_camera_name() {
2108        assert_eq!(
2109            clean_camera_name("Integrated Camera: Real"),
2110            "Integrated Camera"
2111        );
2112        assert_eq!(
2113            clean_camera_name("Integrated Webcam: HD"),
2114            "Integrated Webcam"
2115        );
2116        assert_eq!(clean_camera_name("  HD Webcam C920  "), "HD Webcam C920");
2117        assert_eq!(
2118            clean_camera_name("FaceTime HD Camera"),
2119            "FaceTime HD Camera"
2120        );
2121    }
2122
2123    #[test]
2124    fn test_parse_macos_camera() {
2125        let sample = "Camera:\n\n    FaceTime HD Camera:\n\n      Model ID: UVC Camera VendorID_1452 ProductID_34068\n      Unique ID: 0x8020000005ac8514\n";
2126        assert_eq!(
2127            parse_macos_camera(sample),
2128            vec!["FaceTime HD Camera".to_string()]
2129        );
2130    }
2131
2132    #[test]
2133    fn test_parse_macos_gamepad() {
2134        let usb_sample = "USB 3.1 Bus:\n\n    Xbox Wireless Controller:\n\n      Product ID: 0x02e0\n      Vendor ID: 0x045e\n";
2135        let bt_sample = "Bluetooth:\n\n      Devices (Connected):\n          DualSense Wireless Controller:\n              Address: AA-BB-CC\n              Connected: Yes\n";
2136        let parsed = parse_macos_gamepad(usb_sample, bt_sample);
2137        assert_eq!(
2138            parsed,
2139            vec![
2140                "Xbox Wireless Controller".to_string(),
2141                "DualSense Wireless Controller".to_string()
2142            ]
2143        );
2144    }
2145}
2146
2147#[cfg(target_os = "windows")]
2148#[allow(clippy::upper_case_acronyms)]
2149mod win_reg {
2150    use std::ffi::OsStr;
2151    use std::os::windows::ffi::OsStrExt;
2152    use std::ptr;
2153
2154    type HKEY = *mut std::ffi::c_void;
2155    pub const HKEY_LOCAL_MACHINE: HKEY = 0x80000002 as HKEY;
2156    pub const HKEY_CURRENT_USER: HKEY = 0x80000001 as HKEY;
2157    const KEY_READ: u32 = 0x20019;
2158
2159    extern "system" {
2160        fn RegOpenKeyExW(
2161            hKey: HKEY,
2162            lpSubKey: *const u16,
2163            ulOptions: u32,
2164            samDesired: u32,
2165            phkResult: *mut HKEY,
2166        ) -> i32;
2167
2168        fn RegQueryValueExW(
2169            hKey: HKEY,
2170            lpValueName: *const u16,
2171            lpReserved: *mut u32,
2172            lpType: *mut u32,
2173            lpData: *mut u8,
2174            lpcbData: *mut u32,
2175        ) -> i32;
2176
2177        fn RegCloseKey(hKey: HKEY) -> i32;
2178    }
2179
2180    pub fn get_reg_string(hkey: HKEY, subkey: &str, value: &str) -> Option<String> {
2181        let subkey_w: Vec<u16> = OsStr::new(subkey).encode_wide().chain(Some(0)).collect();
2182        let value_w: Vec<u16> = OsStr::new(value).encode_wide().chain(Some(0)).collect();
2183        let mut hk: HKEY = ptr::null_mut();
2184
2185        let res = unsafe { RegOpenKeyExW(hkey, subkey_w.as_ptr(), 0, KEY_READ, &mut hk) };
2186        if res != 0 {
2187            return None;
2188        }
2189
2190        let mut size: u32 = 0;
2191        let mut ty: u32 = 0;
2192        unsafe {
2193            RegQueryValueExW(
2194                hk,
2195                value_w.as_ptr(),
2196                ptr::null_mut(),
2197                &mut ty,
2198                ptr::null_mut(),
2199                &mut size,
2200            );
2201        }
2202
2203        if size == 0 {
2204            unsafe {
2205                RegCloseKey(hk);
2206            }
2207            return None;
2208        }
2209
2210        let mut buf = vec![0u8; size as usize];
2211        let res = unsafe {
2212            RegQueryValueExW(
2213                hk,
2214                value_w.as_ptr(),
2215                ptr::null_mut(),
2216                &mut ty,
2217                buf.as_mut_ptr(),
2218                &mut size,
2219            )
2220        };
2221        unsafe {
2222            RegCloseKey(hk);
2223        }
2224
2225        if res == 0 {
2226            if ty == 1 || ty == 2 {
2227                // REG_SZ or REG_EXPAND_SZ
2228                let words = unsafe {
2229                    std::slice::from_raw_parts(buf.as_ptr() as *const u16, buf.len() / 2)
2230                };
2231                let len = words.iter().position(|&x| x == 0).unwrap_or(words.len());
2232                String::from_utf16(&words[..len]).ok()
2233            } else {
2234                None
2235            }
2236        } else {
2237            None
2238        }
2239    }
2240
2241    pub fn get_reg_u32(hkey: HKEY, subkey: &str, value: &str) -> Option<u32> {
2242        let subkey_w: Vec<u16> = OsStr::new(subkey).encode_wide().chain(Some(0)).collect();
2243        let value_w: Vec<u16> = OsStr::new(value).encode_wide().chain(Some(0)).collect();
2244        let mut hk: HKEY = ptr::null_mut();
2245
2246        let res = unsafe { RegOpenKeyExW(hkey, subkey_w.as_ptr(), 0, KEY_READ, &mut hk) };
2247        if res != 0 {
2248            return None;
2249        }
2250
2251        let mut size: u32 = 4;
2252        let mut ty: u32 = 0;
2253        let mut val: u32 = 0;
2254        let res = unsafe {
2255            RegQueryValueExW(
2256                hk,
2257                value_w.as_ptr(),
2258                ptr::null_mut(),
2259                &mut ty,
2260                &mut val as *mut u32 as *mut u8,
2261                &mut size,
2262            )
2263        };
2264        unsafe {
2265            RegCloseKey(hk);
2266        }
2267
2268        if res == 0 && ty == 4 {
2269            // REG_DWORD
2270            Some(val)
2271        } else {
2272            None
2273        }
2274    }
2275}