2 * Copyright (C) 2006 John Ellis
3 * Copyright (C) 2008 - 2016 The Geeqie Team
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 2 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License along
18 * with this program; if not, write to the Free Software Foundation, Inc.,
19 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
26 #include "filefilter.h"
27 #include "pixbuf-util.h"
28 #include "ui-fileops.h"
29 #include "ui-spinner.h"
32 #define EDITOR_WINDOW_WIDTH 500
33 #define EDITOR_WINDOW_HEIGHT 300
37 struct EditorVerboseData {
39 GtkWidget *button_close;
40 GtkWidget *button_stop;
53 EditorVerboseData *vd;
54 EditorCallback callback;
56 const EditorDescription *editor;
57 gchar *working_directory; /* fallback if no files are given (editor_no_param) */
61 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
62 static EditorFlags editor_command_next_start(EditorData *ed);
63 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
64 static EditorFlags editor_command_done(EditorData *ed);
67 *-----------------------------------------------------------------------------
68 * external editor routines
69 *-----------------------------------------------------------------------------
72 GHashTable *editors = nullptr;
73 GtkListStore *desktop_file_list;
74 gboolean editors_finished = FALSE;
76 #ifdef G_KEY_FILE_DESKTOP_GROUP
77 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
79 #define DESKTOP_GROUP "Desktop Entry"
82 void editor_description_free(EditorDescription *editor)
90 g_free(editor->menu_path);
91 g_free(editor->hotkey);
92 g_free(editor->comment);
93 string_list_free(editor->ext_list);
98 static GList *editor_mime_types_to_extensions(gchar **mime_types)
100 /** @FIXME this should be rewritten to use the shared mime database, as soon as we switch to gio */
102 static const gchar *conv_table[][2] = {
104 {"image/bmp", ".bmp"},
105 {"image/gif", ".gif"},
106 {"image/heic", ".heic"},
107 {"image/jpeg", ".jpeg;.jpg;.mpo"},
108 {"image/jpg", ".jpg;.jpeg"},
109 {"image/jxl", ".jxl"},
110 {"image/webp", ".webp"},
111 {"image/pcx", ".pcx"},
112 {"image/png", ".png"},
113 {"image/svg", ".svg"},
114 {"image/svg+xml", ".svg"},
115 {"image/svg+xml-compressed", ".svg"},
116 {"image/tiff", ".tiff;.tif;.mef"},
117 {"image/vnd-ms.dds", ".dds"},
118 {"image/x-adobe-dng", ".dng"},
119 {"image/x-bmp", ".bmp"},
120 {"image/x-canon-crw", ".crw"},
121 {"image/x-canon-cr2", ".cr2"},
122 {"image/x-canon-cr3", ".cr3"},
123 {"image/x-cr2", ".cr2"},
124 {"image/x-dcraw", "%raw;.mos"},
125 {"image/x-epson-erf", "%erf"},
126 {"image/x-ico", ".ico"},
127 {"image/x-kodak-kdc", ".kdc"},
128 {"image/x-mrw", ".mrw"},
129 {"image/x-minolta-mrw", ".mrw"},
130 {"image/x-MS-bmp", ".bmp"},
131 {"image/x-nef", ".nef"},
132 {"image/x-nikon-nef", ".nef"},
133 {"image/x-panasonic-raw", ".raw"},
134 {"image/x-panasonic-rw2", ".rw2"},
135 {"image/x-pentax-pef", ".pef"},
136 {"image/x-orf", ".orf"},
137 {"image/x-olympus-orf", ".orf"},
138 {"image/x-pcx", ".pcx"},
139 {"image/xpm", ".xpm"},
140 {"image/x-png", ".png"},
141 {"image/x-portable-anymap", ".pam"},
142 {"image/x-portable-bitmap", ".pbm"},
143 {"image/x-portable-graymap", ".pgm"},
144 {"image/x-portable-pixmap", ".ppm"},
145 {"image/x-psd", ".psd"},
146 {"image/x-raf", ".raf"},
147 {"image/x-fuji-raf", ".raf"},
148 {"image/x-sgi", ".sgi"},
149 {"image/x-sony-arw", ".arw"},
150 {"image/x-sony-sr2", ".sr2"},
151 {"image/x-sony-srf", ".srf"},
152 {"image/x-tga", ".tga"},
153 {"image/x-xbitmap", ".xbm"},
154 {"image/x-xcf", ".xcf"},
155 {"image/x-xpixmap", ".xpm"},
156 {"image/x-x3f", ".x3f"},
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 (j = 0; conv_table[j][0]; j++)
166 if (strcmp(mime_types[i], conv_table[j][0]) == 0)
167 list = g_list_concat(list, filter_to_list(conv_table[j][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 UNUSED(key), gpointer value, gpointer UNUSED(user_data))
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 UNUSED(key), 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 *UNUSED(gd), 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 spinner_set_interval(vd->spinner, -1);
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, GTK_STOCK_STOP, NULL,
593 //~ editor_verbose_window_stop, FALSE);
594 vd->button_stop = generic_dialog_add_button(vd->gd, "process-stop", nullptr,
595 editor_verbose_window_stop, FALSE);
596 gtk_widget_set_sensitive(vd->button_stop, FALSE);
597 vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, nullptr,
598 editor_verbose_window_close, TRUE);
599 gtk_widget_set_sensitive(vd->button_close, FALSE);
601 scrolled = gtk_scrolled_window_new(nullptr, nullptr);
602 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
603 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
604 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
605 gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
606 gtk_widget_show(scrolled);
608 vd->text = gtk_text_view_new();
609 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
610 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
611 gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
612 gtk_widget_show(vd->text);
614 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
615 gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
616 gtk_widget_show(hbox);
618 vd->progress = gtk_progress_bar_new();
619 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
620 gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
621 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
622 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
623 gtk_widget_show(vd->progress);
625 vd->spinner = spinner_new(nullptr, SPINNER_SPEED);
626 gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
627 gtk_widget_show(vd->spinner);
629 gtk_widget_show(vd->gd->dialog);
635 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
637 GtkTextBuffer *buffer;
640 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
641 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
642 gtk_text_buffer_insert(buffer, &iter, text, len);
645 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
651 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
654 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
657 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
659 auto ed = static_cast<EditorData *>(data);
663 if (condition & G_IO_IN)
665 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
667 if (!g_utf8_validate(buf, count, nullptr))
671 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
674 editor_verbose_window_fill(ed->vd, utf8, -1);
679 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
684 editor_verbose_window_fill(ed->vd, buf, count);
689 if (condition & (G_IO_ERR | G_IO_HUP))
691 g_io_channel_shutdown(source, TRUE, nullptr);
705 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
709 const gchar *p = nullptr;
711 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
713 string = g_string_new("");
715 if (type == PATH_FILE || type == PATH_FILE_URL)
717 GList *work = editor->ext_list;
726 auto ext = static_cast<gchar *>(work->data);
729 if (strcmp(ext, "*") == 0 ||
730 g_ascii_strcasecmp(ext, fd->extension) == 0)
736 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
739 auto sfd = static_cast<FileData *>(work2->data);
742 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
750 if (!p) return nullptr;
753 else if (type == PATH_DEST)
755 if (fd->change && fd->change->dest)
756 p = fd->change->dest;
762 string = g_string_append(string, p);
764 if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
765 pathl = path_from_utf8(string->str);
766 g_string_free(string, TRUE);
768 if (pathl && !pathl[0]) /* empty string case */
774 DEBUG_2("editor_command_path_parse: return %s", pathl);
778 static GString *append_quoted(GString *str, const char *s, gboolean single_quotes, gboolean double_quotes)
785 g_string_append_c(str, '\'');
787 g_string_append(str, "\"'");
790 for (p = s; *p != '\0'; p++)
793 g_string_append(str, "'\\''");
795 g_string_append_c(str, *p);
801 g_string_append_c(str, '\'');
803 g_string_append(str, "'\"");
810 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
812 auto flags = static_cast<EditorFlags>(0);
814 GString *result = nullptr;
815 gboolean escape = FALSE;
816 gboolean single_quotes = FALSE;
817 gboolean double_quotes = FALSE;
819 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
822 result = g_string_new("");
824 if (editor->exec == nullptr || editor->exec[0] == '\0')
826 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
831 /* skip leading whitespaces if any */
832 while (g_ascii_isspace(*p)) p++;
841 if (output) result = g_string_append_c(result, *p);
845 if (!single_quotes) escape = TRUE;
846 if (output) result = g_string_append_c(result, *p);
850 if (output) result = g_string_append_c(result, *p);
851 if (!single_quotes && !double_quotes)
852 single_quotes = TRUE;
853 else if (single_quotes)
854 single_quotes = FALSE;
858 if (output) result = g_string_append_c(result, *p);
859 if (!single_quotes && !double_quotes)
860 double_quotes = TRUE;
861 else if (double_quotes)
862 double_quotes = FALSE;
864 else if (*p == '%' && p[1])
866 gchar *pathl = nullptr;
872 case 'f': /* single file */
873 case 'u': /* single url */
874 flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
875 if (flags & EDITOR_SINGLE_COMMAND)
877 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
882 /* use the first file from the list */
885 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
888 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
890 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
894 /* just testing, check also the rest of the list (like with F and U)
895 any matching file is OK */
896 GList *work = list->next;
898 while (!pathl && work)
900 auto fd = static_cast<FileData *>(work->data);
901 pathl = editor_command_path_parse(fd,
903 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
911 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
916 result = append_quoted(result, pathl, single_quotes, double_quotes);
924 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
925 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
927 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
939 auto fd = static_cast<FileData *>(work->data);
940 pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
948 if (work != list) g_string_append_c(result, ' ');
949 result = append_quoted(result, pathl, single_quotes, double_quotes);
957 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
963 if (editor->icon && *editor->icon)
967 result = g_string_append(result, "--icon ");
968 result = append_quoted(result, editor->icon, single_quotes, double_quotes);
975 result = append_quoted(result, editor->name, single_quotes, double_quotes);
981 result = append_quoted(result, editor->file, single_quotes, double_quotes);
985 /* %% = % escaping */
986 if (output) result = g_string_append_c(result, *p);
994 /* deprecated according to spec, ignore */
997 flags = static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1003 if (output) result = g_string_append_c(result, *p);
1008 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1012 *output = g_string_free(result, FALSE);
1013 DEBUG_3("Editor cmd: %s", *output);
1022 g_string_free(result, TRUE);
1029 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1031 auto ed = static_cast<EditorData *>(data);
1032 g_spawn_close_pid(pid);
1035 editor_command_next_finish(ed, status);
1039 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1042 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1044 gint standard_output;
1045 gint standard_error;
1049 ed->flags = editor->flags;
1050 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1052 ok = !EDITOR_ERRORS(ed->flags);
1056 ok = (options->shell.path && *options->shell.path);
1057 if (!ok) log_printf("ERROR: empty shell command\n");
1061 ok = (access(options->shell.path, X_OK) == 0);
1062 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1065 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1070 gchar *working_directory;
1074 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1075 args[n++] = options->shell.path;
1076 if (options->shell.options && *options->shell.options)
1077 args[n++] = options->shell.options;
1078 args[n++] = command;
1081 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1083 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1087 g_unsetenv("GEEQIE_DESTINATION");
1090 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1091 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1095 ed->vd ? &standard_output : nullptr,
1096 ed->vd ? &standard_error : nullptr,
1099 g_free(working_directory);
1101 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1106 g_child_watch_add(pid, editor_child_exit_cb, ed);
1116 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1117 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1123 GIOChannel *channel_output;
1124 GIOChannel *channel_error;
1126 channel_output = g_io_channel_unix_new(standard_output);
1127 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1128 g_io_channel_set_encoding(channel_output, nullptr, nullptr);
1130 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1131 editor_verbose_io_cb, ed, nullptr);
1132 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1133 editor_verbose_io_cb, ed, nullptr);
1134 g_io_channel_unref(channel_output);
1136 channel_error = g_io_channel_unix_new(standard_error);
1137 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1138 g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1140 g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1141 editor_verbose_io_cb, ed, nullptr);
1142 g_io_channel_unref(channel_error);
1148 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1151 static EditorFlags editor_command_next_start(EditorData *ed)
1153 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1155 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1160 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1164 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1165 editor_verbose_window_progress(ed, fd->path);
1167 editor_verbose_window_progress(ed, _("running..."));
1171 error = editor_command_one(ed->editor, ed->list, ed);
1172 if (!error && ed->vd)
1174 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1175 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1177 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1178 editor_verbose_window_fill(ed->vd, "\n", 1);
1183 return static_cast<EditorFlags>(0);
1185 /* command was not started, call the finish immediately */
1186 return editor_command_next_finish(ed, 0);
1189 /* everything is done */
1190 return editor_command_done(ed);
1193 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1195 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1198 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1200 if (ed->flags & EDITOR_FOR_EACH)
1202 /* handle the first element from the list */
1203 GList *fd_element = ed->list;
1205 ed->list = g_list_remove_link(ed->list, fd_element);
1208 cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1209 if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1211 filelist_free(fd_element);
1215 /* handle whole list */
1217 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1218 filelist_free(ed->list);
1224 case EDITOR_CB_SUSPEND:
1225 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1226 case EDITOR_CB_SKIP:
1227 return editor_command_done(ed);
1230 return editor_command_next_start(ed);
1233 static EditorFlags editor_command_done(EditorData *ed)
1239 if (ed->count == ed->total)
1241 editor_verbose_window_progress(ed, _("done"));
1245 editor_verbose_window_progress(ed, _("stopped by user"));
1247 editor_verbose_window_enable_close(ed->vd);
1250 /* free the not-handled items */
1253 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1254 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1255 filelist_free(ed->list);
1261 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1263 if (!ed->vd) editor_data_free(ed);
1268 void editor_resume(gpointer ed)
1270 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1273 void editor_skip(gpointer ed)
1275 editor_command_done(static_cast<EditorData *>(ed));
1278 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1281 EditorFlags flags = editor->flags;
1283 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1285 ed = g_new0(EditorData, 1);
1286 ed->list = filelist_copy(list);
1288 ed->editor = editor;
1289 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1292 ed->working_directory = g_strdup(working_directory);
1294 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1295 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1297 if (flags & EDITOR_VERBOSE)
1298 editor_verbose_window(ed, text);
1300 editor_command_next_start(ed);
1301 /* errors from editor_command_next_start will be handled via callback */
1302 return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1305 gboolean is_valid_editor_command(const gchar *key)
1307 if (!key) return FALSE;
1308 return g_hash_table_lookup(editors, key) != nullptr;
1311 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1314 EditorDescription *editor;
1315 if (!key) return EDITOR_ERROR_EMPTY;
1317 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1319 if (!editor) return EDITOR_ERROR_EMPTY;
1320 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1322 error = editor_command_parse(editor, list, TRUE, nullptr);
1324 if (EDITOR_ERRORS(error)) return error;
1326 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1328 if (EDITOR_ERRORS(error))
1330 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1332 file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, nullptr);
1336 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1339 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1341 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1344 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1349 if (!fd) return static_cast<EditorFlags>(FALSE);
1351 list = g_list_append(nullptr, fd);
1352 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1357 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1359 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1362 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1364 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1367 gboolean editor_window_flag_set(const gchar *key)
1369 EditorDescription *editor;
1370 if (!key) return TRUE;
1372 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1373 if (!editor) return TRUE;
1375 return !!(editor->flags & EDITOR_KEEP_FS);
1378 gboolean editor_is_filter(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_DEST);
1389 gboolean editor_no_param(const gchar *key)
1391 EditorDescription *editor;
1392 if (!key) return FALSE;
1394 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1395 if (!editor) return FALSE;
1397 return !!(editor->flags & EDITOR_NO_PARAM);
1400 gboolean editor_blocks_file(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 /* Decide if the image file should be blocked during editor execution
1409 Editors like gimp can be used long time after the original file was
1410 saved, for editing unrelated files.
1411 %f vs. %F seems to be a good heuristic to detect this kind of editors.
1414 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1417 const gchar *editor_get_error_str(EditorFlags flags)
1419 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1420 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1421 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1422 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1423 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1424 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1425 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1426 return _("Unknown error.");
1429 //const gchar *editor_get_name(const gchar *key)
1431 //EditorDescription *editor = g_hash_table_lookup(editors, key);
1433 //if (!editor) return NULL;
1435 //return editor->name;
1437 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */