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