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 UNUSED(key), gpointer value, gpointer UNUSED(user_data))
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 UNUSED(key), 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 *UNUSED(gd), 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);
592 vd->button_stop = generic_dialog_add_button(vd->gd, "process-stop", nullptr,
593 editor_verbose_window_stop, FALSE);
594 gtk_widget_set_sensitive(vd->button_stop, FALSE);
595 vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, nullptr,
596 editor_verbose_window_close, TRUE);
597 gtk_widget_set_sensitive(vd->button_close, FALSE);
599 scrolled = gtk_scrolled_window_new(nullptr, nullptr);
600 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
601 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
602 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
603 gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
604 gtk_widget_show(scrolled);
606 vd->text = gtk_text_view_new();
607 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
608 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
609 gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
610 gtk_widget_show(vd->text);
612 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
613 gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
614 gtk_widget_show(hbox);
616 vd->progress = gtk_progress_bar_new();
617 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
618 gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
619 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
620 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
621 gtk_widget_show(vd->progress);
623 vd->spinner = spinner_new(nullptr, SPINNER_SPEED);
624 gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
625 gtk_widget_show(vd->spinner);
627 gtk_widget_show(vd->gd->dialog);
633 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
635 GtkTextBuffer *buffer;
638 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
639 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
640 gtk_text_buffer_insert(buffer, &iter, text, len);
643 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
649 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
652 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
655 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
657 auto ed = static_cast<EditorData *>(data);
661 if (condition & G_IO_IN)
663 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
665 if (!g_utf8_validate(buf, count, nullptr))
669 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
672 editor_verbose_window_fill(ed->vd, utf8, -1);
677 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
682 editor_verbose_window_fill(ed->vd, buf, count);
687 if (condition & (G_IO_ERR | G_IO_HUP))
689 g_io_channel_shutdown(source, TRUE, nullptr);
703 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
706 const gchar *p = nullptr;
708 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
710 if (type == PATH_FILE || type == PATH_FILE_URL)
712 GList *work = editor->ext_list;
721 auto ext = static_cast<gchar *>(work->data);
724 if (strcmp(ext, "*") == 0 ||
725 g_ascii_strcasecmp(ext, fd->extension) == 0)
731 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
734 auto sfd = static_cast<FileData *>(work2->data);
737 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
745 if (!p) return nullptr;
748 else if (type == PATH_DEST)
750 if (fd->change && fd->change->dest)
751 p = fd->change->dest;
758 GString *string = g_string_new(p);
759 if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
760 pathl = path_from_utf8(string->str);
761 g_string_free(string, TRUE);
763 if (pathl && !pathl[0]) /* empty string case */
769 DEBUG_2("editor_command_path_parse: return %s", pathl);
773 struct CommandBuilder
779 g_string_free(str, TRUE);
786 str = g_string_new("");
789 void append(const gchar *val)
793 str = g_string_append(str, val);
796 void append_c(gchar c)
800 str = g_string_append_c(str, c);
803 void append_quoted(const char *s, gboolean single_quotes, gboolean double_quotes)
810 str = g_string_append_c(str, '\'');
812 str = g_string_append(str, "\"'");
815 for (const char *p = s; *p != '\0'; p++)
818 str = g_string_append(str, "'\\''");
820 str = g_string_append_c(str, *p);
826 str = g_string_append_c(str, '\'');
828 str = g_string_append(str, "'\"");
834 if (!str) return nullptr;
836 auto command = g_string_free(str, FALSE);
842 GString *str{nullptr};
846 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
848 auto flags = static_cast<EditorFlags>(0);
850 CommandBuilder result;
851 gboolean escape = FALSE;
852 gboolean single_quotes = FALSE;
853 gboolean double_quotes = FALSE;
855 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
863 if (editor->exec == nullptr || editor->exec[0] == '\0')
865 return static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
869 /* skip leading whitespaces if any */
870 while (g_ascii_isspace(*p)) p++;
883 if (!single_quotes) escape = TRUE;
889 if (!single_quotes && !double_quotes)
890 single_quotes = TRUE;
891 else if (single_quotes)
892 single_quotes = FALSE;
897 if (!single_quotes && !double_quotes)
898 double_quotes = TRUE;
899 else if (double_quotes)
900 double_quotes = FALSE;
902 else if (*p == '%' && p[1])
904 gchar *pathl = nullptr;
910 case 'f': /* single file */
911 case 'u': /* single url */
912 flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
913 if (flags & EDITOR_SINGLE_COMMAND)
915 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
919 /* use the first file from the list */
922 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
924 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
926 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
930 /* just testing, check also the rest of the list (like with F and U)
931 any matching file is OK */
932 GList *work = list->next;
934 while (!pathl && work)
936 auto fd = static_cast<FileData *>(work->data);
937 pathl = editor_command_path_parse(fd,
939 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
947 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
949 result.append_quoted(pathl, single_quotes, double_quotes);
956 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
957 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
959 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
970 auto fd = static_cast<FileData *>(work->data);
971 pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
978 result.append_c(' ');
980 result.append_quoted(pathl, single_quotes, double_quotes);
987 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
992 if (editor->icon && *editor->icon)
994 result.append("--icon ");
995 result.append_quoted(editor->icon, single_quotes, double_quotes);
999 result.append_quoted(editor->name, single_quotes, double_quotes);
1002 result.append_quoted(editor->file, single_quotes, double_quotes);
1005 /* %% = % escaping */
1006 result.append_c(*p);
1014 /* deprecated according to spec, ignore */
1017 return static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1022 result.append_c(*p);
1027 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1031 *output = result.get_command();
1032 DEBUG_3("Editor cmd: %s", *output);
1039 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1041 auto ed = static_cast<EditorData *>(data);
1042 g_spawn_close_pid(pid);
1045 editor_command_next_finish(ed, status);
1049 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1052 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1054 gint standard_output;
1055 gint standard_error;
1059 ed->flags = editor->flags;
1060 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1062 ok = !EDITOR_ERRORS(ed->flags);
1066 ok = (options->shell.path && *options->shell.path);
1067 if (!ok) log_printf("ERROR: empty shell command\n");
1071 ok = (access(options->shell.path, X_OK) == 0);
1072 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1075 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1080 gchar *working_directory;
1084 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1085 args[n++] = options->shell.path;
1086 if (options->shell.options && *options->shell.options)
1087 args[n++] = options->shell.options;
1088 args[n++] = command;
1091 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1093 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1097 g_unsetenv("GEEQIE_DESTINATION");
1100 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1101 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1105 ed->vd ? &standard_output : nullptr,
1106 ed->vd ? &standard_error : nullptr,
1109 g_free(working_directory);
1111 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1116 g_child_watch_add(pid, editor_child_exit_cb, ed);
1126 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1127 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1133 GIOChannel *channel_output;
1134 GIOChannel *channel_error;
1136 channel_output = g_io_channel_unix_new(standard_output);
1137 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1138 g_io_channel_set_encoding(channel_output, nullptr, nullptr);
1140 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1141 editor_verbose_io_cb, ed, nullptr);
1142 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1143 editor_verbose_io_cb, ed, nullptr);
1144 g_io_channel_unref(channel_output);
1146 channel_error = g_io_channel_unix_new(standard_error);
1147 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1148 g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1150 g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1151 editor_verbose_io_cb, ed, nullptr);
1152 g_io_channel_unref(channel_error);
1158 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1161 static EditorFlags editor_command_next_start(EditorData *ed)
1163 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1165 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1170 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1174 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1175 editor_verbose_window_progress(ed, fd->path);
1177 editor_verbose_window_progress(ed, _("running..."));
1181 error = editor_command_one(ed->editor, ed->list, ed);
1182 if (!error && ed->vd)
1184 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1185 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1187 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1188 editor_verbose_window_fill(ed->vd, "\n", 1);
1193 return static_cast<EditorFlags>(0);
1195 /* command was not started, call the finish immediately */
1196 return editor_command_next_finish(ed, 0);
1199 /* everything is done */
1200 return editor_command_done(ed);
1203 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1205 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1208 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1210 if (ed->flags & EDITOR_FOR_EACH)
1212 /* handle the first element from the list */
1213 GList *fd_element = ed->list;
1215 ed->list = g_list_remove_link(ed->list, fd_element);
1218 cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1219 if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1221 filelist_free(fd_element);
1225 /* handle whole list */
1227 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1228 filelist_free(ed->list);
1234 case EDITOR_CB_SUSPEND:
1235 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1236 case EDITOR_CB_SKIP:
1237 return editor_command_done(ed);
1240 return editor_command_next_start(ed);
1243 static EditorFlags editor_command_done(EditorData *ed)
1249 if (ed->count == ed->total)
1251 editor_verbose_window_progress(ed, _("done"));
1255 editor_verbose_window_progress(ed, _("stopped by user"));
1257 editor_verbose_window_enable_close(ed->vd);
1260 /* free the not-handled items */
1263 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1264 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1265 filelist_free(ed->list);
1271 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1273 if (!ed->vd) editor_data_free(ed);
1278 void editor_resume(gpointer ed)
1280 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1283 void editor_skip(gpointer ed)
1285 editor_command_done(static_cast<EditorData *>(ed));
1288 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1291 EditorFlags flags = editor->flags;
1293 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1295 ed = g_new0(EditorData, 1);
1296 ed->list = filelist_copy(list);
1298 ed->editor = editor;
1299 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1302 ed->working_directory = g_strdup(working_directory);
1304 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1305 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1307 if (flags & EDITOR_VERBOSE)
1308 editor_verbose_window(ed, text);
1310 editor_command_next_start(ed);
1311 /* errors from editor_command_next_start will be handled via callback */
1312 return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1315 gboolean is_valid_editor_command(const gchar *key)
1317 if (!key) return FALSE;
1318 return g_hash_table_lookup(editors, key) != nullptr;
1321 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1324 EditorDescription *editor;
1325 if (!key) return EDITOR_ERROR_EMPTY;
1327 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1329 if (!editor) return EDITOR_ERROR_EMPTY;
1330 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1332 error = editor_command_parse(editor, list, TRUE, nullptr);
1334 if (EDITOR_ERRORS(error)) return error;
1336 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1338 if (EDITOR_ERRORS(error))
1340 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1342 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, nullptr);
1346 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1349 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1351 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1354 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1359 if (!fd) return static_cast<EditorFlags>(FALSE);
1361 list = g_list_append(nullptr, fd);
1362 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1367 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1369 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1372 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1374 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1377 gboolean editor_window_flag_set(const gchar *key)
1379 EditorDescription *editor;
1380 if (!key) return TRUE;
1382 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1383 if (!editor) return TRUE;
1385 return !!(editor->flags & EDITOR_KEEP_FS);
1388 gboolean editor_is_filter(const gchar *key)
1390 EditorDescription *editor;
1391 if (!key) return TRUE;
1393 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1394 if (!editor) return TRUE;
1396 return !!(editor->flags & EDITOR_DEST);
1399 gboolean editor_no_param(const gchar *key)
1401 EditorDescription *editor;
1402 if (!key) return FALSE;
1404 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1405 if (!editor) return FALSE;
1407 return !!(editor->flags & EDITOR_NO_PARAM);
1410 gboolean editor_blocks_file(const gchar *key)
1412 EditorDescription *editor;
1413 if (!key) return FALSE;
1415 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1416 if (!editor) return FALSE;
1418 /* Decide if the image file should be blocked during editor execution
1419 Editors like gimp can be used long time after the original file was
1420 saved, for editing unrelated files.
1421 %f vs. %F seems to be a good heuristic to detect this kind of editors.
1424 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1427 const gchar *editor_get_error_str(EditorFlags flags)
1429 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1430 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1431 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1432 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1433 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1434 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1435 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1436 return _("Unknown error.");
1439 #pragma GCC diagnostic push
1440 #pragma GCC diagnostic ignored "-Wunused-function"
1441 const gchar *editor_get_name_unused(const gchar *key)
1443 EditorDescription *editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1445 if (!editor) return NULL;
1447 return editor->name;
1449 #pragma GCC diagnostic pop
1451 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */