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