/* $Id: e2_utils.c 896 2008-05-21 20:46:53Z tpgww $

Copyright (C) 2003-2008 tooar <tooar@gmx.net>
Portions copyright (C) 1999 Michael Clark.

This file is part of emelFM2.
emelFM2 is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.

emelFM2 is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with emelFM2; see the file GPL. If not, contact the Free Software
Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "emelfm2.h"
#include <string.h>
#include <signal.h>
#include "e2_utils.h"
#include "e2_dialog.h"
#include "e2_filelist.h"

typedef struct _E2_WildData
{
	gchar **path_elements;	//array of strings, from g_strsplit of utf8 path
	gchar **path_matches;	//array of localised strings, with matching path segments, if any
	guint curr_depth;		//count of element currently being matched
	guint last_depth;		//count of the last path element
	guint first_wild_depth;	//first element that has wildcard char(s)
	guint last_wild_depth;	//last element to be matched (maybe 1 more than last depth)
	struct stat *statptr;	//access to statbuf for shared use
} E2_WildData;

//static gchar *last_trashpath;
/*max msec between clicks when checking for a doubleclick
maybe replaced by gtk's value in e2_utils_update_gtk_settings()*/
guint click_interval = E2_CLICKINTERVAL;

/**
@brief setup struct with 3 NULL pointers
@return the struct
*/
/*E2_Trio *e2_utils_trio_new (void)
{
	E2_Trio *t = ALLOCATE0 (E2_Trio);
	CHECKALLOCATEDWARN (t, );
	return t;
} */
/**
@brief setup struct with 6 NULL pointers
@return the struct
*/
E2_Sextet *e2_utils_sextet_new (void)
{
	E2_Sextet *s = ALLOCATE0 (E2_Sextet);
	CHECKALLOCATEDWARN (s, );
	return s;
}
/**
@brief setup struct with 9 NULL pointers
@return the struct
*/
E2_Nontet *e2_utils_nontet_new (void)
{
	E2_Nontet *n = ALLOCATE0 (E2_Nontet);
	CHECKALLOCATEDWARN (n, );
	return n;
}
/**
@brief de-allocate struct with 3 pointers
@param t pointer to the struct to clear
@return
*/
/*void e2_utils_trio_destroy (E2_Trio *t)
{
	if (t != NULL) DEALLOCATE (E2_Trio, t);
} */
/**
@brief de-allocate struct with 6 pointers
@param s pointer to the struct to clear
@return
*/
void e2_utils_sextet_destroy (E2_Sextet *s)
{
	if (s != NULL) DEALLOCATE (E2_Sextet, s);
}
/**
@brief de-allocate struct with 9 pointers
@param n pointer to the struct to clear
@return
*/
void e2_utils_nontet_destroy (E2_Nontet *n)
{
	if (n != NULL) DEALLOCATE (E2_Nontet, n);
}
/**
@brief display error message about insufficient memory
Expects BGL to be closed
@return
*/
void e2_utils_show_memory_message (void)
{
	e2_output_print_error (_("Not enough memory! Things may not work as expected"), FALSE);
}
/**
@brief handle fatal lack of memory
Expects BGL to be open
@return never does
*/
void e2_utils_memory_error (void)
{
	g_critical ("Not enough memory");
	gdk_threads_enter (); 	//called func expects BGL closed
	e2_main_closedown (TRUE, TRUE, TRUE); //no user-cancellation
}
/**
@brief show user help at the heading @a title
Expects BGL to be on/closed
@param title heading string (NO []) to search for in the main help doc, undefined encoding
@return
*/
void e2_utils_show_help (gchar *title)
{
	gchar *helpfile = e2_option_str_get ("usage-help-doc");
	if (helpfile != NULL)
	{
		gchar *local = F_FILENAME_TO_LOCALE (helpfile);
#ifdef E2_VFS
		VPATH ddata = { local, NULL };	//local helpfiles only
		if (!e2_fs_access (&ddata, R_OK E2_ERR_NONE()))
#else
		if (!e2_fs_access (local, R_OK E2_ERR_NONE()))
#endif
		{
			//setup help doc name and heading name, as parameters for command
			gchar *filepath = g_strdup_printf ("%s [%s]", local, title);
#ifdef E2_VFS
			ddata.localpath = filepath; //possibly mixed encoding
			e2_view_dialog_create_immediate (&ddata);
#else
			e2_view_dialog_create_immediate (filepath);
#endif
			g_free (filepath);
		}
		else
			helpfile = NULL;

		F_FREE (local);
	}

	if (helpfile == NULL)
	{
		gchar *msg = g_strdup_printf (_("Cannot read USAGE help document"));
		e2_output_print_error (msg, TRUE);
	}
}
/**
@brief convert color data to string form
@param color Gdk color data struct
@return newly-allocated string with the color data
*/
gchar *e2_utils_color2str (GdkColor *color)
{
	return g_strdup_printf ("#%.2X%.2X%.2X",
		color->red/256, color->green/256, color->blue/256);
}
/**
@brief replace all occurrences of @a old in @a str with @a new
This uses strsplit and strjoinv. No utf8 consideration.
@param str the 'haystack' string in which to search for @a old
@param old the 'needle' string which is to be repaced
@param new the replacement string for @a old
@return newly allocated string with the repacements made
*/
gchar *e2_utils_str_replace (const gchar *str, const gchar *old, const gchar *new)
{
	gchar **split = g_strsplit (str, old, -1);
	gchar *join = g_strjoinv (new, split);
	g_strfreev (split);
	return join;
}
/**
@brief put spaces between all characters in string @a str
@param str utf-8 string which is to be expanded
@return newly-allocated expanded string
*/
gchar *e2_utils_str_stretch (gchar *str)
{
	if (!g_utf8_validate (str, -1, NULL))
		return (g_strdup (str));

	gchar *retval;
	glong len;
	gunichar *conv = g_utf8_to_ucs4_fast (str, -1, &len);
	//the last ' ' is replaced by \0 so don't need memory for that
#ifdef __USE_GNU
	gunichar stretch [len * 2];
#else
	gunichar *stretch = NEW (gunichar, len * 2);
	if (stretch != NULL)
#endif
	{
		glong i, j;
		for (i = 0, j = 0; i < len; i++, j++)
		{
			stretch[j] = conv[i];
			stretch[++j] = ' ';
		}
		stretch[--j] = '\0';

		retval = g_ucs4_to_utf8 (stretch, -1, NULL, NULL, NULL);
	}
#ifndef __USE_GNU
	else
		retval = g_strdup (str);

	g_free (stretch);
#endif
	g_free (conv);
	return retval;
}
/**
@brief ellipsize the start of string @a string if it's longer than desired
"..." is substituted for the relevant part of @a string, if that's longer than @a length
Note - for middle-position dots, @a limit >= 8
If @a limit is < 3, the returned string may be 1 or 2 chars longer than @a length
@param string utf string which may need to be shortened
@param limit the threshold length (chars, not bytes) for ellipsizing
@param position enumerator for dots at start, middle or end

@return newly-allocated string, shortened or the same as @a string
*/
gchar *e2_utils_str_shorten (gchar *string, gint limit, E2_DotMode position)
{
	g_strchug (string);	//just in case string is a path with trailing space
	glong len = g_utf8_strlen (string, -1);
	if (len > limit)
	{
		gchar *p, *s, *copy;
		glong trim = (limit > 2) ? len - limit + 3 : len - 3 ;
		if (trim > 0)
		{
			switch (position)
			{
				case E2_DOTS_START:
					p = g_utf8_offset_to_pointer (string, trim);
					return g_strconcat ("...", p, NULL);
				case E2_DOTS_END:
					s = g_strdup (string);
					p = g_utf8_offset_to_pointer (s, len - trim);
					*p = '\0';
					p = g_strconcat (s, "...", NULL);
					g_free (s);
					return p;
				default:
					p = s = string;
					glong gapoffset, chroffset = 0;
					//-8 allows for 3 dots and 5 trailing chars
					//FIXME assumes limit >= 8
					while (s != NULL)
					{
						s = e2_utils_find_whitespace (s);
						if (s != NULL)
						{
							gapoffset = g_utf8_pointer_to_offset (string, s);
							if (gapoffset >= limit-8)
								break;
							p = s;	//p = start of last usable whitespace
							chroffset = gapoffset;
							s = e2_utils_pass_whitespace (s);
						}
					}
					if (p == string)
					{	//no suitable gap found, break toward the middle
						chroffset = limit/2 - 2;
						p = g_utf8_offset_to_pointer (string, chroffset);
					}
					copy = g_strndup (string, (p-string));
					s = g_utf8_offset_to_pointer (string, chroffset+len-limit+3);
					p = g_strconcat (copy, "...", s, NULL);
					g_free (copy);
					return p;
			}
		}
	}
	return g_strdup (string);
}

/**
@brief convert from a character-offset to a byte-offset within @a utf8_string
@param utf8_string the string in which we want the offset
@param charoffset 0-based character-position in @a utf8_string, may be < 0 to indicate position relative to end
@return the byte-offset, or -1 upon error
*/
gint e2_utils_get_byte_position (const gchar *utf8_string, gint charoffset)
{
	if (charoffset == 0)
		return 0;
	//glib doesn't do any bounds check, so we do that here
	gint len = g_utf8_strlen (utf8_string, -1);
	if (charoffset > 0)
	{
		if (charoffset >= len)
			return -1;
	}
	else
#ifdef USE_GLIB2_10
	{
		if (-charoffset >= len)
			return -1;
	}
#else
		return -1;
#endif

	const gchar *s = g_utf8_offset_to_pointer (utf8_string, charoffset);
	return (s - utf8_string);
}

