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.
30 #include <glib-object.h>
35 #include "filefilter.h"
37 #include "main-defines.h"
40 #include "pixbuf-util.h"
42 #include "ui-fileops.h"
43 #include "ui-utildlg.h"
47 EDITOR_WINDOW_WIDTH = 500,
48 EDITOR_WINDOW_HEIGHT = 300
53 struct EditorVerboseData {
55 GtkWidget *button_close;
56 GtkWidget *button_stop;
69 EditorVerboseData *vd;
70 EditorCallback callback;
72 const EditorDescription *editor;
73 gchar *working_directory; /* fallback if no files are given (editor_no_param) */
77 static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
78 static EditorFlags editor_command_next_start(EditorData *ed);
79 static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
80 static EditorFlags editor_command_done(EditorData *ed);
83 *-----------------------------------------------------------------------------
84 * external editor routines
85 *-----------------------------------------------------------------------------
88 GHashTable *editors = nullptr;
89 GtkListStore *desktop_file_list;
90 gboolean editors_finished = FALSE;
92 #ifdef G_KEY_FILE_DESKTOP_GROUP
93 #define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
95 #define DESKTOP_GROUP "Desktop Entry"
98 void editor_description_free(EditorDescription *editor)
103 g_free(editor->name);
104 g_free(editor->icon);
105 g_free(editor->exec);
106 g_free(editor->menu_path);
107 g_free(editor->hotkey);
108 g_free(editor->comment);
109 g_list_free_full(editor->ext_list, g_free);
110 g_free(editor->file);
114 static GList *editor_mime_types_to_extensions(gchar **mime_types)
116 /** @FIXME this should be rewritten to use the shared mime database, as soon as we switch to gio */
118 static constexpr const gchar *conv_table[][2] = {
120 {"image/bmp", ".bmp"},
121 {"image/gif", ".gif"},
122 {"image/heic", ".heic"},
123 {"image/jpeg", ".jpeg;.jpg;.mpo"},
124 {"image/jpg", ".jpg;.jpeg"},
125 {"image/jxl", ".jxl"},
126 {"image/webp", ".webp"},
127 {"image/pcx", ".pcx"},
128 {"image/png", ".png"},
129 {"image/svg", ".svg"},
130 {"image/svg+xml", ".svg"},
131 {"image/svg+xml-compressed", ".svg"},
132 {"image/tiff", ".tiff;.tif;.mef"},
133 {"image/vnd-ms.dds", ".dds"},
134 {"image/x-adobe-dng", ".dng"},
135 {"image/x-bmp", ".bmp"},
136 {"image/x-canon-crw", ".crw"},
137 {"image/x-canon-cr2", ".cr2"},
138 {"image/x-canon-cr3", ".cr3"},
139 {"image/x-cr2", ".cr2"},
140 {"image/x-dcraw", "%raw;.mos"},
141 {"image/x-epson-erf", "%erf"},
142 {"image/x-ico", ".ico"},
143 {"image/x-kodak-kdc", ".kdc"},
144 {"image/x-mrw", ".mrw"},
145 {"image/x-minolta-mrw", ".mrw"},
146 {"image/x-MS-bmp", ".bmp"},
147 {"image/x-nef", ".nef"},
148 {"image/x-nikon-nef", ".nef"},
149 {"image/x-panasonic-raw", ".raw"},
150 {"image/x-panasonic-rw2", ".rw2"},
151 {"image/x-pentax-pef", ".pef"},
152 {"image/x-orf", ".orf"},
153 {"image/x-olympus-orf", ".orf"},
154 {"image/x-pcx", ".pcx"},
155 {"image/xpm", ".xpm"},
156 {"image/x-png", ".png"},
157 {"image/x-portable-anymap", ".pam"},
158 {"image/x-portable-bitmap", ".pbm"},
159 {"image/x-portable-graymap", ".pgm"},
160 {"image/x-portable-pixmap", ".ppm"},
161 {"image/x-psd", ".psd"},
162 {"image/x-raf", ".raf"},
163 {"image/x-fuji-raf", ".raf"},
164 {"image/x-sgi", ".sgi"},
165 {"image/x-sony-arw", ".arw"},
166 {"image/x-sony-sr2", ".sr2"},
167 {"image/x-sony-srf", ".srf"},
168 {"image/x-tga", ".tga"},
169 {"image/x-xbitmap", ".xbm"},
170 {"image/x-xcf", ".xcf"},
171 {"image/x-xpixmap", ".xpm"},
172 {"application/x-navi-animation", ".ani"},
173 {"application/x-ptoptimizer-script", ".pto"},
177 GList *list = nullptr;
179 for (i = 0; mime_types[i]; i++)
180 for (const auto& c : conv_table)
181 if (strcmp(mime_types[i], c[0]) == 0)
182 list = g_list_concat(list, filter_to_list(c[1]));
187 gboolean editor_read_desktop_file(const gchar *path)
190 EditorDescription *editor;
193 const gchar *key = filename_from_path(path);
195 gchar **only_show_in;
199 gboolean category_geeqie = FALSE;
203 if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
205 key_file = g_key_file_new();
206 if (!g_key_file_load_from_file(key_file, path, static_cast<GKeyFileFlags>(0), nullptr))
208 g_key_file_free(key_file);
212 type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", nullptr);
213 if (!type || strcmp(type, "Application") != 0)
215 /* We only consider desktop entries of Application type */
216 g_key_file_free(key_file);
222 editor = g_new0(EditorDescription, 1);
224 editor->key = g_strdup(key);
225 editor->file = g_strdup(path);
227 g_hash_table_insert(editors, editor->key, editor);
229 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", nullptr)
230 || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", nullptr))
232 editor->hidden = TRUE;
235 categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", nullptr, nullptr);
238 gboolean found = FALSE;
240 for (i = 0; categories[i]; i++)
242 /* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
243 if (strcmp(categories[i], "Graphics") == 0)
247 if (strcmp(categories[i], "X-Geeqie") == 0)
250 category_geeqie = TRUE;
254 if (!found) editor->ignored = TRUE;
255 g_strfreev(categories);
259 editor->ignored = TRUE;
262 only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", nullptr, nullptr);
265 gboolean found = FALSE;
267 for (i = 0; only_show_in[i]; i++)
268 if (strcmp(only_show_in[i], "X-Geeqie") == 0)
273 if (!found) editor->ignored = TRUE;
274 g_strfreev(only_show_in);
277 not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", nullptr, nullptr);
280 gboolean found = FALSE;
282 for (i = 0; not_show_in[i]; i++)
283 if (strcmp(not_show_in[i], "X-Geeqie") == 0)
288 if (found) editor->ignored = TRUE;
289 g_strfreev(not_show_in);
293 try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", nullptr);
294 if (try_exec && !editor->hidden && !editor->ignored)
296 gchar *try_exec_res = g_find_program_in_path(try_exec);
297 if (!try_exec_res) editor->hidden = TRUE;
298 g_free(try_exec_res);
304 /* ignored editors will be deleted, no need to parse the rest */
305 g_key_file_free(key_file);
309 editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", nullptr, nullptr);
310 editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", nullptr);
312 /* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
313 if (editor->icon && !g_path_is_absolute(editor->icon))
315 gchar *ext = strrchr(editor->icon, '.');
317 if (ext && strlen(ext) == 4 &&
318 (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
320 log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
321 editor->file, editor->icon);
327 if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
329 g_free(editor->icon);
330 editor->icon = nullptr;
333 editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", nullptr);
335 editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", nullptr);
336 if (!editor->menu_path) editor->menu_path = g_strdup("PluginsMenu");
338 editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", nullptr);
340 editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", nullptr);
342 extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", nullptr);
344 editor->ext_list = filter_to_list(extensions);
347 gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", nullptr, nullptr);
350 editor->ext_list = editor_mime_types_to_extensions(mime_types);
351 g_strfreev(mime_types);
352 if (!editor->ext_list) editor->hidden = TRUE;
356 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);
357 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_VERBOSE);
358 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);
359 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_DEST);
360 if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", nullptr)) editor->flags = static_cast<EditorFlags>(editor->flags | EDITOR_TERMINAL);
362 editor->flags = static_cast<EditorFlags>(editor->flags | editor_command_parse(editor, nullptr, FALSE, nullptr));
364 if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;
366 g_key_file_free(key_file);
368 if (editor->ignored) return TRUE;
370 work = options->disabled_plugins;
375 if (g_strcmp0(path, static_cast<const gchar *>(work->data)) == 0)
383 editor->disabled = disabled;
385 gtk_list_store_append(desktop_file_list, &iter);
386 gtk_list_store_set(desktop_file_list, &iter,
387 DESKTOP_FILE_COLUMN_KEY, key,
388 DESKTOP_FILE_COLUMN_DISABLED, editor->disabled,
389 DESKTOP_FILE_COLUMN_NAME, editor->name,
390 DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden ? _("yes") : _("no"),
391 DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
392 DESKTOP_FILE_COLUMN_PATH, path, -1);
397 static gboolean editor_remove_desktop_file_cb(gpointer, gpointer value, gpointer)
399 auto editor = static_cast<EditorDescription *>(value);
400 return editor->hidden || editor->ignored;
403 void editor_table_finish()
405 g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, nullptr);
406 editors_finished = TRUE;
409 void editor_table_clear()
411 if (desktop_file_list)
413 gtk_list_store_clear(desktop_file_list);
417 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);
421 g_hash_table_destroy(editors);
423 editors = g_hash_table_new_full(g_str_hash, g_str_equal, nullptr, reinterpret_cast<GDestroyNotify>(editor_description_free));
424 editors_finished = FALSE;
427 static GList *editor_add_desktop_dir(GList *list, const gchar *path)
433 pathl = path_from_utf8(path);
441 while ((dir = readdir(dp)) != nullptr)
443 gchar *namel = dir->d_name;
445 if (g_str_has_suffix(namel, ".desktop"))
447 gchar *name = path_to_utf8(namel);
448 gchar *dpath = g_build_filename(path, name, NULL);
449 list = g_list_prepend(list, dpath);
457 GList *editor_get_desktop_files()
460 gchar *xdg_data_dirs;
464 GList *list = nullptr;
466 xdg_data_dirs = getenv("XDG_DATA_DIRS");
467 if (xdg_data_dirs && xdg_data_dirs[0])
468 xdg_data_dirs = path_to_utf8(xdg_data_dirs);
470 xdg_data_dirs = g_strdup("/usr/share");
472 all_dirs = g_strconcat(get_rc_dir(), ":", gq_appdir, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
474 g_free(xdg_data_dirs);
476 split_dirs = g_strsplit(all_dirs, ":", 0);
480 for (i = 0; split_dirs[i]; i++);
481 for (--i; i >= 0; i--)
483 path = g_build_filename(split_dirs[i], "applications", NULL);
484 list = editor_add_desktop_dir(list, path);
488 g_strfreev(split_dirs);
492 static void editor_list_add_cb(gpointer, gpointer value, gpointer data)
494 auto listp = static_cast<GList **>(data);
495 auto editor = static_cast<EditorDescription *>(value);
497 /* do not show the special commands in any list, they are called explicitly */
498 if (strcmp(editor->key, CMD_COPY) == 0 ||
499 strcmp(editor->key, CMD_MOVE) == 0 ||
500 strcmp(editor->key, CMD_RENAME) == 0 ||
501 strcmp(editor->key, CMD_DELETE) == 0 ||
502 strcmp(editor->key, CMD_FOLDER) == 0) return;
504 if (editor->disabled)
509 *listp = g_list_prepend(*listp, editor);
512 static gint editor_sort(gconstpointer a, gconstpointer b)
514 auto ea = static_cast<const EditorDescription *>(a);
515 auto eb = static_cast<const EditorDescription *>(b);
516 gchar *caseless_name_ea;
517 gchar *caseless_name_eb;
518 gchar *collate_key_ea;
519 gchar *collate_key_eb;
522 ret = strcmp(ea->menu_path, eb->menu_path);
523 if (ret != 0) return ret;
525 caseless_name_ea = g_utf8_casefold(ea->name, -1);
526 caseless_name_eb = g_utf8_casefold(eb->name, -1);
527 collate_key_ea = g_utf8_collate_key_for_filename(caseless_name_ea, -1);
528 collate_key_eb = g_utf8_collate_key_for_filename(caseless_name_eb, -1);
529 ret = g_strcmp0(collate_key_ea, collate_key_eb);
531 g_free(collate_key_ea);
532 g_free(collate_key_eb);
533 g_free(caseless_name_ea);
534 g_free(caseless_name_eb);
539 GList *editor_list_get()
541 GList *editors_list = nullptr;
543 if (!editors_finished) return nullptr;
545 g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
546 editors_list = g_list_sort(editors_list, editor_sort);
551 /* ------------------------------ */
554 static void editor_verbose_data_free(EditorData *ed)
561 static void editor_data_free(EditorData *ed)
563 editor_verbose_data_free(ed);
564 g_free(ed->working_directory);
568 static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
570 auto ed = static_cast<EditorData *>(data);
572 generic_dialog_close(gd);
573 editor_verbose_data_free(ed);
574 if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
577 static void editor_verbose_window_stop(GenericDialog *, gpointer data)
579 auto ed = static_cast<EditorData *>(data);
582 editor_verbose_window_progress(ed, _("stopping..."));
585 static void editor_verbose_window_enable_close(EditorVerboseData *vd)
587 vd->gd->cancel_cb = editor_verbose_window_close;
589 gtk_spinner_stop(GTK_SPINNER(vd->spinner));
590 gtk_widget_set_sensitive(vd->button_stop, FALSE);
591 gtk_widget_set_sensitive(vd->button_close, TRUE);
594 static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
596 EditorVerboseData *vd;
601 vd = g_new0(EditorVerboseData, 1);
603 vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
606 buf = g_strdup_printf(_("Output of %s"), text);
607 generic_dialog_add_message(vd->gd, nullptr, buf, nullptr, FALSE);
609 vd->button_stop = generic_dialog_add_button(vd->gd, GQ_ICON_STOP, nullptr,
610 editor_verbose_window_stop, FALSE);
611 gtk_widget_set_sensitive(vd->button_stop, FALSE);
612 vd->button_close = generic_dialog_add_button(vd->gd, GQ_ICON_CLOSE, _("Close"),
613 editor_verbose_window_close, TRUE);
614 gtk_widget_set_sensitive(vd->button_close, FALSE);
616 scrolled = gq_gtk_scrolled_window_new(nullptr, nullptr);
617 gq_gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
618 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
619 GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
620 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
621 gtk_widget_show(scrolled);
623 vd->text = gtk_text_view_new();
624 gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
625 gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
626 gq_gtk_container_add(GTK_WIDGET(scrolled), vd->text);
627 gtk_widget_show(vd->text);
629 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
630 gq_gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
631 gtk_widget_show(hbox);
633 vd->progress = gtk_progress_bar_new();
634 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
635 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
636 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(vd->progress), "");
637 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(vd->progress), TRUE);
638 gtk_widget_show(vd->progress);
640 vd->spinner = gtk_spinner_new();
641 gtk_spinner_start(GTK_SPINNER(vd->spinner));
642 gq_gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
643 gtk_widget_show(vd->spinner);
645 gtk_widget_show(vd->gd->dialog);
651 static void editor_verbose_window_fill(EditorVerboseData *vd, const gchar *text, gint len)
653 GtkTextBuffer *buffer;
656 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
657 gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
658 gtk_text_buffer_insert(buffer, &iter, text, len);
661 static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
667 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), static_cast<gdouble>(ed->count) / ed->total);
670 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
673 static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
675 auto ed = static_cast<EditorData *>(data);
679 if (condition & G_IO_IN)
681 while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, nullptr) == G_IO_STATUS_NORMAL)
683 if (!g_utf8_validate(buf, count, nullptr))
687 utf8 = g_locale_to_utf8(buf, count, nullptr, nullptr, nullptr);
690 editor_verbose_window_fill(ed->vd, utf8, -1);
695 editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
700 editor_verbose_window_fill(ed->vd, buf, count);
705 if (condition & (G_IO_ERR | G_IO_HUP))
707 g_io_channel_shutdown(source, TRUE, nullptr);
721 static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
724 const gchar *p = nullptr;
726 DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);
728 if (type == PATH_FILE || type == PATH_FILE_URL)
730 GList *work = editor->ext_list;
739 auto ext = static_cast<gchar *>(work->data);
742 if (strcmp(ext, "*") == 0 ||
743 g_ascii_strcasecmp(ext, fd->extension) == 0)
749 work2 = consider_sidecars ? fd->sidecar_files : nullptr;
752 auto sfd = static_cast<FileData *>(work2->data);
755 if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
763 if (!p) return nullptr;
766 else if (type == PATH_DEST)
768 if (fd->change && fd->change->dest)
769 p = fd->change->dest;
776 GString *string = g_string_new(p);
777 if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
778 pathl = path_from_utf8(string->str);
779 g_string_free(string, TRUE);
781 if (pathl && !pathl[0]) /* empty string case */
787 DEBUG_2("editor_command_path_parse: return %s", pathl);
791 struct CommandBuilder
797 g_string_free(str, TRUE);
804 str = g_string_new("");
807 void append(const gchar *val)
811 str = g_string_append(str, val);
814 void append_c(gchar c)
818 str = g_string_append_c(str, c);
821 void append_quoted(const char *s, gboolean single_quotes, gboolean double_quotes)
828 str = g_string_append_c(str, '\'');
830 str = g_string_append(str, "\"'");
833 for (const char *p = s; *p != '\0'; p++)
836 str = g_string_append(str, "'\\''");
838 str = g_string_append_c(str, *p);
844 str = g_string_append_c(str, '\'');
846 str = g_string_append(str, "'\"");
852 if (!str) return nullptr;
854 auto command = g_string_free(str, FALSE);
860 GString *str{nullptr};
864 EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
866 auto flags = static_cast<EditorFlags>(0);
868 CommandBuilder result;
869 gboolean escape = FALSE;
870 gboolean single_quotes = FALSE;
871 gboolean double_quotes = FALSE;
873 DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);
881 if (editor->exec == nullptr || editor->exec[0] == '\0')
883 return static_cast<EditorFlags>(flags | EDITOR_ERROR_EMPTY);
887 /* skip leading whitespaces if any */
888 while (g_ascii_isspace(*p)) p++;
901 if (!single_quotes) escape = TRUE;
907 if (!single_quotes && !double_quotes)
908 single_quotes = TRUE;
909 else if (single_quotes)
910 single_quotes = FALSE;
915 if (!single_quotes && !double_quotes)
916 double_quotes = TRUE;
917 else if (double_quotes)
918 double_quotes = FALSE;
920 else if (*p == '%' && p[1])
922 gchar *pathl = nullptr;
928 case 'f': /* single file */
929 case 'u': /* single url */
930 flags = static_cast<EditorFlags>(flags | EDITOR_FOR_EACH);
931 if (flags & EDITOR_SINGLE_COMMAND)
933 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
937 /* use the first file from the list */
940 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
942 pathl = editor_command_path_parse(static_cast<FileData *>(list->data),
944 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
948 /* just testing, check also the rest of the list (like with F and U)
949 any matching file is OK */
950 GList *work = list->next;
952 while (!pathl && work)
954 auto fd = static_cast<FileData *>(work->data);
955 pathl = editor_command_path_parse(fd,
957 (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
965 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
967 result.append_quoted(pathl, single_quotes, double_quotes);
974 flags = static_cast<EditorFlags>(flags | EDITOR_SINGLE_COMMAND);
975 if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
977 return static_cast<EditorFlags>(flags | EDITOR_ERROR_INCOMPATIBLE);
988 auto fd = static_cast<FileData *>(work->data);
989 pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
996 result.append_c(' ');
998 result.append_quoted(pathl, single_quotes, double_quotes);
1005 return static_cast<EditorFlags>(flags | EDITOR_ERROR_NO_FILE);
1010 if (editor->icon && *editor->icon)
1012 result.append("--icon ");
1013 result.append_quoted(editor->icon, single_quotes, double_quotes);
1017 result.append_quoted(editor->name, single_quotes, double_quotes);
1020 result.append_quoted(editor->file, single_quotes, double_quotes);
1023 /* %% = % escaping */
1024 result.append_c(*p);
1032 /* deprecated according to spec, ignore */
1035 return static_cast<EditorFlags>(flags | EDITOR_ERROR_SYNTAX);
1040 result.append_c(*p);
1045 if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags = static_cast<EditorFlags>(flags | EDITOR_NO_PARAM);
1049 *output = result.get_command();
1050 DEBUG_3("Editor cmd: %s", *output);
1057 static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
1059 auto ed = static_cast<EditorData *>(data);
1060 g_spawn_close_pid(pid);
1063 editor_command_next_finish(ed, status);
1067 static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
1070 auto fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : list->data);;
1072 gint standard_output;
1073 gint standard_error;
1077 ed->flags = editor->flags;
1078 ed->flags = static_cast<EditorFlags>(ed->flags | editor_command_parse(editor, list, TRUE, &command));
1080 ok = !EDITOR_ERRORS(ed->flags);
1084 ok = (options->shell.path && *options->shell.path);
1085 if (!ok) log_printf("ERROR: empty shell command\n");
1089 ok = (access(options->shell.path, X_OK) == 0);
1090 if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
1093 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1098 gchar *working_directory;
1102 working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
1103 args[n++] = options->shell.path;
1104 if (options->shell.options && *options->shell.options)
1105 args[n++] = options->shell.options;
1106 args[n++] = command;
1109 if ((ed->flags & EDITOR_DEST) && fd && fd->change && fd->change->dest) /** @FIXME error handling */
1111 g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
1115 g_unsetenv("GEEQIE_DESTINATION");
1118 ok = g_spawn_async_with_pipes(working_directory, args, nullptr,
1119 G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
1123 ed->vd ? &standard_output : nullptr,
1124 ed->vd ? &standard_error : nullptr,
1127 g_free(working_directory);
1129 if (!ok) ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_CANT_EXEC);
1134 g_child_watch_add(pid, editor_child_exit_cb, ed);
1144 buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
1145 editor_verbose_window_fill(ed->vd, buf, strlen(buf));
1151 GIOChannel *channel_output;
1152 GIOChannel *channel_error;
1154 channel_output = g_io_channel_unix_new(standard_output);
1155 g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, nullptr);
1156 g_io_channel_set_encoding(channel_output, nullptr, nullptr);
1158 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1159 editor_verbose_io_cb, ed, nullptr);
1160 g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1161 editor_verbose_io_cb, ed, nullptr);
1162 g_io_channel_unref(channel_output);
1164 channel_error = g_io_channel_unix_new(standard_error);
1165 g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, nullptr);
1166 g_io_channel_set_encoding(channel_error, nullptr, nullptr);
1168 g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, static_cast<GIOCondition>(G_IO_IN | G_IO_ERR | G_IO_HUP),
1169 editor_verbose_io_cb, ed, nullptr);
1170 g_io_channel_unref(channel_error);
1176 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1179 static EditorFlags editor_command_next_start(EditorData *ed)
1181 if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);
1183 if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
1188 fd = static_cast<FileData *>((ed->flags & EDITOR_NO_PARAM) ? nullptr : ed->list->data);
1192 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1193 editor_verbose_window_progress(ed, fd->path);
1195 editor_verbose_window_progress(ed, _("running..."));
1199 error = editor_command_one(ed->editor, ed->list, ed);
1200 if (!error && ed->vd)
1202 gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != nullptr) );
1203 if ((ed->flags & EDITOR_FOR_EACH) && fd)
1205 editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
1206 editor_verbose_window_fill(ed->vd, "\n", 1);
1211 return static_cast<EditorFlags>(0);
1213 /* command was not started, call the finish immediately */
1214 return editor_command_next_finish(ed, 0);
1217 /* everything is done */
1218 return editor_command_done(ed);
1221 static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
1223 gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;
1226 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_STATUS);
1228 if (ed->flags & EDITOR_FOR_EACH)
1230 /* handle the first element from the list */
1231 GList *fd_element = ed->list;
1233 ed->list = g_list_remove_link(ed->list, fd_element);
1236 cont = ed->callback(ed->list ? ed : nullptr, ed->flags, fd_element, ed->data);
1237 if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
1239 filelist_free(fd_element);
1243 /* handle whole list */
1245 cont = ed->callback(nullptr, ed->flags, ed->list, ed->data);
1246 filelist_free(ed->list);
1252 case EDITOR_CB_SUSPEND:
1253 return static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1254 case EDITOR_CB_SKIP:
1255 return editor_command_done(ed);
1258 return editor_command_next_start(ed);
1261 static EditorFlags editor_command_done(EditorData *ed)
1267 if (ed->count == ed->total)
1269 editor_verbose_window_progress(ed, _("done"));
1273 editor_verbose_window_progress(ed, _("stopped by user"));
1275 editor_verbose_window_enable_close(ed->vd);
1278 /* free the not-handled items */
1281 ed->flags = static_cast<EditorFlags>(ed->flags | EDITOR_ERROR_SKIPPED);
1282 if (ed->callback) ed->callback(nullptr, ed->flags, ed->list, ed->data);
1283 filelist_free(ed->list);
1289 flags = static_cast<EditorFlags>(EDITOR_ERRORS(ed->flags));
1291 if (!ed->vd) editor_data_free(ed);
1296 void editor_resume(gpointer ed)
1298 editor_command_next_start(reinterpret_cast<EditorData *>(ed));
1301 void editor_skip(gpointer ed)
1303 editor_command_done(static_cast<EditorData *>(ed));
1306 static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1309 EditorFlags flags = editor->flags;
1311 if (EDITOR_ERRORS(flags)) return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1313 ed = g_new0(EditorData, 1);
1314 ed->list = filelist_copy(list);
1316 ed->editor = editor;
1317 ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
1320 ed->working_directory = g_strdup(working_directory);
1322 if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
1323 flags = static_cast<EditorFlags>(flags | EDITOR_VERBOSE);
1325 if (flags & EDITOR_VERBOSE)
1326 editor_verbose_window(ed, text);
1328 editor_command_next_start(ed);
1329 /* errors from editor_command_next_start will be handled via callback */
1330 return static_cast<EditorFlags>(EDITOR_ERRORS(flags));
1333 gboolean is_valid_editor_command(const gchar *key)
1335 if (!key) return FALSE;
1336 return g_hash_table_lookup(editors, key) != nullptr;
1339 EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
1342 EditorDescription *editor;
1343 if (!key) return EDITOR_ERROR_EMPTY;
1345 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1347 if (!editor) return EDITOR_ERROR_EMPTY;
1348 if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;
1350 error = editor_command_parse(editor, list, TRUE, nullptr);
1352 if (EDITOR_ERRORS(error)) return error;
1354 error = static_cast<EditorFlags>(error | editor_command_start(editor, editor->name, list, working_directory, cb, data));
1356 if (EDITOR_ERRORS(error))
1358 gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
1360 file_util_warning_dialog(_("Invalid editor command"), text, GQ_ICON_DIALOG_ERROR, nullptr);
1364 return static_cast<EditorFlags>(EDITOR_ERRORS(error));
1367 EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
1369 return start_editor_from_filelist_full(key, list, nullptr, nullptr, nullptr);
1372 EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
1377 if (!fd) return static_cast<EditorFlags>(FALSE);
1379 list = g_list_append(nullptr, fd);
1380 error = start_editor_from_filelist_full(key, list, nullptr, cb, data);
1385 EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
1387 return start_editor_from_file_full(key, fd, nullptr, nullptr);
1390 EditorFlags start_editor(const gchar *key, const gchar *working_directory)
1392 return start_editor_from_filelist_full(key, nullptr, working_directory, nullptr, nullptr);
1395 gboolean editor_window_flag_set(const gchar *key)
1397 EditorDescription *editor;
1398 if (!key) return TRUE;
1400 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1401 if (!editor) return TRUE;
1403 return !!(editor->flags & EDITOR_KEEP_FS);
1406 gboolean editor_is_filter(const gchar *key)
1408 EditorDescription *editor;
1409 if (!key) return TRUE;
1411 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1412 if (!editor) return TRUE;
1414 return !!(editor->flags & EDITOR_DEST);
1417 gboolean editor_no_param(const gchar *key)
1419 EditorDescription *editor;
1420 if (!key) return FALSE;
1422 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1423 if (!editor) return FALSE;
1425 return !!(editor->flags & EDITOR_NO_PARAM);
1428 gboolean editor_blocks_file(const gchar *key)
1430 EditorDescription *editor;
1431 if (!key) return FALSE;
1433 editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1434 if (!editor) return FALSE;
1436 /* Decide if the image file should be blocked during editor execution
1437 Editors like gimp can be used long time after the original file was
1438 saved, for editing unrelated files.
1439 %f vs. %F seems to be a good heuristic to detect this kind of editors.
1442 return !(editor->flags & EDITOR_SINGLE_COMMAND);
1445 const gchar *editor_get_error_str(EditorFlags flags)
1447 if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
1448 if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
1449 if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
1450 if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
1451 if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
1452 if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
1453 if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
1454 return _("Unknown error.");
1457 #pragma GCC diagnostic push
1458 #pragma GCC diagnostic ignored "-Wunused-function"
1459 const gchar *editor_get_name_unused(const gchar *key)
1461 auto *editor = static_cast<EditorDescription *>(g_hash_table_lookup(editors, key));
1463 if (!editor) return nullptr;
1465 return editor->name;
1467 #pragma GCC diagnostic pop
1469 /* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */