editor fixes
[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, FALSE, 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 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, gboolean consider_sidecars, 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 = consider_sidecars ? fd->sidecar_files : NULL;
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, gboolean consider_sidecars, 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                                                                                   consider_sidecars,
822                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
823                                                                                   editor);
824                                                 if (!pathl)
825                                                         {
826                                                         flags |= EDITOR_ERROR_NO_FILE;
827                                                         goto err;
828                                                         }
829                                                 if (output)
830                                                         {
831                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
832                                                         }
833                                                 g_free(pathl);
834                                                 }
835                                         break;
836
837                                 case 'F':
838                                 case 'U':
839                                         flags |= EDITOR_SINGLE_COMMAND;
840                                         if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
841                                                 {
842                                                 flags |= EDITOR_ERROR_INCOMPATIBLE;
843                                                 goto err;
844                                                 }
845
846                                         if (list)
847                                                 {
848                                                 /* use whole list */
849                                                 GList *work = list;
850                                                 gboolean ok = FALSE;
851
852                                                 while (work)
853                                                         {
854                                                         FileData *fd = work->data;
855                                                         pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
856                                                         if (pathl)
857                                                                 {
858                                                                 ok = TRUE;
859
860                                                                 if (output)
861                                                                         {
862                                                                         ok = TRUE;
863                                                                         if (work != list) g_string_append_c(result, ' ');
864                                                                         result = append_quoted(result, pathl, single_quotes, double_quotes);
865                                                                         }
866                                                                 g_free(pathl);
867                                                                 }
868                                                         work = work->next;
869                                                         }
870                                                 if (!ok)
871                                                         {
872                                                         flags |= EDITOR_ERROR_NO_FILE;
873                                                         goto err;
874                                                         }
875                                                 }
876                                         break;
877                                 case 'i':
878                                         if (editor->icon && *editor->icon)
879                                                 {
880                                                 if (output)
881                                                         {
882                                                         result = g_string_append(result, "--icon ");
883                                                         result = append_quoted(result, editor->icon, single_quotes, double_quotes);
884                                                         }
885                                                 }
886                                         break;
887                                 case 'c':
888                                         if (output)
889                                                 {
890                                                 result = append_quoted(result, editor->name, single_quotes, double_quotes);
891                                                 }
892                                         break;
893                                 case 'k':
894                                         if (output)
895                                                 {
896                                                 result = append_quoted(result, editor->file, single_quotes, double_quotes);
897                                                 }
898                                         break;
899                                 case '%':
900                                         /* %% = % escaping */
901                                         if (output) result = g_string_append_c(result, *p);
902                                         break;
903                                 case 'd':
904                                 case 'D':
905                                 case 'n':
906                                 case 'N':
907                                 case 'v':
908                                 case 'm':
909                                         /* deprecated according to spec, ignore */
910                                         break;
911                                 default:
912                                         flags |= EDITOR_ERROR_SYNTAX;
913                                         goto err;
914                                 }
915                         }
916                 else
917                         {
918                         if (output) result = g_string_append_c(result, *p);
919                         }
920                 p++;
921                 }
922
923         if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags |= EDITOR_NO_PARAM;
924
925         if (output)
926                 {
927                 *output = g_string_free(result, FALSE);
928                 DEBUG_3("Editor cmd: %s", *output);
929                 }
930
931         return flags;
932
933
934 err:
935         if (output)
936                 {
937                 g_string_free(result, TRUE);
938                 *output = NULL;
939                 }
940         return flags;
941 }
942
943
944 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
945 {
946         EditorData *ed = data;
947         g_spawn_close_pid(pid);
948         ed->pid = -1;
949
950         editor_command_next_finish(ed, status);
951 }
952
953
954 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
955 {
956         gchar *command;
957         FileData *fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : list->data;;
958         GPid pid;
959         gint standard_output;
960         gint standard_error;
961         gboolean ok;
962
963         ed->pid = -1;
964         ed->flags = editor->flags;
965         ed->flags |= editor_command_parse(editor, list, TRUE, &command);
966
967         ok = !EDITOR_ERRORS(ed->flags);
968
969         if (ok)
970                 {
971                 ok = (options->shell.path && *options->shell.path);
972                 if (!ok) log_printf("ERROR: empty shell command\n");
973                         
974                 if (ok)
975                         {
976                         ok = (access(options->shell.path, X_OK) == 0);
977                         if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
978                         }
979
980                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
981                 }
982
983         if (ok)
984                 {
985                 gchar *working_directory;
986                 gchar *args[4];
987                 guint n = 0;
988
989                 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
990                 args[n++] = options->shell.path;
991                 if (options->shell.options && *options->shell.options)
992                         args[n++] = options->shell.options;
993                 args[n++] = command;
994                 args[n] = NULL;
995
996                 if ((ed->flags & EDITOR_DEST) && fd->change && fd->change->dest) /* FIXME: error handling */
997                         {
998                         g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
999                         }
1000                 else
1001                         {
1002                         g_unsetenv("GEEQIE_DESTINATION");
1003                         }
1004
1005                 ok = g_spawn_async_with_pipes(working_directory, args, NULL,
1006                                       G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1007                                       NULL, NULL,
1008                                       &pid,
1009                                       NULL,
1010                                       ed->vd ? &standard_output : NULL,
1011                                       ed->vd ? &standard_error : NULL,
1012                                       NULL);
1013                 
1014                 g_free(working_directory);
1015
1016                 if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
1017                 }
1018
1019         if (ok)
1020                 {
1021                 g_child_watch_add(pid, editor_child_exit_cb, ed);
1022                 ed->pid = pid;
1023                 }
1024
1025         if (ed->vd)
1026                 {
1027                 if (!ok)
1028                         {
1029                         gchar *buf;
1030
1031                         buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1032                         editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1033                         g_free(buf);
1034
1035                         }
1036                 else
1037                         {
1038                         GIOChannel *channel_output;
1039                         GIOChannel *channel_error;
1040
1041                         channel_output = g_io_channel_unix_new(standard_output);
1042                         g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, NULL);
1043                         g_io_channel_set_encoding(channel_output, NULL, NULL);
1044
1045                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1046                                             editor_verbose_io_cb, ed, NULL);
1047                         g_io_channel_unref(channel_output);
1048
1049                         channel_error = g_io_channel_unix_new(standard_error);
1050                         g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, NULL);
1051                         g_io_channel_set_encoding(channel_error, NULL, NULL);
1052
1053                         g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
1054                                             editor_verbose_io_cb, ed, NULL);
1055                         g_io_channel_unref(channel_error);
1056                         }
1057                 }
1058
1059         g_free(command);
1060
1061         return EDITOR_ERRORS(ed->flags);
1062 }
1063
1064 static EditorFlags editor_command_next_start(EditorData *ed)
1065 {
1066         if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1067
1068         if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1069                 {
1070                 FileData *fd;
1071                 EditorFlags error;
1072
1073                 fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : ed->list->data;
1074
1075                 if (ed->vd)
1076                         {
1077                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1078                                 editor_verbose_window_progress(ed, fd->path);
1079                         else
1080                                 editor_verbose_window_progress(ed, _("running..."));
1081                         }
1082                 ed->count++;
1083
1084                 error = editor_command_one(ed->editor, ed->list, ed);
1085                 if (!error && ed->vd)
1086                         {
1087                         gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != NULL) );
1088                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1089                                 {
1090                                 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1091                                 editor_verbose_window_fill(ed->vd, "\n", 1);
1092                                 }
1093                         }
1094
1095                 if (!error)
1096                         return 0;
1097                 
1098                 /* command was not started, call the finish immediately */
1099                 return editor_command_next_finish(ed, 0);
1100                 }
1101
1102         /* everything is done */
1103         return editor_command_done(ed);
1104 }
1105
1106 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1107 {
1108         gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1109
1110         if (status)
1111                 ed->flags |= EDITOR_ERROR_STATUS;
1112
1113         if (ed->flags & EDITOR_FOR_EACH)
1114                 {
1115                 /* handle the first element from the list */
1116                 GList *fd_element = ed->list;
1117
1118                 ed->list = g_list_remove_link(ed->list, fd_element);
1119                 if (ed->callback)
1120                         {
1121                         cont = ed->callback(ed->list ? ed : NULL, ed->flags, fd_element, ed->data);
1122                         if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1123                         }
1124                 filelist_free(fd_element);
1125                 }
1126         else
1127                 {
1128                 /* handle whole list */
1129                 if (ed->callback)
1130                         cont = ed->callback(NULL, ed->flags, ed->list, ed->data);
1131                 filelist_free(ed->list);
1132                 ed->list = NULL;
1133                 }
1134
1135         switch (cont)
1136                 {
1137                 case EDITOR_CB_SUSPEND:
1138                         return EDITOR_ERRORS(ed->flags);
1139                 case EDITOR_CB_SKIP:
1140                         return editor_command_done(ed);
1141                 }
1142         
1143         return editor_command_next_start(ed);
1144 }
1145
1146 static EditorFlags editor_command_done(EditorData *ed)
1147 {
1148         EditorFlags flags;
1149
1150         if (ed->vd)
1151                 {
1152                 if (ed->count == ed->total)
1153                         {
1154                         editor_verbose_window_progress(ed, _("done"));
1155                         }
1156                 else
1157                         {
1158                         editor_verbose_window_progress(ed, _("stopped by user"));
1159                         }
1160                 editor_verbose_window_enable_close(ed->vd);
1161                 }
1162
1163         /* free the not-handled items */
1164         if (ed->list)
1165                 {
1166                 ed->flags |= EDITOR_ERROR_SKIPPED;
1167                 if (ed->callback) ed->callback(NULL, ed->flags, ed->list, ed->data);
1168                 filelist_free(ed->list);
1169                 ed->list = NULL;
1170                 }
1171
1172         ed->count = 0;
1173
1174         flags = EDITOR_ERRORS(ed->flags);
1175
1176         if (!ed->vd) editor_data_free(ed);
1177
1178         return flags;
1179 }
1180
1181 void editor_resume(gpointer ed)
1182 {
1183         editor_command_next_start(ed);
1184 }
1185
1186 void editor_skip(gpointer ed)
1187 {
1188         editor_command_done(ed);
1189 }
1190
1191 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1192 {
1193         EditorData *ed;
1194         EditorFlags flags = editor->flags;
1195
1196         if (EDITOR_ERRORS(flags)) return EDITOR_ERRORS(flags);
1197
1198         ed = g_new0(EditorData, 1);
1199         ed->list = filelist_copy(list);
1200         ed->flags = flags;
1201         ed->editor = editor;
1202         ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1203         ed->callback = cb;
1204         ed->data = data;
1205         ed->working_directory = g_strdup(working_directory);
1206
1207         if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1208                 flags |= EDITOR_VERBOSE;
1209
1210         if (flags & EDITOR_VERBOSE)
1211                 editor_verbose_window(ed, text);
1212
1213         editor_command_next_start(ed);
1214         /* errors from editor_command_next_start will be handled via callback */
1215         return EDITOR_ERRORS(flags);
1216 }
1217
1218 gboolean is_valid_editor_command(const gchar *key)
1219 {
1220         if (!key) return FALSE;
1221         return g_hash_table_lookup(editors, key) != NULL;
1222 }
1223
1224 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1225 {
1226         EditorFlags error;
1227         EditorDescription *editor;
1228         if (!key) return EDITOR_ERROR_EMPTY;
1229         
1230         editor = g_hash_table_lookup(editors, key);
1231
1232         if (!editor) return EDITOR_ERROR_EMPTY;
1233         if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1234
1235         error = editor_command_parse(editor, list, TRUE, NULL);
1236
1237         if (EDITOR_ERRORS(error)) return error;
1238
1239         error |= editor_command_start(editor, editor->name, list, working_directory, cb, data);
1240
1241         if (EDITOR_ERRORS(error))
1242                 {
1243                 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1244                 
1245                 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL);
1246                 g_free(text);
1247                 }
1248
1249         return EDITOR_ERRORS(error);
1250 }
1251
1252 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1253 {
1254         return start_editor_from_filelist_full(key, list, NULL, NULL, NULL);
1255 }
1256
1257 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1258 {
1259         GList *list;
1260         EditorFlags error;
1261
1262         if (!fd) return FALSE;
1263
1264         list = g_list_append(NULL, fd);
1265         error = start_editor_from_filelist_full(key, list, NULL, cb, data);
1266         g_list_free(list);
1267         return error;
1268 }
1269
1270 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1271 {
1272         return start_editor_from_file_full(key, fd, NULL, NULL);
1273 }
1274
1275 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1276 {
1277         return start_editor_from_filelist_full(key, NULL, working_directory, NULL, NULL);
1278 }
1279
1280 gboolean editor_window_flag_set(const gchar *key)
1281 {
1282         EditorDescription *editor;
1283         if (!key) return TRUE;
1284         
1285         editor = g_hash_table_lookup(editors, key);
1286         if (!editor) return TRUE;
1287
1288         return !!(editor->flags & EDITOR_KEEP_FS);
1289 }
1290
1291 gboolean editor_is_filter(const gchar *key)
1292 {
1293         EditorDescription *editor;
1294         if (!key) return TRUE;
1295         
1296         editor = g_hash_table_lookup(editors, key);
1297         if (!editor) return TRUE;
1298
1299         return !!(editor->flags & EDITOR_DEST);
1300 }
1301
1302 gboolean editor_no_param(const gchar *key)
1303 {
1304         EditorDescription *editor;
1305         if (!key) return FALSE;
1306         
1307         editor = g_hash_table_lookup(editors, key);
1308         if (!editor) return FALSE;
1309
1310         return !!(editor->flags & EDITOR_NO_PARAM);
1311 }
1312
1313 gboolean editor_blocks_file(const gchar *key)
1314 {
1315         EditorDescription *editor;
1316         if (!key) return FALSE;
1317         
1318         editor = g_hash_table_lookup(editors, key);
1319         if (!editor) return FALSE;
1320
1321         /* Decide if the image file should be blocked during editor execution
1322            Editors like gimp can be used long time after the original file was
1323            saved, for editing unrelated files.
1324            %f vs. %F seems to be a good heuristic to detect this kind of editors.
1325         */
1326            
1327         return !(editor->flags & EDITOR_SINGLE_COMMAND);
1328 }
1329
1330 const gchar *editor_get_error_str(EditorFlags flags)
1331 {
1332         if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1333         if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1334         if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1335         if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1336         if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1337         if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1338         if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1339         return _("Unknown error.");
1340 }
1341
1342 const gchar *editor_get_name(const gchar *key)
1343 {
1344         EditorDescription *editor = g_hash_table_lookup(editors, key);
1345
1346         if (!editor) return NULL;
1347
1348         return editor->name;
1349 }
1350 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */