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"
32 EDITOR_WINDOW_WIDTH = 500,
33 EDITOR_WINDOW_HEIGHT = 300
38 struct EditorVerboseData {
40 GtkWidget *button_close;
41 GtkWidget *button_stop;
54 EditorVerboseData *vd;
55 EditorCallback callback;
57 const EditorDescription *editor;
58 gchar *working_directory; /* fallback if no files are given (editor_no_param) */
62 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
63 static EditorFlags editor_command_next_start(EditorData *ed);
64 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
65 static EditorFlags editor_command_done(EditorData *ed);
68 *-----------------------------------------------------------------------------
69 * external editor routines
70 *-----------------------------------------------------------------------------
73 GHashTable *editors = nullptr;
74 GtkListStore *desktop_file_list;
75 gboolean editors_finished = FALSE;
77 #ifdef G_KEY_FILE_DESKTOP_GROUP
78 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
80 #define DESKTOP_GROUP "Desktop Entry"
83 void editor_description_free(EditorDescription *editor)
91 g_free(editor->menu_path);
92 g_free(editor->hotkey);
93 g_free(editor->comment);
94 g_list_free_full(editor->ext_list, g_free);
99 static GList *editor_mime_types_to_extensions(gchar **mime_types)
101 /** @FIXME this should be rewritten to use the shared mime database, as soon as we switch to gio */
103 static constexpr const gchar *conv_table[][2] = {
105 {"image/bmp", ".bmp"},
106 {"image/gif", ".gif"},
107 {"image/heic", ".heic"},
108 {"image/jpeg", ".jpeg;.jpg;.mpo"},
109 {"image/jpg", ".jpg;.jpeg"},
110 {"image/jxl", ".jxl"},
111 {"image/webp", ".webp"},
112 {"image/pcx", ".pcx"},
113 {"image/png", ".png"},
114 {"image/svg", ".svg"},
115 {"image/svg+xml", ".svg"},
116 {"image/svg+xml-compressed", ".svg"},
117 {"image/tiff", ".tiff;.tif;.mef"},
118 {"image/vnd-ms.dds", ".dds"},
119 {"image/x-adobe-dng", ".dng"},
120 {"image/x-bmp", ".bmp"},
121 {"image/x-canon-crw", ".crw"},
122 {"image/x-canon-cr2", ".cr2"},
123 {"image/x-canon-cr3", ".cr3"},
124 {"image/x-cr2", ".cr2"},
125 {"image/x-dcraw", "%raw;.mos"},
126 {"image/x-epson-erf", "%erf"},
127 {"image/x-ico", ".ico"},
128 {"image/x-kodak-kdc", ".kdc"},
129 {"image/x-mrw", ".mrw"},
130 {"image/x-minolta-mrw", ".mrw"},
131 {"image/x-MS-bmp", ".bmp"},
132 {"image/x-nef", ".nef"},
133 {"image/x-nikon-nef", ".nef"},
134 {"image/x-panasonic-raw", ".raw"},
135 {"image/x-panasonic-rw2", ".rw2"},
136 {"image/x-pentax-pef", ".pef"},
137 {"image/x-orf", ".orf"},
138 {"image/x-olympus-orf", ".orf"},
139 {"image/x-pcx", ".pcx"},
140 {"image/xpm", ".xpm"},
141 {"image/x-png", ".png"},
142 {"image/x-portable-anymap", ".pam"},
143 {"image/x-portable-bitmap", ".pbm"},
144 {"image/x-portable-graymap", ".pgm"},
145 {"image/x-portable-pixmap", ".ppm"},
146 {"image/x-psd", ".psd"},
147 {"image/x-raf", ".raf"},
148 {"image/x-fuji-raf", ".raf"},
149 {"image/x-sgi", ".sgi"},
150 {"image/x-sony-arw", ".arw"},
151 {"image/x-sony-sr2", ".sr2"},
152 {"image/x-sony-srf", ".srf"},
153 {"image/x-tga", ".tga"},
154 {"image/x-xbitmap", ".xbm"},
155 {"image/x-xcf", ".xcf"},
156 {"image/x-xpixmap", ".xpm"},
157 {"application/x-navi-animation", ".ani"},
158 {"application/x-ptoptimizer-script", ".pto"},
162 GList *list = nullptr;
164 for (i = 0; mime_types[i]; i++)
165 for (const auto& c : conv_table)
166 if (strcmp(mime_types[i], c[0]) == 0)
167 list = g_list_concat(list, filter_to_list(c[1]));
172 gboolean editor_read_desktop_file(const gchar *path)
175 EditorDescription *editor;
178 const gchar *key = filename_from_path(path);
180 gchar **only_show_in;
184 gboolean category_geeqie = FALSE;
188 if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
190 key_file = g_key_file_new();
191 if (!g_key_file_load_from_file(key_file, path, static_cast<GKeyFileFlags>(0), nullptr))
193 g_key_file_free(key_file);
197 type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", nullptr);
198 if (!type || strcmp(type, "Application") != 0)
200 /* We only consider desktop entries of Application type */
201 g_key_file_free(key_file);
207 editor = g_new0(EditorDescription, 1);
209 editor->key = g_strdup(key);
210 editor->file = g_strdup(path);
212 g_hash_table_insert(editors, editor->key, editor);
214 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", nullptr)
215 || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", nullptr))
217 editor->hidden = TRUE;
220 categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", nullptr, nullptr);
223 gboolean found = FALSE;
225 for (i = 0; categories[i]; i++)
227 /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
228 if (strcmp(categories[i], "Graphics") == 0)
232 if (strcmp(categories[i], "X-Geeqie") == 0)
235 category_geeqie = TRUE;
239 if (!found) editor->ignored = TRUE;
240 g_strfreev(categories);
244 editor->ignored = TRUE;
247 only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", nullptr, nullptr);
250 gboolean found = FALSE;
252 for (i = 0; only_show_in[i]; i++)
253 if (strcmp(only_show_in[i], "X-Geeqie") == 0)
258 if (!found) editor->ignored = TRUE;
259 g_strfreev(only_show_in);
262 not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", nullptr, nullptr);
265 gboolean found = FALSE;
267 for (i = 0; not_show_in[i]; i++)
268 if (strcmp(not_show_in[i], "X-Geeqie") == 0)
273 if (found) editor->ignored = TRUE;
274 g_strfreev(not_show_in);
278 try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", nullptr);
279 if (try_exec && !editor->hidden && !editor->ignored)
281 gchar *try_exec_res = g_find_program_in_path(try_exec);
282 if (!try_exec_res) editor->hidden = TRUE;
283 g_free(try_exec_res);
289 /* ignored editors will be deleted, no need to parse the rest */
290 g_key_file_free(key_file);
294 editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", nullptr, nullptr);
295 editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", nullptr);
297 /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
298 if (editor->icon && !g_path_is_absolute(editor->icon))
300 gchar *ext = strrchr(editor->icon, '.');
302 if (ext && strlen(ext) == 4 &&
303 (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
305 log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
306 editor->file, editor->icon);
312 if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
314 g_free(editor->icon);
315 editor->icon = nullptr;
318 editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", nullptr);
320 editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", nullptr);
321 if (!editor->menu_path) editor->menu_path = g_strdup("PluginsMenu");
323 editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", nullptr);
325 editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", nullptr);
327 extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", nullptr);
329 editor->ext_list = filter_to_list(extensions);
332 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", nullptr, nullptr);
335 editor->ext_list = editor_mime_types_to_extensions(mime_types);
336 g_strfreev(mime_types);
337 if (!editor->ext_list) editor->hidden = TRUE;
341 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Keep-Fullscreen", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_KEEP_FS);
342 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_VERBOSE);
343 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose-Multi", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_VERBOSE_MULTI);
344 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_DEST);
345 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_TERMINAL);
347 editor->flags = static_cast<EditorFlags>(editor->flags | editor_command_parse(editor, nullptr, FALSE, nullptr));
349 if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
351 g_key_file_free(key_file);
353 if (editor->ignored) return TRUE;
355 work = options->disabled_plugins;
360 if (g_strcmp0(path, static_cast<const gchar *>(work->data)) == 0)
368 editor->disabled = disabled;
370 gtk_list_store_append(desktop_file_list, &iter);
371 gtk_list_store_set(desktop_file_list, &iter,
372 DESKTOP_FILE_COLUMN_KEY, key,
373 DESKTOP_FILE_COLUMN_DISABLED, editor->disabled,
374 DESKTOP_FILE_COLUMN_NAME, editor->name,
375 DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden ? _("yes") : _("no"),
376 DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
377 DESKTOP_FILE_COLUMN_PATH, path, -1);
382 static gboolean editor_remove_desktop_file_cb(gpointer, gpointer value, gpointer)
384 auto editor = static_cast<EditorDescription *>(value);
385 return editor->hidden || editor->ignored;
388 void editor_table_finish()
390 g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, nullptr);
391 editors_finished = TRUE;
394 void editor_table_clear()
396 if (desktop_file_list)
398 gtk_list_store_clear(desktop_file_list);
402 desktop_file_list = gtk_list_store_new(DESKTOP_FILE_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING);
406 g_hash_table_destroy(editors);
408 editors = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(editor_description_free));
409 editors_finished = FALSE;
412 static GList *editor_add_desktop_dir(GList *list, const gchar *path)
418 pathl = path_from_utf8(path);
426 while ((dir = readdir(dp)) != nullptr)
428 gchar *namel = dir->d_name;
430 if (g_str_has_suffix(namel, ".desktop"))
432 gchar *name = path_to_utf8(namel);
433 gchar *dpath = g_build_filename(path, name, NULL);
434 list = g_list_prepend(list, dpath);
442 GList *editor_get_desktop_files()
445 gchar *xdg_data_dirs;
449 GList *list = nullptr;
451 xdg_data_dirs = getenv("XDG_DATA_DIRS");
452 if (xdg_data_dirs && xdg_data_dirs[0])
453 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
455 xdg_data_dirs = g_strdup("/usr/share");
457 all_dirs = g_strconcat(get_rc_dir(), ":", gq_appdir, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
459 g_free(xdg_data_dirs);
461 split_dirs = g_strsplit(all_dirs, ":", 0);
465 for (i = 0; split_dirs[i]; i++);
466 for (--i; i >= 0; i--)
468 path = g_build_filename(split_dirs[i], "applications", NULL);
469 list = editor_add_desktop_dir(list, path);
473 g_strfreev(split_dirs);
477 static void editor_list_add_cb(gpointer, gpointer value, gpointer data)
479 auto listp = static_cast<GList **>(data);
480 auto editor = static_cast<EditorDescription *>(value);
482 /* do not show the special commands in any list, they are called explicitly */
483 if (strcmp(editor->key, CMD_COPY) == 0 ||
484 strcmp(editor->key, CMD_MOVE) == 0 ||
485 strcmp(editor->key, CMD_RENAME) == 0 ||
486 strcmp(editor->key, CMD_DELETE) == 0 ||
487 strcmp(editor->key, CMD_FOLDER) == 0) return;
489 if (editor->disabled)
494 *listp = g_list_prepend(*listp, editor);
497 static gint editor_sort(gconstpointer a, gconstpointer b)
499 auto ea = static_cast<const EditorDescription *>(a);
500 auto eb = static_cast<const EditorDescription *>(b);
501 gchar *caseless_name_ea;
502 gchar *caseless_name_eb;
503 gchar *collate_key_ea;
504 gchar *collate_key_eb;
507 ret = strcmp(ea->menu_path, eb->menu_path);
508 if (ret != 0) return ret;
510 caseless_name_ea = g_utf8_casefold(ea->name, -1);
511 caseless_name_eb = g_utf8_casefold(eb->name, -1);
512 collate_key_ea = g_utf8_collate_key_for_filename(caseless_name_ea, -1);
513 collate_key_eb = g_utf8_collate_key_for_filename(caseless_name_eb, -1);
514 ret = g_strcmp0(collate_key_ea, collate_key_eb);
516 g_free(collate_key_ea);
517 g_free(collate_key_eb);
518 g_free(caseless_name_ea);
519 g_free(caseless_name_eb);
524 GList *editor_list_get()
526 GList *editors_list = nullptr;
528 if (!editors_finished) return nullptr;
530 g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
531 editors_list = g_list_sort(editors_list, editor_sort);
536 /* ------------------------------ */
539 static void editor_verbose_data_free(EditorData *ed)
546 static void editor_data_free(EditorData *ed)
548 editor_verbose_data_free(ed);
549 g_free(ed->working_directory);
553 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
555 auto ed = static_cast<EditorData *>(data);
557 generic_dialog_close(gd);
558 editor_verbose_data_free(ed);
559 if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
562 static void editor_verbose_window_stop(GenericDialog *, gpointer data)
564 auto ed = static_cast<EditorData *>(data);
567 editor_verbose_window_progress(ed, _("stopping..."));
570 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
572 vd->gd->cancel_cb = editor_verbose_window_close;
574 gtk_spinner_stop(GTK_SPINNER(vd->spinner));
575 gtk_widget_set_sensitive(vd->button_stop, FALSE);
576 gtk_widget_set_sensitive(vd->button_close, TRUE);
579 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
581 EditorVerboseData *vd;
586 vd = g_new0(EditorVerboseData, 1);
588 vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
591 buf = g_strdup_printf(_("Output of %s"), text);
592 generic_dialog_add_message(vd->gd, nullptr, buf, nullptr, FALSE);
594 vd->button_stop = generic_dialog_add_button(vd->gd, GQ_ICON_STOP, nullptr,
595 editor_verbose_window_stop, FALSE);
596 gtk_widget_set_sensitive(vd->button_stop, FALSE);
597 vd->button_close = generic_dialog_add_button(vd->gd, GQ_ICON_CLOSE, _("Close"),
598 editor_verbose_window_close, TRUE);
599 gtk_widget_set_sensitive(vd->button_close, FALSE);
601 scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
602 gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
603 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
604 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
605 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
606 gtk_widget_show(scrolled);
608 vd->text = gtk_text_view_new();
609 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
610 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
611 gq_gtk_container_add(GTK_WIDGET(scrolled), vd->text);
612 gtk_widget_show(vd->text);
614 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
615 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
616 gtk_widget_show(hbox);
618 vd->progress = gtk_progress_bar_new();
619 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
620 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
621 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
622 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
623 gtk_widget_show(vd->progress);
625 vd->spinner = gtk_spinner_new();
626 gtk_spinner_start(GTK_SPINNER(vd->spinner));
627 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
628 gtk_widget_show(vd->spinner);
630 gtk_widget_show(vd->gd->dialog);
636 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
638 GtkTextBuffer *buffer;
641 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
642 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
643 gtk_text_buffer_insert(buffer, &iter, text, len);
646 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
652 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
655 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
658 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
660 auto ed = static_cast<EditorData *>(data);
664 if (condition & G_IO_IN)
666 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
668 if (!g_utf8_validate(buf, count, nullptr))
672 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
675 editor_verbose_window_fill(ed->vd, utf8, -1);
680 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
685 editor_verbose_window_fill(ed->vd, buf, count);
690 if (condition & (G_IO_ERR | G_IO_HUP))
692 g_io_channel_shutdown(source, TRUE, nullptr);
706 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
709 const gchar *p = nullptr;
711 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
713 if (type == PATH_FILE || type == PATH_FILE_URL)
715 GList *work = editor->ext_list;
724 auto ext = static_cast<gchar *>(work->data);
727 if (strcmp(ext, "*") == 0 ||
728 g_ascii_strcasecmp(ext, fd->extension) == 0)
734 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
737 auto sfd = static_cast<FileData *>(work2->data);
740 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
748 if (!p) return nullptr;
751 else if (type == PATH_DEST)
753 if (fd->change && fd->change->dest)
754 p = fd->change->dest;
761 GString *string = g_string_new(p);
762 if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
763 pathl = path_from_utf8(string->str);
764 g_string_free(string, TRUE);
766 if (pathl && !pathl[0]) /* empty string case */
772 DEBUG_2("editor_command_path_parse: return %s", pathl);
776 struct CommandBuilder
782 g_string_free(str, TRUE);
789 str = g_string_new("");
792 void append(const gchar *val)
796 str = g_string_append(str, val);
799 void append_c(gchar c)
803 str = g_string_append_c(str, c);
806 void append_quoted(const char *s, gboolean single_quotes, gboolean double_quotes)
813 str = g_string_append_c(str, '\'');
815 str = g_string_append(str, "\"'");
818 for (const char *p = s; *p != '\0'; p++)
821 str = g_string_append(str, "'\\''");
823 str = g_string_append_c(str, *p);
829 str = g_string_append_c(str, '\'');
831 str = g_string_append(str, "'\"");
837 if (!str) return nullptr;
839 auto command = g_string_free(str, FALSE);
845 GString *str{nullptr};
849 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
851 auto flags = static_cast<EditorFlags>(0);
853 CommandBuilder result;
854 gboolean escape = FALSE;
855 gboolean single_quotes = FALSE;
856 gboolean double_quotes = FALSE;
858 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
866 if (editor->exec == nullptr || editor->exec[0] == '\0')
868 return static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
872 /* skip leading whitespaces if any */
873 while (g_ascii_isspace(*p)) p++;
886 if (!single_quotes) escape = TRUE;
892 if (!single_quotes && !double_quotes)
893 single_quotes = TRUE;
894 else if (single_quotes)
895 single_quotes = FALSE;
900 if (!single_quotes && !double_quotes)
901 double_quotes = TRUE;
902 else if (double_quotes)
903 double_quotes = FALSE;
905 else if (*p == '%' && p[1])
907 gchar *pathl = nullptr;
913 case 'f': /* single file */
914 case 'u': /* single url */
915 flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
916 if (flags & EDITOR_SINGLE_COMMAND)
918 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
922 /* use the first file from the list */
925 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
927 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
929 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
933 /* just testing, check also the rest of the list (like with F and U)
934 any matching file is OK */
935 GList *work = list->next;
937 while (!pathl && work)
939 auto fd = static_cast<FileData *>(work->data);
940 pathl = editor_command_path_parse(fd,
942 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
950 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
952 result.append_quoted(pathl, single_quotes, double_quotes);
959 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
960 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
962 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
973 auto fd = static_cast<FileData *>(work->data);
974 pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
981 result.append_c(' ');
983 result.append_quoted(pathl, single_quotes, double_quotes);
990 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
995 if (editor->icon && *editor->icon)
997 result.append("--icon ");
998 result.append_quoted(editor->icon, single_quotes, double_quotes);
1002 result.append_quoted(editor->name, single_quotes, double_quotes);
1005 result.append_quoted(editor->file, single_quotes, double_quotes);
1008 /* %% = % escaping */
1009 result.append_c(*p);
1017 /* deprecated according to spec, ignore */
1020 return static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1025 result.append_c(*p);
1030 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1034 *output = result.get_command();
1035 DEBUG_3("Editor cmd: %s", *output);
1042 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1044 auto ed = static_cast<EditorData *>(data);
1045 g_spawn_close_pid(pid);
1048 editor_command_next_finish(ed, status);
1052 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1055 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1057 gint standard_output;
1058 gint standard_error;
1062 ed->flags = editor->flags;
1063 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1065 ok = !EDITOR_ERRORS(ed->flags);
1069 ok = (options->shell.path && *options->shell.path);
1070 if (!ok) log_printf("ERROR: empty shell command\n");
1074 ok = (access(options->shell.path, X_OK) == 0);
1075 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1078 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1083 gchar *working_directory;
1087 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1088 args[n++] = options->shell.path;
1089 if (options->shell.options && *options->shell.options)
1090 args[n++] = options->shell.options;
1091 args[n++] = command;
1094 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1096 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1100 g_unsetenv("GEEQIE_DESTINATION");
1103 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1104 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1108 ed->vd ? &standard_output : nullptr,
1109 ed->vd ? &standard_error : nullptr,
1112 g_free(working_directory);
1114 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1119 g_child_watch_add(pid, editor_child_exit_cb, ed);
1129 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1130 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1136 GIOChannel *channel_output;
1137 GIOChannel *channel_error;
1139 channel_output = g_io_channel_unix_new(standard_output);
1140 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1141 g_io_channel_set_encoding(channel_output, nullptr, nullptr);
1143 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1144 editor_verbose_io_cb, ed, nullptr);
1145 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1146 editor_verbose_io_cb, ed, nullptr);
1147 g_io_channel_unref(channel_output);
1149 channel_error = g_io_channel_unix_new(standard_error);
1150 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1151 g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1153 g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1154 editor_verbose_io_cb, ed, nullptr);
1155 g_io_channel_unref(channel_error);
1161 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1164 static EditorFlags editor_command_next_start(EditorData *ed)
1166 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1168 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1173 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1177 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1178 editor_verbose_window_progress(ed, fd->path);
1180 editor_verbose_window_progress(ed, _("running..."));
1184 error = editor_command_one(ed->editor, ed->list, ed);
1185 if (!error && ed->vd)
1187 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1188 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1190 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1191 editor_verbose_window_fill(ed->vd, "\n", 1);
1196 return static_cast<EditorFlags>(0);
1198 /* command was not started, call the finish immediately */
1199 return editor_command_next_finish(ed, 0);
1202 /* everything is done */
1203 return editor_command_done(ed);
1206 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1208 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1211 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1213 if (ed->flags & EDITOR_FOR_EACH)
1215 /* handle the first element from the list */
1216 GList *fd_element = ed->list;
1218 ed->list = g_list_remove_link(ed->list, fd_element);
1221 cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1222 if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1224 filelist_free(fd_element);
1228 /* handle whole list */
1230 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1231 filelist_free(ed->list);
1237 case EDITOR_CB_SUSPEND:
1238 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1239 case EDITOR_CB_SKIP:
1240 return editor_command_done(ed);
1243 return editor_command_next_start(ed);
1246 static EditorFlags editor_command_done(EditorData *ed)
1252 if (ed->count == ed->total)
1254 editor_verbose_window_progress(ed, _("done"));
1258 editor_verbose_window_progress(ed, _("stopped by user"));
1260 editor_verbose_window_enable_close(ed->vd);
1263 /* free the not-handled items */
1266 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1267 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1268 filelist_free(ed->list);
1274 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1276 if (!ed->vd) editor_data_free(ed);
1281 void editor_resume(gpointer ed)
1283 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1286 void editor_skip(gpointer ed)
1288 editor_command_done(static_cast<EditorData *>(ed));
1291 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1294 EditorFlags flags = editor->flags;
1296 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1298 ed = g_new0(EditorData, 1);
1299 ed->list = filelist_copy(list);
1301 ed->editor = editor;
1302 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1305 ed->working_directory = g_strdup(working_directory);
1307 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1308 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1310 if (flags & EDITOR_VERBOSE)
1311 editor_verbose_window(ed, text);
1313 editor_command_next_start(ed);
1314 /* errors from editor_command_next_start will be handled via callback */
1315 return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1318 gboolean is_valid_editor_command(const gchar *key)
1320 if (!key) return FALSE;
1321 return g_hash_table_lookup(editors, key) != nullptr;
1324 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1327 EditorDescription *editor;
1328 if (!key) return EDITOR_ERROR_EMPTY;
1330 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1332 if (!editor) return EDITOR_ERROR_EMPTY;
1333 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1335 error = editor_command_parse(editor, list, TRUE, nullptr);
1337 if (EDITOR_ERRORS(error)) return error;
1339 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1341 if (EDITOR_ERRORS(error))
1343 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1345 file_util_warning_dialog(_("Invalid editor command"), text, GQ_ICON_DIALOG_ERROR, nullptr);
1349 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1352 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1354 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1357 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1362 if (!fd) return static_cast<EditorFlags>(FALSE);
1364 list = g_list_append(nullptr, fd);
1365 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1370 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1372 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1375 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1377 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1380 gboolean editor_window_flag_set(const gchar *key)
1382 EditorDescription *editor;
1383 if (!key) return TRUE;
1385 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1386 if (!editor) return TRUE;
1388 return !!(editor->flags & EDITOR_KEEP_FS);
1391 gboolean editor_is_filter(const gchar *key)
1393 EditorDescription *editor;
1394 if (!key) return TRUE;
1396 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1397 if (!editor) return TRUE;
1399 return !!(editor->flags & EDITOR_DEST);
1402 gboolean editor_no_param(const gchar *key)
1404 EditorDescription *editor;
1405 if (!key) return FALSE;
1407 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1408 if (!editor) return FALSE;
1410 return !!(editor->flags & EDITOR_NO_PARAM);
1413 gboolean editor_blocks_file(const gchar *key)
1415 EditorDescription *editor;
1416 if (!key) return FALSE;
1418 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1419 if (!editor) return FALSE;
1421 /* Decide if the image file should be blocked during editor execution
1422 Editors like gimp can be used long time after the original file was
1423 saved, for editing unrelated files.
1424 %f vs. %F seems to be a good heuristic to detect this kind of editors.
1427 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1430 const gchar *editor_get_error_str(EditorFlags flags)
1432 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1433 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1434 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1435 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1436 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1437 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1438 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1439 return _("Unknown error.");
1442 #pragma GCC diagnostic push
1443 #pragma GCC diagnostic ignored "-Wunused-function"
1444 const gchar *editor_get_name_unused(const gchar *key)
1446 auto *editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1448 if (!editor) return nullptr;
1450 return editor->name;
1452 #pragma GCC diagnostic pop
1454 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */