Skip to main content

rlmctl_core/
desktop.rs

1use common::Result;
2use std::fs;
3use std::os::unix::fs::PermissionsExt;
4use std::path::Path;
5
6/// Desktop application entry
7#[derive(Clone)]
8pub struct DesktopApp {
9    pub name: String,
10    pub exec: String,
11    pub is_cli: bool,
12}
13
14/// List installed applications from .desktop files
15pub fn list_applications() -> Result<Vec<DesktopApp>> {
16    let mut apps = Vec::new();
17    let dirs = [
18        "/usr/share/applications",
19        "/usr/local/share/applications",
20        "/var/lib/flatpak/exports/share/applications",
21    ];
22
23    // Also check user's local applications
24    let home_apps = dirs::data_dir().map(|d| d.join("applications"));
25
26    for dir in dirs.iter().map(Path::new).chain(home_apps.as_deref()) {
27        if let Ok(entries) = fs::read_dir(dir) {
28            for entry in entries.flatten() {
29                let path = entry.path();
30                if path.extension().is_some_and(|e| e == "desktop") {
31                    if let Some(app) = parse_desktop_file(&path) {
32                        apps.push(app);
33                    }
34                }
35            }
36        }
37    }
38
39    apps.sort_by_key(|a| a.name.to_lowercase());
40    apps.dedup_by(|a, b| a.name == b.name);
41    Ok(apps)
42}
43
44fn parse_desktop_file(path: &Path) -> Option<DesktopApp> {
45    let content = fs::read_to_string(path).ok()?;
46    let mut name = None;
47    let mut exec = None;
48    let mut no_display = false;
49    let mut in_desktop_entry = false;
50
51    for line in content.lines() {
52        let line = line.trim();
53
54        if line.starts_with('[') {
55            in_desktop_entry = line == "[Desktop Entry]";
56            continue;
57        }
58
59        if !in_desktop_entry {
60            continue;
61        }
62
63        if line.starts_with("Name=") && name.is_none() {
64            name = Some(line[5..].to_string());
65        } else if line.starts_with("Exec=") && exec.is_none() {
66            // Extract command, stripping field codes (%u, %F, etc.) but keeping arguments
67            let cmd_line = &line[5..];
68            let filtered: Vec<&str> = cmd_line
69                .split_whitespace()
70                .map(|arg| arg.trim_matches('"'))
71                .filter(|arg| !arg.is_empty() && !arg.starts_with('%'))
72                .collect();
73
74            // Handle env wrappers (e.g., "env VAR=val app args")
75            let command = if filtered.first() == Some(&"env") {
76                // Skip env and any VAR=val pairs
77                filtered
78                    .iter()
79                    .skip(1)
80                    .skip_while(|arg| arg.contains('='))
81                    .copied()
82                    .collect::<Vec<_>>()
83                    .join(" ")
84            } else {
85                filtered.join(" ")
86            };
87
88            if !command.is_empty() {
89                exec = Some(command);
90            }
91        } else if line == "NoDisplay=true" || line == "Hidden=true" {
92            no_display = true;
93        } else if line.starts_with("Type=") && line != "Type=Application" {
94            return None;
95        }
96    }
97
98    if no_display {
99        return None;
100    }
101
102    Some(DesktopApp {
103        name: name?,
104        exec: exec?,
105        is_cli: false,
106    })
107}
108
109/// Search PATH for executables matching a query
110pub fn search_cli_apps(query: &str) -> Vec<DesktopApp> {
111    if query.len() < 2 {
112        return Vec::new();
113    }
114
115    let query_lower = query.to_lowercase();
116    let mut apps = Vec::new();
117
118    if let Ok(path_var) = std::env::var("PATH") {
119        for dir in path_var.split(':') {
120            let dir_path = Path::new(dir);
121            if let Ok(entries) = fs::read_dir(dir_path) {
122                for entry in entries.flatten() {
123                    let name = entry.file_name().to_string_lossy().to_string();
124                    if !name.to_lowercase().contains(&query_lower) {
125                        continue;
126                    }
127
128                    // Check if executable
129                    if let Ok(meta) = entry.metadata() {
130                        if meta.is_file() && (meta.permissions().mode() & 0o111 != 0) {
131                            apps.push(DesktopApp {
132                                name: format!("{} (CLI)", name),
133                                exec: name,
134                                is_cli: true,
135                            });
136                        }
137                    }
138                }
139            }
140        }
141    }
142
143    apps.sort_by(|a, b| a.name.cmp(&b.name));
144    apps.dedup_by(|a, b| a.exec == b.exec);
145    apps
146}