Skip to main content

retch_cli/
fetch.rs

1use crate::cli::Cli;
2use crate::config::Config;
3use crate::gpu;
4use chrono::TimeZone;
5use owo_colors::OwoColorize;
6use sysinfo::{Components, Disks, Networks, System, Users};
7
8/// Comprehensive system information data structure.
9///
10/// This struct holds all the metrics collected from the system,
11/// ranging from OS details to hardware specs and network status.
12#[derive(Debug)]
13pub struct SystemInfo {
14    /// Operating system name and version.
15    pub os: String,
16    /// Kernel version.
17    pub kernel: Option<String>,
18    /// System hostname.
19    pub hostname: Option<String>,
20    /// CPU architecture (e.g., x86_64).
21    pub arch: String,
22    /// CPU model brand string.
23    pub cpu: String,
24    /// Total number of logical CPU cores.
25    pub cpu_cores: usize,
26    /// Formatted memory usage (Used / Total).
27    pub memory: String,
28    /// Formatted swap usage (Used / Total).
29    pub swap: String,
30    /// System uptime formatted as a duration.
31    pub uptime: String,
32    /// Number of currently running processes.
33    pub processes: usize,
34    /// Load average (1, 5, 15 minutes).
35    pub load_avg: Option<String>,
36    /// List of mounted disks with usage information.
37    pub disks: Vec<String>,
38    /// Hardware component temperatures.
39    pub temps: Vec<String>,
40    /// Network interface statistics and status.
41    pub networks: Vec<String>,
42    /// System boot time in ISO 8601 format.
43    pub boot_time: String,
44    /// Battery status (currently placeholder for future feature).
45    pub battery: Option<String>,
46    /// Path to the current user's shell.
47    pub shell: Option<String>,
48    /// Name of the terminal emulator in use.
49    pub terminal: Option<String>,
50    /// Detected desktop environment or window manager.
51    pub desktop: Option<String>,
52    /// Current CPU frequency (formatted).
53    pub cpu_freq: Option<String>,
54    /// Number of interactive users (UID >= 1000).
55    pub users: usize,
56    /// List of detected GPUs with model names.
57    pub gpu: Vec<String>,
58    /// Total count of installed packages across supported managers.
59    pub packages: Option<usize>,
60    /// Name of the user running the process.
61    pub current_user: Option<String>,
62}
63
64impl SystemInfo {
65    /// Collects system information using sysinfo and environment probes.
66    ///
67    /// This method aggregates data from the operating system, hardware,
68    /// and current user environment into a `SystemInfo` struct.
69    pub fn collect(_cli: &Cli, _config: &Config) -> anyhow::Result<Self> {
70        let mut sys = System::new_all();
71        sys.refresh_all();
72
73        let os = System::long_os_version()
74            .or_else(System::name)
75            .unwrap_or_else(|| "Unknown".to_string());
76
77        let kernel = System::kernel_version();
78        let hostname = System::host_name();
79
80        let cpu = sys
81            .cpus()
82            .first()
83            .map(|c| c.brand().to_string())
84            .unwrap_or_else(|| "Unknown CPU".to_string());
85
86        let cpu_cores = sys.cpus().len();
87
88        let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0;
89        let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0;
90        let memory = format!("{:.1} / {:.1} GB", used_mem, total_mem);
91
92        let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0;
93        let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0;
94        let swap = if total_swap > 0.0 {
95            format!("{:.1} / {:.1} GB", used_swap, total_swap)
96        } else {
97            "No swap".to_string()
98        };
99
100        let uptime = format!("{}s", System::uptime());
101
102        let disks = Disks::new_with_refreshed_list()
103            .iter()
104            .map(|d| {
105                let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
106                let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
107                let fs = d.file_system().to_string_lossy();
108                format!(
109                    "{} ({}): {:.1} GB free / {:.1} GB",
110                    d.mount_point().display(),
111                    fs,
112                    avail,
113                    total
114                )
115            })
116            .collect();
117
118        let battery: Option<String> = None; // Requires sysinfo "battery" feature
119
120        let arch = System::cpu_arch();
121
122        let processes = sys.processes().len();
123
124        let load_avg = {
125            let avg = System::load_average();
126            if avg.one > 0.0 || avg.five > 0.0 {
127                Some(format!(
128                    "{:.2}, {:.2}, {:.2}",
129                    avg.one, avg.five, avg.fifteen
130                ))
131            } else {
132                None
133            }
134        };
135
136        let temps = Components::new_with_refreshed_list()
137            .iter()
138            .filter_map(|c| {
139                c.temperature().and_then(|t| {
140                    if t > 0.0 {
141                        Some(format!("{}: {:.1}°C", c.label(), t))
142                    } else {
143                        None
144                    }
145                })
146            })
147            .collect();
148
149        let networks = Networks::new_with_refreshed_list()
150            .iter()
151            .map(|(name, data)| {
152                let rx = format_bytes(data.total_received());
153                let tx = format_bytes(data.total_transmitted());
154                let status = if data.total_received() > 0 || data.total_transmitted() > 0 {
155                    "Up".green().to_string()
156                } else {
157                    "Down".red().to_string()
158                };
159                format!("{} [{}] RX: {} TX: {}", name, status, rx, tx)
160            })
161            .collect();
162
163        let boot_timestamp = System::boot_time();
164        let boot_dt = chrono::Local
165            .timestamp_opt(boot_timestamp as i64, 0)
166            .single()
167            .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
168            .unwrap_or_else(|| boot_timestamp.to_string());
169        let boot_time = boot_dt;
170
171        // Environment-based info
172        let shell = std::env::var("SHELL").ok();
173        let terminal = std::env::var("TERM").ok();
174        let desktop = std::env::var("XDG_CURRENT_DESKTOP")
175            .or_else(|_| std::env::var("DESKTOP_SESSION"))
176            .ok();
177
178        // CPU frequency (first CPU)
179        let cpu_freq = sys
180            .cpus()
181            .first()
182            .map(|c| format!("{:.2} GHz", c.frequency() as f64 / 1000.0));
183
184        // Current logged in user
185        let current_user = std::env::var("USER").ok();
186
187        // Number of interactive users (UID >= 1000, excluding system accounts)
188        let users = Users::new_with_refreshed_list()
189            .iter()
190            .filter(|user| {
191                // UID is exposed via Display
192                let uid_str = user.id().to_string();
193                if let Ok(uid) = uid_str.parse::<u32>() {
194                    uid >= 1000
195                } else {
196                    false
197                }
198            })
199            .count();
200
201        Ok(Self {
202            os,
203            kernel,
204            hostname,
205            arch,
206            cpu,
207            cpu_cores,
208            memory,
209            swap,
210            uptime,
211            processes,
212            load_avg,
213            disks,
214            temps,
215            networks,
216            boot_time,
217            battery,
218            shell,
219            terminal,
220            desktop,
221            cpu_freq,
222            users,
223            gpu: gpu::detect_gpus().into_iter().map(|g| g.format()).collect(),
224            packages: detect_packages(),
225            current_user,
226        })
227    }
228}
229
230/// Count installed packages by inspecting package manager databases directly.
231///
232/// Supports Pacman (Arch), Dpkg (Debian), XBPS (Void), and RPM (Fedora/RHEL).
233fn detect_packages() -> Option<usize> {
234    // Arch / Manjaro
235    if let Ok(entries) = std::fs::read_dir("/var/lib/pacman/local") {
236        let count = entries.filter_map(|e| e.ok()).count();
237        if count > 0 {
238            return Some(count);
239        }
240    }
241
242    // Debian / Ubuntu
243    if let Ok(entries) = std::fs::read_dir("/var/lib/dpkg/info") {
244        let count = entries
245            .filter_map(|e| e.ok())
246            .filter(|e| e.path().extension().is_some_and(|ext| ext == "list"))
247            .count();
248        if count > 0 {
249            return Some(count);
250        }
251    }
252
253    // Gentoo
254    if let Ok(entries) = std::fs::read_dir("/var/db/pkg") {
255        let count: usize = entries
256            .filter_map(|e| e.ok())
257            .map(|e| {
258                std::fs::read_dir(e.path())
259                    .map(|d| d.filter(|_| true).count())
260                    .unwrap_or(0)
261            })
262            .sum();
263        if count > 0 {
264            return Some(count);
265        }
266    }
267
268    // Void Linux
269    if let Ok(entries) = std::fs::read_dir("/var/db/xbps") {
270        let count = entries
271            .filter_map(|e| e.ok())
272            .filter(|e| e.path().extension().is_some_and(|ext| ext == "plist"))
273            .count();
274        if count > 0 {
275            return Some(count);
276        }
277    }
278
279    // Fedora / RHEL / openSUSE - read from RPM SQLite database
280    let rpm_db = "/var/lib/rpm/rpmdb.sqlite";
281    if std::path::Path::new(rpm_db).exists() {
282        if let Ok(conn) = rusqlite::Connection::open(rpm_db) {
283            if let Ok(count) = conn.query_row("SELECT COUNT(*) FROM Packages", [], |row| {
284                row.get::<_, i64>(0)
285            }) {
286                if count > 0 {
287                    return Some(count as usize);
288                }
289            }
290        }
291    }
292
293    None
294}
295
296/// Format bytes into human-readable form (KB, MB, GB, etc.)
297fn format_bytes(bytes: u64) -> String {
298    const KB: u64 = 1024;
299    const MB: u64 = KB * 1024;
300    const GB: u64 = MB * 1024;
301
302    if bytes >= GB {
303        format!("{:.1} GB", bytes as f64 / GB as f64)
304    } else if bytes >= MB {
305        format!("{:.1} MB", bytes as f64 / MB as f64)
306    } else if bytes >= KB {
307        format!("{:.1} KB", bytes as f64 / KB as f64)
308    } else {
309        format!("{} B", bytes)
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_format_bytes() {
319        assert_eq!(format_bytes(500), "500 B");
320        assert_eq!(format_bytes(1024), "1.0 KB");
321        assert_eq!(format_bytes(1024 * 1024), "1.0 MB");
322        assert_eq!(format_bytes(1024 * 1024 * 1024), "1.0 GB");
323        assert_eq!(format_bytes(1536), "1.5 KB");
324    }
325}