/**
@brief if necessary, replace "non-UNIX" line-separtors in @a text with LF
In-place conversion is performed.
Assumes CR always occurs before LF, and no embedded 0 in @a text
@param text string to be processed
@return value indicating the type of separator, CR or LF or CR+LF
*/
gint e2_utils_LF_line_ends (gchar *text)
{
	gint retval;
	gchar *i = text, *j;
	//quick check for whether conversion is needed
	while (*i != '\0')
	{
		if (*i == LF || *i == CR)
			break;
		i++;
	}
	if (*i == CR && *(i+1) == LF)
		retval = CR + LF;
	else if (*i != '\0')
		retval = *i;
	else
		retval = LF;	//default to UNIX-style if we find no break

	//replace or remove CR's
	if (*i == CR)
	{
		for (i = text, j = text; *j != '\0'; i++, j++)
		{
			if (*i == CR)
			{
				*j = LF;
				if (*(i + 1) == LF)
					i++;
			}
			else
				*j = *i;
		}
	}

	return retval;
}
/**
@brief Convert "non-UNIX" line-separtors in @a text to @a separator
@param text string to be processed, must be freeable
@param linecount no. of extra spaces needed when reverting to CR+LF
@param separator desired line-break = CR or LF or CR+LF
@return pointer to adjusted text, maybe different from the original
*/
gchar *e2_utils_revert_line_ends (gchar *text, guint linecount, gint separator)
{
	gchar c;
	gchar *s, *d;
	switch (separator)
	{
		case CR:
			//same size of text, simple replacement
			s = text;
			while ((c = *s) != '\0')
			{
				if (c == LF)
					*s = CR;
				s++;
			}
			break;
		case CR+LF:
		{
			gint textlen = strlen (text) + 1;	//+1 = \0
			gchar *newtext = (gchar *) g_try_realloc (text, (textlen + linecount));
			if (newtext != NULL)
			{
				text = newtext;
				s = text+linecount;
				d = text;
				memmove (s, d, textlen);
//				*d = *s;	//ensure no bad finish at 1st byte
				for (; *d != '\0'; s++, d++)
				{
					if (*s == LF)
					{
						*d = CR;
						d++;
					}
					*d = *s;
				}
			}
		}
			break;
		//case LF:
		default:
			//nothing to do
			break;
	}
	return text;
}
/**
@brief get name of current locale's default character encoding, with fallback

@param encoding store for pointer to name string
@return
*/
void e2_utils_get_charset (const gchar **encoding)
{
	g_get_charset (encoding);
	if (*encoding == NULL)
	//might as well use fs fallback encoding as any other !
		*encoding = e2_cl_options.fallback_encoding;
}
/**
@brief convert utf8 string @a string to lower case

@param string the string which is to be processed
@return newly allocated lc string: must be freed
*/
gchar *e2_utils_str_to_lower (gchar *string)
{
	return g_utf8_strdown (string, -1);
}
/**
@brief append @a string to the full path of the dir shown in @a view

@param view pointer to view data struct
@param string the string which will be appended, localised or utf-8, in accord with @a localised
@param localised TRUE to localise dir path before joining

@return newly allocated joined localised or utf8 string, or NULL on error
*/
gchar *e2_utils_dircat (ViewInfo *view, const gchar *string, gboolean localised)
{
	gchar *dir, *joined;
	dir = (localised) ? F_FILENAME_TO_LOCALE (view->dir) : view->dir;
	joined = e2_utils_strcat (dir, string);
	if (localised)
		F_FREE (dir);
	return joined;
}
/**
@brief join strings @a string1 and @a string2

This is for localised strings, for which
g_strconcat() (maybe?) shouldn't be used

@param string1 the string which will start the returned string
@param string2 the string which will be appended
@return newly allocated joined string, or NULL
*/
gchar *e2_utils_strcat (const gchar *string1, const gchar *string2)
{
	gint len1 = strlen (string1);
	gint len2 = strlen (string2) + sizeof (gchar);	//include the trailing 0
	gchar *result = g_try_malloc (len1 + len2);
	if (result != NULL)
	{
#ifdef __USE_GNU
		gchar *next = mempcpy (result, string1, len1);
		next = mempcpy (next, string2, len2);
#else
		memcpy (result, string1, len1);
		memcpy (result + len1, string2, len2);
#endif
	}
	return result;
}
/**
@brief Like strsplit() but retains the delimiter as part of the string

@param string string to be split
@param delimiter separator string
@param max_tokens maximum number of separated parts, or -1 for no limit
@return NULL-terminated array of strings, or NULL if error occurred
*/
gchar **e2_utils_str_breakup (const gchar *string, const gchar *delimiter,
	gint max_tokens)
{
	GSList *string_list = NULL, *slist;
	gchar *s, *casefold, *new_string;
	gchar **str_array;
	guint i, n = 1;

	g_return_val_if_fail (string != NULL, NULL);
	g_return_val_if_fail (delimiter != NULL, NULL);

	if (max_tokens < 1)
		max_tokens = G_MAXINT;

	s = strstr (string, delimiter);
	if (s != NULL)
	{
		guint delimiter_len = strlen (delimiter);

		do
		{
			guint len = s - string + delimiter_len;
			new_string = NEW (gchar, len + 1);
			if (new_string == NULL && string_list != NULL)
			{
				g_slist_foreach (string_list, (GFunc) g_free, NULL);
				g_slist_free (string_list);
			}
			g_return_val_if_fail (new_string != NULL, NULL);
			g_strlcpy (new_string, string, len);
			new_string[len] = 0;
			casefold = g_utf8_casefold (new_string, -1);
			g_free (new_string);
			new_string = g_utf8_normalize (casefold, -1, G_NORMALIZE_ALL);
			g_free (casefold);
			string_list = g_slist_prepend (string_list, new_string);
			n++;
			string = s + delimiter_len;
			s = strstr (string, delimiter);
		} while (--max_tokens && s);
	}

	if (*string)
	{
		n++;
		casefold = g_utf8_casefold (string, -1);
		new_string = g_utf8_normalize (casefold, -1, G_NORMALIZE_ALL);
		g_free (casefold);
		string_list = g_slist_prepend (string_list, new_string);
	}

	str_array = NEW (gchar*, n);
	if (str_array == NULL && string_list != NULL)
	{
		g_slist_foreach (string_list, (GFunc) g_free, NULL);
		g_slist_free (string_list);
	}
	g_return_val_if_fail (str_array != NULL, NULL);

	i = n - 1;

	str_array[i--] = NULL;
	for (slist = string_list; slist; slist = slist->next)
		str_array[i--] = slist->data;

	g_slist_free (string_list);

	return str_array;
}
/**
@brief find the next (if any) "normal" occurrence of @a c in @a string
A normal occurrence is outside quotes, and not escaped.
This is intended for simple chars like ' ' or ';', and so uses ascii scanning
@param string the string which is to be processed
@param c the character to scan for
@return pointer to the char, or NULL if not found
*/
gchar *e2_utils_bare_strchr (gchar *string, gchar c)
{
	gchar *p = string;
	gint cnt1 = 0; //counter for ' chars
	gint cnt2 = 0; //counter for " chars

	while (*p != '\0')
	{
		if (*p == '\'')
		{
			if (p == string || *(p-1) != '\\')
				cnt1++;
		}
		else if (*p == '"')
		{
			if (p == string || *(p-1) != '\\')
				cnt2++;
		}
		else if (*p == c)
		{	//check if separator seems to be outside parentheses
			if (cnt1 % 2 == 0 && cnt2 % 2 == 0)
				return p; //found one
		}
		p++;
	}
	return NULL;
}
/**
@brief find the first (if any) space or tab from the start of @a string
This uses ascii scanning
@param string the string which is to be processed
@return pointer to the whitespace char, or NULL if not found
*/
gchar *e2_utils_find_whitespace (gchar *string)
{
	register gchar c;
	gchar *s = string;
	while ((c = *s) != '\0')
	{
		if (c == ' ' || c == '\t')
			return s;
		s++;
	}
	return NULL;
}
/**
@brief find the first (if any) non-space-or-tab from the start of @a string
This uses ascii scanning
@param string the string which is to be processed
@return pointer to the non-whitespace char, or NULL if not found
*/
gchar *e2_utils_pass_whitespace (gchar *string)
{
	register gchar c;
	gchar *s = string;
	while ((c = *s) != '\0')
	{
		if (!(c == ' ' || c == '\t'))
			return s;
		s++;
	}
	return NULL;
}
/**
@brief get a copy of @a utf8string without quotes if any
@param utf8string the string which is to be processed
Quotes may be ' or "

@return newly-allocated string, or NULL if string is empty
*/
gchar *e2_utils_unquote_string (const gchar *utf8string)
{
	gboolean quoted;
	gunichar uc1;
	gchar *p;

	p = e2_utils_pass_whitespace ((gchar *)utf8string);
	if (p == NULL)
		return NULL;
	uc1 = g_utf8_get_char (p);
	quoted = ((gchar)uc1 == '"' || (gchar)uc1 == '\'');
	if (quoted)
	{
		gint len;
		gunichar uc2;
		gchar *q;
		len = g_utf8_strlen (p, -1);
		q = g_utf8_offset_to_pointer (p, len - 1);
getlast:
		uc2 = g_utf8_get_char (q);
		if (uc1 == uc2)
		{
			p = g_utf8_next_char (p);
			p = g_strndup (p, q - p);
		}
		else if ((gchar)uc2 == ' ' || (gchar)uc2 == '\t')
		{
			q = g_utf8_prev_char (q);
			goto getlast;
		}
		else
			p = g_strdup (p);
	}
	else
		p = g_strdup (p);
	return p;
}
/**
@brief find the first (if any) argument-substring in @a string
If @a quoted is TRUE, but @a string has no "", then quotes are added
only if @a string contains whitespace. AND then, the whole of @a string is
treated as one, as we don't know which gap is intended as the break.
This uses ascii scanning to find quotes and whitespace.
@param string the string which is to be processed
@param quoted TRUE to return the argument with surrounding ""
@return newly-allocated string, or NULL if no argument found
*/
gchar *e2_utils_get_first_arg (gchar *string, gboolean quoted)
{
	gchar *s1, *s2;
	gchar c;
	gboolean quotednow;

	s1 = e2_utils_pass_whitespace (string);
	if (s1 == NULL)
		return NULL;
	quotednow = (*s1 == '"');	// || *s1 == '\'');
	if (quoted)
	{
		if (quotednow)
		{
			s2 = s1+1;
			while ((s2 = strchr (s2, '"')) != NULL)
			{
				if (*(s2-1) != '\\')
					break;
			}
			if (s2 != NULL)
			{
				s2++;
				c = *s2;
				*s2 = '\0';
				s1 = g_strdup (s1);
				*s2 = c;
			}
			else
				s1 = g_strconcat (s1, "\"", NULL);
		}
		else
		{	//add quotes if appropriate
			s2 = e2_utils_find_whitespace (s1);
			s1 = (s2 != NULL) ?
				//quote whole string as we don't know which gap is relevant
				g_strconcat ("\"", s1, "\"", NULL) :
				g_strdup (s1);	//no need to quote if no gap exists
		}
	}
	else	//want arg without quotes
	{
		if (quotednow)
		{	//strip quotes
			s1++;
			s2 = s1;
			while ((s2 = strchr (s2, '"')) != NULL)
			{
				if (*(s2-1) != '\\')
					break;
			}
			if (s2 != NULL)
			{
				*s2 = '\0';
				s1 = g_strdup (s1);
				*s2 = '"';
			}
			else
				s1 = g_strconcat (s1, "\"", NULL);
		}
		else
		{	//get arg without quotes
			s2 = e2_utils_find_whitespace (s1);
			if (s2 != NULL)
			{
				c = *s2;
				*s2 = '\0';
			}
			s1 = g_strdup (s1);
			if (s2 != NULL)
				*s2 = c;
		}
	}
	return s1;
}
/*gchar *get_key_name (gint keyval)
{
	return gdk_keyval_name (gdk_keyval_to_lower (keyval));
} */
static gchar *prefix = NULL;
static gulong savecount = 0;	//so initial used value defaults to 1

