1use crate::gpu;
10use chrono::TimeZone;
11use sysinfo::{Components, Disks, System, Users};
12
13#[derive(Debug, Default, Clone)]
18pub struct CollectOptions {
19 pub long: bool,
21}
22
23#[derive(Debug)]
28pub struct SystemInfo {
29 pub os: String,
31 pub kernel: Option<String>,
33 pub hostname: Option<String>,
35 pub arch: String,
37 pub cpu: String,
39 pub cpu_cores: usize,
41 pub memory: String,
43 pub swap: String,
45 pub uptime: String,
47 pub processes: usize,
49 pub load_avg: Option<String>,
51 pub disks: Vec<String>,
53 pub temps: Vec<String>,
55 pub networks: Vec<String>,
57 pub boot_time: String,
59 pub battery: Option<String>,
61 pub shell: Option<String>,
63 pub terminal: Option<String>,
65 pub desktop: Option<String>,
67 pub cpu_freq: Option<String>,
69 pub users: usize,
71 pub gpu: Vec<String>,
73 pub packages: Option<usize>,
75 pub current_user: Option<String>,
77 pub local_ip: Option<String>,
79 pub public_ip: Option<String>,
81 pub active_interface: Option<String>,
83 pub motherboard: Option<String>,
85 pub bios: Option<String>,
87 pub displays: Vec<String>,
89 pub audio: Option<String>,
91 pub wifi: Option<String>,
93 pub bluetooth: Option<String>,
95 pub ui_theme: Option<String>,
97 pub icons: Option<String>,
99 pub cursor: Option<String>,
101 pub font: Option<String>,
103 pub terminal_font: Option<String>,
105 pub camera: Vec<String>,
107 pub gamepad: Vec<String>,
109}
110
111impl SystemInfo {
112 pub fn collect(opts: CollectOptions) -> anyhow::Result<Self> {
117 let mut sys = System::new_all();
118 sys.refresh_all();
119
120 let os = System::long_os_version()
121 .or_else(System::name)
122 .unwrap_or_else(|| "Unknown".to_string());
123
124 let kernel = System::kernel_version();
125 let hostname = System::host_name();
126
127 let cpu = sys
128 .cpus()
129 .first()
130 .map(|c| c.brand().to_string())
131 .unwrap_or_else(|| "Unknown CPU".to_string());
132
133 let cpu_cores = sys.cpus().len();
134
135 let total_mem = sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
136 let used_mem = sys.used_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
137 let memory = format!("{:.1} / {:.1} GB", used_mem, total_mem);
138
139 let total_swap = sys.total_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
140 let used_swap = sys.used_swap() as f64 / 1024.0 / 1024.0 / 1024.0;
141 let swap = if total_swap > 0.0 {
142 format!("{:.1} / {:.1} GB", used_swap, total_swap)
143 } else {
144 "No swap".to_string()
145 };
146
147 let uptime = format!("{}s", System::uptime());
148
149 let disks_list = Disks::new_with_refreshed_list();
150 let disks: Vec<String> = if !opts.long {
151 let home = dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("/"));
152 let mut best_match: Option<&sysinfo::Disk> = None;
153 for disk in disks_list.iter() {
154 if home.starts_with(disk.mount_point()) {
155 if let Some(best) = best_match {
156 if disk.mount_point().components().count()
157 > best.mount_point().components().count()
158 {
159 best_match = Some(disk);
160 }
161 } else {
162 best_match = Some(disk);
163 }
164 }
165 }
166 let selected_disks = if let Some(best) = best_match {
167 vec![best]
168 } else {
169 disks_list.iter().collect::<Vec<_>>()
170 };
171
172 selected_disks
173 .iter()
174 .map(|d| {
175 let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
176 let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
177 let fs = d.file_system().to_string_lossy();
178 format!(
179 "{} ({}): {:.1} GB free / {:.1} GB",
180 d.mount_point().display(),
181 fs,
182 avail,
183 total
184 )
185 })
186 .collect()
187 } else {
188 disks_list
189 .iter()
190 .map(|d| {
191 let total = d.total_space() as f64 / 1024.0 / 1024.0 / 1024.0;
192 let avail = d.available_space() as f64 / 1024.0 / 1024.0 / 1024.0;
193 let fs = d.file_system().to_string_lossy();
194 format!(
195 "{} ({}): {:.1} GB free / {:.1} GB",
196 d.mount_point().display(),
197 fs,
198 avail,
199 total
200 )
201 })
202 .collect()
203 };
204
205 let battery = crate::battery::get_battery_info().map(|bat| {
206 let pct = bat.percentage;
207 let state = match bat.state {
208 crate::battery::BatteryState::Charging => "charging",
209 crate::battery::BatteryState::Discharging => "discharging",
210 crate::battery::BatteryState::Full => "full",
211 _ => "not charging",
212 };
213 let vendor = bat.vendor;
214 let model = bat.model;
215
216 let time_str = match bat.state {
218 crate::battery::BatteryState::Charging => bat.time_remaining.map(|d| {
219 let total_mins = d.as_secs() / 60;
220 let hours = total_mins / 60;
221 let mins = total_mins % 60;
222 if hours >= 24 {
223 let days = hours / 24;
224 let rem_hours = hours % 24;
225 format!("{}d {}h until full", days, rem_hours)
226 } else if hours > 0 {
227 format!("{}h {}m until full", hours, mins)
228 } else {
229 format!("{}m until full", mins)
230 }
231 }),
232 crate::battery::BatteryState::Discharging => bat.time_remaining.map(|d| {
233 let total_mins = d.as_secs() / 60;
234 let hours = total_mins / 60;
235 let mins = total_mins % 60;
236 if hours >= 24 {
237 let days = hours / 24;
238 let rem_hours = hours % 24;
239 format!("{}d {}h remaining", days, rem_hours)
240 } else if hours > 0 {
241 format!("{}h {}m remaining", hours, mins)
242 } else {
243 format!("{}m remaining", mins)
244 }
245 }),
246 _ => None,
247 };
248
249 let mut parts = vec![state.to_string()];
250 if let Some(t) = time_str {
251 parts.insert(0, t);
252 }
253 if let Some(health) = bat.health {
254 if health < 99.0 {
255 parts.push(format!("{:.0}% health", health));
256 }
257 }
258
259 let base = format!("{:.0}% ({})", pct, parts.join(", "));
260
261 match (vendor, model) {
262 (Some(v), Some(m)) => format!("{} [{} {}]", base, v, m),
263 (Some(v), None) => format!("{} [{}]", base, v),
264 _ => base,
265 }
266 });
267
268 let arch = System::cpu_arch();
269
270 let processes = sys.processes().len();
271
272 let load_avg = {
273 let avg = System::load_average();
274 if avg.one > 0.0 || avg.five > 0.0 {
275 Some(format!(
276 "{:.2}, {:.2}, {:.2}",
277 avg.one, avg.five, avg.fifteen
278 ))
279 } else {
280 None
281 }
282 };
283
284 let (
286 gpu,
287 packages,
288 public_ip,
289 (local_ip, active_interface),
290 motherboard,
291 bios,
292 displays,
293 audio,
294 wifi,
295 bluetooth,
296 (ui_theme, icons, cursor, font),
297 camera,
298 gamepad,
299 ) = std::thread::scope(|s| {
300 let gpu_handle = s.spawn(|| {
301 gpu::detect_gpus()
302 .into_iter()
303 .map(|g| g.format())
304 .collect::<Vec<String>>()
305 });
306 let packages_handle = s.spawn(crate::packages::detect_packages);
307 let public_ip_handle = s.spawn(crate::network::detect_public_ip);
308 let network_ips_handle = s.spawn(crate::network::detect_active_interface_and_local_ip);
309 let motherboard_handle = s.spawn(crate::motherboard::detect_motherboard);
310 let bios_handle = s.spawn(crate::bios::detect_bios);
311 let displays_handle = s.spawn(crate::display::detect_displays);
312 let audio_handle = s.spawn(|| crate::audio::detect_audio(&sys));
313 let wifi_handle = s.spawn(crate::network::detect_wifi);
314 let bluetooth_handle = s.spawn(crate::bluetooth::detect_bluetooth);
315 let ui_theme_and_fonts_handle = s.spawn(crate::theme::detect_ui_theme_and_fonts);
316 let camera_handle = s.spawn(crate::camera::detect_camera);
317 let gamepad_handle = s.spawn(crate::gamepad::detect_gamepad);
318
319 (
320 gpu_handle.join().unwrap_or_default(),
321 packages_handle.join().ok().flatten(),
322 public_ip_handle.join().ok().flatten(),
323 network_ips_handle.join().unwrap_or((None, None)),
324 motherboard_handle.join().ok().flatten(),
325 bios_handle.join().ok().flatten(),
326 displays_handle.join().unwrap_or_default(),
327 audio_handle.join().ok().flatten(),
328 wifi_handle.join().ok().flatten(),
329 bluetooth_handle.join().ok().flatten(),
330 ui_theme_and_fonts_handle
331 .join()
332 .unwrap_or((None, None, None, None)),
333 camera_handle.join().unwrap_or_default(),
334 gamepad_handle.join().unwrap_or_default(),
335 )
336 });
337
338 let mut temps: Vec<String> = Components::new_with_refreshed_list()
339 .iter()
340 .filter_map(|c| {
341 c.temperature().and_then(|t| {
342 if t > 0.0 {
343 Some(format!("{}: {:.0}°C", c.label(), t))
344 } else {
345 None
346 }
347 })
348 })
349 .collect();
350
351 temps.sort_by(|a, b| {
353 let a_cpu = a.to_lowercase().contains("cpu") || a.to_lowercase().contains("core");
354 let b_cpu = b.to_lowercase().contains("cpu") || b.to_lowercase().contains("core");
355 b_cpu.cmp(&a_cpu)
356 });
357
358 let networks =
359 crate::network::detect_networks(active_interface.as_deref(), local_ip.as_deref());
360
361 let boot_timestamp = System::boot_time();
362 let boot_dt = chrono::Local
363 .timestamp_opt(boot_timestamp as i64, 0)
364 .single()
365 .map(|dt| dt.format("%Y-%m-%dT%H:%M:%S%:z").to_string())
366 .unwrap_or_else(|| boot_timestamp.to_string());
367 let boot_time = boot_dt;
368
369 let shell = crate::shell::detect_shell(&sys);
371 let terminal = crate::terminal::detect_terminal(&sys);
372 let terminal_font = crate::terminal::detect_terminal_font(terminal.as_deref());
373 let desktop = std::env::var("XDG_CURRENT_DESKTOP")
374 .or_else(|_| std::env::var("DESKTOP_SESSION"))
375 .ok();
376
377 let cpu_freq = sys
379 .cpus()
380 .first()
381 .map(|c| format!("{:.2} GHz", c.frequency() as f64 / 1000.0));
382
383 let current_user = std::env::var("USER").ok();
385
386 let users = Users::new_with_refreshed_list()
388 .iter()
389 .filter(|user| {
390 let uid_str = user.id().to_string();
392 if let Ok(uid) = uid_str.parse::<u32>() {
393 uid >= 1000
394 } else {
395 false
396 }
397 })
398 .count();
399
400 Ok(Self {
401 os,
402 kernel,
403 hostname,
404 arch,
405 cpu,
406 cpu_cores,
407 memory,
408 swap,
409 uptime,
410 processes,
411 load_avg,
412 disks,
413 temps,
414 networks,
415 boot_time,
416 battery,
417 shell,
418 terminal,
419 desktop,
420 cpu_freq,
421 users,
422 gpu,
423 packages,
424 current_user,
425 local_ip,
426 public_ip,
427 active_interface,
428 motherboard,
429 bios,
430 displays,
431 audio,
432 wifi,
433 bluetooth,
434 ui_theme,
435 icons,
436 cursor,
437 font,
438 terminal_font,
439 camera,
440 gamepad,
441 })
442 }
443}