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