view src/editors.c @ 1783:b0352818977b

Allow to switch to fullscreen mode using LIRC. Imagine the following situation (which happened to me several times) : you want to see photos from your bed or your sofa so you launch geeqie and go to the right directory, then you take your remote control and sit comfortably far from your keyboard and mouse. And when you want to begin to watch photos, you realize you forgot to enable full screen! You have to stand up and to go until your computer and come back, whereas you could have done it with your remote control. Patch by Bernard Massot.
author zas_
date Tue, 05 Jan 2010 17:49:50 +0000
parents 650915809048
children 956aab097ea7
line wrap: on
line source

/*
 * Geeqie
 * (C) 2006 John Ellis
 * Copyright (C) 2008 - 2009 The Geeqie Team
 *
 * Author: John Ellis
 *
 * This software is released under the GNU General Public License (GNU GPL).
 * Please read the included file COPYING for more information.
 * This software comes with no warranty of any kind, use at your own risk!
 */


#include "main.h"
#include "editors.h"

#include "filedata.h"
#include "filefilter.h"
#include "misc.h"
#include "pixbuf_util.h"
#include "ui_fileops.h"
#include "ui_spinner.h"
#include "ui_utildlg.h"
#include "utilops.h"

#include <errno.h>


#define EDITOR_WINDOW_WIDTH 500
#define EDITOR_WINDOW_HEIGHT 300



typedef struct _EditorVerboseData EditorVerboseData;
struct _EditorVerboseData {
	GenericDialog *gd;
	GtkWidget *button_close;
	GtkWidget *button_stop;
	GtkWidget *text;
	GtkWidget *progress;
	GtkWidget *spinner;
};

typedef struct _EditorData EditorData;
struct _EditorData {
	EditorFlags flags;
	GPid pid;
	GList *list;
	gint count;
	gint total;
	gboolean stopping;
	EditorVerboseData *vd;
	EditorCallback callback;
	gpointer data;
	const EditorDescription *editor;
	gchar *working_directory; /* fallback if no files are given (editor_no_param) */
};


static void editor_verbose_window_progress(EditorData *ed, const gchar *text);
static EditorFlags editor_command_next_start(EditorData *ed);
static EditorFlags editor_command_next_finish(EditorData *ed, gint status);
static EditorFlags editor_command_done(EditorData *ed);

/*
 *-----------------------------------------------------------------------------
 * external editor routines
 *-----------------------------------------------------------------------------
 */

GHashTable *editors = NULL;
GtkListStore *desktop_file_list;
gboolean editors_finished = FALSE;

#ifdef G_KEY_FILE_DESKTOP_GROUP
#define DESKTOP_GROUP G_KEY_FILE_DESKTOP_GROUP
#else
#define DESKTOP_GROUP "Desktop Entry"
#endif

void editor_description_free(EditorDescription *editor)
{
	if (!editor) return;
	
	g_free(editor->key);
	g_free(editor->name);
	g_free(editor->icon);
	g_free(editor->exec);
	g_free(editor->menu_path);
	g_free(editor->hotkey);
	g_free(editor->comment);
	string_list_free(editor->ext_list);
	g_free(editor->file);
	g_free(editor);
}

static GList *editor_mime_types_to_extensions(gchar **mime_types)
{
	/* FIXME: this should be rewritten to use the shared mime database, as soon as we switch to gio */
	
	static const gchar *conv_table[][2] = {
		{"application/x-ufraw",	".ufraw"},
		{"image/*",		"*"},
		{"image/bmp",		".bmp"},
		{"image/gif",		".gif"},
		{"image/jpeg",		".jpeg;.jpg"},
		{"image/jpg",		".jpg;.jpeg"},
		{"image/pcx",		".pcx"},
		{"image/png",		".png"},
		{"image/svg",		".svg"},
		{"image/svg+xml",	".svg"},
		{"image/svg+xml-compressed", 	".svg"},	
		{"image/tiff",		".tiff;.tif"},
		{"image/x-bmp",		".bmp"},
		{"image/x-canon-crw",	".crw"},
		{"image/x-cr2",		".cr2"},
		{"image/x-dcraw",	"%raw"},
		{"image/x-ico",		".ico"},
		{"image/x-mrw",		".mrw"},
		{"image/x-MS-bmp",	".bmp"},
		{"image/x-nef",		".nef"},
		{"image/x-orf",		".orf"},
		{"image/x-pcx",		".pcx"},
		{"image/xpm",		".xpm"},
		{"image/x-png",		".png"},
		{"image/x-portable-anymap",	".pam"},	
		{"image/x-portable-bitmap",	".pbm"},
		{"image/x-portable-graymap",	".pgm"},
		{"image/x-portable-pixmap",	".ppm"},
		{"image/x-psd",		".psd"},
		{"image/x-raf",		".raf"},
		{"image/x-sgi",		".sgi"},
		{"image/x-tga",		".tga"},
		{"image/x-xbitmap",	".xbm"},
		{"image/x-xcf",		".xcf"},
		{"image/x-xpixmap",	".xpm"},
		{"image/x-x3f",		".x3f"},
		{NULL, NULL}};
	
	gint i, j;
	GList *list = NULL;
	
	for (i = 0; mime_types[i]; i++) 
		for (j = 0; conv_table[j][0]; j++)
			if (strcmp(mime_types[i], conv_table[j][0]) == 0)
				list = g_list_concat(list, filter_to_list(conv_table[j][1]));
	
	return list;
}

