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