Basic library for parsing XDG desktop files
Needs more comments, more tests, and a better, richer API
Getty Ritter
6 years ago
1 | [package] | |
2 | name = "xdg-desktop" | |
3 | version = "0.1.0" | |
4 | authors = ["Getty Ritter <gettylefou@gmail.com>"] | |
5 | ||
6 | [dependencies] | |
7 | regex = "1.0" | |
8 | failure = "0.1.1" | |
9 | lazy_static = "1.0" |
1 | #[macro_use] extern crate failure; | |
2 | #[macro_use] extern crate lazy_static; | |
3 | extern crate regex; | |
4 | ||
5 | use failure::Error; | |
6 | ||
7 | use std::collections::HashMap; | |
8 | use std::io::{Read}; | |
9 | ||
10 | #[derive(Debug)] | |
11 | pub struct DesktopEntry { | |
12 | pub info: DesktopEntryInfo, | |
13 | pub typ: EntryType, | |
14 | } | |
15 | ||
16 | #[derive(Debug)] | |
17 | pub enum EntryType { | |
18 | Application(ApplicationInfo), | |
19 | Link(String), | |
20 | Directory, | |
21 | } | |
22 | ||
23 | #[derive(Debug)] | |
24 | pub struct ApplicationInfo { | |
25 | pub dbus_activatable: bool, | |
26 | pub try_exec: Option<String>, | |
27 | pub exec: Option<String>, | |
28 | pub path: Option<String>, | |
29 | pub terminal: bool, | |
30 | pub actions: Vec<Action>, | |
31 | pub mime_type: Vec<String>, | |
32 | pub categories: Vec<String>, | |
33 | pub implements: Vec<String>, | |
34 | pub keywords: Vec<String>, | |
35 | pub startup_notify: bool, | |
36 | pub startup_wm_class: Option<String>, | |
37 | } | |
38 | ||
39 | #[derive(Debug)] | |
40 | pub struct Action { | |
41 | pub name: String, | |
42 | pub icon: Option<String>, | |
43 | pub exec: Option<String>, | |
44 | } | |
45 | ||
46 | impl Action { | |
47 | fn from_hashmap(bag: &mut HashMap<String, String>) -> Result<Action, Error> { | |
48 | let name = bag.remove("Name") | |
49 | .ok_or(format_err!("No Name!"))?; | |
50 | let icon = bag.remove("Icon"); | |
51 | let exec = bag.remove("Exec"); | |
52 | ||
53 | Ok(Action { name, icon, exec }) | |
54 | } | |
55 | } | |
56 | ||
57 | impl ApplicationInfo { | |
58 | fn from_hashmap( | |
59 | bag: &mut HashMap<String, String>, | |
60 | rest: &mut parser::Bag, | |
61 | ) -> | |
62 | Result<ApplicationInfo, Error> | |
63 | { | |
64 | let exec = bag.remove("Exec"); | |
65 | let path = bag.remove("Path"); | |
66 | let try_exec = bag.remove("TryExec"); | |
67 | ||
68 | ||
69 | let actions = bag.remove("Actions") | |
70 | .map_or_else(|| Vec::new(), parser::list_of_strings); | |
71 | let actions: Result<Vec<Action>, Error> = | |
72 | actions.into_iter().map(|a: String| { | |
73 | let key = format!("Desktop Action {}", a); | |
74 | let mut b = rest.remove(&key) | |
75 | .ok_or(format_err!("No action named {}", key))?; | |
76 | let r: Result<Action, Error> = Action::from_hashmap(&mut b); | |
77 | r | |
78 | }).collect(); | |
79 | let actions = actions?; | |
80 | ||
81 | let mime_type = bag.remove("MimeType") | |
82 | .map_or_else(|| Vec::new(), parser::list_of_strings); | |
83 | let categories = bag.remove("Categories") | |
84 | .map_or_else(|| Vec::new(), parser::list_of_strings); | |
85 | let implements = bag.remove("Implements") | |
86 | .map_or_else(|| Vec::new(), parser::list_of_strings); | |
87 | let keywords = bag.remove("Keywords") | |
88 | .map_or_else(|| Vec::new(), parser::list_of_strings); | |
89 | ||
90 | let terminal = bag.remove("Terminal") | |
91 | .map_or_else(|| Ok(false), parser::to_bool)?; | |
92 | let dbus_activatable = bag.remove("DbusActivatable") | |
93 | .map_or_else(|| Ok(false), parser::to_bool)?; | |
94 | ||
95 | let startup_notify = bag.remove("StartupNotify") | |
96 | .map_or_else(|| Ok(false), parser::to_bool)?; | |
97 | let startup_wm_class = None; | |
98 | ||
99 | Ok(ApplicationInfo { | |
100 | dbus_activatable, | |
101 | try_exec, | |
102 | exec, | |
103 | path, | |
104 | terminal, | |
105 | actions, | |
106 | mime_type, | |
107 | categories, | |
108 | implements, | |
109 | keywords, | |
110 | startup_notify, | |
111 | startup_wm_class, | |
112 | }) | |
113 | } | |
114 | } | |
115 | ||
116 | #[derive(Debug)] | |
117 | pub struct DesktopEntryInfo { | |
118 | pub version: Option<String>, | |
119 | pub name: String, | |
120 | pub generic_name: Option<String>, | |
121 | pub no_display: bool, | |
122 | pub comment: Option<String>, | |
123 | pub icon: Option<String>, | |
124 | pub hidden: bool, | |
125 | pub only_show_in: Vec<String>, | |
126 | pub not_show_in: Vec<String>, | |
127 | } | |
128 | ||
129 | ||
130 | mod parser { | |
131 | use std::collections::HashMap; | |
132 | use std::io::{BufRead, BufReader, Read}; | |
133 | use std::mem; | |
134 | ||
135 | use failure::Error; | |
136 | use regex::Regex; | |
137 | ||
138 | pub type Bag = HashMap<String, HashMap<String, String>>; | |
139 | ||
140 | pub fn bags_from_file<R: Read>(reader: &mut R) -> Result<Bag, Error> { | |
141 | lazy_static! { | |
142 | static ref SECTION: Regex = | |
143 | Regex::new(r"\[([A-Za-z0-9 -]*)\]").unwrap(); | |
144 | static ref KV: Regex = | |
145 | Regex::new(r"([A-Za-z0-9-]*(\[[A-Za-z@_]*\])?)=(.*)").unwrap(); | |
146 | } | |
147 | ||
148 | let reader = BufReader::new(reader); | |
149 | let mut bags = HashMap::new(); | |
150 | let mut current_section = None; | |
151 | let mut current_bag = HashMap::new(); | |
152 | ||
153 | for ln in reader.lines() { | |
154 | let ln = ln?; | |
155 | ||
156 | if let Some(kv) = KV.captures(&ln) { | |
157 | current_bag.insert(kv[1].to_owned(), kv[3].to_owned()); | |
158 | } else if let Some(s) = SECTION.captures(&ln) { | |
159 | if let Some(name) = current_section.take() { | |
160 | let bag = mem::replace(&mut current_bag, HashMap::new()); | |
161 | bags.insert(name, bag); | |
162 | } | |
163 | current_section = Some(s[1].to_owned()); | |
164 | } | |
165 | } | |
166 | ||
167 | if let Some(name) = current_section.take() { | |
168 | bags.insert(name, current_bag); | |
169 | } | |
170 | ||
171 | Ok(bags) | |
172 | } | |
173 | ||
174 | pub fn list_of_strings(str: String) -> Vec<String> { | |
175 | str.split(";").filter(|s| s.len() > 0).map(|s| s.to_owned()).collect() | |
176 | } | |
177 | ||
178 | pub fn to_bool(str: String) -> Result<bool, Error> { | |
179 | match str.as_ref() { | |
180 | "true" => Ok(true), | |
181 | "false" => Ok(false), | |
182 | _ => Err(format_err!("Invalid value for boolean field: {}", str)), | |
183 | } | |
184 | } | |
185 | } | |
186 | ||
187 | impl DesktopEntryInfo { | |
188 | pub fn from_bag(bags: &mut parser::Bag) -> Result<DesktopEntryInfo, Error> { | |
189 | let bag = bags.get_mut("Desktop Entry") | |
190 | .ok_or(format_err!("No Desktop Entry"))?; | |
191 | ||
192 | let version = bag.remove("Version"); | |
193 | ||
194 | let name = bag.remove("Name") | |
195 | .ok_or(format_err!("No name"))? | |
196 | .to_owned(); | |
197 | let generic_name = bag.remove("Generic Name"); | |
198 | let comment = bag.remove("Comment"); | |
199 | let icon = bag.remove("Icon"); | |
200 | ||
201 | let no_display = bag.remove("No Display") | |
202 | .map_or_else(|| Ok(false), parser::to_bool)?; | |
203 | let hidden = bag.remove("Hidden") | |
204 | .map_or_else(|| Ok(false), parser::to_bool)?; | |
205 | ||
206 | let only_show_in = bag.remove("OnlyShowIn") | |
207 | .map_or_else(|| Vec::new(), parser::list_of_strings); | |
208 | let not_show_in = bag.remove("NotShowIn") | |
209 | .map_or_else(|| Vec::new(), parser::list_of_strings); | |
210 | ||
211 | let info = DesktopEntryInfo { | |
212 | version, | |
213 | name, | |
214 | generic_name, | |
215 | no_display, | |
216 | comment, | |
217 | icon, | |
218 | hidden, | |
219 | only_show_in, | |
220 | not_show_in, | |
221 | }; | |
222 | ||
223 | Ok(info) | |
224 | } | |
225 | } | |
226 | ||
227 | impl DesktopEntry { | |
228 | pub fn from_file<R: Read>(reader: &mut R) -> Result<DesktopEntry, Error> { | |
229 | let mut bags = parser::bags_from_file(reader)?; | |
230 | DesktopEntry::from_bags(&mut bags) | |
231 | } | |
232 | ||
233 | fn from_bags(bags: &mut parser::Bag) -> Result<DesktopEntry, Error> { | |
234 | let info = DesktopEntryInfo::from_bag(bags)?; | |
235 | let mut bag = bags.remove("Desktop Entry") | |
236 | .ok_or(format_err!("No Desktop Entry"))?; | |
237 | let typ = bag.remove("Type") | |
238 | .ok_or(format_err!("No Type"))?; | |
239 | let typ = match typ.as_ref() { | |
240 | "Application" => { | |
241 | let app = ApplicationInfo::from_hashmap(&mut bag, bags)?; | |
242 | EntryType::Application(app) | |
243 | } | |
244 | "Link" => { | |
245 | let url = bag.remove("URL") | |
246 | .ok_or(format_err!("No URL"))?; | |
247 | EntryType::Link(url) | |
248 | } | |
249 | "Directory" => EntryType::Directory, | |
250 | r => Err(format_err!("Bad type: {}", r))?, | |
251 | }; | |
252 | Ok(DesktopEntry { info, typ }) | |
253 | } | |
254 | } | |
255 | ||
256 | ||
257 | #[cfg(test)] | |
258 | mod tests { | |
259 | #[test] | |
260 | fn firefox_example() { | |
261 | let firefox_example = include_str!("../test_cases/firefox.desktop"); | |
262 | let mut f = ::std::io::Cursor::new(firefox_example); | |
263 | let mut entry = ::parser::bags_from_file(&mut f).unwrap(); | |
264 | assert_eq!( | |
265 | entry["Desktop Entry"]["Name"], | |
266 | "Firefox".to_owned(), | |
267 | ); | |
268 | ||
269 | let info = ::DesktopEntryInfo::from_bag(&mut entry).unwrap(); | |
270 | println!("Got: {:#?}", info); | |
271 | assert_eq!( | |
272 | info.name, | |
273 | "Firefox".to_owned(), | |
274 | ); | |
275 | } | |
276 | ||
277 | #[test] | |
278 | fn steam_example() { | |
279 | let steam_example = include_str!("../test_cases/steam.desktop"); | |
280 | let mut f = ::std::io::Cursor::new(steam_example); | |
281 | let entry = ::DesktopEntry::from_file(&mut f).unwrap(); | |
282 | println!("got {:#?}", entry); | |
283 | assert_eq!( | |
284 | entry.info.name, | |
285 | "Steam".to_owned(), | |
286 | ); | |
287 | } | |
288 | } |
1 | [Desktop Entry] | |
2 | Name=Firefox | |
3 | Name[bn]=ফায়ারফক্স | |
4 | Name[eo]=Fajrovulpo | |
5 | Name[fi]=Firefox | |
6 | Name[pa]=ਫਾਇਰਫੋਕਸ | |
7 | Name[tg]=Рӯбоҳи оташин | |
8 | GenericName=Web Browser | |
9 | GenericName[af]=Web Blaaier | |
10 | GenericName[ar]=متصفح ويب | |
11 | GenericName[az]=Veb Səyyahı | |
12 | GenericName[bg]=Браузър | |
13 | GenericName[bn]=ওয়েব ব্রাউজার | |
14 | GenericName[br]=Furcher ar Gwiad | |
15 | GenericName[bs]=WWW Preglednik | |
16 | GenericName[ca]=Fullejador web | |
17 | GenericName[cs]=WWW prohlížeč | |
18 | GenericName[cy]=Porydd Gwe | |
19 | GenericName[da]=Browser | |
20 | GenericName[de]=Web-Browser | |
21 | GenericName[el]=Περιηγητής Ιστού | |
22 | GenericName[eo]=TTT-legilo | |
23 | GenericName[es]=Navegador web | |
24 | GenericName[et]=Veebilehitseja | |
25 | GenericName[eu]=Web arakatzailea | |
26 | GenericName[fa]=مرورگر وب | |
27 | GenericName[fi]=WWW-selain | |
28 | GenericName[fo]=Alnótsfar | |
29 | GenericName[fr]=Navigateur web | |
30 | GenericName[gl]=Navegador Web | |
31 | GenericName[he]=דפדפן אינטרנט | |
32 | GenericName[hi]=वेब ब्राउज़र | |
33 | GenericName[hr]=Web preglednik | |
34 | GenericName[hu]=Webböngésző | |
35 | GenericName[is]=Vafri | |
36 | GenericName[it]=Browser Web | |
37 | GenericName[ja]=ウェブブラウザ | |
38 | GenericName[ko]=웹 브라우저 | |
39 | GenericName[lo]=ເວັບບຣາວເຊີ | |
40 | GenericName[lt]=Žiniatinklio naršyklė | |
41 | GenericName[lv]=Web Pārlūks | |
42 | GenericName[mk]=Прелистувач на Интернет | |
43 | GenericName[mn]=Веб-Хөтөч | |
44 | GenericName[nb]=Nettleser | |
45 | GenericName[nds]=Nettkieker | |
46 | GenericName[nl]=Webbrowser | |
47 | GenericName[nn]=Nettlesar | |
48 | GenericName[nso]=Seinyakisi sa Web | |
49 | GenericName[pa]=ਵੈਬ ਝਲਕਾਰਾ | |
50 | GenericName[pl]=Przeglądarka WWW | |
51 | GenericName[pt]=Navegador Web | |
52 | GenericName[pt_BR]=Navegador Web | |
53 | GenericName[ro]=Navigator de web | |
54 | GenericName[ru]=Веб-браузер | |
55 | GenericName[se]=Fierpmádatlogan | |
56 | GenericName[sk]=Webový prehliadač | |
57 | GenericName[sl]=Spletni brskalnik | |
58 | GenericName[sr]=Веб претраживач | |
59 | GenericName[sr@Latn]=Veb pretraživač | |
60 | GenericName[ss]=Ibrawuza yeWeb | |
61 | GenericName[sv]=Webbläsare | |
62 | GenericName[ta]=வலை உலாவி | |
63 | GenericName[tg]=Тафсиргари вэб | |
64 | GenericName[th]=เว็บบราวเซอร์ | |
65 | GenericName[tr]=Web Tarayıcı | |
66 | GenericName[uk]=Навігатор Тенет | |
67 | GenericName[uz]=Веб-браузер | |
68 | GenericName[ven]=Buronza ya Webu | |
69 | GenericName[vi]=Trình duyệt Web | |
70 | GenericName[wa]=Betchteu waibe | |
71 | GenericName[xh]=Umkhangeli zincwadi we Web | |
72 | GenericName[zh_CN]=网页浏览器 | |
73 | GenericName[zh_TW]=網頁瀏覽器 | |
74 | GenericName[zu]=Umcingi we-Web | |
75 | Comment=Browse the World Wide Web | |
76 | Comment[ar]=تصفح الشبكة العنكبوتية العالمية | |
77 | Comment[ast]=Restola pela Rede | |
78 | Comment[bn]=ইন্টারনেট ব্রাউজ করুন | |
79 | Comment[ca]=Navegueu per la web | |
80 | Comment[cs]=Prohlížení stránek World Wide Webu | |
81 | Comment[da]=Surf på internettet | |
82 | Comment[de]=Im Internet surfen | |
83 | Comment[el]=Μπορείτε να περιηγηθείτε στο διαδίκτυο (Web) | |
84 | Comment[es]=Navegue por la web | |
85 | Comment[et]=Lehitse veebi | |
86 | Comment[fa]=صفحات شبکه جهانی اینترنت را مرور نمایید | |
87 | Comment[fi]=Selaa Internetin WWW-sivuja | |
88 | Comment[fr]=Naviguer sur le Web | |
89 | Comment[gl]=Navegar pola rede | |
90 | Comment[he]=גלישה ברחבי האינטרנט | |
91 | Comment[hr]=Pretražite web | |
92 | Comment[hu]=A világháló böngészése | |
93 | Comment[it]=Esplora il web | |
94 | Comment[ja]=ウェブを閲覧します | |
95 | Comment[ko]=웹을 돌아 다닙니다 | |
96 | Comment[ku]=Li torê bigere | |
97 | Comment[lt]=Naršykite internete | |
98 | Comment[nb]=Surf på nettet | |
99 | Comment[nl]=Verken het internet | |
100 | Comment[nn]=Surf på nettet | |
101 | Comment[no]=Surf på nettet | |
102 | Comment[pl]=Przeglądanie stron WWW | |
103 | Comment[pt]=Navegue na Internet | |
104 | Comment[pt_BR]=Navegue na Internet | |
105 | Comment[ro]=Navigați pe Internet | |
106 | Comment[ru]=Доступ в Интернет | |
107 | Comment[sk]=Prehliadanie internetu | |
108 | Comment[sl]=Brskajte po spletu | |
109 | Comment[sv]=Surfa på webben | |
110 | Comment[ug]=دۇنيادىكى توربەتلەرنى كۆرگىلى بولىدۇ | |
111 | Comment[uk]=Перегляд сторінок Інтернету | |
112 | Comment[vi]=Để duyệt các trang web | |
113 | Comment[zh_CN]=浏览互联网 | |
114 | Comment[zh_TW]=瀏覽網際網路 | |
115 | Exec=firefox %u | |
116 | Icon=firefox | |
117 | Terminal=false | |
118 | Type=Application | |
119 | MimeType=text/html;text/xml;application/xhtml+xml;application/vnd.mozilla.xul+xml;text/mml;x-scheme-handler/http;x-scheme-handler/https; | |
120 | StartupNotify=true | |
121 | Categories=Network;WebBrowser; |
1 | [Desktop Entry] | |
2 | Name=Steam | |
3 | Comment=Application for managing and playing games on Steam | |
4 | Exec=/usr/bin/steam %U | |
5 | Icon=steam | |
6 | Terminal=false | |
7 | Type=Application | |
8 | Categories=Network;FileTransfer;Game; | |
9 | MimeType=x-scheme-handler/steam; | |
10 | Actions=Store;Community;Library;Servers;Screenshots;News;Settings;BigPicture;Friends; | |
11 | ||
12 | [Desktop Action Store] | |
13 | Name=Store | |
14 | Name[de]=Shop | |
15 | Name[es]=Tienda | |
16 | Name[fr]=Magasin | |
17 | Name[it]=Negozio | |
18 | Name[pt]=Loja | |
19 | Name[ru]=Магазин | |
20 | Name[zh_CN]=商店 | |
21 | Name[zh_TW]=商店 | |
22 | Exec=steam steam://store | |
23 | ||
24 | [Desktop Action Community] | |
25 | Name=Community | |
26 | Name[es]=Comunidad | |
27 | Name[fr]=Communauté | |
28 | Name[it]=Comunità | |
29 | Name[pt]=Comunidade | |
30 | Name[ru]=Сообщество | |
31 | Name[zh_CN]=社区 | |
32 | Name[zh_TW]=社群 | |
33 | Exec=steam steam://url/SteamIDControlPage | |
34 | ||
35 | [Desktop Action Library] | |
36 | Name=Library | |
37 | Name[de]=Bibliothek | |
38 | Name[es]=Biblioteca | |
39 | Name[fr]=Bibliothèque | |
40 | Name[it]=Libreria | |
41 | Name[pt]=Biblioteca | |
42 | Name[ru]=Библиотека | |
43 | Name[zh_CN]=库 | |
44 | Name[zh_TW]=遊戲庫 | |
45 | Exec=steam steam://open/games | |
46 | ||
47 | [Desktop Action Servers] | |
48 | Name=Servers | |
49 | Name[de]=Server | |
50 | Name[es]=Servidores | |
51 | Name[fr]=Serveurs | |
52 | Name[it]=Server | |
53 | Name[pt]=Servidores | |
54 | Name[ru]=Серверы | |
55 | Name[zh_CN]=服务器 | |
56 | Name[zh_TW]=伺服器 | |
57 | Exec=steam steam://open/servers | |
58 | ||
59 | [Desktop Action Screenshots] | |
60 | Name=Screenshots | |
61 | Name[es]=Capturas | |
62 | Name[fr]=Captures d'écran | |
63 | Name[it]=Screenshot | |
64 | Name[ru]=Скриншоты | |
65 | Name[zh_CN]=截图 | |
66 | Name[zh_TW]=螢幕擷圖 | |
67 | Exec=steam steam://open/screenshots | |
68 | ||
69 | [Desktop Action News] | |
70 | Name=News | |
71 | Name[de]=Neuigkeiten | |
72 | Name[es]=Noticias | |
73 | Name[fr]=Actualités | |
74 | Name[it]=Notizie | |
75 | Name[pt]=Notícias | |
76 | Name[ru]=Новости | |
77 | Name[zh_CN]=新闻 | |
78 | Name[zh_TW]=新聞 | |
79 | Exec=steam steam://open/news | |
80 | ||
81 | [Desktop Action Settings] | |
82 | Name=Settings | |
83 | Name[de]=Einstellungen | |
84 | Name[es]=Parámetros | |
85 | Name[fr]=Paramètres | |
86 | Name[it]=Impostazioni | |
87 | Name[pt]=Configurações | |
88 | Name[ru]=Настройки | |
89 | Name[zh_CN]=设置 | |
90 | Name[zh_TW]=設定 | |
91 | Exec=steam steam://open/settings | |
92 | ||
93 | [Desktop Action BigPicture] | |
94 | Name=Big Picture | |
95 | Exec=steam steam://open/bigpicture | |
96 | ||
97 | [Desktop Action Friends] | |
98 | Name=Friends | |
99 | Name[de]=Freunde | |
100 | Name[es]=Amigos | |
101 | Name[fr]=Amis | |
102 | Name[it]=Amici | |
103 | Name[pt]=Amigos | |
104 | Name[ru]=Друзья | |
105 | Name[zh_CN]=好友 | |
106 | Name[zh_TW]=好友 | |
107 | Exec=steam steam://open/friends |