view src/editors.c @ 737:8a8873e7a552

Make shell command and its option rc file options instead of hardcoded strings. This allows users to modify the shell command that execute "editors". Two new options appear in rc file: - shell.path (default to "/bin/sh") - shell.options (default to "-c") These options can only be changed from the rc file, not at runtime. Tests are made to check that shell.path is not empty and lead to an executable file.
author zas_
date Thu, 22 May 2008 20:22:13 +0000
parents fa8f7d7396cf
children 477f48ba28d8
line wrap: on
line source

/*
 * Geeqie
 * (C) 2006 John Ellis
 * Copyright (C) 2008 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 "utilops.h"
#include "ui_fileops.h"
#include "ui_spinner.h"
#include "ui_utildlg.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 {
	gint flags;
	GPid pid;
	gchar *command_template;
	GList *list;
	gint count;
	gint total;
	gboolean stopping;
	EditorVerboseData *vd;
	EditorCallback callback;
	gpointer data;
};


static Editor editor_slot_defaults[GQ_EDITOR_SLOTS] = {
	{ N_("The Gimp"), "gimp-remote %{.cr2;.crw;.nef;.raw;*}f" },
	{ N_("XV"), "xv %f" },
	{ N_("Xpaint"), "xpaint %f" },
	{ N_("UFraw"), "ufraw %{.cr2;.crw;.nef;.raw}p" },
	{ N_("Add XMP sidecar"), "%vFILE=%{.cr2;.crw;.nef;.raw}p;XMP=`echo \"$FILE\"|sed -e 's|\\.[^.]*$|.xmp|'`; exiftool -tagsfromfile \"$FILE\" \"$XMP\"" },
	{ NULL, NULL },
	{ NULL, NULL },
	{ NULL, NULL },
	{ N_("Rotate jpeg clockwise"), "%vif jpegtran -rotate 90 -copy all -outfile %{.jpg;.jpeg}p_tmp %{.jpg;.jpeg}p; then mv %{.jpg;.jpeg}p_tmp %{.jpg;.jpeg}p;else rm %{.jpg;.jpeg}p_tmp;fi" },
	{ N_("Rotate jpeg counterclockwise"), "%vif jpegtran -rotate 270 -copy all -outfile %{.jpg;.jpeg}p_tmp %{.jpg;.jpeg}p; then mv %{.jpg;.jpeg}p_tmp %{.jpg;.jpeg}p;else rm %{.jpg;.jpeg}p_tmp;fi" },
	/* special slots */
#if 1
	/* for testing */
	{ N_("External Copy command"), "%vset -x;cp %p %d" },
	{ N_("External Move command"), "%vset -x;mv %p %d" },
	{ N_("External Rename command"), "%vset -x;mv %p %d" },
	{ N_("External Delete command"), NULL },
	{ N_("External New Folder command"), NULL },
#else
	{ N_("External Copy command"), NULL },
	{ N_("External Move command"), NULL },
	{ N_("External Rename command"), NULL },
	{ N_("External Delete command"), NULL },
	{ N_("External New Folder command"), NULL },
#endif
};

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

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

void editor_reset_defaults(void)
{
	gint i;

	for (i = 0; i < GQ_EDITOR_SLOTS; i++)
		{
		g_free(options->editor[i].name);
		options->editor[i].name = g_strdup(_(editor_slot_defaults[i].name));
		g_free(options->editor[i].command);
		options->editor[i].command = g_strdup(editor_slot_defaults[i].command);
		}
}

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->command_template);
	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"), GQ_WMCLASS, "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), (double)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_DEST
} PathType;


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

	string = g_string_new("");

	if (type == PATH_FILE)
		{
		GList *ext_list = filter_to_list(extensions);
		GList *work = ext_list;

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

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

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

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

	while (*p != '\0')
		{
		/* must escape \, ", `, and $ to avoid problems,
		 * we assume system shell supports bash-like escaping
		 */
		if (strchr("\\\"`$", *p) != NULL)
			{
			string = g_string_append_c(string, '\\');
			}
		string = g_string_append_c(string, *p);
		p++;
		}

	pathl = path_from_utf8(string->str);
	g_string_free(string, TRUE);

	return pathl;
}