/**
@brief Replace macro(s) in string @a text with appropriate value(s)

Supported macro codes are %c, [%]%d, [%]%D, [%]%f, [%]%F, [%]%p, [%]%P(= F),
 *	%t, %{...} %$...$
These 'letters' are hard-coded CHECKME should the letters be translatable, non-ascii ?
(%c macro is also processed in rename plugin, so that counter(s) can be
recorded for incrementation purposes)
Expects BGL on/closed

@param text utf-8 (or ascii) string, possibly with macros to be expanded
@param for_each utf8 string with name(s) or path(s) of item(s) to substitute for any "%f" or "%p", or NULL to use selected items
@return newly allocated string, or NULL if failure, or 1 if prompt macro cancelled
*/
gchar *e2_utils_expand_macros (gchar *text, gchar *for_each)
{
	GString *command_string = g_string_new ("");
	gchar *s, *free_this, *command_copy, *utf;
	FileInfo *info;

#ifdef E2_REFRESH_DEBUG
	printd (DEBUG, "disable refresh, expand macros");
#endif
	command_copy = g_strdup (text);
	free_this = s = command_copy;
	while ((s = strchr (command_copy, '%')) != NULL)	//if always ascii %, don't need g_utf8_strchr()
	{
		*s = '\0'; s++;	//NCHR(s);
		g_string_append (command_string, command_copy);
		gboolean with_quotes = TRUE;
		command_copy = s+1; //NCHR(command_copy);
		if (*s == '%')
		{
			s++;	//NCHR(s);
			command_copy++;	//NCHR(command_copy);
			with_quotes = FALSE;
		}
		switch (*s)
		{
			case 'f':
			case 'p':
			{
				if (for_each != NULL)	//use specified item name
				{
					gchar *name, *p;
					//the supplied name may or may not have a path
					name = g_path_get_basename (for_each);
					if (g_str_equal (name, for_each)) //no path in for_each
						p = (*s == 'f') ?
							for_each : e2_utils_dircat (curr_view, for_each, FALSE);
					else //path in for_each
						p = (*s == 'f') ? name : for_each;
					if (with_quotes)
						g_string_append_printf (command_string, "\"%s\"", p);
					else
						command_string = g_string_append (command_string, p);
					if (p != for_each && p != name)
						g_free (p);
					g_free (name);
				}
				else	//use selected items
				{
					GList *base, *tmp;
					e2_filelist_disable_refresh ();  //prevent any change to the selected items ?
					base = tmp = e2_fileview_get_selected_local (curr_view);
#ifdef E2_REFRESH_DEBUG
	printd (DEBUG, "enable refresh, expand macros");
#endif
					e2_filelist_enable_refresh();
					if (tmp == NULL)
					{
						e2_output_print_error (_("No item selected"), FALSE);
						//CHECKME continue parsing instead of aborting
						g_free (free_this);
						g_string_free (command_string, TRUE);
						return NULL;
					}
					else
					{
						for (; tmp != NULL; tmp = tmp->next)
						{
							info = tmp->data;
							utf = F_FILENAME_FROM_LOCALE (info->filename);
							if (with_quotes)
								command_string = g_string_append_c (command_string, '"');
							if (*s == 'f')
								g_string_append_printf (command_string, "%s%s",
									(prefix == NULL) ? "" : prefix, utf);
							else
								g_string_append_printf (command_string, "%s%s%s",
									(prefix == NULL) ? "" : prefix, curr_view->dir, utf);
							if (with_quotes)
								command_string = g_string_append_c (command_string, '"');
							if (tmp->next != NULL)
								command_string = g_string_append_c (command_string, ' ');
							F_FREE (utf);
						}
					}
					g_list_free (base);
				}
			}
			break;
			case 'F':
			case 'P':
			{
				GList *base, *tmp;
				e2_filelist_disable_refresh ();
				base = tmp = e2_fileview_get_selected_local (other_view);
#ifdef E2_REFRESH_DEBUG
	printd (DEBUG, "enable refresh, expand macros 2");
#endif
				e2_filelist_enable_refresh();
				if (tmp == NULL)
				{
					e2_output_print_error (_("No item selected in other pane"), FALSE);
					//FIXME continue parsing instead of aborting
					g_free (free_this);
					g_string_free (command_string, TRUE);
					return NULL;
				}
				else
				{
					for (; tmp != NULL; tmp = tmp->next)
					{
						info = tmp->data;
						utf = F_FILENAME_FROM_LOCALE (info->filename);
						if (with_quotes)
						{
							g_string_append_printf (command_string, "\"%s%s%s\"%s",
								(prefix == NULL) ? "" : prefix,
								other_view->dir, utf, tmp->next != NULL ? " " : "");
						}
						else
						{
							g_string_append_printf (command_string, "%s%s%s%s",
								(prefix == NULL) ? "" : prefix,
								other_view->dir, utf, tmp->next != NULL ? " " : "");
						}
						F_FREE (utf);
					}
				}
				g_list_free (base);
			}
			break;
			case 'd':
			case 'D':
			{
				gchar *s1, *s2;
				s1 = (*s == 'd') ? curr_view->dir : other_view->dir;
				s1 = g_strdup (s1);
				s2 = s1 + strlen (s1) - 1;
				if (*s2 == G_DIR_SEPARATOR)
					*s2 = '\0';
				s2 = (with_quotes) ? "\"%s\"" : "%s" ;
				g_string_append_printf (command_string, s2, s1);
				g_free (s1);
			}
				break;
			case 'c':
			{
				gchar numfmt[20];
				gulong count, width;
				//parse count and width
				s++;
				count = strtoul (s, &command_copy, 10);
				if (command_copy == s)	//no number provided
					count = ++savecount;	//use stored value
				else
				{
					savecount = count;
					s = command_copy;
				}
				if (*s == ',')	//no whitespace check
				{
					s++;
					width  = strtoul (s, &command_copy, 10);
					if  (command_copy == s)
						width = 1;	//no number provided
					else
						s = command_copy;
				}
				else
					width = 1;
				numfmt[0] = '%';
				//create count string using value and width
				if (width > 1)
					g_snprintf (numfmt+1, sizeof(numfmt)-1, "0%uu", (guint) width);
				else
					g_strlcpy (numfmt+1, "u", sizeof(numfmt)-1);
				g_string_append_printf (command_string, numfmt, count);

				command_copy = s;
			}
				break;
			case 't':
			{
				gchar *tmp = e2_utils_get_temp_path (NULL);
				g_string_append (command_string, tmp);	//no quoting
				g_free (tmp);
			}
				break;
			case '{':
				if ((s = strchr (command_copy, '}')) == NULL)	//always ascii }, don't need g_utf8_strchr()
				{
					e2_output_print_error (_("No matching '}' found in action text"), FALSE);
					g_free (free_this);
					g_string_free (command_string, TRUE);
					return NULL;
				}
				else
				{
					*s = '\0';	//end of bracketed text
					gchar *user_input, *cend, *sep, *cleaned;
					DialogButtons result;
//tag PASSWORDINPUT gboolean hidden = FALSE;
					gboolean has_history = FALSE;
					GList *thishistory = NULL;
					sep = command_copy;
					//FIXME a better syntax (but | separator makes command look like a pipe)
					while ((sep = strchr (sep, '@')) != NULL)
					{
						if (*(sep-1) == '\\' || *(sep+1) == '@' || *(sep-1) == '@')
							sep++;
						else
							break;
					}
					if (sep != NULL)
					{
						while (command_copy < sep)
						{
							if (*command_copy == '(')
							{
								command_copy++;
								*sep = '\0';
								if ((cend = strchr (command_copy, ')')) != NULL)
								{
									*cend = '\0';
									has_history = TRUE;
									break;
								}
							}
/*tag PASSWORDINPUT
							else if (*command_copy == '*')
							{
								hidden = TRUE;
								break;
							}
*/							command_copy++;	//keep looking
						}
					}
					if (//!hidden &&
						!has_history)
						cleaned = e2_utils_str_replace (command_copy, "\\@", "@");
					else
						cleaned = e2_utils_str_replace (sep+1, "\\@", "@");
/*tag PASSWORDINPUT
					if (hidden)
						result = e2_dialog_password_input (NULL, cleaned, &user_input);

					else
*/						if (has_history)
					{
						e2_cache_list_register (command_copy, &thishistory);
						result = e2_dialog_combo_input (NULL, cleaned, NULL, 0,
							&thishistory, &user_input);
						e2_cache_unregister (command_copy);	//backup the history list
						e2_list_free_with_data (&thishistory);
					}
					else	//default
						result = e2_dialog_line_input (NULL, cleaned, "", 0,
							FALSE, &user_input);
					g_free (cleaned);

					command_copy = s+1;
					if (result == OK)
					{	//a blank entry will not return OK
						//re-enter to expand %f etc in input
						gchar *expinput = e2_utils_expand_macros (user_input, for_each);
						if (expinput > (gchar *)1)	//no check for 1 (no nested inputs)
						{
							g_string_append (command_string, expinput);
							g_free (expinput);
						}
						g_free (user_input);
					}
					else
					{
						g_free (free_this);
						g_string_free (command_string, TRUE);
						return GINT_TO_POINTER (1);	//1 is cancel signal
					}
				}
				break;
			case '$':
				if ((s = strchr (command_copy, '$')) == NULL)	//if always ascii }, don't need g_utf8_strchr()
				{
					e2_output_print_error (_("No matching '$' found in action text"), FALSE);
					g_free (free_this);
					g_string_free (command_string, TRUE);
					return NULL;
				}
				else if (s > command_copy) //ignore $$
				{
					prefix = command_copy;	//store prefix for use in rest of expansion
					*s = '\0';
					gboolean freepfx;
					if (strchr (prefix, '%') != NULL)
					{
						prefix = e2_utils_expand_macros (prefix, NULL);
						freepfx = TRUE;
					}
					else
						freepfx = FALSE;
					command_copy = s+1;
					//re-enter to expand %f etc in rest of string
					gchar *expinput = e2_utils_expand_macros (command_copy, for_each);
					if (freepfx)
						g_free (prefix);
					prefix = NULL;
					if (expinput > (gchar *)1)
					{
						g_string_append (command_string, expinput);
						s = command_string->str;
						g_free (expinput);
						g_free (free_this);
						g_string_free (command_string, FALSE);
						return s;
					}
				}
				break;
			default:
				g_string_append_c (command_string, '%');
				g_string_append_c (command_string, *s);
				break;
		}
	}
	g_string_append (command_string, command_copy);
	g_free (free_this);
#ifdef E2_REFRESH_DEBUG
	printd (DEBUG, "enable refresh, expand macros7");
#endif
	s = command_string->str;
	g_string_free (command_string, FALSE);
	return s;
}
/**
@brief replace all instances of macros [%]%f and [%]%p in @a string, with @a eachitem
This differs from e2_utils_expand_macros(), due to no distinction between
%f and %p, and a slightly different approach to quoting
@param text string to be processed
@param eachitem replacement string

@return newly-allocted replacement string, or NULL if no replacement done
*/
gchar *e2_utils_replace_name (const gchar *text, const gchar *eachitem)
{
	gchar *retval, *withquotes, *freeme;
	withquotes = (gchar *) eachitem + strlen (eachitem) - 1;
	gboolean quoted = ((*eachitem == '"' && *withquotes == '"')
		|| (*eachitem == '\'' && *withquotes == '\''));
	withquotes = (quoted) ? (gchar *)eachitem : g_strconcat ("\"", eachitem, "\"", NULL);
	retval = g_strdup (text);

	if (strstr (retval, "%%p") != NULL)
	{
		freeme = retval;
		retval = e2_utils_str_replace (retval, "%%p", eachitem);
		g_free (freeme);
	}
	if (strstr (retval, "%p") != NULL)
	{
		freeme = retval;
		retval = e2_utils_str_replace (retval, "%p", withquotes);
		g_free (freeme);
	}
	if (strstr (retval, "%%f") != NULL)
	{
		freeme = retval;
		retval = e2_utils_str_replace (retval, "%%f", eachitem);
		g_free (freeme);
	}
	if (strstr (retval, "%f") != NULL)
	{
		freeme = retval;
		retval = e2_utils_str_replace (retval, "%f", withquotes);
		g_free (freeme);
	}
	if (withquotes != eachitem)
		g_free (withquotes);
	if (g_str_equal (text, retval))
	{
		g_free (retval);
		return NULL;
	}
	return retval;
}
/**
@brief replace any instance(s) of macros [%]%f and [%]%p in @a string, with @a eachitem
Any [%]%p will prevail over any [%]%f in @a text
@param text utf8 string to be processed
@param path utf8 path, or NULL to use active pane dir
@param array array of selected items (localised)
@param single TRUE to use only the first item of @a array
@return newly-allocted replacement string, or NULL if no replacement done
*/
gchar *e2_utils_replace_name2 (gchar *text, gchar *path, GPtrArray *array,
	gboolean single)
{
	gboolean quoted, withpath;
	gchar *retval, *usepath, *utf;
	guint count;
	GString *joined;
	if (array->len == 0)
		return NULL;
	if (strstr (text, "%%p") != NULL)
	{
		quoted = FALSE;
		withpath = TRUE;
	}
	else if (strstr (text, "%p") != NULL)
	{
		quoted = TRUE;
		withpath = TRUE;
	}
	else if (strstr (text, "%%f") != NULL)
	{
		quoted = FALSE;
		withpath = FALSE;
	}
	else if (strstr (text, "%f") != NULL)
	{
		quoted = TRUE;
		withpath = FALSE;
	}
	else
		return NULL;

	//get path - from supplied parameter, or active pane
	if (withpath)
	{
		if (path != NULL)
			usepath = g_strdup (path);
		else
			usepath = g_strdup (curr_view->dir);
		gchar *s = usepath + strlen (usepath) - 1;
		if (*s == G_DIR_SEPARATOR)
			*s = '\0';
	}
	else
		usepath = NULL;	//warning prevention

	joined = g_string_sized_new (256);
	E2_SelectedItemInfo **iterator = (E2_SelectedItemInfo **) array->pdata;
	for (count=0; count < array->len; count++, iterator++)
	{
		if (quoted)
			joined = g_string_append_c (joined, '"');
		if (withpath)
		{
			joined = g_string_append (joined, usepath);
			joined = g_string_append_c (joined, G_DIR_SEPARATOR);
		}
		utf = F_FILENAME_FROM_LOCALE ((*iterator)->filename);
		joined = g_string_append (joined, utf);
		F_FREE (utf);
		if (quoted)
			joined = g_string_append_c (joined, '"');
		joined = g_string_append_c (joined, ' ');
		if (single && (count == 0))
			break;
	}
	joined = g_string_truncate (joined, joined->len - 1);	//clear last ' '
	if (withpath)
		g_free (usepath);
	retval = e2_utils_replace_name (text, joined->str);
	g_string_free (joined, TRUE);
	return retval;
}
/**
@brief construct a temporary local itemname by adding a suffix to @a localpath
@param localpath path of item to be processed, localised string
@return newly-allocated, localised, path string comprising the temp name
*/
gchar *e2_utils_get_tempname (const gchar *localpath)
{
#ifdef E2_VFS
	VPATH ddata;
	ddata.spacedata = NULL;
#endif
	gchar *temppath;
	guint i = 0;
	while (TRUE)
	{
		temppath = g_strdup_printf ("%s.tmp~%d", localpath, i);	//no translation or utf-8 conversion needed
		if (i == 0)
		{	//first attempt has no "~N" suffix
			gchar *s = strrchr (temppath, '~');
			*s = '\0';
		}
		E2_ERR_DECLARE
#ifdef E2_VFS
		ddata.localpath = temppath;
		if (e2_fs_access2 (&ddata E2_ERR_PTR()) && E2_ERR_IS (ENOENT))
#else
		if (e2_fs_access2 (temppath E2_ERR_PTR()) && E2_ERR_IS (ENOENT))
#endif
		{
			E2_ERR_CLEAR
			break;
		}
		E2_ERR_CLEAR
		g_free (temppath);
		i++;
	}
	return temppath;
}
/**
@brief check whether the dir shown in pane related to @a rt is accessible
This expects BGL closed
@param rt pointer to file-pane data struct

@return TRUE if rt->path was opened, FALSE if an alternative was opened
*/
gboolean e2_utils_goto_accessible_path (E2_PaneRuntime *rt)
{
	printd (DEBUG, "check that %s is accessible", rt->path);
#ifdef E2_VFSTMP
	//FIXME path for non-native dirs
#else
	gchar *checkpath = g_strdup (rt->path);	//don't clobber the rt->path string
#endif
#ifdef E2_VFSTMP
	//FIXME path check for non-mounted dirs not relevant ?
	//what about incomplete mount of fuse-dir
#endif
	gboolean retval = e2_fs_get_valid_path (&checkpath, TRUE E2_ERR_NONE());
	if (!retval)
	{
		gchar *msg = g_strdup_printf (_("Cannot access %s, going to %s instead"),
#ifdef E2_VFSTMP
	//FIXME path for non-mounted dirs
#else
			rt->path, checkpath);
#endif
		e2_output_print_error (msg, TRUE);
	}
	e2_pane_change_dir (rt, checkpath);
	g_free (checkpath);
	return retval;
}
/**
@brief truncate @a path at the right-most separator, if any
Multiple adjacent separators are treated as one.
@param path path string to be processed
@param ignore_trailer TRUE to strip any trailing separator before checking

@return TRUE if parent path was created
*/
gboolean e2_utils_get_parent_path (gchar *path, gboolean ignore_trailer)
{
	gchar *s;
	gint len = strlen (path);
	if (ignore_trailer)
	{
		s = path + len - sizeof (gchar);
		if (s > path && *s == G_DIR_SEPARATOR)
		{
			*s = '\0';
			len -= sizeof (gchar);
		}
	}
#ifdef E2_VFSTMP
	//FIXME handle going past namespace root
#endif
	if (len == sizeof (gchar))
		return FALSE;	//path this short may be root or not, but can't have a parent
	s = strrchr (path, G_DIR_SEPARATOR);
	if (s == NULL)
		return FALSE;
	if (s > path)
	{
		while (*(--s) == G_DIR_SEPARATOR && s > path) {}
	}
	*(s + sizeof (gchar)) = '\0';
	return TRUE;
}
/**
@brief ensure path string @a path has appropriate separators and embedded relativities

Removes superfluous separators, and "./", "../" from inside @a path, but doesn't
change any leading instances of "../".
Appends separator if not one there already.
Unless @a path is just "/", the returned string will be newly allocated, and the
old one will have been freed, so the returned string may be assigned to the
supplied path argument.

@param path absolute or relative path string to be checked (UTF-8), must be freeable

@return pointer to cleaned absolute path string (UTF-8), may be same as @a path
*/
gchar *e2_utils_path_clean (gchar *path)
{
	g_strchug (path);	//trailing whitespace may be deliberate

	if (path[0] == G_DIR_SEPARATOR && e2_utils_pass_whitespace (path + sizeof (gchar)) == NULL)
		return path;
	//path is not just the file system root
	//(what a clunker this is, with utf parsing !!) FIXME
	gunichar c;
	gchar *pn, *po, *pp, *s;

	gint path_len = strlen (path);
	gchar clean[path_len + 4];  //space for an extra trailer if needed
	po = pp = path;
	pn = clean;
	//remove any superfluous separator from within it
	while ((pp = strchr (pp, G_DIR_SEPARATOR)) != NULL ) //if always ascii /, don't need g_utf8_strchr()
	{
		//check for a repeated separator, or 0
		pp++;	//NCHR(pp);
		c = g_utf8_get_char (pp);
		if (c == '\0')
			break;
		if (c != G_DIR_SEPARATOR)
		{
			NCHR (pp);
			continue;
		}
		//found at least 2 separators,
		//copy old to clean, with the first separator
		while (po < pp)
			*(pn++) = *(po++);
		//skip any more separators
		while ((c = g_utf8_get_char (pp)) == G_DIR_SEPARATOR)
			NCHR (pp);
		po = pp;  //point to the next start for copying
	}
	//copy rest, to the terminator
	while (*po != '\0')
		*(pn++) = *(po++);
	//check for trailing /
	PCHR (pn);
	c = g_utf8_get_char (pn);
	NCHR (pn);
	if (c != G_DIR_SEPARATOR)
	{	//we need to add a trailer
		//g_strlcpy (pn, G_DIR_SEPARATOR_S, path_len-pn+4);
		//since separator is ascii, just shove it into place
		*pn = G_DIR_SEPARATOR;
		NCHR (pn);
	}
	*(pn) = '\0';

	//remove any superfluous . from within it
	pp = clean;
	while ((pp = strchr (pp, '.')) != NULL) //always ascii '.', don't need g_utf8_strchr()
	{
		s = pp + sizeof (gchar);	//s = pp; NCHR (s);
		if (*s == G_DIR_SEPARATOR &&
			(pp == clean || *(pp - sizeof (gchar)) == G_DIR_SEPARATOR))
		{	//eliminate "./" from path
			pn = pp;
			po = s + sizeof (gchar);
			while (*po != '\0')
				*(pn++) = *(po++);
			*(pn) = '\0';
		}
		else if (*s == '.' &&
				(*(s + sizeof (gchar)) == G_DIR_SEPARATOR || *(s+1) == '\0') &&
				(pp == clean || *(pp - sizeof (gchar)) == G_DIR_SEPARATOR))
		{	//backup or ignore "../" in path
			*pp = '\0';

			if (e2_utils_get_parent_path (clean, TRUE))
			{
				if (*(s + sizeof (gchar)) == G_DIR_SEPARATOR)
				{
					pp = pn = clean + strlen (clean);
					if (pp == clean || *(pp - sizeof (gchar)) == G_DIR_SEPARATOR)
						po = s + 2 * sizeof (gchar);	//don't want duplicate separator
					else
						po = s + sizeof (gchar);
					while (*po != '\0')
						*(pn++) = *(po++);
					*(pn) = '\0';
				}
				else	//".." is at end of path string
					break;	//finished now
			}
			else	//no parent
			{
				if (*(s + sizeof (gchar)) == G_DIR_SEPARATOR)
				{
					pn = pp;
					if (pp == clean || *(pp - sizeof (gchar)) == G_DIR_SEPARATOR)
						po = s + 2 * sizeof (gchar);	//don't want duplicate separator
					else
						po = s + sizeof (gchar);
					while (*po != '\0')
						*(pn++) = *(po++);
					*(pn) = '\0';
				}
				else	//".." is at end of path string
				{
					if (pp == clean)
						*pp = '.';	//reinstate ".." is the whole path
					break;
				}
			}
		}
		else
			pp++;
	}

	g_free (path);	//cleanup the one supplied
	return (strdup (clean)); //send back a copy
}
/**
@brief translate relative path

Creates an absolute path string from a relative one, or if the supplied path is
already absolute, just makes sure it is 'clean'.
Works with UTF and with non-UTF path strings.

@param current_dir current-path string, UTF-8
@param new_dir absolute or relative new-path string, UTF-8

* @return newly-allocated, cleaned, absolute path with trailing separator
*/
gchar *e2_utils_translate_relative_path (gchar *current_dir, gchar *new_dir)
{
	gchar *s = (g_path_is_absolute (new_dir)) ?
		g_strdup (new_dir): //copy so that e2_utils_path_clean() can free it
		g_strconcat (current_dir, G_DIR_SEPARATOR_S, new_dir, NULL);
	return (e2_utils_path_clean (s));
}
/**
@brief create relative path of @a src relative to @a dest
If @a src is not an absolute path, it is returned unchanged
This only works for ascii single-byte separator-characters in filepaths
@param src string containing the path to be relativised
@param dest string containing the reference path
@return newly-allocated path string
*/
gchar *e2_utils_create_relative_path (const gchar *src, const gchar *dest)
{
	if (*src != G_DIR_SEPARATOR)
		//not an absolute path, we don't want, or don't know how, to relativise
		return g_strdup (src);

	const gchar *sp = src, *dp = dest, *tsp = NULL;
	GString *rel = g_string_sized_new (PATH_MAX);

	// strip common path
	while (*sp && (*sp == *dp))
	{
		if (*sp == G_DIR_SEPARATOR)
		{
			NCHR(sp);
			tsp = sp; // save as latest short path
		}
		else
			NCHR(sp);
		NCHR(dp);
	}
	// insert non-common parent dirs (if any) as uplinks
	while (*dp)
	{
		if (*dp == G_DIR_SEPARATOR)
			g_string_append (rel, ".."G_DIR_SEPARATOR_S);  //.. applies in all languages ?
		NCHR(dp);
	}
	// if not an updir ref. make it relative to current
//	if (rel->len == 0)
//		g_string_append (rel, "."G_DIR_SEPARATOR_S);
	// and add short src path
	if (tsp != NULL)
		g_string_append (rel, tsp);

	return (g_string_free (rel, FALSE));
}
/**
@brief get unique temp dir name

@param id localised string to embed in returned basename, or NULL

@return newly-allocated path, utf8 string, no trailing "/"
*/
gchar *e2_utils_get_temp_path (const gchar *id)
{
	gchar *tmp = (id == NULL) ? "":(gchar *)id;
	const gchar *systmp = g_get_tmp_dir ();
	if (g_str_has_prefix (systmp, g_get_home_dir ()))
		tmp = g_strdup_printf ("%s"G_DIR_SEPARATOR_S"%s%s", systmp, BINNAME, tmp);
	else
		//in shared space, make user-identifiable temp dir
		tmp = g_strdup_printf ("%s"G_DIR_SEPARATOR_S"%d-%s%s", systmp, getuid (),
			BINNAME, tmp);
	//systmp, BINNAME and uid no. are all localised
	gchar *retval = e2_utils_get_tempname (tmp);
	g_free (tmp);
	tmp = retval;
	retval = D_FILENAME_FROM_LOCALE (tmp);
	g_free (tmp);
	return retval;
}
/**
@brief get path of relevant trash directory
Returned string should NOT be freed elsewhere
@return pointer to utf8 pathstring (with trailing /), or NULL if there is none
*/
const gchar *e2_utils_get_trash_path (void)
{
/*
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
	if (g_str_has_suffix (curr_view->dir, "Trash"G_DIR_SEPARATOR_S))
	//already at a trash place, just use that
		return curr_view->dir;
#endif
	//check if there is a local trash directory
	//needs separator at end, for comparisons when simply open a trash dir
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
	gchar *trash = g_build_filename (curr_view->dir, ".Trash"G_DIR_SEPARATOR_S, NULL);
#endif
	gchar *local = F_FILENAME_TO_LOCALE (trash);

	if (!e2_fs_is_dir3 (local E2_ERR_PTR()))
	{	//if not, use the default, set at session start
		g_free (trash);
		if (app.trashpath != NULL)
			trash = g_strdup (app.trashpath);
		else
		{
			e2_output_print_error(_("No trash directory"), FALSE);
			return NULL;
		}
	}
	F_FREE (local);
	//remember, so it can be cleaned later, as a convenience
	if (last_trashpath != NULL)
		g_free (last_trashpath);
	last_trashpath = trash;

	return trash;

	//pointer to last-used topdir trash, freed here for convenience
	static gchar *last_trashpath;
	static gchar *suffix;
	struct stat sb;

	if (suffix == NULL)
	{
		gint myuid = getuid ();
		suffix = g_strdup_printf ("-%d", myuid);
	}

	gchar *tlocal;
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
	gchar *local = D_FILENAME_TO_LOCALE (curr_view->dir);
#endif
	if (e2_fs_stat (local, &sb E2_ERR_NONE()))
	{
		FIXME handle error;
	}
	dev_t curr_dev = sb.st_dev;

	if (e2_cl_options.trash_dir != NULL)
	{
		tlocal = F_FILENAME_TO_LOCALE (e2_cl_options.trash_dir);
		if (e2_fs_stat (tlocal, &sb E2_ERR_NONE()))
		{
			//FIXME handle error
		}
		dev_t t_dev = sb.st_dev;
		F_FREE (tlocal);
		if (curr_dev == t_dev)
		{	//CWD is on same device as default trash
			g_free (local);
			return (e2_cl_options.trash_dir);
		}
	}
	//find the top dir of the device where CWD is
	gchar *s;
	while ((s = strrchr (local, G_DIR_SEPARATOR)) != NULL)
	{
		*s = '\0';
		if (e2_fs_stat (local, &sb E2_ERR_NONE()))
		{
			FIXME handle error;
		}
		if (sb.st_dev != curr_dev)
		{
			*s = G_DIR_SEPARATOR;
			break;
		}
	}
	if (s == NULL)
	{	//all CDW is on same device
		*local = G_DIR_SEPARATOR;	//work from the root dir
		*(local+1) = '\0';
	}
	//try to access (but NOT create) a valid trash dir there
	s = e2_utils_strcat (".Trash", suffix);
 	tlocal = g_build_filename (local, s, "files", NULL);	//no translation
	g_free (s);
	if (e2_fs_access3 (tlocal, W_OK | X_OK))
	{
		g_free (tlocal);
		tlocal = g_build_filename (local, ".Trash", "files", NULL);
		if (e2_fs_access3 (tlocal, W_OK | X_OK))
		{
//			if (last_trashpath != NULL)
//			{
//				g_free (last_trashpath);
//				last_trashpath = NULL;
//			}
			g_free (local);
			g_free (tlocal);
			//no deal, just use the default
			return (e2_cl_options.trash_dir);
		}
	}
	s = e2_utils_strcat (tlocal, G_DIR_SEPARATOR_S);
	if (last_trashpath != NULL)
		g_free (last_trashpath);
	last_trashpath = D_FILENAME_FROM_LOCALE (s);
	g_free (local);
	g_free (tlocal);
	g_free (s);

	return last_trashpath;
*/
	return e2_cl_options.trash_dir;
}
/**
@brief get path of directory nominated to contain emelfm2 custom icons
No checking of the directory's accessibilty is done
@param withtrailer TRUE if the returned string needs to have a trailing path-separator

@return newly-allocated, localised, absolute path string
*/
gchar *e2_utils_get_icons_path (gboolean withtrailer)
{
	const gchar *path;
	gchar *localpath, *freeme;
	if (e2_option_bool_get ("use-icon-dir"))
	{
		path = e2_option_str_get ("icon-dir");
		localpath = D_FILENAME_TO_LOCALE (path);
		if (!g_path_is_absolute (localpath))
		{
			freeme = localpath;
			localpath = g_build_filename (ICON_DIR, localpath, NULL);
			g_free (freeme);
		}
/*	//default icons in config dir if that's usable
		if (!e2_fs_is_dir3 (localpath E2_ERR_NONE()))
			openpath = g_strconcat (
#ifdef E2_FAM
				e2_cl_options.config_dir,
#else
				g_get_home_dir (), G_DIR_SEPARATOR_S,
#endif
				_("icons"), NULL);
*/

		if (g_str_has_suffix (localpath, G_DIR_SEPARATOR_S))
		{
			if (!withtrailer)
				*(localpath + strlen (localpath) - sizeof(gchar)) = '\0';
		}
		else if (withtrailer)
		{
			freeme = localpath;
			localpath = e2_utils_strcat (freeme, G_DIR_SEPARATOR_S);
			g_free (freeme);
		}
	}
	else
	{
		path = (withtrailer) ? ICON_DIR G_DIR_SEPARATOR_S : ICON_DIR;	//localised
		localpath = g_strdup (path);
	}
	return localpath;
}
#ifdef E2_IMAGECACHE
gint iconsizes [GTK_ICON_SIZE_DIALOG+1];
/**
@brief determine all icon pixel-sizes

assumes width = height
@return
*/
void e2_utils_init_icon_sizes (void)
{
	GtkIconSize size;
/*	0 invalid/theme, 1 menu, 2 toolbar small, 3 toolbar large, 4 button, 5 dnd, 6 dialog
	GTK_ICON_SIZE_INVALID, GTK_ICON_SIZE_MENU, GTK_ICON_SIZE_SMALL_TOOLBAR,
	GTK_ICON_SIZE_LARGE_TOOLBAR, GTK_ICON_SIZE_BUTTON, GTK_ICON_SIZE_DND,
	GTK_ICON_SIZE_DIALOG
*/
	gint wide, high;
	gint defsizes[] = {18, 16, 18, 24, 20, 32, 48};	//default sizes
	GtkSettings* defs = gtk_settings_get_default ();
	gchar *sizestr, *this;
	g_object_get (G_OBJECT (defs), "gtk-icon-sizes", &sizestr, NULL);
	//sizestr = NULL or "gtk-menu=16,16;gtk-button=20,20 ..."
/*	if (sizestr == NULL)
	{ //this is one way to work around strange behaviour of gtk_icon_size_get_name ();
		gchar *names[] = {
			"", "menu", "toolbar-small", "toolbar-large", "button", "dnd", "dialog"
			};	//CHECKME
	} */

	//get the pixel-size corresponding to each GtkIconSize
	for (size = GTK_ICON_SIZE_MENU; size <= GTK_ICON_SIZE_DIALOG; size++)
	{
		if (sizestr == NULL)
		{
			if (gtk_icon_size_lookup_for_settings (defs, size, &wide, &high))
				iconsizes [size] = high;
			else
				iconsizes [size] = defsizes [size];
		}
		else
		{
			gchar *s, *p;
//			this = g_strconcat ("gtk-", names [size], "=", NULL);
			this = (gchar *) gtk_icon_size_get_name (size);
			if (this == NULL)
			{  //could be a malformed theme-descriptor string
				//and for gtk 2.6.7/8 at least - strange behaviour - GTK_ICON_SIZE_MENU
				//doesn't match here, but does match if we call the fn later!
				if (gtk_icon_size_lookup_for_settings (defs, size, &wide, &high))
					iconsizes [size] = high;
				else
					iconsizes [size] = defsizes [size];
			}
			else
			{
				s = strstr (sizestr, this);
				if (s != NULL)
				{	//the theme sets this icon size
					s += strlen (this) + 1;	//skip the descriptor and "="
					p = strchr (s, ',');	//always ascii
					s = g_strndup (s, (p-s));
					iconsizes [size] = atoi (s);
					g_free (s);
				}
				else
				{
					if (gtk_icon_size_lookup_for_settings (defs, size, &wide, &high))
						iconsizes [size] = high;
					else
						iconsizes [size] = defsizes [size];
				}
	//			g_free (this);
			}
		}
	}

	//now set the default
/* gtk 2.6.8 spits warning about gtk-toolbar-icon-size property
	not existing, tho' API doco says it does !
	size = GTK_ICON_SIZE_INVALID;
	g_object_get (G_OBJECT (defs), "gtk-toolbar-icon-size", &size, NULL);
	if (size == GTK_ICON_SIZE_INVALID)
		size = GTK_ICON_SIZE_LARGE_TOOLBAR;
	iconsizes [0] = iconsizes [size]; */
	iconsizes [0] = iconsizes [GTK_ICON_SIZE_LARGE_TOOLBAR];
}
/**
@brief get icon pixel size for icon size enumerator @a size

@param size GtkIconSize value

@return icon pixelsize
*/
gint e2_utils_get_icon_size (GtkIconSize size)
{
	return iconsizes [size];
}
/**
@brief get icon size enumerator for icon which is closest-below @a psize

@param psize icon pixel size

@return iconsize
*/
GtkIconSize e2_utils_get_best_icon_size (gint psize)
{
	GtkIconSize i, isz = GTK_ICON_SIZE_MENU;
	gint psz = 0;
	for (i = GTK_ICON_SIZE_MENU; i <= GTK_ICON_SIZE_DIALOG; i++)
	{
		if (iconsizes [i] > psz && iconsizes [i] <= psize)
		{
			isz = i;
			psz = iconsizes [i];
		}
	}

	return isz;
}
#endif //def E2_IMAGECACHE
/**
@brief determine whether @a name represents a gtk stock icon

@param name string with icon name, a file path/name or "gtk-*"

@return TRUE if @a name represents a stock-icon
*/
gboolean e2_utils_check_stock_icon (const gchar *name)
{
/* gtk_stock_lookup is badly unreliable !!
	GtkStockItem item;
	return (gtk_stock_lookup (rt->icon, &item)) */
	//quick n dirty check
//	return (g_str_has_prefix (name, "gtk-"));
	gboolean retval = FALSE;
	if (g_str_has_prefix (name, "gtk-"))
	{
		GSList *tmp, *ids = gtk_stock_list_ids ();
		for (tmp = ids; tmp != NULL; tmp = g_slist_next (tmp))
		{
			if (!retval && g_str_equal ((gchar *)tmp->data, name))
				retval = TRUE;
			g_free (tmp->data);
		}
		g_slist_free (ids);
	}
	return retval;
}
/**
@brief get output-pane font name

@return string with the name
*/
const gchar *e2_utils_get_output_font (void)
{
	gchar *fntname;
	if (e2_option_bool_get ("custom-output-font"))
	{
		fntname = e2_option_str_get ("output-font");
		if (*fntname == '\0')
			fntname = NULL;
	}
	else
		fntname = NULL;
	if (fntname == NULL)
	{
		GtkSettings* defs = gtk_settings_get_default ();
		g_object_get (G_OBJECT (defs), "gtk-font-name", &fntname, NULL);
		if (fntname == NULL)	//CHECKME needed ?
		{
			printd (WARN, "No default font detected");
			fntname = "Sans 10";
		}
	}
	return fntname;
}
/**
@brief update gtk properties like menu delays

update gtk internal properties. these are currently only menu
popup and popdown delays. this function is usually called
after configuration changes to update the gtk properties.

@return
*/
void e2_utils_update_gtk_settings (void)
{
	GtkSettings *defs = gtk_settings_get_default ();
	gint delay_up = e2_option_int_get ("submenu-up-delay");
	gint delay_down = e2_option_int_get ("submenu-down-delay");
	//be on the safe side
	if (delay_up < 0) delay_up = 0;
	if (delay_down < 0) delay_down = 0;
	gtk_settings_set_long_property (defs, "gtk-menu-popup-delay",
		delay_up, "XProperty");
	gtk_settings_set_long_property (defs, "gtk-menu-popdown-delay",
		delay_down, "XProperty");
	gtk_settings_set_long_property (defs, "gtk-menubar-popup-delay",
		delay_up, "XProperty");
	//set doubleclick interval threshold to gtk's value
	g_object_get (G_OBJECT (defs), "gtk-double-click-time", &click_interval, NULL);
	if (click_interval < E2_CLICKINTERVAL)
	{
		click_interval = E2_CLICKINTERVAL;
		g_object_set (G_OBJECT (defs), "gtk-double-click-time", E2_CLICKINTERVAL, NULL);
	}
//	gtk_rc_parse_string ("style \"e2_default\" { GtkComboBoxEntry::appears-as-list = 1 } class \"*\" style \"e2_default\"");
}

