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 string_list_free(editor->ext_list);
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 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 (j = 0; conv_table[j][0]; j++)
165 if (strcmp(mime_types[i], conv_table[j][0]) == 0)
166 list = g_list_concat(list, filter_to_list(conv_table[j][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);
591 //~ vd->button_stop = generic_dialog_add_button(vd->gd, GTK_STOCK_STOP, NULL,
592 //~ editor_verbose_window_stop, FALSE);
593 vd->button_stop = generic_dialog_add_button(vd->gd, "process-stop", nullptr,
594 editor_verbose_window_stop, FALSE);
595 gtk_widget_set_sensitive(vd->button_stop, FALSE);
596 vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, nullptr,
597 editor_verbose_window_close, TRUE);
598 gtk_widget_set_sensitive(vd->button_close, FALSE);
600 scrolled = gtk_scrolled_window_new(nullptr, nullptr);
601 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
602 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
603 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
604 gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
605 gtk_widget_show(scrolled);
607 vd->text = gtk_text_view_new();
608 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
609 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
610 gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
611 gtk_widget_show(vd->text);
613 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
614 gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
615 gtk_widget_show(hbox);
617 vd->progress = gtk_progress_bar_new();
618 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
619 gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
620 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
621 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
622 gtk_widget_show(vd->progress);
624 vd->spinner = spinner_new(nullptr, SPINNER_SPEED);
625 gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
626 gtk_widget_show(vd->spinner);
628 gtk_widget_show(vd->gd->dialog);
634 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
636 GtkTextBuffer *buffer;
639 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
640 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
641 gtk_text_buffer_insert(buffer, &iter, text, len);
644 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
650 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
653 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
656 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
658 auto ed = static_cast<EditorData *>(data);
662 if (condition & G_IO_IN)
664 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
666 if (!g_utf8_validate(buf, count, nullptr))
670 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
673 editor_verbose_window_fill(ed->vd, utf8, -1);
678 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
683 editor_verbose_window_fill(ed->vd, buf, count);
688 if (condition & (G_IO_ERR | G_IO_HUP))
690 g_io_channel_shutdown(source, TRUE, nullptr);
704 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
708 const gchar *p = nullptr;
710 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
712 string = g_string_new("");
714 if (type == PATH_FILE || type == PATH_FILE_URL)
716 GList *work = editor->ext_list;
725 auto ext = static_cast<gchar *>(work->data);
728 if (strcmp(ext, "*") == 0 ||
729 g_ascii_strcasecmp(ext, fd->extension) == 0)
735 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
738 auto sfd = static_cast<FileData *>(work2->data);
741 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
749 if (!p) return nullptr;
752 else if (type == PATH_DEST)
754 if (fd->change && fd->change->dest)
755 p = fd->change->dest;
761 string = g_string_append(string, p);
763 if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
764 pathl = path_from_utf8(string->str);
765 g_string_free(string, TRUE);
767 if (pathl && !pathl[0]) /* empty string case */
773 DEBUG_2("editor_command_path_parse: return %s", pathl);
777 static GString *append_quoted(GString *str, const char *s, gboolean single_quotes, gboolean double_quotes)
784 g_string_append_c(str, '\'');
786 g_string_append(str, "\"'");
789 for (p = s; *p != '\0'; p++)
792 g_string_append(str, "'\\''");
794 g_string_append_c(str, *p);
800 g_string_append_c(str, '\'');
802 g_string_append(str, "'\"");
809 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
811 auto flags = static_cast<EditorFlags>(0);
813 GString *result = nullptr;
814 gboolean escape = FALSE;
815 gboolean single_quotes = FALSE;
816 gboolean double_quotes = FALSE;
818 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
821 result = g_string_new("");
823 if (editor->exec == nullptr || editor->exec[0] == '\0')
825 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
830 /* skip leading whitespaces if any */
831 while (g_ascii_isspace(*p)) p++;
840 if (output) result = g_string_append_c(result, *p);
844 if (!single_quotes) escape = TRUE;
845 if (output) result = g_string_append_c(result, *p);
849 if (output) result = g_string_append_c(result, *p);
850 if (!single_quotes && !double_quotes)
851 single_quotes = TRUE;
852 else if (single_quotes)
853 single_quotes = FALSE;
857 if (output) result = g_string_append_c(result, *p);
858 if (!single_quotes && !double_quotes)
859 double_quotes = TRUE;
860 else if (double_quotes)
861 double_quotes = FALSE;
863 else if (*p == '%' && p[1])
865 gchar *pathl = nullptr;
871 case 'f': /* single file */
872 case 'u': /* single url */
873 flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
874 if (flags & EDITOR_SINGLE_COMMAND)
876 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
881 /* use the first file from the list */
884 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
887 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
889 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
893 /* just testing, check also the rest of the list (like with F and U)
894 any matching file is OK */
895 GList *work = list->next;
897 while (!pathl && work)
899 auto fd = static_cast<FileData *>(work->data);
900 pathl = editor_command_path_parse(fd,
902 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
910 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
915 result = append_quoted(result, pathl, single_quotes, double_quotes);
923 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
924 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
926 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
938 auto fd = static_cast<FileData *>(work->data);
939 pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
947 if (work != list) g_string_append_c(result, ' ');
948 result = append_quoted(result, pathl, single_quotes, double_quotes);
956 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
962 if (editor->icon && *editor->icon)
966 result = g_string_append(result, "--icon ");
967 result = append_quoted(result, editor->icon, single_quotes, double_quotes);
974 result = append_quoted(result, editor->name, single_quotes, double_quotes);
980 result = append_quoted(result, editor->file, single_quotes, double_quotes);
984 /* %% = % escaping */
985 if (output) result = g_string_append_c(result, *p);
993 /* deprecated according to spec, ignore */
996 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1002 if (output) result = g_string_append_c(result, *p);
1007 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1011 *output = g_string_free(result, FALSE);
1012 DEBUG_3("Editor cmd: %s", *output);
1021 g_string_free(result, TRUE);
1028 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1030 auto ed = static_cast<EditorData *>(data);
1031 g_spawn_close_pid(pid);
1034 editor_command_next_finish(ed, status);
1038 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1041 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1043 gint standard_output;
1044 gint standard_error;
1048 ed->flags = editor->flags;
1049 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1051 ok = !EDITOR_ERRORS(ed->flags);
1055 ok = (options->shell.path && *options->shell.path);
1056 if (!ok) log_printf("ERROR: empty shell command\n");
1060 ok = (access(options->shell.path, X_OK) == 0);
1061 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1064 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1069 gchar *working_directory;
1073 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1074 args[n++] = options->shell.path;
1075 if (options->shell.options && *options->shell.options)
1076 args[n++] = options->shell.options;
1077 args[n++] = command;
1080 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1082 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1086 g_unsetenv("GEEQIE_DESTINATION");
1089 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1090 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1094 ed->vd ? &standard_output : nullptr,
1095 ed->vd ? &standard_error : nullptr,
1098 g_free(working_directory);
1100 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1105 g_child_watch_add(pid, editor_child_exit_cb, ed);
1115 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1116 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1122 GIOChannel *channel_output;
1123 GIOChannel *channel_error;
1125 channel_output = g_io_channel_unix_new(standard_output);
1126 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1127 g_io_channel_set_encoding(channel_output, nullptr, nullptr);
1129 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1130 editor_verbose_io_cb, ed, nullptr);
1131 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1132 editor_verbose_io_cb, ed, nullptr);
1133 g_io_channel_unref(channel_output);
1135 channel_error = g_io_channel_unix_new(standard_error);
1136 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1137 g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1139 g_io_add_watch_full(channel_error, 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_channel_unref(channel_error);
1147 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1150 static EditorFlags editor_command_next_start(EditorData *ed)
1152 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1154 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1159 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1163 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1164 editor_verbose_window_progress(ed, fd->path);
1166 editor_verbose_window_progress(ed, _("running..."));
1170 error = editor_command_one(ed->editor, ed->list, ed);
1171 if (!error && ed->vd)
1173 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1174 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1176 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1177 editor_verbose_window_fill(ed->vd, "\n", 1);
1182 return static_cast<EditorFlags>(0);
1184 /* command was not started, call the finish immediately */
1185 return editor_command_next_finish(ed, 0);
1188 /* everything is done */
1189 return editor_command_done(ed);
1192 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1194 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1197 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1199 if (ed->flags & EDITOR_FOR_EACH)
1201 /* handle the first element from the list */
1202 GList *fd_element = ed->list;
1204 ed->list = g_list_remove_link(ed->list, fd_element);
1207 cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1208 if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1210 filelist_free(fd_element);
1214 /* handle whole list */
1216 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1217 filelist_free(ed->list);
1223 case EDITOR_CB_SUSPEND:
1224 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1225 case EDITOR_CB_SKIP:
1226 return editor_command_done(ed);
1229 return editor_command_next_start(ed);
1232 static EditorFlags editor_command_done(EditorData *ed)
1238 if (ed->count == ed->total)
1240 editor_verbose_window_progress(ed, _("done"));
1244 editor_verbose_window_progress(ed, _("stopped by user"));
1246 editor_verbose_window_enable_close(ed->vd);
1249 /* free the not-handled items */
1252 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1253 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1254 filelist_free(ed->list);
1260 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1262 if (!ed->vd) editor_data_free(ed);
1267 void editor_resume(gpointer ed)
1269 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1272 void editor_skip(gpointer ed)
1274 editor_command_done(static_cast<EditorData *>(ed));
1277 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1280 EditorFlags flags = editor->flags;
1282 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1284 ed = g_new0(EditorData, 1);
1285 ed->list = filelist_copy(list);
1287 ed->editor = editor;
1288 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1291 ed->working_directory = g_strdup(working_directory);
1293 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1294 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1296 if (flags & EDITOR_VERBOSE)
1297 editor_verbose_window(ed, text);
1299 editor_command_next_start(ed);
1300 /* errors from editor_command_next_start will be handled via callback */
1301 return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1304 gboolean is_valid_editor_command(const gchar *key)
1306 if (!key) return FALSE;
1307 return g_hash_table_lookup(editors, key) != nullptr;
1310 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1313 EditorDescription *editor;
1314 if (!key) return EDITOR_ERROR_EMPTY;
1316 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1318 if (!editor) return EDITOR_ERROR_EMPTY;
1319 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1321 error = editor_command_parse(editor, list, TRUE, nullptr);
1323 if (EDITOR_ERRORS(error)) return error;
1325 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1327 if (EDITOR_ERRORS(error))
1329 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1331 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, nullptr);
1335 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1338 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1340 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1343 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1348 if (!fd) return static_cast<EditorFlags>(FALSE);
1350 list = g_list_append(nullptr, fd);
1351 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1356 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1358 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1361 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1363 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1366 gboolean editor_window_flag_set(const gchar *key)
1368 EditorDescription *editor;
1369 if (!key) return TRUE;
1371 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1372 if (!editor) return TRUE;
1374 return !!(editor->flags & EDITOR_KEEP_FS);
1377 gboolean editor_is_filter(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_DEST);
1388 gboolean editor_no_param(const gchar *key)
1390 EditorDescription *editor;
1391 if (!key) return FALSE;
1393 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1394 if (!editor) return FALSE;
1396 return !!(editor->flags & EDITOR_NO_PARAM);
1399 gboolean editor_blocks_file(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 /* Decide if the image file should be blocked during editor execution
1408 Editors like gimp can be used long time after the original file was
1409 saved, for editing unrelated files.
1410 %f vs. %F seems to be a good heuristic to detect this kind of editors.
1413 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1416 const gchar *editor_get_error_str(EditorFlags flags)
1418 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1419 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1420 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1421 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1422 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1423 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1424 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1425 return _("Unknown error.");
1428 //const gchar *editor_get_name(const gchar *key)
1430 //EditorDescription *editor = g_hash_table_lookup(editors, key);
1432 //if (!editor) return NULL;
1434 //return editor->name;
1436 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */