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#[derive(Debug)]
13pub struct SystemInfo {
14 pub os: String,
16 pub kernel: Option<String>,
18 pub hostname: Option<String>,
20 pub arch: String,
22 pub cpu: String,
24 pub cpu_cores: usize,
26 pub memory: String,
28 pub swap: String,
30 pub uptime: String,
32 pub processes: usize,
34 pub load_avg: Option<String>,
36 pub disks: Vec<String>,
38 pub temps: Vec<String>,
40 pub networks: Vec<String>,
42 pub boot_time: String,
44 pub battery: Option<String>,
46 pub shell: Option<String>,
48 pub terminal: Option<String>,
50 pub desktop: Option<String>,
52 pub cpu_freq: Option<String>,
54 pub users: usize,
56 pub gpu: Vec<String>,
58 pub packages: Option<usize>,
60 pub current_user: Option<String>,
62}
63
64impl SystemInfo {
65 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; 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 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 let cpu_freq = sys
180 .cpus()
181 .first()
182 .map(|c| format!("{:.2} GHz", c.frequency() as f64 / 1000.0));
183
184 let current_user = std::env::var("USER").ok();
186
187 let users = Users::new_with_refreshed_list()
189 .iter()
190 .filter(|user| {
191 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
230fn 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}
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}