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"
31 #define EDITOR_WINDOW_WIDTH 500
32 #define EDITOR_WINDOW_HEIGHT 300
36 struct EditorVerboseData {
38 GtkWidget *button_close;
39 GtkWidget *button_stop;
52 EditorVerboseData *vd;
53 EditorCallback callback;
55 const EditorDescription *editor;
56 gchar *working_directory; /* fallback if no files are given (editor_no_param) */
60 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
61 static EditorFlags editor_command_next_start(EditorData *ed);
62 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
63 static EditorFlags editor_command_done(EditorData *ed);
66 *-----------------------------------------------------------------------------
67 * external editor routines
68 *-----------------------------------------------------------------------------
71 GHashTable *editors = nullptr;
72 GtkListStore *desktop_file_list;
73 gboolean editors_finished = FALSE;
75 #ifdef G_KEY_FILE_DESKTOP_GROUP
76 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
78 #define DESKTOP_GROUP "Desktop Entry"
81 void editor_description_free(EditorDescription *editor)
89 g_free(editor->menu_path);
90 g_free(editor->hotkey);
91 g_free(editor->comment);
92 g_list_free_full(editor->ext_list, g_free);
97 static GList *editor_mime_types_to_extensions(gchar **mime_types)
99 /** @FIXME this should be rewritten to use the shared mime database, as soon as we switch to gio */
101 static constexpr const gchar *conv_table[][2] = {
103 {"image/bmp", ".bmp"},
104 {"image/gif", ".gif"},
105 {"image/heic", ".heic"},
106 {"image/jpeg", ".jpeg;.jpg;.mpo"},
107 {"image/jpg", ".jpg;.jpeg"},
108 {"image/jxl", ".jxl"},
109 {"image/webp", ".webp"},
110 {"image/pcx", ".pcx"},
111 {"image/png", ".png"},
112 {"image/svg", ".svg"},
113 {"image/svg+xml", ".svg"},
114 {"image/svg+xml-compressed", ".svg"},
115 {"image/tiff", ".tiff;.tif;.mef"},
116 {"image/vnd-ms.dds", ".dds"},
117 {"image/x-adobe-dng", ".dng"},
118 {"image/x-bmp", ".bmp"},
119 {"image/x-canon-crw", ".crw"},
120 {"image/x-canon-cr2", ".cr2"},
121 {"image/x-canon-cr3", ".cr3"},
122 {"image/x-cr2", ".cr2"},
123 {"image/x-dcraw", "%raw;.mos"},
124 {"image/x-epson-erf", "%erf"},
125 {"image/x-ico", ".ico"},
126 {"image/x-kodak-kdc", ".kdc"},
127 {"image/x-mrw", ".mrw"},
128 {"image/x-minolta-mrw", ".mrw"},
129 {"image/x-MS-bmp", ".bmp"},
130 {"image/x-nef", ".nef"},
131 {"image/x-nikon-nef", ".nef"},
132 {"image/x-panasonic-raw", ".raw"},
133 {"image/x-panasonic-rw2", ".rw2"},
134 {"image/x-pentax-pef", ".pef"},
135 {"image/x-orf", ".orf"},
136 {"image/x-olympus-orf", ".orf"},
137 {"image/x-pcx", ".pcx"},
138 {"image/xpm", ".xpm"},
139 {"image/x-png", ".png"},
140 {"image/x-portable-anymap", ".pam"},
141 {"image/x-portable-bitmap", ".pbm"},
142 {"image/x-portable-graymap", ".pgm"},
143 {"image/x-portable-pixmap", ".ppm"},
144 {"image/x-psd", ".psd"},
145 {"image/x-raf", ".raf"},
146 {"image/x-fuji-raf", ".raf"},
147 {"image/x-sgi", ".sgi"},
148 {"image/x-sony-arw", ".arw"},
149 {"image/x-sony-sr2", ".sr2"},
150 {"image/x-sony-srf", ".srf"},
151 {"image/x-tga", ".tga"},
152 {"image/x-xbitmap", ".xbm"},
153 {"image/x-xcf", ".xcf"},
154 {"image/x-xpixmap", ".xpm"},
155 {"application/x-navi-animation", ".ani"},
156 {"application/x-ptoptimizer-script", ".pto"},
160 GList *list = nullptr;
162 for (i = 0; mime_types[i]; i++)
163 for (const auto& c : conv_table)
164 if (strcmp(mime_types[i], c[0]) == 0)
165 list = g_list_concat(list, filter_to_list(c[1]));
170 gboolean editor_read_desktop_file(const gchar *path)
173 EditorDescription *editor;
176 const gchar *key = filename_from_path(path);
177 gchar **categories, **only_show_in, **not_show_in;
180 gboolean category_geeqie = FALSE;
184 if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
186 key_file = g_key_file_new();
187 if (!g_key_file_load_from_file(key_file, path, static_cast<GKeyFileFlags>(0), nullptr))
189 g_key_file_free(key_file);
193 type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", nullptr);
194 if (!type || strcmp(type, "Application") != 0)
196 /* We only consider desktop entries of Application type */
197 g_key_file_free(key_file);
203 editor = g_new0(EditorDescription, 1);
205 editor->key = g_strdup(key);
206 editor->file = g_strdup(path);
208 g_hash_table_insert(editors, editor->key, editor);
210 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", nullptr)
211 || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", nullptr))
213 editor->hidden = TRUE;
216 categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", nullptr, nullptr);
219 gboolean found = FALSE;
221 for (i = 0; categories[i]; i++)
223 /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
224 if (strcmp(categories[i], "Graphics") == 0)
228 if (strcmp(categories[i], "X-Geeqie") == 0)
231 category_geeqie = TRUE;
235 if (!found) editor->ignored = TRUE;
236 g_strfreev(categories);
240 editor->ignored = TRUE;
243 only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", nullptr, nullptr);
246 gboolean found = FALSE;
248 for (i = 0; only_show_in[i]; i++)
249 if (strcmp(only_show_in[i], "X-Geeqie") == 0)
254 if (!found) editor->ignored = TRUE;
255 g_strfreev(only_show_in);
258 not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", nullptr, nullptr);
261 gboolean found = FALSE;
263 for (i = 0; not_show_in[i]; i++)
264 if (strcmp(not_show_in[i], "X-Geeqie") == 0)
269 if (found) editor->ignored = TRUE;
270 g_strfreev(not_show_in);
274 try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", nullptr);
275 if (try_exec && !editor->hidden && !editor->ignored)
277 gchar *try_exec_res = g_find_program_in_path(try_exec);
278 if (!try_exec_res) editor->hidden = TRUE;
279 g_free(try_exec_res);
285 /* ignored editors will be deleted, no need to parse the rest */
286 g_key_file_free(key_file);
290 editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", nullptr, nullptr);
291 editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", nullptr);
293 /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
294 if (editor->icon && !g_path_is_absolute(editor->icon))
296 gchar *ext = strrchr(editor->icon, '.');
298 if (ext && strlen(ext) == 4 &&
299 (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
301 log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
302 editor->file, editor->icon);
308 if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
310 g_free(editor->icon);
311 editor->icon = nullptr;
314 editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", nullptr);
316 editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", nullptr);
317 if (!editor->menu_path) editor->menu_path = g_strdup("PluginsMenu");
319 editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", nullptr);
321 editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", nullptr);
323 extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", nullptr);
325 editor->ext_list = filter_to_list(extensions);
328 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", nullptr, nullptr);
331 editor->ext_list = editor_mime_types_to_extensions(mime_types);
332 g_strfreev(mime_types);
333 if (!editor->ext_list) editor->hidden = TRUE;
337 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);
338 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_VERBOSE);
339 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);
340 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_DEST);
341 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_TERMINAL);
343 editor->flags = static_cast<EditorFlags>(editor->flags | editor_command_parse(editor, nullptr, FALSE, nullptr));
345 if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
347 g_key_file_free(key_file);
349 if (editor->ignored) return TRUE;
351 work = options->disabled_plugins;
356 if (g_strcmp0(path, static_cast<const gchar *>(work->data)) == 0)
364 editor->disabled = disabled;
366 gtk_list_store_append(desktop_file_list, &iter);
367 gtk_list_store_set(desktop_file_list, &iter,
368 DESKTOP_FILE_COLUMN_KEY, key,
369 DESKTOP_FILE_COLUMN_DISABLED, editor->disabled,
370 DESKTOP_FILE_COLUMN_NAME, editor->name,
371 DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden ? _("yes") : _("no"),
372 DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
373 DESKTOP_FILE_COLUMN_PATH, path, -1);
378 static gboolean editor_remove_desktop_file_cb(gpointer, gpointer value, gpointer)
380 auto editor = static_cast<EditorDescription *>(value);
381 return editor->hidden || editor->ignored;
384 void editor_table_finish()
386 g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, nullptr);
387 editors_finished = TRUE;
390 void editor_table_clear()
392 if (desktop_file_list)
394 gtk_list_store_clear(desktop_file_list);
398 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);
402 g_hash_table_destroy(editors);
404 editors = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(editor_description_free));
405 editors_finished = FALSE;
408 static GList *editor_add_desktop_dir(GList *list, const gchar *path)
414 pathl = path_from_utf8(path);
422 while ((dir = readdir(dp)) != nullptr)
424 gchar *namel = dir->d_name;
426 if (g_str_has_suffix(namel, ".desktop"))
428 gchar *name = path_to_utf8(namel);
429 gchar *dpath = g_build_filename(path, name, NULL);
430 list = g_list_prepend(list, dpath);
438 GList *editor_get_desktop_files()
441 gchar *xdg_data_dirs;
445 GList *list = nullptr;
447 xdg_data_dirs = getenv("XDG_DATA_DIRS");
448 if (xdg_data_dirs && xdg_data_dirs[0])
449 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
451 xdg_data_dirs = g_strdup("/usr/share");
453 all_dirs = g_strconcat(get_rc_dir(), ":", gq_appdir, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
455 g_free(xdg_data_dirs);
457 split_dirs = g_strsplit(all_dirs, ":", 0);
461 for (i = 0; split_dirs[i]; i++);
462 for (--i; i >= 0; i--)
464 path = g_build_filename(split_dirs[i], "applications", NULL);
465 list = editor_add_desktop_dir(list, path);
469 g_strfreev(split_dirs);
473 static void editor_list_add_cb(gpointer, gpointer value, gpointer data)
475 auto listp = static_cast<GList **>(data);
476 auto editor = static_cast<EditorDescription *>(value);
478 /* do not show the special commands in any list, they are called explicitly */
479 if (strcmp(editor->key, CMD_COPY) == 0 ||
480 strcmp(editor->key, CMD_MOVE) == 0 ||
481 strcmp(editor->key, CMD_RENAME) == 0 ||
482 strcmp(editor->key, CMD_DELETE) == 0 ||
483 strcmp(editor->key, CMD_FOLDER) == 0) return;
485 if (editor->disabled)
490 *listp = g_list_prepend(*listp, editor);
493 static gint editor_sort(gconstpointer a, gconstpointer b)
495 auto ea = static_cast<const EditorDescription *>(a);
496 auto eb = static_cast<const EditorDescription *>(b);
497 gchar *caseless_name_ea;
498 gchar *caseless_name_eb;
499 gchar *collate_key_ea;
500 gchar *collate_key_eb;
503 ret = strcmp(ea->menu_path, eb->menu_path);
504 if (ret != 0) return ret;
506 caseless_name_ea = g_utf8_casefold(ea->name, -1);
507 caseless_name_eb = g_utf8_casefold(eb->name, -1);
508 collate_key_ea = g_utf8_collate_key_for_filename(caseless_name_ea, -1);
509 collate_key_eb = g_utf8_collate_key_for_filename(caseless_name_eb, -1);
510 ret = g_strcmp0(collate_key_ea, collate_key_eb);
512 g_free(collate_key_ea);
513 g_free(collate_key_eb);
514 g_free(caseless_name_ea);
515 g_free(caseless_name_eb);
520 GList *editor_list_get()
522 GList *editors_list = nullptr;
524 if (!editors_finished) return nullptr;
526 g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
527 editors_list = g_list_sort(editors_list, editor_sort);
532 /* ------------------------------ */
535 static void editor_verbose_data_free(EditorData *ed)
542 static void editor_data_free(EditorData *ed)
544 editor_verbose_data_free(ed);
545 g_free(ed->working_directory);
549 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
551 auto ed = static_cast<EditorData *>(data);
553 generic_dialog_close(gd);
554 editor_verbose_data_free(ed);
555 if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
558 static void editor_verbose_window_stop(GenericDialog *, gpointer data)
560 auto ed = static_cast<EditorData *>(data);
563 editor_verbose_window_progress(ed, _("stopping..."));
566 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
568 vd->gd->cancel_cb = editor_verbose_window_close;
570 gtk_spinner_stop(GTK_SPINNER(vd->spinner));
571 gtk_widget_set_sensitive(vd->button_stop, FALSE);
572 gtk_widget_set_sensitive(vd->button_close, TRUE);
575 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
577 EditorVerboseData *vd;
582 vd = g_new0(EditorVerboseData, 1);
584 vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
587 buf = g_strdup_printf(_("Output of %s"), text);
588 generic_dialog_add_message(vd->gd, nullptr, buf, nullptr, FALSE);
590 vd->button_stop = generic_dialog_add_button(vd->gd, GQ_ICON_STOP, nullptr,
591 editor_verbose_window_stop, FALSE);
592 gtk_widget_set_sensitive(vd->button_stop, FALSE);
593 vd->button_close = generic_dialog_add_button(vd->gd, GQ_ICON_CLOSE, _("Close"),
594 editor_verbose_window_close, TRUE);
595 gtk_widget_set_sensitive(vd->button_close, FALSE);
597 scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
598 gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
599 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
600 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
601 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
602 gtk_widget_show(scrolled);
604 vd->text = gtk_text_view_new();
605 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
606 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
607 gq_gtk_container_add(GTK_WIDGET(scrolled), vd->text);
608 gtk_widget_show(vd->text);
610 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
611 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
612 gtk_widget_show(hbox);
614 vd->progress = gtk_progress_bar_new();
615 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
616 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
617 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
618 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
619 gtk_widget_show(vd->progress);
621 vd->spinner = gtk_spinner_new();
622 gtk_spinner_start(GTK_SPINNER(vd->spinner));
623 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
624 gtk_widget_show(vd->spinner);
626 gtk_widget_show(vd->gd->dialog);
632 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
634 GtkTextBuffer *buffer;
637 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
638 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
639 gtk_text_buffer_insert(buffer, &iter, text, len);
642 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
648 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
651 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
654 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
656 auto ed = static_cast<EditorData *>(data);
660 if (condition & G_IO_IN)
662 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
664 if (!g_utf8_validate(buf, count, nullptr))
668 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
671 editor_verbose_window_fill(ed->vd, utf8, -1);
676 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
681 editor_verbose_window_fill(ed->vd, buf, count);
686 if (condition & (G_IO_ERR | G_IO_HUP))
688 g_io_channel_shutdown(source, TRUE, nullptr);
702 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
705 const gchar *p = nullptr;
707 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
709 if (type == PATH_FILE || type == PATH_FILE_URL)
711 GList *work = editor->ext_list;
720 auto ext = static_cast<gchar *>(work->data);
723 if (strcmp(ext, "*") == 0 ||
724 g_ascii_strcasecmp(ext, fd->extension) == 0)
730 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
733 auto sfd = static_cast<FileData *>(work2->data);
736 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
744 if (!p) return nullptr;
747 else if (type == PATH_DEST)
749 if (fd->change && fd->change->dest)
750 p = fd->change->dest;
757 GString *string = g_string_new(p);
758 if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
759 pathl = path_from_utf8(string->str);
760 g_string_free(string, TRUE);
762 if (pathl && !pathl[0]) /* empty string case */
768 DEBUG_2("editor_command_path_parse: return %s", pathl);
772 struct CommandBuilder
778 g_string_free(str, TRUE);
785 str = g_string_new("");
788 void append(const gchar *val)
792 str = g_string_append(str, val);
795 void append_c(gchar c)
799 str = g_string_append_c(str, c);
802 void append_quoted(const char *s, gboolean single_quotes, gboolean double_quotes)
809 str = g_string_append_c(str, '\'');
811 str = g_string_append(str, "\"'");
814 for (const char *p = s; *p != '\0'; p++)
817 str = g_string_append(str, "'\\''");
819 str = g_string_append_c(str, *p);
825 str = g_string_append_c(str, '\'');
827 str = g_string_append(str, "'\"");
833 if (!str) return nullptr;
835 auto command = g_string_free(str, FALSE);
841 GString *str{nullptr};
845 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
847 auto flags = static_cast<EditorFlags>(0);
849 CommandBuilder result;
850 gboolean escape = FALSE;
851 gboolean single_quotes = FALSE;
852 gboolean double_quotes = FALSE;
854 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
862 if (editor->exec == nullptr || editor->exec[0] == '\0')
864 return static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
868 /* skip leading whitespaces if any */
869 while (g_ascii_isspace(*p)) p++;
882 if (!single_quotes) escape = TRUE;
888 if (!single_quotes && !double_quotes)
889 single_quotes = TRUE;
890 else if (single_quotes)
891 single_quotes = FALSE;
896 if (!single_quotes && !double_quotes)
897 double_quotes = TRUE;
898 else if (double_quotes)
899 double_quotes = FALSE;
901 else if (*p == '%' && p[1])
903 gchar *pathl = nullptr;
909 case 'f': /* single file */
910 case 'u': /* single url */
911 flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
912 if (flags & EDITOR_SINGLE_COMMAND)
914 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
918 /* use the first file from the list */
921 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
923 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
925 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
929 /* just testing, check also the rest of the list (like with F and U)
930 any matching file is OK */
931 GList *work = list->next;
933 while (!pathl && work)
935 auto fd = static_cast<FileData *>(work->data);
936 pathl = editor_command_path_parse(fd,
938 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
946 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
948 result.append_quoted(pathl, single_quotes, double_quotes);
955 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
956 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
958 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
969 auto fd = static_cast<FileData *>(work->data);
970 pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
977 result.append_c(' ');
979 result.append_quoted(pathl, single_quotes, double_quotes);
986 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
991 if (editor->icon && *editor->icon)
993 result.append("--icon ");
994 result.append_quoted(editor->icon, single_quotes, double_quotes);
998 result.append_quoted(editor->name, single_quotes, double_quotes);
1001 result.append_quoted(editor->file, single_quotes, double_quotes);
1004 /* %% = % escaping */
1005 result.append_c(*p);
1013 /* deprecated according to spec, ignore */
1016 return static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1021 result.append_c(*p);
1026 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1030 *output = result.get_command();
1031 DEBUG_3("Editor cmd: %s", *output);
1038 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1040 auto ed = static_cast<EditorData *>(data);
1041 g_spawn_close_pid(pid);
1044 editor_command_next_finish(ed, status);
1048 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1051 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1053 gint standard_output;
1054 gint standard_error;
1058 ed->flags = editor->flags;
1059 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1061 ok = !EDITOR_ERRORS(ed->flags);
1065 ok = (options->shell.path && *options->shell.path);
1066 if (!ok) log_printf("ERROR: empty shell command\n");
1070 ok = (access(options->shell.path, X_OK) == 0);
1071 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1074 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1079 gchar *working_directory;
1083 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1084 args[n++] = options->shell.path;
1085 if (options->shell.options && *options->shell.options)
1086 args[n++] = options->shell.options;
1087 args[n++] = command;
1090 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1092 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1096 g_unsetenv("GEEQIE_DESTINATION");
1099 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1100 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1104 ed->vd ? &standard_output : nullptr,
1105 ed->vd ? &standard_error : nullptr,
1108 g_free(working_directory);
1110 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1115 g_child_watch_add(pid, editor_child_exit_cb, ed);
1125 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1126 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1132 GIOChannel *channel_output;
1133 GIOChannel *channel_error;
1135 channel_output = g_io_channel_unix_new(standard_output);
1136 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1137 g_io_channel_set_encoding(channel_output, nullptr, nullptr);
1139 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1140 editor_verbose_io_cb, ed, nullptr);
1141 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1142 editor_verbose_io_cb, ed, nullptr);
1143 g_io_channel_unref(channel_output);
1145 channel_error = g_io_channel_unix_new(standard_error);
1146 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1147 g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1149 g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1150 editor_verbose_io_cb, ed, nullptr);
1151 g_io_channel_unref(channel_error);
1157 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1160 static EditorFlags editor_command_next_start(EditorData *ed)
1162 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1164 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1169 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1173 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1174 editor_verbose_window_progress(ed, fd->path);
1176 editor_verbose_window_progress(ed, _("running..."));
1180 error = editor_command_one(ed->editor, ed->list, ed);
1181 if (!error && ed->vd)
1183 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1184 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1186 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1187 editor_verbose_window_fill(ed->vd, "\n", 1);
1192 return static_cast<EditorFlags>(0);
1194 /* command was not started, call the finish immediately */
1195 return editor_command_next_finish(ed, 0);
1198 /* everything is done */
1199 return editor_command_done(ed);
1202 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1204 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1207 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1209 if (ed->flags & EDITOR_FOR_EACH)
1211 /* handle the first element from the list */
1212 GList *fd_element = ed->list;
1214 ed->list = g_list_remove_link(ed->list, fd_element);
1217 cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1218 if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1220 filelist_free(fd_element);
1224 /* handle whole list */
1226 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1227 filelist_free(ed->list);
1233 case EDITOR_CB_SUSPEND:
1234 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1235 case EDITOR_CB_SKIP:
1236 return editor_command_done(ed);
1239 return editor_command_next_start(ed);
1242 static EditorFlags editor_command_done(EditorData *ed)
1248 if (ed->count == ed->total)
1250 editor_verbose_window_progress(ed, _("done"));
1254 editor_verbose_window_progress(ed, _("stopped by user"));
1256 editor_verbose_window_enable_close(ed->vd);
1259 /* free the not-handled items */
1262 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1263 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1264 filelist_free(ed->list);
1270 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1272 if (!ed->vd) editor_data_free(ed);
1277 void editor_resume(gpointer ed)
1279 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1282 void editor_skip(gpointer ed)
1284 editor_command_done(static_cast<EditorData *>(ed));
1287 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1290 EditorFlags flags = editor->flags;
1292 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1294 ed = g_new0(EditorData, 1);
1295 ed->list = filelist_copy(list);
1297 ed->editor = editor;
1298 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1301 ed->working_directory = g_strdup(working_directory);
1303 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1304 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1306 if (flags & EDITOR_VERBOSE)
1307 editor_verbose_window(ed, text);
1309 editor_command_next_start(ed);
1310 /* errors from editor_command_next_start will be handled via callback */
1311 return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1314 gboolean is_valid_editor_command(const gchar *key)
1316 if (!key) return FALSE;
1317 return g_hash_table_lookup(editors, key) != nullptr;
1320 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1323 EditorDescription *editor;
1324 if (!key) return EDITOR_ERROR_EMPTY;
1326 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1328 if (!editor) return EDITOR_ERROR_EMPTY;
1329 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1331 error = editor_command_parse(editor, list, TRUE, nullptr);
1333 if (EDITOR_ERRORS(error)) return error;
1335 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1337 if (EDITOR_ERRORS(error))
1339 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1341 file_util_warning_dialog(_("Invalid editor command"), text, GQ_ICON_DIALOG_ERROR, nullptr);
1345 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1348 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1350 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1353 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1358 if (!fd) return static_cast<EditorFlags>(FALSE);
1360 list = g_list_append(nullptr, fd);
1361 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1366 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1368 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1371 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1373 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1376 gboolean editor_window_flag_set(const gchar *key)
1378 EditorDescription *editor;
1379 if (!key) return TRUE;
1381 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1382 if (!editor) return TRUE;
1384 return !!(editor->flags & EDITOR_KEEP_FS);
1387 gboolean editor_is_filter(const gchar *key)
1389 EditorDescription *editor;
1390 if (!key) return TRUE;
1392 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1393 if (!editor) return TRUE;
1395 return !!(editor->flags & EDITOR_DEST);
1398 gboolean editor_no_param(const gchar *key)
1400 EditorDescription *editor;
1401 if (!key) return FALSE;
1403 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1404 if (!editor) return FALSE;
1406 return !!(editor->flags & EDITOR_NO_PARAM);
1409 gboolean editor_blocks_file(const gchar *key)
1411 EditorDescription *editor;
1412 if (!key) return FALSE;
1414 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1415 if (!editor) return FALSE;
1417 /* Decide if the image file should be blocked during editor execution
1418 Editors like gimp can be used long time after the original file was
1419 saved, for editing unrelated files.
1420 %f vs. %F seems to be a good heuristic to detect this kind of editors.
1423 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1426 const gchar *editor_get_error_str(EditorFlags flags)
1428 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1429 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1430 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1431 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1432 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1433 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1434 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1435 return _("Unknown error.");
1438 #pragma GCC diagnostic push
1439 #pragma GCC diagnostic ignored "-Wunused-function"
1440 const gchar *editor_get_name_unused(const gchar *key)
1442 EditorDescription *editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1444 if (!editor) return NULL;
1446 return editor->name;
1448 #pragma GCC diagnostic pop
1450 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */