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; 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 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 let cpu_freq = sys
147 .cpus()
148 .first()
149 .map(|c| format!("{:.2} GHz", c.frequency() as f64 / 1000.0));
150
151 let current_user = std::env::var("USER").ok();
153
154 let users = Users::new_with_refreshed_list()
156 .iter()
157 .filter(|user| {
158 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
197fn detect_gpu() -> Option<String> {
199 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 return Some("NVIDIA GPU".to_string());
210 } else if vendor.contains("0x1002") {
211 return Some("AMD GPU".to_string());
213 } else if vendor.contains("0x8086") {
214 return Some("Intel GPU".to_string());
216 }
217 }
218 }
219
220 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
231fn detect_packages() -> Option<usize> {
234 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 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 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 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 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
296fn 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}