gboolean editor_read_desktop_file(const gchar *path)
{
	GKeyFile *key_file;
	EditorDescription *editor;
	gchar *extensions;
	gchar *type;
	const gchar *key = filename_from_path(path);
	gchar **categories, **only_show_in, **not_show_in;
	gchar *try_exec;
	GtkTreeIter iter;
	gboolean category_geeqie = FALSE;

	if (g_hash_table_lookup(editors, key)) return FALSE; /* the file found earlier wins */
	
	key_file = g_key_file_new();
	if (!g_key_file_load_from_file(key_file, path, 0, NULL))
		{
		g_key_file_free(key_file);
		return FALSE;
		}

	type = g_key_file_get_string(key_file, DESKTOP_GROUP, "Type", NULL);
	if (!type || strcmp(type, "Application") != 0)
		{
		/* We only consider desktop entries of Application type */
		g_key_file_free(key_file);
		g_free(type);
		return FALSE;
		}
	g_free(type);
	
	editor = g_new0(EditorDescription, 1);
	
	editor->key = g_strdup(key);
	editor->file = g_strdup(path);

	g_hash_table_insert(editors, editor->key, editor);

	if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Hidden", NULL)
	    || g_key_file_get_boolean(key_file, DESKTOP_GROUP, "NoDisplay", NULL))
	    	{
	    	editor->hidden = TRUE;
		}

	categories = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "Categories", NULL, NULL);
	if (categories)
		{
		gboolean found = FALSE;
		gint i;
		for (i = 0; categories[i]; i++) 
			{
			/* IMHO "Graphics" is exactly the category that we are interested in, so this does not have to be configurable */
			if (strcmp(categories[i], "Graphics") == 0)
				{
				found = TRUE;
				}
			if (strcmp(categories[i], "X-Geeqie") == 0) 
				{
				found = TRUE;
				category_geeqie = TRUE;
				break;
				}
			}
		if (!found) editor->ignored = TRUE;
		g_strfreev(categories);
		}
	else
		{
		editor->ignored = TRUE;
		}

	only_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "OnlyShowIn", NULL, NULL);
	if (only_show_in)
		{
		gboolean found = FALSE;
		gint i;
		for (i = 0; only_show_in[i]; i++) 
			if (strcmp(only_show_in[i], "X-Geeqie") == 0)
				{
				found = TRUE;
				break;
				}
		if (!found) editor->ignored = TRUE;
		g_strfreev(only_show_in);
		}

	not_show_in = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "NotShowIn", NULL, NULL);
	if (not_show_in)
		{
		gboolean found = FALSE;
		gint i;
		for (i = 0; not_show_in[i]; i++) 
			if (strcmp(not_show_in[i], "X-Geeqie") == 0)
				{
				found = TRUE;
				break;
				}
		if (found) editor->ignored = TRUE;
		g_strfreev(not_show_in);
		}
		
		
	try_exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "TryExec", NULL);
	if (try_exec && !editor->hidden && !editor->ignored)
		{
		gchar *try_exec_res = g_find_program_in_path(try_exec);
		if (!try_exec_res) editor->hidden = TRUE;
		g_free(try_exec_res);
		g_free(try_exec);
		}

	if (editor->ignored) 
		{
		/* ignored editors will be deleted, no need to parse the rest */
		g_key_file_free(key_file);
		return TRUE;
		}
	
	editor->name = g_key_file_get_locale_string(key_file, DESKTOP_GROUP, "Name", NULL, NULL);
	editor->icon = g_key_file_get_string(key_file, DESKTOP_GROUP, "Icon", NULL);
	
	/* Icon key can be either a full path (absolute with file name extension) or an icon name (without extension) */
	if (editor->icon && !g_path_is_absolute(editor->icon))
		{
		gchar *ext = strrchr(editor->icon, '.');
		
		if (ext && strlen(ext) == 4 && 
		    (!strcmp(ext, ".png") || !strcmp(ext, ".xpm") || !strcmp(ext, ".svg")))
			{
			log_printf(_("Desktop file '%s' should not include extension in Icon key: '%s'\n"),
				   editor->file, editor->icon);
	  		
			// drop extension
			*ext = '\0';
			}
		}
	if (editor->icon && !register_theme_icon_as_stock(editor->key, editor->icon))
		{
		g_free(editor->icon);
		editor->icon = NULL;
		}

	editor->exec = g_key_file_get_string(key_file, DESKTOP_GROUP, "Exec", NULL);
	
	editor->menu_path = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Menu-Path", NULL);
	if (!editor->menu_path) editor->menu_path = g_strdup("EditMenu/ExternalMenu");
	
	editor->hotkey = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-Hotkey", NULL);

	editor->comment = g_key_file_get_string(key_file, DESKTOP_GROUP, "Comment", NULL);

	extensions = g_key_file_get_string(key_file, DESKTOP_GROUP, "X-Geeqie-File-Extensions", NULL);
	if (extensions)
		editor->ext_list = filter_to_list(extensions);
	else
		{
		gchar **mime_types = g_key_file_get_string_list(key_file, DESKTOP_GROUP, "MimeType", NULL, NULL);
		if (mime_types)
			{
			editor->ext_list = editor_mime_types_to_extensions(mime_types);
			g_strfreev(mime_types);
			if (!editor->ext_list) editor->hidden = TRUE; 
			}
		}
		
	if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Keep-Fullscreen", NULL)) editor->flags |= EDITOR_KEEP_FS;
	if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose", NULL)) editor->flags |= EDITOR_VERBOSE;
	if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Verbose-Multi", NULL)) editor->flags |= EDITOR_VERBOSE_MULTI;
	if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "X-Geeqie-Filter", NULL)) editor->flags |= EDITOR_DEST;
	if (g_key_file_get_boolean(key_file, DESKTOP_GROUP, "Terminal", NULL)) editor->flags |= EDITOR_TERMINAL;
	
	editor->flags |= editor_command_parse(editor, NULL, FALSE, NULL);

	if ((editor->flags & EDITOR_NO_PARAM) && !category_geeqie) editor->hidden = TRUE;

	g_key_file_free(key_file);

	if (editor->ignored) return TRUE;
	
	gtk_list_store_append(desktop_file_list, &iter);
	gtk_list_store_set(desktop_file_list, &iter, 
			   DESKTOP_FILE_COLUMN_KEY, key,
			   DESKTOP_FILE_COLUMN_NAME, editor->name,
			   DESKTOP_FILE_COLUMN_HIDDEN, editor->hidden ? _("yes") : _("no"),
			   DESKTOP_FILE_COLUMN_WRITABLE, access_file(path, W_OK),
			   DESKTOP_FILE_COLUMN_PATH, path, -1);
	
	return TRUE;	
}

