807d16ebdbc0fac4c68507eda11e2bc5e684b51f
[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, gpointer value, gpointer)
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, 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 *, 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         vd->button_stop = generic_dialog_add_button(vd->gd, GQ_ICON_STOP, nullptr,
592                                                    editor_verbose_window_stop, FALSE);
593         gtk_widget_set_sensitive(vd->button_stop, FALSE);
594         vd->button_close = generic_dialog_add_button(vd->gd, GQ_ICON_CLOSE, _("Close"),
595                                                     editor_verbose_window_close, TRUE);
596         gtk_widget_set_sensitive(vd->button_close, FALSE);
597
598         scrolled = gtk_scrolled_window_new(nullptr, nullptr);
599         gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
600         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
601                                        GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
602         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
603         gtk_widget_show(scrolled);
604
605         vd->text = gtk_text_view_new();
606         gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
607         gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
608         gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
609         gtk_widget_show(vd->text);
610
611         hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
612         gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
613         gtk_widget_show(hbox);
614
615         vd->progress = gtk_progress_bar_new();
616         gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
617         gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
618         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
619         gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
620         gtk_widget_show(vd->progress);
621
622         vd->spinner = spinner_new(nullptr, SPINNER_SPEED);
623         gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
624         gtk_widget_show(vd->spinner);
625
626         gtk_widget_show(vd->gd->dialog);
627
628         ed->vd = vd;
629         return vd;
630 }
631
632 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
633 {
634         GtkTextBuffer *buffer;
635         GtkTextIter iter;
636
637         buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
638         gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
639         gtk_text_buffer_insert(buffer, &iter, text, len);
640 }
641
642 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
643 {
644         if (!ed->vd) return;
645
646         if (ed->total)
647                 {
648                 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
649                 }
650
651         gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
652 }
653
654 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
655 {
656         auto ed = static_cast<EditorData *>(data);
657         gchar buf[512];
658         gsize count;
659
660         if (condition & G_IO_IN)
661                 {
662                 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
663                         {
664                         if (!g_utf8_validate(buf, count, nullptr))
665                                 {
666                                 gchar *utf8;
667
668                                 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
669                                 if (utf8)
670                                         {
671                                         editor_verbose_window_fill(ed->vd, utf8, -1);
672                                         g_free(utf8);
673                                         }
674                                 else
675                                         {
676                                         editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
677                                         }
678                                 }
679                         else
680                                 {
681                                 editor_verbose_window_fill(ed->vd, buf, count);
682                                 }
683                         }
684                 }
685
686         if (condition & (G_IO_ERR | G_IO_HUP))
687                 {
688                 g_io_channel_shutdown(source, TRUE, nullptr);
689                 return FALSE;
690                 }
691
692         return TRUE;
693 }
694
695 enum PathType {
696         PATH_FILE,
697         PATH_FILE_URL,
698         PATH_DEST
699 };
700
701
702 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
703 {
704         gchar *pathl;
705         const gchar *p = nullptr;
706
707         DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
708
709         if (type == PATH_FILE || type == PATH_FILE_URL)
710                 {
711                 GList *work = editor->ext_list;
712
713                 if (!work)
714                         p = fd->path;
715                 else
716                         {
717                         while (work)
718                                 {
719                                 GList *work2;
720                                 auto ext = static_cast<gchar *>(work->data);
721                                 work = work->next;
722
723                                 if (strcmp(ext, "*") == 0 ||
724                                     g_ascii_strcasecmp(ext, fd->extension) == 0)
725                                         {
726                                         p = fd->path;
727                                         break;
728                                         }
729
730                                 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
731                                 while (work2)
732                                         {
733                                         auto sfd = static_cast<FileData *>(work2->data);
734                                         work2 = work2->next;
735
736                                         if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
737                                                 {
738                                                 p = sfd->path;
739                                                 break;
740                                                 }
741                                         }
742                                 if (p) break;
743                                 }
744                         if (!p) return nullptr;
745                         }
746                 }
747         else if (type == PATH_DEST)
748                 {
749                 if (fd->change && fd->change->dest)
750                         p = fd->change->dest;
751                 else
752                         p = "";
753                 }
754
755         g_assert(p);
756
757         GString *string = g_string_new(p);
758         if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
759         pathl = path_from_utf8(string->str);
760         g_string_free(string, TRUE);
761
762         if (pathl && !pathl[0]) /* empty string case */
763                 {
764                 g_free(pathl);
765                 pathl = nullptr;
766                 }
767
768         DEBUG_2("editor_command_path_parse: return %s", pathl);
769         return pathl;
770 }
771
772 struct CommandBuilder
773 {
774         ~CommandBuilder()
775         {
776                 if (!str) return;
777
778                 g_string_free(str, TRUE);
779         }
780
781         void init()
782         {
783                 if (str) return;
784
785                 str = g_string_new("");
786         }
787
788         void append(const gchar *val)
789         {
790                 if (!str) return;
791
792                 str = g_string_append(str, val);
793         }
794
795         void append_c(gchar c)
796         {
797                 if (!str) return;
798
799                 str = g_string_append_c(str, c);
800         }
801
802         void append_quoted(const char *s, gboolean single_quotes, gboolean double_quotes)
803         {
804                 if (!str) return;
805
806                 if (!single_quotes)
807                         {
808                         if (!double_quotes)
809                                 str = g_string_append_c(str, '\'');
810                         else
811                                 str = g_string_append(str, "\"'");
812                         }
813
814                 for (const char *p = s; *p != '\0'; p++)
815                         {
816                         if (*p == '\'')
817                                 str = g_string_append(str, "'\\''");
818                         else
819                                 str = g_string_append_c(str, *p);
820                         }
821
822                 if (!single_quotes)
823                         {
824                         if (!double_quotes)
825                                 str = g_string_append_c(str, '\'');
826                         else
827                                 str = g_string_append(str, "'\"");
828                         }
829         }
830
831         gchar *get_command()
832         {
833                 if (!str) return nullptr;
834
835                 auto command = g_string_free(str, FALSE);
836                 str = nullptr;
837                 return command;
838         }
839
840 private:
841         GString *str{nullptr};
842 };
843
844
845 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
846 {
847         auto flags = static_cast<EditorFlags>(0);
848         const gchar *p;
849         CommandBuilder result;
850         gboolean escape = FALSE;
851         gboolean single_quotes = FALSE;
852         gboolean double_quotes = FALSE;
853
854         DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
855
856         if (output)
857                 {
858                 *output = nullptr;
859                 result.init();
860                 }
861
862         if (editor->exec == nullptr || editor->exec[0] == '\0')
863                 {
864                 return static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
865                 }
866
867         p = editor->exec;
868         /* skip leading whitespaces if any */
869         while (g_ascii_isspace(*p)) p++;
870
871         /* command */
872
873         while (*p)
874                 {
875                 if (escape)
876                         {
877                         escape = FALSE;
878                         result.append_c(*p);
879                         }
880                 else if (*p == '\\')
881                         {
882                         if (!single_quotes) escape = TRUE;
883                         result.append_c(*p);
884                         }
885                 else if (*p == '\'')
886                         {
887                         result.append_c(*p);
888                         if (!single_quotes && !double_quotes)
889                                 single_quotes = TRUE;
890                         else if (single_quotes)
891                                 single_quotes = FALSE;
892                         }
893                 else if (*p == '"')
894                         {
895                         result.append_c(*p);
896                         if (!single_quotes && !double_quotes)
897                                 double_quotes = TRUE;
898                         else if (double_quotes)
899                                 double_quotes = FALSE;
900                         }
901                 else if (*p == '%' && p[1])
902                         {
903                         gchar *pathl = nullptr;
904
905                         p++;
906
907                         switch (*p)
908                                 {
909                                 case 'f': /* single file */
910                                 case 'u': /* single url */
911                                         flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
912                                         if (flags & EDITOR_SINGLE_COMMAND)
913                                                 {
914                                                 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
915                                                 }
916                                         if (list)
917                                                 {
918                                                 /* use the first file from the list */
919                                                 if (!list->data)
920                                                         {
921                                                         return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
922                                                         }
923                                                 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
924                                                                                   consider_sidecars,
925                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
926                                                                                   editor);
927                                                 if (!output)
928                                                         {
929                                                         /* just testing, check also the rest of the list (like with F and U)
930                                                            any matching file is OK */
931                                                         GList *work = list->next;
932
933                                                         while (!pathl && work)
934                                                                 {
935                                                                 auto fd = static_cast<FileData *>(work->data);
936                                                                 pathl = editor_command_path_parse(fd,
937                                                                                                   consider_sidecars,
938                                                                                                   (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
939                                                                                                   editor);
940                                                                 work = work->next;
941                                                                 }
942                                                         }
943
944                                                 if (!pathl)
945                                                         {
946                                                         return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
947                                                         }
948                                                 result.append_quoted(pathl, single_quotes, double_quotes);
949                                                 g_free(pathl);
950                                                 }
951                                         break;
952
953                                 case 'F':
954                                 case 'U':
955                                         flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
956                                         if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
957                                                 {
958                                                 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
959                                                 }
960
961                                         if (list)
962                                                 {
963                                                 /* use whole list */
964                                                 GList *work = list;
965                                                 gboolean ok = FALSE;
966
967                                                 while (work)
968                                                         {
969                                                         auto fd = static_cast<FileData *>(work->data);
970                                                         pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
971                                                         if (pathl)
972                                                                 {
973                                                                 ok = TRUE;
974
975                                                                 if (work != list)
976                                                                         {
977                                                                         result.append_c(' ');
978                                                                         }
979                                                                 result.append_quoted(pathl, single_quotes, double_quotes);
980                                                                 g_free(pathl);
981                                                                 }
982                                                         work = work->next;
983                                                         }
984                                                 if (!ok)
985                                                         {
986                                                         return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
987                                                         }
988                                                 }
989                                         break;
990                                 case 'i':
991                                         if (editor->icon && *editor->icon)
992                                                 {
993                                                 result.append("--icon ");
994                                                 result.append_quoted(editor->icon, single_quotes, double_quotes);
995                                                 }
996                                         break;
997                                 case 'c':
998                                         result.append_quoted(editor->name, single_quotes, double_quotes);
999                                         break;
1000                                 case 'k':
1001                                         result.append_quoted(editor->file, single_quotes, double_quotes);
1002                                         break;
1003                                 case '%':
1004                                         /* %% = % escaping */
1005                                         result.append_c(*p);
1006                                         break;
1007                                 case 'd':
1008                                 case 'D':
1009                                 case 'n':
1010                                 case 'N':
1011                                 case 'v':
1012                                 case 'm':
1013                                         /* deprecated according to spec, ignore */
1014                                         break;
1015                                 default:
1016                                         return static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1017                                 }
1018                         }
1019                 else
1020                         {
1021                         result.append_c(*p);
1022                         }
1023                 p++;
1024                 }
1025
1026         if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1027
1028         if (output)
1029                 {
1030                 *output = result.get_command();
1031                 DEBUG_3("Editor cmd: %s", *output);
1032                 }
1033
1034         return flags;
1035 }
1036
1037
1038 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1039 {
1040         auto ed = static_cast<EditorData *>(data);
1041         g_spawn_close_pid(pid);
1042         ed->pid = -1;
1043
1044         editor_command_next_finish(ed, status);
1045 }
1046
1047
1048 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1049 {
1050         gchar *command;
1051         auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1052         GPid pid;
1053         gint standard_output;
1054         gint standard_error;
1055         gboolean ok;
1056
1057         ed->pid = -1;
1058         ed->flags = editor->flags;
1059         ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1060
1061         ok = !EDITOR_ERRORS(ed->flags);
1062
1063         if (ok)
1064                 {
1065                 ok = (options->shell.path && *options->shell.path);
1066                 if (!ok) log_printf("ERROR: empty shell command\n");
1067
1068                 if (ok)
1069                         {
1070                         ok = (access(options->shell.path, X_OK) == 0);
1071                         if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1072                         }
1073
1074                 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1075                 }
1076
1077         if (ok)
1078                 {
1079                 gchar *working_directory;
1080                 gchar *args[4];
1081                 guint n = 0;
1082
1083                 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1084                 args[n++] = options->shell.path;
1085                 if (options->shell.options && *options->shell.options)
1086                         args[n++] = options->shell.options;
1087                 args[n++] = command;
1088                 args[n] = nullptr;
1089
1090                 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1091                         {
1092                         g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1093                         }
1094                 else
1095                         {
1096                         g_unsetenv("GEEQIE_DESTINATION");
1097                         }
1098
1099                 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1100                                       G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1101                                       nullptr, nullptr,
1102                                       &pid,
1103                                       nullptr,
1104                                       ed->vd ? &standard_output : nullptr,
1105                                       ed->vd ? &standard_error : nullptr,
1106                                       nullptr);
1107
1108                 g_free(working_directory);
1109
1110                 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1111                 }
1112
1113         if (ok)
1114                 {
1115                 g_child_watch_add(pid, editor_child_exit_cb, ed);
1116                 ed->pid = pid;
1117                 }
1118
1119         if (ed->vd)
1120                 {
1121                 if (!ok)
1122                         {
1123                         gchar *buf;
1124
1125                         buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1126                         editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1127                         g_free(buf);
1128
1129                         }
1130                 else
1131                         {
1132                         GIOChannel *channel_output;
1133                         GIOChannel *channel_error;
1134
1135                         channel_output = g_io_channel_unix_new(standard_output);
1136                         g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1137                         g_io_channel_set_encoding(channel_output, nullptr, nullptr);
1138
1139                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1140                                             editor_verbose_io_cb, ed, nullptr);
1141                         g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1142                                             editor_verbose_io_cb, ed, nullptr);
1143                         g_io_channel_unref(channel_output);
1144
1145                         channel_error = g_io_channel_unix_new(standard_error);
1146                         g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1147                         g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1148
1149                         g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1150                                             editor_verbose_io_cb, ed, nullptr);
1151                         g_io_channel_unref(channel_error);
1152                         }
1153                 }
1154
1155         g_free(command);
1156
1157         return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1158 }
1159
1160 static EditorFlags editor_command_next_start(EditorData *ed)
1161 {
1162         if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1163
1164         if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1165                 {
1166                 FileData *fd;
1167                 EditorFlags error;
1168
1169                 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1170
1171                 if (ed->vd)
1172                         {
1173                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1174                                 editor_verbose_window_progress(ed, fd->path);
1175                         else
1176                                 editor_verbose_window_progress(ed, _("running..."));
1177                         }
1178                 ed->count++;
1179
1180                 error = editor_command_one(ed->editor, ed->list, ed);
1181                 if (!error && ed->vd)
1182                         {
1183                         gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1184                         if ((ed->flags & EDITOR_FOR_EACH) && fd)
1185                                 {
1186                                 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1187                                 editor_verbose_window_fill(ed->vd, "\n", 1);
1188                                 }
1189                         }
1190
1191                 if (!error)
1192                         return static_cast<EditorFlags>(0);
1193
1194                 /* command was not started, call the finish immediately */
1195                 return editor_command_next_finish(ed, 0);
1196                 }
1197
1198         /* everything is done */
1199         return editor_command_done(ed);
1200 }
1201
1202 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1203 {
1204         gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1205
1206         if (status)
1207                 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1208
1209         if (ed->flags & EDITOR_FOR_EACH)
1210                 {
1211                 /* handle the first element from the list */
1212                 GList *fd_element = ed->list;
1213
1214                 ed->list = g_list_remove_link(ed->list, fd_element);
1215                 if (ed->callback)
1216                         {
1217                         cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1218                         if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1219                         }
1220                 filelist_free(fd_element);
1221                 }
1222         else
1223                 {
1224                 /* handle whole list */
1225                 if (ed->callback)
1226                         cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1227                 filelist_free(ed->list);
1228                 ed->list = nullptr;
1229                 }
1230
1231         switch (cont)
1232                 {
1233                 case EDITOR_CB_SUSPEND:
1234                         return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1235                 case EDITOR_CB_SKIP:
1236                         return editor_command_done(ed);
1237                 }
1238
1239         return editor_command_next_start(ed);
1240 }
1241
1242 static EditorFlags editor_command_done(EditorData *ed)
1243 {
1244         EditorFlags flags;
1245
1246         if (ed->vd)
1247                 {
1248                 if (ed->count == ed->total)
1249                         {
1250                         editor_verbose_window_progress(ed, _("done"));
1251                         }
1252                 else
1253                         {
1254                         editor_verbose_window_progress(ed, _("stopped by user"));
1255                         }
1256                 editor_verbose_window_enable_close(ed->vd);
1257                 }
1258
1259         /* free the not-handled items */
1260         if (ed->list)
1261                 {
1262                 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1263                 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1264                 filelist_free(ed->list);
1265                 ed->list = nullptr;
1266                 }
1267
1268         ed->count = 0;
1269
1270         flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1271
1272         if (!ed->vd) editor_data_free(ed);
1273
1274         return flags;
1275 }
1276
1277 void editor_resume(gpointer ed)
1278 {
1279         editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1280 }
1281
1282 void editor_skip(gpointer ed)
1283 {
1284         editor_command_done(static_cast<EditorData *>(ed));
1285 }
1286
1287 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1288 {
1289         EditorData *ed;
1290         EditorFlags flags = editor->flags;
1291
1292         if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1293
1294         ed = g_new0(EditorData, 1);
1295         ed->list = filelist_copy(list);
1296         ed->flags = flags;
1297         ed->editor = editor;
1298         ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1299         ed->callback = cb;
1300         ed->data = data;
1301         ed->working_directory = g_strdup(working_directory);
1302
1303         if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1304                 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1305
1306         if (flags & EDITOR_VERBOSE)
1307                 editor_verbose_window(ed, text);
1308
1309         editor_command_next_start(ed);
1310         /* errors from editor_command_next_start will be handled via callback */
1311         return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1312 }
1313
1314 gboolean is_valid_editor_command(const gchar *key)
1315 {
1316         if (!key) return FALSE;
1317         return g_hash_table_lookup(editors, key) != nullptr;
1318 }
1319
1320 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1321 {
1322         EditorFlags error;
1323         EditorDescription *editor;
1324         if (!key) return EDITOR_ERROR_EMPTY;
1325
1326         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1327
1328         if (!editor) return EDITOR_ERROR_EMPTY;
1329         if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1330
1331         error = editor_command_parse(editor, list, TRUE, nullptr);
1332
1333         if (EDITOR_ERRORS(error)) return error;
1334
1335         error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1336
1337         if (EDITOR_ERRORS(error))
1338                 {
1339                 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1340
1341                 file_util_warning_dialog(_("Invalid editor command"), text, GQ_ICON_DIALOG_ERROR, nullptr);
1342                 g_free(text);
1343                 }
1344
1345         return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1346 }
1347
1348 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1349 {
1350         return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1351 }
1352
1353 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1354 {
1355         GList *list;
1356         EditorFlags error;
1357
1358         if (!fd) return static_cast<EditorFlags>(FALSE);
1359
1360         list = g_list_append(nullptr, fd);
1361         error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1362         g_list_free(list);
1363         return error;
1364 }
1365
1366 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1367 {
1368         return start_editor_from_file_full(key, fd, nullptr, nullptr);
1369 }
1370
1371 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1372 {
1373         return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1374 }
1375
1376 gboolean editor_window_flag_set(const gchar *key)
1377 {
1378         EditorDescription *editor;
1379         if (!key) return TRUE;
1380
1381         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1382         if (!editor) return TRUE;
1383
1384         return !!(editor->flags & EDITOR_KEEP_FS);
1385 }
1386
1387 gboolean editor_is_filter(const gchar *key)
1388 {
1389         EditorDescription *editor;
1390         if (!key) return TRUE;
1391
1392         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1393         if (!editor) return TRUE;
1394
1395         return !!(editor->flags & EDITOR_DEST);
1396 }
1397
1398 gboolean editor_no_param(const gchar *key)
1399 {
1400         EditorDescription *editor;
1401         if (!key) return FALSE;
1402
1403         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1404         if (!editor) return FALSE;
1405
1406         return !!(editor->flags & EDITOR_NO_PARAM);
1407 }
1408
1409 gboolean editor_blocks_file(const gchar *key)
1410 {
1411         EditorDescription *editor;
1412         if (!key) return FALSE;
1413
1414         editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1415         if (!editor) return FALSE;
1416
1417         /* Decide if the image file should be blocked during editor execution
1418            Editors like gimp can be used long time after the original file was
1419            saved, for editing unrelated files.
1420            %f vs. %F seems to be a good heuristic to detect this kind of editors.
1421         */
1422
1423         return !(editor->flags & EDITOR_SINGLE_COMMAND);
1424 }
1425
1426 const gchar *editor_get_error_str(EditorFlags flags)
1427 {
1428         if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1429         if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1430         if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1431         if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1432         if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1433         if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1434         if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1435         return _("Unknown error.");
1436 }
1437
1438 #pragma GCC diagnostic push
1439 #pragma GCC diagnostic ignored "-Wunused-function"
1440 const gchar *editor_get_name_unused(const gchar *key)
1441 {
1442         EditorDescription *editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1443
1444         if (!editor) return NULL;
1445
1446         return editor->name;
1447 }
1448 #pragma GCC diagnostic pop
1449
1450 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */