do not block the files sent to external editors like gimp
[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 GtkListStore *desktop_file_list;
71
72
73 #ifdef G_KEY_FILE_DESKTOP_GROUP
74 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
75 #else
76 #define DESKTOP_GROUP "Desktop Entry"
77 #endif
78
79 void editor_description_free(EditorDescription *editor)
80 {
81         if (!editor) return;
82         
83         g_free(editor->key);
84         g_free(editor->name);
85         g_free(editor->icon);
86         g_free(editor->exec);
87         g_free(editor->menu_path);
88         g_free(editor->hotkey);
89         g_free(editor->comment);
90         string_list_free(editor->ext_list);
91         g_free(editor->file);
92         g_free(editor);
93 }
94
95 static GList *editor_mime_types_to_extensions(gchar **mime_types)
96 {
97         /* FIXME: this should be rewritten to use the shared mime database, as soon as we switch to gio */
98         
99         static const gchar *conv_table[][2] = {
100                 {"application/x-ufraw", ".ufraw"},
101                 {"image/*",             "*"},
102                 {"image/bmp",           ".bmp"},
103                 {"image/gif",           ".gif"},
104                 {"image/jpeg",          ".jpeg;.jpg"},
105                 {"image/jpg",           ".jpg;.jpeg"},
106                 {"image/pcx",           ".pcx"},
107                 {"image/png",           ".png"},
108                 {"image/svg",           ".svg"},
109                 {"image/svg+xml",       ".svg"},
110                 {"image/svg+xml-compressed",    ".svg"},        
111                 {"image/tiff",          ".tiff;.tif"},
112                 {"image/x-bmp",         ".bmp"},
113                 {"image/x-canon-crw",   ".crw"},
114                 {"image/x-cr2",         ".cr2"},
115                 {"image/x-dcraw",       "%raw"},
116                 {"image/x-ico",         ".ico"},
117                 {"image/x-mrw",         ".mrw"},
118                 {"image/x-MS-bmp",      ".bmp"},
119                 {"image/x-nef",         ".nef"},
120                 {"image/x-orf",         ".orf"},
121                 {"image/x-pcx",         ".pcx"},
122                 {"image/xpm",           ".xpm"},
123                 {"image/x-png",         ".png"},
124                 {"image/x-portable-anymap",     ".pam"},        
125                 {"image/x-portable-bitmap",     ".pbm"},
126                 {"image/x-portable-graymap",    ".pgm"},
127                 {"image/x-portable-pixmap",     ".ppm"},
128                 {"image/x-psd",         ".psd"},
129                 {"image/x-raf",         ".raf"},
130                 {"image/x-sgi",         ".sgi"},
131                 {"image/x-tga",         ".tga"},
132                 {"image/x-xbitmap",     ".xbm"},
133                 {"image/x-xcf",         ".xcf"},
134                 {"image/x-xpixmap",     ".xpm"},
135                 {"image/x-x3f",         ".x3f"},
136                 {NULL, NULL}};
137         
138         gint i, j;
139         GList *list = NULL;
140         
141         for (i = 0; mime_types[i]; i++) 
142                 for (j = 0; conv_table[j][0]; j++)
143                         if (strcmp(mime_types[i], conv_table[j][0]) == 0)
144                                 list = g_list_concat(list, filter_to_list(conv_table[j][1]));
145         
146         return list;
147 }
148
149 static gboolean editor_read_desktop_file(const gchar *path)
150 {
151         GKeyFile *key_file;
152         EditorDescription *editor;
153         gchar *extensions;
154         gchar *type;
155         const gchar *key = filename_from_path(path);
156         gchar **categories, **only_show_in, **not_show_in;
157         gchar *try_exec;
158         GtkTreeIter iter;
159         gboolean category_geeqie = FALSE;
160
161         if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
162         
163         key_file = g_key_file_new();
164         if (!g_key_file_load_from_file(key_file, path, 0, NULL))
165                 {
166                 g_key_file_free(key_file);
167                 return FALSE;
168                 }
169
170         type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", NULL);
171         if (!type || strcmp(type, "Application") != 0)
172                 {
173                 /* We only consider desktop entries of Application type */
174                 g_key_file_free(key_file);
175                 g_free(type);
176                 return FALSE;
177                 }
178         g_free(type);
179         
180         editor = g_new0(EditorDescription, 1);
181         
182         editor->key = g_strdup(key);
183         editor->file = g_strdup(path);
184
185         g_hash_table_insert(editors, editor->key, editor);
186
187         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", NULL)
188             || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", NULL))
189                 {
190                 editor->hidden = TRUE;
191                 }
192
193         categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", NULL, NULL);
194         if (categories)
195                 {
196                 gboolean found = FALSE;
197                 gint i;
198                 for (i = 0; categories[i]; i++) 
199                         {
200                         /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
201                         if (strcmp(categories[i], "Graphics") == 0)
202                                 {
203                                 found = TRUE;
204                                 }
205                         if (strcmp(categories[i], "X-Geeqie") == 0) 
206                                 {
207                                 found = TRUE;
208                                 category_geeqie = TRUE;
209                                 break;
210                                 }
211                         }
212                 if (!found) editor->ignored = TRUE;
213                 g_strfreev(categories);
214                 }
215         else
216                 {
217                 editor->ignored = TRUE;
218                 }
219
220         only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", NULL, NULL);
221         if (only_show_in)
222                 {
223                 gboolean found = FALSE;
224                 gint i;
225                 for (i = 0; only_show_in[i]; i++) 
226                         if (strcmp(only_show_in[i], "X-Geeqie") == 0)
227                                 {
228                                 found = TRUE;
229                                 break;
230                                 }
231                 if (!found) editor->ignored = TRUE;
232                 g_strfreev(only_show_in);
233                 }
234
235         not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", NULL, NULL);
236         if (not_show_in)
237                 {
238                 gboolean found = FALSE;
239                 gint i;
240                 for (i = 0; not_show_in[i]; i++) 
241                         if (strcmp(not_show_in[i], "X-Geeqie") == 0)
242                                 {
243                                 found = TRUE;
244                                 break;
245                                 }
246                 if (found) editor->ignored = TRUE;
247                 g_strfreev(not_show_in);
248                 }
249                 
250                 
251         try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", NULL);
252         if (try_exec && !editor->hidden && !editor->ignored)
253                 {
254                 gchar *try_exec_res = g_find_program_in_path(try_exec);
255                 if (!try_exec_res) editor->hidden = TRUE;
256                 g_free(try_exec_res);
257                 g_free(try_exec);
258                 }
259
260         if (editor->ignored) 
261                 {
262                 /* ignored editors will be deleted, no need to parse the rest */
263                 g_key_file_free(key_file);
264                 return TRUE;
265                 }
266         
267         editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", NULL, NULL);
268         editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", NULL);
269         
270         /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
271         if (editor->icon && !g_path_is_absolute(editor->icon))
272                 {
273                 gchar *ext = strrchr(editor->icon, '.');
274                 
275                 if (ext && strlen(ext) == 4 && 
276                     (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
277                         {
278                         log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
279                                    editor->file, editor->icon);
280                         
281                         // drop extension
282                         *ext = '\0';
283                         }
284                 }
285
286         editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", NULL);
287         
288         editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", NULL);
289         if (!editor->menu_path) editor->menu_path = g_strdup("EditMenu/ExternalMenu");
290         
291         editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", NULL);
292
293         editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", NULL);
294
295         extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", NULL);
296         if (extensions)
297                 editor->ext_list = filter_to_list(extensions);
298         else
299                 {
300                 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", NULL, NULL);
301                 if (mime_types)
302                         {
303                         editor->ext_list = editor_mime_types_to_extensions(mime_types);
304                         g_strfreev(mime_types);
305                         if (!editor->ext_list) editor->hidden = TRUE; 
306                         }
307                 }
308                 
309         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Keep-Fullscreen", NULL)) editor->flags |= EDITOR_KEEP_FS;
310         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", NULL)) editor->flags |= EDITOR_VERBOSE;
311         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose-Multi", NULL)) editor->flags |= EDITOR_VERBOSE_MULTI;
312         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", NULL)) editor->flags |= EDITOR_DEST;
313         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", NULL)) editor->flags |= EDITOR_TERMINAL;
314         
315         editor->flags |= editor_command_parse(editor, NULL, NULL);
316
317         if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
318
319         g_key_file_free(key_file);
320
321         if (editor->ignored) return TRUE;
322         
323         gtk_list_store_append(desktop_file_list, &iter);
324         gtk_list_store_set(desktop_file_list, &iter, 
325                            DESKTOP_FILE_COLUMN_KEY, key,
326                            DESKTOP_FILE_COLUMN_NAME, editor->name,
327                            DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden,
328                            DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
329                            DESKTOP_FILE_COLUMN_PATH, path, -1);
330         
331         return TRUE;    
332 }
333
334 static gboolean editor_remove_desktop_file_cb(gpointer key, gpointer value, gpointer user_data)
335 {
336         EditorDescription *editor = value;
337         return editor->hidden || editor->ignored;
338 }
339
340 static void editor_read_desktop_dir(const gchar *path)
341 {
342         DIR *dp;
343         struct dirent *dir;
344         gchar *pathl;
345
346         pathl = path_from_utf8(path);
347         dp = opendir(pathl);
348         g_free(pathl);
349         if (!dp)
350                 {
351                 /* dir not found */
352                 return;
353                 }
354         while ((dir = readdir(dp)) != NULL)
355                 {
356                 gchar *namel = dir->d_name;
357                 
358                 if (g_str_has_suffix(namel, ".desktop"))
359                         {
360                         gchar *name = path_to_utf8(namel);
361                         gchar *dpath = g_build_filename(path, name, NULL);
362                         editor_read_desktop_file(dpath);
363                         g_free(dpath);
364                         g_free(name);
365                         }       
366                 }
367         closedir(dp);
368 }
369
370 void editor_load_descriptions(void)
371 {
372         gchar *path;
373         gchar *xdg_data_dirs;
374         gchar *all_dirs;
375         gchar **split_dirs;
376         gint i;
377         
378         if (desktop_file_list)
379                 {
380                 gtk_list_store_clear(desktop_file_list);
381                 }
382         else 
383                 {
384                 desktop_file_list = gtk_list_store_new(DESKTOP_FILE_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_BOOLEAN, G_TYPE_STRING);
385                 }
386         if (editors)
387                 {
388                 g_hash_table_destroy(editors);
389                 }
390         editors = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)editor_description_free);
391
392         xdg_data_dirs = getenv("XDG_DATA_DIRS");
393         if (xdg_data_dirs && xdg_data_dirs[0])
394                 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
395         else
396                 xdg_data_dirs = g_strdup("/usr/share");
397         
398         all_dirs = g_strconcat(get_rc_dir(), ":", GQ_APP_DIR, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
399         
400         g_free(xdg_data_dirs);
401
402         split_dirs = g_strsplit(all_dirs, ":", 0);
403         
404         g_free(all_dirs);
405
406         for (i = 0; split_dirs[i]; i++)
407                 {
408                 path = g_build_filename(split_dirs[i], "applications", NULL);
409                 editor_read_desktop_dir(path);
410                 g_free(path);
411                 }
412                 
413         g_strfreev(split_dirs);
414         
415         g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, NULL);
416 }
417
418 static void editor_list_add_cb(gpointer key, gpointer value, gpointer data)
419 {
420         GList **listp = data;
421         EditorDescription *editor = value;
422         
423         /* do not show the special commands in any list, they are called explicitly */ 
424         if (strcmp(editor->key, CMD_COPY) == 0 ||
425             strcmp(editor->key, CMD_MOVE) == 0 ||  
426             strcmp(editor->key, CMD_RENAME) == 0 ||
427             strcmp(editor->key, CMD_DELETE) == 0 ||
428             strcmp(editor->key, CMD_FOLDER) == 0) return;
429
430         *listp = g_list_prepend(*listp, editor);
431 }
432
433 static gint editor_sort(gconstpointer a, gconstpointer b)
434 {
435         const EditorDescription *ea = a;
436         const EditorDescription *eb = b;
437         gint ret;
438         
439         ret = strcmp(ea->menu_path, eb->menu_path);
440         if (ret != 0) return ret;
441         
442         return g_utf8_collate(ea->name, eb->name);
443 }
444
445 GList *editor_list_get(void)
446 {
447         GList *editors_list = NULL;
448         g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
449         editors_list = g_list_sort(editors_list, editor_sort);
450
451         return editors_list;
452 }
453
454 /* ------------------------------ */
455
456
457 static void editor_verbose_data_free(EditorData *ed)
458 {
459         if (!ed->vd) return;
460         g_free(ed->vd);
461         ed->vd = NULL;
462 }
463
464 static void editor_data_free(EditorData *ed)
465 {
466         editor_verbose_data_free(ed);
467         g_free(ed);
468 }
469
470 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
471 {
472         EditorData *ed = data;
473
474         generic_dialog_close(gd);
475         editor_verbose_data_free(ed);
476         if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
477 }
478
479 static void editor_verbose_window_stop(GenericDialog *gd, gpointer data)
480 {
481         EditorData *ed = data;
482         ed->stopping = TRUE;
483         ed->count = 0;
484         editor_verbose_window_progress(ed, _("stopping..."));
485 }
486
487 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
488 {
489         vd->gd->cancel_cb = editor_verbose_window_close;
490
491         spinner_set_interval(vd->spinner, -1);
492         gtk_widget_set_sensitive(vd->button_stop, FALSE);
493         gtk_widget_set_sensitive(vd->button_close, TRUE);
494 }
495
496 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
497 {
498         EditorVerboseData *vd;
499         GtkWidget *scrolled;
500         GtkWidget *hbox;
501         gchar *buf;
502
503         vd = g_new0(EditorVerboseData, 1);
504
505         vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
506                                    NULL, FALSE,
507                                    NULL, ed);
508         buf = g_strdup_printf(_("Output of %s"), text);
509         generic_dialog_add_message(vd->gd, NULL, buf, NULL);
510         g_free(buf);
511         vd->button_stop = generic_dialog_add_button(vd->gd, GTK_STOCK_STOP, NULL,
512                                                    editor_verbose_window_stop, FALSE);
513         gtk_widget_set_sensitive(vd->button_stop, FALSE);
514         vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, NULL,
515                                                     editor_verbose_window_close, TRUE);
516         gtk_widget_set_sensitive(vd->button_close, FALSE);
517
518         scrolled = gtk_scrolled_window_new(NULL, NULL);
519         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
520         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
521                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
522         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
523         gtk_widget_show(scrolled);
524
525         vd->text = gtk_text_view_new();
526         gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
527         gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
528         gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
529         gtk_widget_show(vd->text);
530
531         hbox = gtk_hbox_new(FALSE, 0);
532         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
533         gtk_widget_show(hbox);
534
535         vd->progress = gtk_progress_bar_new();
536         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
537         gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
538         gtk_widget_show(vd->progress);
539
540         vd->spinner = spinner_new(NULL, SPINNER_SPEED);
541         gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
542         gtk_widget_show(vd->spinner);
543
544         gtk_widget_show(vd->gd->dialog);
545
546         ed->vd = vd;
547         return vd;
548 }
549
550 static void editor_verbose_window_fill(EditorVerboseData *vd, gchar *text, gint len)
551 {
552         GtkTextBuffer *buffer;
553         GtkTextIter iter;
554
555         buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
556         gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
557         gtk_text_buffer_insert(buffer, &iter, text, len);
558 }
559
560 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
561 {
562         if (!ed->vd) return;
563
564         if (ed->total)
565                 {
566                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), (gdouble)ed->count / ed->total);
567                 }
568
569         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
570 }
571
572 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
573 {
574         EditorData *ed = data;
575         gchar buf[512];
576         gsize count;
577
578         if (condition & G_IO_IN)
579                 {
580                 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, NULL) == G_IO_STATUS_NORMAL)
581                         {
582                         if (!g_utf8_validate(buf, count, NULL))
583                                 {
584                                 gchar *utf8;
585
586                                 utf8 = g_locale_to_utf8(buf, count, NULL, NULL, NULL);
587                                 if (utf8)
588                                         {
589                                         editor_verbose_window_fill(ed->vd, utf8, -1);
590                                         g_free(utf8);
591                                         }
592                                 else
593                                         {
594                                         editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
595                                         }
596                                 }
597                         else
598                                 {
599                                 editor_verbose_window_fill(ed->vd, buf, count);
600                                 }
601                         }
602                 }
603
604         if (condition & (G_IO_ERR | G_IO_HUP))
605                 {
606                 g_io_channel_shutdown(source, TRUE, NULL);
607                 return FALSE;
608                 }
609
610         return TRUE;
611 }
612
613 typedef enum {
614         PATH_FILE,
615         PATH_FILE_URL,
616         PATH_DEST
617 } PathType;
618
619
620 static gchar *editor_command_path_parse(const FileData *fd, PathType type, const EditorDescription *editor)
621 {
622         GString *string;
623         gchar *pathl;
624         const gchar *p = NULL;
625
626         string = g_string_new("");
627
628         if (type == PATH_FILE || type == PATH_FILE_URL)
629                 {
630                 GList *work = editor->ext_list;
631
632                 if (!work)
633                         p = fd->path;
634                 else
635                         {
636                         while (work)
637                                 {
638                                 GList *work2;
639                                 gchar *ext = work->data;
640                                 work = work->next;
641
642                                 if (strcmp(ext, "*") == 0 ||
643                                     g_ascii_strcasecmp(ext, fd->extension) == 0)
644                                         {
645                                         p = fd->path;
646                                         break;
647                                         }
648
649                                 work2 = fd->sidecar_files;
650                                 while (work2)
651                                         {
652                                         FileData *sfd = work2->data;
653                                         work2 = work2->next;
654
655                                         if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
656                                                 {
657                                                 p = sfd->path;
658                                                 break;
659                                                 }
660                                         }
661                                 if (p) break;
662                                 }
663                         if (!p) return NULL;
664                         }
665                 }
666         else if (type == PATH_DEST)
667                 {
668                 if (fd->change && fd->change->dest)
669                         p = fd->change->dest;
670                 else
671                         p = "";
672                 }
673
674         g_assert(p);
675         string = g_string_append(string, p);
676
677         if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
678         pathl = path_from_utf8(string->str);
679         g_string_free(string, TRUE);
680
681         if (pathl && !pathl[0]) /* empty string case */
682                 {
683                 g_free(pathl);
684                 pathl = NULL;
685                 }
686
687         return pathl;
688 }
689
690 static GString *append_quoted(GString *str, const char *s, gboolean single_quotes, gboolean double_quotes)
691 {
692         const char *p;
693         
694         if (!single_quotes)
695                 {
696                 if (!double_quotes)
697                         g_string_append_c(str, '\'');
698                 else
699                         g_string_append(str, "\"'");
700                 }
701
702         for (p = s; *p != '\0'; p++)
703                 {
704                 if (*p == '\'')
705                         g_string_append(str, "'\\''");
706                 else
707                         g_string_append_c(str, *p);
708                 }
709         
710         if (!single_quotes)
711                 {
712                 if (!double_quotes)
713                         g_string_append_c(str, '\'');
714                 else
715                         g_string_append(str, "'\"");
716                 }
717
718         return str;
719 }
720
721
722 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gchar **output)
723 {
724         EditorFlags flags = 0;
725         const gchar *p;
726         GString *result = NULL;
727         gboolean escape = FALSE;
728         gboolean single_quotes = FALSE;
729         gboolean double_quotes = FALSE;
730
731         if (output)
732                 result = g_string_new("");
733
734         if (editor->exec[0] == '\0')
735                 {
736                 flags |= EDITOR_ERROR_EMPTY;
737                 goto err;
738                 }
739         
740         p = editor->exec;
741         /* skip leading whitespaces if any */
742         while (g_ascii_isspace(*p)) p++;
743
744         /* command */
745
746         while (*p)
747                 {
748                 if (escape)
749                         {
750                         escape = FALSE;
751                         if (output) result = g_string_append_c(result, *p);
752                         }
753                 else if (*p == '\\')
754                         {
755                         if (!single_quotes) escape = TRUE;
756                         if (output) result = g_string_append_c(result, *p);
757                         }
758                 else if (*p == '\'')
759                         {
760                         if (output) result = g_string_append_c(result, *p);
761                         if (!single_quotes && !double_quotes)
762                                 single_quotes = TRUE;
763                         else if (single_quotes)
764                                 single_quotes = FALSE;
765                         }
766                 else if (*p == '"')
767                         {
768                         if (output) result = g_string_append_c(result, *p);
769                         if (!single_quotes && !double_quotes)
770                                 double_quotes = TRUE;
771                         else if (double_quotes)
772                                 double_quotes = FALSE;
773                         }
774                 else if (*p == '%' && p[1])
775                         {
776                         gchar *pathl = NULL;
777
778                         p++;
779
780                         switch (*p)
781                                 {
782                                 case 'f': /* single file */
783                                 case 'u': /* single url */
784                                         flags |= EDITOR_FOR_EACH;
785                                         if (flags & EDITOR_SINGLE_COMMAND)
786                                                 {
787                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
788                                                 goto err;
789                                                 }
790                                         if (list)
791                                                 {
792                                                 /* use the first file from the list */
793                                                 if (!list->data)
794                                                         {
795                                                         flags |= EDITOR_ERROR_NO_FILE;
796                                                         goto err;
797                                                         }
798                                                 pathl = editor_command_path_parse((FileData *)list->data,
799                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
800                                                                                   editor);
801                                                 if (!pathl)
802                                                         {
803                                                         flags |= EDITOR_ERROR_NO_FILE;
804                                                         goto err;
805                                                         }
806                                                 if (output)
807                                                         {
808                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
809                                                         }
810                                                 g_free(pathl);
811                                                 }
812                                         break;
813
814                                 case 'F':
815                                 case 'U':
816                                         flags |= EDITOR_SINGLE_COMMAND;
817                                         if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
818                                                 {
819                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
820                                                 goto err;
821                                                 }
822
823                                         if (list)
824                                                 {
825                                                 /* use whole list */
826                                                 GList *work = list;
827                                                 gboolean ok = FALSE;
828
829                                                 while (work)
830                                                         {
831                                                         FileData *fd = work->data;
832                                                         pathl = editor_command_path_parse(fd, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
833                                                         if (pathl)
834                                                                 {
835                                                                 ok = TRUE;
836
837                                                                 if (output)
838                                                                         {
839                                                                         ok = TRUE;
840                                                                         if (work != list) g_string_append_c(result, ' ');
841                                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
842                                                                         }
843                                                                 g_free(pathl);
844                                                                 }
845                                                         work = work->next;
846                                                         }
847                                                 if (!ok)
848                                                         {
849                                                         flags |= EDITOR_ERROR_NO_FILE;
850                                                         goto err;
851                                                         }
852                                                 }
853                                         break;
854                                 case 'i':
855                                         if (editor->icon && *editor->icon)
856                                                 {
857                                                 if (output)
858                                                         {
859                                                         result = g_string_append(result, "--icon ");
860                                                         result = append_quoted(result, editor->icon, single_quotes, double_quotes);
861                                                         }
862                                                 }
863                                         break;
864                                 case 'c':
865                                         if (output)
866                                                 {
867                                                 result = append_quoted(result, editor->name, single_quotes, double_quotes);
868                                                 }
869                                         break;
870                                 case 'k':
871                                         if (output)
872                                                 {
873                                                 result = append_quoted(result, editor->file, single_quotes, double_quotes);
874                                                 }
875                                         break;
876                                 case '%':
877                                         /* %% = % escaping */
878                                         if (output) result = g_string_append_c(result, *p);
879                                         break;
880                                 case 'd':
881                                 case 'D':
882                                 case 'n':
883                                 case 'N':
884                                 case 'v':
885                                 case 'm':
886                                         /* deprecated according to spec, ignore */
887                                         break;
888                                 default:
889                                         flags |= EDITOR_ERROR_SYNTAX;
890                                         goto err;
891                                 }
892                         }
893                 else
894                         {
895                         if (output) result = g_string_append_c(result, *p);
896                         }
897                 p++;
898                 }
899
900         if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags |= EDITOR_NO_PARAM;
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 = (ed->flags & EDITOR_NO_PARAM) ? NULL : 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 = fd ? remove_level_from_path(fd->path) : NULL;
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->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1046                 {
1047                 FileData *fd;
1048                 EditorFlags error;
1049
1050                 fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : ed->list->data;
1051
1052                 if (ed->vd)
1053                         {
1054                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
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) && fd)
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 | EDITOR_NO_PARAM)) ? 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 (!editor) return FALSE;
1209         if (!list && !(editor->flags & EDITOR_NO_PARAM)) 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 EditorFlags start_editor(const gchar *key)
1248 {
1249         return start_editor_from_filelist_full(key, NULL, NULL, NULL);
1250 }
1251
1252 gboolean editor_window_flag_set(const gchar *key)
1253 {
1254         EditorDescription *editor;
1255         if (!key) return TRUE;
1256         
1257         editor = g_hash_table_lookup(editors, key);
1258         if (!editor) return TRUE;
1259
1260         return !!(editor->flags & EDITOR_KEEP_FS);
1261 }
1262
1263 gboolean editor_is_filter(const gchar *key)
1264 {
1265         EditorDescription *editor;
1266         if (!key) return TRUE;
1267         
1268         editor = g_hash_table_lookup(editors, key);
1269         if (!editor) return TRUE;
1270
1271         return !!(editor->flags & EDITOR_DEST);
1272 }
1273
1274 gboolean editor_no_param(const gchar *key)
1275 {
1276         EditorDescription *editor;
1277         if (!key) return FALSE;
1278         
1279         editor = g_hash_table_lookup(editors, key);
1280         if (!editor) return FALSE;
1281
1282         return !!(editor->flags & EDITOR_NO_PARAM);
1283 }
1284
1285 gboolean editor_blocks_file(const gchar *key)
1286 {
1287         EditorDescription *editor;
1288         if (!key) return FALSE;
1289         
1290         editor = g_hash_table_lookup(editors, key);
1291         if (!editor) return FALSE;
1292
1293         /* Decide if the image file should be blocked during editor execution
1294            Editors like gimp can be used long time after the original file was
1295            saved, for editing unrelated files.
1296            %f vs. %F seems to be a good heuristic to detect this kind of editors.
1297         */
1298            
1299         return !(editor->flags & EDITOR_SINGLE_COMMAND);
1300 }
1301
1302 const gchar *editor_get_error_str(EditorFlags flags)
1303 {
1304         if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1305         if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1306         if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1307         if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1308         if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1309         if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1310         if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1311         return _("Unknown error.");
1312 }
1313
1314 const gchar *editor_get_name(const gchar *key)
1315 {
1316         EditorDescription *editor = g_hash_table_lookup(editors, key);
1317
1318         if (!editor) return NULL;
1319
1320         return editor->name;
1321 }
1322 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */