Skip to main content

xdg_desktop_entries/
lib.rs

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}