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