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