Skip to main content

applications/
desktopapp.rs

1use crate::private_prelude::*;
2use configparser::ini::Ini;
3
4fn string_to_vec(value: Option<String>) -> Vec<String> {
5    value
6        .map(|value| {
7            value
8                .split(";")
9                .filter(|s| !s.is_empty())
10                .map(String::from)
11                .collect()
12        })
13        .unwrap_or_default()
14}
15pub(crate) struct DesktopApp {
16    pub(crate) app_id: String,
17    pub(crate) ini: Ini,
18    pub(crate) actions: Vec<Arc<Action>>,
19    pub(crate) name: String,
20}
21
22impl DesktopApp {
23    pub(crate) fn new(app_id: String, contents: String) -> Option<Self> {
24        let mut ini = Ini::new();
25        ini.set_comment_symbols(&['#']);
26
27        ini.read(contents).ok()?;
28
29        if ini
30            .getbool("Desktop Entry", "NoDisplay")
31            .unwrap_or(Some(false))
32            .unwrap_or(false)
33        {
34            return None;
35        }
36
37        if ini.get("Desktop Entry", "Type")? != "Application" {
38            return None;
39        };
40
41        let actions: Vec<Arc<Action>> = ini
42            .get("Desktop Entry", "Actions")
43            .map(|a| {
44                a.split(";")
45                    .filter(|a| !a.is_empty())
46                    .filter_map(|id| Action::new(String::from(id), ini.clone()))
47                    .map(Arc::new)
48                    .collect()
49            })
50            .unwrap_or_default();
51
52        let name = ini.get("Desktop Entry", "Name")?;
53
54        Some(Self {
55            app_id,
56            ini,
57            actions,
58            name,
59        })
60    }
61}
62
63/// A handle which represents a desktop application.
64#[derive(Clone)]
65pub struct DesktopAppHandle {
66    pub(crate) inner: Arc<DesktopApp>,
67    pub(crate) service: ApplicationService,
68}
69
70impl DesktopAppHandle {
71    fn get_value(&self, key: &str) -> Option<String> {
72        self.inner.ini.get("Desktop Entry", key)
73    }
74
75    fn get_value_locale(&self, key: &str) -> Option<String> {
76        // Locale matching with order:
77        // lang_COUNTRY@MODIFIER - lang_COUNTRY@MODIFIER, lang_COUNTRY, lang@MODIFIER, lang, default value
78        // lang_COUNTRY	- lang_COUNTRY, lang, default value
79        // lang@MODIFIER - lang@MODIFIER, lang, default value
80        // lang	- lang, default value
81        //
82        // See spec for more info: https://specifications.freedesktop.org/desktop-entry/latest/localized-keys.html
83
84        utils::get_locale_string(
85            &self.inner.ini,
86            "Desktop Entry",
87            key,
88            &self.service.inner.locale,
89        )
90    }
91
92    /// Returns the unique ID of the application.
93    pub fn app_id(&self) -> String {
94        self.inner.app_id.clone()
95    }
96
97    /// Returns the name of the application.
98    ///
99    /// For example: `firefox`.
100    pub fn name(&self) -> String {
101        self.inner.name.clone()
102    }
103
104    /// Returns the localized name of the application.
105    pub fn name_locale(&self) -> String {
106        self.get_value_locale("Name").unwrap_or_else(|| self.name())
107    }
108
109    /// Returns the generic name of the application.
110    ///
111    /// For example: `Web browser`.
112    pub fn generic_name(&self) -> Option<String> {
113        self.get_value("GenericName")
114    }
115
116    /// Returns the localized generic name of the application.
117    pub fn generic_name_locale(&self) -> Option<String> {
118        self.get_value_locale("GenericName")
119    }
120
121    /// Returns the icon of the application.
122    ///
123    /// It's either the name of the icon or the absolute path.
124    pub fn icon(&self) -> Option<String> {
125        self.get_value("Icon")
126    }
127
128    /// Returns the localized icon of the application.
129    pub fn icon_locale(&self) -> Option<String> {
130        self.get_value_locale("Icon")
131    }
132
133    /// Returns a list of keywords describing the application.
134    pub fn keywords(&self) -> Vec<String> {
135        string_to_vec(self.get_value("Keywords"))
136    }
137
138    /// Returns a list of localized keywords describing the application.
139    pub fn keywords_locale(&self) -> Vec<String> {
140        string_to_vec(self.get_value_locale("Keywords"))
141    }
142
143    /// Returns the string containing the program to execute, possibly with arguments.
144    pub fn exec(&self) -> Option<String> {
145        self.get_value("Exec")
146    }
147
148    /// Returns whether the program should run in a terminal window.
149    pub fn terminal(&self) -> bool {
150        self.get_value("Terminal")
151            .and_then(|value| value.parse::<bool>().ok())
152            .unwrap_or(false)
153    }
154
155    /// Returns a list of application actions. Can be empty.
156    pub fn actions(&self) -> Vec<ActionHandle> {
157        self.inner
158            .actions
159            .iter()
160            .map(|action| ActionHandle {
161                inner: action.clone(),
162                service: self.service.clone(),
163            })
164            .collect()
165    }
166
167    /// Launches the application based on the [`exec()`] string.
168    ///
169    /// Starts a default terminal window if [`terminal()`] is `true`.
170    ///
171    /// The launched child process is detached from this process.
172    pub fn launch(&self) -> Result<()> {
173        utils::launch_from_exec_string(self.exec(), self.terminal())
174    }
175}
176
177#[cfg(test)]
178mod tests {
179
180    use super::*;
181
182    fn new_handle(contents: String, locale: &str) -> DesktopAppHandle {
183        DesktopAppHandle {
184            inner: Arc::new(
185                DesktopApp::new(String::from("com.example.program"), contents).unwrap(),
186            ),
187            service: ApplicationService::new_with_env(Vec::new(), locale),
188        }
189    }
190
191    #[test]
192    fn test_properties() {
193        let contents = format!(
194            r#"[Desktop Entry]
195Name=Some Name
196GenericName=This feels too generic
197Icon=some-icon
198Keywords=One;Two;Three;
199Exec=do --this
200Terminal=true
201Type=Application
202"#
203        );
204
205        let handle = new_handle(contents, "");
206        assert_eq!(handle.name(), "Some Name");
207        assert_eq!(handle.generic_name().unwrap(), "This feels too generic");
208        assert_eq!(handle.icon().unwrap(), "some-icon");
209        assert_eq!(handle.keywords(), vec!["One", "Two", "Three"]);
210        assert_eq!(handle.exec().unwrap(), "do --this");
211        assert_eq!(handle.terminal(), true);
212    }
213
214    #[test]
215    fn test_locale() {
216        let contents = String::from(
217            r#"[Desktop Entry]
218Name=Some Name
219Name[en_US@Idk]=Lang Country Modifier
220Name[en_US]=Lang Country
221Name[en@Idk]=Lang Modifier
222Name[en]=Lang Only
223Type=Application
224"#,
225        );
226        let handle = new_handle(contents.clone(), "en_US@Idk");
227
228        assert_eq!(handle.name_locale(), "Lang Country Modifier");
229
230        let handle = new_handle(contents.clone(), "en_US");
231        assert_eq!(handle.name_locale(), "Lang Country");
232
233        let handle = new_handle(contents.clone(), "en@Idk");
234        assert_eq!(handle.name_locale(), "Lang Modifier");
235
236        let handle = new_handle(contents.clone(), "en");
237        assert_eq!(handle.name_locale(), "Lang Only");
238
239        let handle = new_handle(contents, "invalid");
240        assert_eq!(handle.name_locale(), "Some Name");
241
242        let contents = String::from(
243            r#"[Desktop Entry]
244Name=Some Name
245Type=Application
246Name[en_US]=Lang Country
247Name[en@Idk]=Lang Modifier
248Name[en]=Lang Only
249"#,
250        );
251
252        let handle = new_handle(contents.clone(), "en_US@Idk");
253
254        assert_eq!(handle.name_locale(), "Lang Country");
255    }
256
257    #[test]
258    fn test_actions() {
259        let contents = String::from(
260            r#"[Desktop Entry]
261Name=Some Name
262Type=Application
263Actions=Meow;Open;Test
264
265[Desktop Action Meow]
266Name=Meow
267Icon=some-icon
268Exec=ls
269
270[Desktop Action Open]
271Name=Meow
272Icon=some-icon
273Exec=ls
274"#,
275        );
276
277        let handle = new_handle(contents, "");
278
279        assert_eq!(handle.actions().len(), 2);
280    }
281
282    #[tokio::test]
283    async fn test_exec() {
284        let contents = String::from(
285            r#"[Desktop Entry]
286Name=Some Name
287Type=Application
288Exec=ls
289"#,
290        );
291        let handle = new_handle(contents, "");
292        handle.launch().unwrap();
293    }
294}