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>
51 #include "wmmenugen.h"
53 /* LocaleString match levels */
59 MATCH_LANG_COUNTRY_MODIFIER
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 */
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 char *parse_xdg_exec(char *exec
);
80 static void init_xdg_storage(XDGMenuEntry
*xdg
);
81 static void init_wm_storage(WMMenuEntry
*wm
);
84 void parse_xdg(const char *file
, cb_add_menu_entry
*addWMMenuEntryCallback
)
93 fp
= fopen(file
, "r");
96 fprintf(stderr
, "Error opening file %s: %s\n", file
, strerror(errno
));
101 xdg
= (XDGMenuEntry
*)wmalloc(sizeof(XDGMenuEntry
));
102 wm
= (WMMenuEntry
*)wmalloc(sizeof(WMMenuEntry
));
104 memset(buf
, 0, sizeof(buf
));
106 while (fgets(buf
, sizeof(buf
), fp
)) {
110 /* skip whitespaces */
113 /* skip comments, empty lines */
114 if (*p
== '\r' || *p
== '\n' || *p
== '#') {
115 memset(buf
, 0, sizeof(buf
));
119 buf
[strcspn(buf
, "\r\n")] = '\0';
120 if (strlen(buf
) == 0)
123 if (strcmp(p
, "[Desktop Entry]") == 0) {
124 /* if currently processing a group, we've just hit the
125 * end of its definition, try processing it
127 if (InGroup
&& xdg_to_wm(xdg
, wm
)) {
128 (*addWMMenuEntryCallback
)(wm
);
130 init_xdg_storage(xdg
);
133 /* start processing group */
134 memset(buf
, 0, sizeof(buf
));
136 } else if (p
[0] == '[') {
137 /* If we find a new group and the previous group was the main one,
138 * we stop all further processing
144 memset(buf
, 0, sizeof(buf
));
149 if (key
== NULL
) { /* not `key' = `value' */
150 memset(buf
, 0, sizeof(buf
));
154 if (strcmp(key
, "Type") == 0) {
155 getStringValue(&tmp
, p
);
156 if (strcmp(tmp
, "Application") != 0)
157 InGroup
= 0; /* if not application, skip current group */
160 } else if (strcmp(key
, "Name") == 0) {
161 getLocalizedStringValue(&xdg
->Name
, p
, &xdg
->MatchLevel
);
162 } else if (strcmp(key
, "NoDisplay") == 0) {
163 if (getBooleanValue(p
)) /* if nodisplay, skip current group */
165 } else if (strcmp(key
, "Hidden") == 0) {
166 if (getBooleanValue(p
))
167 InGroup
= 0; /* if hidden, skip current group */
168 } else if (strcmp(key
, "TryExec") == 0) {
169 getStringValue(&xdg
->TryExec
, p
);
170 } else if (strcmp(key
, "Exec") == 0) {
171 getStringValue(&xdg
->Exec
, p
);
172 } else if (strcmp(key
, "Path") == 0) {
173 getStringValue(&xdg
->Path
, p
);
174 } else if (strcmp(key
, "Terminal") == 0) {
175 if (getBooleanValue(p
))
176 xdg
->Flags
|= F_TERMINAL
;
177 } else if (strcmp(key
, "Categories") == 0) {
178 getStringValue(&xdg
->Category
, p
);
179 getMenuHierarchyFor(&xdg
->Category
);
182 if (xdg
->Category
== NULL
)
183 xdg
->Category
= wstrdup(_("Other"));
191 /* at the end of the file, might as well try to menuize what we have
192 * unless there was no group at all or it was marked as hidden
194 if (InGroup
&& xdg_to_wm(xdg
, wm
))
195 (*addWMMenuEntryCallback
)(wm
);
200 /* coerce an xdg entry type into a wm entry type
202 static Bool
xdg_to_wm(XDGMenuEntry
*xdg
, WMMenuEntry
*wm
)
206 /* Exec or TryExec is mandatory */
207 if (!(xdg
->Exec
|| xdg
->TryExec
))
210 /* if there's no Name, use the first word of Exec or TryExec
213 wm
->Name
= xdg
->Name
;
216 wm
->Name
= wstrdup(xdg
->TryExec
);
218 wm
->Name
= wstrdup(xdg
->Exec
);
220 p
= strchr(wm
->Name
, ' ');
226 wm
->CmdLine
= xdg
->TryExec
;
227 else { /* xdg->Exec */
228 wm
->CmdLine
= parse_xdg_exec(xdg
->Exec
);
233 wm
->SubMenu
= xdg
->Category
;
234 wm
->Flags
= xdg
->Flags
;
235 if (wm
->CmdLine
!= xdg
->TryExec
)
236 wm
->Flags
|= F_FREE_CMD_LINE
;
241 static char *parse_xdg_exec(char *exec
)
243 char *cmd_line
, *dst
, *src
;
246 cmd_line
= wstrdup(exec
);
248 for (dst
= src
= cmd_line
; *src
; src
++) {
252 else if (*src
== '\\')
268 else if (*src
== '%') {
272 else if (strchr ("fFuUdDnNickvm", *src
))
274 * Skip valid field-code.
279 * Invalid field-code.
292 while (dst
> cmd_line
&& isspace(*--dst
));
301 /* (re-)initialize a XDGMenuEntry storage
303 static void init_xdg_storage(XDGMenuEntry
*xdg
)
313 wfree(xdg
->Category
);
320 xdg
->Category
= NULL
;
323 xdg
->MatchLevel
= -1;
326 /* (re-)initialize a WMMenuEntry storage
328 static void init_wm_storage(WMMenuEntry
*wm
)
330 if (wm
->Flags
& F_FREE_CMD_LINE
)
338 /* get a key from line. allocates target, which must be wfreed later */
339 static void getKey(char **target
, const char *line
)
346 if (strchr(p
, '=') == NULL
) { /* not `key' = `value' */
353 /* skip whitespace */
354 while (isspace(*(p
+ kstart
)))
357 /* skip up until first whitespace or '[' (localestring) or '=' */
359 while (*(p
+ kend
) && !isspace(*(p
+ kend
)) && *(p
+ kend
) != '=' && *(p
+ kend
) != '[')
362 *target
= wstrndup(p
+ kstart
, kend
- kstart
);
365 /* get a string value from line. allocates target, which must be wfreed later. */
366 static void getStringValue(char **target
, const char *line
)
375 /* skip until after '=' */
376 while (*(p
+ kstart
) && *(p
+ kstart
) != '=')
380 /* skip whitespace */
381 while (*(p
+ kstart
) && isspace(*(p
+ kstart
)))
384 *target
= wstrdup(p
+ kstart
);
386 for (p
= q
= *target
; *p
; p
++) {
391 case 's': *q
++ = ' '; break;
392 case 'n': *q
++ = '\n'; break;
393 case 't': *q
++ = '\t'; break;
394 case 'r': *q
++ = '\r'; break;
395 case '\\': *q
++ = '\\'; break;
398 * Skip invalid escape.
407 /* get a localized string value from line. allocates target, which must be wfreed later.
408 * matching is dependent on the current value of target as well as on the
409 * level the current value is matched on. guts matching algorithm is in
410 * compare_matchlevel().
412 static void getLocalizedStringValue(char **target
, const char *line
, int *match_level
)
417 int sqbstart
, sqbend
;
425 /* skip until after '=', mark if '[' and ']' is found */
426 while (*(p
+ kstart
) && *(p
+ kstart
) != '=') {
427 switch (*(p
+ kstart
)) {
428 case '[': sqbstart
= kstart
+ 1;break;
429 case ']': sqbend
= kstart
; break;
436 /* skip whitespace */
437 while (isspace(*(p
+ kstart
)))
440 if (sqbstart
> 0 && sqbend
> sqbstart
)
441 locale
= wstrndup(p
+ sqbstart
, sqbend
- sqbstart
);
443 /* if there is no value yet and this is the default key, return */
444 if (!*target
&& !locale
) {
445 *match_level
= MATCH_DEFAULT
;
446 *target
= wstrdup(p
+ kstart
);
450 if (compare_matchlevel(match_level
, locale
)) {
451 *target
= wstrdup(p
+ kstart
);
459 /* get a boolean value from line */
460 static Bool
getBooleanValue(const char *line
)
465 getStringValue(&p
, line
);
466 ret
= strcmp(p
, "true") == 0 ? True
: False
;
472 /* perform locale matching by implementing the algorithm specified in
473 * xdg desktop entry specification, section "localized values for keys".
475 static Bool
compare_matchlevel(int *current_level
, const char *found_locale
)
477 /* current key locale */
478 char *key_lang
, *key_ctry
, *key_enc
, *key_mod
;
480 parse_locale(found_locale
, &key_lang
, &key_ctry
, &key_enc
, &key_mod
);
482 if (env_lang
&& key_lang
&& strcmp(env_lang
, key_lang
) != 0) {
484 * Shortcut: if key and env languages don't match,
485 * don't even bother. This takes care of the great
486 * majority of the cases without having to go through
487 * the more theoretical parts of the spec'd algo.
496 if (!env_mod
&& key_mod
) {
498 * If LC_MESSAGES does not have a MODIFIER field,
499 * then no key with a modifier will be matched.
508 if (!env_ctry
&& key_ctry
) {
510 * Similarly, if LC_MESSAGES does not have a COUNTRY field,
511 * then no key with a country specified will be matched.
520 /* LC_MESSAGES value: lang_COUNTRY@MODIFIER */
521 if (env_lang
&& env_ctry
&& env_mod
) { /* lang_COUNTRY@MODIFIER */
522 if (key_lang
&& key_ctry
&& key_mod
&&
523 strcmp(env_lang
, key_lang
) == 0 &&
524 strcmp(env_ctry
, key_ctry
) == 0 &&
525 strcmp(env_mod
, key_mod
) == 0) {
526 *current_level
= MATCH_LANG_COUNTRY_MODIFIER
;
532 } else if (key_lang
&& key_ctry
&& /* lang_COUNTRY */
533 strcmp(env_lang
, key_lang
) == 0 &&
534 strcmp(env_ctry
, key_ctry
) == 0 &&
535 *current_level
< MATCH_LANG_COUNTRY
) {
536 *current_level
= MATCH_LANG_COUNTRY
;
542 } else if (key_lang
&& key_mod
&& /* lang@MODIFIER */
543 strcmp(env_lang
, key_lang
) == 0 &&
544 strcmp(env_mod
, key_mod
) == 0 &&
545 *current_level
< MATCH_LANG_MODIFIER
) {
546 *current_level
= MATCH_LANG_MODIFIER
;
552 } else if (key_lang
&& /* lang */
553 strcmp(env_lang
, key_lang
) == 0 &&
554 *current_level
< MATCH_LANG
) {
555 *current_level
= MATCH_LANG
;
570 /* LC_MESSAGES value: lang_COUNTRY */
571 if (env_lang
&& env_ctry
) { /* lang_COUNTRY */
572 if (key_lang
&& key_ctry
&&
573 strcmp(env_lang
, key_lang
) == 0 &&
574 strcmp(env_ctry
, key_ctry
) == 0 &&
575 *current_level
< MATCH_LANG_COUNTRY
) {
576 *current_level
= MATCH_LANG_COUNTRY
;
582 } else if (key_lang
&& /* lang */
583 strcmp(env_lang
, key_lang
) == 0 &&
584 *current_level
< MATCH_LANG
) {
585 *current_level
= MATCH_LANG
;
600 /* LC_MESSAGES value: lang@MODIFIER */
601 if (env_lang
&& env_mod
) { /* lang@MODIFIER */
602 if (key_lang
&& key_mod
&&
603 strcmp(env_lang
, key_lang
) == 0 &&
604 strcmp(env_mod
, key_mod
) == 0 &&
605 *current_level
< MATCH_LANG_MODIFIER
) {
606 *current_level
= MATCH_LANG_MODIFIER
;
612 } else if (key_lang
&& /* lang */
613 strcmp(env_lang
, key_lang
) == 0 &&
614 *current_level
< MATCH_LANG
) {
615 *current_level
= MATCH_LANG
;
630 /* LC_MESSAGES value: lang */
631 if (env_lang
) { /* lang */
633 strcmp(env_lang
, key_lang
) == 0 &&
634 *current_level
< MATCH_LANG
) {
635 *current_level
= MATCH_LANG
;
650 /* MATCH_DEFAULT is handled in getLocalizedStringValue */
659 /* get the (first) xdg main category from a list of categories
661 static void getMenuHierarchyFor(char **xdgmenuspec
)
666 if (!*xdgmenuspec
|| !**xdgmenuspec
)
669 category
= wstrdup(*xdgmenuspec
);
671 memset(buf
, 0, sizeof(buf
));
673 p
= strtok(category
, ";");
674 while (p
) { /* get a known category */
675 if (strcmp(p
, "AudioVideo") == 0) {
676 snprintf(buf
, sizeof(buf
), "%s", _("Audio & Video"));
678 } else if (strcmp(p
, "Audio") == 0) {
679 snprintf(buf
, sizeof(buf
), "%s", _("Audio"));
681 } else if (strcmp(p
, "Video") == 0) {
682 snprintf(buf
, sizeof(buf
), "%s", _("Video"));
684 } else if (strcmp(p
, "Development") == 0) {
685 snprintf(buf
, sizeof(buf
), "%s", _("Development"));
687 } else if (strcmp(p
, "Education") == 0) {
688 snprintf(buf
, sizeof(buf
), "%s", _("Education"));
690 } else if (strcmp(p
, "Game") == 0) {
691 snprintf(buf
, sizeof(buf
), "%s", _("Game"));
693 } else if (strcmp(p
, "Graphics") == 0) {
694 snprintf(buf
, sizeof(buf
), "%s", _("Graphics"));
696 } else if (strcmp(p
, "Network") == 0) {
697 snprintf(buf
, sizeof(buf
), "%s", _("Network"));
699 } else if (strcmp(p
, "Office") == 0) {
700 snprintf(buf
, sizeof(buf
), "%s", _("Office"));
702 } else if (strcmp(p
, "Science") == 0) {
703 snprintf(buf
, sizeof(buf
), "%s", _("Science"));
705 } else if (strcmp(p
, "Settings") == 0) {
706 snprintf(buf
, sizeof(buf
), "%s", _("Settings"));
708 } else if (strcmp(p
, "System") == 0) {
709 snprintf(buf
, sizeof(buf
), "%s", _("System"));
711 } else if (strcmp(p
, "Utility") == 0) {
712 snprintf(buf
, sizeof(buf
), "%s", _("Utility"));
714 /* reserved categories */
715 } else if (strcmp(p
, "Screensaver") == 0) {
716 snprintf(buf
, sizeof(buf
), "%s", _("Screensaver"));
718 } else if (strcmp(p
, "TrayIcon") == 0) {
719 snprintf(buf
, sizeof(buf
), "%s", _("Tray Icon"));
721 } else if (strcmp(p
, "Applet") == 0) {
722 snprintf(buf
, sizeof(buf
), "%s", _("Applet"));
724 } else if (strcmp(p
, "Shell") == 0) {
725 snprintf(buf
, sizeof(buf
), "%s", _("Shell"));
728 p
= strtok(NULL
, ";");
733 if (!*buf
) /* come up with something if nothing found */
734 snprintf(buf
, sizeof(buf
), "%s", _("Other"));
736 *xdgmenuspec
= wstrdup(buf
);