1use std::{collections::VecDeque, env, fs, io::Result, process::Command};
2
3use glob::glob;
4use globset::Glob;
5use libmacchina::{
6 traits::GeneralReadout as _, traits::KernelReadout as _, traits::MemoryReadout as _,
7 traits::PackageReadout as _, GeneralReadout, KernelReadout, MemoryReadout, PackageReadout,
8};
9use pfetch_logo_parser::{parse_logo, Logo};
10
11#[derive(Debug)]
12pub enum PackageManager {
13 Pacman,
14 Dpkg,
15 Xbps,
16 Apk,
17 Rpm,
18 Flatpak,
19 Crux,
20 Guix,
21 Opkg,
22 Kiss,
23 Portage,
24 Pkgtool,
25 Nix,
26}
27
28pub fn total_packages(package_readout: &PackageReadout, skip_slow: bool) -> usize {
31 match env::consts::OS {
32 "linux" => {
33 let macchina_package_count: Vec<(String, usize)> = package_readout
34 .count_pkgs()
35 .iter()
36 .map(|(macchina_manager, count)| (macchina_manager.to_string(), *count))
37 .collect();
38 [
39 PackageManager::Pacman,
40 PackageManager::Dpkg,
41 PackageManager::Xbps,
42 PackageManager::Apk,
43 PackageManager::Rpm,
44 PackageManager::Flatpak,
45 PackageManager::Crux,
46 PackageManager::Guix,
47 PackageManager::Opkg,
48 PackageManager::Kiss,
49 PackageManager::Portage,
50 PackageManager::Pkgtool,
51 PackageManager::Nix,
52 ]
53 .iter()
54 .map(|mngr| packages(mngr, &macchina_package_count, skip_slow))
55 .sum()
56 }
57 _ => package_readout.count_pkgs().iter().map(|elem| elem.1).sum(),
58 }
59}
60
61fn get_macchina_package_count(
62 macchina_result: &[(String, usize)],
63 package_manager_name: &str,
64) -> Option<usize> {
65 macchina_result
66 .iter()
67 .find(|entry| entry.0 == package_manager_name)
68 .map(|entry| entry.1)
69}
70
71fn packages(
74 pkg_manager: &PackageManager,
75 macchina_package_count: &[(String, usize)],
76 skip_slow: bool,
77) -> usize {
78 match pkg_manager {
79 PackageManager::Pacman
82 | PackageManager::Flatpak
83 | PackageManager::Dpkg
84 | PackageManager::Xbps
85 | PackageManager::Apk
86 | PackageManager::Portage
87 | PackageManager::Nix
88 | PackageManager::Opkg => get_macchina_package_count(
89 macchina_package_count,
90 &format!("{pkg_manager:?}").to_lowercase(),
91 )
92 .unwrap_or(0),
93 PackageManager::Rpm => match get_macchina_package_count(
94 macchina_package_count,
95 &format!("{pkg_manager:?}").to_lowercase(),
96 ) {
97 Some(count) => count,
98 None => {
99 if !skip_slow {
100 run_and_count_lines("rpm", &["-qa"])
101 } else {
102 0
103 }
104 }
105 },
106 PackageManager::Guix => run_and_count_lines("guix", &["package", "--list-installed"]),
107 PackageManager::Crux => {
108 if check_if_command_exists("crux") {
109 run_and_count_lines("pkginfo", &["-i"])
110 } else {
111 0
112 }
113 }
114 PackageManager::Kiss => {
115 if check_if_command_exists("kiss") {
116 match glob("/var/db/kiss/installed/*/") {
117 Ok(files) => files.count(),
118 Err(_) => 0,
119 }
120 } else {
121 0
122 }
123 }
124 PackageManager::Pkgtool => {
125 if check_if_command_exists("pkgtool") {
126 match glob("/var/log/packages/*") {
127 Ok(files) => files.count(),
128 Err(_) => 0,
129 }
130 } else {
131 0
132 }
133 }
134 }
135}
136
137pub fn user_at_hostname(
138 general_readout: &GeneralReadout,
139 username_override: &Option<String>,
140 hostname_override: &Option<String>,
141) -> Option<String> {
142 let username = match username_override {
143 Some(username) => Ok(username.to_string()),
144 None => general_readout.username(),
145 };
146 let hostname = match hostname_override {
147 Some(hostname) => Ok(hostname.to_string()),
148 None => general_readout.hostname(),
149 };
150 if username.is_err() || hostname.is_err() {
151 None
152 } else {
153 Some(format!(
154 "{}@{}",
155 username.unwrap_or_default(),
156 hostname.unwrap_or_default()
157 ))
158 }
159}
160
161pub fn memory(memory_readout: &MemoryReadout) -> Option<String> {
162 let total_memory = memory_readout.total();
163 let used_memory = memory_readout.used();
164 if total_memory.is_err() || used_memory.is_err() {
165 None
166 } else {
167 Some(format!(
168 "{}M / {}M",
169 used_memory.unwrap() / 1024,
170 total_memory.unwrap() / 1024
171 ))
172 }
173}
174
175pub fn cpu(general_readout: &GeneralReadout) -> Option<String> {
176 general_readout.cpu_model_name().ok()
177}
178
179pub fn os(general_readout: &GeneralReadout) -> Option<String> {
180 match env::consts::OS {
181 "linux" => {
182 if dotenvy::var("PATH")
184 .unwrap_or_default()
185 .contains("/bedrock/cross/")
186 {
187 return Some("Bedrock Linux".to_string());
188 }
189 let content = os_release::OsRelease::new().ok()?;
190 let version = if !content.version.is_empty() {
191 content.version
192 } else {
193 content.version_id
194 };
195 if content.pretty_name.contains("Bazzite") {
197 return Some(format!("Bazzite {version}"));
198 }
199 if !version.is_empty() {
200 return Some(format!("{} {}", content.name, version));
201 }
202 Some(content.name)
203 }
204 _ => Some(general_readout.os_name().ok()?.replace("Unknown", "")),
205 }
206}
207
208pub fn kernel(kernel_readout: &KernelReadout) -> Option<String> {
209 kernel_readout.os_release().ok()
210}
211
212pub fn seconds_to_string(seconds: usize) -> String {
213 let days = seconds / 86400;
214 let hours = (seconds % 86400) / 3600;
215 let minutes = (seconds % 3600) / 60;
216
217 let mut result = String::with_capacity(10);
218
219 if days > 0 {
220 result.push_str(&format!("{}d", days));
221 }
222 if hours > 0 {
223 if !result.is_empty() {
224 result.push(' ');
225 }
226 result.push_str(&format!("{}h", hours));
227 }
228 if minutes > 0 || result.is_empty() {
229 if !result.is_empty() {
230 result.push(' ');
231 }
232 result.push_str(&format!("{}m", minutes));
233 }
234
235 result
236}
237
238pub fn uptime(general_readout: &GeneralReadout) -> Option<String> {
239 Some(seconds_to_string(general_readout.uptime().ok()?))
240}
241
242pub fn host(general_readout: &GeneralReadout) -> Option<String> {
243 match env::consts::OS {
244 "linux" => {
245 const BLACKLIST: &[&str] = &[
246 "To",
247 "Be",
248 "be",
249 "Filled",
250 "filled",
251 "By",
252 "by",
253 "O.E.M.",
254 "OEM",
255 "Not",
256 "Applicable",
257 "Specified",
258 "System",
259 "Product",
260 "Name",
261 "Version",
262 "Undefined",
263 "Default",
264 "string",
265 "INVALID",
266 "�",
267 "os",
268 "Type1ProductConfigId",
269 "",
270 ];
271
272 let product_name =
274 fs::read_to_string("/sys/devices/virtual/dmi/id/product_name").unwrap_or_default();
275 let product_name = product_name.trim();
276 let product_version = fs::read_to_string("/sys/devices/virtual/dmi/id/product_version")
277 .unwrap_or_default();
278 let product_version = product_version.trim();
279 let product_model =
280 fs::read_to_string("/sys/firmware/devicetree/base/model").unwrap_or_default();
281 let product_model = product_model.trim();
282
283 let final_str = format!("{product_name} {product_version} {product_model}")
284 .split(' ')
285 .filter(|word| !BLACKLIST.contains(word))
286 .collect::<Vec<_>>()
287 .join(" ");
288
289 let final_str = if final_str.is_empty() {
291 run_system_command("uname", &["-m"]).unwrap_or("Unknown".to_owned())
292 } else {
293 final_str
294 };
295 if final_str.is_empty() {
296 None
297 } else {
298 Some(final_str)
299 }
300 }
301 _ => general_readout
303 .machine()
304 .ok()
305 .or_else(|| general_readout.cpu_model_name().ok()),
306 }
307}
308
309fn parse_custom_logos(filename: &str) -> Vec<Option<Logo>> {
310 let file_contents = fs::read_to_string(filename).expect("Could not open custom logo file");
311 file_contents
312 .split(";;")
313 .map(|raw_logo| parse_logo(raw_logo).map(|(_, logo)| logo))
314 .collect::<Vec<_>>()
315}
316
317pub fn logo(logo_name: &str) -> Logo {
318 let (tux, included_logos) = pfetch_extractor::parse_logos!();
319 let mut logos: VecDeque<_> = included_logos.into();
320 if let Ok(filename) = dotenvy::var("PF_CUSTOM_LOGOS") {
321 for custom_logo in parse_custom_logos(&filename).into_iter().flatten() {
323 logos.insert(0, custom_logo.clone());
324 }
325 };
326 logos
327 .into_iter()
328 .find(|logo| {
329 logo.pattern.split('|').any(|glob| {
330 Glob::new(glob.trim())
331 .expect("Invalid logo pattern")
332 .compile_matcher()
333 .is_match(logo_name)
334 })
335 })
336 .unwrap_or(tux)
337}
338
339pub fn shell(general_readout: &GeneralReadout) -> Option<String> {
340 general_readout
341 .shell(
342 libmacchina::traits::ShellFormat::Relative,
343 libmacchina::traits::ShellKind::Default,
344 )
345 .ok()
346 .or_else(|| dotenvy::var("SHELL").ok())
347}
348
349pub fn editor() -> Option<String> {
350 env::var("VISUAL")
351 .or_else(|_| env::var("EDITOR"))
352 .ok()
353 .map(|editor| editor.trim().to_owned())
354}
355
356pub fn wm(general_readout: &GeneralReadout) -> Option<String> {
357 general_readout.window_manager().ok()
358}
359
360pub fn de(general_readout: &GeneralReadout) -> Option<String> {
361 general_readout
362 .desktop_environment()
363 .ok()
364 .or_else(|| dotenvy::var("XDG_CURRENT_DESKTOP").ok())
365}
366
367pub fn palette() -> String {
368 (1..7).fold("".to_string(), |a, e| a + &format!("\x1b[4{e}m ")) + "\x1b[0m"
369}
370
371fn run_system_command(command: &str, args: &[&str]) -> Result<String> {
372 let mut output =
373 String::from_utf8_lossy(&Command::new(command).args(args).output()?.stdout).into_owned();
374 output.truncate(output.trim_end().len());
375 Ok(output)
376}
377
378fn check_if_command_exists(command: &str) -> bool {
379 which::which(command).is_ok()
380}
381
382fn _system_command_error(command: &str, args: &[&str]) -> Result<String> {
383 let mut output =
384 String::from_utf8_lossy(&Command::new(command).args(args).output()?.stderr).into_owned();
385 output.truncate(output.trim_end().len());
386 Ok(output)
387}
388
389fn run_and_count_lines(command: &str, args: &[&str]) -> usize {
392 run_system_command(command, args)
393 .unwrap_or_default()
394 .lines()
395 .count()
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_seconds_to_string_0() {
404 assert_eq!(seconds_to_string(0), "0m".to_string());
405 }
406
407 #[test]
408 fn test_seconds_to_string_60() {
409 assert_eq!(seconds_to_string(60), "1m".to_string());
410 }
411
412 #[test]
413 fn test_seconds_to_string_3600() {
414 assert_eq!(seconds_to_string(3600), "1h".to_string());
415 }
416
417 #[test]
418 fn test_seconds_to_string_3660() {
419 assert_eq!(seconds_to_string(3660), "1h 1m".to_string());
420 }
421
422 #[test]
423 fn test_seconds_to_string_86400() {
424 assert_eq!(seconds_to_string(86400), "1d".to_string());
425 }
426
427 #[test]
428 fn test_seconds_to_string_90000() {
429 assert_eq!(seconds_to_string(90000), "1d 1h".to_string());
430 }
431
432 #[test]
433 fn test_seconds_to_string_86460() {
434 assert_eq!(seconds_to_string(86460), "1d 1m".to_string());
435 }
436
437 #[test]
438 fn test_seconds_to_string_90060() {
439 assert_eq!(seconds_to_string(90060), "1d 1h 1m".to_string());
440 }
441}
442