static gboolean editor_remove_desktop_file_cb(gpointer key, gpointer value, gpointer user_data)
{
	EditorDescription *editor = value;
	return editor->hidden || editor->ignored;
}

void editor_table_finish(void)
{
	g_hash_table_foreach_remove(editors, editor_remove_desktop_file_cb, NULL);
	editors_finished = TRUE;
}

void editor_table_clear(void)
{
	if (desktop_file_list)
		{
		gtk_list_store_clear(desktop_file_list);
		}
	else 
		{
		desktop_file_list = gtk_list_store_new(DESKTOP_FILE_COLUMN_COUNT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_STRING);
		}
	if (editors)
		{
		g_hash_table_destroy(editors);
		}
	editors = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (GDestroyNotify)editor_description_free);
	editors_finished = FALSE;
}

static GList *editor_add_desktop_dir(GList *list, const gchar *path)
{
	DIR *dp;
	struct dirent *dir;
	gchar *pathl;

	pathl = path_from_utf8(path);
	dp = opendir(pathl);
	g_free(pathl);
	if (!dp)
		{
		/* dir not found */
		return list;
		}
	while ((dir = readdir(dp)) != NULL)
		{
		gchar *namel = dir->d_name;
		
		if (g_str_has_suffix(namel, ".desktop"))
			{
			gchar *name = path_to_utf8(namel);
			gchar *dpath = g_build_filename(path, name, NULL);
			list = g_list_prepend(list, dpath);
			g_free(name);
			}	
		}
	closedir(dp);
	return list;
}

GList *editor_get_desktop_files(void)
{
	gchar *path;
	gchar *xdg_data_dirs;
	gchar *all_dirs;
	gchar **split_dirs;
	gint i;
	GList *list = NULL;
	
	xdg_data_dirs = getenv("XDG_DATA_DIRS");
	if (xdg_data_dirs && xdg_data_dirs[0])
		xdg_data_dirs = path_to_utf8(xdg_data_dirs);
	else
		xdg_data_dirs = g_strdup("/usr/share");
	
	all_dirs = g_strconcat(get_rc_dir(), ":", GQ_APP_DIR, ":", xdg_data_home_get(), ":", xdg_data_dirs, NULL);
	
	g_free(xdg_data_dirs);

	split_dirs = g_strsplit(all_dirs, ":", 0);
	
	g_free(all_dirs);

	for (i = 0; split_dirs[i]; i++);
	for (--i; i >= 0; i--)
		{
		path = g_build_filename(split_dirs[i], "applications", NULL);
		list = editor_add_desktop_dir(list, path);
		g_free(path);
		}
		
	g_strfreev(split_dirs);
	return list;
}

static void editor_list_add_cb(gpointer key, gpointer value, gpointer data)
{
	GList **listp = data;
	EditorDescription *editor = value;
	
	/* do not show the special commands in any list, they are called explicitly */ 
	if (strcmp(editor->key, CMD_COPY) == 0 ||
	    strcmp(editor->key, CMD_MOVE) == 0 ||  
	    strcmp(editor->key, CMD_RENAME) == 0 ||
	    strcmp(editor->key, CMD_DELETE) == 0 ||
	    strcmp(editor->key, CMD_FOLDER) == 0) return;

	*listp = g_list_prepend(*listp, editor);
}

static gint editor_sort(gconstpointer a, gconstpointer b)
{
	const EditorDescription *ea = a;
	const EditorDescription *eb = b;
	gint ret;
	
	ret = strcmp(ea->menu_path, eb->menu_path);
	if (ret != 0) return ret;
	
	return g_utf8_collate(ea->name, eb->name);
}

GList *editor_list_get(void)
{
	GList *editors_list = NULL;
	
	if (!editors_finished) return NULL;
	
	g_hash_table_foreach(editors, editor_list_add_cb, &editors_list);
	editors_list = g_list_sort(editors_list, editor_sort);

	return editors_list;
}

/* ------------------------------ */


static void editor_verbose_data_free(EditorData *ed)
{
	if (!ed->vd) return;
	g_free(ed->vd);
	ed->vd = NULL;
}

static void editor_data_free(EditorData *ed)
{
	editor_verbose_data_free(ed);
	g_free(ed->working_directory);
	g_free(ed);
}

static void editor_verbose_window_close(GenericDialog *gd, gpointer data)
{
	EditorData *ed = data;

	generic_dialog_close(gd);
	editor_verbose_data_free(ed);
	if (ed->pid == -1) editor_data_free(ed); /* the process has already terminated */
}

static void editor_verbose_window_stop(GenericDialog *gd, gpointer data)
{
	EditorData *ed = data;
	ed->stopping = TRUE;
	ed->count = 0;
	editor_verbose_window_progress(ed, _("stopping..."));
}

static void editor_verbose_window_enable_close(EditorVerboseData *vd)
{
	vd->gd->cancel_cb = editor_verbose_window_close;

	spinner_set_interval(vd->spinner, -1);
	gtk_widget_set_sensitive(vd->button_stop, FALSE);
	gtk_widget_set_sensitive(vd->button_close, TRUE);
}

static EditorVerboseData *editor_verbose_window(EditorData *ed, const gchar *text)
{
	EditorVerboseData *vd;
	GtkWidget *scrolled;
	GtkWidget *hbox;
	gchar *buf;

	vd = g_new0(EditorVerboseData, 1);

	vd->gd = file_util_gen_dlg(_("Edit command results"), "editor_results",
				   NULL, FALSE,
				   NULL, ed);
	buf = g_strdup_printf(_("Output of %s"), text);
	generic_dialog_add_message(vd->gd, NULL, buf, NULL);
	g_free(buf);
	vd->button_stop = generic_dialog_add_button(vd->gd, GTK_STOCK_STOP, NULL,
						   editor_verbose_window_stop, FALSE);
	gtk_widget_set_sensitive(vd->button_stop, FALSE);
	vd->button_close = generic_dialog_add_button(vd->gd, GTK_STOCK_CLOSE, NULL,
						    editor_verbose_window_close, TRUE);
	gtk_widget_set_sensitive(vd->button_close, FALSE);

	scrolled = gtk_scrolled_window_new(NULL, NULL);
	gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN);
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled),
				       GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
	gtk_box_pack_start(GTK_BOX(vd->gd->vbox), scrolled, TRUE, TRUE, 5);
	gtk_widget_show(scrolled);

	vd->text = gtk_text_view_new();
	gtk_text_view_set_editable(GTK_TEXT_VIEW(vd->text), FALSE);
	gtk_widget_set_size_request(vd->text, EDITOR_WINDOW_WIDTH, EDITOR_WINDOW_HEIGHT);
	gtk_container_add(GTK_CONTAINER(scrolled), vd->text);
	gtk_widget_show(vd->text);

	hbox = gtk_hbox_new(FALSE, 0);
	gtk_box_pack_start(GTK_BOX(vd->gd->vbox), hbox, FALSE, FALSE, 0);
	gtk_widget_show(hbox);

	vd->progress = gtk_progress_bar_new();
	gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(vd->progress), 0.0);
	gtk_box_pack_start(GTK_BOX(hbox), vd->progress, TRUE, TRUE, 0);
	gtk_widget_show(vd->progress);

	vd->spinner = spinner_new(NULL, SPINNER_SPEED);
	gtk_box_pack_start(GTK_BOX(hbox), vd->spinner, FALSE, FALSE, 0);
	gtk_widget_show(vd->spinner);

	gtk_widget_show(vd->gd->dialog);

	ed->vd = vd;
	return vd;
}

static void editor_verbose_window_fill(EditorVerboseData *vd, gchar *text, gint len)
{
	GtkTextBuffer *buffer;
	GtkTextIter iter;

	buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(vd->text));
	gtk_text_buffer_get_iter_at_offset(buffer, &iter, -1);
	gtk_text_buffer_insert(buffer, &iter, text, len);
}

static void editor_verbose_window_progress(EditorData *ed, const gchar *text)
{
	if (!ed->vd) return;

	if (ed->total)
		{
		gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(ed->vd->progress), (gdouble)ed->count / ed->total);
		}

	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(ed->vd->progress), (text) ? text : "");
}

static gboolean editor_verbose_io_cb(GIOChannel *source, GIOCondition condition, gpointer data)
{
	EditorData *ed = data;
	gchar buf[512];
	gsize count;

	if (condition & G_IO_IN)
		{
		while (g_io_channel_read_chars(source, buf, sizeof(buf), &count, NULL) == G_IO_STATUS_NORMAL)
			{
			if (!g_utf8_validate(buf, count, NULL))
				{
				gchar *utf8;

				utf8 = g_locale_to_utf8(buf, count, NULL, NULL, NULL);
				if (utf8)
					{
					editor_verbose_window_fill(ed->vd, utf8, -1);
					g_free(utf8);
					}
				else
					{
					editor_verbose_window_fill(ed->vd, "Error converting text to valid utf8\n", -1);
					}
				}
			else
				{
				editor_verbose_window_fill(ed->vd, buf, count);
				}
			}
		}

	if (condition & (G_IO_ERR | G_IO_HUP))
		{
		g_io_channel_shutdown(source, TRUE, NULL);
		return FALSE;
		}

	return TRUE;
}

typedef enum {
	PATH_FILE,
	PATH_FILE_URL,
	PATH_DEST
} PathType;


static gchar *editor_command_path_parse(const FileData *fd, gboolean consider_sidecars, PathType type, const EditorDescription *editor)
{
	GString *string;
	gchar *pathl;
	const gchar *p = NULL;

	DEBUG_2("editor_command_path_parse: %s %d %d %s", fd->path, consider_sidecars, type, editor->key);

	string = g_string_new("");

	if (type == PATH_FILE || type == PATH_FILE_URL)
		{
		GList *work = editor->ext_list;

		if (!work)
			p = fd->path;
		else
			{
			while (work)
				{
				GList *work2;
				gchar *ext = work->data;
				work = work->next;

				if (strcmp(ext, "*") == 0 ||
				    g_ascii_strcasecmp(ext, fd->extension) == 0)
					{
					p = fd->path;
					break;
					}

				work2 = consider_sidecars ? fd->sidecar_files : NULL;
				while (work2)
					{
					FileData *sfd = work2->data;
					work2 = work2->next;

					if (g_ascii_strcasecmp(ext, sfd->extension) == 0)
						{
						p = sfd->path;
						break;
						}
					}
				if (p) break;
				}
			if (!p) return NULL;
			}
		}
	else if (type == PATH_DEST)
		{
		if (fd->change && fd->change->dest)
			p = fd->change->dest;
		else
			p = "";
		}

	g_assert(p);
	string = g_string_append(string, p);

	if (type == PATH_FILE_URL) g_string_prepend(string, "file://");
	pathl = path_from_utf8(string->str);
	g_string_free(string, TRUE);

	if (pathl && !pathl[0]) /* empty string case */
		{
		g_free(pathl);
		pathl = NULL;
		}
	
	DEBUG_2("editor_command_path_parse: return %s", pathl);
	return pathl;
}

static GString *append_quoted(GString *str, const char *s, gboolean single_quotes, gboolean double_quotes)
{
	const char *p;
	
	if (!single_quotes)
		{
		if (!double_quotes)
			g_string_append_c(str, '\'');
		else
			g_string_append(str, "\"'");
		}

	for (p = s; *p != '\0'; p++)
		{
		if (*p == '\'')
			g_string_append(str, "'\\''");
		else
			g_string_append_c(str, *p);
		}
	
	if (!single_quotes)
		{
		if (!double_quotes)
			g_string_append_c(str, '\'');
		else
			g_string_append(str, "'\"");
		}

	return str;
}


EditorFlags editor_command_parse(const EditorDescription *editor, GList *list, gboolean consider_sidecars, gchar **output)
{
	EditorFlags flags = 0;
	const gchar *p;
	GString *result = NULL;
	gboolean escape = FALSE;
	gboolean single_quotes = FALSE;
	gboolean double_quotes = FALSE;

	DEBUG_2("editor_command_parse: %s %d %d", editor->key, consider_sidecars, !!output);

	if (output)
		result = g_string_new("");

	if (editor->exec[0] == '\0')
		{
		flags |= EDITOR_ERROR_EMPTY;
		goto err;
		}
	
	p = editor->exec;
	/* skip leading whitespaces if any */
	while (g_ascii_isspace(*p)) p++;

	/* command */

	while (*p)
		{
		if (escape)
			{
			escape = FALSE;
			if (output) result = g_string_append_c(result, *p);
			}
		else if (*p == '\\')
			{
			if (!single_quotes) escape = TRUE;
			if (output) result = g_string_append_c(result, *p);
			}
		else if (*p == '\'')
			{
			if (output) result = g_string_append_c(result, *p);
			if (!single_quotes && !double_quotes)
				single_quotes = TRUE;
			else if (single_quotes)
				single_quotes = FALSE;
			}
		else if (*p == '"')
			{
			if (output) result = g_string_append_c(result, *p);
			if (!single_quotes && !double_quotes)
				double_quotes = TRUE;
			else if (double_quotes)
				double_quotes = FALSE;
			}
		else if (*p == '%' && p[1])
			{
			gchar *pathl = NULL;

			p++;

			switch (*p)
				{
				case 'f': /* single file */
				case 'u': /* single url */
					flags |= EDITOR_FOR_EACH;
					if (flags & EDITOR_SINGLE_COMMAND)
						{
						flags |= EDITOR_ERROR_INCOMPATIBLE;
						goto err;
						}
					if (list)
						{
						/* use the first file from the list */
						if (!list->data)
							{
							flags |= EDITOR_ERROR_NO_FILE;
							goto err;
							}
						pathl = editor_command_path_parse((FileData *)list->data,
										  consider_sidecars,
										  (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
										  editor);
						if (!output)
							{
							/* just testing, check also the rest of the list (like with F and U)
							   any matching file is OK */
							GList *work = list->next;
							
							while (!pathl && work)
								{
								FileData *fd = work->data;
								pathl = editor_command_path_parse(fd,
												  consider_sidecars,
												  (*p == 'f') ? PATH_FILE : PATH_FILE_URL,
												  editor);
								work = work->next;
								}
							}
							
						if (!pathl)
							{
							flags |= EDITOR_ERROR_NO_FILE;
							goto err;
							}
						if (output)
							{
							result = append_quoted(result, pathl, single_quotes, double_quotes);
							}
						g_free(pathl);
						}
					break;

				case 'F':
				case 'U':
					flags |= EDITOR_SINGLE_COMMAND;
					if (flags & (EDITOR_FOR_EACH | EDITOR_DEST))
						{
						flags |= EDITOR_ERROR_INCOMPATIBLE;
						goto err;
						}

					if (list)
						{
						/* use whole list */
						GList *work = list;
						gboolean ok = FALSE;

						while (work)
							{
							FileData *fd = work->data;
							pathl = editor_command_path_parse(fd, consider_sidecars, (*p == 'F') ? PATH_FILE : PATH_FILE_URL, editor);
							if (pathl)
								{
								ok = TRUE;

								if (output)
									{
									ok = TRUE;
									if (work != list) g_string_append_c(result, ' ');
									result = append_quoted(result, pathl, single_quotes, double_quotes);
									}
								g_free(pathl);
								}
							work = work->next;
							}
						if (!ok)
							{
							flags |= EDITOR_ERROR_NO_FILE;
							goto err;
							}
						}
					break;
				case 'i':
					if (editor->icon && *editor->icon)
						{
						if (output)
							{
							result = g_string_append(result, "--icon ");
							result = append_quoted(result, editor->icon, single_quotes, double_quotes);
							}
						}
					break;
				case 'c':
					if (output)
						{
						result = append_quoted(result, editor->name, single_quotes, double_quotes);
						}
					break;
				case 'k':
					if (output)
						{
						result = append_quoted(result, editor->file, single_quotes, double_quotes);
						}
					break;
				case '%':
					/* %% = % escaping */
					if (output) result = g_string_append_c(result, *p);
					break;
				case 'd':
				case 'D':
				case 'n':
				case 'N':
				case 'v':
				case 'm':
					/* deprecated according to spec, ignore */
					break;
				default:
					flags |= EDITOR_ERROR_SYNTAX;
					goto err;
				}
			}
		else
			{
			if (output) result = g_string_append_c(result, *p);
			}
		p++;
		}

	if (!(flags & (EDITOR_FOR_EACH | EDITOR_SINGLE_COMMAND))) flags |= EDITOR_NO_PARAM;

	if (output)
		{
		*output = g_string_free(result, FALSE);
		DEBUG_3("Editor cmd: %s", *output);
		}

	return flags;


err:
	if (output)
		{
		g_string_free(result, TRUE);
		*output = NULL;
		}
	return flags;
}


static void editor_child_exit_cb(GPid pid, gint status, gpointer data)
{
	EditorData *ed = data;
	g_spawn_close_pid(pid);
	ed->pid = -1;

	editor_command_next_finish(ed, status);
}


static EditorFlags editor_command_one(const EditorDescription *editor, GList *list, EditorData *ed)
{
	gchar *command;
	FileData *fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : list->data;;
	GPid pid;
	gint standard_output;
	gint standard_error;
	gboolean ok;

	ed->pid = -1;
	ed->flags = editor->flags;
	ed->flags |= editor_command_parse(editor, list, TRUE, &command);

	ok = !EDITOR_ERRORS(ed->flags);

	if (ok)
		{
		ok = (options->shell.path && *options->shell.path);
		if (!ok) log_printf("ERROR: empty shell command\n");
			
		if (ok)
			{
			ok = (access(options->shell.path, X_OK) == 0);
			if (!ok) log_printf("ERROR: cannot execute shell command '%s'\n", options->shell.path);
			}

		if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
		}

	if (ok)
		{
		gchar *working_directory;
		gchar *args[4];
		guint n = 0;

		working_directory = fd ? remove_level_from_path(fd->path) : g_strdup(ed->working_directory);
		args[n++] = options->shell.path;
		if (options->shell.options && *options->shell.options)
			args[n++] = options->shell.options;
		args[n++] = command;
		args[n] = NULL;

		if ((ed->flags & EDITOR_DEST) && fd->change && fd->change->dest) /* FIXME: error handling */
			{
			g_setenv("GEEQIE_DESTINATION", fd->change->dest, TRUE);
			}
		else
			{
			g_unsetenv("GEEQIE_DESTINATION");
			}

		ok = g_spawn_async_with_pipes(working_directory, args, NULL,
				      G_SPAWN_DO_NOT_REAP_CHILD, /* GSpawnFlags */
				      NULL, NULL,
				      &pid,
				      NULL,
				      ed->vd ? &standard_output : NULL,
				      ed->vd ? &standard_error : NULL,
				      NULL);
		
		g_free(working_directory);

		if (!ok) ed->flags |= EDITOR_ERROR_CANT_EXEC;
		}

	if (ok)
		{
		g_child_watch_add(pid, editor_child_exit_cb, ed);
		ed->pid = pid;
		}

	if (ed->vd)
		{
		if (!ok)
			{
			gchar *buf;

			buf = g_strdup_printf(_("Failed to run command:\n%s\n"), editor->file);
			editor_verbose_window_fill(ed->vd, buf, strlen(buf));
			g_free(buf);

			}
		else
			{
			GIOChannel *channel_output;
			GIOChannel *channel_error;

			channel_output = g_io_channel_unix_new(standard_output);
			g_io_channel_set_flags(channel_output, G_IO_FLAG_NONBLOCK, NULL);
			g_io_channel_set_encoding(channel_output, NULL, NULL);

			g_io_add_watch_full(channel_output, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
					    editor_verbose_io_cb, ed, NULL);
			g_io_channel_unref(channel_output);

			channel_error = g_io_channel_unix_new(standard_error);
			g_io_channel_set_flags(channel_error, G_IO_FLAG_NONBLOCK, NULL);
			g_io_channel_set_encoding(channel_error, NULL, NULL);

			g_io_add_watch_full(channel_error, G_PRIORITY_HIGH, G_IO_IN | G_IO_ERR | G_IO_HUP,
					    editor_verbose_io_cb, ed, NULL);
			g_io_channel_unref(channel_error);
			}
		}

	g_free(command);

	return EDITOR_ERRORS(ed->flags);
}

static EditorFlags editor_command_next_start(EditorData *ed)
{
	if (ed->vd) editor_verbose_window_fill(ed->vd, "\n", 1);

	if ((ed->list || (ed->flags & EDITOR_NO_PARAM)) && ed->count < ed->total)
		{
		FileData *fd;
		EditorFlags error;

		fd = (ed->flags & EDITOR_NO_PARAM) ? NULL : ed->list->data;

		if (ed->vd)
			{
			if ((ed->flags & EDITOR_FOR_EACH) && fd)
				editor_verbose_window_progress(ed, fd->path);
			else
				editor_verbose_window_progress(ed, _("running..."));
			}
		ed->count++;

		error = editor_command_one(ed->editor, ed->list, ed);
		if (!error && ed->vd)
			{
			gtk_widget_set_sensitive(ed->vd->button_stop, (ed->list != NULL) );
			if ((ed->flags & EDITOR_FOR_EACH) && fd)
				{
				editor_verbose_window_fill(ed->vd, fd->path, strlen(fd->path));
				editor_verbose_window_fill(ed->vd, "\n", 1);
				}
			}

		if (!error)
			return 0;
		
		/* command was not started, call the finish immediately */
		return editor_command_next_finish(ed, 0);
		}

	/* everything is done */
	return editor_command_done(ed);
}

static EditorFlags editor_command_next_finish(EditorData *ed, gint status)
{
	gint cont = ed->stopping ? EDITOR_CB_SKIP : EDITOR_CB_CONTINUE;

	if (status)
		ed->flags |= EDITOR_ERROR_STATUS;

	if (ed->flags & EDITOR_FOR_EACH)
		{
		/* handle the first element from the list */
		GList *fd_element = ed->list;

		ed->list = g_list_remove_link(ed->list, fd_element);
		if (ed->callback)
			{
			cont = ed->callback(ed->list ? ed : NULL, ed->flags, fd_element, ed->data);
			if (ed->stopping && cont == EDITOR_CB_CONTINUE) cont = EDITOR_CB_SKIP;
			}
		filelist_free(fd_element);
		}
	else
		{
		/* handle whole list */
		if (ed->callback)
			cont = ed->callback(NULL, ed->flags, ed->list, ed->data);
		filelist_free(ed->list);
		ed->list = NULL;
		}

	switch (cont)
		{
		case EDITOR_CB_SUSPEND:
			return EDITOR_ERRORS(ed->flags);
		case EDITOR_CB_SKIP:
			return editor_command_done(ed);
		}
	
	return editor_command_next_start(ed);
}

static EditorFlags editor_command_done(EditorData *ed)
{
	EditorFlags flags;

	if (ed->vd)
		{
		if (ed->count == ed->total)
			{
			editor_verbose_window_progress(ed, _("done"));
			}
		else
			{
			editor_verbose_window_progress(ed, _("stopped by user"));
			}
		editor_verbose_window_enable_close(ed->vd);
		}

	/* free the not-handled items */
	if (ed->list)
		{
		ed->flags |= EDITOR_ERROR_SKIPPED;
		if (ed->callback) ed->callback(NULL, ed->flags, ed->list, ed->data);
		filelist_free(ed->list);
		ed->list = NULL;
		}

	ed->count = 0;

	flags = EDITOR_ERRORS(ed->flags);

	if (!ed->vd) editor_data_free(ed);

	return flags;
}

void editor_resume(gpointer ed)
{
	editor_command_next_start(ed);
}

void editor_skip(gpointer ed)
{
	editor_command_done(ed);
}

static EditorFlags editor_command_start(const EditorDescription *editor, const gchar *text, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
{
	EditorData *ed;
	EditorFlags flags = editor->flags;

	if (EDITOR_ERRORS(flags)) return EDITOR_ERRORS(flags);

	ed = g_new0(EditorData, 1);
	ed->list = filelist_copy(list);
	ed->flags = flags;
	ed->editor = editor;
	ed->total = (flags & (EDITOR_SINGLE_COMMAND | EDITOR_NO_PARAM)) ? 1 : g_list_length(list);
	ed->callback = cb;
	ed->data = data;
	ed->working_directory = g_strdup(working_directory);

	if ((flags & EDITOR_VERBOSE_MULTI) && list && list->next)
		flags |= EDITOR_VERBOSE;

	if (flags & EDITOR_VERBOSE)
		editor_verbose_window(ed, text);

	editor_command_next_start(ed);
	/* errors from editor_command_next_start will be handled via callback */
	return EDITOR_ERRORS(flags);
}

gboolean is_valid_editor_command(const gchar *key)
{
	if (!key) return FALSE;
	return g_hash_table_lookup(editors, key) != NULL;
}

EditorFlags start_editor_from_filelist_full(const gchar *key, GList *list, const gchar *working_directory, EditorCallback cb, gpointer data)
{
	EditorFlags error;
	EditorDescription *editor;
	if (!key) return EDITOR_ERROR_EMPTY;
	
	editor = g_hash_table_lookup(editors, key);

	if (!editor) return EDITOR_ERROR_EMPTY;
	if (!list && !(editor->flags & EDITOR_NO_PARAM)) return EDITOR_ERROR_NO_FILE;

	error = editor_command_parse(editor, list, TRUE, NULL);

	if (EDITOR_ERRORS(error)) return error;

	error |= editor_command_start(editor, editor->name, list, working_directory, cb, data);

	if (EDITOR_ERRORS(error))
		{
		gchar *text = g_strdup_printf(_("%s\n\"%s\""), editor_get_error_str(error), editor->file);
		
		file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL);
		g_free(text);
		}

	return EDITOR_ERRORS(error);
}

EditorFlags start_editor_from_filelist(const gchar *key, GList *list)
{
	return start_editor_from_filelist_full(key, list, NULL, NULL, NULL);
}

EditorFlags start_editor_from_file_full(const gchar *key, FileData *fd, EditorCallback cb, gpointer data)
{
	GList *list;
	EditorFlags error;

	if (!fd) return FALSE;

	list = g_list_append(NULL, fd);
	error = start_editor_from_filelist_full(key, list, NULL, cb, data);
	g_list_free(list);
	return error;
}

EditorFlags start_editor_from_file(const gchar *key, FileData *fd)
{
	return start_editor_from_file_full(key, fd, NULL, NULL);
}

EditorFlags start_editor(const gchar *key, const gchar *working_directory)
{
	return start_editor_from_filelist_full(key, NULL, working_directory, NULL, NULL);
}

gboolean editor_window_flag_set(const gchar *key)
{
	EditorDescription *editor;
	if (!key) return TRUE;
	
	editor = g_hash_table_lookup(editors, key);
	if (!editor) return TRUE;

	return !!(editor->flags & EDITOR_KEEP_FS);
}

gboolean editor_is_filter(const gchar *key)
{
	EditorDescription *editor;
	if (!key) return TRUE;
	
	editor = g_hash_table_lookup(editors, key);
	if (!editor) return TRUE;

	return !!(editor->flags & EDITOR_DEST);
}

gboolean editor_no_param(const gchar *key)
{
	EditorDescription *editor;
	if (!key) return FALSE;
	
	editor = g_hash_table_lookup(editors, key);
	if (!editor) return FALSE;

	return !!(editor->flags & EDITOR_NO_PARAM);
}

gboolean editor_blocks_file(const gchar *key)
{
	EditorDescription *editor;
	if (!key) return FALSE;
	
	editor = g_hash_table_lookup(editors, key);
	if (!editor) return FALSE;

	/* Decide if the image file should be blocked during editor execution
	   Editors like gimp can be used long time after the original file was
	   saved, for editing unrelated files.
	   %f vs. %F seems to be a good heuristic to detect this kind of editors.
	*/
	   
	return !(editor->flags & EDITOR_SINGLE_COMMAND);
}

const gchar *editor_get_error_str(EditorFlags flags)
{
	if (flags & EDITOR_ERROR_EMPTY) return _("Editor template is empty.");
	if (flags & EDITOR_ERROR_SYNTAX) return _("Editor template has incorrect syntax.");
	if (flags & EDITOR_ERROR_INCOMPATIBLE) return _("Editor template uses incompatible macros.");
	if (flags & EDITOR_ERROR_NO_FILE) return _("Can't find matching file type.");
	if (flags & EDITOR_ERROR_CANT_EXEC) return _("Can't execute external editor.");
	if (flags & EDITOR_ERROR_STATUS) return _("External editor returned error status.");
	if (flags & EDITOR_ERROR_SKIPPED) return _("File was skipped.");
	return _("Unknown error.");
}

const gchar *editor_get_name(const gchar *key)
{
	EditorDescription *editor = g_hash_table_lookup(editors, key);

	if (!editor) return NULL;

	return editor->name;
}
/* vim: set shiftwidth=8 softtabstop=0 cindent cinoptions={1s: */