run external commands from current directory even with no files
[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         gchar *working_directory; /* fallback if no files are given (editor_no_param) */
56 };
57
58
59 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
60 static EditorFlags editor_command_next_start(EditorData *ed);
61 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
62 static EditorFlags editor_command_done(EditorData *ed);
63
64 /*
65  *-----------------------------------------------------------------------------
66  * external editor routines
67  *-----------------------------------------------------------------------------
68  */
69
70 GHashTable *editors = NULL;
71 GtkListStore *desktop_file_list;
72
73
74 #ifdef G_KEY_FILE_DESKTOP_GROUP
75 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
76 #else
77 #define DESKTOP_GROUP "Desktop Entry"
78 #endif
79
80 void editor_description_free(EditorDescription *editor)
81 {
82         if (!editor) return;
83         
84         g_free(editor->key);
85         g_free(editor->name);
86         g_free(editor->icon);
87         g_free(editor->exec);
88         g_free(editor->menu_path);
89         g_free(editor->hotkey);
90         g_free(editor->comment);
91         string_list_free(editor->ext_list);
92         g_free(editor->file);
93         g_free(editor);
94 }
95
96 static GList *editor_mime_types_to_extensions(gchar **mime_types)
97 {
98         /* FIXME: this should be rewritten to use the shared mime database, as soon as we switch to gio */
99         
100         static const gchar *conv_table[][2] = {
101                 {"application/x-ufraw", ".ufraw"},
102                 {"image/*",             "*"},
103                 {"image/bmp",           ".bmp"},
104                 {"image/gif",           ".gif"},
105                 {"image/jpeg",          ".jpeg;.jpg"},
106                 {"image/jpg",           ".jpg;.jpeg"},
107                 {"image/pcx",           ".pcx"},
108                 {"image/png",           ".png"},
109                 {"image/svg",           ".svg"},
110                 {"image/svg+xml",       ".svg"},
111                 {"image/svg+xml-compressed",    ".svg"},        
112                 {"image/tiff",          ".tiff;.tif"},
113                 {"image/x-bmp",         ".bmp"},
114                 {"image/x-canon-crw",   ".crw"},
115                 {"image/x-cr2",         ".cr2"},
116                 {"image/x-dcraw",       "%raw"},
117                 {"image/x-ico",         ".ico"},
118                 {"image/x-mrw",         ".mrw"},
119                 {"image/x-MS-bmp",      ".bmp"},
120                 {"image/x-nef",         ".nef"},
121                 {"image/x-orf",         ".orf"},
122                 {"image/x-pcx",         ".pcx"},
123                 {"image/xpm",           ".xpm"},
124                 {"image/x-png",         ".png"},
125                 {"image/x-portable-anymap",     ".pam"},        
126                 {"image/x-portable-bitmap",     ".pbm"},
127                 {"image/x-portable-graymap",    ".pgm"},
128                 {"image/x-portable-pixmap",     ".ppm"},
129                 {"image/x-psd",         ".psd"},
130                 {"image/x-raf",         ".raf"},
131                 {"image/x-sgi",         ".sgi"},
132                 {"image/x-tga",         ".tga"},
133                 {"image/x-xbitmap",     ".xbm"},
134                 {"image/x-xcf",         ".xcf"},
135                 {"image/x-xpixmap",     ".xpm"},
136                 {"image/x-x3f",         ".x3f"},
137                 {NULL, NULL}};
138         
139         gint i, j;
140         GList *list = NULL;
141         
142         for (i = 0; mime_types[i]; i++) 
143                 for (j = 0; conv_table[j][0]; j++)
144                         if (strcmp(mime_types[i], conv_table[j][0]) == 0)
145                                 list = g_list_concat(list, filter_to_list(conv_table[j][1]));
146         
147         return list;
148 }
149
150 static gboolean editor_read_desktop_file(const gchar *path)
151 {
152         GKeyFile *key_file;
153         EditorDescription *editor;
154         gchar *extensions;
155         gchar *type;
156         const gchar *key = filename_from_path(path);
157         gchar **categories, **only_show_in, **not_show_in;
158         gchar *try_exec;
159         GtkTreeIter iter;
160         gboolean category_geeqie = FALSE;
161
162         if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
163         
164         key_file = g_key_file_new();
165         if (!g_key_file_load_from_file(key_file, path, 0, NULL))
166                 {
167                 g_key_file_free(key_file);
168                 return FALSE;
169                 }
170
171         type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", NULL);
172         if (!type || strcmp(type, "Application") != 0)
173                 {
174                 /* We only consider desktop entries of Application type */
175                 g_key_file_free(key_file);
176                 g_free(type);
177                 return FALSE;
178                 }
179         g_free(type);
180         
181         editor = g_new0(EditorDescription, 1);
182         
183         editor->key = g_strdup(key);
184         editor->file = g_strdup(path);
185
186         g_hash_table_insert(editors, editor->key, editor);
187
188         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", NULL)
189             || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", NULL))
190                 {
191                 editor->hidden = TRUE;
192                 }
193
194         categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", NULL, NULL);
195         if (categories)
196                 {
197                 gboolean found = FALSE;
198                 gint i;
199                 for (i = 0; categories[i]; i++) 
200                         {
201                         /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
202                         if (strcmp(categories[i], "Graphics") == 0)
203                                 {
204                                 found = TRUE;
205                                 }
206                         if (strcmp(categories[i], "X-Geeqie") == 0) 
207                                 {
208                                 found = TRUE;
209                                 category_geeqie = TRUE;
210                                 break;
211                                 }
212                         }
213                 if (!found) editor->ignored = TRUE;
214                 g_strfreev(categories);
215                 }
216         else
217                 {
218                 editor->ignored = TRUE;
219                 }
220
221         only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", NULL, NULL);
222         if (only_show_in)
223                 {
224                 gboolean found = FALSE;
225                 gint i;
226                 for (i = 0; only_show_in[i]; i++) 
227                         if (strcmp(only_show_in[i], "X-Geeqie") == 0)
228                                 {
229                                 found = TRUE;
230                                 break;
231                                 }
232                 if (!found) editor->ignored = TRUE;
233                 g_strfreev(only_show_in);
234                 }
235
236         not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", NULL, NULL);
237         if (not_show_in)
238                 {
239                 gboolean found = FALSE;
240                 gint i;
241                 for (i = 0; not_show_in[i]; i++) 
242                         if (strcmp(not_show_in[i], "X-Geeqie") == 0)
243                                 {
244                                 found = TRUE;
245                                 break;
246                                 }
247                 if (found) editor->ignored = TRUE;
248                 g_strfreev(not_show_in);
249                 }
250                 
251                 
252         try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", NULL);
253         if (try_exec && !editor->hidden && !editor->ignored)
254                 {
255                 gchar *try_exec_res = g_find_program_in_path(try_exec);
256                 if (!try_exec_res) editor->hidden = TRUE;
257                 g_free(try_exec_res);
258                 g_free(try_exec);
259                 }
260
261         if (editor->ignored) 
262                 {
263                 /* ignored editors will be deleted, no need to parse the rest */
264                 g_key_file_free(key_file);
265                 return TRUE;
266                 }
267         
268         editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", NULL, NULL);
269         editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", NULL);
270         
271         /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
272         if (editor->icon && !g_path_is_absolute(editor->icon))
273                 {
274                 gchar *ext = strrchr(editor->icon, '.');
275                 
276                 if (ext && strlen(ext) == 4 && 
277                     (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
278                         {
279                         log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
280                                    editor->file, editor->icon);
281                         
282                         // drop extension
283                         *ext = '\0';
284                         }
285                 }
286
287         editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", NULL);
288         
289         editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", NULL);
290         if (!editor->menu_path) editor->menu_path = g_strdup("EditMenu/ExternalMenu");
291         
292         editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", NULL);
293
294         editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", NULL);
295
296         extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", NULL);
297         if (extensions)
298                 editor->ext_list = filter_to_list(extensions);
299         else
300                 {
301                 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", NULL, NULL);
302                 if (mime_types)
303                         {
304                         editor->ext_list = editor_mime_types_to_extensions(mime_types);
305                         g_strfreev(mime_types);
306                         if (!editor->ext_list) editor->hidden = TRUE; 
307                         }
308                 }
309                 
310         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Keep-Fullscreen", NULL)) editor->flags |= EDITOR_KEEP_FS;
311         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", NULL)) editor->flags |= EDITOR_VERBOSE;
312         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose-Multi", NULL)) editor->flags |= EDITOR_VERBOSE_MULTI;
313         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", NULL)) editor->flags |= EDITOR_DEST;
314         if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", NULL)) editor->flags |= EDITOR_TERMINAL;
315         
316         editor->flags |= editor_command_parse(editor, NULL, NULL);
317
318         if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
319
320         g_key_file_free(key_file);
321
322         if (editor->ignored) return TRUE;
323         
324         gtk_list_store_append(desktop_file_list, &iter);
325         gtk_list_store_set(desktop_file_list, &iter, 
326                            DESKTOP_FILE_COLUMN_KEY, key,
327                            DESKTOP_FILE_COLUMN_NAME, editor->name,
328                            DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden,
329                            DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
330                            DESKTOP_FILE_COLUMN_PATH, path, -1);
331         
332         return TRUE;    
333 }
334
335 static gboolean editor_remove_desktop_file_cb(gpointer key, gpointer value, gpointer user_data)
336 {
337         EditorDescription *editor = value;
338         return editor->hidden || editor->ignored;
339 }
340
341 static void editor_read_desktop_dir(const gchar *path)
342 {
343         DIR *dp;
344         struct dirent *dir;
345         gchar *pathl;
346
347         pathl = path_from_utf8(path);
348         dp = opendir(pathl);
349         g_free(pathl);
350         if (!dp)
351                 {
352                 /* dir not found */
353                 return;
354                 }
355         while ((dir = readdir(dp)) != NULL)
356                 {
357                 gchar *namel = dir->d_name;
358                 
359                 if (g_str_has_suffix(namel, ".desktop"))
360                         {
361                         gchar *name = path_to_utf8(namel);
362                         gchar *dpath = g_build_filename(path, name, NULL);
363                         editor_read_desktop_file(dpath);
364                         g_free(dpath);
365                         g_free(name);
366                         }       
367                 }
368         closedir(dp);
369 }
370
371 void editor_load_descriptions(void)
372 {
373         gchar *path;
374         gchar *xdg_data_dirs;
375         gchar *all_dirs;
376         gchar **split_dirs;
377         gint i;
378         
379         if (desktop_file_list)
380                 {
381                 gtk_list_store_clear(desktop_file_list);
382                 }
383         else 
384                 {
385                 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);
386                 }
387         if (editors)
388                 {
389                 g_hash_table_destroy(editors);
390                 }
391         editors = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)editor_description_free);
392
393         xdg_data_dirs = getenv("XDG_DATA_DIRS");
394         if (xdg_data_dirs && xdg_data_dirs[0])
395                 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
396         else
397                 xdg_data_dirs = g_strdup("/usr/share");
398         
399         all_dirs = g_strconcat(get_rc_dir(), ":", GQ_APP_DIR, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
400         
401         g_free(xdg_data_dirs);
402
403         split_dirs = g_strsplit(all_dirs, ":", 0);
404         
405         g_free(all_dirs);
406
407         for (i = 0; split_dirs[i]; i++)
408                 {
409                 path = g_build_filename(split_dirs[i], "applications", NULL);
410                 editor_read_desktop_dir(path);
411                 g_free(path);
412                 }
413                 
414         g_strfreev(split_dirs);
415         
416         g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, NULL);
417 }
418
419 static void editor_list_add_cb(gpointer key, gpointer value, gpointer data)
420 {
421         GList **listp = data;
422         EditorDescription *editor = value;
423         
424         /* do not show the special commands in any list, they are called explicitly */ 
425         if (strcmp(editor->key, CMD_COPY) == 0 ||
426             strcmp(editor->key, CMD_MOVE) == 0 ||  
427             strcmp(editor->key, CMD_RENAME) == 0 ||
428             strcmp(editor->key, CMD_DELETE) == 0 ||
429             strcmp(editor->key, CMD_FOLDER) == 0) return;
430
431         *listp = g_list_prepend(*listp, editor);
432 }
433
434 static gint editor_sort(gconstpointer a, gconstpointer b)
435 {
436         const EditorDescription *ea = a;
437         const EditorDescription *eb = b;
438         gint ret;
439         
440         ret = strcmp(ea->menu_path, eb->menu_path);
441         if (ret != 0) return ret;
442         
443         return g_utf8_collate(ea->name, eb->name);
444 }
445
446 GList *editor_list_get(void)
447 {
448         GList *editors_list = NULL;
449         g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
450         editors_list = g_list_sort(editors_list, editor_sort);
451
452         return editors_list;
453 }
454
455 /* ------------------------------ */
456
457
458 static void editor_verbose_data_free(EditorData *ed)
459 {
460         if (!ed->vd) return;
461         g_free(ed->vd);
462         ed->vd = NULL;
463 }
464
465 static void editor_data_free(EditorData *ed)
466 {
467         editor_verbose_data_free(ed);
468         g_free(ed->working_directory);
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 (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags |= EDITOR_NO_PARAM;
903
904         if (output)
905                 {
906                 *output = g_string_free(result, FALSE);
907                 DEBUG_3("Editor cmd: %s", *output);
908                 }
909
910         return flags;
911
912
913 err:
914         if (output)
915                 {
916                 g_string_free(result, TRUE);
917                 *output = NULL;
918                 }
919         return flags;
920 }
921
922
923 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
924 {
925         EditorData *ed = data;
926         g_spawn_close_pid(pid);
927         ed->pid = -1;
928
929         editor_command_next_finish(ed, status);
930 }
931
932
933 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
934 {
935         gchar *command;
936         FileData *fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : list->data;;
937         GPid pid;
938         gint standard_output;
939         gint standard_error;
940         gboolean ok;
941
942         ed->pid = -1;
943         ed->flags = editor->flags;
944         ed->flags |= editor_command_parse(editor, list, &command);
945
946         ok = !EDITOR_ERRORS(ed->flags);
947
948         if (ok)
949                 {
950                 ok = (options->shell.path && *options->shell.path);
951                 if (!ok) log_printf("ERROR: empty shell command\n");
952                         
953                 if (ok)
954                         {
955                         ok = (access(options->shell.path, X_OK) == 0);
956                         if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
957                         }
958
959                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
960                 }
961
962         if (ok)
963                 {
964                 gchar *working_directory;
965                 gchar *args[4];
966                 guint n = 0;
967
968                 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
969                 args[n++] = options->shell.path;
970                 if (options->shell.options && *options->shell.options)
971                         args[n++] = options->shell.options;
972                 args[n++] = command;
973                 args[n] = NULL;
974
975                 if ((ed->flags & EDITOR_DEST) && fd->change && fd->change->dest) /* FIXME: error handling */
976                         {
977                         g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
978                         }
979                 else
980                         {
981                         g_unsetenv("GEEQIE_DESTINATION");
982                         }
983
984                 ok = g_spawn_async_with_pipes(working_directory, args, NULL,
985                                       G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
986                                       NULL, NULL,
987                                       &pid,
988                                       NULL,
989                                       ed->vd ? &standard_output : NULL,
990                                       ed->vd ? &standard_error : NULL,
991                                       NULL);
992                 
993                 g_free(working_directory);
994
995                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
996                 }
997
998         if (ok)
999                 {
1000                 g_child_watch_add(pid, editor_child_exit_cb, ed);
1001                 ed->pid = pid;
1002                 }
1003
1004         if (ed->vd)
1005                 {
1006                 if (!ok)
1007                         {
1008                         gchar *buf;
1009
1010                         buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1011                         editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1012                         g_free(buf);
1013
1014                         }
1015                 else
1016                         {
1017                         GIOChannel *channel_output;
1018                         GIOChannel *channel_error;
1019
1020                         channel_output = g_io_channel_unix_new(standard_output);
1021                         g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, NULL);
1022                         g_io_channel_set_encoding(channel_output, NULL, NULL);
1023
1024                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1025                                             editor_verbose_io_cb, ed, NULL);
1026                         g_io_channel_unref(channel_output);
1027
1028                         channel_error = g_io_channel_unix_new(standard_error);
1029                         g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, NULL);
1030                         g_io_channel_set_encoding(channel_error, NULL, NULL);
1031
1032                         g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1033                                             editor_verbose_io_cb, ed, NULL);
1034                         g_io_channel_unref(channel_error);
1035                         }
1036                 }
1037
1038         g_free(command);
1039
1040         return EDITOR_ERRORS(ed->flags);
1041 }
1042
1043 static EditorFlags editor_command_next_start(EditorData *ed)
1044 {
1045         if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1046
1047         if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1048                 {
1049                 FileData *fd;
1050                 EditorFlags error;
1051
1052                 fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : ed->list->data;
1053
1054                 if (ed->vd)
1055                         {
1056                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1057                                 editor_verbose_window_progress(ed, fd->path);
1058                         else
1059                                 editor_verbose_window_progress(ed, _("running..."));
1060                         }
1061                 ed->count++;
1062
1063                 error = editor_command_one(ed->editor, ed->list, ed);
1064                 if (!error && ed->vd)
1065                         {
1066                         gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != NULL) );
1067                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1068                                 {
1069                                 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1070                                 editor_verbose_window_fill(ed->vd, "\n", 1);
1071                                 }
1072                         }
1073
1074                 if (!error)
1075                         return 0;
1076                 
1077                 /* command was not started, call the finish immediately */
1078                 return editor_command_next_finish(ed, 0);
1079                 }
1080
1081         /* everything is done */
1082         return editor_command_done(ed);
1083 }
1084
1085 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1086 {
1087         gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1088
1089         if (status)
1090                 ed->flags |= EDITOR_ERROR_STATUS;
1091
1092         if (ed->flags & EDITOR_FOR_EACH)
1093                 {
1094                 /* handle the first element from the list */
1095                 GList *fd_element = ed->list;
1096
1097                 ed->list = g_list_remove_link(ed->list, fd_element);
1098                 if (ed->callback)
1099                         {
1100                         cont = ed->callback(ed->list ? ed : NULL, ed->flags, fd_element, ed->data);
1101                         if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1102                         }
1103                 filelist_free(fd_element);
1104                 }
1105         else
1106                 {
1107                 /* handle whole list */
1108                 if (ed->callback)
1109                         cont = ed->callback(NULL, ed->flags, ed->list, ed->data);
1110                 filelist_free(ed->list);
1111                 ed->list = NULL;
1112                 }
1113
1114         switch (cont)
1115                 {
1116                 case EDITOR_CB_SUSPEND:
1117                         return EDITOR_ERRORS(ed->flags);
1118                 case EDITOR_CB_SKIP:
1119                         return editor_command_done(ed);
1120                 }
1121         
1122         return editor_command_next_start(ed);
1123 }
1124
1125 static EditorFlags editor_command_done(EditorData *ed)
1126 {
1127         EditorFlags flags;
1128
1129         if (ed->vd)
1130                 {
1131                 if (ed->count == ed->total)
1132                         {
1133                         editor_verbose_window_progress(ed, _("done"));
1134                         }
1135                 else
1136                         {
1137                         editor_verbose_window_progress(ed, _("stopped by user"));
1138                         }
1139                 editor_verbose_window_enable_close(ed->vd);
1140                 }
1141
1142         /* free the not-handled items */
1143         if (ed->list)
1144                 {
1145                 ed->flags |= EDITOR_ERROR_SKIPPED;
1146                 if (ed->callback) ed->callback(NULL, ed->flags, ed->list, ed->data);
1147                 filelist_free(ed->list);
1148                 ed->list = NULL;
1149                 }
1150
1151         ed->count = 0;
1152
1153         flags = EDITOR_ERRORS(ed->flags);
1154
1155         if (!ed->vd) editor_data_free(ed);
1156
1157         return flags;
1158 }
1159
1160 void editor_resume(gpointer ed)
1161 {
1162         editor_command_next_start(ed);
1163 }
1164
1165 void editor_skip(gpointer ed)
1166 {
1167         editor_command_done(ed);
1168 }
1169
1170 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1171 {
1172         EditorData *ed;
1173         EditorFlags flags = editor->flags;
1174
1175         if (EDITOR_ERRORS(flags)) return EDITOR_ERRORS(flags);
1176
1177         ed = g_new0(EditorData, 1);
1178         ed->list = filelist_copy(list);
1179         ed->flags = flags;
1180         ed->editor = editor;
1181         ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1182         ed->callback = cb;
1183         ed->data = data;
1184         ed->working_directory = g_strdup(working_directory);
1185
1186         if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1187                 flags |= EDITOR_VERBOSE;
1188
1189         if (flags & EDITOR_VERBOSE)
1190                 editor_verbose_window(ed, text);
1191
1192         editor_command_next_start(ed);
1193         /* errors from editor_command_next_start will be handled via callback */
1194         return EDITOR_ERRORS(flags);
1195 }
1196
1197 gboolean is_valid_editor_command(const gchar *key)
1198 {
1199         if (!key) return FALSE;
1200         return g_hash_table_lookup(editors, key) != NULL;
1201 }
1202
1203 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1204 {
1205         EditorFlags error;
1206         EditorDescription *editor;
1207         if (!key) return FALSE;
1208         
1209         editor = g_hash_table_lookup(editors, key);
1210
1211         if (!editor) return FALSE;
1212         if (!list && !(editor->flags & EDITOR_NO_PARAM)) return FALSE;
1213
1214         error = editor_command_start(editor, editor->name, list, working_directory, cb, data);
1215
1216         if (EDITOR_ERRORS(error))
1217                 {
1218                 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1219                 
1220                 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL);
1221                 g_free(text);
1222                 }
1223
1224         return error;
1225 }
1226
1227 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1228 {
1229         return start_editor_from_filelist_full(key, list, NULL, NULL, NULL);
1230 }
1231
1232 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1233 {
1234         GList *list;
1235         EditorFlags error;
1236
1237         if (!fd) return FALSE;
1238
1239         list = g_list_append(NULL, fd);
1240         error = start_editor_from_filelist_full(key, list, NULL, cb, data);
1241         g_list_free(list);
1242         return error;
1243 }
1244
1245 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1246 {
1247         return start_editor_from_file_full(key, fd, NULL, NULL);
1248 }
1249
1250 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1251 {
1252         return start_editor_from_filelist_full(key, NULL, working_directory, NULL, NULL);
1253 }
1254
1255 gboolean editor_window_flag_set(const gchar *key)
1256 {
1257         EditorDescription *editor;
1258         if (!key) return TRUE;
1259         
1260         editor = g_hash_table_lookup(editors, key);
1261         if (!editor) return TRUE;
1262
1263         return !!(editor->flags & EDITOR_KEEP_FS);
1264 }
1265
1266 gboolean editor_is_filter(const gchar *key)
1267 {
1268         EditorDescription *editor;
1269         if (!key) return TRUE;
1270         
1271         editor = g_hash_table_lookup(editors, key);
1272         if (!editor) return TRUE;
1273
1274         return !!(editor->flags & EDITOR_DEST);
1275 }
1276
1277 gboolean editor_no_param(const gchar *key)
1278 {
1279         EditorDescription *editor;
1280         if (!key) return FALSE;
1281         
1282         editor = g_hash_table_lookup(editors, key);
1283         if (!editor) return FALSE;
1284
1285         return !!(editor->flags & EDITOR_NO_PARAM);
1286 }
1287
1288 gboolean editor_blocks_file(const gchar *key)
1289 {
1290         EditorDescription *editor;
1291         if (!key) return FALSE;
1292         
1293         editor = g_hash_table_lookup(editors, key);
1294         if (!editor) return FALSE;
1295
1296         /* Decide if the image file should be blocked during editor execution
1297            Editors like gimp can be used long time after the original file was
1298            saved, for editing unrelated files.
1299            %f vs. %F seems to be a good heuristic to detect this kind of editors.
1300         */
1301            
1302         return !(editor->flags & EDITOR_SINGLE_COMMAND);
1303 }
1304
1305 const gchar *editor_get_error_str(EditorFlags flags)
1306 {
1307         if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1308         if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1309         if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1310         if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1311         if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1312         if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1313         if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1314         return _("Unknown error.");
1315 }
1316
1317 const gchar *editor_get_name(const gchar *key)
1318 {
1319         EditorDescription *editor = g_hash_table_lookup(editors, key);
1320
1321         if (!editor) return NULL;
1322
1323         return editor->name;
1324 }
1325 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */