2 * Copyright (C) 2006 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
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.
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.
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.
26 #include "filefilter.h"
27 #include "pixbuf-util.h"
28 #include "ui-fileops.h"
29 #include "ui-spinner.h"
32 #define EDITOR_WINDOW_WIDTH 500
33 #define EDITOR_WINDOW_HEIGHT 300
37 struct EditorVerboseData {
39 GtkWidget *button_close;
40 GtkWidget *button_stop;
53 EditorVerboseData *vd;
54 EditorCallback callback;
56 const EditorDescription *editor;
57 gchar *working_directory; /* fallback if no files are given (editor_no_param) */
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);
67 *-----------------------------------------------------------------------------
68 * external editor routines
69 *-----------------------------------------------------------------------------
72 GHashTable *editors = nullptr;
73 GtkListStore *desktop_file_list;
74 gboolean editors_finished = FALSE;
76 #ifdef G_KEY_FILE_DESKTOP_GROUP
77 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
79 #define DESKTOP_GROUP "Desktop Entry"
82 void editor_description_free(EditorDescription *editor)
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);
98 static GList *editor_mime_types_to_extensions(gchar **mime_types)
100 /** @FIXME this should be rewritten to use the shared mime database, as soon as we switch to gio */
102 static constexpr const gchar *conv_table[][2] = {
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"},
161 GList *list = nullptr;
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]));
171 gboolean editor_read_desktop_file(const gchar *path)
174 EditorDescription *editor;
177 const gchar *key = filename_from_path(path);
178 gchar **categories, **only_show_in, **not_show_in;
181 gboolean category_geeqie = FALSE;
185 if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
187 key_file = g_key_file_new();
188 if (!g_key_file_load_from_file(key_file, path, static_cast<GKeyFileFlags>(0), nullptr))
190 g_key_file_free(key_file);
194 type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", nullptr);
195 if (!type || strcmp(type, "Application") != 0)
197 /* We only consider desktop entries of Application type */
198 g_key_file_free(key_file);
204 editor = g_new0(EditorDescription, 1);
206 editor->key = g_strdup(key);
207 editor->file = g_strdup(path);
209 g_hash_table_insert(editors, editor->key, editor);
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))
214 editor->hidden = TRUE;
217 categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", nullptr, nullptr);
220 gboolean found = FALSE;
222 for (i = 0; categories[i]; i++)
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)
229 if (strcmp(categories[i], "X-Geeqie") == 0)
232 category_geeqie = TRUE;
236 if (!found) editor->ignored = TRUE;
237 g_strfreev(categories);
241 editor->ignored = TRUE;
244 only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", nullptr, nullptr);
247 gboolean found = FALSE;
249 for (i = 0; only_show_in[i]; i++)
250 if (strcmp(only_show_in[i], "X-Geeqie") == 0)
255 if (!found) editor->ignored = TRUE;
256 g_strfreev(only_show_in);
259 not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", nullptr, nullptr);
262 gboolean found = FALSE;
264 for (i = 0; not_show_in[i]; i++)
265 if (strcmp(not_show_in[i], "X-Geeqie") == 0)
270 if (found) editor->ignored = TRUE;
271 g_strfreev(not_show_in);
275 try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", nullptr);
276 if (try_exec && !editor->hidden && !editor->ignored)
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);
286 /* ignored editors will be deleted, no need to parse the rest */
287 g_key_file_free(key_file);
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);
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))
297 gchar *ext = strrchr(editor->icon, '.');
299 if (ext && strlen(ext) == 4 &&
300 (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
302 log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
303 editor->file, editor->icon);
309 if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
311 g_free(editor->icon);
312 editor->icon = nullptr;
315 editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", nullptr);
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");
320 editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", nullptr);
322 editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", nullptr);
324 extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", nullptr);
326 editor->ext_list = filter_to_list(extensions);
329 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", nullptr, nullptr);
332 editor->ext_list = editor_mime_types_to_extensions(mime_types);
333 g_strfreev(mime_types);
334 if (!editor->ext_list) editor->hidden = TRUE;
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);
344 editor->flags = static_cast<EditorFlags>(editor->flags | editor_command_parse(editor, nullptr, FALSE, nullptr));
346 if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
348 g_key_file_free(key_file);
350 if (editor->ignored) return TRUE;
352 work = options->disabled_plugins;
357 if (g_strcmp0(path, static_cast<const gchar *>(work->data)) == 0)
365 editor->disabled = disabled;
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);
379 static gboolean editor_remove_desktop_file_cb(gpointer, gpointer value, gpointer)
381 auto editor = static_cast<EditorDescription *>(value);
382 return editor->hidden || editor->ignored;
385 void editor_table_finish()
387 g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, nullptr);
388 editors_finished = TRUE;
391 void editor_table_clear()
393 if (desktop_file_list)
395 gtk_list_store_clear(desktop_file_list);
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);
403 g_hash_table_destroy(editors);
405 editors = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(editor_description_free));
406 editors_finished = FALSE;
409 static GList *editor_add_desktop_dir(GList *list, const gchar *path)
415 pathl = path_from_utf8(path);
423 while ((dir = readdir(dp)) != nullptr)
425 gchar *namel = dir->d_name;
427 if (g_str_has_suffix(namel, ".desktop"))
429 gchar *name = path_to_utf8(namel);
430 gchar *dpath = g_build_filename(path, name, NULL);
431 list = g_list_prepend(list, dpath);
439 GList *editor_get_desktop_files()
442 gchar *xdg_data_dirs;
446 GList *list = nullptr;
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);
452 xdg_data_dirs = g_strdup("/usr/share");
454 all_dirs = g_strconcat(get_rc_dir(), ":", gq_appdir, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
456 g_free(xdg_data_dirs);
458 split_dirs = g_strsplit(all_dirs, ":", 0);
462 for (i = 0; split_dirs[i]; i++);
463 for (--i; i >= 0; i--)
465 path = g_build_filename(split_dirs[i], "applications", NULL);
466 list = editor_add_desktop_dir(list, path);
470 g_strfreev(split_dirs);
474 static void editor_list_add_cb(gpointer, gpointer value, gpointer data)
476 auto listp = static_cast<GList **>(data);
477 auto editor = static_cast<EditorDescription *>(value);
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;
486 if (editor->disabled)
491 *listp = g_list_prepend(*listp, editor);
494 static gint editor_sort(gconstpointer a, gconstpointer b)
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;
504 ret = strcmp(ea->menu_path, eb->menu_path);
505 if (ret != 0) return ret;
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);
513 g_free(collate_key_ea);
514 g_free(collate_key_eb);
515 g_free(caseless_name_ea);
516 g_free(caseless_name_eb);
521 GList *editor_list_get()
523 GList *editors_list = nullptr;
525 if (!editors_finished) return nullptr;
527 g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
528 editors_list = g_list_sort(editors_list, editor_sort);
533 /* ------------------------------ */
536 static void editor_verbose_data_free(EditorData *ed)
543 static void editor_data_free(EditorData *ed)
545 editor_verbose_data_free(ed);
546 g_free(ed->working_directory);
550 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
552 auto ed = static_cast<EditorData *>(data);
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 */
559 static void editor_verbose_window_stop(GenericDialog *, gpointer data)
561 auto ed = static_cast<EditorData *>(data);
564 editor_verbose_window_progress(ed, _("stopping..."));
567 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
569 vd->gd->cancel_cb = editor_verbose_window_close;
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);
576 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
578 EditorVerboseData *vd;
583 vd = g_new0(EditorVerboseData, 1);
585 vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
588 buf = g_strdup_printf(_("Output of %s"), text);
589 generic_dialog_add_message(vd->gd, nullptr, buf, nullptr, FALSE);
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);
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);
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);
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);
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);
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);
626 gtk_widget_show(vd->gd->dialog);
632 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
634 GtkTextBuffer *buffer;
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);
642 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
648 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
651 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
654 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
656 auto ed = static_cast<EditorData *>(data);
660 if (condition & G_IO_IN)
662 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
664 if (!g_utf8_validate(buf, count, nullptr))
668 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
671 editor_verbose_window_fill(ed->vd, utf8, -1);
676 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
681 editor_verbose_window_fill(ed->vd, buf, count);
686 if (condition & (G_IO_ERR | G_IO_HUP))
688 g_io_channel_shutdown(source, TRUE, nullptr);
702 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
705 const gchar *p = nullptr;
707 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
709 if (type == PATH_FILE || type == PATH_FILE_URL)
711 GList *work = editor->ext_list;
720 auto ext = static_cast<gchar *>(work->data);
723 if (strcmp(ext, "*") == 0 ||
724 g_ascii_strcasecmp(ext, fd->extension) == 0)
730 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
733 auto sfd = static_cast<FileData *>(work2->data);
736 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
744 if (!p) return nullptr;
747 else if (type == PATH_DEST)
749 if (fd->change && fd->change->dest)
750 p = fd->change->dest;
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);
762 if (pathl && !pathl[0]) /* empty string case */
768 DEBUG_2("editor_command_path_parse: return %s", pathl);
772 struct CommandBuilder
778 g_string_free(str, TRUE);
785 str = g_string_new("");
788 void append(const gchar *val)
792 str = g_string_append(str, val);
795 void append_c(gchar c)
799 str = g_string_append_c(str, c);
802 void append_quoted(const char *s, gboolean single_quotes, gboolean double_quotes)
809 str = g_string_append_c(str, '\'');
811 str = g_string_append(str, "\"'");
814 for (const char *p = s; *p != '\0'; p++)
817 str = g_string_append(str, "'\\''");
819 str = g_string_append_c(str, *p);
825 str = g_string_append_c(str, '\'');
827 str = g_string_append(str, "'\"");
833 if (!str) return nullptr;
835 auto command = g_string_free(str, FALSE);
841 GString *str{nullptr};
845 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
847 auto flags = static_cast<EditorFlags>(0);
849 CommandBuilder result;
850 gboolean escape = FALSE;
851 gboolean single_quotes = FALSE;
852 gboolean double_quotes = FALSE;
854 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
862 if (editor->exec == nullptr || editor->exec[0] == '\0')
864 return static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
868 /* skip leading whitespaces if any */
869 while (g_ascii_isspace(*p)) p++;
882 if (!single_quotes) escape = TRUE;
888 if (!single_quotes && !double_quotes)
889 single_quotes = TRUE;
890 else if (single_quotes)
891 single_quotes = FALSE;
896 if (!single_quotes && !double_quotes)
897 double_quotes = TRUE;
898 else if (double_quotes)
899 double_quotes = FALSE;
901 else if (*p == '%' && p[1])
903 gchar *pathl = nullptr;
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)
914 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
918 /* use the first file from the list */
921 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
923 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
925 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
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;
933 while (!pathl && work)
935 auto fd = static_cast<FileData *>(work->data);
936 pathl = editor_command_path_parse(fd,
938 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
946 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
948 result.append_quoted(pathl, single_quotes, double_quotes);
955 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
956 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
958 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
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);
977 result.append_c(' ');
979 result.append_quoted(pathl, single_quotes, double_quotes);
986 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
991 if (editor->icon && *editor->icon)
993 result.append("--icon ");
994 result.append_quoted(editor->icon, single_quotes, double_quotes);
998 result.append_quoted(editor->name, single_quotes, double_quotes);
1001 result.append_quoted(editor->file, single_quotes, double_quotes);
1004 /* %% = % escaping */
1005 result.append_c(*p);
1013 /* deprecated according to spec, ignore */
1016 return static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1021 result.append_c(*p);
1026 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1030 *output = result.get_command();
1031 DEBUG_3("Editor cmd: %s", *output);
1038 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1040 auto ed = static_cast<EditorData *>(data);
1041 g_spawn_close_pid(pid);
1044 editor_command_next_finish(ed, status);
1048 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1051 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1053 gint standard_output;
1054 gint standard_error;
1058 ed->flags = editor->flags;
1059 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1061 ok = !EDITOR_ERRORS(ed->flags);
1065 ok = (options->shell.path && *options->shell.path);
1066 if (!ok) log_printf("ERROR: empty shell command\n");
1070 ok = (access(options->shell.path, X_OK) == 0);
1071 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1074 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1079 gchar *working_directory;
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;
1090 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1092 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1096 g_unsetenv("GEEQIE_DESTINATION");
1099 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1100 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1104 ed->vd ? &standard_output : nullptr,
1105 ed->vd ? &standard_error : nullptr,
1108 g_free(working_directory);
1110 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1115 g_child_watch_add(pid, editor_child_exit_cb, ed);
1125 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1126 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1132 GIOChannel *channel_output;
1133 GIOChannel *channel_error;
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);
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);
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);
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);
1157 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1160 static EditorFlags editor_command_next_start(EditorData *ed)
1162 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1164 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1169 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1173 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1174 editor_verbose_window_progress(ed, fd->path);
1176 editor_verbose_window_progress(ed, _("running..."));
1180 error = editor_command_one(ed->editor, ed->list, ed);
1181 if (!error && ed->vd)
1183 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1184 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1186 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1187 editor_verbose_window_fill(ed->vd, "\n", 1);
1192 return static_cast<EditorFlags>(0);
1194 /* command was not started, call the finish immediately */
1195 return editor_command_next_finish(ed, 0);
1198 /* everything is done */
1199 return editor_command_done(ed);
1202 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1204 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1207 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1209 if (ed->flags & EDITOR_FOR_EACH)
1211 /* handle the first element from the list */
1212 GList *fd_element = ed->list;
1214 ed->list = g_list_remove_link(ed->list, fd_element);
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;
1220 filelist_free(fd_element);
1224 /* handle whole list */
1226 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1227 filelist_free(ed->list);
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);
1239 return editor_command_next_start(ed);
1242 static EditorFlags editor_command_done(EditorData *ed)
1248 if (ed->count == ed->total)
1250 editor_verbose_window_progress(ed, _("done"));
1254 editor_verbose_window_progress(ed, _("stopped by user"));
1256 editor_verbose_window_enable_close(ed->vd);
1259 /* free the not-handled items */
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);
1270 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1272 if (!ed->vd) editor_data_free(ed);
1277 void editor_resume(gpointer ed)
1279 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1282 void editor_skip(gpointer ed)
1284 editor_command_done(static_cast<EditorData *>(ed));
1287 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1290 EditorFlags flags = editor->flags;
1292 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1294 ed = g_new0(EditorData, 1);
1295 ed->list = filelist_copy(list);
1297 ed->editor = editor;
1298 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1301 ed->working_directory = g_strdup(working_directory);
1303 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1304 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1306 if (flags & EDITOR_VERBOSE)
1307 editor_verbose_window(ed, text);
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));
1314 gboolean is_valid_editor_command(const gchar *key)
1316 if (!key) return FALSE;
1317 return g_hash_table_lookup(editors, key) != nullptr;
1320 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1323 EditorDescription *editor;
1324 if (!key) return EDITOR_ERROR_EMPTY;
1326 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1328 if (!editor) return EDITOR_ERROR_EMPTY;
1329 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1331 error = editor_command_parse(editor, list, TRUE, nullptr);
1333 if (EDITOR_ERRORS(error)) return error;
1335 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1337 if (EDITOR_ERRORS(error))
1339 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1341 file_util_warning_dialog(_("Invalid editor command"), text, GQ_ICON_DIALOG_ERROR, nullptr);
1345 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1348 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1350 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1353 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1358 if (!fd) return static_cast<EditorFlags>(FALSE);
1360 list = g_list_append(nullptr, fd);
1361 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1366 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1368 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1371 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1373 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1376 gboolean editor_window_flag_set(const gchar *key)
1378 EditorDescription *editor;
1379 if (!key) return TRUE;
1381 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1382 if (!editor) return TRUE;
1384 return !!(editor->flags & EDITOR_KEEP_FS);
1387 gboolean editor_is_filter(const gchar *key)
1389 EditorDescription *editor;
1390 if (!key) return TRUE;
1392 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1393 if (!editor) return TRUE;
1395 return !!(editor->flags & EDITOR_DEST);
1398 gboolean editor_no_param(const gchar *key)
1400 EditorDescription *editor;
1401 if (!key) return FALSE;
1403 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1404 if (!editor) return FALSE;
1406 return !!(editor->flags & EDITOR_NO_PARAM);
1409 gboolean editor_blocks_file(const gchar *key)
1411 EditorDescription *editor;
1412 if (!key) return FALSE;
1414 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1415 if (!editor) return FALSE;
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.
1423 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1426 const gchar *editor_get_error_str(EditorFlags flags)
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.");
1438 #pragma GCC diagnostic push
1439 #pragma GCC diagnostic ignored "-Wunused-function"
1440 const gchar *editor_get_name_unused(const gchar *key)
1442 EditorDescription *editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1444 if (!editor) return NULL;
1446 return editor->name;
1448 #pragma GCC diagnostic pop
1450 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */