Skip to main content

leenfetch_core/modules/linux/
packages.rs

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