applications/
desktopapp.rs1use 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#[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 utils::get_locale_string(
85 &self.inner.ini,
86 "Desktop Entry",
87 key,
88 &self.service.inner.locale,
89 )
90 }
91
92 pub fn app_id(&self) -> String {
94 self.inner.app_id.clone()
95 }
96
97 pub fn name(&self) -> String {
101 self.inner.name.clone()
102 }
103
104 pub fn name_locale(&self) -> String {
106 self.get_value_locale("Name").unwrap_or_else(|| self.name())
107 }
108
109 pub fn generic_name(&self) -> Option<String> {
113 self.get_value("GenericName")
114 }
115
116 pub fn generic_name_locale(&self) -> Option<String> {
118 self.get_value_locale("GenericName")
119 }
120
121 pub fn icon(&self) -> Option<String> {
125 self.get_value("Icon")
126 }
127
128 pub fn icon_locale(&self) -> Option<String> {
130 self.get_value_locale("Icon")
131 }
132
133 pub fn keywords(&self) -> Vec<String> {
135 string_to_vec(self.get_value("Keywords"))
136 }
137
138 pub fn keywords_locale(&self) -> Vec<String> {
140 string_to_vec(self.get_value_locale("Keywords"))
141 }
142
143 pub fn exec(&self) -> Option<String> {
145 self.get_value("Exec")
146 }
147
148 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 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 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}