]> www.fi.muni.cz Git - evince.git/blob - cut-n-paste/smclient/eggdesktopfile.c
EggSMClient copied from libegg
[evince.git] / cut-n-paste / smclient / eggdesktopfile.c
1 /* eggdesktopfile.c - Freedesktop.Org Desktop Files
2  * Copyright (C) 2007 Novell, Inc.
3  *
4  * Based on gnome-desktop-item.c
5  * Copyright (C) 1999, 2000 Red Hat Inc.
6  * Copyright (C) 2001 George Lebl
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public License
10  * as published by the Free Software Foundation; either version 2 of
11  * the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful, but
14  * WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; see the file COPYING.LIB. If not,
20  * write to the Free Software Foundation, Inc., 59 Temple Place -
21  * Suite 330, Boston, MA 02111-1307, USA.
22  */
23
24 #ifdef HAVE_CONFIG_H
25 #include "config.h"
26 #endif
27
28 #include "eggdesktopfile.h"
29
30 #include <string.h>
31 #include <unistd.h>
32
33 #include <glib/gi18n.h>
34 #include <gdk/gdk.h>
35 #include <gtk/gtkwindow.h>
36 #include <gdk/gdkx.h>
37
38 struct EggDesktopFile {
39   GKeyFile           *key_file;
40   char               *source;
41
42   char               *name, *icon;
43   EggDesktopFileType  type;
44   char                document_code;
45 };
46
47 /**
48  * egg_desktop_file_new:
49  * @desktop_file_path: path to a Freedesktop-style Desktop file
50  * @error: error pointer
51  *
52  * Creates a new #EggDesktopFile for @desktop_file.
53  *
54  * Return value: the new #EggDesktopFile, or %NULL on error.
55  **/
56 EggDesktopFile *
57 egg_desktop_file_new (const char *desktop_file_path, GError **error)
58 {
59   EggDesktopFile *desktop_file;
60   GKeyFile *key_file;
61
62   key_file = g_key_file_new ();
63   if (!g_key_file_load_from_file (key_file, desktop_file_path, 0, error))
64     {
65       g_key_file_free (key_file);
66       return NULL;
67     }
68
69   desktop_file = egg_desktop_file_new_from_key_file (key_file,
70                                                      desktop_file_path,
71                                                      error);
72   if (!desktop_file)
73     g_key_file_free (key_file);
74
75   return desktop_file;
76 }
77
78 /**
79  * egg_desktop_file_new_from_data_dirs:
80  * @desktop_file_path: relative path to a Freedesktop-style Desktop file
81  * @error: error pointer
82  *
83  * Looks for @desktop_file_path in the paths returned from
84  * g_get_user_data_dir() and g_get_system_data_dirs(), and creates
85  * a new #EggDesktopFile from it.
86  *
87  * Return value: the new #EggDesktopFile, or %NULL on error.
88  **/
89 EggDesktopFile *
90 egg_desktop_file_new_from_data_dirs (const char  *desktop_file_path,
91                                      GError     **error)
92 {
93   EggDesktopFile *desktop_file;
94   GKeyFile *key_file;
95   char *full_path;
96
97   key_file = g_key_file_new ();
98   if (!g_key_file_load_from_data_dirs (key_file, desktop_file_path,
99                                        &full_path, 0, error))
100     {
101       g_key_file_free (key_file);
102       return NULL;
103     }
104
105   desktop_file = egg_desktop_file_new_from_key_file (key_file,
106                                                      full_path,
107                                                      error);
108   g_free (full_path);
109   if (!desktop_file)
110     g_key_file_free (key_file);
111
112   return desktop_file;
113 }
114
115 /**
116  * egg_desktop_file_new_from_key_file:
117  * @key_file: a #GKeyFile representing a desktop file
118  * @source: the path or URI that @key_file was loaded from, or %NULL
119  * @error: error pointer
120  *
121  * Creates a new #EggDesktopFile for @key_file. Assumes ownership of
122  * @key_file on success (meaning it will be freed when the desktop_file
123  * is freed).
124  *
125  * Return value: the new #EggDesktopFile, or %NULL on error.
126  **/
127 EggDesktopFile *
128 egg_desktop_file_new_from_key_file (GKeyFile    *key_file,
129                                     const char  *source,
130                                     GError     **error)
131 {
132   EggDesktopFile *desktop_file;
133   char *version, *type;
134
135   if (!g_key_file_has_group (key_file, EGG_DESKTOP_FILE_GROUP))
136     {
137       g_set_error (error, EGG_DESKTOP_FILE_ERROR,
138                    EGG_DESKTOP_FILE_ERROR_INVALID,
139                    _("File is not a valid .desktop file"));
140       return NULL;
141     }
142
143   version = g_key_file_get_value (key_file, EGG_DESKTOP_FILE_GROUP,
144                                   EGG_DESKTOP_FILE_KEY_VERSION,
145                                   NULL);
146   if (version)
147     {
148       double version_num;
149       char *end;
150
151       version_num = g_ascii_strtod (version, &end);
152       if (*end)
153         {
154           g_warning ("Invalid Version string '%s' in %s",
155                      version, source ? source : "(unknown)");
156         }
157       else if (version_num > 1.0)
158         {
159           g_set_error (error, EGG_DESKTOP_FILE_ERROR,
160                        EGG_DESKTOP_FILE_ERROR_INVALID,
161                        _("Unrecognized desktop file Version '%s'"), version);
162           g_free (version);
163           return NULL;
164         }
165       else 
166       g_free (version);
167     }
168
169   desktop_file = g_new0 (EggDesktopFile, 1);
170
171   if (g_path_is_absolute (source))
172     desktop_file->source = g_filename_to_uri (source, NULL, NULL);
173   else
174     desktop_file->source = g_strdup (source);
175
176   desktop_file->name = g_key_file_get_string (key_file, EGG_DESKTOP_FILE_GROUP,
177                                               EGG_DESKTOP_FILE_KEY_NAME, error);
178   if (!desktop_file->name)
179     {
180       egg_desktop_file_free (desktop_file);
181       return NULL;
182     }
183
184   type = g_key_file_get_string (key_file, EGG_DESKTOP_FILE_GROUP,
185                                 EGG_DESKTOP_FILE_KEY_TYPE, error);
186   if (!type)
187     {
188       egg_desktop_file_free (desktop_file);
189       return NULL;
190     }
191
192   if (!strcmp (type, "Application"))
193     {
194       char *exec, *p;
195
196       desktop_file->type = EGG_DESKTOP_FILE_TYPE_APPLICATION;
197
198       exec = g_key_file_get_string (key_file,
199                                     EGG_DESKTOP_FILE_GROUP,
200                                     EGG_DESKTOP_FILE_KEY_EXEC,
201                                     error);
202       if (!exec)
203         {
204           egg_desktop_file_free (desktop_file);
205           g_free(type);
206           return NULL;
207         }
208
209       /* See if it takes paths or URIs or neither */
210       for (p = exec; *p; p++)
211         {
212           if (*p == '%')
213             {
214               if (p[1] == '\0' || strchr ("FfUu", p[1]))
215                 {
216                   desktop_file->document_code = p[1];
217                   break;
218                 }
219               p++;
220             }
221         }
222
223       g_free (exec);
224     }
225   else if (!strcmp (type, "Link"))
226     {
227       char *url;
228
229       desktop_file->type = EGG_DESKTOP_FILE_TYPE_LINK;
230
231       url = g_key_file_get_string (key_file,
232                                    EGG_DESKTOP_FILE_GROUP,
233                                    EGG_DESKTOP_FILE_KEY_URL,
234                                    error);
235       if (!url)
236         {
237           egg_desktop_file_free (desktop_file);
238           g_free(type);
239           return NULL;
240         }
241       g_free (url);
242     }
243   else if (!strcmp (type, "Directory"))
244     desktop_file->type = EGG_DESKTOP_FILE_TYPE_DIRECTORY;
245   else
246     desktop_file->type = EGG_DESKTOP_FILE_TYPE_UNRECOGNIZED;
247
248   g_free(type);
249
250   /* Check the Icon key */
251   desktop_file->icon = g_key_file_get_string (key_file,
252                                               EGG_DESKTOP_FILE_GROUP,
253                                               EGG_DESKTOP_FILE_KEY_ICON,
254                                               NULL);
255   if (desktop_file->icon && !g_path_is_absolute (desktop_file->icon))
256     {
257       char *ext;
258
259       /* Lots of .desktop files still get this wrong */
260       ext = strrchr (desktop_file->icon, '.');
261       if (ext && (!strcmp (ext, ".png") ||
262                   !strcmp (ext, ".xpm") ||
263                   !strcmp (ext, ".svg")))
264         {
265           g_warning ("Desktop file '%s' has malformed Icon key '%s'"
266                      "(should not include extension)",
267                      source ? source : "(unknown)",
268                      desktop_file->icon);
269           *ext = '\0';
270         }
271     }
272
273   desktop_file->key_file = key_file;
274   return desktop_file;
275 }
276
277 /**
278  * egg_desktop_file_free:
279  * @desktop_file: an #EggDesktopFile
280  *
281  * Frees @desktop_file.
282  **/
283 void
284 egg_desktop_file_free (EggDesktopFile *desktop_file)
285 {
286   g_key_file_free (desktop_file->key_file);
287   g_free (desktop_file->source);
288   g_free (desktop_file->name);
289   g_free (desktop_file->icon);
290   g_free (desktop_file);
291 }
292
293 /**
294  * egg_desktop_file_get_key_file:
295  * @desktop_file: an #EggDesktopFile
296  *
297  * Gets the #GKeyFile associated with @desktop_file. You must not free
298  * this value, and changes made to it will not be reflected by
299  * @desktop_file.
300  *
301  * Return value: the #GKeyFile associated with @desktop_file.
302  **/
303 GKeyFile *
304 egg_desktop_file_get_key_file (EggDesktopFile *desktop_file)
305 {
306   return desktop_file->key_file;
307 }
308
309 /**
310  * egg_desktop_file_get_source:
311  * @desktop_file: an #EggDesktopFile
312  *
313  * Gets the URI that @desktop_file was loaded from.
314  *
315  * Return value: @desktop_file's source URI
316  **/
317 const char *
318 egg_desktop_file_get_source (EggDesktopFile *desktop_file)
319 {
320   return desktop_file->source;
321 }
322
323 /**
324  * egg_desktop_file_get_desktop_file_type:
325  * @desktop_file: an #EggDesktopFile
326  *
327  * Gets the desktop file type of @desktop_file.
328  *
329  * Return value: @desktop_file's type
330  **/
331 EggDesktopFileType
332 egg_desktop_file_get_desktop_file_type (EggDesktopFile *desktop_file)
333 {
334   return desktop_file->type;
335 }
336
337 /**
338  * egg_desktop_file_get_name:
339  * @desktop_file: an #EggDesktopFile
340  *
341  * Gets the (localized) value of @desktop_file's "Name" key.
342  *
343  * Return value: the application/link name
344  **/
345 const char *
346 egg_desktop_file_get_name (EggDesktopFile *desktop_file)
347 {
348   return desktop_file->name;
349 }
350
351 /**
352  * egg_desktop_file_get_icon:
353  * @desktop_file: an #EggDesktopFile
354  *
355  * Gets the value of @desktop_file's "Icon" key.
356  *
357  * If the icon string is a full path (that is, if g_path_is_absolute()
358  * returns %TRUE when called on it), it points to a file containing an
359  * unthemed icon. If the icon string is not a full path, it is the
360  * name of a themed icon, which can be looked up with %GtkIconTheme,
361  * or passed directly to a theme-aware widget like %GtkImage or
362  * %GtkCellRendererPixbuf.
363  *
364  * Return value: the icon path or name
365  **/
366 const char *
367 egg_desktop_file_get_icon (EggDesktopFile *desktop_file)
368 {
369   return desktop_file->icon;
370 }
371
372 /**
373  * egg_desktop_file_can_launch:
374  * @desktop_file: an #EggDesktopFile
375  * @desktop_environment: the name of the running desktop environment,
376  * or %NULL
377  *
378  * Tests if @desktop_file can/should be launched in the current
379  * environment. If @desktop_environment is non-%NULL, @desktop_file's
380  * "OnlyShowIn" and "NotShowIn" keys are checked to make sure that
381  * this desktop_file is appropriate for the named environment.
382  *
383  * Furthermore, if @desktop_file has type
384  * %EGG_DESKTOP_FILE_TYPE_APPLICATION, its "TryExec" key (if any) is
385  * also checked, to make sure the binary it points to exists.
386  *
387  * egg_desktop_file_can_launch() does NOT check the value of the
388  * "Hidden" key.
389  *
390  * Return value: %TRUE if @desktop_file can be launched
391  **/
392 gboolean
393 egg_desktop_file_can_launch (EggDesktopFile *desktop_file,
394                              const char     *desktop_environment)
395 {
396   char *try_exec, *found_program;
397   char **only_show_in, **not_show_in;
398   gboolean found;
399   int i;
400
401   if (desktop_file->type != EGG_DESKTOP_FILE_TYPE_APPLICATION &&
402       desktop_file->type != EGG_DESKTOP_FILE_TYPE_LINK)
403     return FALSE;
404
405   if (desktop_environment)
406     {
407       only_show_in = g_key_file_get_string_list (desktop_file->key_file,
408                                                  EGG_DESKTOP_FILE_GROUP,
409                                                  EGG_DESKTOP_FILE_KEY_ONLY_SHOW_IN,
410                                                  NULL, NULL);
411       if (only_show_in)
412         {
413           for (i = 0, found = FALSE; only_show_in[i] && !found; i++)
414             {
415               if (!strcmp (only_show_in[i], desktop_environment))
416                 found = TRUE;
417             }
418
419           g_strfreev (only_show_in);
420
421           if (!found)
422             return FALSE;
423         }
424
425       not_show_in = g_key_file_get_string_list (desktop_file->key_file,
426                                                 EGG_DESKTOP_FILE_GROUP,
427                                                 EGG_DESKTOP_FILE_KEY_NOT_SHOW_IN,
428                                                 NULL, NULL);
429       if (not_show_in)
430         {
431           for (i = 0, found = FALSE; not_show_in[i] && !found; i++)
432             {
433               if (!strcmp (not_show_in[i], desktop_environment))
434                 found = TRUE;
435             }
436
437           g_strfreev (not_show_in);
438
439           if (found)
440             return FALSE;
441         }
442     }
443
444   if (desktop_file->type == EGG_DESKTOP_FILE_TYPE_APPLICATION)
445     {
446       try_exec = g_key_file_get_string (desktop_file->key_file,
447                                         EGG_DESKTOP_FILE_GROUP,
448                                         EGG_DESKTOP_FILE_KEY_TRY_EXEC,
449                                         NULL);
450       if (try_exec)
451         {
452           found_program = g_find_program_in_path (try_exec);
453           g_free (try_exec);
454
455           if (!found_program)
456             return FALSE;
457           g_free (found_program);
458         }
459     }
460
461   return TRUE;
462 }
463
464 /**
465  * egg_desktop_file_accepts_documents:
466  * @desktop_file: an #EggDesktopFile
467  *
468  * Tests if @desktop_file represents an application that can accept
469  * documents on the command line.
470  *
471  * Return value: %TRUE or %FALSE
472  **/
473 gboolean
474 egg_desktop_file_accepts_documents (EggDesktopFile *desktop_file)
475 {
476   return desktop_file->document_code != 0;
477 }
478
479 /**
480  * egg_desktop_file_accepts_multiple:
481  * @desktop_file: an #EggDesktopFile
482  *
483  * Tests if @desktop_file can accept multiple documents at once.
484  *
485  * If this returns %FALSE, you can still pass multiple documents to
486  * egg_desktop_file_launch(), but that will result in multiple copies
487  * of the application being launched. See egg_desktop_file_launch()
488  * for more details.
489  *
490  * Return value: %TRUE or %FALSE
491  **/
492 gboolean
493 egg_desktop_file_accepts_multiple (EggDesktopFile *desktop_file)
494 {
495   return (desktop_file->document_code == 'F' ||
496           desktop_file->document_code == 'U');
497 }
498
499 /**
500  * egg_desktop_file_accepts_uris:
501  * @desktop_file: an #EggDesktopFile
502  *
503  * Tests if @desktop_file can accept (non-"file:") URIs as documents to
504  * open.
505  *
506  * Return value: %TRUE or %FALSE
507  **/
508 gboolean
509 egg_desktop_file_accepts_uris (EggDesktopFile *desktop_file)
510 {
511   return (desktop_file->document_code == 'U' ||
512           desktop_file->document_code == 'u');
513 }
514
515 static void
516 append_quoted_word (GString    *str,
517                     const char *s,
518                     gboolean    in_single_quotes,
519                     gboolean    in_double_quotes)
520 {
521   const char *p;
522
523   if (!in_single_quotes && !in_double_quotes)
524     g_string_append_c (str, '\'');
525   else if (!in_single_quotes && in_double_quotes)
526     g_string_append (str, "\"'");
527
528   if (!strchr (s, '\''))
529     g_string_append (str, s);
530   else
531     {
532       for (p = s; *p != '\0'; p++)
533         {
534           if (*p == '\'')
535             g_string_append (str, "'\\''");
536           else
537             g_string_append_c (str, *p);
538         }
539     }
540
541   if (!in_single_quotes && !in_double_quotes)
542     g_string_append_c (str, '\'');
543   else if (!in_single_quotes && in_double_quotes)
544     g_string_append (str, "'\"");
545 }
546
547 static void
548 do_percent_subst (EggDesktopFile *desktop_file,
549                   char            code,
550                   GString        *str,
551                   GSList        **documents,
552                   gboolean        in_single_quotes,
553                   gboolean        in_double_quotes)
554 {
555   GSList *d;
556   char *doc;
557
558   switch (code)
559     {
560     case '%':
561       g_string_append_c (str, '%');
562       break;
563
564     case 'F':
565     case 'U':
566       for (d = *documents; d; d = d->next)
567         {
568           doc = d->data;
569           g_string_append (str, " ");
570           append_quoted_word (str, doc, in_single_quotes, in_double_quotes);
571         }
572       *documents = NULL;
573       break;
574
575     case 'f':
576     case 'u':
577       if (*documents)
578         {
579           doc = (*documents)->data;
580           g_string_append (str, " ");
581           append_quoted_word (str, doc, in_single_quotes, in_double_quotes);
582           *documents = (*documents)->next;
583         }
584       break;
585
586     case 'i':
587       if (desktop_file->icon)
588         {
589           g_string_append (str, "--icon ");
590           append_quoted_word (str, desktop_file->icon,
591                               in_single_quotes, in_double_quotes);
592         }
593       break;
594
595     case 'c':
596       if (desktop_file->name)
597         {
598           append_quoted_word (str, desktop_file->name,
599                               in_single_quotes, in_double_quotes);
600         }
601       break;
602
603     case 'k':
604       if (desktop_file->source)
605         {
606           append_quoted_word (str, desktop_file->source,
607                               in_single_quotes, in_double_quotes);
608         }
609       break;
610
611     case 'D':
612     case 'N':
613     case 'd':
614     case 'n':
615     case 'v':
616     case 'm':
617       /* Deprecated; skip */
618       break;
619
620     default:
621       g_warning ("Unrecognized %%-code '%%%c' in Exec", code);
622       break;
623     }
624 }
625
626 static char *
627 parse_exec (EggDesktopFile  *desktop_file,
628             GSList         **documents,
629             GError         **error)
630 {
631   char *exec, *p, *command;
632   gboolean escape, single_quot, double_quot;
633   GString *gs;
634
635   exec = g_key_file_get_string (desktop_file->key_file,
636                                 EGG_DESKTOP_FILE_GROUP,
637                                 EGG_DESKTOP_FILE_KEY_EXEC,
638                                 error);
639   if (!exec)
640     return NULL;
641
642   /* Build the command */
643   gs = g_string_new (NULL);
644   escape = single_quot = double_quot = FALSE;
645
646   for (p = exec; *p != '\0'; p++)
647     {
648       if (escape)
649         {
650           escape = FALSE;
651           g_string_append_c (gs, *p);
652         }
653       else if (*p == '\\')
654         {
655           if (!single_quot)
656             escape = TRUE;
657           g_string_append_c (gs, *p);
658         }
659       else if (*p == '\'')
660         {
661           g_string_append_c (gs, *p);
662           if (!single_quot && !double_quot)
663             single_quot = TRUE;
664           else if (single_quot)
665             single_quot = FALSE;
666         }
667       else if (*p == '"')
668         {
669           g_string_append_c (gs, *p);
670           if (!single_quot && !double_quot)
671             double_quot = TRUE;
672           else if (double_quot)
673             double_quot = FALSE;
674         }
675       else if (*p == '%' && p[1])
676         {
677           do_percent_subst (desktop_file, p[1], gs, documents,
678                             single_quot, double_quot);
679           p++;
680         }
681       else
682         g_string_append_c (gs, *p);
683     }
684
685   g_free (exec);
686   command = g_string_free (gs, FALSE);
687
688   /* Prepend "xdg-terminal " if needed (FIXME: use gvfs) */
689   if (g_key_file_has_key (desktop_file->key_file,
690                           EGG_DESKTOP_FILE_GROUP,
691                           EGG_DESKTOP_FILE_KEY_TERMINAL,
692                           NULL))
693     {
694       GError *terminal_error = NULL;
695       gboolean use_terminal =
696         g_key_file_get_boolean (desktop_file->key_file,
697                                 EGG_DESKTOP_FILE_GROUP,
698                                 EGG_DESKTOP_FILE_KEY_TERMINAL,
699                                 &terminal_error);
700       if (terminal_error)
701         {
702           g_free (command);
703           g_propagate_error (error, terminal_error);
704           return NULL;
705         }
706
707       if (use_terminal)
708         {
709           gs = g_string_new ("xdg-terminal ");
710           append_quoted_word (gs, command, FALSE, FALSE);
711           g_free (command);
712           command = g_string_free (gs, FALSE);
713         }
714     }
715
716   return command;
717 }
718
719 static GSList *
720 translate_document_list (EggDesktopFile *desktop_file, GSList *documents)
721 {
722   gboolean accepts_uris = egg_desktop_file_accepts_uris (desktop_file);
723   GSList *ret, *d;
724
725   for (d = documents, ret = NULL; d; d = d->next)
726     {
727       const char *document = d->data;
728       gboolean is_uri = !g_path_is_absolute (document);
729       char *translated;
730
731       if (accepts_uris)
732         {
733           if (is_uri)
734             translated = g_strdup (document);
735           else
736             translated = g_filename_to_uri (document, NULL, NULL);
737         }
738       else
739         {
740           if (is_uri)
741             translated = g_filename_from_uri (document, NULL, NULL);
742           else
743             translated = g_strdup (document);
744         }
745
746       if (translated)
747         ret = g_slist_prepend (ret, translated);
748     }
749
750   return g_slist_reverse (ret);
751 }
752
753 static void
754 free_document_list (GSList *documents)
755 {
756   GSList *d;
757
758   for (d = documents; d; d = d->next)
759     g_free (d->data);
760   g_slist_free (documents);
761 }
762
763 /**
764  * egg_desktop_file_parse_exec:
765  * @desktop_file: a #EggDesktopFile
766  * @documents: a list of document paths or URIs
767  * @error: error pointer
768  *
769  * Parses @desktop_file's Exec key, inserting @documents into it, and
770  * returns the result.
771  *
772  * If @documents contains non-file: URIs and @desktop_file does not
773  * accept URIs, those URIs will be ignored. Likewise, if @documents
774  * contains more elements than @desktop_file accepts, the extra
775  * documents will be ignored.
776  *
777  * Return value: the parsed Exec string
778  **/
779 char *
780 egg_desktop_file_parse_exec (EggDesktopFile  *desktop_file,
781                              GSList          *documents,
782                              GError         **error)
783 {
784   GSList *translated, *docs;
785   char *command;
786
787   docs = translated = translate_document_list (desktop_file, documents);
788   command = parse_exec (desktop_file, &docs, error);
789   free_document_list (translated);
790
791   return command;
792 }
793
794 static gboolean
795 parse_link (EggDesktopFile  *desktop_file,
796             EggDesktopFile **app_desktop_file,
797             GSList         **documents,
798             GError         **error)
799 {
800   char *url;
801   GKeyFile *key_file;
802
803   url = g_key_file_get_string (desktop_file->key_file,
804                                EGG_DESKTOP_FILE_GROUP,
805                                EGG_DESKTOP_FILE_KEY_URL,
806                                error);
807   if (!url)
808     return FALSE;
809   *documents = g_slist_prepend (NULL, url);
810
811   /* FIXME: use gvfs */
812   key_file = g_key_file_new ();
813   g_key_file_set_string (key_file, EGG_DESKTOP_FILE_GROUP,
814                          EGG_DESKTOP_FILE_KEY_NAME,
815                          "xdg-open");
816   g_key_file_set_string (key_file, EGG_DESKTOP_FILE_GROUP,
817                          EGG_DESKTOP_FILE_KEY_TYPE,
818                          "Application");
819   g_key_file_set_string (key_file, EGG_DESKTOP_FILE_GROUP,
820                          EGG_DESKTOP_FILE_KEY_EXEC,
821                          "xdg-open %u");
822   *app_desktop_file = egg_desktop_file_new_from_key_file (key_file, NULL, NULL);
823   return TRUE;
824 }
825
826 #ifdef HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE
827 static char *
828 start_startup_notification (GdkDisplay     *display,
829                             EggDesktopFile *desktop_file,
830                             const char     *argv0,
831                             int             screen,
832                             int             workspace,
833                             guint32         launch_time)
834 {
835   static int sequence = 0;
836   char *startup_id;
837   char *description, *wmclass;
838   char *screen_str, *workspace_str;
839
840   if (g_key_file_has_key (desktop_file->key_file,
841                           EGG_DESKTOP_FILE_GROUP,
842                           EGG_DESKTOP_FILE_KEY_STARTUP_NOTIFY,
843                           NULL))
844     {
845       if (!g_key_file_get_boolean (desktop_file->key_file,
846                                    EGG_DESKTOP_FILE_GROUP,
847                                    EGG_DESKTOP_FILE_KEY_STARTUP_NOTIFY,
848                                    NULL))
849         return NULL;
850       wmclass = NULL;
851     }
852   else
853     {
854       wmclass = g_key_file_get_string (desktop_file->key_file,
855                                        EGG_DESKTOP_FILE_GROUP,
856                                        EGG_DESKTOP_FILE_KEY_STARTUP_WM_CLASS,
857                                        NULL);
858       if (!wmclass)
859         return NULL;
860     }
861
862   if (launch_time == (guint32)-1)
863     launch_time = gdk_x11_display_get_user_time (display);
864   startup_id = g_strdup_printf ("%s-%lu-%s-%s-%d_TIME%lu",
865                                 g_get_prgname (),
866                                 (unsigned long)getpid (),
867                                 g_get_host_name (),
868                                 argv0,
869                                 sequence++,
870                                 (unsigned long)launch_time);
871
872   description = g_strdup_printf (_("Starting %s"), desktop_file->name);
873   screen_str = g_strdup_printf ("%d", screen);
874   workspace_str = workspace == -1 ? NULL : g_strdup_printf ("%d", workspace);
875
876   gdk_x11_display_broadcast_startup_message (display, "new",
877                                              "ID", startup_id,
878                                              "NAME", desktop_file->name,
879                                              "SCREEN", screen_str,
880                                              "BIN", argv0,
881                                              "ICON", desktop_file->icon,
882                                              "DESKTOP", workspace_str,
883                                              "DESCRIPTION", description,
884                                              "WMCLASS", wmclass,
885                                              NULL);
886
887   g_free (description);
888   g_free (wmclass);
889   g_free (screen_str);
890   g_free (workspace_str);
891
892   return startup_id;
893 }
894
895 static void
896 end_startup_notification (GdkDisplay *display,
897                           const char *startup_id)
898 {
899   gdk_x11_display_broadcast_startup_message (display, "remove",
900                                              "ID", startup_id,
901                                              NULL);
902 }
903
904 #define EGG_DESKTOP_FILE_SN_TIMEOUT_LENGTH (30 /* seconds */ * 1000)
905
906 typedef struct {
907   GdkDisplay *display;
908   char *startup_id;
909 } StartupNotificationData;
910
911 static gboolean
912 startup_notification_timeout (gpointer data)
913 {
914   StartupNotificationData *sn_data = data;
915
916   end_startup_notification (sn_data->display, sn_data->startup_id);
917   g_object_unref (sn_data->display);
918   g_free (sn_data->startup_id);
919   g_free (sn_data);
920
921   return FALSE;
922 }
923
924 static void
925 set_startup_notification_timeout (GdkDisplay *display,
926                                   const char *startup_id)
927 {
928   StartupNotificationData *sn_data;
929
930   sn_data = g_new (StartupNotificationData, 1);
931   sn_data->display = g_object_ref (display);
932   sn_data->startup_id = g_strdup (startup_id);
933
934   g_timeout_add (EGG_DESKTOP_FILE_SN_TIMEOUT_LENGTH,
935                  startup_notification_timeout, sn_data);
936 }
937 #endif /* HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE */
938
939 extern char **environ;
940
941 static GPtrArray *
942 array_putenv (GPtrArray *env, char *variable)
943 {
944   int i, keylen;
945
946   if (!env)
947     {
948       env = g_ptr_array_new ();
949
950       for (i = 0; environ[i]; i++)
951         g_ptr_array_add (env, g_strdup (environ[i]));
952     }
953
954   keylen = strcspn (variable, "=");
955
956   /* Remove old value of key */
957   for (i = 0; i < env->len; i++)
958     {
959       char *envvar = env->pdata[i];
960
961       if (!strncmp (envvar, variable, keylen) && envvar[keylen] == '=')
962         {
963           g_free (envvar);
964           g_ptr_array_remove_index_fast (env, i);
965           break;
966         }
967     }
968
969   /* Add new value */
970   g_ptr_array_add (env, g_strdup (variable));
971
972   return env;
973 }
974
975 static gboolean
976 egg_desktop_file_launchv (EggDesktopFile *desktop_file,
977                           GSList *documents, va_list args,
978                           GError **error)
979 {
980   EggDesktopFileLaunchOption option;
981   GSList *translated_documents, *docs;
982   char *command, **argv;
983   int argc, i, screen_num;
984   gboolean success, current_success;
985   GdkDisplay *display;
986   char *startup_id;
987
988   GPtrArray   *env = NULL;
989   char       **variables = NULL;
990   GdkScreen   *screen = NULL;
991   int          workspace = -1;
992   const char  *directory = NULL;
993   guint32      launch_time = (guint32)-1;
994   GSpawnFlags  flags = G_SPAWN_SEARCH_PATH;
995   GSpawnChildSetupFunc setup_func = NULL;
996   gpointer     setup_data = NULL;
997
998   GPid        *ret_pid = NULL;
999   int         *ret_stdin = NULL, *ret_stdout = NULL, *ret_stderr = NULL;
1000   char       **ret_startup_id = NULL;
1001
1002   if (documents && desktop_file->document_code == 0)
1003     {
1004       g_set_error (error, EGG_DESKTOP_FILE_ERROR,
1005                    EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE,
1006                    _("Application does not accept documents on command line"));
1007       return FALSE;
1008     }
1009
1010   /* Read the options: technically it's incorrect for the caller to
1011    * NULL-terminate the list of options (rather than 0-terminating
1012    * it), but NULL-terminating lets us use G_GNUC_NULL_TERMINATED,
1013    * it's more consistent with other glib/gtk methods, and it will
1014    * work as long as sizeof (int) <= sizeof (NULL), and NULL is
1015    * represented as 0. (Which is true everywhere we care about.)
1016    */
1017   while ((option = va_arg (args, EggDesktopFileLaunchOption)))
1018     {
1019       switch (option)
1020         {
1021         case EGG_DESKTOP_FILE_LAUNCH_CLEARENV:
1022           if (env)
1023             g_ptr_array_free (env, TRUE);
1024           env = g_ptr_array_new ();
1025           break;
1026         case EGG_DESKTOP_FILE_LAUNCH_PUTENV:
1027           variables = va_arg (args, char **);
1028           for (i = 0; variables[i]; i++)
1029             env = array_putenv (env, variables[i]);
1030           break;
1031
1032         case EGG_DESKTOP_FILE_LAUNCH_SCREEN:
1033           screen = va_arg (args, GdkScreen *);
1034           break;
1035         case EGG_DESKTOP_FILE_LAUNCH_WORKSPACE:
1036           workspace = va_arg (args, int);
1037           break;
1038
1039         case EGG_DESKTOP_FILE_LAUNCH_DIRECTORY:
1040           directory = va_arg (args, const char *);
1041           break;
1042         case EGG_DESKTOP_FILE_LAUNCH_TIME:
1043           launch_time = va_arg (args, guint32);
1044           break;
1045         case EGG_DESKTOP_FILE_LAUNCH_FLAGS:
1046           flags |= va_arg (args, GSpawnFlags);
1047           /* Make sure they didn't set any flags that don't make sense. */
1048           flags &= ~G_SPAWN_FILE_AND_ARGV_ZERO;
1049           break;
1050         case EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC:
1051           setup_func = va_arg (args, GSpawnChildSetupFunc);
1052           setup_data = va_arg (args, gpointer);
1053           break;
1054
1055         case EGG_DESKTOP_FILE_LAUNCH_RETURN_PID:
1056           ret_pid = va_arg (args, GPid *);
1057           break;
1058         case EGG_DESKTOP_FILE_LAUNCH_RETURN_STDIN_PIPE:
1059           ret_stdin = va_arg (args, int *);
1060           break;
1061         case EGG_DESKTOP_FILE_LAUNCH_RETURN_STDOUT_PIPE:
1062           ret_stdout = va_arg (args, int *);
1063           break;
1064         case EGG_DESKTOP_FILE_LAUNCH_RETURN_STDERR_PIPE:
1065           ret_stderr = va_arg (args, int *);
1066           break;
1067         case EGG_DESKTOP_FILE_LAUNCH_RETURN_STARTUP_ID:
1068           ret_startup_id = va_arg (args, char **);
1069           break;
1070
1071         default:
1072           g_set_error (error, EGG_DESKTOP_FILE_ERROR,
1073                        EGG_DESKTOP_FILE_ERROR_UNRECOGNIZED_OPTION,
1074                        _("Unrecognized launch option: %d"),
1075                        GPOINTER_TO_INT (option));
1076           success = FALSE;
1077           goto out;
1078         }
1079     }
1080
1081   if (screen)
1082     {
1083       char *display_name = gdk_screen_make_display_name (screen);
1084       char *display_env = g_strdup_printf ("DISPLAY=%s", display_name);
1085       env = array_putenv (env, display_env);
1086       g_free (display_name);
1087       g_free (display_env);
1088
1089       display = gdk_screen_get_display (screen);
1090     }
1091   else
1092     {
1093       display = gdk_display_get_default ();
1094       screen = gdk_display_get_default_screen (display);
1095     }
1096   screen_num = gdk_screen_get_number (screen);
1097
1098   translated_documents = translate_document_list (desktop_file, documents);
1099   docs = translated_documents;
1100
1101   success = FALSE;
1102
1103   do
1104     {
1105       command = parse_exec (desktop_file, &docs, error);
1106       if (!command)
1107         goto out;
1108
1109       if (!g_shell_parse_argv (command, &argc, &argv, error))
1110         {
1111           g_free (command);
1112           goto out;
1113         }
1114       g_free (command);
1115
1116 #ifdef HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE
1117       startup_id = start_startup_notification (display, desktop_file,
1118                                                argv[0], screen_num,
1119                                                workspace, launch_time);
1120       if (startup_id)
1121         {
1122           char *startup_id_env = g_strdup_printf ("DESKTOP_STARTUP_ID=%s",
1123                                                   startup_id);
1124           env = array_putenv (env, startup_id_env);
1125           g_free (startup_id_env);
1126         }
1127 #else
1128       startup_id = NULL;
1129 #endif /* HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE */
1130
1131       current_success =
1132         g_spawn_async_with_pipes (directory,
1133                                   argv,
1134                                   env ? (char **)(env->pdata) : NULL,
1135                                   flags,
1136                                   setup_func, setup_data,
1137                                   ret_pid,
1138                                   ret_stdin, ret_stdout, ret_stderr,
1139                                   error);
1140       g_strfreev (argv);
1141
1142       if (startup_id)
1143         {
1144 #ifdef HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE
1145           if (current_success)
1146             {
1147               set_startup_notification_timeout (display, startup_id);
1148
1149               if (ret_startup_id)
1150                 *ret_startup_id = startup_id;
1151               else
1152                 g_free (startup_id);
1153             }
1154           else
1155 #endif /* HAVE_GDK_X11_DISPLAY_BROADCAST_STARTUP_MESSAGE */
1156             g_free (startup_id);
1157         }
1158       else if (ret_startup_id)
1159         *ret_startup_id = NULL;
1160
1161       if (current_success)
1162         {
1163           /* If we successfully launch any instances of the app, make
1164            * sure we return TRUE and don't set @error.
1165            */
1166           success = TRUE;
1167           error = NULL;
1168
1169           /* Also, only set the output params on the first one */
1170           ret_pid = NULL;
1171           ret_stdin = ret_stdout = ret_stderr = NULL;
1172           ret_startup_id = NULL;
1173         }
1174     }
1175   while (docs && current_success);
1176
1177  out:
1178   if (env)
1179     {
1180       g_strfreev ((char **)env->pdata);
1181       g_ptr_array_free (env, FALSE);
1182     }
1183   free_document_list (translated_documents);
1184
1185   return success;
1186 }
1187
1188 /**
1189  * egg_desktop_file_launch:
1190  * @desktop_file: an #EggDesktopFile
1191  * @documents: a list of URIs or paths to documents to open
1192  * @error: error pointer
1193  * @...: additional options
1194  *
1195  * Launches @desktop_file with the given arguments. Additional options
1196  * can be specified as follows:
1197  *
1198  *   %EGG_DESKTOP_FILE_LAUNCH_CLEARENV: (no arguments)
1199  *       clears the environment in the child process
1200  *   %EGG_DESKTOP_FILE_LAUNCH_PUTENV: (char **variables)
1201  *       adds the NAME=VALUE strings in the given %NULL-terminated
1202  *       array to the child process's environment
1203  *   %EGG_DESKTOP_FILE_LAUNCH_SCREEN: (GdkScreen *screen)
1204  *       causes the application to be launched on the given screen
1205  *   %EGG_DESKTOP_FILE_LAUNCH_WORKSPACE: (int workspace)
1206  *       causes the application to be launched on the given workspace
1207  *   %EGG_DESKTOP_FILE_LAUNCH_DIRECTORY: (char *dir)
1208  *       causes the application to be launched in the given directory
1209  *   %EGG_DESKTOP_FILE_LAUNCH_TIME: (guint32 launch_time)
1210  *       sets the "launch time" for the application. If the user
1211  *       interacts with another window after @launch_time but before
1212  *       the launched application creates its first window, the window
1213  *       manager may choose to not give focus to the new application.
1214  *       Passing 0 for @launch_time will explicitly request that the
1215  *       application not receive focus.
1216  *   %EGG_DESKTOP_FILE_LAUNCH_FLAGS (GSpawnFlags flags)
1217  *       Sets additional #GSpawnFlags to use. See g_spawn_async() for
1218  *       more details.
1219  *   %EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC (GSpawnChildSetupFunc, gpointer)
1220  *       Sets the child setup callback and the data to pass to it.
1221  *       (See g_spawn_async() for more details.)
1222  *
1223  *   %EGG_DESKTOP_FILE_LAUNCH_RETURN_PID (GPid **pid)
1224  *       On a successful launch, sets *@pid to the PID of the launched
1225  *       application.
1226  *   %EGG_DESKTOP_FILE_LAUNCH_RETURN_STARTUP_ID (char **startup_id)
1227  *       On a successful launch, sets *@startup_id to the Startup
1228  *       Notification "startup id" of the launched application.
1229  *   %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDIN_PIPE (int *fd)
1230  *       On a successful launch, sets *@fd to the file descriptor of
1231  *       a pipe connected to the application's stdin.
1232  *   %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDOUT_PIPE (int *fd)
1233  *       On a successful launch, sets *@fd to the file descriptor of
1234  *       a pipe connected to the application's stdout.
1235  *   %EGG_DESKTOP_FILE_LAUNCH_RETURN_STDERR_PIPE (int *fd)
1236  *       On a successful launch, sets *@fd to the file descriptor of
1237  *       a pipe connected to the application's stderr.
1238  *
1239  * The options should be terminated with a single %NULL.
1240  *
1241  * If @documents contains multiple documents, but
1242  * egg_desktop_file_accepts_multiple() returns %FALSE for
1243  * @desktop_file, then egg_desktop_file_launch() will actually launch
1244  * multiple instances of the application. In that case, the return
1245  * value (as well as any values passed via
1246  * %EGG_DESKTOP_FILE_LAUNCH_RETURN_PID, etc) will only reflect the
1247  * first instance of the application that was launched (but the
1248  * %EGG_DESKTOP_FILE_LAUNCH_SETUP_FUNC will be called for each
1249  * instance).
1250  *
1251  * Return value: %TRUE if the application was successfully launched.
1252  **/
1253 gboolean
1254 egg_desktop_file_launch (EggDesktopFile *desktop_file,
1255                          GSList *documents, GError **error,
1256                          ...)
1257 {
1258   va_list args;
1259   gboolean success;
1260   EggDesktopFile *app_desktop_file;
1261
1262   switch (desktop_file->type)
1263     {
1264     case EGG_DESKTOP_FILE_TYPE_APPLICATION:
1265       va_start (args, error);
1266       success = egg_desktop_file_launchv (desktop_file, documents,
1267                                           args, error);
1268       va_end (args);
1269       break;
1270
1271     case EGG_DESKTOP_FILE_TYPE_LINK:
1272       if (documents)
1273         {
1274           g_set_error (error, EGG_DESKTOP_FILE_ERROR,
1275                        EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE,
1276                        _("Can't pass document URIs to a 'Type=Link' desktop entry"));
1277           return FALSE;
1278         }         
1279
1280       if (!parse_link (desktop_file, &app_desktop_file, &documents, error))
1281         return FALSE;
1282
1283       va_start (args, error);
1284       success = egg_desktop_file_launchv (app_desktop_file, documents,
1285                                           args, error);
1286       va_end (args);
1287
1288       egg_desktop_file_free (app_desktop_file);
1289       free_document_list (documents);
1290       break;
1291
1292     default:
1293       g_set_error (error, EGG_DESKTOP_FILE_ERROR,
1294                    EGG_DESKTOP_FILE_ERROR_NOT_LAUNCHABLE,
1295                    _("Not a launchable item"));
1296       success = FALSE;
1297       break;
1298     }
1299
1300   return success;
1301 }
1302
1303
1304 GQuark
1305 egg_desktop_file_error_quark (void)
1306 {
1307   return g_quark_from_static_string ("egg-desktop_file-error-quark");
1308 }
1309
1310
1311 G_LOCK_DEFINE_STATIC (egg_desktop_file);
1312 static EggDesktopFile *egg_desktop_file;
1313
1314 /**
1315  * egg_set_desktop_file:
1316  * @desktop_file_path: path to the application's desktop file
1317  *
1318  * Creates an #EggDesktopFile for the application from the data at
1319  * @desktop_file_path. This will also call g_set_application_name()
1320  * with the localized application name from the desktop file, and
1321  * gtk_window_set_default_icon_name() or
1322  * gtk_window_set_default_icon_from_file() with the application's
1323  * icon. Other code may use additional information from the desktop
1324  * file.
1325  *
1326  * Note that for thread safety reasons, this function can only
1327  * be called once.
1328  **/
1329 void
1330 egg_set_desktop_file (const char *desktop_file_path)
1331 {
1332   GError *error = NULL;
1333
1334   G_LOCK (egg_desktop_file);
1335   if (egg_desktop_file)
1336     egg_desktop_file_free (egg_desktop_file);
1337
1338   egg_desktop_file = egg_desktop_file_new (desktop_file_path, &error);
1339   if (error)
1340     {
1341       g_warning ("Could not load desktop file '%s': %s",
1342                  desktop_file_path, error->message);
1343       g_error_free (error);
1344     }
1345
1346   /* Set localized application name and default window icon */
1347   if (egg_desktop_file->name)
1348     g_set_application_name (egg_desktop_file->name);
1349   if (egg_desktop_file->icon)
1350     {
1351       if (g_path_is_absolute (egg_desktop_file->icon))
1352         gtk_window_set_default_icon_from_file (egg_desktop_file->icon, NULL);
1353       else
1354         gtk_window_set_default_icon_name (egg_desktop_file->icon);
1355     }
1356
1357   G_UNLOCK (egg_desktop_file);
1358 }
1359
1360 /**
1361  * egg_get_desktop_file:
1362  * 
1363  * Gets the application's #EggDesktopFile, as set by
1364  * egg_set_desktop_file().
1365  * 
1366  * Return value: the #EggDesktopFile, or %NULL if it hasn't been set.
1367  **/
1368 EggDesktopFile *
1369 egg_get_desktop_file (void)
1370 {
1371   EggDesktopFile *retval;
1372
1373   G_LOCK (egg_desktop_file);
1374   retval = egg_desktop_file;
1375   G_UNLOCK (egg_desktop_file);
1376
1377   return retval;
1378 }