1use std::collections::HashMap;
2use std::path::Path;
3use std::result;
4
5pub type Result<T> = result::Result<T, Error>;
6pub type RawDesktopEntry = HashMap<String, HashMap<String, String>>;
7
8#[derive(Debug)]
9#[allow(unused)]
10
11pub enum Error {
12 IoError(std::io::Error),
13 FormatError(String),
14}
15
16#[derive(Debug, Clone)]
17#[allow(unused)]
18
19pub struct ApplicationDesktopEntry {
20 pub version: Option<String>,
21 pub name: String,
22 pub generic_name: Option<String>,
23 pub no_display: Option<bool>,
24 pub comment: Option<String>,
25 pub icon: Option<String>,
26 pub hidden: Option<bool>,
27 pub only_show_in: Option<String>,
28 pub not_show_in: Option<String>,
29 pub try_exec: Option<String>,
30 pub exec: Option<String>,
31 pub path: Option<String>,
32 pub terminal: Option<bool>,
33 pub actions: Option<String>,
34 pub mime_type: Option<String>,
35 pub categories: Option<String>,
36 pub keywords: Option<String>,
37 pub startup_notify: Option<bool>,
38 pub startup_wm_class: Option<String>,
39 pub prefers_non_default_gpu: Option<bool>,
40 pub single_main_window: Option<bool>,
41}
42
43#[derive(Debug, Clone)]
44#[allow(unused)]
45
46pub struct LinkDesktopEntry {
47 pub version: Option<String>,
48 pub name: String,
49 pub generic_name: Option<String>,
50 pub no_display: Option<bool>,
51 pub comment: Option<String>,
52 pub icon: Option<String>,
53 pub hidden: Option<bool>,
54 pub only_show_in: Option<String>,
55 pub not_show_in: Option<String>,
56 pub url: String,
57}
58
59#[derive(Debug, Clone)]
60#[allow(unused)]
61pub struct DirectoryDesktopEntry {
62 pub version: Option<String>,
63 pub name: String,
64 pub generic_name: Option<String>,
65 pub no_display: Option<bool>,
66 pub comment: Option<String>,
67 pub icon: Option<String>,
68 pub hidden: Option<bool>,
69 pub only_show_in: Option<String>,
70 pub not_show_in: Option<String>,
71}
72
73#[derive(Debug)]
74#[allow(unused)]
75
76pub enum DesktopEntryType {
77 Application(ApplicationDesktopEntry),
78 Link(LinkDesktopEntry),
79 Directory(DirectoryDesktopEntry),
80}
81
82pub fn parse_desktop_entry_raw<P: AsRef<Path>>(path: P) -> Result<RawDesktopEntry> {
83 let mut groups: RawDesktopEntry = HashMap::new();
84 let mut current_group: String = String::new();
85
86 let content = std::fs::read_to_string(path).map_err(|e| Error::IoError(e))?;
87
88 for line in content.lines() {
89 if line.is_empty() || line.starts_with('#') {
90 continue;
91 };
92
93 if line.starts_with('[') && line.ends_with(']') {
94 current_group = line[1..line.len() - 1].to_string();
95 continue;
96 }
97
98 if current_group.is_empty() {
99 return Err(Error::FormatError(
100 "Entry found outside of group".to_string(),
101 ));
102 }
103
104 let entry: Vec<&str> = line.splitn(2, '=').collect();
105
106 if entry.len() != 2 {
107 return Err(Error::FormatError("Entry not key/value".to_string()));
108 }
109
110 groups
111 .entry(current_group.clone())
112 .or_insert_with(|| HashMap::new())
113 .insert(entry[0].trim().to_string(), entry[1].trim().to_string());
114 }
115
116 Ok(groups)
117}
118
119pub fn parse_desktop_entry<P: AsRef<Path>>(path: P) -> Result<DesktopEntryType> {
120 match parse_desktop_entry_raw(path) {
121 Ok(raw_entry) => raw_entry.try_into(),
122 Err(error) => Err(error),
123 }
124}
125
126impl TryFrom<RawDesktopEntry> for DesktopEntryType {
127 type Error = Error;
128
129 fn try_from(value: RawDesktopEntry) -> result::Result<Self, Self::Error> {
130 let group = value.get("Desktop Entry").ok_or(Error::FormatError(
131 "Desktop entry group missing!".to_string(),
132 ))?;
133 match group
134 .get("Type")
135 .ok_or(Error::FormatError("Entry type missing!".to_string()))?
136 .as_str()
137 {
138 "Application" => {
139 return ApplicationDesktopEntry::try_from(group)
140 .map(|e| DesktopEntryType::Application(e));
141 }
142 "Link" => {
143 return LinkDesktopEntry::try_from(group).map(|e| DesktopEntryType::Link(e));
144 }
145 "Directory" => {
146 return DirectoryDesktopEntry::try_from(group)
147 .map(|e| DesktopEntryType::Directory(e));
148 }
149 unknown => return Err(Error::FormatError(format!("Unknown entry type {unknown}"))),
150 }
151 }
152}
153
154impl TryFrom<&HashMap<String, String>> for ApplicationDesktopEntry {
155 type Error = Error;
156
157 fn try_from(entry: &HashMap<String, String>) -> result::Result<Self, Self::Error> {
158 Ok(ApplicationDesktopEntry {
159 version: entry.get("Version").cloned(),
160 name: entry
161 .get("Name")
162 .ok_or(Error::FormatError(
163 "Missing required key 'Name'".to_string(),
164 ))?
165 .to_string(),
166 generic_name: entry.get("GenericName").cloned(),
167 no_display: entry
168 .get("NoDisplay")
169 .map(|value| value.parse().is_ok_and(|e| e)),
170 comment: entry.get("Comment").cloned(),
171 icon: entry.get("Icon").cloned(),
172 hidden: entry
173 .get("Hidden")
174 .map(|value| value.parse().is_ok_and(|e| e)),
175 only_show_in: entry.get("OnlyShowIn").cloned(),
176 not_show_in: entry.get("NotShowIn").cloned(),
177 try_exec: entry.get("TryExec").cloned(),
178 exec: entry.get("Exec").cloned(),
179 path: entry.get("Path").cloned(),
180 terminal: entry
181 .get("Terminal")
182 .map(|value| value.parse().is_ok_and(|e| e)),
183 actions: entry.get("Actions").cloned(),
184 mime_type: entry.get("MimeType").cloned(),
185 categories: entry.get("Categories").cloned(),
186 keywords: entry.get("Keywords").cloned(),
187 startup_notify: entry
188 .get("StartupNotify")
189 .map(|value| value.parse().is_ok_and(|e| e)),
190 startup_wm_class: entry.get("StartupWMClass").cloned(),
191 prefers_non_default_gpu: entry
192 .get("PrefersNonDefaultGPU")
193 .map(|value| value.parse().is_ok_and(|e| e)),
194 single_main_window: entry
195 .get("SingleMainWindow")
196 .map(|value| value.parse().is_ok_and(|e| e)),
197 })
198 }
199}
200
201impl TryFrom<&HashMap<String, String>> for LinkDesktopEntry {
202 type Error = Error;
203
204 fn try_from(entry: &HashMap<String, String>) -> result::Result<Self, Self::Error> {
205 Ok(LinkDesktopEntry {
206 version: entry.get("Version").cloned(),
207 name: entry
208 .get("Name")
209 .ok_or(Error::FormatError(
210 "Missing required key 'Name'".to_string(),
211 ))?
212 .to_string(),
213 generic_name: entry.get("GenericName").cloned(),
214 no_display: entry
215 .get("NoDisplay")
216 .map(|value| value.parse().is_ok_and(|e| e)),
217 comment: entry.get("Comment").cloned(),
218 icon: entry.get("Icon").cloned(),
219 hidden: entry
220 .get("Hidden")
221 .map(|value| value.parse().is_ok_and(|e| e)),
222 only_show_in: entry.get("OnlyShowIn").cloned(),
223 not_show_in: entry.get("NotShowIn").cloned(),
224 url: entry
225 .get("URL")
226 .ok_or(Error::FormatError("Missing required key 'URL'".to_string()))?
227 .to_string(),
228 })
229 }
230}
231
232impl TryFrom<&HashMap<String, String>> for DirectoryDesktopEntry {
233 type Error = Error;
234
235 fn try_from(entry: &HashMap<String, String>) -> result::Result<Self, Self::Error> {
236 Ok(DirectoryDesktopEntry {
237 version: entry.get("Version").cloned(),
238 name: entry
239 .get("Name")
240 .ok_or(Error::FormatError(
241 "Missing required key 'Name'".to_string(),
242 ))?
243 .to_string(),
244 generic_name: entry.get("GenericName").cloned(),
245 no_display: entry
246 .get("NoDisplay")
247 .map(|value| value.parse().is_ok_and(|e| e)),
248 comment: entry.get("Comment").cloned(),
249 icon: entry.get("Icon").cloned(),
250 hidden: entry
251 .get("Hidden")
252 .map(|value| value.parse().is_ok_and(|e| e)),
253 only_show_in: entry.get("OnlyShowIn").cloned(),
254 not_show_in: entry.get("NotShowIn").cloned(),
255 })
256 }
257}