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