1use crate::cli::Cli;
2use crate::config::Config;
3use chrono::TimeZone;
4use owo_colors::OwoColorize;
5use std::collections::HashSet;
6use std::fs;
7use sysinfo::{Components, Disks, Networks, System, Users};
8
9#[derive(Debug)]
14pub struct SystemInfo {
15 pub os: String,
17 pub kernel: Option<String>,
19 pub hostname: Option<String>,
21 pub arch: String,
23 pub cpu: String,
25 pub cpu_cores: usize,
27 pub memory: String,
29 pub swap: String,
31 pub uptime: String,
33 pub processes: usize,
35 pub load_avg: Option<String>,
37 pub disks: Vec<String>,
39 pub temps: Vec<String>,
41 pub networks: Vec<String>,
43 pub boot_time: String,
45 pub battery: Option<String>,
47 pub shell: Option<String>,
49 pub terminal: Option<String>,
51 pub desktop: Option<String>,
53 pub cpu_freq: Option<String>,
55 pub users: usize,
57 pub gpu: Vec<String>,
59 pub packages: Option<usize>,
61 pub current_user: Option<String>,
63}
64
65fn lookup_pci_device(vendor_id: &str, device_id: &str) -> Option<String> {
69 let vendor_id = vendor_id.trim_start_matches("0x").to_lowercase();
70 let device_id = device_id.trim_start_matches("0x").to_lowercase();
71
72 let paths = ["/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids"];
73
74 for path in &paths {
75 if let Ok(content) = fs::read_to_string(path) {
76 let mut in_vendor = false;
77 for line in content.lines() {
78 if line.starts_with('#') || line.is_empty() {
79 continue;
80 }
81
82 if !line.starts_with('\t') {
83 in_vendor = line.starts_with(&vendor_id);
85 } else if in_vendor && line.starts_with('\t') && !line.starts_with("\t\t") {
86 let trimmed = line.trim_start();
88 if trimmed.starts_with(&device_id) {
89 let name = trimmed[device_id.len()..].trim();
90 return Some(name.to_string());
91 }
92 }
93 }
94 }
95 }
96 None
97}
98
99impl SystemInfo {
100 pub fn collect(_cli: &Cli, _config: &Config) -> anyhow::Result<Self> {
105 let mut sys = System::new_all();
106 sys.refresh_all();
107
108 let os = System::long_os_version()
109 .or_else(System::name)
110 .unwrap_or_else(|| "Unknown".to_string());
111
112 let kernel = System::kernel_version();
113 let hostname = System::host_name();
114
115 let cpu = sys
116 .cpus()
117 .first()
118 .map(|c| c.brand().to_string())
119 .unwrap_or_else(|| "Unknown CPU".to_string());
120
121 let cpu_cores = sys.cpus().len();
122
123 let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0;
124 let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0;
125 let memory = format!("{:.1} / {:.1} GB", used_mem, total_mem);
126
127 let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0;
128 let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0;
129 let swap = if total_swap > 0.0 {
130 format!("{:.1} / {:.1} GB", used_swap, total_swap)
131 } else {
132 "No swap".to_string()
133 };
134
135 let uptime = format!("{}s", System::uptime());
136
137 let disks = Disks::new_with_refreshed_list()
138 .iter()
139 .map(|d| {
140 let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
141 let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
142 let fs = d.file_system().to_string_lossy();
143 format!(
144 "{} ({}): {:.1} GB free / {:.1} GB",
145 d.mount_point().display(),
146 fs,
147 avail,
148 total
149 )
150 })
151 .collect();
152
153 let battery: Option<String> = None; let arch = System::cpu_arch();
156
157 let processes = sys.processes().len();
158
159 let load_avg = {
160 let avg = System::load_average();
161 if avg.one > 0.0 || avg.five > 0.0 {
162 Some(format!(
163 "{:.2}, {:.2}, {:.2}",
164 avg.one, avg.five, avg.fifteen
165 ))
166 } else {
167 None
168 }
169 };
170
171 let temps = Components::new_with_refreshed_list()
172 .iter()
173 .filter_map(|c| {
174 c.temperature().and_then(|t| {
175 if t > 0.0 {
176 Some(format!("{}: {:.1}°C", c.label(), t))
177 } else {
178 None
179 }
180 })
181 })
182 .collect();
183
184 let networks = Networks::new_with_refreshed_list()
185 .iter()
186 .map(|(name, data)| {
187 let rx = format_bytes(data.total_received());
188 let tx = format_bytes(data.total_transmitted());
189 let status = if data.total_received() > 0 || data.total_transmitted() > 0 {
190 "Up".green().to_string()
191 } else {
192 "Down".red().to_string()
193 };
194 format!("{} [{}] RX: {} TX: {}", name, status, rx, tx)
195 })
196 .collect();
197
198 let boot_timestamp = System::boot_time();
199 let boot_dt = chrono::Local
200 .timestamp_opt(boot_timestamp as i64, 0)
201 .single()
202 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
203 .unwrap_or_else(|| boot_timestamp.to_string());
204 let boot_time = boot_dt;
205
206 let shell = std::env::var("SHELL").ok();
208 let terminal = std::env::var("TERM").ok();
209 let desktop = std::env::var("XDG_CURRENT_DESKTOP")
210 .or_else(|_| std::env::var("DESKTOP_SESSION"))
211 .ok();
212
213 let cpu_freq = sys
215 .cpus()
216 .first()
217 .map(|c| format!("{:.2} GHz", c.frequency() as f64 / 1000.0));
218
219 let current_user = std::env::var("USER").ok();
221
222 let users = Users::new_with_refreshed_list()
224 .iter()
225 .filter(|user| {
226 let uid_str = user.id().to_string();
228 if let Ok(uid) = uid_str.parse::<u32>() {
229 uid >= 1000
230 } else {
231 false
232 }
233 })
234 .count();
235
236 Ok(Self {
237 os,
238 kernel,
239 hostname,
240 arch,
241 cpu,
242 cpu_cores,
243 memory,
244 swap,
245 uptime,
246 processes,
247 load_avg,
248 disks,
249 temps,
250 networks,
251 boot_time,
252 battery,
253 shell,
254 terminal,
255 desktop,
256 cpu_freq,
257 users,
258 gpu: detect_gpu(),
259 packages: detect_packages(),
260 current_user,
261 })
262 }
263}
264
265fn detect_gpu() -> Vec<String> {
270 let mut gpus = Vec::new();
271 let mut seen_devices = HashSet::new();
272
273 if let Ok(entries) = std::fs::read_dir("/sys/class/drm") {
275 for entry in entries.flatten() {
276 let name = entry.file_name().into_string().unwrap_or_default();
277 if !name.starts_with("card") && !name.starts_with("renderD") {
278 continue;
279 }
280
281 let device_path = entry.path().join("device");
282 if let Ok(real_path) = std::fs::canonicalize(&device_path) {
283 if !seen_devices.insert(real_path) {
284 continue;
285 }
286
287 let vendor_id = fs::read_to_string(device_path.join("vendor"))
289 .unwrap_or_default()
290 .trim()
291 .to_string();
292 let device_id = fs::read_to_string(device_path.join("device"))
293 .unwrap_or_default()
294 .trim()
295 .to_string();
296
297 if vendor_id.is_empty() || device_id.is_empty() {
298 continue;
299 }
300
301 let mut gpu_name = lookup_pci_device(&vendor_id, &device_id).unwrap_or_else(|| {
302 if vendor_id.contains("10de") {
303 "NVIDIA GPU".to_string()
304 } else if vendor_id.contains("1002") {
305 "AMD GPU".to_string()
306 } else if vendor_id.contains("8086") {
307 "Intel GPU".to_string()
308 } else {
309 "Unknown GPU".to_string()
310 }
311 });
312
313 if vendor_id.contains("10de") {
315 if let Ok(pci_slot_path) = fs::read_link(&device_path) {
316 if let Some(slot_name) = pci_slot_path.file_name() {
317 let proc_info_path = format!(
318 "/proc/driver/nvidia/gpus/{}/information",
319 slot_name.to_string_lossy()
320 );
321 if let Ok(info) = fs::read_to_string(proc_info_path) {
322 for line in info.lines() {
323 if line.starts_with("Model:") {
324 gpu_name = line.replace("Model:", "").trim().to_string();
325 break;
326 }
327 }
328 }
329 }
330 }
331 }
332
333 let vram_path = device_path.join("mem_info_vram_total");
335 if let Ok(vram_str) = fs::read_to_string(vram_path) {
336 if let Ok(vram_bytes) = vram_str.trim().parse::<u64>() {
337 let vram_gb = vram_bytes as f64 / 1024.0 / 1024.0 / 1024.0;
338 if vram_gb >= 1.0 {
339 gpu_name = format!("{} ({:.0} GB)", gpu_name, vram_gb);
340 } else {
341 let vram_mb = vram_bytes / 1024 / 1024;
342 gpu_name = format!("{} ({} MB)", gpu_name, vram_mb);
343 }
344 }
345 }
346
347 gpus.push(gpu_name);
348 }
349 }
350 }
351
352 if gpus.is_empty() {
353 if let Ok(model) = fs::read_to_string("/sys/class/drm/card0/device/model") {
355 let model = model.trim();
356 if !model.is_empty() {
357 gpus.push(model.to_string());
358 }
359 }
360 }
361
362 gpus
363}
364
365fn detect_packages() -> Option<usize> {
369 if let Ok(entries) = std::fs::read_dir("/var/lib/pacman/local") {
371 let count = entries.filter_map(|e| e.ok()).count();
372 if count > 0 {
373 return Some(count);
374 }
375 }
376
377 if let Ok(entries) = std::fs::read_dir("/var/lib/dpkg/info") {
379 let count = entries
380 .filter_map(|e| e.ok())
381 .filter(|e| e.path().extension().is_some_and(|ext| ext == "list"))
382 .count();
383 if count > 0 {
384 return Some(count);
385 }
386 }
387
388 if let Ok(entries) = std::fs::read_dir("/var/db/pkg") {
390 let count: usize = entries
391 .filter_map(|e| e.ok())
392 .map(|e| {
393 std::fs::read_dir(e.path())
394 .map(|d| d.filter(|_| true).count())
395 .unwrap_or(0)
396 })
397 .sum();
398 if count > 0 {
399 return Some(count);
400 }
401 }
402
403 if let Ok(entries) = std::fs::read_dir("/var/db/xbps") {
405 let count = entries
406 .filter_map(|e| e.ok())
407 .filter(|e| e.path().extension().is_some_and(|ext| ext == "plist"))
408 .count();
409 if count > 0 {
410 return Some(count);
411 }
412 }
413
414 let rpm_db = "/var/lib/rpm/rpmdb.sqlite";
416 if std::path::Path::new(rpm_db).exists() {
417 if let Ok(conn) = rusqlite::Connection::open(rpm_db) {
418 if let Ok(count) = conn.query_row("SELECT COUNT(*) FROM Packages", [], |row| {
419 row.get::<_, i64>(0)
420 }) {
421 if count > 0 {
422 return Some(count as usize);
423 }
424 }
425 }
426 }
427
428 None
429}
430
431fn format_bytes(bytes: u64) -> String {
433 const KB: u64 = 1024;
434 const MB: u64 = KB * 1024;
435 const GB: u64 = MB * 1024;
436
437 if bytes >= GB {
438 format!("{:.1} GB", bytes as f64 / GB as f64)
439 } else if bytes >= MB {
440 format!("{:.1} MB", bytes as f64 / MB as f64)
441 } else if bytes >= KB {
442 format!("{:.1} KB", bytes as f64 / KB as f64)
443 } else {
444 format!("{} B", bytes)
445 }
446}