Undo XDG string-escaping when generating menu-entries.
[wmaker-crm.git] / util / wmmenugen_parse_xdg.c
blob02c7b0076ab627b728593e6df13a072a7b8b8e2c
1 /*
2 * wmmenugen - Window Maker PropList menu generator
4 * Desktop Entry Specification parser functions
6 * Copyright (c) 2010. Tamas Tevesz <ice@extreme.hu>
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
24 * http://standards.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.1.html
25 * http://standards.freedesktop.org/menu-spec/menu-spec-1.1.html
27 * We will only deal with Type == "Application" entries in [Desktop Entry]
28 * groups. Since there is no passing of file name arguments or anything of
29 * the sort to applications from the menu, execname is determined as follows:
30 * - If `TryExec' is present, use that;
31 * - else use `Exec' with any switches stripped
33 * Only the (first, though there should not be more than one) `Main Category'
34 * is used to place the entry in a submenu.
36 * Basic validation of the .desktop file is done.
39 #include <sys/types.h>
40 #include <sys/stat.h>
42 #include <ctype.h>
43 #include <ftw.h>
44 #if DEBUG
45 #include <errno.h>
46 #endif
47 #include <stdio.h>
48 #include <stdlib.h>
49 #include <string.h>
51 #include "wmmenugen.h"
53 /* LocaleString match levels */
54 enum {
55 MATCH_DEFAULT,
56 MATCH_LANG,
57 MATCH_LANG_MODIFIER,
58 MATCH_LANG_COUNTRY,
59 MATCH_LANG_COUNTRY_MODIFIER
62 typedef struct {
63 char *Name; /* Name */ /* localestring */
64 int MatchLevel; /* LocaleString match type */ /* int */
65 char *TryExec; /* TryExec */ /* string */
66 char *Exec; /* Exec */ /* string */
67 char *Path; /* Path */ /* string */
68 int Flags; /* Flags */
69 char *Category; /* Categories (first item only) */ /* string */
70 } XDGMenuEntry;
72 static void getKey(char **target, const char *line);
73 static void getStringValue(char **target, const char *line);
74 static void getLocalizedStringValue(char **target, const char *line, int *match_level);
75 static int getBooleanValue(const char *line);
76 static void getMenuHierarchyFor(char **xdgmenuspec);
77 static int compare_matchlevel(int *current_level, const char *found_locale);
78 static Bool xdg_to_wm(XDGMenuEntry *xdg, WMMenuEntry *wmentry);
79 static void init_xdg_storage(XDGMenuEntry *xdg);
80 static void init_wm_storage(WMMenuEntry *wm);
83 void parse_xdg(const char *file, cb_add_menu_entry *addWMMenuEntryCallback)
85 FILE *fp;
86 char buf[1024];
87 char *p, *tmp, *key;
88 WMMenuEntry *wm;
89 XDGMenuEntry *xdg;
90 int InGroup;
92 fp = fopen(file, "r");
93 if (!fp) {
94 #if DEBUG
95 fprintf(stderr, "Error opening file %s: %s\n", file, strerror(errno));
96 #endif
97 return;
100 xdg = (XDGMenuEntry *)wmalloc(sizeof(XDGMenuEntry));
101 wm = (WMMenuEntry *)wmalloc(sizeof(WMMenuEntry));
102 InGroup = 0;
103 memset(buf, 0, sizeof(buf));
105 while (fgets(buf, sizeof(buf), fp)) {
107 p = buf;
109 /* skip whitespaces */
110 while (isspace(*p))
111 p++;
112 /* skip comments, empty lines */
113 if (*p == '\r' || *p == '\n' || *p == '#') {
114 memset(buf, 0, sizeof(buf));
115 continue;
117 /* trim crlf */
118 buf[strcspn(buf, "\r\n")] = '\0';
119 if (strlen(buf) == 0)
120 continue;
122 if (strcmp(p, "[Desktop Entry]") == 0) {
123 /* if currently processing a group, we've just hit the
124 * end of its definition, try processing it
126 if (InGroup && xdg_to_wm(xdg, wm)) {
127 (*addWMMenuEntryCallback)(wm);
129 init_xdg_storage(xdg);
130 init_wm_storage(wm);
131 InGroup = 1;
132 /* start processing group */
133 memset(buf, 0, sizeof(buf));
134 continue;
135 } else if (p[0] == '[') {
136 /* If we find a new group and the previous group was the main one,
137 * we stop all further processing
139 if (InGroup) break;
142 if (!InGroup) {
143 memset(buf, 0, sizeof(buf));
144 continue;
147 getKey(&key, p);
148 if (key == NULL) { /* not `key' = `value' */
149 memset(buf, 0, sizeof(buf));
150 continue;
153 if (strcmp(key, "Type") == 0) {
154 getStringValue(&tmp, p);
155 if (strcmp(tmp, "Application") != 0)
156 InGroup = 0; /* if not application, skip current group */
157 wfree(tmp);
158 tmp = NULL;
159 } else if (strcmp(key, "Name") == 0) {
160 getLocalizedStringValue(&xdg->Name, p, &xdg->MatchLevel);
161 } else if (strcmp(key, "NoDisplay") == 0) {
162 if (getBooleanValue(p)) /* if nodisplay, skip current group */
163 InGroup = 0;
164 } else if (strcmp(key, "Hidden") == 0) {
165 if (getBooleanValue(p))
166 InGroup = 0; /* if hidden, skip current group */
167 } else if (strcmp(key, "TryExec") == 0) {
168 getStringValue(&xdg->TryExec, p);
169 } else if (strcmp(key, "Exec") == 0) {
170 getStringValue(&xdg->Exec, p);
171 } else if (strcmp(key, "Path") == 0) {
172 getStringValue(&xdg->Path, p);
173 } else if (strcmp(key, "Terminal") == 0) {
174 if (getBooleanValue(p))
175 xdg->Flags |= F_TERMINAL;
176 } else if (strcmp(key, "Categories") == 0) {
177 getStringValue(&xdg->Category, p);
178 getMenuHierarchyFor(&xdg->Category);
181 if (xdg->Category == NULL)
182 xdg->Category = wstrdup(_("Other"));
184 wfree(key);
185 key = NULL;
188 fclose(fp);
190 /* at the end of the file, might as well try to menuize what we have
191 * unless there was no group at all or it was marked as hidden
193 if (InGroup && xdg_to_wm(xdg, wm))
194 (*addWMMenuEntryCallback)(wm);
199 /* coerce an xdg entry type into a wm entry type
201 static Bool xdg_to_wm(XDGMenuEntry *xdg, WMMenuEntry *wm)
203 char *p;
205 /* Exec or TryExec is mandatory */
206 if (!(xdg->Exec || xdg->TryExec))
207 return False;
209 /* if there's no Name, use the first word of Exec or TryExec
211 if (xdg->Name) {
212 wm->Name = xdg->Name;
213 } else {
214 if (xdg->Exec)
215 wm->Name = wstrdup(xdg->Exec);
216 else /* xdg->TryExec */
217 wm->Name = wstrdup(xdg->TryExec);
219 p = strchr(wm->Name, ' ');
220 if (p)
221 *p = '\0';
224 if (xdg->Exec)
225 wm->CmdLine = xdg->Exec;
226 else /* xdg->TryExec */
227 wm->CmdLine = xdg->TryExec;
229 wm->SubMenu = xdg->Category;
230 wm->Flags = xdg->Flags;
232 return True;
235 /* (re-)initialize a XDGMenuEntry storage
237 static void init_xdg_storage(XDGMenuEntry *xdg)
240 if (xdg->Name)
241 wfree(xdg->Name);
242 if (xdg->TryExec)
243 wfree(xdg->TryExec);
244 if (xdg->Exec)
245 wfree(xdg->Exec);
246 if (xdg->Category)
247 wfree(xdg->Category);
248 if (xdg->Path)
249 wfree(xdg->Path);
251 xdg->Name = NULL;
252 xdg->TryExec = NULL;
253 xdg->Exec = NULL;
254 xdg->Category = NULL;
255 xdg->Path = NULL;
256 xdg->Flags = 0;
257 xdg->MatchLevel = -1;
260 /* (re-)initialize a WMMenuEntry storage
262 static void init_wm_storage(WMMenuEntry *wm)
264 wm->Name = NULL;
265 wm->CmdLine = NULL;
266 wm->Flags = 0;
269 /* get a key from line. allocates target, which must be wfreed later */
270 static void getKey(char **target, const char *line)
272 const char *p;
273 int kstart, kend;
275 p = line;
277 if (strchr(p, '=') == NULL) { /* not `key' = `value' */
278 *target = NULL;
279 return;
282 kstart = 0;
284 /* skip whitespace */
285 while (isspace(*(p + kstart)))
286 kstart++;
288 /* skip up until first whitespace or '[' (localestring) or '=' */
289 kend = kstart + 1;
290 while (*(p + kend) && !isspace(*(p + kend)) && *(p + kend) != '=' && *(p + kend) != '[')
291 kend++;
293 *target = wstrndup(p + kstart, kend - kstart);
296 /* get a string value from line. allocates target, which must be wfreed later. */
297 static void getStringValue(char **target, const char *line)
299 const char *p;
300 char *q;
301 int kstart;
303 p = line;
304 kstart = 0;
306 /* skip until after '=' */
307 while (*(p + kstart) && *(p + kstart) != '=')
308 kstart++;
309 kstart++;
311 /* skip whitespace */
312 while (*(p + kstart) && isspace(*(p + kstart)))
313 kstart++;
315 *target = wstrdup(p + kstart);
317 for (p = q = *target; *p; p++) {
318 if (*p != '\\') {
319 *q++ = *p;
320 } else {
321 switch (*++p) {
322 case 's': *q++ = ' '; break;
323 case 'n': *q++ = '\n'; break;
324 case 't': *q++ = '\t'; break;
325 case 'r': *q++ = '\r'; break;
326 case '\\': *q++ = '\\'; break;
327 default:
329 * Skip invalid escape.
331 break;
335 *q = '\0';
338 /* get a localized string value from line. allocates target, which must be wfreed later.
339 * matching is dependent on the current value of target as well as on the
340 * level the current value is matched on. guts matching algorithm is in
341 * compare_matchlevel().
343 static void getLocalizedStringValue(char **target, const char *line, int *match_level)
345 const char *p;
346 char *locale;
347 int kstart;
348 int sqbstart, sqbend;
350 p = line;
351 kstart = 0;
352 sqbstart = 0;
353 sqbend = 0;
354 locale = NULL;
356 /* skip until after '=', mark if '[' and ']' is found */
357 while (*(p + kstart) && *(p + kstart) != '=') {
358 switch (*(p + kstart)) {
359 case '[': sqbstart = kstart + 1;break;
360 case ']': sqbend = kstart; break;
361 default : break;
363 kstart++;
365 kstart++;
367 /* skip whitespace */
368 while (isspace(*(p + kstart)))
369 kstart++;
371 if (sqbstart > 0 && sqbend > sqbstart)
372 locale = wstrndup(p + sqbstart, sqbend - sqbstart);
374 /* if there is no value yet and this is the default key, return */
375 if (!*target && !locale) {
376 *match_level = MATCH_DEFAULT;
377 *target = wstrdup(p + kstart);
378 return;
381 if (compare_matchlevel(match_level, locale)) {
382 wfree(locale);
383 *target = wstrdup(p + kstart);
384 return;
387 return;
390 /* get a boolean value from line */
391 static Bool getBooleanValue(const char *line)
393 char *p;
394 int ret;
396 getStringValue(&p, line);
397 ret = strcmp(p, "true") == 0 ? True : False;
398 wfree(p);
400 return ret;
403 /* perform locale matching by implementing the algorithm specified in
404 * xdg desktop entry specification, section "localized values for keys".
406 static Bool compare_matchlevel(int *current_level, const char *found_locale)
408 /* current key locale */
409 char *key_lang, *key_ctry, *key_enc, *key_mod;
411 parse_locale(found_locale, &key_lang, &key_ctry, &key_enc, &key_mod);
413 if (env_lang && key_lang && /* Shortcut: if key and env languages don't match, */
414 strcmp(env_lang, key_lang) != 0) /* don't even bother. This takes care of the great */
415 return False; /* majority of the cases without having to go through */
416 /* the more theoretical parts of the spec'd algo. */
418 if (!env_mod && key_mod) /* If LC_MESSAGES does not have a MODIFIER field, */
419 return False; /* then no key with a modifier will be matched. */
421 if (!env_ctry && key_ctry) /* Similarly, if LC_MESSAGES does not have a COUNTRY field, */
422 return False; /* then no key with a country specified will be matched. */
424 /* LC_MESSAGES value: lang_COUNTRY@MODIFIER */
425 if (env_lang && env_ctry && env_mod) { /* lang_COUNTRY@MODIFIER */
426 if (key_lang && key_ctry && key_mod &&
427 strcmp(env_lang, key_lang) == 0 &&
428 strcmp(env_ctry, key_ctry) == 0 &&
429 strcmp(env_mod, key_mod) == 0) {
430 *current_level = MATCH_LANG_COUNTRY_MODIFIER;
431 return True;
432 } else if (key_lang && key_ctry && /* lang_COUNTRY */
433 strcmp(env_lang, key_lang) == 0 &&
434 strcmp(env_ctry, key_ctry) == 0 &&
435 *current_level < MATCH_LANG_COUNTRY) {
436 *current_level = MATCH_LANG_COUNTRY;
437 return True;
438 } else if (key_lang && key_mod && /* lang@MODIFIER */
439 strcmp(env_lang, key_lang) == 0 &&
440 strcmp(env_mod, key_mod) == 0 &&
441 *current_level < MATCH_LANG_MODIFIER) {
442 *current_level = MATCH_LANG_MODIFIER;
443 return True;
444 } else if (key_lang && /* lang */
445 strcmp(env_lang, key_lang) == 0 &&
446 *current_level < MATCH_LANG) {
447 *current_level = MATCH_LANG;
448 return True;
449 } else {
450 return False;
454 /* LC_MESSAGES value: lang_COUNTRY */
455 if (env_lang && env_ctry) { /* lang_COUNTRY */
456 if (key_lang && key_ctry &&
457 strcmp(env_lang, key_lang) == 0 &&
458 strcmp(env_ctry, key_ctry) == 0 &&
459 *current_level < MATCH_LANG_COUNTRY) {
460 *current_level = MATCH_LANG_COUNTRY;
461 return True;
462 } else if (key_lang && /* lang */
463 strcmp(env_lang, key_lang) == 0 &&
464 *current_level < MATCH_LANG) {
465 *current_level = MATCH_LANG;
466 return True;
467 } else {
468 return False;
472 /* LC_MESSAGES value: lang@MODIFIER */
473 if (env_lang && env_mod) { /* lang@MODIFIER */
474 if (key_lang && key_mod &&
475 strcmp(env_lang, key_lang) == 0 &&
476 strcmp(env_mod, key_mod) == 0 &&
477 *current_level < MATCH_LANG_MODIFIER) {
478 *current_level = MATCH_LANG_MODIFIER;
479 return True;
480 } else if (key_lang && /* lang */
481 strcmp(env_lang, key_lang) == 0 &&
482 *current_level < MATCH_LANG) {
483 *current_level = MATCH_LANG;
484 return True;
485 } else {
486 return False;
490 /* LC_MESSAGES value: lang */
491 if (env_lang) { /* lang */
492 if (key_lang &&
493 strcmp(env_lang, key_lang) == 0 &&
494 *current_level < MATCH_LANG) {
495 *current_level = MATCH_LANG;
496 return True;
497 } else {
498 return False;
502 /* MATCH_DEFAULT is handled in getLocalizedStringValue */
504 return False;
507 /* get the (first) xdg main category from a list of categories
509 static void getMenuHierarchyFor(char **xdgmenuspec)
511 char *category, *p;
512 char buf[1024];
514 if (!*xdgmenuspec || !**xdgmenuspec)
515 return;
517 category = wstrdup(*xdgmenuspec);
518 wfree(*xdgmenuspec);
519 memset(buf, 0, sizeof(buf));
521 p = strtok(category, ";");
522 while (p) { /* get a known category */
523 if (strcmp(p, "AudioVideo") == 0) {
524 snprintf(buf, sizeof(buf), "%s", _("Audio & Video"));
525 break;
526 } else if (strcmp(p, "Audio") == 0) {
527 snprintf(buf, sizeof(buf), "%s", _("Audio"));
528 break;
529 } else if (strcmp(p, "Video") == 0) {
530 snprintf(buf, sizeof(buf), "%s", _("Video"));
531 break;
532 } else if (strcmp(p, "Development") == 0) {
533 snprintf(buf, sizeof(buf), "%s", _("Development"));
534 break;
535 } else if (strcmp(p, "Education") == 0) {
536 snprintf(buf, sizeof(buf), "%s", _("Education"));
537 break;
538 } else if (strcmp(p, "Game") == 0) {
539 snprintf(buf, sizeof(buf), "%s", _("Game"));
540 break;
541 } else if (strcmp(p, "Graphics") == 0) {
542 snprintf(buf, sizeof(buf), "%s", _("Graphics"));
543 break;
544 } else if (strcmp(p, "Network") == 0) {
545 snprintf(buf, sizeof(buf), "%s", _("Network"));
546 break;
547 } else if (strcmp(p, "Office") == 0) {
548 snprintf(buf, sizeof(buf), "%s", _("Office"));
549 break;
550 } else if (strcmp(p, "Science") == 0) {
551 snprintf(buf, sizeof(buf), "%s", _("Science"));
552 break;
553 } else if (strcmp(p, "Settings") == 0) {
554 snprintf(buf, sizeof(buf), "%s", _("Settings"));
555 break;
556 } else if (strcmp(p, "System") == 0) {
557 snprintf(buf, sizeof(buf), "%s", _("System"));
558 break;
559 } else if (strcmp(p, "Utility") == 0) {
560 snprintf(buf, sizeof(buf), "%s", _("Utility"));
561 break;
562 /* reserved categories */
563 } else if (strcmp(p, "Screensaver") == 0) {
564 snprintf(buf, sizeof(buf), "%s", _("Screensaver"));
565 break;
566 } else if (strcmp(p, "TrayIcon") == 0) {
567 snprintf(buf, sizeof(buf), "%s", _("Tray Icon"));
568 break;
569 } else if (strcmp(p, "Applet") == 0) {
570 snprintf(buf, sizeof(buf), "%s", _("Applet"));
571 break;
572 } else if (strcmp(p, "Shell") == 0) {
573 snprintf(buf, sizeof(buf), "%s", _("Shell"));
574 break;
576 p = strtok(NULL, ";");
580 if (!*buf) /* come up with something if nothing found */
581 snprintf(buf, sizeof(buf), "%s", _("Other"));
583 *xdgmenuspec = wstrdup(buf);