Skip to main content

leenfetch_core/modules/linux/
packages.rs

1use std::fs;
2use std::path::Path;
3
4use crate::modules::enums::PackageShorthand;
5
6pub fn get_packages(shorthand: PackageShorthand) -> Option<String> {
7    let mut packages = 0u64;
8    let mut managers = vec![];
9    let mut manager_string = vec![];
10
11    // dpkg (Debian/Ubuntu)
12    if let Some(count) = count_dpkg_packages() {
13        packages += count;
14        managers.push(format!("{} ({})", count, "dpkg"));
15        manager_string.push("dpkg");
16    }
17
18    // pacman (Arch)
19    if let Some(count) = count_pacman_packages() {
20        packages += count;
21        managers.push(format!("{} ({})", count, "pacman"));
22        manager_string.push("pacman");
23    }
24
25    // rpm (RHEL/Fedora) - check multiple possible locations
26    if let Some(count) = count_rpm_packages() {
27        packages += count;
28        managers.push(format!("{} ({})", count, "rpm"));
29        manager_string.push("rpm");
30    }
31
32    // flatpak
33    if let Some(count) = count_flatpak_packages() {
34        packages += count;
35        managers.push(format!("{} ({})", count, "flatpak"));
36        manager_string.push("flatpak");
37    }
38
39    // snap - check if snapd is running via socket
40    if is_snapd_running() {
41        if let Some(count) = count_snap_packages() {
42            packages += count;
43            managers.push(format!("{} ({})", count, "snap"));
44            manager_string.push("snap");
45        }
46    }
47
48    if packages == 0 {
49        return None;
50    }
51
52    match shorthand {
53        PackageShorthand::Off => Some(format!("{} total", packages)),
54        PackageShorthand::On => Some(managers.join(", ")),
55        PackageShorthand::Tiny => Some(format!("{} ({})", packages, manager_string.join(", "))),
56    }
57}
58
59fn is_snapd_running() -> bool {
60    // Check for snapd socket instead of running ps
61    Path::new("/run/snapd.socket").exists() || Path::new("/var/run/snapd.socket").exists()
62}
63
64fn pkg_root() -> String {
65    std::env::var("LEENFETCH_PKG_ROOT").unwrap_or_default()
66}
67
68fn count_dpkg_packages() -> Option<u64> {
69    let root = pkg_root();
70    let status = fs::read_to_string(format!("{root}/var/lib/dpkg/status")).ok()?;
71    let count = status
72        .lines()
73        .filter(|line| line.starts_with("Package: "))
74        .count() as u64;
75    Some(count)
76}
77
78fn count_pacman_packages() -> Option<u64> {
79    let root = pkg_root();
80    let entries = fs::read_dir(format!("{root}/var/lib/pacman/local")).ok()?;
81    let count = entries
82        .filter_map(|entry| entry.ok())
83        .filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
84        .count() as u64;
85    Some(count)
86}
87
88fn count_rpm_packages() -> Option<u64> {
89    let root = pkg_root();
90    // Try /var/lib/rpm first (RPM DB)
91    if let Ok(db_path) = fs::read_dir(format!("{root}/var/lib/rpm")) {
92        // Count packages in RPM database
93        for entry in db_path.flatten() {
94            let name = entry.file_name();
95            if name.to_string_lossy().starts_with("Packages") {
96                // This is the RPM database - count entries
97                if let Ok(content) = fs::read_to_string(entry.path()) {
98                    return Some(content.lines().filter(|l| !l.is_empty()).count() as u64);
99                }
100            }
101        }
102    }
103
104    // Fallback: try to count from /var/cache/Packages for apt-based systems
105    if let Ok(entries) = fs::read_dir(format!("{root}/var/cache/apt")) {
106        let count = entries.filter_map(|e| e.ok()).count() as u64;
107        if count > 0 {
108            return Some(count);
109        }
110    }
111
112    None
113}
114
115fn count_flatpak_packages() -> Option<u64> {
116    let root = pkg_root();
117    // Check flatpak installation directories
118    let paths = [
119        format!("{root}/var/lib/flatpak/app"),
120        format!("{root}/home/.local/share/flatpak/app"),
121    ];
122
123    for path in &paths {
124        if let Ok(entries) = fs::read_dir(&path) {
125            let count = entries
126                .filter_map(|e| e.ok())
127                .filter(|e| e.path().is_dir())
128                .count() as u64;
129            if count > 0 {
130                return Some(count);
131            }
132        }
133    }
134
135    // Try system-wide installations
136    if let Ok(home) = std::env::var("HOME") {
137        let user_path = format!("{root}{home}/.local/share/flatpak/app");
138        if let Ok(entries) = fs::read_dir(&user_path) {
139            let count = entries
140                .filter_map(|e| e.ok())
141                .filter(|e| e.path().is_dir())
142                .count() as u64;
143            if count > 0 {
144                return Some(count);
145            }
146        }
147    }
148
149    None
150}
151
152fn count_snap_packages() -> Option<u64> {
153    let root = pkg_root();
154    // Read snap list from /var/lib/snapd/state.json or direct snap data
155    let snap_data_path = format!("{root}/var/lib/snapd/state.json");
156
157    if let Ok(content) = fs::read_to_string(&snap_data_path) {
158        // Try to parse JSON and count installed snaps
159        // Simplified: count "name" occurrences in the JSON
160        let count = content.matches("\"name\":").count() as u64;
161        if count > 0 {
162            return Some(count.saturating_sub(1)); // Subtract potential false positive
163        }
164    }
165
166    // Fallback: count snap directories
167    if let Ok(entries) = fs::read_dir(format!("{root}/snap")) {
168        let count = entries
169            .filter_map(|e| e.ok())
170            .filter(|e| e.path().is_dir() && e.file_name() != "snap")
171            .count() as u64;
172        if count > 0 {
173            return Some(count);
174        }
175    }
176
177    None
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::test_utils::EnvLock;
184
185    #[test]
186    fn returns_none_when_no_managers_found() {
187        let env_lock = EnvLock::acquire(&["LEENFETCH_PKG_ROOT"]);
188        env_lock.set_var("LEENFETCH_PKG_ROOT", "/nonexistent");
189        let result = get_packages(PackageShorthand::Off);
190        assert!(result.is_none());
191        drop(env_lock);
192    }
193}