/*
 * The supported macros for editor commands:
 *
 *   %f   first occurence replaced by quoted sequence of filenames, command is run once.
 *        only one occurence of this macro is supported.
 *        ([ls %f] results in [ls "file1" "file2" ... "lastfile"])
 *   %p   command is run for each filename in turn, each instance replaced with single filename.
 *        multiple occurences of this macro is supported for complex shell commands.
 *        This macro will BLOCK THE APPLICATION until it completes, since command is run once
 *        for every file in syncronous order. To avoid blocking add the %v macro, below.
 *        ([ls %p] results in [ls "file1"], [ls "file2"] ... [ls "lastfile"])
 *   none if no macro is supplied, the result is equivalent to "command %f"
 *        ([ls] results in [ls "file1" "file2" ... "lastfile"])
 *
 *  Only one of the macros %f or %p may be used in a given commmand.
 *
 *   %v   must be the first two characters[1] in a command, causes a window to display
 *        showing the output of the command(s).
 *   %V   same as %v except in the case of %p only displays a window for multiple files,
 *        operating on a single file is suppresses the output dialog.
 *
 *   %w   must be first two characters in a command, presence will disable full screen
 *        from exiting upon invocation.
 *
 *
 * [1] Note: %v,%V may also be preceded by "%w".
 */


gint editor_command_parse(const gchar *template, GList *list, gchar **output)
{
	gint flags = 0;
	const gchar *p = template;
	GString *result = NULL;
	gchar *extensions = NULL;

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

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

	/* global flags */
	while (*p == '%')
		{
		switch (*++p)
			{
			case 'w':
				flags |= EDITOR_KEEP_FS;
				p++;
				break;
			case 'v':
				flags |= EDITOR_VERBOSE;
				p++;
				break;
			case 'V':
				flags |= EDITOR_VERBOSE_MULTI;
				p++;
				break;
			default:
				flags |= EDITOR_ERROR_SYNTAX;
				goto err;
			}
		}

	/* skip whitespaces if any */
	while (g_ascii_isspace(*p)) p++;

	/* command */

	while (*p)
		{
		if (*p != '%')
			{
			if (output) result = g_string_append_c(result, *p);
			}
		else /* *p == '%' */
			{
			extensions = NULL;
			gchar *pathl = NULL;

			p++;

			/* for example "%f" or "%{.crw;.raw;.cr2}f" */
			if (*p == '{')
				{
				gchar *end;
				
				p++;
				end = strchr(p, '}');
				if (!end)
					{
					flags |= EDITOR_ERROR_SYNTAX;
					goto err;
					}

				extensions = g_strndup(p, end - p);
				p = end + 1;
				}

			switch (*p)
				{
				case 'd':
					flags |= EDITOR_DEST;
					/* fall through */
				case 'p':
					flags |= EDITOR_FOR_EACH;
					if (flags & EDITOR_SINGLE_COMMAND)
						{
						flags |= EDITOR_ERROR_INCOMPATIBLE;
						goto err;
						}
					if (output)
						{
						/* use the first file from the list */
						if (!list || !list->data)
							{
							flags |= EDITOR_ERROR_NO_FILE;
							goto err;
							}
						pathl = editor_command_path_parse((FileData *)list->data,
										  (flags & EDITOR_DEST) ? PATH_DEST : PATH_FILE,
										  extensions);
						if (!pathl)
							{
							flags |= EDITOR_ERROR_NO_FILE;
							goto err;
							}
						result = g_string_append_c(result, '"');
						result = g_string_append(result, pathl);
						g_free(pathl);
						result = g_string_append_c(result, '"');
						}
					break;

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

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

						while (work)
							{
							FileData *fd = work->data;
							pathl = editor_command_path_parse(fd, PATH_FILE, extensions);

							if (pathl)
								{
								ok = TRUE;
								if (work != list) g_string_append_c(result, ' ');
								result = g_string_append_c(result, '"');
								result = g_string_append(result, pathl);
								g_free(pathl);
								result = g_string_append_c(result, '"');
								}
							work = work->next;
							}
						if (!ok)
							{
							flags |= EDITOR_ERROR_NO_FILE;
							goto err;
							}
						}
					break;
				case '%':
					/* %% = % escaping */
					if (output) result = g_string_append_c(result, *p);
					break;
				default:
					flags |= EDITOR_ERROR_SYNTAX;
					goto err;
				}
			if (extensions) g_free(extensions);
			extensions = NULL;
			}
		p++;
		}

	if (output) *output = g_string_free(result, FALSE);
	return flags;


