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