Skip to main content

retch_cli/
fetch.rs

1use crate::cli::Cli;
2use crate::config::Config;
3use chrono::TimeZone;
4use owo_colors::OwoColorize;
5use sysinfo::{Components, Disks, Networks, System, Users};
6
7#[derive(Debug)]
8pub struct SystemInfo {
9    pub os: String,
10    pub kernel: Option<String>,
11    pub hostname: Option<String>,
12    pub arch: String,
13    pub cpu: String,
14    pub cpu_cores: usize,
15    pub memory: String,
16    pub swap: String,
17    pub uptime: String,
18    pub processes: usize,
19    pub load_avg: Option<String>,
20    pub disks: Vec<String>,
21    pub temps: Vec<String>,
22    pub networks: Vec<String>,
23    pub boot_time: String,
24    pub battery: Option<String>,
25    pub shell: Option<String>,
26    pub terminal: Option<String>,
27    pub desktop: Option<String>,
28    pub cpu_freq: Option<String>,
29    pub users: usize,
30    pub gpu: Option<String>,
31    pub packages: Option<usize>,
32    pub current_user: Option<String>,
33}
34
35impl SystemInfo {
36    pub fn collect(_cli: &Cli, _config: &Config) -> anyhow::Result<Self> {
37        let mut sys = System::new_all();
38        sys.refresh_all();
39
40        let os = System::long_os_version()
41            .or_else(System::name)
42            .unwrap_or_else(|| "Unknown".to_string());
43
44        let kernel = System::kernel_version();
45        let hostname = System::host_name();
46
47        let cpu = sys
48            .cpus()
49            .first()
50            .map(|c| c.brand().to_string())
51            .unwrap_or_else(|| "Unknown CPU".to_string());
52
53        let cpu_cores = sys.cpus().len();
54
55        let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0;
56        let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0;
57        let memory = format!("{:.1} / {:.1} GB", used_mem, total_mem);
58
59        let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0;
60        let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0;
61        let swap = if total_swap > 0.0 {
62            format!("{:.1} / {:.1} GB", used_swap, total_swap)
63        } else {
64            "No swap".to_string()
65        };
66
67        let uptime = format!("{}s", System::uptime());
68
69        let disks = Disks::new_with_refreshed_list()
70            .iter()
71            .map(|d| {
72                let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
73                let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
74                let fs = d.file_system().to_string_lossy();
75                format!(
76                    "{} ({}): {:.1} GB free / {:.1} GB",
77                    d.mount_point().display(),
78                    fs,
79                    avail,
80                    total
81                )
82            })
83            .collect();
84
85        let battery: Option<String> = None; // Requires sysinfo "battery" feature
86
87        let arch = System::cpu_arch();
88
89        let processes = sys.processes().len();
90
91        let load_avg = {
92            let avg = System::load_average();
93            if avg.one > 0.0 || avg.five > 0.0 {
94                Some(format!(
95                    "{:.2}, {:.2}, {:.2}",
96                    avg.one, avg.five, avg.fifteen
97                ))
98            } else {
99                None
100            }
101        };
102
103        let temps = Components::new_with_refreshed_list()
104            .iter()
105            .filter_map(|c| {
106                c.temperature().and_then(|t| {
107                    if t > 0.0 {
108                        Some(format!("{}: {:.1}°C", c.label(), t))
109                    } else {
110                        None
111                    }
112                })
113            })
114            .collect();
115
116        let networks = Networks::new_with_refreshed_list()
117            .iter()
118            .map(|(name, data)| {
119                let rx = format_bytes(data.total_received());
120                let tx = format_bytes(data.total_transmitted());
121                let status = if data.total_received() > 0 || data.total_transmitted() > 0 {
122                    "Up".green().to_string()
123                } else {
124                    "Down".red().to_string()
125                };
126                format!("{} [{}] RX: {} TX: {}", name, status, rx, tx)
127            })
128            .collect();
129
130        let boot_timestamp = System::boot_time();
131        let boot_dt = chrono::Local
132            .timestamp_opt(boot_timestamp as i64, 0)
133            .single()
134            .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
135            .unwrap_or_else(|| boot_timestamp.to_string());
136        let boot_time = boot_dt;
137
138        // Environment-based info
139        let shell = std::env::var("SHELL").ok();
140        let terminal = std::env::var("TERM").ok();
141        let desktop = std::env::var("XDG_CURRENT_DESKTOP")
142            .or_else(|_| std::env::var("DESKTOP_SESSION"))
143            .ok();
144
145        // CPU frequency (first CPU)
146        let cpu_freq = sys
147            .cpus()
148            .first()
149            .map(|c| format!("{:.2} GHz", c.frequency() as f64 / 1000.0));
150
151        // Current logged in user
152        let current_user = std::env::var("USER").ok();
153
154        // Number of interactive users (UID >= 1000, excluding system accounts)
155        let users = Users::new_with_refreshed_list()
156            .iter()
157            .filter(|user| {
158                // UID is exposed via Display
159                let uid_str = user.id().to_string();
160                if let Ok(uid) = uid_str.parse::<u32>() {
161                    uid >= 1000
162                } else {
163                    false
164                }
165            })
166            .count();
167
168        Ok(Self {
169            os,
170            kernel,
171            hostname,
172            arch,
173            cpu,
174            cpu_cores,
175            memory,
176            swap,
177            uptime,
178            processes,
179            load_avg,
180            disks,
181            temps,
182            networks,
183            boot_time,
184            battery,
185            shell,
186            terminal,
187            desktop,
188            cpu_freq,
189            users,
190            gpu: detect_gpu(),
191            packages: detect_packages(),
192            current_user,
193        })
194    }
195}
196
197/// Basic GPU detection (Linux-focused for now)
198fn detect_gpu() -> Option<String> {
199    // Try common locations for GPU info
200    let paths = [
201        "/sys/class/drm/card0/device/vendor",
202        "/sys/class/drm/renderD128/device/vendor",
203    ];
204
205    for path in &paths {
206        if let Ok(vendor) = std::fs::read_to_string(path) {
207            if vendor.contains("0x10de") {
208                // NVIDIA
209                return Some("NVIDIA GPU".to_string());
210            } else if vendor.contains("0x1002") {
211                // AMD
212                return Some("AMD GPU".to_string());
213            } else if vendor.contains("0x8086") {
214                // Intel
215                return Some("Intel GPU".to_string());
216            }
217        }
218    }
219
220    // Fallback: try to read model name if available
221    if let Ok(model) = std::fs::read_to_string("/sys/class/drm/card0/device/model") {
222        let model = model.trim();
223        if !model.is_empty() {
224            return Some(model.to_string());
225        }
226    }
227
228    None
229}
230
231/// Count installed packages by inspecting package manager databases.
232/// Avoids calling any external tools.
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}