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