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