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