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