pfetch/
lib.rs

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
28/// Obtain the amount of installed packages on the system by checking all installed supported package
29/// managers and adding the amounts
30pub 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
71/// return the amount of packages installed with a given linux package manager
72/// Return `0` if the package manager is not installed
73fn packages(
74    pkg_manager: &PackageManager,
75    macchina_package_count: &[(String, usize)],
76    skip_slow: bool,
77) -> usize {
78    match pkg_manager {
79        // libmacchina has very fast implementations for most package managers, so we use them
80        // where we can, otherwise we fall back to method used by dylans version of pfetch
81        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            // check for Bedrock Linux
183            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            // check for Bazzite
196            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            // get device from system files
273            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            // if string is empty, display system architecture instead
290            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        // on non-linux systems, try general_readout.machine(), use cpu model name as fallback
302        _ => 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        // insert custom logos in front of incuded logos
322        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
389/// Return the amount of line the output of a system command produces
390/// Returns `0` if command fails
391fn 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