err:
	if (output)
		{
		g_string_free(result, TRUE);
		*output = NULL;
		}
	if (extensions) g_free(extensions);
	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 gint editor_command_one(const gchar *template, GList *list, EditorData *ed)
{
	gchar *command;
	FileData *fd = list->data;
	GPid pid;
	gint standard_output;
	gint standard_error;
	gboolean ok;

	ed->pid = -1;
	ed->flags = editor_command_parse(template, list, &command);

	ok = !(ed->flags & EDITOR_ERROR_MASK);

	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 = remove_level_from_path(fd->path);
		args[n++] = options->shell.path;
		if (options->shell.options && *options->shell.options)
			args[n++] = options->shell.options;
		args[n++] = command;
		args[n] = NULL;

		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"), template);
			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_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_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 ed->flags & EDITOR_ERROR_MASK;
}

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

	if (ed->list && ed->count < ed->total)
		{
		FileData *fd;
		gint error;

		fd = ed->list->data;

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

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

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

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

static gint 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);
		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;
		}

	if (cont == EDITOR_CB_SUSPEND)
		return ed->flags & EDITOR_ERROR_MASK;
	else if (cont == EDITOR_CB_SKIP)
		return editor_command_done(ed);
	else
		return editor_command_next_start(ed);
}

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

	if (ed->vd)
		{
		const gchar *text;

		if (ed->count == ed->total)
			{
			text = _("done");
			}
		else
			{
			text = _("stopped by user");
			}
		editor_verbose_window_progress(ed, text);
		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 = ed->flags & EDITOR_ERROR_MASK;

	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 gint editor_command_start(const gchar *template, const gchar *text, GList *list, EditorCallback cb, gpointer data)
{
	EditorData *ed;
	gint flags = editor_command_parse(template, NULL, NULL);

	if (flags & EDITOR_ERROR_MASK) return flags & EDITOR_ERROR_MASK;

	ed = g_new0(EditorData, 1);
	ed->list = filelist_copy(list);
	ed->flags = flags;
	ed->command_template = g_strdup(template);
	ed->total = (flags & EDITOR_SINGLE_COMMAND) ? 1 : g_list_length(list);
	ed->count = 0;
	ed->stopping = FALSE;
	ed->callback = cb;
	ed->data =  data;

	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 flags & EDITOR_ERROR_MASK;
}

static gint is_valid_editor_command(gint n)
{
	return (n >= 0 && n < GQ_EDITOR_SLOTS
		&& options->editor[n].command
		&& strlen(options->editor[n].command) > 0); 
}

gint start_editor_from_filelist_full(gint n, GList *list, EditorCallback cb, gpointer data)
{
	gchar *command;
	gint error;

	if (!list) return FALSE;
	if (!is_valid_editor_command(n)) return FALSE;

	command = g_locale_from_utf8(options->editor[n].command, -1, NULL, NULL, NULL);
	error = editor_command_start(command, options->editor[n].name, list, cb, data);
	g_free(command);

	if (n < GQ_EDITOR_GENERIC_SLOTS && (error & EDITOR_ERROR_MASK))
		{
		gchar *text = g_strdup_printf(_("%s\n#%d \"%s\":\n%s"), editor_get_error_str(error), n+1,
					      options->editor[n].name, options->editor[n].command);
		
		file_util_warning_dialog(_("Invalid editor command"), text, GTK_STOCK_DIALOG_ERROR, NULL);
		g_free(text);
		}

	return error;
}

gint start_editor_from_filelist(gint n, GList *list)
{
	return start_editor_from_filelist_full(n, list,  NULL, NULL);
}

gint start_editor_from_file_full(gint n, FileData *fd, EditorCallback cb, gpointer data)
{
	GList *list;
	gint error;

	if (!fd) return FALSE;

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

gint start_editor_from_file(gint n, FileData *fd)
{
	return start_editor_from_file_full(n, fd, NULL, NULL);
}

gint editor_window_flag_set(gint n)
{
	if (!is_valid_editor_command(n)) return TRUE;

	return (editor_command_parse(options->editor[n].command, NULL, NULL) & EDITOR_KEEP_FS);
}

const gchar *editor_get_error_str(gint 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(gint n)
{
	if (!is_valid_editor_command(n)) return NULL;

	if (options->editor[n].name && strlen(options->editor[n].name) > 0)
		return options->editor[n].name;
	
	return _("(unknown)");
}