/**
@brief helper function to find matches for last or only path segment
Arrives here with BGL off
Note: to eliminate BGL-racing, no UI-change from here
@param parent absolute path of dir being processed, localised string with trailer
@param itemname name of discovered item, localised string
@param found pointer to store for list of data items for @a parent
@param pair pointer to struct with parameters for use here

@return TRUE to signal the read has not been aborted
*/
static gboolean _e2_utils_drcb_match_wild_last (VPATH *parent,
	const gchar *itemname, GList **found, E2_Duo *pair)
{
	if (!g_str_equal (itemname, ".."))
	{
		GPatternSpec *pattern = (GPatternSpec *)pair->b;
		gchar *utfname = F_FILENAME_FROM_LOCALE (itemname);
		if (g_pattern_match_string (pattern, utfname))
		{
			gchar *escname;
			gboolean all = GPOINTER_TO_INT (pair->a);
			if (all)
			{
				escname = e2_utf8_escape (utfname, ' ');
				*found = g_list_append (*found, escname);
			}
			else
			{
				gchar *freeme = e2_utils_strcat (VPSTR (parent), itemname);
#ifdef E2_VFS
				VPATH ddata = { freeme, parent->spacedata };
				if (e2_fs_is_dir3 (&ddata E2_ERR_NONE()))
#else
				if (e2_fs_is_dir3 (freeme E2_ERR_NONE()))
#endif
				{
					escname = e2_utf8_escape (utfname, ' ');
					*found = g_list_append (*found, escname);
				}
				g_free (freeme);
			}
		}
		F_FREE (utfname);
	}
	return TRUE;
}
/**
@brief find matches for last or only path segment of @a arg, if that contains * and/or ?
Searching is in $PATH directories
@a arg may include an absolute or relative path.
Relative path is assumed relative to curr_view->dir
If @a arg does include any path, that is expected to be all non-wild.
"." and ".." items are excluded
Expects BGL to be on/closed on arrival here
@param arg path or itemname which may have wildcard(s) in its only or last segment, utf-8 string
@param all TRUE to match any type of item, FALSE to match dirs only

@return list of utf8 names which match, or 0x1 if if no match was found,
or NULL if there is no wildcard in @a arg or an error occurred
*/
static GList *_e2_utils_match_wild_last (gchar *arg, gboolean all)
{
	gboolean freepath, freename;
	E2_FSType dirtype;
	gchar *name, *path, *localpath, *freeme;
#ifdef E2_VFSTMP
	//FIXME relevant path is ?
	dirtype = ?;
#else
	path = g_path_get_dirname (arg);
	dirtype = FS_LOCAL;	//FIXME
#endif
	if (g_str_equal (path, "."))
	{	//no path in arg
		g_free (path);
		if (strchr (arg, '*') == NULL && strchr (arg, '?') == NULL)
			return NULL;
		if (g_str_has_prefix (arg, "$"))	//some shell or language variables can be ignored
			return GINT_TO_POINTER (1);
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
		path = curr_view->dir;
#endif
		name = arg;
		freepath = freename = FALSE;
	}
	else
	{	//path was found
		name = g_path_get_basename (arg);
		if (g_str_equal (name, ".")
			|| (strchr (name, '*') == NULL && strchr (name, '?') == NULL))
		{
			g_free (path);
			g_free (name);
			return NULL;
		}
		//ensure trailing separator
		freeme = path;
		path = g_strconcat (path, G_DIR_SEPARATOR_S, NULL);
		g_free (freeme);
		if (!g_path_is_absolute (path))
		{
			freeme = path;
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
			path = e2_utils_dircat (curr_view, path, FALSE);
#endif
			g_free (freeme);
		}
		freepath = freename = TRUE;
	}

	gdk_threads_leave ();	//needed for dirforeach
	localpath = F_FILENAME_TO_LOCALE (path);
	GPatternSpec *pattern = g_pattern_spec_new (name);
	E2_Duo pair = { GINT_TO_POINTER (all), pattern };

#ifdef E2_VFS
#ifdef E2_VFSTMP
	FIXME placedata
#endif
	VPATH ddata = { localpath, NULL };
	GList *matches = (GList *)e2_fs_dir_foreach (&ddata,
#else
	GList *matches = (GList *)e2_fs_dir_foreach (localpath,
#endif
		E2_DIRWATCH_NO,	//$PATH is local places only
		_e2_utils_drcb_match_wild_last, &pair, NULL E2_ERR_NONE());
	//conform results to API
	if (E2DREAD_FAILED (matches))
		matches = NULL;
	else if (matches == NULL)
		matches = (GList *) 1;

	g_pattern_spec_free (pattern);
	if (freepath)
		g_free (path);
	F_FREE (localpath);
	if (freename)
		g_free (name);
	gdk_threads_enter ();
	return matches;
}

/**
@brief helper function to match path which has wildcard(s) in its element(s)
Arrives with BGL open/off
Note: to eliminate BGL-racing, no UI-change from here
Reentrant use of _e2_utils_match_wild_path() assmumes BGL is still off here
@param parent UNUSED absolute path of dir being processed, localised string with or without trailer
@param itemname name of discovered item, localised string
@param found pointer to store for list of data items for @a parent
@param pattern the matcher for desired items

@return TRUE to signal the read has not been aborted
*/
static gboolean _e2_utils_drcb_match_wild_path (VPATH *parent,
	const gchar *itemname, GList **found, GPatternSpec *pattern)
{
	if (!g_str_equal (itemname, ".."))	//"." entries are filtered at source
	{
		gchar *utfname = F_FILENAME_FROM_LOCALE (itemname);
		if (g_pattern_match_string (pattern, utfname))
			*found = g_list_append (*found, g_strdup (itemname));

		F_FREE (utfname);
	}
	return TRUE;
}
/**
@brief recursively find a path that is a descendant of @a parent and otherwise matches wildcard data in @a wdata
This scans @a parent depth-first, until either a complete match is found, or
no match is possible, in which case it backs up a level and tries the next
match at that level, and so on.
Matching path segments are stored in @a wdata
Expects BGL to be closed on arrival here
@param parent absolute path, no wildcard char(s) or redundant separators or trailer, localised string
@param wdata pointer to struct with parameters for use here

@return TRUE if a matching path was found
*/
static gboolean _e2_utils_match_wild_path (gchar *parent, E2_WildData *wdata)
{
	gboolean retval;
	guint i, here;
	gchar *format;
	GString *checker;
	GPatternSpec *pattern;
	GList *matches, *member;
#ifdef E2_VFS
	VPATH ddata;
#ifdef E2_VFSTMP
	FIXME valid spacedata for parent
#endif
	ddata.spacedata = NULL;
#endif
	gdk_threads_leave ();	//needed for dirforeach

	here = wdata->curr_depth;
	checker = g_string_sized_new (256);
	checker = g_string_append (checker, parent);
	pattern = g_pattern_spec_new (wdata->path_elements[here]);
#ifdef E2_VFS
	ddata.localpath = checker->str;
	matches = (GList *)e2_fs_dir_foreach (&ddata,
#else
	matches = (GList *)e2_fs_dir_foreach (checker->str,
#endif
		E2_DIRWATCH_NO,	//$PATH is local places only
		_e2_utils_drcb_match_wild_path, pattern, NULL E2_ERR_NONE());
	retval = !(matches == NULL || E2DREAD_FAILED (matches));

	if (retval)
	{
		if (here < wdata->last_wild_depth)
		{
			i = checker->len;	//for truncating
			format = (i == sizeof (gchar)) ? "%s" : G_DIR_SEPARATOR_S"%s";
			wdata->curr_depth++;
			for (member = matches; member != NULL; member = member->next)
			{
				g_string_append_printf (checker, format, (gchar *)member->data);
#ifdef E2_VFS
				ddata.localpath = checker->str;
				retval = !e2_fs_stat (&ddata, wdata->statptr E2_ERR_NONE())
#else
				retval = !e2_fs_stat (checker->str, wdata->statptr E2_ERR_NONE())
#endif
						&& S_ISDIR (wdata->statptr->st_mode)
						&& _e2_utils_match_wild_path (checker->str, wdata);	//recurse into dir at next level
				if (retval)
					break;	//everything afterward is matched
				//not a dir or not completely matched downwards
				//prepare to try again at this level
				checker = g_string_truncate (checker, i);
			}
			if (!retval)	//preserve success pointer if all was successful
				wdata->curr_depth--;
		}
		else
			member = matches;

		if (retval)
			wdata->path_matches [here] = g_strdup ((gchar *)member->data);

		e2_list_free_with_data (&matches);
	}

	g_pattern_spec_free (pattern);
	g_string_free (checker, TRUE);

	gdk_threads_enter ();
	return retval;
}
/**
@brief replace any wildcard character(s) '*' and '?' in @a string
If it's quoted (" or ') nothing is done. If it includes path separator(s),
any wildcard in path element(s) before the last one is replaced by the
fist-found valid match, or if there's no match, the expansion fails.
Any wildcard in the last (or only) path segment is expanded to all matches,
with prepended path if appropriate
Downstream code assumes BGL is closed, here
@param string a whitespace-separated "segment" of a commandline utf-8 string maybe with wildcard(s) to replace

@return newly-allocated string, copy of @a string or string with wildcards replaced
*/
static gchar *_e2_utils_match_wild_segment (gchar *string)
{
	if (strchr (string, '*') == NULL && strchr (string, '?') == NULL)
		return (g_strdup (string));
	gint len = strlen (string);
	if (*string == '"' && *(string + len - 1) == '"')
		return (g_strdup (string));
	if (*string == '\'' && *(string + len - 1) == '\'')
		return (g_strdup (string));

	guint last_wild_depth, last_depth;
	gchar *freeme, *path, *parent_path, *expanded;
	GList *matches;

	//trailing separator confuses things
	if (g_str_has_suffix (string, G_DIR_SEPARATOR_S))
		*(string + len - 1) = '\0';

#ifdef E2_VFSTMP
	//FIXME path when not mounted local
#endif
	path = g_path_get_dirname (string);	//no trailing separator
	if (!g_str_equal (path, "."))
	{	//the string has a path
		gboolean abs = g_path_is_absolute (path);
		if (strchr (path, '*') != NULL || strchr (path, '?') != NULL)
		{
			guint i, count;
			gchar *s;
			E2_WildData wdata = {0};

			g_free (path);
			//make sure processed string is absolute
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
			path = (abs) ?
				g_strdup (string):	//use whole string in case basename is also wild
				e2_utils_dircat (curr_view, string, FALSE);
#endif
			//making GPatternSpec's requires utf-8 patterns
			wdata.path_elements = g_strsplit (path, G_DIR_SEPARATOR_S, -1);
			//for the matched elements ... setup for strfreev later
			count = g_strv_length (wdata.path_elements);
			wdata.path_matches = (gchar **)
#ifdef USE_GLIB2_8
				g_try_malloc0 ((count + 1) * sizeof (gchar *));
#else
				calloc (count + 1, sizeof (gchar *));
#endif
			if (wdata.path_matches == NULL)
			{
				g_free (path);
				g_strfreev (wdata.path_elements);
				return (g_strdup (string));
			}
			//find the first reportable segment of the path
/*			if (!abs)
			{
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
				s = curr_view->dir;
#endif
				while (*s != '\0')
				{
					if (*s == G_DIR_SEPARATOR)
						wdata.first_depth++;
					s++;
				}
			}
*/
			last_wild_depth = 0;	//warning prevention
			//find the bounds of the scan, skipping empty fictitious segment
			//from before leading separator
			for (i = 1; i < count; i++)
			{
				s = wdata.path_elements[i];
				if (strchr (s, '*') != NULL || strchr (s, '*') != NULL)
				{
					if (wdata.first_wild_depth == 0)
						wdata.first_wild_depth = i;//highest level that has a wildcard char
					last_wild_depth = i;//lowest level that has a wildcard char
				}
			}
			last_depth = i - 1;	//level of the last path segment
			//extra match needed when path extends after the last wild segment
			if (last_wild_depth < last_depth)
				last_wild_depth++;
/*			else
			{	//special treatment if 2nd last segment of string is wild and last is explicit
				gchar *name = g_path_get_basename (string);
				if (strchr (name, '*') != NULL || strchr (name, '*') != NULL)
				{
					//FIXME allocate a replacement array with 1 more pointer
					local = F_FILENAME_TO_LOCALE (name);
					wdata.path_segments[wdata.last_depth] = local;
					wdata.path_segments[wdata.last_depth+1] = NULL;
					F_FREE (local);
					wdata.last_wild_depth++;
				}
				g_free (name);
			}
*/
			if (last_wild_depth == count - 1)	//the last item also is wild
				last_depth--;	//later, we don't want to use the last matched element

			wdata.curr_depth = wdata.first_wild_depth;
			wdata.last_wild_depth = last_wild_depth;
			wdata.last_depth = last_depth;

			//construct non-wild localised parent path, to start the scan (no trailing separator)
			//and fill corresponding elements of the matches array
			wdata.path_matches [0] = g_strdup ("");
			if (wdata.first_wild_depth == 1)
				s = g_strdup (G_DIR_SEPARATOR_S);
			else
			{
				s = g_strdup ("");
				for (i = 1; i < wdata.first_wild_depth; i++)
				{
					wdata.path_matches[i] = D_FILENAME_TO_LOCALE (wdata.path_elements[i]);
					freeme = s;
					s = g_strconcat
						(freeme, G_DIR_SEPARATOR_S, wdata.path_matches[i], NULL);
					g_free (freeme);
				}
			}

			//scan the filesystem to match the full path
			struct stat statbuf;
			wdata.statptr = &statbuf;
			if (_e2_utils_match_wild_path (s, &wdata))
			{
				g_free (s);
				//create "real" parent path string from the matched segments, with trailer
#ifdef E2_VFSTMP
	//CHECKME dir when not mounted local
#endif
				//FIXME strip any prepended curr_view->dir
				//FIXME ensure this array is fully populated
				s = g_strdup ("");

//				for (i = wdata.first_depth; i <= last_depth; i++)
				for (i = 0; i <= last_depth; i++)
				{
					freeme = s;
					s = g_strconcat
						(freeme, wdata.path_matches[i], G_DIR_SEPARATOR_S, NULL);
					g_free (freeme);
				}
				parent_path = D_FILENAME_FROM_LOCALE (s);
			}
			else	//no match found for the path
			{
				if (!abs)
				{	//we want the supplied path only
					g_free (path);
					path = g_path_get_dirname (string);
				}
				freeme = g_path_get_dirname (string);
				parent_path = g_strconcat (freeme, G_DIR_SEPARATOR_S, NULL);
				g_free (freeme);
			}

			g_free (s);

			g_strfreev (wdata.path_elements);
			g_strfreev (wdata.path_matches);
		}
		else	//the path is all explicit
		{
			if (!abs)
			{	//we want the supplied path only
				g_free (path);
				path = g_path_get_dirname (string);
			}
			parent_path = g_strconcat (path, G_DIR_SEPARATOR_S, NULL);
		}
		//allocated parent_path has no prepended cwd, and has trailing separator

		//now expand the last segment in the supplied path string
		gchar *name = g_path_get_basename (string);
		if (strchr (name, '*') != NULL || strchr (name, '*') != NULL)
		{	//last path segment is wild
			freeme = g_strconcat (parent_path, name, NULL);
			matches = _e2_utils_match_wild_last (freeme, TRUE);
			g_free (freeme);
			if (matches == NULL //error
				|| matches == GINT_TO_POINTER (0x1)) //no match found
				//send back is what we have now
				expanded = g_strconcat (parent_path, name, NULL);
			else
			{	//append each matched item to matched path
				expanded = g_strdup ("");
				GList *tmp;
				for (tmp = matches; tmp != NULL; tmp = tmp->next)
				{
					freeme = expanded;
					expanded = g_strconcat (freeme, " ", parent_path,
						(gchar*)tmp->data, NULL);
					g_free (freeme);
				}
				e2_list_free_with_data (&matches);
			}
		}
		else	//last path segment is explicit
			expanded = g_strconcat (parent_path, name, NULL);

		g_free (parent_path);
		g_free (name);
	}
	else
	{	//no path in the string
		matches = _e2_utils_match_wild_last (string, TRUE);
		if (matches == NULL) //no wildcard in the name (or error)
			expanded = g_strdup (string);
		else if (matches == GINT_TO_POINTER (0x1)) //no match
			expanded = g_strdup ("");
		else
		{
			expanded = g_strdup ("");
			GList *tmp;
			for (tmp = matches; tmp != NULL; tmp = tmp->next)
			{
				freeme = expanded;
				expanded = g_strconcat (freeme, " ", (gchar*)tmp->data, NULL);
				g_free (freeme);
			}
			e2_list_free_with_data (&matches);
		}
	}

	g_free (path);
	return expanded;
}
/**
@brief replace wildcard character(s) '*' and '?' in commandline utf-8 string @a raw
@a raw may include whitespace gap(s), in which case each gap-separated
"element" is separately handled.
If any element is quoted (by " or ') no wildcard is expanded in that element.
If any element includes path separator(s), any wildcarded path segment(s)
before the last one are replaced by the first-found match, or if there's no
match, the whole expansion fails. Any wildcard in the last (or only) path segment
is expanded to _all_ matches, with prepended path if appropriate.
@param raw freeable string maybe with wildcard(s) to replace

@return @a raw, or a replacement string with wildcards replaced
*/
gchar *e2_utils_replace_wildcards (gchar *raw)
{
	//quick check
	if (strchr (raw, '*') == NULL	//if always ascii ;, don't need g_utf8_strchr()
		&& strchr (raw, '?') == NULL)	//if always ascii ;, don't need g_utf8_strchr()
		return raw;

	gchar *p, *s, *freeme, *expanded = g_strdup ("");
	gchar sep[2] = {'\0', '\0'};
	gint cnt1 = 0; //counter for ' chars
	gint cnt2 = 0; //counter for " chars

	s = p = raw;
	while (*p != '\0')
	{
		if (*p == '\'')
		{
			if (p == raw || *(p-1) != '\\')
				cnt1++;
		}
		else if (*p == '"')
		{
			if (p == raw || *(p-1) != '\\')
				cnt2++;
		}
		else if (*p == ' ' || *p == '\t')
		{	//check if separator seems to be outside parentheses
			if (cnt1 % 2 == 0 && cnt2 % 2 == 0)
			{ //found a gap in the command string
				sep[0] = *p;
				*p = '\0';
				s = _e2_utils_match_wild_segment (s);
				freeme = expanded;
				expanded = g_strconcat (freeme, s, sep, NULL);
				g_free (s);
				g_free (freeme);
				*p = sep[0];
				//resume scanning
				s = e2_utils_pass_whitespace (p+1);
				if (s == NULL) 	//irrelevant trailing whitespace
					break;
				p = s;
				continue;
			}
		}
		p++;
	}
	if (s != NULL && *s != '\0')
	{
		//process last (or only) command_element
		s = _e2_utils_match_wild_segment (s);
		//append string to buffer
		freeme = expanded;
		expanded = g_strconcat (freeme, s, NULL);
		g_free (s);
		g_free (freeme);
	}

	g_free (raw);
	return expanded;
}
//no. of single-quote characters found in the replacement string,
//up to the end of the current segment
static gint p_count;
//and correspondingly for double-quotes
static gint p2_count;
static gchar *dollar = "$";
/**
@brief add segment to the replacement string

Empty strings are ignored. The index for the next segment
is updated.
The count of single-parentheses is updated. This uses
single-byte ascii scanning.

@param string string to record
@param join pointer to the array of segments of the replacement string
@param join_count pointer to index of the next segment

@return
*/
static void _e2_utils_replace_vars_add (gchar *string, gchar *join[], gint *join_count)
{
	if (*string == '\0')
		return;	//no point in adding empty strings
	join[*join_count] = string;
	(*join_count)++;
	//update count of ' characters sofar detected
	gchar *p = string;
	while (*p != '\0')
	{
		if (*p != '\\')
		{
			if (*p++ == '\'')
				p_count++;
			if (*p == '"')
				p2_count++;
		}
		else
			p++;
	}
}
/**
@brief add original segment to the replacement string

To avoid leaks, this adds 2 segments - one with just the
separator "$" and the second with the ignored segment
of the original string.
The count of single-parentheses is updated. This uses
single-byte ascii scanning.

@param string string to record
@param join pointer to the array of segments of the replacement string
@param join_count pointer to index of the next segment

@return
*/
static void _e2_utils_replace_vars_ignore (gchar *string, gchar *join[], gint *join_count)
{
	join[*join_count] = dollar;
	(*join_count)++;
	_e2_utils_replace_vars_add (string, join, join_count);
}
/**
@brief replace variables in a string

Replaces all relevant '~' characters, and all valid
environment variables and option variables, in @a raw.
~ will be replaced if preceeded by nothing or whitespace,
and followed by nothing or whitespace or '/'
Environment references and internal variables must have the form @c \$VAR or
@c \${VAR} where @c VAR is the name of the variable. Internal variables get
precedence over environment variable with the same name.
As a special case, $$ will be replaced by active directory
(without trailing /), in effect = `pwd`
Option references must have the form @c \$[VAR] where
@c VAR is the 'internal' name of the option.
Unrecognised variables are ignored. Environment variables
inside single parentheses are ignored. Strings prefaced by
"\$" are ignored.

@code
gchar *str = e2_utils_replace_vars ("my home is \${HOME} and my e2 terminal is \$[command-xterm]");
@endcode

@param raw utf string maybe with variable(s) etc to replace

@return newly allocated string, with variables (if any) replaced as appropriate
*/
gchar *e2_utils_replace_vars (gchar *raw)
{
//	printd (DEBUG, "e2_utils_replace_vars (raw:%s)", raw);
	//replace all relevant occurrences of ~ with ${HOME}
	gchar *s = g_strdup (raw), *p = s, *h = "${HOME}", *freeme;
	gint cnt1 = 0; //counter for ' chars
	gint cnt2 = 0; //counter for " chars
	while (*p != '\0')
	{
		if (*p == '\'')
		{
			if (p == s || *(p-1) != '\\')
				cnt1++;
		}
		else if (*p == '"')
		{
			if (p == s || *(p-1) != '\\')
				cnt2++;
		}
		else if (*p == '~')	//always ascii ~, don't need utf8 handler
		{	//check if tilde seems to be outside parentheses
			if (cnt1 % 2 == 0 && cnt2 % 2 == 0)
			{ //found a candidate for replacement
				if (
					   (p == s || *(p-1) == ' ' || *(p-1) == '\t' ) //preceeded by nothing or whitespace
					&& (*(p+1) == G_DIR_SEPARATOR //and followed by '/'
							|| *(p+1) == '\0' || *(p+1) == ' ' || *(p+1) == '\t' )	//or lone char in argument
					)
				{
					*p = '\0';
					freeme = s;
					s = g_strconcat (s, h, p+1, NULL);
					p = s + (p - freeme);
					g_free (freeme);
					continue;
				}
			}
		}
		p++;
	}

	//replace $... occurrences, taking quoting into account
	if (strchr (s, '$') != NULL)	//if always ascii $, don't need g_utf8_strchr()
	{
		//break into pieces and count them
		gchar **split = g_strsplit (s, "$", -1);
		gint split_count = 0;
		while (split[split_count] != NULL)
			split_count++;
		//init the stack-store for the pieces of the replacement string
		gint join_count = 0;
		gchar *join[split_count * 2 + 1];
		//init the counts of quote-chars
		p_count = p2_count = 0;
		gchar *rest;

		_e2_utils_replace_vars_add (split[0], join, &join_count);	//save the segment before the 1st '$'

		gint i;
		//scan from after 1st '$'
		for (i = 1; i < split_count; i++)
		{
			if (*split[i] == '\0')	//2 '$' chars in a row, or trailing '$'
			{
				if (split[i+1] != NULL)
				{
#ifdef E2_VFSTMP
	//FIXME dir when not mounted local
#else
					gchar *cwd = g_strdup (curr_view->dir);
#endif
					//generally strip trailer
					gint len = strlen (cwd);
					if (len > 1)
						*(cwd + len - 1) = '\0';
					_e2_utils_replace_vars_add (cwd, join, &join_count);
				}
				else	//trailing $
					_e2_utils_replace_vars_add ("$", join, &join_count);
			}
			else if (g_str_has_suffix (split[i-1], "\\")	//escaped '$'
					  || *split[i] == '(')	//shell command
				_e2_utils_replace_vars_ignore (split[i], join, &join_count);

			else if (*split[i] == '[')	//option to replace
			{
				//no protection check for internal vars
				gchar *rest = strchr (split[i], ']'); //if always ascii ], don't need g_utf8_strchr()
				if (rest != NULL)
				{
					*rest++ = '\0';
					E2_OptionSet *set = e2_option_get (split[i]+1);
					if (set != NULL)
					{
						join[join_count++] = e2_option_str_get_direct (set);
						_e2_utils_replace_vars_add (rest, join, &join_count);
					}
					else
					{
						*(--rest) = ']';
						_e2_utils_replace_vars_ignore (split[i], join, &join_count);
					}
				}
				else
					_e2_utils_replace_vars_ignore (split[i], join, &join_count);
			}

			else if (*split[i] == '{')	//variable to replace
			{
				//make sure separator was outside ' '
				if (p_count % 2 == 1)
					_e2_utils_replace_vars_ignore (split[i], join, &join_count);
				else
				{
					rest = strchr (split[i], '}');	//if always ascii }, don't need g_utf8_strchr()
					if (rest != NULL)
					{
						*rest++ = '\0';
						const gchar *env = e2_command_get_variable_value (split[i]+1);
						if (env == NULL)
							env = g_getenv (split[i]+1);
						if (env != NULL)
						{
							join[join_count++] = (gchar *) env;
							_e2_utils_replace_vars_add (rest, join, &join_count);
						}
						else
						{
							*(--rest) = '}';
							_e2_utils_replace_vars_ignore (split[i], join, &join_count);
						}
					}
					else
						_e2_utils_replace_vars_ignore (split[i], join, &join_count);
				}
			}
			else	//something else (maybe a variable)
			{
				//make sure separator was outside ' '
				if (p_count % 2 == 1)
					_e2_utils_replace_vars_ignore (split[i], join, &join_count);
				else
				{
					gchar c = 0;
					gchar *st;
					rest = e2_utils_find_whitespace (split[i]);
					if (p2_count % 2 == 1)
					{	//inside double-quote
						st = strchr (split[i], '"');
						if (st != NULL && (rest == NULL || st < rest))
							rest = st;
					}
					if (rest != NULL)
					{
						c = *rest;
						*rest = '\0';
					}
					st = e2_utils_strcat ("$", split[i]);
					const gchar *env = e2_command_get_variable_value (st);
					g_free (st);
					if (env == NULL)
						env = g_getenv (split[i]); //WHAT IS THIS FOR?
					if (rest == NULL)
					{
						if (env != NULL)
							join[join_count++] = (gchar *) env;
						else
							_e2_utils_replace_vars_ignore (split[i], join, &join_count);
					}
					else //rest != NULL
					{
						*rest = c;
						if (env != NULL)
						{
							join[join_count++] = (gchar *) env;
							_e2_utils_replace_vars_add (rest, join, &join_count);
						}
						else
							_e2_utils_replace_vars_ignore (split[i], join, &join_count);
					}
				}
			}
		}
		join[join_count] = NULL;
		g_free (s);
		s = g_strjoinv (NULL, join);

		g_strfreev (split);
	}
	return s;
}
/**
@brief get coordinates of @a widget relative to its current screen
@param widget the activated widget whose position is to be calculated
@param x pointer to gint storage for the x (left) coordinate
@param y pointer to gint storage for the y (top) coordinate
@return
*/
void e2_utils_get_abs_pos (GtkWidget *widget, gint *x, gint *y)
{
	if (GTK_WIDGET_TOPLEVEL (widget))
		gdk_window_get_position (widget->window, x, y);
	else
	{
		GdkWindow *window;
		GtkWidget *current = widget;
		while (GTK_WIDGET_NO_WINDOW (current) && (current = gtk_widget_get_parent(current)) != NULL) {}

		if (current != NULL)
			window = current->window;
		else	//should never happen
			window = app.main_window->window;

		gdk_window_get_origin (window, x, y);

		*x += widget->allocation.x;
		*y += widget->allocation.y;
	}
}
/**
@brief get current modifiers mask
@return
*/
GdkModifierType e2_utils_get_modifiers (void)
{
	GdkDisplay *display =
	gdk_display_manager_get_default_display (gdk_display_manager_get());
	GdkModifierType mask;
	gdk_display_get_pointer (display, NULL, NULL, NULL, &mask);
//	guint modifiers = gtk_accelerator_get_default_mod_mask ();
//	return mask & modifiers;
	return mask;
}
/**
@brief emit beep sound
@return
*/
void e2_utils_beep (void)
{
	GdkDisplay *display =
	gdk_display_manager_get_default_display (gdk_display_manager_get());
	gdk_display_beep (display);
}
/**
@brief check whether more than 1 item is selected
@param srclist glist of selected items
@return TRUE if more than 1 is selected
*/
gboolean e2_utils_multi_src (GList *srclist)
{
	gint ctr=0;
	for (; srclist != NULL ; srclist = srclist->next)
	{
		ctr++;
		if (ctr > 1) break;
	}
	return (ctr > 1);
}
/**
@brief get char (if any) used as mnemonic in translated @a label
@param label translated, utf8-compatible string which may include an '_'
		indicating the following char is a mnemonic
@return lower-case char, if there's a mnemonic, otherwise (gunichar)0
*/
gunichar e2_utils_get_mnemonic_char (gchar *label)
{
	gunichar c;
	gchar *uscore = g_utf8_strchr (label, -1, (gunichar)'_');
	if (uscore == NULL)
		c = (gunichar)'\0';
	else
	{
		uscore = g_utf8_next_char (uscore);
		if (*uscore == '\0')
			c = (gunichar)'\0';
		else
		{
			c = g_utf8_get_char_validated (uscore, -1);
			if (c == (gunichar)-1 || c == (gunichar)-2)
				c = (gunichar)'\0';
			else
				c = g_unichar_tolower (c);
		}
	}
	return c;
}
/**
@brief get gdk key code which matches the char (if any) used as mnemonic in translated @a label
@param label translated, utf8-compatible string which may include an '_'
		indicating the following char is a mnemonic
@return gdk keycode, if there's a mnemonic, otherwise 0
*/
guint e2_utils_get_mnemonic_keycode (gchar *label)
{
	gunichar c = e2_utils_get_mnemonic_char (label);
	guint retval = (c == (gunichar)'\0') ? 0 : gdk_unicode_to_keyval (c);
	return retval;
}
/**
@brief block all relevant signals to a thread
Posix doesn't specify which thread receives signals. So this func is generally
called inside newly-created threads to prevent signals (esp. SIGCHILD etc for
running commands) being delivered to the wrong thread.
@return
*/
void e2_utils_block_thread_signals (void)
{
	sigset_t set;
	sigfillset (&set);	//block all allowed signals
//	sigemptyset (&set);	//block SIGCHILD signals
//	sigaddset (&set, SIGCHLD);
	pthread_sigmask (SIG_BLOCK, &set, NULL);
}
