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