2 * Copyright (C) 2006 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 #include "filefilter.h"
27 #include "pixbuf-util.h"
28 #include "ui-fileops.h"
32 EDITOR_WINDOW_WIDTH = 500,
33 EDITOR_WINDOW_HEIGHT = 300
38 struct EditorVerboseData {
40 GtkWidget *button_close;
41 GtkWidget *button_stop;
54 EditorVerboseData *vd;
55 EditorCallback callback;
57 const EditorDescription *editor;
58 gchar *working_directory; /* fallback if no files are given (editor_no_param) */
62 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
63 static EditorFlags editor_command_next_start(EditorData *ed);
64 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
65 static EditorFlags editor_command_done(EditorData *ed);
68 *-----------------------------------------------------------------------------
69 * external editor routines
70 *-----------------------------------------------------------------------------
73 GHashTable *editors = nullptr;
74 GtkListStore *desktop_file_list;
75 gboolean editors_finished = FALSE;
77 #ifdef G_KEY_FILE_DESKTOP_GROUP
78 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
80 #define DESKTOP_GROUP "Desktop Entry"
83 void editor_description_free(EditorDescription *editor)
91 g_free(editor->menu_path);
92 g_free(editor->hotkey);
93 g_free(editor->comment);
94 g_list_free_full(editor->ext_list, g_free);
99 static GList *editor_mime_types_to_extensions(gchar **mime_types)
101 /** @FIXME this should be rewritten to use the shared mime database, as soon as we switch to gio */
103 static constexpr const gchar *conv_table[][2] = {
105 {"image/bmp", ".bmp"},
106 {"image/gif", ".gif"},
107 {"image/heic", ".heic"},
108 {"image/jpeg", ".jpeg;.jpg;.mpo"},
109 {"image/jpg", ".jpg;.jpeg"},
110 {"image/jxl", ".jxl"},
111 {"image/webp", ".webp"},
112 {"image/pcx", ".pcx"},
113 {"image/png", ".png"},
114 {"image/svg", ".svg"},
115 {"image/svg+xml", ".svg"},
116 {"image/svg+xml-compressed", ".svg"},
117 {"image/tiff", ".tiff;.tif;.mef"},
118 {"image/vnd-ms.dds", ".dds"},
119 {"image/x-adobe-dng", ".dng"},
120 {"image/x-bmp", ".bmp"},
121 {"image/x-canon-crw", ".crw"},
122 {"image/x-canon-cr2", ".cr2"},
123 {"image/x-canon-cr3", ".cr3"},
124 {"image/x-cr2", ".cr2"},
125 {"image/x-dcraw", "%raw;.mos"},
126 {"image/x-epson-erf", "%erf"},
127 {"image/x-ico", ".ico"},
128 {"image/x-kodak-kdc", ".kdc"},
129 {"image/x-mrw", ".mrw"},
130 {"image/x-minolta-mrw", ".mrw"},
131 {"image/x-MS-bmp", ".bmp"},
132 {"image/x-nef", ".nef"},
133 {"image/x-nikon-nef", ".nef"},
134 {"image/x-panasonic-raw", ".raw"},
135 {"image/x-panasonic-rw2", ".rw2"},
136 {"image/x-pentax-pef", ".pef"},
137 {"image/x-orf", ".orf"},
138 {"image/x-olympus-orf", ".orf"},
139 {"image/x-pcx", ".pcx"},
140 {"image/xpm", ".xpm"},
141 {"image/x-png", ".png"},
142 {"image/x-portable-anymap", ".pam"},
143 {"image/x-portable-bitmap", ".pbm"},
144 {"image/x-portable-graymap", ".pgm"},
145 {"image/x-portable-pixmap", ".ppm"},
146 {"image/x-psd", ".psd"},
147 {"image/x-raf", ".raf"},
148 {"image/x-fuji-raf", ".raf"},
149 {"image/x-sgi", ".sgi"},
150 {"image/x-sony-arw", ".arw"},
151 {"image/x-sony-sr2", ".sr2"},
152 {"image/x-sony-srf", ".srf"},
153 {"image/x-tga", ".tga"},
154 {"image/x-xbitmap", ".xbm"},
155 {"image/x-xcf", ".xcf"},
156 {"image/x-xpixmap", ".xpm"},
157 {"application/x-navi-animation", ".ani"},
158 {"application/x-ptoptimizer-script", ".pto"},
162 GList *list = nullptr;
164 for (i = 0; mime_types[i]; i++)
165 for (const auto& c : conv_table)
166 if (strcmp(mime_types[i], c[0]) == 0)
167 list = g_list_concat(list, filter_to_list(c[1]));
172 gboolean editor_read_desktop_file(const gchar *path)
175 EditorDescription *editor;
178 const gchar *key = filename_from_path(path);
179 gchar **categories, **only_show_in, **not_show_in;
182 gboolean category_geeqie = FALSE;
186 if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
188 key_file = g_key_file_new();
189 if (!g_key_file_load_from_file(key_file, path, static_cast<GKeyFileFlags>(0), nullptr))
191 g_key_file_free(key_file);
195 type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", nullptr);
196 if (!type || strcmp(type, "Application") != 0)
198 /* We only consider desktop entries of Application type */
199 g_key_file_free(key_file);
205 editor = g_new0(EditorDescription, 1);
207 editor->key = g_strdup(key);
208 editor->file = g_strdup(path);
210 g_hash_table_insert(editors, editor->key, editor);
212 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", nullptr)
213 || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", nullptr))
215 editor->hidden = TRUE;
218 categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", nullptr, nullptr);
221 gboolean found = FALSE;
223 for (i = 0; categories[i]; i++)
225 /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
226 if (strcmp(categories[i], "Graphics") == 0)
230 if (strcmp(categories[i], "X-Geeqie") == 0)
233 category_geeqie = TRUE;
237 if (!found) editor->ignored = TRUE;
238 g_strfreev(categories);
242 editor->ignored = TRUE;
245 only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", nullptr, nullptr);
248 gboolean found = FALSE;
250 for (i = 0; only_show_in[i]; i++)
251 if (strcmp(only_show_in[i], "X-Geeqie") == 0)
256 if (!found) editor->ignored = TRUE;
257 g_strfreev(only_show_in);
260 not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", nullptr, nullptr);
263 gboolean found = FALSE;
265 for (i = 0; not_show_in[i]; i++)
266 if (strcmp(not_show_in[i], "X-Geeqie") == 0)
271 if (found) editor->ignored = TRUE;
272 g_strfreev(not_show_in);
276 try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", nullptr);
277 if (try_exec && !editor->hidden && !editor->ignored)
279 gchar *try_exec_res = g_find_program_in_path(try_exec);
280 if (!try_exec_res) editor->hidden = TRUE;
281 g_free(try_exec_res);
287 /* ignored editors will be deleted, no need to parse the rest */
288 g_key_file_free(key_file);
292 editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", nullptr, nullptr);
293 editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", nullptr);
295 /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
296 if (editor->icon && !g_path_is_absolute(editor->icon))
298 gchar *ext = strrchr(editor->icon, '.');
300 if (ext && strlen(ext) == 4 &&
301 (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
303 log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
304 editor->file, editor->icon);
310 if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
312 g_free(editor->icon);
313 editor->icon = nullptr;
316 editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", nullptr);
318 editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", nullptr);
319 if (!editor->menu_path) editor->menu_path = g_strdup("PluginsMenu");
321 editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", nullptr);
323 editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", nullptr);
325 extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", nullptr);
327 editor->ext_list = filter_to_list(extensions);
330 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", nullptr, nullptr);
333 editor->ext_list = editor_mime_types_to_extensions(mime_types);
334 g_strfreev(mime_types);
335 if (!editor->ext_list) editor->hidden = TRUE;
339 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);
340 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_VERBOSE);
341 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);
342 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_DEST);
343 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_TERMINAL);
345 editor->flags = static_cast<EditorFlags>(editor->flags | editor_command_parse(editor, nullptr, FALSE, nullptr));
347 if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
349 g_key_file_free(key_file);
351 if (editor->ignored) return TRUE;
353 work = options->disabled_plugins;
358 if (g_strcmp0(path, static_cast<const gchar *>(work->data)) == 0)
366 editor->disabled = disabled;
368 gtk_list_store_append(desktop_file_list, &iter);
369 gtk_list_store_set(desktop_file_list, &iter,
370 DESKTOP_FILE_COLUMN_KEY, key,
371 DESKTOP_FILE_COLUMN_DISABLED, editor->disabled,
372 DESKTOP_FILE_COLUMN_NAME, editor->name,
373 DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden ? _("yes") : _("no"),
374 DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
375 DESKTOP_FILE_COLUMN_PATH, path, -1);
380 static gboolean editor_remove_desktop_file_cb(gpointer, gpointer value, gpointer)
382 auto editor = static_cast<EditorDescription *>(value);
383 return editor->hidden || editor->ignored;
386 void editor_table_finish()
388 g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, nullptr);
389 editors_finished = TRUE;
392 void editor_table_clear()
394 if (desktop_file_list)
396 gtk_list_store_clear(desktop_file_list);
400 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);
404 g_hash_table_destroy(editors);
406 editors = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(editor_description_free));
407 editors_finished = FALSE;
410 static GList *editor_add_desktop_dir(GList *list, const gchar *path)
416 pathl = path_from_utf8(path);
424 while ((dir = readdir(dp)) != nullptr)
426 gchar *namel = dir->d_name;
428 if (g_str_has_suffix(namel, ".desktop"))
430 gchar *name = path_to_utf8(namel);
431 gchar *dpath = g_build_filename(path, name, NULL);
432 list = g_list_prepend(list, dpath);
440 GList *editor_get_desktop_files()
443 gchar *xdg_data_dirs;
447 GList *list = nullptr;
449 xdg_data_dirs = getenv("XDG_DATA_DIRS");
450 if (xdg_data_dirs && xdg_data_dirs[0])
451 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
453 xdg_data_dirs = g_strdup("/usr/share");
455 all_dirs = g_strconcat(get_rc_dir(), ":", gq_appdir, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
457 g_free(xdg_data_dirs);
459 split_dirs = g_strsplit(all_dirs, ":", 0);
463 for (i = 0; split_dirs[i]; i++);
464 for (--i; i >= 0; i--)
466 path = g_build_filename(split_dirs[i], "applications", NULL);
467 list = editor_add_desktop_dir(list, path);
471 g_strfreev(split_dirs);
475 static void editor_list_add_cb(gpointer, gpointer value, gpointer data)
477 auto listp = static_cast<GList **>(data);
478 auto editor = static_cast<EditorDescription *>(value);
480 /* do not show the special commands in any list, they are called explicitly */
481 if (strcmp(editor->key, CMD_COPY) == 0 ||
482 strcmp(editor->key, CMD_MOVE) == 0 ||
483 strcmp(editor->key, CMD_RENAME) == 0 ||
484 strcmp(editor->key, CMD_DELETE) == 0 ||
485 strcmp(editor->key, CMD_FOLDER) == 0) return;
487 if (editor->disabled)
492 *listp = g_list_prepend(*listp, editor);
495 static gint editor_sort(gconstpointer a, gconstpointer b)
497 auto ea = static_cast<const EditorDescription *>(a);
498 auto eb = static_cast<const EditorDescription *>(b);
499 gchar *caseless_name_ea;
500 gchar *caseless_name_eb;
501 gchar *collate_key_ea;
502 gchar *collate_key_eb;
505 ret = strcmp(ea->menu_path, eb->menu_path);
506 if (ret != 0) return ret;
508 caseless_name_ea = g_utf8_casefold(ea->name, -1);
509 caseless_name_eb = g_utf8_casefold(eb->name, -1);
510 collate_key_ea = g_utf8_collate_key_for_filename(caseless_name_ea, -1);
511 collate_key_eb = g_utf8_collate_key_for_filename(caseless_name_eb, -1);
512 ret = g_strcmp0(collate_key_ea, collate_key_eb);
514 g_free(collate_key_ea);
515 g_free(collate_key_eb);
516 g_free(caseless_name_ea);
517 g_free(caseless_name_eb);
522 GList *editor_list_get()
524 GList *editors_list = nullptr;
526 if (!editors_finished) return nullptr;
528 g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
529 editors_list = g_list_sort(editors_list, editor_sort);
534 /* ------------------------------ */
537 static void editor_verbose_data_free(EditorData *ed)
544 static void editor_data_free(EditorData *ed)
546 editor_verbose_data_free(ed);
547 g_free(ed->working_directory);
551 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
553 auto ed = static_cast<EditorData *>(data);
555 generic_dialog_close(gd);
556 editor_verbose_data_free(ed);
557 if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
560 static void editor_verbose_window_stop(GenericDialog *, gpointer data)
562 auto ed = static_cast<EditorData *>(data);
565 editor_verbose_window_progress(ed, _("stopping..."));
568 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
570 vd->gd->cancel_cb = editor_verbose_window_close;
572 gtk_spinner_stop(GTK_SPINNER(vd->spinner));
573 gtk_widget_set_sensitive(vd->button_stop, FALSE);
574 gtk_widget_set_sensitive(vd->button_close, TRUE);
577 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
579 EditorVerboseData *vd;
584 vd = g_new0(EditorVerboseData, 1);
586 vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
589 buf = g_strdup_printf(_("Output of %s"), text);
590 generic_dialog_add_message(vd->gd, nullptr, buf, nullptr, FALSE);
592 vd->button_stop = generic_dialog_add_button(vd->gd, GQ_ICON_STOP, nullptr,
593 editor_verbose_window_stop, FALSE);
594 gtk_widget_set_sensitive(vd->button_stop, FALSE);
595 vd->button_close = generic_dialog_add_button(vd->gd, GQ_ICON_CLOSE, _("Close"),
596 editor_verbose_window_close, TRUE);
597 gtk_widget_set_sensitive(vd->button_close, FALSE);
599 scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
600 gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
601 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
602 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
603 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
604 gtk_widget_show(scrolled);
606 vd->text = gtk_text_view_new();
607 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
608 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
609 gq_gtk_container_add(GTK_WIDGET(scrolled), vd->text);
610 gtk_widget_show(vd->text);
612 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
613 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
614 gtk_widget_show(hbox);
616 vd->progress = gtk_progress_bar_new();
617 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
618 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
619 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
620 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
621 gtk_widget_show(vd->progress);
623 vd->spinner = gtk_spinner_new();
624 gtk_spinner_start(GTK_SPINNER(vd->spinner));
625 gq_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)
707 const gchar *p = nullptr;
709 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
711 if (type == PATH_FILE || type == PATH_FILE_URL)
713 GList *work = editor->ext_list;
722 auto ext = static_cast<gchar *>(work->data);
725 if (strcmp(ext, "*") == 0 ||
726 g_ascii_strcasecmp(ext, fd->extension) == 0)
732 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
735 auto sfd = static_cast<FileData *>(work2->data);
738 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
746 if (!p) return nullptr;
749 else if (type == PATH_DEST)
751 if (fd->change && fd->change->dest)
752 p = fd->change->dest;
759 GString *string = g_string_new(p);
760 if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
761 pathl = path_from_utf8(string->str);
762 g_string_free(string, TRUE);
764 if (pathl && !pathl[0]) /* empty string case */
770 DEBUG_2("editor_command_path_parse: return %s", pathl);
774 struct CommandBuilder
780 g_string_free(str, TRUE);
787 str = g_string_new("");
790 void append(const gchar *val)
794 str = g_string_append(str, val);
797 void append_c(gchar c)
801 str = g_string_append_c(str, c);
804 void append_quoted(const char *s, gboolean single_quotes, gboolean double_quotes)
811 str = g_string_append_c(str, '\'');
813 str = g_string_append(str, "\"'");
816 for (const char *p = s; *p != '\0'; p++)
819 str = g_string_append(str, "'\\''");
821 str = g_string_append_c(str, *p);
827 str = g_string_append_c(str, '\'');
829 str = g_string_append(str, "'\"");
835 if (!str) return nullptr;
837 auto command = g_string_free(str, FALSE);
843 GString *str{nullptr};
847 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
849 auto flags = static_cast<EditorFlags>(0);
851 CommandBuilder result;
852 gboolean escape = FALSE;
853 gboolean single_quotes = FALSE;
854 gboolean double_quotes = FALSE;
856 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
864 if (editor->exec == nullptr || editor->exec[0] == '\0')
866 return static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
870 /* skip leading whitespaces if any */
871 while (g_ascii_isspace(*p)) p++;
884 if (!single_quotes) escape = TRUE;
890 if (!single_quotes && !double_quotes)
891 single_quotes = TRUE;
892 else if (single_quotes)
893 single_quotes = FALSE;
898 if (!single_quotes && !double_quotes)
899 double_quotes = TRUE;
900 else if (double_quotes)
901 double_quotes = FALSE;
903 else if (*p == '%' && p[1])
905 gchar *pathl = nullptr;
911 case 'f': /* single file */
912 case 'u': /* single url */
913 flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
914 if (flags & EDITOR_SINGLE_COMMAND)
916 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
920 /* use the first file from the list */
923 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
925 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
927 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
931 /* just testing, check also the rest of the list (like with F and U)
932 any matching file is OK */
933 GList *work = list->next;
935 while (!pathl && work)
937 auto fd = static_cast<FileData *>(work->data);
938 pathl = editor_command_path_parse(fd,
940 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
948 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
950 result.append_quoted(pathl, single_quotes, double_quotes);
957 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
958 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
960 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
971 auto fd = static_cast<FileData *>(work->data);
972 pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
979 result.append_c(' ');
981 result.append_quoted(pathl, single_quotes, double_quotes);
988 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
993 if (editor->icon && *editor->icon)
995 result.append("--icon ");
996 result.append_quoted(editor->icon, single_quotes, double_quotes);
1000 result.append_quoted(editor->name, single_quotes, double_quotes);
1003 result.append_quoted(editor->file, single_quotes, double_quotes);
1006 /* %% = % escaping */
1007 result.append_c(*p);
1015 /* deprecated according to spec, ignore */
1018 return static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1023 result.append_c(*p);
1028 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1032 *output = result.get_command();
1033 DEBUG_3("Editor cmd: %s", *output);
1040 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1042 auto ed = static_cast<EditorData *>(data);
1043 g_spawn_close_pid(pid);
1046 editor_command_next_finish(ed, status);
1050 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1053 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1055 gint standard_output;
1056 gint standard_error;
1060 ed->flags = editor->flags;
1061 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1063 ok = !EDITOR_ERRORS(ed->flags);
1067 ok = (options->shell.path && *options->shell.path);
1068 if (!ok) log_printf("ERROR: empty shell command\n");
1072 ok = (access(options->shell.path, X_OK) == 0);
1073 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1076 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1081 gchar *working_directory;
1085 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1086 args[n++] = options->shell.path;
1087 if (options->shell.options && *options->shell.options)
1088 args[n++] = options->shell.options;
1089 args[n++] = command;
1092 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1094 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1098 g_unsetenv("GEEQIE_DESTINATION");
1101 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1102 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1106 ed->vd ? &standard_output : nullptr,
1107 ed->vd ? &standard_error : nullptr,
1110 g_free(working_directory);
1112 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1117 g_child_watch_add(pid, editor_child_exit_cb, ed);
1127 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1128 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1134 GIOChannel *channel_output;
1135 GIOChannel *channel_error;
1137 channel_output = g_io_channel_unix_new(standard_output);
1138 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1139 g_io_channel_set_encoding(channel_output, nullptr, 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_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1144 editor_verbose_io_cb, ed, nullptr);
1145 g_io_channel_unref(channel_output);
1147 channel_error = g_io_channel_unix_new(standard_error);
1148 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1149 g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1151 g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1152 editor_verbose_io_cb, ed, nullptr);
1153 g_io_channel_unref(channel_error);
1159 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1162 static EditorFlags editor_command_next_start(EditorData *ed)
1164 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1166 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1171 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1175 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1176 editor_verbose_window_progress(ed, fd->path);
1178 editor_verbose_window_progress(ed, _("running..."));
1182 error = editor_command_one(ed->editor, ed->list, ed);
1183 if (!error && ed->vd)
1185 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1186 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1188 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1189 editor_verbose_window_fill(ed->vd, "\n", 1);
1194 return static_cast<EditorFlags>(0);
1196 /* command was not started, call the finish immediately */
1197 return editor_command_next_finish(ed, 0);
1200 /* everything is done */
1201 return editor_command_done(ed);
1204 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1206 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1209 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1211 if (ed->flags & EDITOR_FOR_EACH)
1213 /* handle the first element from the list */
1214 GList *fd_element = ed->list;
1216 ed->list = g_list_remove_link(ed->list, fd_element);
1219 cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1220 if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1222 filelist_free(fd_element);
1226 /* handle whole list */
1228 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1229 filelist_free(ed->list);
1235 case EDITOR_CB_SUSPEND:
1236 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1237 case EDITOR_CB_SKIP:
1238 return editor_command_done(ed);
1241 return editor_command_next_start(ed);
1244 static EditorFlags editor_command_done(EditorData *ed)
1250 if (ed->count == ed->total)
1252 editor_verbose_window_progress(ed, _("done"));
1256 editor_verbose_window_progress(ed, _("stopped by user"));
1258 editor_verbose_window_enable_close(ed->vd);
1261 /* free the not-handled items */
1264 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1265 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1266 filelist_free(ed->list);
1272 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1274 if (!ed->vd) editor_data_free(ed);
1279 void editor_resume(gpointer ed)
1281 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1284 void editor_skip(gpointer ed)
1286 editor_command_done(static_cast<EditorData *>(ed));
1289 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1292 EditorFlags flags = editor->flags;
1294 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1296 ed = g_new0(EditorData, 1);
1297 ed->list = filelist_copy(list);
1299 ed->editor = editor;
1300 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1303 ed->working_directory = g_strdup(working_directory);
1305 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1306 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1308 if (flags & EDITOR_VERBOSE)
1309 editor_verbose_window(ed, text);
1311 editor_command_next_start(ed);
1312 /* errors from editor_command_next_start will be handled via callback */
1313 return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1316 gboolean is_valid_editor_command(const gchar *key)
1318 if (!key) return FALSE;
1319 return g_hash_table_lookup(editors, key) != nullptr;
1322 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1325 EditorDescription *editor;
1326 if (!key) return EDITOR_ERROR_EMPTY;
1328 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1330 if (!editor) return EDITOR_ERROR_EMPTY;
1331 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1333 error = editor_command_parse(editor, list, TRUE, nullptr);
1335 if (EDITOR_ERRORS(error)) return error;
1337 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1339 if (EDITOR_ERRORS(error))
1341 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1343 file_util_warning_dialog(_("Invalid editor command"), text, GQ_ICON_DIALOG_ERROR, nullptr);
1347 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1350 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1352 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1355 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1360 if (!fd) return static_cast<EditorFlags>(FALSE);
1362 list = g_list_append(nullptr, fd);
1363 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1368 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1370 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1373 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1375 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1378 gboolean editor_window_flag_set(const gchar *key)
1380 EditorDescription *editor;
1381 if (!key) return TRUE;
1383 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1384 if (!editor) return TRUE;
1386 return !!(editor->flags & EDITOR_KEEP_FS);
1389 gboolean editor_is_filter(const gchar *key)
1391 EditorDescription *editor;
1392 if (!key) return TRUE;
1394 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1395 if (!editor) return TRUE;
1397 return !!(editor->flags & EDITOR_DEST);
1400 gboolean editor_no_param(const gchar *key)
1402 EditorDescription *editor;
1403 if (!key) return FALSE;
1405 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1406 if (!editor) return FALSE;
1408 return !!(editor->flags & EDITOR_NO_PARAM);
1411 gboolean editor_blocks_file(const gchar *key)
1413 EditorDescription *editor;
1414 if (!key) return FALSE;
1416 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1417 if (!editor) return FALSE;
1419 /* Decide if the image file should be blocked during editor execution
1420 Editors like gimp can be used long time after the original file was
1421 saved, for editing unrelated files.
1422 %f vs. %F seems to be a good heuristic to detect this kind of editors.
1425 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1428 const gchar *editor_get_error_str(EditorFlags flags)
1430 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1431 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1432 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1433 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1434 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1435 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1436 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1437 return _("Unknown error.");
1440 #pragma GCC diagnostic push
1441 #pragma GCC diagnostic ignored "-Wunused-function"
1442 const gchar *editor_get_name_unused(const gchar *key)
1444 auto *editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1446 if (!editor) return nullptr;
1448 return editor->name;
1450 #pragma GCC diagnostic pop
1452 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */