1use common::Result;
2use std::fs;
3use std::os::unix::fs::PermissionsExt;
4use std::path::Path;
5
6#[derive(Clone)]
8pub struct DesktopApp {
9 pub name: String,
10 pub exec: String,
11 pub is_cli: bool,
12}
13
14pub 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 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 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 let command = if filtered.first() == Some(&"env") {
76 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
109pub 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 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}