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.
29 #include "filefilter.h"
32 #include "main-defines.h"
34 #include "pixbuf-util.h"
35 #include "ui-fileops.h"
39 EDITOR_WINDOW_WIDTH = 500,
40 EDITOR_WINDOW_HEIGHT = 300
45 struct EditorVerboseData {
47 GtkWidget *button_close;
48 GtkWidget *button_stop;
61 EditorVerboseData *vd;
62 EditorCallback callback;
64 const EditorDescription *editor;
65 gchar *working_directory; /* fallback if no files are given (editor_no_param) */
69 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
70 static EditorFlags editor_command_next_start(EditorData *ed);
71 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
72 static EditorFlags editor_command_done(EditorData *ed);
75 *-----------------------------------------------------------------------------
76 * external editor routines
77 *-----------------------------------------------------------------------------
80 GHashTable *editors = nullptr;
81 GtkListStore *desktop_file_list;
82 gboolean editors_finished = FALSE;
84 #ifdef G_KEY_FILE_DESKTOP_GROUP
85 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
87 #define DESKTOP_GROUP "Desktop Entry"
90 void editor_description_free(EditorDescription *editor)
98 g_free(editor->menu_path);
99 g_free(editor->hotkey);
100 g_free(editor->comment);
101 g_list_free_full(editor->ext_list, g_free);
102 g_free(editor->file);
106 static GList *editor_mime_types_to_extensions(gchar **mime_types)
108 /** @FIXME this should be rewritten to use the shared mime database, as soon as we switch to gio */
110 static constexpr const gchar *conv_table[][2] = {
112 {"image/bmp", ".bmp"},
113 {"image/gif", ".gif"},
114 {"image/heic", ".heic"},
115 {"image/jpeg", ".jpeg;.jpg;.mpo"},
116 {"image/jpg", ".jpg;.jpeg"},
117 {"image/jxl", ".jxl"},
118 {"image/webp", ".webp"},
119 {"image/pcx", ".pcx"},
120 {"image/png", ".png"},
121 {"image/svg", ".svg"},
122 {"image/svg+xml", ".svg"},
123 {"image/svg+xml-compressed", ".svg"},
124 {"image/tiff", ".tiff;.tif;.mef"},
125 {"image/vnd-ms.dds", ".dds"},
126 {"image/x-adobe-dng", ".dng"},
127 {"image/x-bmp", ".bmp"},
128 {"image/x-canon-crw", ".crw"},
129 {"image/x-canon-cr2", ".cr2"},
130 {"image/x-canon-cr3", ".cr3"},
131 {"image/x-cr2", ".cr2"},
132 {"image/x-dcraw", "%raw;.mos"},
133 {"image/x-epson-erf", "%erf"},
134 {"image/x-ico", ".ico"},
135 {"image/x-kodak-kdc", ".kdc"},
136 {"image/x-mrw", ".mrw"},
137 {"image/x-minolta-mrw", ".mrw"},
138 {"image/x-MS-bmp", ".bmp"},
139 {"image/x-nef", ".nef"},
140 {"image/x-nikon-nef", ".nef"},
141 {"image/x-panasonic-raw", ".raw"},
142 {"image/x-panasonic-rw2", ".rw2"},
143 {"image/x-pentax-pef", ".pef"},
144 {"image/x-orf", ".orf"},
145 {"image/x-olympus-orf", ".orf"},
146 {"image/x-pcx", ".pcx"},
147 {"image/xpm", ".xpm"},
148 {"image/x-png", ".png"},
149 {"image/x-portable-anymap", ".pam"},
150 {"image/x-portable-bitmap", ".pbm"},
151 {"image/x-portable-graymap", ".pgm"},
152 {"image/x-portable-pixmap", ".ppm"},
153 {"image/x-psd", ".psd"},
154 {"image/x-raf", ".raf"},
155 {"image/x-fuji-raf", ".raf"},
156 {"image/x-sgi", ".sgi"},
157 {"image/x-sony-arw", ".arw"},
158 {"image/x-sony-sr2", ".sr2"},
159 {"image/x-sony-srf", ".srf"},
160 {"image/x-tga", ".tga"},
161 {"image/x-xbitmap", ".xbm"},
162 {"image/x-xcf", ".xcf"},
163 {"image/x-xpixmap", ".xpm"},
164 {"application/x-navi-animation", ".ani"},
165 {"application/x-ptoptimizer-script", ".pto"},
169 GList *list = nullptr;
171 for (i = 0; mime_types[i]; i++)
172 for (const auto& c : conv_table)
173 if (strcmp(mime_types[i], c[0]) == 0)
174 list = g_list_concat(list, filter_to_list(c[1]));
179 gboolean editor_read_desktop_file(const gchar *path)
182 EditorDescription *editor;
185 const gchar *key = filename_from_path(path);
187 gchar **only_show_in;
191 gboolean category_geeqie = FALSE;
195 if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
197 key_file = g_key_file_new();
198 if (!g_key_file_load_from_file(key_file, path, static_cast<GKeyFileFlags>(0), nullptr))
200 g_key_file_free(key_file);
204 type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", nullptr);
205 if (!type || strcmp(type, "Application") != 0)
207 /* We only consider desktop entries of Application type */
208 g_key_file_free(key_file);
214 editor = g_new0(EditorDescription, 1);
216 editor->key = g_strdup(key);
217 editor->file = g_strdup(path);
219 g_hash_table_insert(editors, editor->key, editor);
221 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", nullptr)
222 || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", nullptr))
224 editor->hidden = TRUE;
227 categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", nullptr, nullptr);
230 gboolean found = FALSE;
232 for (i = 0; categories[i]; i++)
234 /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
235 if (strcmp(categories[i], "Graphics") == 0)
239 if (strcmp(categories[i], "X-Geeqie") == 0)
242 category_geeqie = TRUE;
246 if (!found) editor->ignored = TRUE;
247 g_strfreev(categories);
251 editor->ignored = TRUE;
254 only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", nullptr, nullptr);
257 gboolean found = FALSE;
259 for (i = 0; only_show_in[i]; i++)
260 if (strcmp(only_show_in[i], "X-Geeqie") == 0)
265 if (!found) editor->ignored = TRUE;
266 g_strfreev(only_show_in);
269 not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", nullptr, nullptr);
272 gboolean found = FALSE;
274 for (i = 0; not_show_in[i]; i++)
275 if (strcmp(not_show_in[i], "X-Geeqie") == 0)
280 if (found) editor->ignored = TRUE;
281 g_strfreev(not_show_in);
285 try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", nullptr);
286 if (try_exec && !editor->hidden && !editor->ignored)
288 gchar *try_exec_res = g_find_program_in_path(try_exec);
289 if (!try_exec_res) editor->hidden = TRUE;
290 g_free(try_exec_res);
296 /* ignored editors will be deleted, no need to parse the rest */
297 g_key_file_free(key_file);
301 editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", nullptr, nullptr);
302 editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", nullptr);
304 /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
305 if (editor->icon && !g_path_is_absolute(editor->icon))
307 gchar *ext = strrchr(editor->icon, '.');
309 if (ext && strlen(ext) == 4 &&
310 (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
312 log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
313 editor->file, editor->icon);
319 if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
321 g_free(editor->icon);
322 editor->icon = nullptr;
325 editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", nullptr);
327 editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", nullptr);
328 if (!editor->menu_path) editor->menu_path = g_strdup("PluginsMenu");
330 editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", nullptr);
332 editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", nullptr);
334 extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", nullptr);
336 editor->ext_list = filter_to_list(extensions);
339 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", nullptr, nullptr);
342 editor->ext_list = editor_mime_types_to_extensions(mime_types);
343 g_strfreev(mime_types);
344 if (!editor->ext_list) editor->hidden = TRUE;
348 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);
349 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_VERBOSE);
350 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);
351 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_DEST);
352 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_TERMINAL);
354 editor->flags = static_cast<EditorFlags>(editor->flags | editor_command_parse(editor, nullptr, FALSE, nullptr));
356 if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
358 g_key_file_free(key_file);
360 if (editor->ignored) return TRUE;
362 work = options->disabled_plugins;
367 if (g_strcmp0(path, static_cast<const gchar *>(work->data)) == 0)
375 editor->disabled = disabled;
377 gtk_list_store_append(desktop_file_list, &iter);
378 gtk_list_store_set(desktop_file_list, &iter,
379 DESKTOP_FILE_COLUMN_KEY, key,
380 DESKTOP_FILE_COLUMN_DISABLED, editor->disabled,
381 DESKTOP_FILE_COLUMN_NAME, editor->name,
382 DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden ? _("yes") : _("no"),
383 DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
384 DESKTOP_FILE_COLUMN_PATH, path, -1);
389 static gboolean editor_remove_desktop_file_cb(gpointer, gpointer value, gpointer)
391 auto editor = static_cast<EditorDescription *>(value);
392 return editor->hidden || editor->ignored;
395 void editor_table_finish()
397 g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, nullptr);
398 editors_finished = TRUE;
401 void editor_table_clear()
403 if (desktop_file_list)
405 gtk_list_store_clear(desktop_file_list);
409 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);
413 g_hash_table_destroy(editors);
415 editors = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(editor_description_free));
416 editors_finished = FALSE;
419 static GList *editor_add_desktop_dir(GList *list, const gchar *path)
425 pathl = path_from_utf8(path);
433 while ((dir = readdir(dp)) != nullptr)
435 gchar *namel = dir->d_name;
437 if (g_str_has_suffix(namel, ".desktop"))
439 gchar *name = path_to_utf8(namel);
440 gchar *dpath = g_build_filename(path, name, NULL);
441 list = g_list_prepend(list, dpath);
449 GList *editor_get_desktop_files()
452 gchar *xdg_data_dirs;
456 GList *list = nullptr;
458 xdg_data_dirs = getenv("XDG_DATA_DIRS");
459 if (xdg_data_dirs && xdg_data_dirs[0])
460 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
462 xdg_data_dirs = g_strdup("/usr/share");
464 all_dirs = g_strconcat(get_rc_dir(), ":", gq_appdir, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
466 g_free(xdg_data_dirs);
468 split_dirs = g_strsplit(all_dirs, ":", 0);
472 for (i = 0; split_dirs[i]; i++);
473 for (--i; i >= 0; i--)
475 path = g_build_filename(split_dirs[i], "applications", NULL);
476 list = editor_add_desktop_dir(list, path);
480 g_strfreev(split_dirs);
484 static void editor_list_add_cb(gpointer, gpointer value, gpointer data)
486 auto listp = static_cast<GList **>(data);
487 auto editor = static_cast<EditorDescription *>(value);
489 /* do not show the special commands in any list, they are called explicitly */
490 if (strcmp(editor->key, CMD_COPY) == 0 ||
491 strcmp(editor->key, CMD_MOVE) == 0 ||
492 strcmp(editor->key, CMD_RENAME) == 0 ||
493 strcmp(editor->key, CMD_DELETE) == 0 ||
494 strcmp(editor->key, CMD_FOLDER) == 0) return;
496 if (editor->disabled)
501 *listp = g_list_prepend(*listp, editor);
504 static gint editor_sort(gconstpointer a, gconstpointer b)
506 auto ea = static_cast<const EditorDescription *>(a);
507 auto eb = static_cast<const EditorDescription *>(b);
508 gchar *caseless_name_ea;
509 gchar *caseless_name_eb;
510 gchar *collate_key_ea;
511 gchar *collate_key_eb;
514 ret = strcmp(ea->menu_path, eb->menu_path);
515 if (ret != 0) return ret;
517 caseless_name_ea = g_utf8_casefold(ea->name, -1);
518 caseless_name_eb = g_utf8_casefold(eb->name, -1);
519 collate_key_ea = g_utf8_collate_key_for_filename(caseless_name_ea, -1);
520 collate_key_eb = g_utf8_collate_key_for_filename(caseless_name_eb, -1);
521 ret = g_strcmp0(collate_key_ea, collate_key_eb);
523 g_free(collate_key_ea);
524 g_free(collate_key_eb);
525 g_free(caseless_name_ea);
526 g_free(caseless_name_eb);
531 GList *editor_list_get()
533 GList *editors_list = nullptr;
535 if (!editors_finished) return nullptr;
537 g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
538 editors_list = g_list_sort(editors_list, editor_sort);
543 /* ------------------------------ */
546 static void editor_verbose_data_free(EditorData *ed)
553 static void editor_data_free(EditorData *ed)
555 editor_verbose_data_free(ed);
556 g_free(ed->working_directory);
560 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
562 auto ed = static_cast<EditorData *>(data);
564 generic_dialog_close(gd);
565 editor_verbose_data_free(ed);
566 if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
569 static void editor_verbose_window_stop(GenericDialog *, gpointer data)
571 auto ed = static_cast<EditorData *>(data);
574 editor_verbose_window_progress(ed, _("stopping..."));
577 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
579 vd->gd->cancel_cb = editor_verbose_window_close;
581 gtk_spinner_stop(GTK_SPINNER(vd->spinner));
582 gtk_widget_set_sensitive(vd->button_stop, FALSE);
583 gtk_widget_set_sensitive(vd->button_close, TRUE);
586 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
588 EditorVerboseData *vd;
593 vd = g_new0(EditorVerboseData, 1);
595 vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
598 buf = g_strdup_printf(_("Output of %s"), text);
599 generic_dialog_add_message(vd->gd, nullptr, buf, nullptr, FALSE);
601 vd->button_stop = generic_dialog_add_button(vd->gd, GQ_ICON_STOP, nullptr,
602 editor_verbose_window_stop, FALSE);
603 gtk_widget_set_sensitive(vd->button_stop, FALSE);
604 vd->button_close = generic_dialog_add_button(vd->gd, GQ_ICON_CLOSE, _("Close"),
605 editor_verbose_window_close, TRUE);
606 gtk_widget_set_sensitive(vd->button_close, FALSE);
608 scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
609 gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
610 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
611 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
612 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
613 gtk_widget_show(scrolled);
615 vd->text = gtk_text_view_new();
616 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
617 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
618 gq_gtk_container_add(GTK_WIDGET(scrolled), vd->text);
619 gtk_widget_show(vd->text);
621 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
622 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
623 gtk_widget_show(hbox);
625 vd->progress = gtk_progress_bar_new();
626 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
627 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
628 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
629 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
630 gtk_widget_show(vd->progress);
632 vd->spinner = gtk_spinner_new();
633 gtk_spinner_start(GTK_SPINNER(vd->spinner));
634 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
635 gtk_widget_show(vd->spinner);
637 gtk_widget_show(vd->gd->dialog);
643 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
645 GtkTextBuffer *buffer;
648 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
649 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
650 gtk_text_buffer_insert(buffer, &iter, text, len);
653 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
659 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
662 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
665 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
667 auto ed = static_cast<EditorData *>(data);
671 if (condition & G_IO_IN)
673 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
675 if (!g_utf8_validate(buf, count, nullptr))
679 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
682 editor_verbose_window_fill(ed->vd, utf8, -1);
687 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
692 editor_verbose_window_fill(ed->vd, buf, count);
697 if (condition & (G_IO_ERR | G_IO_HUP))
699 g_io_channel_shutdown(source, TRUE, nullptr);
713 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
716 const gchar *p = nullptr;
718 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
720 if (type == PATH_FILE || type == PATH_FILE_URL)
722 GList *work = editor->ext_list;
731 auto ext = static_cast<gchar *>(work->data);
734 if (strcmp(ext, "*") == 0 ||
735 g_ascii_strcasecmp(ext, fd->extension) == 0)
741 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
744 auto sfd = static_cast<FileData *>(work2->data);
747 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
755 if (!p) return nullptr;
758 else if (type == PATH_DEST)
760 if (fd->change && fd->change->dest)
761 p = fd->change->dest;
768 GString *string = g_string_new(p);
769 if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
770 pathl = path_from_utf8(string->str);
771 g_string_free(string, TRUE);
773 if (pathl && !pathl[0]) /* empty string case */
779 DEBUG_2("editor_command_path_parse: return %s", pathl);
783 struct CommandBuilder
789 g_string_free(str, TRUE);
796 str = g_string_new("");
799 void append(const gchar *val)
803 str = g_string_append(str, val);
806 void append_c(gchar c)
810 str = g_string_append_c(str, c);
813 void append_quoted(const char *s, gboolean single_quotes, gboolean double_quotes)
820 str = g_string_append_c(str, '\'');
822 str = g_string_append(str, "\"'");
825 for (const char *p = s; *p != '\0'; p++)
828 str = g_string_append(str, "'\\''");
830 str = g_string_append_c(str, *p);
836 str = g_string_append_c(str, '\'');
838 str = g_string_append(str, "'\"");
844 if (!str) return nullptr;
846 auto command = g_string_free(str, FALSE);
852 GString *str{nullptr};
856 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
858 auto flags = static_cast<EditorFlags>(0);
860 CommandBuilder result;
861 gboolean escape = FALSE;
862 gboolean single_quotes = FALSE;
863 gboolean double_quotes = FALSE;
865 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
873 if (editor->exec == nullptr || editor->exec[0] == '\0')
875 return static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
879 /* skip leading whitespaces if any */
880 while (g_ascii_isspace(*p)) p++;
893 if (!single_quotes) escape = TRUE;
899 if (!single_quotes && !double_quotes)
900 single_quotes = TRUE;
901 else if (single_quotes)
902 single_quotes = FALSE;
907 if (!single_quotes && !double_quotes)
908 double_quotes = TRUE;
909 else if (double_quotes)
910 double_quotes = FALSE;
912 else if (*p == '%' && p[1])
914 gchar *pathl = nullptr;
920 case 'f': /* single file */
921 case 'u': /* single url */
922 flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
923 if (flags & EDITOR_SINGLE_COMMAND)
925 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
929 /* use the first file from the list */
932 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
934 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
936 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
940 /* just testing, check also the rest of the list (like with F and U)
941 any matching file is OK */
942 GList *work = list->next;
944 while (!pathl && work)
946 auto fd = static_cast<FileData *>(work->data);
947 pathl = editor_command_path_parse(fd,
949 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
957 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
959 result.append_quoted(pathl, single_quotes, double_quotes);
966 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
967 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
969 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
980 auto fd = static_cast<FileData *>(work->data);
981 pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
988 result.append_c(' ');
990 result.append_quoted(pathl, single_quotes, double_quotes);
997 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
1002 if (editor->icon && *editor->icon)
1004 result.append("--icon ");
1005 result.append_quoted(editor->icon, single_quotes, double_quotes);
1009 result.append_quoted(editor->name, single_quotes, double_quotes);
1012 result.append_quoted(editor->file, single_quotes, double_quotes);
1015 /* %% = % escaping */
1016 result.append_c(*p);
1024 /* deprecated according to spec, ignore */
1027 return static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1032 result.append_c(*p);
1037 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1041 *output = result.get_command();
1042 DEBUG_3("Editor cmd: %s", *output);
1049 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1051 auto ed = static_cast<EditorData *>(data);
1052 g_spawn_close_pid(pid);
1055 editor_command_next_finish(ed, status);
1059 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1062 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1064 gint standard_output;
1065 gint standard_error;
1069 ed->flags = editor->flags;
1070 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1072 ok = !EDITOR_ERRORS(ed->flags);
1076 ok = (options->shell.path && *options->shell.path);
1077 if (!ok) log_printf("ERROR: empty shell command\n");
1081 ok = (access(options->shell.path, X_OK) == 0);
1082 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1085 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1090 gchar *working_directory;
1094 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1095 args[n++] = options->shell.path;
1096 if (options->shell.options && *options->shell.options)
1097 args[n++] = options->shell.options;
1098 args[n++] = command;
1101 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1103 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1107 g_unsetenv("GEEQIE_DESTINATION");
1110 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1111 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1115 ed->vd ? &standard_output : nullptr,
1116 ed->vd ? &standard_error : nullptr,
1119 g_free(working_directory);
1121 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1126 g_child_watch_add(pid, editor_child_exit_cb, ed);
1136 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1137 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1143 GIOChannel *channel_output;
1144 GIOChannel *channel_error;
1146 channel_output = g_io_channel_unix_new(standard_output);
1147 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1148 g_io_channel_set_encoding(channel_output, nullptr, nullptr);
1150 g_io_add_watch_full(channel_output, 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_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1153 editor_verbose_io_cb, ed, nullptr);
1154 g_io_channel_unref(channel_output);
1156 channel_error = g_io_channel_unix_new(standard_error);
1157 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1158 g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1160 g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1161 editor_verbose_io_cb, ed, nullptr);
1162 g_io_channel_unref(channel_error);
1168 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1171 static EditorFlags editor_command_next_start(EditorData *ed)
1173 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1175 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1180 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1184 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1185 editor_verbose_window_progress(ed, fd->path);
1187 editor_verbose_window_progress(ed, _("running..."));
1191 error = editor_command_one(ed->editor, ed->list, ed);
1192 if (!error && ed->vd)
1194 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1195 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1197 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1198 editor_verbose_window_fill(ed->vd, "\n", 1);
1203 return static_cast<EditorFlags>(0);
1205 /* command was not started, call the finish immediately */
1206 return editor_command_next_finish(ed, 0);
1209 /* everything is done */
1210 return editor_command_done(ed);
1213 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1215 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1218 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1220 if (ed->flags & EDITOR_FOR_EACH)
1222 /* handle the first element from the list */
1223 GList *fd_element = ed->list;
1225 ed->list = g_list_remove_link(ed->list, fd_element);
1228 cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1229 if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1231 filelist_free(fd_element);
1235 /* handle whole list */
1237 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1238 filelist_free(ed->list);
1244 case EDITOR_CB_SUSPEND:
1245 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1246 case EDITOR_CB_SKIP:
1247 return editor_command_done(ed);
1250 return editor_command_next_start(ed);
1253 static EditorFlags editor_command_done(EditorData *ed)
1259 if (ed->count == ed->total)
1261 editor_verbose_window_progress(ed, _("done"));
1265 editor_verbose_window_progress(ed, _("stopped by user"));
1267 editor_verbose_window_enable_close(ed->vd);
1270 /* free the not-handled items */
1273 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1274 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1275 filelist_free(ed->list);
1281 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1283 if (!ed->vd) editor_data_free(ed);
1288 void editor_resume(gpointer ed)
1290 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1293 void editor_skip(gpointer ed)
1295 editor_command_done(static_cast<EditorData *>(ed));
1298 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1301 EditorFlags flags = editor->flags;
1303 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1305 ed = g_new0(EditorData, 1);
1306 ed->list = filelist_copy(list);
1308 ed->editor = editor;
1309 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1312 ed->working_directory = g_strdup(working_directory);
1314 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1315 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1317 if (flags & EDITOR_VERBOSE)
1318 editor_verbose_window(ed, text);
1320 editor_command_next_start(ed);
1321 /* errors from editor_command_next_start will be handled via callback */
1322 return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1325 gboolean is_valid_editor_command(const gchar *key)
1327 if (!key) return FALSE;
1328 return g_hash_table_lookup(editors, key) != nullptr;
1331 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1334 EditorDescription *editor;
1335 if (!key) return EDITOR_ERROR_EMPTY;
1337 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1339 if (!editor) return EDITOR_ERROR_EMPTY;
1340 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1342 error = editor_command_parse(editor, list, TRUE, nullptr);
1344 if (EDITOR_ERRORS(error)) return error;
1346 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1348 if (EDITOR_ERRORS(error))
1350 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1352 file_util_warning_dialog(_("Invalid editor command"), text, GQ_ICON_DIALOG_ERROR, nullptr);
1356 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1359 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1361 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1364 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1369 if (!fd) return static_cast<EditorFlags>(FALSE);
1371 list = g_list_append(nullptr, fd);
1372 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1377 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1379 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1382 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1384 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1387 gboolean editor_window_flag_set(const gchar *key)
1389 EditorDescription *editor;
1390 if (!key) return TRUE;
1392 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1393 if (!editor) return TRUE;
1395 return !!(editor->flags & EDITOR_KEEP_FS);
1398 gboolean editor_is_filter(const gchar *key)
1400 EditorDescription *editor;
1401 if (!key) return TRUE;
1403 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1404 if (!editor) return TRUE;
1406 return !!(editor->flags & EDITOR_DEST);
1409 gboolean editor_no_param(const gchar *key)
1411 EditorDescription *editor;
1412 if (!key) return FALSE;
1414 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1415 if (!editor) return FALSE;
1417 return !!(editor->flags & EDITOR_NO_PARAM);
1420 gboolean editor_blocks_file(const gchar *key)
1422 EditorDescription *editor;
1423 if (!key) return FALSE;
1425 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1426 if (!editor) return FALSE;
1428 /* Decide if the image file should be blocked during editor execution
1429 Editors like gimp can be used long time after the original file was
1430 saved, for editing unrelated files.
1431 %f vs. %F seems to be a good heuristic to detect this kind of editors.
1434 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1437 const gchar *editor_get_error_str(EditorFlags flags)
1439 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1440 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1441 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1442 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1443 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1444 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1445 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1446 return _("Unknown error.");
1449 #pragma GCC diagnostic push
1450 #pragma GCC diagnostic ignored "-Wunused-function"
1451 const gchar *editor_get_name_unused(const gchar *key)
1453 auto *editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1455 if (!editor) return nullptr;
1457 return editor->name;
1459 #pragma GCC diagnostic pop
1461 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */