Skip to main content

retch_sysinfo/
fetch.rs

1// SPDX-FileCopyrightText: 2026 Ken Tobias
2// SPDX-License-Identifier: GPL-3.0-or-later
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(crate::packages::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(crate::motherboard::detect_motherboard);
310            let bios_handle = s.spawn(crate::bios::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(crate::theme::detect_ui_theme_and_fonts);
316            let camera_handle = s.spawn(crate::camera::detect_camera);
317            let gamepad_handle = s.spawn(crate::gamepad::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 = crate::shell::detect_shell(&sys);
371        let terminal = crate::terminal::detect_terminal(&sys);
372        let terminal_font = crate::terminal::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}