Improve editors through .desktop files implementation:
[geeqie.git] / src / editors.c
1 /*
2  * Geeqie
3  * (C) 2006 John Ellis
4  * Copyright (C) 2008 - 2009 The Geeqie Team
5  *
6  * Author: John Ellis
7  *
8  * This software is released under the GNU General Public License (GNU GPL).
9  * Please read the included file COPYING for more information.
10  * This software comes with no warranty of any kind, use at your own risk!
11  */
12
13
14 #include "main.h"
15 #include "editors.h"
16
17 #include "filedata.h"
18 #include "filefilter.h"
19 #include "misc.h"
20 #include "ui_fileops.h"
21 #include "ui_spinner.h"
22 #include "ui_utildlg.h"
23 #include "utilops.h"
24
25 #include <errno.h>
26
27
28 #define EDITOR_WINDOW_WIDTH 500
29 #define EDITOR_WINDOW_HEIGHT 300
30
31
32
33 typedef struct _EditorVerboseData EditorVerboseData;
34 struct _EditorVerboseData {
35         GenericDialog *gd;
36         GtkWidget *button_close;
37         GtkWidget *button_stop;
38         GtkWidget *text;
39         GtkWidget *progress;
40         GtkWidget *spinner;
41 };
42
43 typedef struct _EditorData EditorData;
44 struct _EditorData {
45         EditorFlags flags;
46         GPid pid;
47         GList *list;
48         gint count;
49         gint total;
50         gboolean stopping;
51         EditorVerboseData *vd;
52         EditorCallback callback;
53         gpointer data;
54         const EditorDescription *editor;
55 };
56
57
58 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
59 static EditorFlags editor_command_next_start(EditorData *ed);
60 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
61 static EditorFlags editor_command_done(EditorData *ed);
62
63 /*
64  *-----------------------------------------------------------------------------
65  * external editor routines
66  *-----------------------------------------------------------------------------
67  */
68
69 GHashTable *editors = NULL;
70
71 #ifdef G_KEY_FILE_DESKTOP_GROUP
72 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
73 #else
74 #define DESKTOP_GROUP "Desktop Entry"
75 #endif
76
77 void editor_description_free(EditorDescription *editor)
78 {
79         if (!editor) return;
80         
81         g_free(editor->key);
82         g_free(editor->name);
83         g_free(editor->icon);
84         g_free(editor->exec);
85         g_free(editor->menu_path);
86         g_free(editor->hotkey);
87         g_free(editor->comment);
88         string_list_free(editor->ext_list);
89         g_free(editor->file);
90         g_free(editor);
91 }
92
93 static GList *editor_mime_types_to_extensions(gchar **mime_types)
94 {
95         /* FIXME: this should be rewritten to use the shared mime database, as soon as we switch to gio */
96         
97         static const gchar *conv_table[][2] = {
98                 {"application/x-ufraw", "%raw"},
99                 {"image/*",             "*"},
100                 {"image/bmp",           ".bmp"},
101                 {"image/gif",           ".gif"},
102                 {"image/jpeg",          ".jpeg;.jpg"},
103                 {"image/jpg",           ".jpg;.jpeg"},
104                 {"image/pcx",           ".pcx"},
105                 {"image/png",           ".png"},
106                 {"image/svg",           ".svg"},
107                 {"image/svg+xml",       ".svg"},
108                 {"image/svg+xml-compressed",    ".svg"},        
109                 {"image/tiff",          ".tiff;.tif"},
110                 {"image/x-bmp",         ".bmp"},
111                 {"image/x-canon-crw",   ".crw"},
112                 {"image/x-cr2",         ".cr2"},
113                 {"image/x-dcraw",       "%raw"},
114                 {"image/x-ico",         ".ico"},
115                 {"image/x-mrw",         ".mrw"},
116                 {"image/x-MS-bmp",      ".bmp"},
117                 {"image/x-nef",         ".nef"},
118                 {"image/x-orf",         ".orf"},
119                 {"image/x-pcx",         ".pcx"},
120                 {"image/xpm",           ".xpm"},
121                 {"image/x-png",         ".png"},
122                 {"image/x-portable-anymap",     ".pam"},        
123                 {"image/x-portable-bitmap",     ".pbm"},
124                 {"image/x-portable-graymap",    ".pgm"},
125                 {"image/x-portable-pixmap",     ".ppm"},
126                 {"image/x-psd",         ".psd"},
127                 {"image/x-raf",         ".raf"},
128                 {"image/x-sgi",         ".sgi"},
129                 {"image/x-tga",         ".tga"},
130                 {"image/x-xbitmap",     ".xbm"},
131                 {"image/x-xcf",         ".xcf"},
132                 {"image/x-xpixmap",     ".xpm"},
133                 {"image/x-x3f",         ".x3f"},
134                 {NULL, NULL}};
135         
136         gint i, j;
137         GList *list = NULL;
138         
139         for (i = 0; mime_types[i]; i++) 
140                 for (j = 0; conv_table[j][0]; j++)
141                         if (strcmp(mime_types[i], conv_table[j][0]) == 0)
142                                 list = g_list_concat(list, filter_to_list(conv_table[j][1]));
143         
144         return list;
145 }
146
147 static gboolean editor_accepts_parameters(EditorDescription *editor)
148 {
149         const gchar *p = editor->exec;
150
151         if (!p) return FALSE;
152
153         while (*p)
154                 {
155                 if (*p == '%' && p[1])
156                         {
157                         switch (p[1])
158                                 {
159                                 case 'F':
160                                 case 'f':
161                                 case 'U':
162                                 case 'u':
163                                 case 'i':
164                                 case 'k':
165                                 case 'c':
166                                         return TRUE;
167                                 default:
168                                         break;
169                                 }
170                         }
171                 p++;
172                 }
173         
174         return FALSE;
175 }
176
177 static gboolean editor_read_desktop_file(const gchar *path)
178 {
179         GKeyFile *key_file;
180         EditorDescription *editor;
181         gchar *extensions;
182         gchar *type;
183         const gchar *key = filename_from_path(path);
184         gchar **categories, **only_show_in, **not_show_in;
185         gchar *try_exec;
186
187         if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
188         
189         key_file = g_key_file_new();
190         if (!g_key_file_load_from_file(key_file, path, 0, NULL))
191                 {
192                 g_key_file_free(key_file);
193                 return FALSE;
194                 }
195
196         type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", NULL);
197         if (!type || strcmp(type, "Application") != 0)
198                 {
199                 /* We only consider desktop entries of Application type */
200                 g_key_file_free(key_file);
201                 g_free(type);
202                 return FALSE;
203                 }
204         g_free(type);
205         
206         editor = g_new0(EditorDescription, 1);
207         
208         editor->key = g_strdup(key);
209         editor->file = g_strdup(path);
210
211         g_hash_table_insert(editors, editor->key, editor);
212
213         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", NULL)
214             || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", NULL))
215                 {
216                 editor->hidden = TRUE;
217                 }
218
219         categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", NULL, NULL);
220         if (categories)
221                 {
222                 gboolean found = FALSE;
223                 gint i;
224                 for (i = 0; categories[i]; i++) 
225                         /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
226                         if (strcmp(categories[i], "Graphics") == 0 ||
227                             strcmp(categories[i], "X-Geeqie") == 0) 
228                                 {
229                                 found = TRUE;
230                                 break;
231                                 }
232                 if (!found) editor->hidden = TRUE;
233                 g_strfreev(categories);
234                 }
235         else
236                 {
237                 editor->hidden = TRUE;
238                 }
239
240         only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", NULL, NULL);
241         if (only_show_in)
242                 {
243                 gboolean found = FALSE;
244                 gint i;
245                 for (i = 0; only_show_in[i]; i++) 
246                         if (strcmp(only_show_in[i], "X-Geeqie") == 0)
247                                 {
248                                 found = TRUE;
249                                 break;
250                                 }
251                 if (!found) editor->hidden = TRUE;
252                 g_strfreev(only_show_in);
253                 }
254
255         not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", NULL, NULL);
256         if (not_show_in)
257                 {
258                 gboolean found = FALSE;
259                 gint i;
260                 for (i = 0; not_show_in[i]; i++) 
261                         if (strcmp(not_show_in[i], "X-Geeqie") == 0)
262                                 {
263                                 found = TRUE;
264                                 break;
265                                 }
266                 if (found) editor->hidden = TRUE;
267                 g_strfreev(not_show_in);
268                 }
269                 
270                 
271         try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", NULL);
272         if (try_exec && !editor->hidden)
273                 {
274                 gchar *try_exec_res = g_find_program_in_path(try_exec);
275                 if (!try_exec_res) editor->hidden = TRUE;
276                 g_free(try_exec_res);
277                 g_free(try_exec);
278                 }
279                 
280         if (editor->hidden) 
281                 {
282                 /* hidden editors will be deleted, no need to parse the rest */
283                 g_key_file_free(key_file);
284                 return TRUE;
285                 }
286         
287         editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", NULL, NULL);
288         editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", NULL);
289         
290         /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
291         if (editor->icon && !g_path_is_absolute(editor->icon))
292                 {
293                 gchar *ext = strrchr(editor->icon, '.');
294                 
295                 if (ext && strlen(ext) == 4 && 
296                     (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
297                         {
298                         log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
299                                    editor->file, editor->icon);
300                         
301                         // drop extension
302                         *ext = '\0';
303                         }
304                 }
305
306         editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", NULL);
307         
308         /* we take only editors that accept parameters */
309         if (!editor_accepts_parameters(editor)) editor->hidden = TRUE;
310
311         editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", NULL);
312         if (!editor->menu_path) editor->menu_path = g_strdup("EditMenu/ExternalMenu");
313         
314         editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", NULL);
315
316         editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", NULL);
317
318         extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", NULL);
319         if (extensions)
320                 editor->ext_list = filter_to_list(extensions);
321         else
322                 {
323                 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", NULL, NULL);
324                 if (mime_types)
325                         {
326                         editor->ext_list = editor_mime_types_to_extensions(mime_types);
327                         g_strfreev(mime_types);
328                         if (!editor->ext_list) editor->hidden = TRUE; 
329                         }
330                 }
331                 
332         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Keep-Fullscreen", NULL)) editor->flags |= EDITOR_KEEP_FS;
333         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", NULL)) editor->flags |= EDITOR_VERBOSE;
334         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose-Multi", NULL)) editor->flags |= EDITOR_VERBOSE_MULTI;
335         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", NULL)) editor->flags |= EDITOR_DEST;
336         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", NULL)) editor->flags |= EDITOR_TERMINAL;
337         
338         editor->flags |= editor_command_parse(editor, NULL, NULL);
339         g_key_file_free(key_file);
340         
341         return TRUE;    
342 }
343
344 static gboolean editor_remove_desktop_file_cb(gpointer key, gpointer value, gpointer user_data)
345 {
346         EditorDescription *editor = value;
347         return editor->hidden;
348 }
349
350 static void editor_read_desktop_dir(const gchar *path)
351 {
352         DIR *dp;
353         struct dirent *dir;
354         gchar *pathl;
355
356         pathl = path_from_utf8(path);
357         dp = opendir(pathl);
358         g_free(pathl);
359         if (!dp)
360                 {
361                 /* dir not found */
362                 return;
363                 }
364         while ((dir = readdir(dp)) != NULL)
365                 {
366                 gchar *namel = dir->d_name;
367                 
368                 if (g_str_has_suffix(namel, ".desktop"))
369                         {
370                         gchar *name = path_to_utf8(namel);
371                         gchar *dpath = g_build_filename(path, name, NULL);
372                         editor_read_desktop_file(dpath);
373                         g_free(dpath);
374                         g_free(name);
375                         }       
376                 }
377         closedir(dp);
378 }
379
380 void editor_load_descriptions(void)
381 {
382         gchar *path;
383         gchar *xdg_data_dirs;
384         gchar *all_dirs;
385         gchar **split_dirs;
386         gint i;
387         
388         if (editors)
389                 {
390                 g_hash_table_destroy(editors);
391                 }
392         editors = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)editor_description_free);
393
394         xdg_data_dirs = getenv("XDG_DATA_DIRS");
395         if (xdg_data_dirs && xdg_data_dirs[0])
396                 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
397         else
398                 xdg_data_dirs = g_strdup("/usr/share");
399         
400         all_dirs = g_strconcat(get_rc_dir(), ":", GQ_APP_DIR, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
401         
402         g_free(xdg_data_dirs);
403
404         split_dirs = g_strsplit(all_dirs, ":", 0);
405         
406         g_free(all_dirs);
407
408         for (i = 0; split_dirs[i]; i++)
409                 {
410                 path = g_build_filename(split_dirs[i], "applications", NULL);
411                 editor_read_desktop_dir(path);
412                 g_free(path);
413                 }
414                 
415         g_strfreev(split_dirs);
416         
417         g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, NULL);
418 }
419
420 static void editor_list_add_cb(gpointer key, gpointer value, gpointer data)
421 {
422         GList **listp = data;
423         EditorDescription *editor = value;
424         
425         /* do not show the special commands in any list, they are called explicitly */ 
426         if (strcmp(editor->key, CMD_COPY) == 0 ||
427             strcmp(editor->key, CMD_MOVE) == 0 ||  
428             strcmp(editor->key, CMD_RENAME) == 0 ||
429             strcmp(editor->key, CMD_DELETE) == 0 ||
430             strcmp(editor->key, CMD_FOLDER) == 0) return;
431
432         *listp = g_list_prepend(*listp, editor);
433 }
434
435 static gint editor_sort(gconstpointer a, gconstpointer b)
436 {
437         const EditorDescription *ea = a;
438         const EditorDescription *eb = b;
439         gint ret;
440         
441         ret = strcmp(ea->menu_path, eb->menu_path);
442         if (ret != 0) return ret;
443         
444         return g_utf8_collate(ea->name, eb->name);
445 }
446
447 GList *editor_list_get(void)
448 {
449         GList *editors_list = NULL;
450         g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
451         editors_list = g_list_sort(editors_list, editor_sort);
452
453         return editors_list;
454 }
455
456 /* ------------------------------ */
457
458
459 static void editor_verbose_data_free(EditorData *ed)
460 {
461         if (!ed->vd) return;
462         g_free(ed->vd);
463         ed->vd = NULL;
464 }
465
466 static void editor_data_free(EditorData *ed)
467 {
468         editor_verbose_data_free(ed);
469         g_free(ed);
470 }
471
472 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
473 {
474         EditorData *ed = data;
475
476         generic_dialog_close(gd);
477         editor_verbose_data_free(ed);
478         if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
479 }
480
481 static void editor_verbose_window_stop(GenericDialog *gd, gpointer data)
482 {
483         EditorData *ed = data;
484         ed->stopping = TRUE;
485         ed->count = 0;
486         editor_verbose_window_progress(ed, _("stopping..."));
487 }
488
489 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
490 {
491         vd->gd->cancel_cb = editor_verbose_window_close;
492
493         spinner_set_interval(vd->spinner, -1);
494         gtk_widget_set_sensitive(vd->button_stop, FALSE);
495         gtk_widget_set_sensitive(vd->button_close, TRUE);
496 }
497
498 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
499 {
500         EditorVerboseData *vd;
501         GtkWidget *scrolled;
502         GtkWidget *hbox;
503         gchar *buf;
504
505         vd = g_new0(EditorVerboseData, 1);
506
507         vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
508                                    NULL, FALSE,
509                                    NULL, ed);
510         buf = g_strdup_printf(_("Output of %s"), text);
511         generic_dialog_add_message(vd->gd, NULL, buf, NULL);
512         g_free(buf);
513         vd->button_stop = generic_dialog_add_button(vd->gd, GTK_STOCK_STOP, NULL,
514                                                    editor_verbose_window_stop, FALSE);
515         gtk_widget_set_sensitive(vd->button_stop, FALSE);
516         vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, NULL,
517                                                     editor_verbose_window_close, TRUE);
518         gtk_widget_set_sensitive(vd->button_close, FALSE);
519
520         scrolled = gtk_scrolled_window_new(NULL, NULL);
521         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
522         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
523                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
524         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
525         gtk_widget_show(scrolled);
526
527         vd->text = gtk_text_view_new();
528         gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
529         gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
530         gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
531         gtk_widget_show(vd->text);
532
533         hbox = gtk_hbox_new(FALSE, 0);
534         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
535         gtk_widget_show(hbox);
536
537         vd->progress = gtk_progress_bar_new();
538         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
539         gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
540         gtk_widget_show(vd->progress);
541
542         vd->spinner = spinner_new(NULL, SPINNER_SPEED);
543         gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
544         gtk_widget_show(vd->spinner);
545
546         gtk_widget_show(vd->gd->dialog);
547
548         ed->vd = vd;
549         return vd;
550 }
551
552 static void editor_verbose_window_fill(EditorVerboseData *vd, gchar *text, gint len)
553 {
554         GtkTextBuffer *buffer;
555         GtkTextIter iter;
556
557         buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
558         gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
559         gtk_text_buffer_insert(buffer, &iter, text, len);
560 }
561
562 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
563 {
564         if (!ed->vd) return;
565
566         if (ed->total)
567                 {
568                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), (gdouble)ed->count / ed->total);
569                 }
570
571         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
572 }
573
574 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
575 {
576         EditorData *ed = data;
577         gchar buf[512];
578         gsize count;
579
580         if (condition & G_IO_IN)
581                 {
582                 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, NULL) == G_IO_STATUS_NORMAL)
583                         {
584                         if (!g_utf8_validate(buf, count, NULL))
585                                 {
586                                 gchar *utf8;
587
588                                 utf8 = g_locale_to_utf8(buf, count, NULL, NULL, NULL);
589                                 if (utf8)
590                                         {
591                                         editor_verbose_window_fill(ed->vd, utf8, -1);
592                                         g_free(utf8);
593                                         }
594                                 else
595                                         {
596                                         editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
597                                         }
598                                 }
599                         else
600                                 {
601                                 editor_verbose_window_fill(ed->vd, buf, count);
602                                 }
603                         }
604                 }
605
606         if (condition & (G_IO_ERR | G_IO_HUP))
607                 {
608                 g_io_channel_shutdown(source, TRUE, NULL);
609                 return FALSE;
610                 }
611
612         return TRUE;
613 }
614
615 typedef enum {
616         PATH_FILE,
617         PATH_FILE_URL,
618         PATH_DEST
619 } PathType;
620
621
622 static gchar *editor_command_path_parse(const FileData *fd, PathType type, const EditorDescription *editor)
623 {
624         GString *string;
625         gchar *pathl;
626         const gchar *p = NULL;
627
628         string = g_string_new("");
629
630         if (type == PATH_FILE || type == PATH_FILE_URL)
631                 {
632                 GList *work = editor->ext_list;
633
634                 if (!work)
635                         p = fd->path;
636                 else
637                         {
638                         while (work)
639                                 {
640                                 GList *work2;
641                                 gchar *ext = work->data;
642                                 work = work->next;
643
644                                 if (strcmp(ext, "*") == 0 ||
645                                     g_ascii_strcasecmp(ext, fd->extension) == 0)
646                                         {
647                                         p = fd->path;
648                                         break;
649                                         }
650
651                                 work2 = fd->sidecar_files;
652                                 while (work2)
653                                         {
654                                         FileData *sfd = work2->data;
655                                         work2 = work2->next;
656
657                                         if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
658                                                 {
659                                                 p = sfd->path;
660                                                 break;
661                                                 }
662                                         }
663                                 if (p) break;
664                                 }
665                         if (!p) return NULL;
666                         }
667                 }
668         else if (type == PATH_DEST)
669                 {
670                 if (fd->change && fd->change->dest)
671                         p = fd->change->dest;
672                 else
673                         p = "";
674                 }
675
676         g_assert(p);
677         string = g_string_append(string, p);
678
679         if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
680         pathl = path_from_utf8(string->str);
681         g_string_free(string, TRUE);
682
683         if (pathl && !pathl[0]) /* empty string case */
684                 {
685                 g_free(pathl);
686                 pathl = NULL;
687                 }
688
689         return pathl;
690 }
691
692 static GString *append_quoted(GString *str, const char *s, gboolean single_quotes, gboolean double_quotes)
693 {
694         const char *p;
695         
696         if (!single_quotes)
697                 {
698                 if (!double_quotes)
699                         g_string_append_c(str, '\'');
700                 else
701                         g_string_append(str, "\"'");
702                 }
703
704         for (p = s; *p != '\0'; p++)
705                 {
706                 if (*p == '\'')
707                         g_string_append(str, "'\\''");
708                 else
709                         g_string_append_c(str, *p);
710                 }
711         
712         if (!single_quotes)
713                 {
714                 if (!double_quotes)
715                         g_string_append_c(str, '\'');
716                 else
717                         g_string_append(str, "'\"");
718                 }
719
720         return str;
721 }
722
723
724 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gchar **output)
725 {
726         EditorFlags flags = 0;
727         const gchar *p;
728         GString *result = NULL;
729         gboolean escape = FALSE;
730         gboolean single_quotes = FALSE;
731         gboolean double_quotes = FALSE;
732
733         if (output)
734                 result = g_string_new("");
735
736         if (editor->exec[0] == '\0')
737                 {
738                 flags |= EDITOR_ERROR_EMPTY;
739                 goto err;
740                 }
741         
742         p = editor->exec;
743         /* skip leading whitespaces if any */
744         while (g_ascii_isspace(*p)) p++;
745
746         /* command */
747
748         while (*p)
749                 {
750                 if (escape)
751                         {
752                         escape = FALSE;
753                         if (output) result = g_string_append_c(result, *p);
754                         }
755                 else if (*p == '\\')
756                         {
757                         if (!single_quotes) escape = TRUE;
758                         if (output) result = g_string_append_c(result, *p);
759                         }
760                 else if (*p == '\'')
761                         {
762                         if (output) result = g_string_append_c(result, *p);
763                         if (!single_quotes && !double_quotes)
764                                 single_quotes = TRUE;
765                         else if (single_quotes)
766                                 single_quotes = FALSE;
767                         }
768                 else if (*p == '"')
769                         {
770                         if (output) result = g_string_append_c(result, *p);
771                         if (!single_quotes && !double_quotes)
772                                 double_quotes = TRUE;
773                         else if (double_quotes)
774                                 double_quotes = FALSE;
775                         }
776                 else if (*p == '%' && p[1])
777                         {
778                         gchar *pathl = NULL;
779
780                         p++;
781
782                         switch (*p)
783                                 {
784                                 case 'f': /* single file */
785                                 case 'u': /* single url */
786                                         flags |= EDITOR_FOR_EACH;
787                                         if (flags & EDITOR_SINGLE_COMMAND)
788                                                 {
789                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
790                                                 goto err;
791                                                 }
792                                         if (list)
793                                                 {
794                                                 /* use the first file from the list */
795                                                 if (!list->data)
796                                                         {
797                                                         flags |= EDITOR_ERROR_NO_FILE;
798                                                         goto err;
799                                                         }
800                                                 pathl = editor_command_path_parse((FileData *)list->data,
801                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
802                                                                                   editor);
803                                                 if (!pathl)
804                                                         {
805                                                         flags |= EDITOR_ERROR_NO_FILE;
806                                                         goto err;
807                                                         }
808                                                 if (output)
809                                                         {
810                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
811                                                         }
812                                                 g_free(pathl);
813                                                 }
814                                         break;
815
816                                 case 'F':
817                                 case 'U':
818                                         flags |= EDITOR_SINGLE_COMMAND;
819                                         if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
820                                                 {
821                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
822                                                 goto err;
823                                                 }
824
825                                         if (list)
826                                                 {
827                                                 /* use whole list */
828                                                 GList *work = list;
829                                                 gboolean ok = FALSE;
830
831                                                 while (work)
832                                                         {
833                                                         FileData *fd = work->data;
834                                                         pathl = editor_command_path_parse(fd, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
835                                                         if (pathl)
836                                                                 {
837                                                                 ok = TRUE;
838
839                                                                 if (output)
840                                                                         {
841                                                                         ok = TRUE;
842                                                                         if (work != list) g_string_append_c(result, ' ');
843                                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
844                                                                         }
845                                                                 g_free(pathl);
846                                                                 }
847                                                         work = work->next;
848                                                         }
849                                                 if (!ok)
850                                                         {
851                                                         flags |= EDITOR_ERROR_NO_FILE;
852                                                         goto err;
853                                                         }
854                                                 }
855                                         break;
856                                 case 'i':
857                                         if (editor->icon && *editor->icon)
858                                                 {
859                                                 if (output)
860                                                         {
861                                                         result = g_string_append(result, "--icon ");
862                                                         result = append_quoted(result, editor->icon, single_quotes, double_quotes);
863                                                         }
864                                                 }
865                                         break;
866                                 case 'c':
867                                         if (output)
868                                                 {
869                                                 result = append_quoted(result, editor->name, single_quotes, double_quotes);
870                                                 }
871                                         break;
872                                 case 'k':
873                                         if (output)
874                                                 {
875                                                 result = append_quoted(result, editor->file, single_quotes, double_quotes);
876                                                 }
877                                         break;
878                                 case '%':
879                                         /* %% = % escaping */
880                                         if (output) result = g_string_append_c(result, *p);
881                                         break;
882                                 case 'd':
883                                 case 'D':
884                                 case 'n':
885                                 case 'N':
886                                 case 'v':
887                                 case 'm':
888                                         /* deprecated according to spec, ignore */
889                                         break;
890                                 default:
891                                         flags |= EDITOR_ERROR_SYNTAX;
892                                         goto err;
893                                 }
894                         }
895                 else
896                         {
897                         if (output) result = g_string_append_c(result, *p);
898                         }
899                 p++;
900                 }
901
902         if (output)
903                 {
904                 *output = g_string_free(result, FALSE);
905                 DEBUG_3("Editor cmd: %s", *output);
906                 }
907
908         return flags;
909
910
911 err:
912         if (output)
913                 {
914                 g_string_free(result, TRUE);
915                 *output = NULL;
916                 }
917         return flags;
918 }
919
920
921 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
922 {
923         EditorData *ed = data;
924         g_spawn_close_pid(pid);
925         ed->pid = -1;
926
927         editor_command_next_finish(ed, status);
928 }
929
930
931 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
932 {
933         gchar *command;
934         FileData *fd = list->data;
935         GPid pid;
936         gint standard_output;
937         gint standard_error;
938         gboolean ok;
939
940         ed->pid = -1;
941         ed->flags = editor->flags;
942         ed->flags |= editor_command_parse(editor, list, &command);
943
944         ok = !EDITOR_ERRORS(ed->flags);
945
946         if (ok)
947                 {
948                 ok = (options->shell.path && *options->shell.path);
949                 if (!ok) log_printf("ERROR: empty shell command\n");
950                         
951                 if (ok)
952                         {
953                         ok = (access(options->shell.path, X_OK) == 0);
954                         if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
955                         }
956
957                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
958                 }
959
960         if (ok)
961                 {
962                 gchar *working_directory;
963                 gchar *args[4];
964                 guint n = 0;
965
966                 working_directory = remove_level_from_path(fd->path);
967                 args[n++] = options->shell.path;
968                 if (options->shell.options && *options->shell.options)
969                         args[n++] = options->shell.options;
970                 args[n++] = command;
971                 args[n] = NULL;
972
973                 if ((ed->flags & EDITOR_DEST) && fd->change && fd->change->dest) /* FIXME: error handling */
974                         {
975                         g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
976                         }
977                 else
978                         {
979                         g_unsetenv("GEEQIE_DESTINATION");
980                         }
981
982                 ok = g_spawn_async_with_pipes(working_directory, args, NULL,
983                                       G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
984                                       NULL, NULL,
985                                       &pid,
986                                       NULL,
987                                       ed->vd ? &standard_output : NULL,
988                                       ed->vd ? &standard_error : NULL,
989                                       NULL);
990                 
991                 g_free(working_directory);
992
993                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
994                 }
995
996         if (ok)
997                 {
998                 g_child_watch_add(pid, editor_child_exit_cb, ed);
999                 ed->pid = pid;
1000                 }
1001
1002         if (ed->vd)
1003                 {
1004                 if (!ok)
1005                         {
1006                         gchar *buf;
1007
1008                         buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1009                         editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1010                         g_free(buf);
1011
1012                         }
1013                 else
1014                         {
1015                         GIOChannel *channel_output;
1016                         GIOChannel *channel_error;
1017
1018                         channel_output = g_io_channel_unix_new(standard_output);
1019                         g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, NULL);
1020                         g_io_channel_set_encoding(channel_output, NULL, NULL);
1021
1022                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1023                                             editor_verbose_io_cb, ed, NULL);
1024                         g_io_channel_unref(channel_output);
1025
1026                         channel_error = g_io_channel_unix_new(standard_error);
1027                         g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, NULL);
1028                         g_io_channel_set_encoding(channel_error, NULL, NULL);
1029
1030                         g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1031                                             editor_verbose_io_cb, ed, NULL);
1032                         g_io_channel_unref(channel_error);
1033                         }
1034                 }
1035
1036         g_free(command);
1037
1038         return EDITOR_ERRORS(ed->flags);
1039 }
1040
1041 static EditorFlags editor_command_next_start(EditorData *ed)
1042 {
1043         if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1044
1045         if (ed->list && ed->count < ed->total)
1046                 {
1047                 FileData *fd;
1048                 EditorFlags error;
1049
1050                 fd = ed->list->data;
1051
1052                 if (ed->vd)
1053                         {
1054                         if (ed->flags & EDITOR_FOR_EACH)
1055                                 editor_verbose_window_progress(ed, fd->path);
1056                         else
1057                                 editor_verbose_window_progress(ed, _("running..."));
1058                         }
1059                 ed->count++;
1060
1061                 error = editor_command_one(ed->editor, ed->list, ed);
1062                 if (!error && ed->vd)
1063                         {
1064                         gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != NULL) );
1065                         if (ed->flags & EDITOR_FOR_EACH)
1066                                 {
1067                                 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1068                                 editor_verbose_window_fill(ed->vd, "\n", 1);
1069                                 }
1070                         }
1071
1072                 if (!error)
1073                         return 0;
1074                 
1075                 /* command was not started, call the finish immediately */
1076                 return editor_command_next_finish(ed, 0);
1077                 }
1078
1079         /* everything is done */
1080         return editor_command_done(ed);
1081 }
1082
1083 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1084 {
1085         gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1086
1087         if (status)
1088                 ed->flags |= EDITOR_ERROR_STATUS;
1089
1090         if (ed->flags & EDITOR_FOR_EACH)
1091                 {
1092                 /* handle the first element from the list */
1093                 GList *fd_element = ed->list;
1094
1095                 ed->list = g_list_remove_link(ed->list, fd_element);
1096                 if (ed->callback)
1097                         {
1098                         cont = ed->callback(ed->list ? ed : NULL, ed->flags, fd_element, ed->data);
1099                         if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1100                         }
1101                 filelist_free(fd_element);
1102                 }
1103         else
1104                 {
1105                 /* handle whole list */
1106                 if (ed->callback)
1107                         cont = ed->callback(NULL, ed->flags, ed->list, ed->data);
1108                 filelist_free(ed->list);
1109                 ed->list = NULL;
1110                 }
1111
1112         switch (cont)
1113                 {
1114                 case EDITOR_CB_SUSPEND:
1115                         return EDITOR_ERRORS(ed->flags);
1116                 case EDITOR_CB_SKIP:
1117                         return editor_command_done(ed);
1118                 }
1119         
1120         return editor_command_next_start(ed);
1121 }
1122
1123 static EditorFlags editor_command_done(EditorData *ed)
1124 {
1125         EditorFlags flags;
1126
1127         if (ed->vd)
1128                 {
1129                 if (ed->count == ed->total)
1130                         {
1131                         editor_verbose_window_progress(ed, _("done"));
1132                         }
1133                 else
1134                         {
1135                         editor_verbose_window_progress(ed, _("stopped by user"));
1136                         }
1137                 editor_verbose_window_enable_close(ed->vd);
1138                 }
1139
1140         /* free the not-handled items */
1141         if (ed->list)
1142                 {
1143                 ed->flags |= EDITOR_ERROR_SKIPPED;
1144                 if (ed->callback) ed->callback(NULL, ed->flags, ed->list, ed->data);
1145                 filelist_free(ed->list);
1146                 ed->list = NULL;
1147                 }
1148
1149         ed->count = 0;
1150
1151         flags = EDITOR_ERRORS(ed->flags);
1152
1153         if (!ed->vd) editor_data_free(ed);
1154
1155         return flags;
1156 }
1157
1158 void editor_resume(gpointer ed)
1159 {
1160         editor_command_next_start(ed);
1161 }
1162
1163 void editor_skip(gpointer ed)
1164 {
1165         editor_command_done(ed);
1166 }
1167
1168 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, EditorCallback cb, gpointer data)
1169 {
1170         EditorData *ed;
1171         EditorFlags flags = editor->flags;
1172
1173         if (EDITOR_ERRORS(flags)) return EDITOR_ERRORS(flags);
1174
1175         ed = g_new0(EditorData, 1);
1176         ed->list = filelist_copy(list);
1177         ed->flags = flags;
1178         ed->editor = editor;
1179         ed->total = (flags & EDITOR_SINGLE_COMMAND) ? 1 : g_list_length(list);
1180         ed->callback = cb;
1181         ed->data =  data;
1182
1183         if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1184                 flags |= EDITOR_VERBOSE;
1185
1186         if (flags & EDITOR_VERBOSE)
1187                 editor_verbose_window(ed, text);
1188
1189         editor_command_next_start(ed);
1190         /* errors from editor_command_next_start will be handled via callback */
1191         return EDITOR_ERRORS(flags);
1192 }
1193
1194 gboolean is_valid_editor_command(const gchar *key)
1195 {
1196         if (!key) return FALSE;
1197         return g_hash_table_lookup(editors, key) != NULL;
1198 }
1199
1200 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, EditorCallback cb, gpointer data)
1201 {
1202         EditorFlags error;
1203         EditorDescription *editor;
1204         if (!key) return FALSE;
1205         
1206         editor = g_hash_table_lookup(editors, key);
1207
1208         if (!list) return FALSE;
1209         if (!editor) return FALSE;
1210
1211         error = editor_command_start(editor, editor->name, list, cb, data);
1212
1213         if (EDITOR_ERRORS(error))
1214                 {
1215                 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1216                 
1217                 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL);
1218                 g_free(text);
1219                 }
1220
1221         return error;
1222 }
1223
1224 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1225 {
1226         return start_editor_from_filelist_full(key, list,  NULL, NULL);
1227 }
1228
1229 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1230 {
1231         GList *list;
1232         EditorFlags error;
1233
1234         if (!fd) return FALSE;
1235
1236         list = g_list_append(NULL, fd);
1237         error = start_editor_from_filelist_full(key, list, cb, data);
1238         g_list_free(list);
1239         return error;
1240 }
1241
1242 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1243 {
1244         return start_editor_from_file_full(key, fd, NULL, NULL);
1245 }
1246
1247 gboolean editor_window_flag_set(const gchar *key)
1248 {
1249         EditorDescription *editor;
1250         if (!key) return TRUE;
1251         
1252         editor = g_hash_table_lookup(editors, key);
1253         if (!editor) return TRUE;
1254
1255         return !!(editor->flags & EDITOR_KEEP_FS);
1256 }
1257
1258 gboolean editor_is_filter(const gchar *key)
1259 {
1260         EditorDescription *editor;
1261         if (!key) return TRUE;
1262         
1263         editor = g_hash_table_lookup(editors, key);
1264         if (!editor) return TRUE;
1265
1266         return !!(editor->flags & EDITOR_DEST);
1267 }
1268
1269 const gchar *editor_get_error_str(EditorFlags flags)
1270 {
1271         if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1272         if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1273         if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1274         if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1275         if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1276         if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1277         if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1278         return _("Unknown error.");
1279 }
1280
1281 const gchar *editor_get_name(const gchar *key)
1282 {
1283         EditorDescription *editor = g_hash_table_lookup(editors, key);
1284
1285         if (!editor) return NULL;
1286
1287         return editor->name;
1288 }
1289 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */