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