/* $Id: e2_output.c 854 2008-04-19 11:45:33Z tpgww $

Copyright (C) 2003-2008 tooar <tooar@gmx.net>
Portions copyright (C) 2004 Florian Zähringer <flo.zaehringer@web.de>
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
*/

/**
@file src/e2_output.c
@brief output pane creation and action functions

includes actions related to output contents, but not
to the output visibility
*/
/**
\page output the output pane

ToDo - descibe how this works

\section tabs output pane tabs

ToDo
*/

#include "emelfm2.h"
#include <string.h>
#include <pthread.h>
#include "e2_output.h"
#include "e2_dialog.h"
//for pane-text activation
#include "e2_task.h"
#include "e2_filetype.h"
#include "e2_context_menu.h"
//for selection-save
#include "e2_view_dialog.h"

#define E2_PANED_TOLERANCE 10

#define VOL volatile
#warning A lot of warnings about discarded qualifiers follow - ignore them
//#define VOL

static GtkWidget *_e2_output_create_view (E2_OutputTabRuntime *rt);
static void _e2_output_tabchange_cb (GtkNotebook *notebook,
	GtkNotebookPage *page, guint page_num, gpointer data);
#ifdef E2_TABS_DETACH
static void _e2_output_tabgone_cb (GtkNotebook *notebook, GtkWidget *child,
	guint page_num, GtkWidget *window);
#endif
//static gboolean output_activated = FALSE;
static GStaticRecMutex print_mutex;
extern pthread_mutex_t task_mutex;

#ifdef E2_TABS_DETACH
#define TARGET_NOTEBOOK_TAB 3
static GtkTargetEntry target_table2[] =
{
	{ "GTK_NOTEBOOK_TAB", GTK_TARGET_SAME_APP, TARGET_NOTEBOOK_TAB },
};
static guint n_targets2 = sizeof(target_table2) / sizeof(GtkTargetEntry);
#endif

  /*****************/
 /***** utils *****/
/*****************/
/**
@brief determine whether a position in the output pane text buffer is presently visible

@param text_view the output pane textview widget
@param iter pointer to data for the position in @a text_view that is to be checked

@return TRUE if @a iter relates to a point in @a text_view that is NOT presently visible
*/
static inline gboolean _e2_output_iter_offscreen
	(GtkTextView *text_view, VOLATILE GtkTextIter *iter)
{
	GdkRectangle visible_rect, iter_rect;
	gtk_text_view_get_visible_rect (text_view, &visible_rect);
	gtk_text_view_get_iter_location (text_view, iter, &iter_rect);
	return
	//this makes the pane scroll 1 line too many, but is faster
	 ((iter_rect.y + iter_rect.height) > (visible_rect.y + visible_rect.height)
	//this causes jiggling, due to trailing blank line (\n at line-ends)
	//being re-scrolled off-screeen-bottom
//	 ((iter_rect.y > (visible_rect.y + visible_rect.height)
	|| iter_rect.y < visible_rect.y);
}
/**
@brief move the 'page' displayed in the output pane, relative to its text content

@param down TRUE to move down, FALSE to move up
@param arg string containing a number, the no. of 'moves'
@param page TRUE move @a arg 'pages', FALSE move @a arg 'steps'

@return
*/
static void _e2_output_scroll_helper (gboolean down, gchar *arg, gboolean page)
{
	printd (DEBUG, "scroll_helper (down:%d,arg:%s,page:%d)", down, arg, page);
	E2_OutputTabRuntime *rt = &app.tab;
	g_return_if_fail (rt->scroll != NULL);
	gchar *end = NULL;
	gdouble times = 1.0;
	if (arg != NULL)
		times = g_ascii_strtod (arg, &end);
	if (end == arg)
		times = 1.0;
	GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment
		(GTK_SCROLLED_WINDOW (rt->scroll));
	gdouble value = gtk_adjustment_get_value (vadj);
	gint inc = page ? vadj->page_increment : vadj->step_increment;
	if (down)
	{
		value += (inc * times);
		if (value > (vadj->upper - vadj->page_size))
			value = vadj->upper - vadj->page_size;
	}
	else
		value -= (inc * times);

	gtk_adjustment_set_value (vadj, value);
}
/**
@brief set a mark at the position of the bottom-left of the output pane
This supports e2_output_scroll_to_bottom()
@param rt pointer to tab data

@return
*/
void e2_output_get_bottom_iter (E2_OutputTabRuntime *rt)
{
	g_return_if_fail (rt->scroll != NULL);

	GdkRectangle visible_rect;
	gtk_text_view_get_visible_rect (rt->text, &visible_rect);

	GtkTextIter iter;
	gtk_text_view_get_iter_at_location (rt->text, &iter,
		visible_rect.x, visible_rect.y + visible_rect.height - 1);

	GtkTextMark *mark = gtk_text_buffer_get_mark (rt->buffer, "bottom_scroll");
	if (mark != NULL)
		gtk_text_buffer_move_mark (rt->buffer, mark, &iter);
	else
		gtk_text_buffer_create_mark (rt->buffer, "bottom_scroll", &iter, TRUE);
}
/**
@brief idle callback to move the 'page' displayed in the output pane

@param rt pointer to tab data

@return FALSE to remove the source
*/
static gboolean _e2_output_do_scroll (E2_OutputTabRuntime *rt)
{
	if (rt->scroll != NULL)
	{
		GtkTextMark *mark = gtk_text_buffer_get_mark (rt->buffer, "bottom_scroll");
		if (mark != NULL)
		{
			gdk_threads_enter ();
			gtk_text_view_scroll_to_mark (rt->text, mark, 0.0, TRUE, 0.0, 1.0);
			gdk_threads_leave ();
		}
	}
	return FALSE;
}
/**
@brief move the 'page' displayed in the output pane, to show a point at the bottom

@param rt pointer to tab data

@return
*/
void e2_output_scroll_to_bottom (E2_OutputTabRuntime *rt)
{
	//this won't work if a higher priority is applied
	g_idle_add ((GSourceFunc) _e2_output_do_scroll, rt);
}
/**
@brief set popup menu position

This function is supplied when calling gtk_menu_popup(), to position
the displayed menu.
set @a push_in to TRUE for menu completely inside the screen,
FALSE for menu clamped to screen size

@param menu the GtkMenu to be positioned
@param x place to store gint representing the menu left
@param y place to store gint representing the menu top
@param push_in place to store pushin flag
@param textview output pane widget in focus when the menu key was pressed

@return
*/
void e2_output_set_menu_position (GtkWidget *menu,
	gint *x, gint *y, gboolean *push_in, GtkWidget *textview)
{
	gint left, top;
	gtk_window_get_position (GTK_WINDOW (app.main_window), &left, &top);
	GtkAllocation alloc = textview->allocation;
	*x = left + alloc.x + alloc.width/2;
	*y = top + alloc.y + alloc.height/2;
	*push_in = FALSE;
}

#ifdef E2_TABS_DETACH
/**
@brief clear text buffer of notebook tab associated with @a rt
This assumes BGL is closed
@param rt tab runtime data struct

@return
*/
static void _e2_output_clear_buffer (E2_OutputTabRuntime *rt)
{
//	gdk_threads_enter ();
	//order of things here is to minimise risk when clearing during an ongoing print operation
	GtkTextTagTable *table = gtk_text_buffer_get_tag_table (rt->buffer);
	GtkTextBuffer *buffer = gtk_text_buffer_new (table);
	GtkTextIter start;
	gtk_text_buffer_get_start_iter (buffer, &start);
	GtkTextMark *mark = gtk_text_buffer_create_mark (buffer, "internal-end-mark", &start, FALSE);
	WAIT_FOR_EVENTS
	gtk_text_view_set_buffer (rt->text, buffer);
//	gdk_threads_leave ();
	rt->buffer = buffer;
	rt->mark = mark;
	rt->onscreen = TRUE;
	g_object_unref (G_OBJECT (buffer));
}
/**
@brief set or clear detached-related data for tab whose widget is @a child

@param child the notebook tab widget
@param dest_notebook the notebook into which a moved tab will go, NULL = app.outbook

@return
*/
static void _e2_output_tab_set_detached_state (GtkWidget *child, GtkWidget *dest_notebook)
{
	gboolean attaching;
	GtkWidget *window;

	attaching = (dest_notebook == NULL || dest_notebook == app.outbook);
	if (attaching)	//moving to main notebook
		window = NULL;
	else //moving from main notebook or some other drop window
	{
		window = gtk_widget_get_toplevel (dest_notebook);
		if (!GTK_WIDGET_TOPLEVEL (window))
		{
			//FIXME
			printd (DEBUG, "can't find top window for moved tab");
			return;
		}
	}

	if (child == app.tab.scroll)
	{
		app.tab.detached = !attaching;
		app.tab.dropwindow = window;
	}
	GList *member;
	for (member = app.tabslist; member != NULL; member = member->next)
	{
		if (((E2_OutputTabRuntime *)member->data)->scroll == child)
		{
			((E2_OutputTabRuntime *)member->data)->detached = !attaching;
			printd (DEBUG, "detached-flag of tab %d set %s",
				((E2_OutputTabRuntime *)member->data)->labelnum, (attaching) ? "FALSE":"TRUE");
			((E2_OutputTabRuntime *)member->data)->dropwindow = window;
			break;
		}
	}
}
/**
@brief move tab whose widget is @a child from @a newbook to @a oldbook

@param child tab widget
@param frombook notebook from which the tab will be removed
@param tobook notebook to which the tab will be appended

@return
*/
static void _e2_output_tab_move (GtkWidget *child,
	GtkNotebook *frombook, GtkNotebook *tobook)
{
	GtkWidget *tab_label, *menu_label;
	gboolean tab_expand, tab_fill;	//, reorderable, detachable;
	guint tab_pack;
	//this is essentially the same process that gtk uses
	g_object_ref (G_OBJECT (child));
	tab_label = gtk_notebook_get_tab_label (frombook, child);
	if (tab_label)
		g_object_ref (G_OBJECT (tab_label));
	menu_label = gtk_notebook_get_menu_label (frombook, child);
	if (menu_label)
		g_object_ref (G_OBJECT (menu_label));

	gtk_container_child_get (GTK_CONTAINER (frombook), child,
		"tab-expand", &tab_expand,
		"tab-fill", &tab_fill,
		"tab-pack", &tab_pack,
//		"reorderable", &reorderable,
//		"detachable", &detachable,
		NULL);

	gtk_container_remove (GTK_CONTAINER (frombook), child);
	gtk_notebook_append_page_menu (tobook, child, tab_label, menu_label);

	gtk_container_child_set (GTK_CONTAINER (tobook), child,
		"tab-pack", tab_pack,
		"tab-expand", tab_expand,
		"tab-fill", tab_fill,
		"reorderable", TRUE,	//reorderable,
		"detachable", TRUE,	//detachable,
		NULL);

	g_object_unref (G_OBJECT (child));
	if (tab_label)
		g_object_unref (G_OBJECT (tab_label));
	if (menu_label)
		g_object_unref (G_OBJECT (menu_label));

	if (child == app.tab.scroll)
	{	//this is the default tab
		gtk_notebook_set_current_page (tobook, -1);
	}
	//update detached-data for the tab
	_e2_output_tab_set_detached_state (child, GTK_WIDGET (tobook));
}

/**
@brief process a tab being dragged
This is called for second and later drops onto a tabdrop window and for all
drops back onto the output-pane main notebook
Page-widget properties are not changed when the page is dragged
@param dest_notebook the notebook to which a tab is being dragged
@param context drag context data
@param x X coordinate where the drop happens
@param y Y coordinate where the drop happens
@param sel_data the received data
@param info the info registered for the target in target_table2
@param time timestamp at which the data was received
@param user_data UNUSED data specified when the callback was connected

@return
*/
static void _e2_output_tabdrag_data_received_cb (
	GtkWidget        *dest_notebook,
	GdkDragContext   *context,
	gint              x,
	gint              y,
	GtkSelectionData *sel_data,
	guint             info,
	guint             time,
	gpointer          user_data)
{
	printd (DEBUG, "_e2_output_tab_drag_data_received_cb");
	gboolean success;
	if (sel_data->length > 0)	//&& sel_data->format == 8)
	{
		GtkWidget *source_notebook = gtk_drag_get_source_widget (context);
		GtkWidget *child = *(GtkWidget **)sel_data->data;
		if (source_notebook == dest_notebook)
		{
//			printd (DEBUG, "trying to drag to same place");
			success = FALSE;
		}
		else if (source_notebook == app.outbook
			&& gtk_notebook_get_n_pages (GTK_NOTEBOOK (source_notebook)) == 1)
		{	//prevent dragging the only output-pane tab
//			printd (DEBUG, "trying to drag last output tab");
			success = FALSE;
		}
		else
		{
			success = TRUE;
			_e2_output_tab_set_detached_state (child, dest_notebook);
			//prevent changes of default tab, by block here
			//(WAS unblock in tab-removed cb, but that spits error - seems
			//that gtk unblocks this all by itself !
			g_signal_handlers_block_by_func (G_OBJECT (source_notebook),
				_e2_output_tabchange_cb, NULL);
		}
	}
	else
		success = FALSE;

	if (!success)
		sel_data->target = GDK_NONE;

	gtk_drag_finish (context, success, FALSE, time);
}
/**
@brief cleanup when closing a tab-drop window
This moves the tab(s) in the closing window back to the output-pane notebook
@param window the window being closed
@param event UNUSED event data struct
@param data UNUSED data specified when callback was connected

@return FALSE to allow event to propogate
*/
static gboolean _e2_output_tabrestore_cb (GtkWidget *window, GdkEvent *event,
	gpointer data)
{
	printd (DEBUG, "_e2_output_tabrestore_cb ()");
	gint j;
	GtkNotebook *newbook = GTK_NOTEBOOK (gtk_bin_get_child (GTK_BIN (window)));
	if (newbook != NULL && (j = gtk_notebook_get_n_pages (newbook)) > 0)
	{
		//prevent sequential tab-changes during the cleanout process
		g_signal_handlers_disconnect_by_func (G_OBJECT (newbook),
			_e2_output_tabchange_cb, NULL);
		//no need to go back and check for empty book
		g_signal_handlers_disconnect_by_func (G_OBJECT (newbook),
			_e2_output_tabgone_cb, window);
		GtkWidget *defchild = app.tab.scroll;	//remember which page is default now
		gint i;
		for (i = 0; i < j; i++)
		{
			GtkWidget *child = gtk_notebook_get_nth_page (newbook, 0);
			_e2_output_tab_move (child, newbook, GTK_NOTEBOOK (app.outbook));
		}
		//reset the default page in the destination book
		gint indx = gtk_notebook_page_num (GTK_NOTEBOOK (app.outbook), defchild);
		if (indx != -1)
		{
			g_signal_handlers_block_by_func (G_OBJECT (app.outbook),
				_e2_output_tabchange_cb, NULL);
			gtk_notebook_set_current_page (GTK_NOTEBOOK (app.outbook), indx);
			g_signal_handlers_unblock_by_func (G_OBJECT (app.outbook),
				_e2_output_tabchange_cb, NULL);
		}
	}
	return FALSE;
}
/**
@brief cleanup when a tab-drop window becomes empty
This is a callback for the "page-removed" signal on @a notebook
@param notebook the affected notebook
@param child UNUSED the widget for the removed page
@param page_num UNUSED the child page number
@param window the parent window for @a notebook

@return
*/
static void _e2_output_tabgone_cb (GtkNotebook *notebook, GtkWidget *child,
	guint page_num, GtkWidget *window)
{
	printd (DEBUG, "_e2_output_tabgone_cb");
	if (notebook != GTK_NOTEBOOK (app.outbook) &&
		gtk_notebook_get_n_pages (notebook) == 0)
			gtk_widget_destroy (window);
/* this generates warning - seems as if gtk already unblocked the thing
	else
		//revert the block added when the drag was in progress
		g_signal_handlers_unblock_by_func (G_OBJECT (notebook),
			_e2_output_tabchange_cb, NULL);
*/
}
/**
@brief when a mouse-button is pressed on tab "label", update current-page data if needed
This is a callback for the "grab-focus" signal on @a notebook
It's called after a tab-change, if any
@param notebook the affected notebook
@param user_data UNUSED data specified when the callback was connected

@return
*/
static void _e2_output_grab_focus_cb (GtkWidget *notebook, gpointer user_data)
{
	printd (DEBUG, "_e2_output_grab_focus_cb");
	gint curindx = gtk_notebook_get_current_page (GTK_NOTEBOOK (notebook));
	GtkWidget *child = gtk_notebook_get_nth_page (GTK_NOTEBOOK (notebook),
		curindx);
	GList *member;
	for (member = app.tabslist; member != NULL; member = member->next)
	{
		E2_OutputTabRuntime *tab;
		tab = (E2_OutputTabRuntime *)member->data;
		if (tab->scroll == child)
		{
			if (tab != curr_tab)
			{
				//swap with mimimum race-risk ...
				*curr_tab = app.tab;	//backup current tab's data from stack to heap
				app.tab = *tab;	//get the replacement stuff into stackspace
				curr_tab = tab;
				//adjust all relevant child foreground-tab pointers to/from the stacked tab data
				e2_command_retab_children (&app.tab, curr_tab);
				printd (DEBUG, "output tab change, new current-tab ID is %d", tab->labelnum);
			}
			break;
		}
	}
}
//for "focus-in-event" NO USE
/*static gboolean _e2_output_focus_in_cb (GtkWidget *widget, GdkEventFocus *event,
	gpointer user_data)
{
	printd (DEBUG, "_e2_output_focus_in_cb");
	return FALSE;
}
void _e2_output_page_add_cb (GtkNotebook *notebook, GtkWidget *child,
		guint page_num, gpointer user_data)
{
	printd (DEBUG, "_e2_output_page_add_cb");
} */
/**
@brief when a detached notebook tab is dropped in an empty area, create a
window containing a notebook to receive the tab
Page-widget properties are not changed when dragged
@param source the source GtkNotebook of the drag operation
@param sw the child GtkWidget to be dropped
@param x the X coordinate where the drop happens
@param y the Y coordinate where the drop happens
@param data data specified when the arrangement was set up

@return the created GtkNotebook where the tab will be attached, or NULL to cancel the drag
*/
static GtkNotebook *_e2_output_tab_drop_new (GtkNotebook *source, GtkWidget *sw,
	gint x, gint y, gpointer data)
{
	printd (DEBUG, "_e2_output_tab_drop_new ()");
	printd (DEBUG, "name of current tab is %d", curr_tab->labelnum);

	if (gtk_notebook_get_n_pages (source) == 1)
		return NULL;	//prevent dragging of the only tab

	GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
	e2_window_set_title (window, _("output tabs"));
	gtk_window_set_role (GTK_WINDOW (window), "tabdrop");
//	gtk_window_set_wmclass (GTK_WINDOW (window), "main", BINNAME);
#ifdef E2_COMPOSIT
	e2_window_set_opacity (window, -1);
#endif
	gtk_window_set_position (GTK_WINDOW (window), GTK_WIN_POS_MOUSE);
	//restore dropped tab(s) when new window is closed
	g_signal_connect (G_OBJECT (window), "delete-event",
		G_CALLBACK (_e2_output_tabrestore_cb), NULL);

	GtkWidget *notebook = gtk_notebook_new ();
	printd (DEBUG, "new notebook widget = %x", notebook);
	gtk_container_add (GTK_CONTAINER (window), notebook);
//CRASHER gtk_drag_source_set (notebook, GDK_BUTTON1_MASK, target_table2, n_targets2,
//		GDK_ACTION_PRIVATE);
	//setup to process further tabs being dragged here
	gtk_drag_dest_set (notebook, GTK_DEST_DEFAULT_DROP, target_table2, n_targets2,
		GDK_ACTION_MOVE);
	g_signal_connect (G_OBJECT (notebook), "drag-data-received",
		G_CALLBACK (_e2_output_tabdrag_data_received_cb), NULL);	//CHECKME user_data
	//close window when all tabs dragged out or re-attached by menu-selection
	g_signal_connect (G_OBJECT (notebook), "page-removed",
		G_CALLBACK (_e2_output_tabgone_cb), window);
	//capture clicks on tab labels
	g_signal_connect (G_OBJECT (notebook), "grab-focus",
		G_CALLBACK (_e2_output_grab_focus_cb), NULL);
//	g_signal_connect (G_OBJECT (notebook), "focus-in-event",
//		G_CALLBACK (_e2_output_focus_in_cb), NULL);
//	g_signal_connect (G_OBJECT (notebook), "page-added",
//		G_CALLBACK (_e2_output_page_add_cb), NULL);
	g_signal_connect (G_OBJECT (notebook), "switch-page",
		G_CALLBACK (_e2_output_tabchange_cb), NULL);	//no data
#ifdef USE_GTK2_12DND
	//allow further dragging from this window
	//(this seems to be un-necessary here, though that may be a gtk deficiency)
	g_signal_connect (G_OBJECT (notebook), "create-window",
		G_CALLBACK (_e2_output_tab_drop_new), NULL);	//CHECKME user_data
#endif

	GtkNotebook *book = GTK_NOTEBOOK (notebook);
	gtk_notebook_popup_enable (book);
	gtk_notebook_set_scrollable (book, TRUE);
	gtk_notebook_set_show_border (book, FALSE);
	gtk_notebook_set_show_tabs (book, TRUE);
	gtk_notebook_set_tab_pos (book, GTK_POS_LEFT);
	// these 2 needed ?
//	gtk_notebook_set_tab_reorderable (book, sw, TRUE);
//	gtk_notebook_set_tab_detachable (book, sw, TRUE);
#ifdef USE_GTK2_12DND
	gpointer group = gtk_notebook_get_group (source);
	gtk_notebook_set_group (book, group);
#else
	gint group_id = gtk_notebook_get_group_id (source);
	gtk_notebook_set_group_id (book, group_id);
#endif
	gtk_window_set_default_size (GTK_WINDOW (window),
		app.main_window->allocation.width,
		app.tab.scroll->allocation.height);
	gtk_widget_show_all (window);

	//dragging has made the dragged tab "current", so we can set data for that
	app.tab.dropwindow = window;
	//set flag for tab context-menu so it shows only items relevant to separate window
	app.tab.detached = TRUE;
	printd (DEBUG, "detached-flag of tab %d set TRUE", app.tab.labelnum);
	//gtk will change the active tab in the source notebook and then the active
	//page in the dest notebook - any way to prevent the former of these ?

	return book;
}
#endif	//def E2_TABS_DETACH
/**
@brief execute action corresponding to item selected from filetype tasks menu
This is the callback for handling a selection of a filetype action from
the context menu
@param widget the selected menu item widget

@return
*/
static void _e2_output_choose_filetype_action_cb (GtkWidget *menu_item)
{
	gpointer *command = g_object_get_data (G_OBJECT (menu_item), "e2-file-operation");
#ifdef E2_COMMANDQ
	e2_command_run ((gchar *) command, E2_COMMAND_RANGE_DEFAULT, FALSE);
#else
	e2_command_run ((gchar *) command, E2_COMMAND_RANGE_DEFAULT);
#endif
}
/**
@brief populate @a menu with items for the actions for a filetype
Each member of @a actions is like "command" or "label@command"
@param text the path of the item to open, utf8 string
@param menu the menu widget to which the action menu-items are to be added
@param actions NULL-terminated array of utf8 strings, each a command for a filetype

@return
*/
static void _e2_output_menu_create_filetype_actions_menu (gchar *text,
	GtkWidget *menu, const gchar **actions)
{
	gchar *sep, *fullcmd;
	GtkWidget *menu_item;

	while (*actions != NULL)
	{
		if ((sep = strchr (*actions, '@')) != NULL)  //if always ascii @, don't need g_utf8_strchr()
		{
			*sep = '\0';
			menu_item = e2_menu_add (menu, (gchar *)*actions, NULL, NULL,
				_e2_output_choose_filetype_action_cb, NULL);
			fullcmd = e2_utils_replace_name (sep+1, text);
			if (fullcmd == NULL)	//no replaced macro in command
				fullcmd = g_strdup_printf ("%s \"%s\"", sep+1, text);
			*sep = '@';	//revert to original form (this is the 'source' data)
		}
		else
		{
			menu_item = e2_menu_add (menu, (gchar *)*actions, NULL, NULL,
				_e2_output_choose_filetype_action_cb, NULL);
			fullcmd = e2_utils_replace_name ((gchar *)*actions, text);
			if (fullcmd == NULL)	//no replaced macro in command
				fullcmd = g_strdup_printf ("%s \"%s\"", *actions, text);
		}
		g_object_set_data_full (G_OBJECT (menu_item), "e2-file-operation", fullcmd,
			g_free);
		actions++;
	}
}
/**
@brief create a filetypes sub-menu for @a menu

@param text utf8 string describing the item to process
@param menu menu widget to add to

@return
*/
static void _e2_output_add_filetype_menu (gchar *text, GtkWidget *menu)
{
	struct stat statbuf;
	E2_ERR_DECLARE
	gchar *local = F_FILENAME_TO_LOCALE (text);
#ifdef E2_VFS
# ifdef E2_VFSTMP
	get relevant spacedata, or allow only local (NULL)
# endif
	VPATH sdata = {local , NULL};
	if (e2_fs_stat (&sdata, &statbuf E2_ERR_PTR()))
#else
	if (e2_fs_stat (local, &statbuf E2_ERR_PTR()))
#endif
	{
#ifdef E2_VFSTMP
		//FIXME handle error
#endif
		E2_ERR_CLEAR
		F_FREE (local);
		return;
	}
	gboolean exec;
	const gchar **actions = NULL;
	gchar *ext, *ext2 = NULL;
	gchar *base = g_path_get_basename (text);
#ifdef E2_VFS
	if (e2_fs_is_dir3 (&sdata E2_ERR_NONE()))
#else
	if (e2_fs_is_dir3 (local E2_ERR_NONE()))
#endif
	{
		exec = FALSE;	//no special treatment of dirs
		ext = ext2 = g_strconcat (".", _("<directory>"), NULL);
	}
	else
	{
#ifdef E2_VFS
		exec = !e2_fs_access (&sdata, X_OK E2_ERR_NONE());
#else
		exec = !e2_fs_access (local, X_OK E2_ERR_NONE());
#endif
		//assumes extension is that part of the name after the leftmost '.'
		ext = strchr (base, '.');	//assumes '.' is ascii
		if (ext == base //it's a dot file
#ifdef E2_VFS
			|| (ext == NULL && e2_fs_is_text (&sdata E2_ERR_NONE()))) //no extension
#else
			|| (ext == NULL && e2_fs_is_text (local E2_ERR_NONE()))) //no extension
#endif
			//fake text extension
			//too bad if this is not a recognised text extension in filetypes
			ext = ".txt";
	}
	if (ext != NULL)
	{
		//check all possible extensions for a matching filetype
		do
		{
			//skip leading dot "."
			ext++;	//NCHR(ext); ascii '.'. always single char
			actions = e2_filetype_get_actions (ext);
			if (actions != NULL)
			{
				_e2_output_menu_create_filetype_actions_menu (text, menu, actions);
				break;
			}
		} while ((ext = strchr (ext, '.')) != NULL);	//always ascii '.', don't need g_utf8_strchr()
	}
	if (exec)
	{
		//add exec-filetype items unless item has been found in that type already
		const gchar **acts2 = e2_filetype_get_actions (_("<executable>"));
		if (actions != NULL //was a matching extension
			&& actions != acts2 && acts2 != NULL)	//CHECKME this test
				_e2_output_menu_create_filetype_actions_menu (text, menu, acts2);
		else if (acts2 != NULL)
			_e2_output_menu_create_filetype_actions_menu (text, menu, acts2);

		//if appropriate, get desktop entry actions and add them too
		gchar *path, *localpath;
		gchar *dfile = e2_utils_strcat (base, ".desktop");
		gchar *localname = F_FILENAME_TO_LOCALE (dfile);
		const gchar* const *sysdir = g_get_system_data_dirs ();
		while (*sysdir != NULL)
		{
			path = g_build_filename (*sysdir, "applications", localname, NULL);
			localpath = F_FILENAME_TO_LOCALE (path); //probably redundant
#ifdef E2_VFS
			sdata.localpath = localpath;
			if (!e2_fs_access (&sdata, R_OK E2_ERR_NONE()))
#else
			if (!e2_fs_access (localpath, R_OK E2_ERR_NONE()))
#endif
			{
				e2_context_menu_create_desktop_actions_menu (menu, localpath);
				g_free (path);
				F_FREE (localpath);
				break;
			}
			g_free (path);
			F_FREE (localpath);
			sysdir++;
		}
		g_free (dfile);
		F_FREE (localname);
	}
	F_FREE (local);
	g_free (base);
	if (ext2 != NULL)
		g_free (ext2);
}
/**
@brief check for or open a taskable item
Expects BGL on/closed
@param text utf8 string describing the item to open
@param run TRUE to execute a matched command, FALSE to just return the match status

@return TRUE if the item was processed (@a run = FALSE) or found (@a run = TRUE)
*/
static gboolean _e2_output_open_text (gchar *text, gboolean run)
{
	gboolean retval;
	struct stat statbuf;
	gchar *local = F_FILENAME_TO_LOCALE (text);
	//make sure the text means something
#ifdef E2_VFS
# ifdef E2_VFSTMP
	assume text from local command run on local items
# endif
	VPATH sdata = { local, NULL };

	if (e2_fs_stat (&sdata, &statbuf E2_ERR_NONE()))
#else
	if (e2_fs_stat (local, &statbuf E2_ERR_NONE()))
#endif
	{
		retval = FALSE;
		if (run)
		{
			gchar *msg = g_strdup_printf (_("Cannot get information about %s"), text);
			e2_output_print_error (msg, TRUE);
		}
	}
	else if (run)
	{
#ifdef E2_VFS
		retval = e2_task_backend_open (&sdata, FALSE);
#else
		retval = e2_task_backend_open (local, FALSE);
#endif
	}
	else
	{	//just check for a known filetype
#ifdef E2_VFS
		if (e2_fs_is_dir3 (&sdata E2_ERR_NONE())	//we can always handle dirs
		|| !e2_fs_access (&sdata, X_OK E2_ERR_NONE()))	//and executable items
#else
		if (e2_fs_is_dir3 (local E2_ERR_NONE())	//we can always handle dirs
		|| !e2_fs_access (local, X_OK E2_ERR_NONE()))	//and executable items
#endif
			retval = TRUE;
		else
		{
			gchar *base = g_path_get_basename (text);
			gchar *ext = strchr (base, '.');	//assumes '.' is ascii
			if (ext == NULL //no extension
				|| ext == base) //hidden file
			{
#ifdef E2_VFS
				retval = e2_fs_is_text (&sdata E2_ERR_NONE());
#else
				retval = e2_fs_is_text (local E2_ERR_NONE());
#endif
			}
			else
			{
				gchar *action;
				retval = FALSE;
				do
				{
					NCHR(ext); //skip the . prefix
					action = e2_filetype_get_default_action (ext);
					if (action != NULL)
					{
						retval = TRUE;
						g_free (action);
						break;
					}
				} while ((ext = strchr (ext, '.')) != NULL);	//if always ascii '.', don't need g_utf8_strchr()
			}
			g_free (base);
		}
	}
	F_FREE (local);
	return retval;
}
/**
@brief select and get text of a taskable item (if any) at event position

@param x event x coordinate
@param y event y coordinate
@param rt pointer to data struct for output pane

@return newly allocated string containing item which can be activated, or NULL
*/
static gchar *_e2_output_get_item_path (gint x, gint y, E2_OutputTabRuntime *rt)
{
	GtkTextIter iter, start, end;
	gint buffer_x, buffer_y;
	gboolean quoted;
	gunichar c, d;
	gtk_text_buffer_get_bounds (rt->buffer, &start, &end);
	if (gtk_text_iter_equal (&start, &end))
		return NULL;
	gtk_text_view_window_to_buffer_coords (rt->text, GTK_TEXT_WINDOW_TEXT,
		x, y, &buffer_x, &buffer_y);
	gtk_text_view_get_iter_at_location (rt->text, &iter, buffer_x, buffer_y);
	if (!gtk_text_buffer_get_selection_bounds (rt->buffer, &start, &end)
		|| !gtk_text_iter_in_range (&iter, &start, &end))
	{
		c = gtk_text_iter_get_char (&iter);
		if (g_unichar_isspace (c))
			return NULL;
		//word separators include valid path chars, so need char iteration
		while (gtk_text_iter_backward_char (&iter))
		{
			c = gtk_text_iter_get_char (&iter);
			if (g_unichar_isspace (c))
			{
				gtk_text_iter_forward_char (&iter);
				break;
			}
		}
		c = gtk_text_iter_get_char (&iter);
		quoted = (c == (gunichar) '"' || c == (gunichar) '\'');
		if (quoted)
		{
			if (!gtk_text_iter_forward_char (&iter))
				return NULL;
		}
		start = iter;
		while (gtk_text_iter_forward_word_end (&iter))
		{
			d = gtk_text_iter_get_char (&iter);
			if (quoted && d == c)
				break;
			else if (g_unichar_isspace (d))
				break;
		}
		end = iter;
	}

	gchar *text = gtk_text_iter_get_text (&start, &end);
	if (_e2_output_open_text (text, FALSE))	//, FALSE))
		gtk_text_buffer_select_range (rt->buffer, &start, &end);
	else
	{
		g_free (text);
		text = NULL;
	}
	return text;
}

  /*******************/
 /***** actions *****/
/*******************/
/**
@brief print to output pane, with default settings

Any escaped newlines are converted to real ones.

@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE
*/
static gboolean _e2_output_print_action (gpointer from, E2_ActionRuntime *art)
{
	gchar *real_message = e2_utils_str_replace ((gchar *)art->data , "\\n", "\n");
	e2_output_print (&app.tab, real_message, NULL, FALSE, NULL);
	g_free (real_message);
	return TRUE;
}
/**
@brief print help message to output pane
This expects as data a string indicating which message is to be displayed
@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE if type of help was recognised
*/
static gboolean _e2_output_help_action (gpointer from, E2_ActionRuntime *art)
{
	gchar *arg = (gchar *)art->data;
	if ((arg == NULL) || (*arg == '\0') || g_str_has_prefix (arg, _("commands")))
	{
		e2_command_output_help ();
		return TRUE;
	}
	else if (g_str_has_prefix (arg, _("keys")))	//this must be translated same as in default aliases
	{
		e2_keybinding_output_help (arg);
		return TRUE;
	}
	return FALSE;
}
/**
@brief move the displayed text window by @a arg 'steps'
This expects as data a string containing the no. of 'steps' to move
@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE
*/
static gboolean _e2_output_scroll_step (gpointer from, E2_ActionRuntime *art)
{
	//action data = TRUE to move down, FALSE to move up
	gboolean down = GPOINTER_TO_INT (art->action->data);
	_e2_output_scroll_helper (down, (gchar *)art->data, FALSE);
	return TRUE;
}
/**
@brief move the displayed text window by @a arg 'pages'
This expects as data a string containing the no. of 'pages' to move
@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE
*/
static gboolean _e2_output_scroll_page (gpointer from, E2_ActionRuntime *art)
{
	//action data = TRUE to move down, FALSE to move up
	gboolean down = GPOINTER_TO_INT (art->action->data);
	_e2_output_scroll_helper (down, (gchar *)art->data, TRUE);
	return TRUE;
}
/**
@brief move the displayed text window to start or end of its content

@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE
*/
static gboolean _e2_output_scroll_all (gpointer from, E2_ActionRuntime *art)
{
	E2_OutputTabRuntime *rt = &app.tab;
	g_return_val_if_fail (rt->scroll != NULL, FALSE);
	GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment
		(GTK_SCROLLED_WINDOW (rt->scroll));
	//action data = TRUE to move down, FALSE to move up
	gboolean down = GPOINTER_TO_INT (art->action->data);
	if (down)
		gtk_adjustment_set_value (vadj, vadj->upper - vadj->page_size);
	else
		gtk_adjustment_set_value (vadj, 0.0);
	return TRUE;
}
/**
@brief move the displayed text window to show text printed by a child process

@param item the selected item from a children-menu
@param rt pointer to command data

@return
*/
static void _e2_output_scroll_to_child (GtkWidget *item, E2_TaskRuntime *rt)
{
	if (rt == NULL)	//rt == NULL for a menu item "no children"
		return;
	pthread_mutex_lock (&task_mutex);
	GList *member = g_list_find (app.taskhistory, rt);
	pthread_mutex_unlock (&task_mutex);
	if (member != NULL)	//command data still exists
	{
		//the buffer may be cleared while the menu is active,
		//so check again for matching content
		GtkTextMark *origin_mark = gtk_text_buffer_get_mark
			(app.tab.buffer, rt->pidstr);
		if (origin_mark != NULL)
			gtk_text_view_scroll_to_mark (app.tab.text, origin_mark, 0.0,
				TRUE, 0.0, 1.0);
		else
		{
			gchar *msg = g_strdup_printf (_("Cannot find any output from process %s"), rt->pidstr);
			e2_output_print_error (msg, TRUE);
		}
	}
}
/**
@brief clear all content of an output-pane tab

@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE
*/
static gboolean _e2_output_clear_action (gpointer from, E2_ActionRuntime *art)
{
#ifdef E2_TABS_DETACH
	_e2_output_clear_buffer (&app.tab);
#else
//	gdk_threads_enter ();
	//order of things here is to handle clearing when the text buffer is being added to
	GtkTextTagTable *table = gtk_text_buffer_get_tag_table (app.tab.buffer);
	GtkTextBuffer *buffer = gtk_text_buffer_new (table);
	GtkTextIter start;
	gtk_text_buffer_get_start_iter (buffer, &start);
	GtkTextMark *mark = gtk_text_buffer_create_mark (buffer, "internal-end-mark", &start, FALSE);
	WAIT_FOR_EVENTS
	gtk_text_view_set_buffer (app.tab.text, buffer);
//	gdk_threads_leave ();
	app.tab.buffer = buffer;
	app.tab.mark = mark;
	app.tab.onscreen = TRUE;
	g_object_unref (G_OBJECT (buffer));
#endif
	*(curr_tab) = app.tab; //listed tab data needs the updated values too
	return TRUE;
}
/**
@brief create another output pane tab, and go there

@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE
*/
static gboolean _e2_output_tab_add (gpointer from, E2_ActionRuntime *art)
{
	E2_OutputTabRuntime *tab = ALLOCATE0 (E2_OutputTabRuntime);
	CHECKALLOCATEDWARN (tab, return FALSE;)
	GtkWidget *sw = _e2_output_create_view (tab);
	GtkWidget *wid;
#ifdef USE_GTK2_10
	//there may be gaps in the tab labels
	//and for gtk >= 2.10, tabs can be in any order or in another notebook,
	//so check all tabs to find the last one and bump its label
	gint lablid, new = 1;
	GList *member;
	for (member = app.tabslist; member != NULL; member = member->next)
	{
		lablid = ((E2_OutputTabRuntime *)member->data)->labelnum;
		if (lablid >= new)
			new = lablid + 1;
	}
	tab->labelnum = new;	//remember, for later searching
#else
	wid = gtk_notebook_get_nth_page (GTK_NOTEBOOK (app.outbook), app.tabcount-1);
	const gchar *labtxt = gtk_notebook_get_tab_label_text (GTK_NOTEBOOK (app.outbook), wid);
	gint new = atoi (labtxt) + 1;
#endif
	gchar *txt = g_strdup_printf ("%d", new);	//no translation
	wid = gtk_label_new (txt);
	g_free (txt);
	gtk_notebook_append_page (GTK_NOTEBOOK (app.outbook), sw, wid);
#ifdef USE_GTK2_10
	gtk_notebook_set_tab_reorderable (GTK_NOTEBOOK (app.outbook), sw, TRUE);
#ifdef E2_TABS_DETACH
	gtk_notebook_set_tab_detachable (GTK_NOTEBOOK (app.outbook), sw, TRUE);
#endif
#endif
	app.tabslist = g_list_append (app.tabslist, tab);
	app.tabcount++;	//log, for cache
	if (app.tabcount == 2)	//doesn't matter where the tabs are
		gtk_notebook_set_show_tabs (GTK_NOTEBOOK (app.outbook), TRUE);
	//focus the new tab
#ifdef E2_TABS_DETACH
	new = gtk_notebook_get_n_pages (GTK_NOTEBOOK (app.outbook));
	gtk_notebook_set_current_page (GTK_NOTEBOOK (app.outbook), new-1);
#else
	gtk_notebook_set_current_page (GTK_NOTEBOOK (app.outbook), app.tabcount - 1); //uses new last-tab index
#endif
	return TRUE;
}
/**
@brief remove currently focused output pane tab
This may be called from a keybinding or context menu item
Essentially, tab-specific data are swpped between stack and heap space,
and pointers for affected running commands are adjusted accordingly
@param from the button, menu item etc which was activated
@param art action runtime data

@return TRUE if the removal was done
*/
static gboolean _e2_output_tab_remove (gpointer from, E2_ActionRuntime *art)
{
#ifdef E2_TABS_DETACH
	//ignore removal of only tab in this notebook
	gint homecount = gtk_notebook_get_n_pages (GTK_NOTEBOOK (app.outbook));
	if (homecount == 1)
		return FALSE;
	//cleanup the notebook
	//FIXME only if tab in this notebook (context menu cleanup to prevent this ?)
	gint index = gtk_notebook_get_current_page (GTK_NOTEBOOK (app.outbook));
	GtkWidget *sw = gtk_notebook_get_nth_page (GTK_NOTEBOOK (app.outbook), index);
	GList *member;
	for (member = app.tabslist; member != NULL; member = member->next)
	{
		if (((E2_OutputTabRuntime *)member->data)->scroll == sw)
			break;
	}
	if (member == NULL)
		return FALSE;	//should never happen

	gtk_notebook_remove_page (GTK_NOTEBOOK (app.outbook), index);
	if (--app.tabcount == 1)
		gtk_notebook_set_show_tabs (GTK_NOTEBOOK (app.outbook), FALSE);

	index = gtk_notebook_get_current_page (GTK_NOTEBOOK (app.outbook));
	sw = gtk_notebook_get_nth_page (GTK_NOTEBOOK (app.outbook), index);

	GList *newmember;
	for (newmember = app.tabslist; newmember != NULL; newmember = newmember->next)
	{
		if (((E2_OutputTabRuntime *)newmember->data)->scroll == sw)
			break;
	}
	if (newmember == NULL)
		return FALSE;	//should never happen
/*//CHECKME does this conform to gtk behaviour ?
	GList *member2;
	if (member == app.tabslist)
		member2 = member->next;
	else
		member2 = member->prev;

	if (member2 != newmember)
		printd (DEBUG, "replacement tab not correct !!");
*/
#else
	//ignore removal of only tab
	if (app.tabcount == 1)
		return FALSE;
	//cleanup the notebook
	gint index = gtk_notebook_get_current_page (GTK_NOTEBOOK (app.outbook));
	gtk_notebook_remove_page (GTK_NOTEBOOK (app.outbook), index);
	if (--app.tabcount == 1)
		gtk_notebook_set_show_tabs (GTK_NOTEBOOK (app.outbook), FALSE);

	//CHECKME does this conform to gtk behaviour ?
	GList *member = g_list_nth (app.tabslist, index);
	GList *newmember;
	if (index == 0)
		newmember = member->next;
	else
		newmember = member->prev;
#endif
	E2_OutputTabRuntime *newtab = newmember->data;
//	printd (DEBUG, "curr_tab is %x", curr_tab);
//	printd (DEBUG, "replacement tab at %x", newtab);
	//quickly install replacement data, ready for use by any printing children
	app.tab = *newtab;
	//anthing running in the focused tab stays there,
	//so no need to update foreground pointers
	//but do update background-tab ptrs of children using the tab
	e2_command_retab2_children (member->data, newtab);  // != curr_tab?
	//now its ok to update the list pointer
	curr_tab = newtab;
	//clean the old tab's data FIXME any leak ? text buffer ?
	if (((E2_OutputTabRuntime *)member->data)->origin_lastime!= NULL)
		g_free (((E2_OutputTabRuntime *)member->data)->origin_lastime);
//	GHashTable *hash = ((E2_OutputTabRuntime *)member->data)->origins;
//	if (hash != NULL)
//		g_hash_table_destroy (hash);
//	printd (DEBUG, "removing tab at %x", member->data);
	DEALLOCATE (E2_OutputTabRuntime, member->data);
	app.tabslist = g_list_delete_link (app.tabslist, member);	//counter adjusted above
	return TRUE;
}
/**
@brief update gtk's flag which sets output pane text wrapping

This is a hook fn

@param pvalue pointerised version of the T/F value to be set
@param rt pointer to data struct for output pane

@return
*/
static void _e2_output_set_op_wrap (gpointer pvalue, E2_OutputTabRuntime *rt)
{
//	printd (DEBUG, "_e2_output_set_op_wrap (pvalue:_,rt:_)");
	gint value = GPOINTER_TO_INT (pvalue);
	gint cur = gtk_text_view_get_wrap_mode (rt->text);
	if (cur != value)
		gtk_text_view_set_wrap_mode (rt->text, value);
}

  /**************/
 /**** menu ****/
/**************/
/**
@brief save selected output-pane text

@param menuitem UNUSED the selected widget, or NULL
@param rt runtime struct to work on

@return
*/
/*static void _e2_output_savesel_cb (GtkWidget *menuitem,	E2_OutputTabRuntime *rt)
{
	e2_edit_dialog_save_selected (rt->buffer,
#ifdef E2_VFS
	NULL,	//local namespace assumed
#endif
	app.main_window);
} */
/**
@brief edit output-pane-tab text

@param menuitem UNUSED the selected widget, or NULL
@param rt data struct for the tab

@return
*/
static void _e2_output_edit_cb (GtkWidget *menuitem, E2_OutputTabRuntime *rt)
{
	//editing an empty buffer will cause a freeze, so we fake some content...
	GtkTextIter start, end;
	gtk_text_buffer_get_bounds (rt->buffer, &start, &end);
	if (gtk_text_iter_equal (&start, &end))
		e2_output_print (rt, " ", NULL, FALSE, NULL);

	e2_edit_dialog_create (NULL, rt->buffer);
}

#ifdef E2_TABS_DETACH
static void _e2_output_tabattach_cb (GtkWidget *menuitem, E2_OutputTabRuntime *rt)
{
	GtkNotebook *newbook = GTK_NOTEBOOK (gtk_bin_get_child (GTK_BIN (rt->dropwindow)));
	_e2_output_tab_move (rt->scroll, newbook, GTK_NOTEBOOK (app.outbook));
}

static void _e2_output_clear_cb (GtkWidget *menuitem, E2_OutputTabRuntime *rt)
{
	return _e2_output_clear_buffer (rt);
}
#endif
/**
@brief construct and show output-pane context menu

@param textview the textview widget where the click happened
@param event_button which mouse button was clicked (0 for a menu key)
@param event_time time that the event happened (0 for a menu key)
@param rt runtime struct to work on

@return
*/
static void _e2_output_show_context_menu (GtkWidget *textview,
	guint event_button, gint event_time, E2_OutputTabRuntime *rt)
{
	gchar *item_name;
	GtkWidget *item;
	GtkWidget *menu = gtk_menu_new ();
#ifdef E2_TABS_DETACH
	if (!rt->detached)
	{
#endif
		item_name = g_strconcat (_A(9),".",_A(27),NULL);
		e2_menu_add_action (menu, _("_Hide"), "output_hide_"E2IP".png",
			_("Do not show the output pane"),
			item_name, "1", NULL);  //no string translation
		g_free (item_name);
		item_name = g_strconcat (_A(9),".",_A(27),NULL);
		e2_menu_add_action (menu, _("_Toggle full"),
			(app.window.output_paned_ratio > 0.01) ?
				GTK_STOCK_ZOOM_FIT : GTK_STOCK_ZOOM_OUT,
			_("Toggle output pane size to/from the full window size"),
			item_name, "*,0", NULL);  //no string translation
		g_free (item_name);
		item_name = g_strconcat (_A(9),".",_A(26),NULL);
		e2_menu_add_action (menu, _("_New tab"), GTK_STOCK_ADD,
			_("Add another tab for the output pane"),
			item_name, NULL, NULL);
		g_free (item_name);
		item_name = g_strconcat (_A(9),".",_A(38),NULL);
		item = e2_menu_add_action (menu, _("_Remove tab"), GTK_STOCK_REMOVE,
			_("Close this this tab"),
			item_name, NULL, NULL);
		g_free (item_name);
		if (app.tabcount == 1)
			gtk_widget_set_sensitive (item, FALSE);
#ifdef E2_TABS_DETACH
	}
	else //rt->detached
	{
		e2_menu_add (menu, _("_Attach"), NULL,
		_("Move this tab back to output pane"), _e2_output_tabattach_cb, rt);
	}
#endif

	e2_menu_add_separator (menu);

#ifdef E2_TABS_DETACH
	e2_menu_add (menu, _("_Clear"), GTK_STOCK_CLEAR,
		_("Clear this tab"), _e2_output_clear_cb, rt);
#else
	item_name = g_strconcat (_A(9),".",_A(29),NULL);
	e2_menu_add_action (menu, _("_Clear"), GTK_STOCK_CLEAR,
		_("Clear this tab"),
		item_name, NULL, NULL);
	g_free (item_name);
#endif
/*	e2_menu_add (menu, _("Save as.."), "save_selection_"E2IP".png",	//no suitable mnemonic
		_("Save the selected text"), _e2_output_savesel_cb, rt);
	if (!gtk_text_buffer_get_selection_bounds (rt->buffer, NULL, NULL))
		gtk_widget_set_sensitive (item, FALSE); */
	e2_menu_add (menu, _("_Edit"), "edit_"E2IP".png",
		_("Edit the tab contents"), _e2_output_edit_cb, rt);

	item = e2_menu_add (menu, _("_Open"), GTK_STOCK_EXECUTE, NULL, NULL, NULL);
	gint x, y;
	gdk_window_get_pointer (textview->window, &x, &y, NULL);
	item_name = _e2_output_get_item_path (x, y, rt);
	if (item_name == NULL)
		gtk_widget_set_sensitive (item, FALSE);
	else
	{
		GtkWidget *submenu = gtk_menu_new ();
		gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu);
		gtk_widget_show (submenu);
		_e2_output_add_filetype_menu (item_name, submenu);
		g_free (item_name);	//??
	}
/*	item_name = g_strconcat (_A(9),".",_A(50),NULL);
	e2_menu_add_action (menu, _("C_ommand help"), GTK_STOCK_HELP,
		_("Show information about using the command line"),
		item_name, "", NULL);
	g_free (item_name);
*/
	item = e2_menu_add (menu, _("Co_mmand output"), "ps_"E2IP".png",
		_("Show output from a completed command"), NULL, NULL);
	GtkWidget *submenu = e2_menu_create_child_menu (E2_CHILD_OUTPUT,
		_e2_output_scroll_to_child);
	if (submenu == NULL)
		gtk_widget_set_sensitive (item, FALSE);
	else
		gtk_menu_item_set_submenu (GTK_MENU_ITEM (item), submenu);

#ifdef E2_TABS_DETACH
	if (!rt->detached)
	{
#endif
		e2_menu_add_separator (menu);
		submenu = e2_menu_add_submenu (menu, _("_Settings"), GTK_STOCK_PREFERENCES);
		e2_menu_create_options_menu (GTK_WIDGET (rt->text), submenu,
			rt->opt_wrap, NULL, NULL,
			app.output.opt_show_on_new, NULL, NULL,
			app.output.opt_show_on_focus_in, NULL, NULL,
			app.output.opt_hide_on_focus_out, NULL, NULL,
			app.output.opt_jump, NULL, NULL,
			app.output.opt_jump_follow, NULL, NULL,
			app.output.opt_jump_end, NULL, NULL,
			NULL);
		item_name = g_strconcat (_A(2),".",_A(32),NULL);
		e2_menu_add_action (submenu, _("_Other"), NULL,
			_("Open the configuration dialog at the output options page"),
			item_name, _C(27), //_("output")
			NULL);
		g_free (item_name);
#ifdef E2_TABS_DETACH
	}
#endif
	g_signal_connect (G_OBJECT (menu), "selection-done",
		G_CALLBACK (e2_menu_destroy_cb), NULL);
	if (event_button == 0)
		gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
			(GtkMenuPositionFunc) e2_output_set_menu_position,
			textview, 0, event_time);
	else
		//this was a button-3 click
		gtk_menu_popup (GTK_MENU (menu), NULL, NULL,
			NULL, NULL, event_button, event_time);
}

  /*********************/
 /***** callbacks *****/
/*********************/

/**
@brief process an 'intercepted' double-click on the output pane
This handles 'activated' text in the output pane.

@param rt runtime struct for the pane

@return TRUE if the press was handled
*/
static gboolean _e2_output_activated_cb (E2_OutputTabRuntime *rt)
{
	printd (DEBUG, "output activated cb");
	GtkTextIter start, end;
	if (gtk_text_buffer_get_selection_bounds (rt->buffer, &start, &end))
	{
		gchar *seltext = gtk_text_buffer_get_text (rt->buffer, &start, &end, FALSE);
		printd (DEBUG, "output text is %s", seltext);
		_e2_output_open_text (seltext, TRUE);	//, TRUE);
		g_free (seltext);
//BAD - this locks text DnD on
//		output_activated = TRUE;	//prevent normal button-release handling
		return TRUE;	//block internal click handling
	}
	printd (DEBUG, "nothing selected in output");
//	output_activated = FALSE;	//allow normal button-release handling
	return FALSE;
}
/**
@brief process mouse button-click in the output pane

1/L = focus, 2/M = hide, 3/R = context menu
<Ctrl>L = open
This also detects left-button double-clicks (for which there is no API)
and performs an open if relevant

@param textview the widget where the click happened
@param event gdk event data struct
@param rt runtime struct to work on

@return TRUE if the signal has been handled here
*/
static gboolean _e2_output_button_press_cb (GtkWidget *textview,
	GdkEventButton *event, E2_OutputTabRuntime *rt)
{
/*	GtkTextWindowType wtype = gtk_text_view_get_window_type (
		GTK_TEXT_VIEW (textview), event->window);
	if (wtype != GTK_TEXT_WINDOW_TEXT)
		return FALSE;
*/
	printd (DEBUG, "output button press cb");
#ifdef E2_TABS_DETACH
	//just changing notebook page is not sufficient to set current tab when
	//there's > 1 notebook
	if (event->type == GDK_BUTTON_PRESS)
	{
		if (rt != curr_tab)
		{
			//swap with mimimum race-risk ...
			*curr_tab = app.tab;	//backup current tab's data from stack to heap
			app.tab = *rt;	//get the replacement stuff into stackspace
			curr_tab = rt;
			//adjust all relevant child foreground-tab pointers to/from the stacked tab data
			e2_command_retab_children (&app.tab, curr_tab);
			printd (DEBUG, "output tab change, new current-tab ID is %d", curr_tab->labelnum);
		}
	}
#endif
	E2_OutputTabRuntime *rrt = (rt == curr_tab) ? &app.tab : rt;
	/*for double-clicks, the callback sequence is:
	1, then another < click interval, then 1 more with 0 click interval
	for triple-clicks, the callback sequence is:
	1, then 2 * last 2 of double click sequence = 5 total
	The 0-click interval clicks are of type GDK_2BUTTON_PRESS or
	GDK_3BUTTON_PRESS as appropriate
	*/
	if (event->button == 1 && event->type == GDK_BUTTON_PRESS)
	{
		static gboolean valid_selection = FALSE;
		static GtkTextIter start, end;
		extern guint click_interval;
		static guint32 last_event_time = 0;
		guint32 interval;

		if (event->state & GDK_CONTROL_MASK)
		{
			gchar *text = _e2_output_get_item_path (event->x, event->y, rrt);
			if (text != NULL)
			{
				_e2_output_open_text (text, TRUE);	//, FALSE);
				g_free (text);
				return TRUE;
			}
		}
		interval = event->time - last_event_time;
		last_event_time = event->time;
//		printd (DEBUG, "output button press interval %d", interval);
		//we don't want to repeat the action, if there was a double/triple click
		if (interval >= click_interval)
		{

//			output_activated = FALSE;	//reinstate normal release handling
			//because a selection will be cleared by thge normal release
			//callback, note what is selected in case it's a double-click
			valid_selection = gtk_text_buffer_get_selection_bounds (rrt->buffer,
				&start, &end);
/*			//CHECKME is this sensible ?? it does not focus the commandline
			gchar *action_name = g_strconcat (_A(1),".",_A(42),NULL);  //_("command.focus")
			e2_action_run_simple (action_name, NULL);
			g_free (action_name);
*/
			//focus the output tab if it's not going to be hidden
			if (!e2_option_bool_get_direct (app.output.opt_hide_on_focus_out)
#ifdef E2_TABS_DETACH
				|| rrt->detached
#endif
				)
				gtk_widget_grab_focus (textview);
		}
		else
		{
			//reselect what was de-selected by the intervening release callback,
			//ready for the next press callback (type GDK_2BUTTON_PRESS etc)
			if (valid_selection)
			{
				gtk_text_buffer_select_range (rrt->buffer, &start, &end);
				valid_selection = FALSE;
			}
		}
	}
	else if (event->button == 1 &&
	//		(
			event->type == GDK_2BUTTON_PRESS //|| event->type == GDK_3BUTTON_PRESS)
			)
	{
		printd (DEBUG, "output button press event type %d", event->type);
		if (_e2_output_get_item_path (event->x, event->y, rrt) != NULL)
			return (_e2_output_activated_cb (rrt));
		//FIXME stop the selected text from being de-selected in the
		//button-release callback - but we can't just block that !!
	}
	else if (event->button == 2
#ifdef E2_TABS_DETACH
		&& !rrt->detached
#endif
		)
	{
		e2_window_output_hide (NULL, NULL, NULL);
		return TRUE;
	}
	else if (event->button == 3)
	{
		_e2_output_show_context_menu (textview, 3, event->time, rrt);
		return TRUE;
	}
	return FALSE;
}
/**
@brief process mouse button-release in the output pane

This is essentially to re-select text which is de-selected by
the standard release callback

@param textview the widget where the release happened
@param event gdk event data struct
@param rt runtime struct to work on

@return TRUE if the signal has been handled
*/
/*static gboolean _e2_output_button_release_cb (GtkWidget *textview,
	GdkEventButton *event, E2_OutputTabRuntime *rt)
{
	printd (DEBUG, "output button release cb");
	return FALSE;
	E2_OutputTabRuntime *rrt = (rt == curr_tab) ? &app.tab : rt;
	gtk_text_buffer_select_range (rrt->buffer, &start, &end);
	return TRUE;
} */
/**
@brief process menu button press in the output pane

@param widget the textview widget where the press happened
@param rt output runtime struct to work on

@return TRUE if the signal has been handled
*/
static gboolean _e2_output_popup_menu_cb (GtkWidget *widget, E2_OutputTabRuntime *rt)
{
	E2_OutputTabRuntime *rrt = (rt == curr_tab) ? &app.tab : rt;
	gint event_time = gtk_get_current_event_time ();
	_e2_output_show_context_menu (widget, 0, event_time, rrt);
	return TRUE;
}
/**
@brief Determine whether the buffer end-mark is on-screen, after manual change to the vertical adjustment

This is a callback for manual changes to the output textview vertical adjustment

@param adjust UNUSED the vertical adjustment for the textview
@param rt data struct for the output pane

@return
*/
static void _e2_output_scrolled_cb (GtkAdjustment *adjust, E2_OutputTabRuntime *rt)
{
	//use active-tab data when relevant
	E2_OutputTabRuntime *rrt = (rt == curr_tab) ? &app.tab : rt;

	//FIXME make this faster - local copy of the option value ?
	GtkTextIter iter;
	if (e2_option_bool_get_direct (app.output.opt_jump_end))
		//scrolling only when new output belongs to the last context in the textbuffer
		gtk_text_buffer_get_end_iter (rrt->buffer, &iter);
	else
		gtk_text_buffer_get_iter_at_mark (rrt->buffer, &iter, rrt->mark);

#ifdef DEBUG_MESSAGES
	if (_e2_output_iter_offscreen (rrt->text, &iter))
	{
		if (rrt->onscreen)
			printd (NOTICE, "not following anymore");
		rrt->onscreen = FALSE;
	}
	else
	{
		if (!rrt->onscreen)
			printd (NOTICE, "following again");
		rrt->onscreen = TRUE;
	}
#else
	rrt->onscreen = !_e2_output_iter_offscreen (rrt->text, &iter);
#endif
}
/*
static gboolean test_cb (GtkWidget *widget, GdkEventMotion *event)
{
	int x, y;
	GdkModifierType state;
	if (event->is_hint)
	{
	        gdk_window_get_pointer(event->window, &x, &y, &state);
	        printf("x = %d y = %d\n", x, y);
	}
	else
	{
	        printf("x: %lf y: %lf\n", event->x, event->y);
	}
	return FALSE;
}
*/
/**
@brief handle an output-pane tab change
This is callback for the notebook's "switch-page" signal
Essentially, tab-specific data are swpped between stack and heap space,
and pointers for affected running commands are adjusted accordingly
@param notebook the notebook widget
@param page UNDOCUMENTED notebook page which is now focused
@param page_num the 0-based index of the new page
@param data UNUSED pointer to data specified when callback was connected

@return
*/
static void _e2_output_tabchange_cb (GtkNotebook *notebook,
	GtkNotebookPage *page, guint page_num, gpointer data)
{
	E2_OutputTabRuntime *newtab;
#ifdef E2_TABS_DETACH
	GtkWidget *child = gtk_notebook_get_nth_page (notebook, page_num);
	GList *member;
	for (member = app.tabslist; member != NULL; member = member->next)
	{
		newtab = (E2_OutputTabRuntime *)member->data;
		if (newtab->scroll == child)
		{
			printd (DEBUG, "output tab change cb, new current-tab ID is %d", newtab->labelnum);
			break;
		}
	}
	if (member == NULL)
		return;	//should never happen
#else
	printd (DEBUG, "output tab change cb, new current-tab index %d", page_num);
//	printd (DEBUG, "curr_tab is %x", curr_tab);
	newtab = g_list_nth_data (app.tabslist, page_num);
#endif
	//swap with mimimum race-risk ...
	*curr_tab = app.tab;	//backup current tab's data from stack to heap
	app.tab = *newtab;	//get the replacement stuff into stackspace
	curr_tab = newtab;
//	printd (DEBUG, "curr_tab NOW is %x", curr_tab);
	//adjust all relevant child foreground-tab pointers to/from the stacked tab data
	e2_command_retab_children (&app.tab, curr_tab);
}

  /******************/
 /***** public *****/
/******************/

/**
@brief show end-of-output message in tab associated with @a tab

@param tab pointer to tab runtime
@param beep TRUE to sound when the message is printed

@return
*/
void e2_output_print_end (E2_OutputTabRuntime *tab, gboolean beep)
{
	e2_output_print (tab, _("-- end-of-output --"), NULL, TRUE, "small", "grey", NULL);
	if (beep)
		e2_utils_beep ();
}
/**
@brief show error message @a msg in current output pane tab, with beep

@param msg message
@param freemsg TRUE to free @a msg after it has been shown

@return
*/
//FIXME use the "correct" tab instead of always the current one
void e2_output_print_error (gchar *msg, gboolean freemsg)
{
	e2_output_print (&app.tab, msg, NULL, TRUE, E2_ERRORTAGS, NULL);
	if (freemsg)
		g_free (msg);
	e2_utils_beep ();
}
/**
@brief show system error message in current output pane tab, with beep

@return
*/
//FIXME use the "correct" tab instead of always the current one
void e2_output_print_strerrno (void)
{
	e2_output_print (&app.tab, (gchar *) g_strerror (errno), NULL, TRUE,
		E2_ERRORTAGS, NULL);
	e2_utils_beep ();
}
/**
@brief show @a msg in output pane

@a msg is converted to utf-8 if it's not that form already
Provides special handling of any '\\b', '\\r' char(s) in @a msg
for an error message, style parameters are ignored
Expects BGL to be on/closed

@param tab pointer to data structure for the tab to get the message
@param msg actual message
@param origin context to which the message belongs (eg a pid string) or NULL for default
@param newline TRUE if @a msg is to be printed with pre- and post- newline
@param first_tag one or more tags to apply, terminated by NULL

@return
*/
void e2_output_print (E2_OutputTabRuntime *tab,
	gchar *msg, gchar *origin, //gboolean error, gboolean debug,
		gboolean newline, const gchar *first_tag, ...)
{
	//early exit
	if (msg == NULL) return;

//	printd (DEBUG, "e2_ouput_print (msg:,origin:%s,error:%d,debug:%d,newline:%d,first_tag:)",
//		origin, error, debug, newline);

	//if this is not a debug message
	//show the output pane if the user wishes that
	if (//!debug &&
		!app.output.visible && e2_option_bool_get_direct (app.output.opt_show_on_new))
	{
		e2_window_output_show (NULL, NULL);
	}

	//parse the msg string to handle special characters

	VOL gint len = strlen (msg);
	//process leading backspaces
	//these delete characters from the last message of this context
	VOL gint back = 0;
	while ((msg[0]) == '\b')
	{
		back++;
		msg++;
		len--;
	}

	//FIXME: handle \r correctly
	//flag whether there's a carriage return '\r' in the message
	VOL gboolean line_back = FALSE;
	//flag for backspaces not at the beginning of the message
	VOL gboolean backspaces_left = FALSE;
	//flag whether message variable should be freed
	VOL gboolean free_message = FALSE;
	VOL gint i;

	//FIXME make this faster !!
	for (i = 0; i < len; i++)
	{
//		printd (DEBUG, "%c - %d", msg[i], (gint) msg[i]);
/* no point in handling just this escape code ...
		//escape
		if (((gint) msg[i]) == 27)
		{
			//clear sequence: ESC[2J
			if (((i + 3) < len) && (msg[i + 1] == '[') &&
				(msg[i + 2] == '2') && (msg[i + 3] == 'J'))
			{
				_e2_output_clear ();
				return;
			}

		}
		else */
		//carriage return
			if ((msg[i]) == '\r')
			line_back = TRUE;
		//backspace
		else if ((msg[i]) == '\b')
		{
			//yes, there are backspaces in the middle
			backspaces_left = TRUE;
			gint j = 1;
			//find a character back in the string that is not
			//already a backspace
			//FIXME handle multi-byte chars
			while (j <= i)
			{
				if (msg[i - j] == '\b')
					j++;
				else
				{
					//overwrite it with backspace (i.e. later replaced)
					msg[i - j] = '\b';
					break;
				}
			}
			//if there is no prior non-backspace character,
			//setup to delete one from the previous message
			if (j > i)
				back++;
		}
	}

	//eliminate any backspace char(s)
	//FIXME handle multi-byte chars
	if (backspaces_left)
	{
		VOL gchar *s1 = msg, *s2;
		while (*s1 != '\0')
		{
			if (*s1 == '\b')
			{
				s2 = s1+1;
				while (*s2 == '\b') s2++;
				gint slide = (msg+len+1-s2);
				memcpy ((gchar *)s1, (gchar *)s2, slide);
				len -= (s2-s1);
			}
			else
				s1++;
		}
	}

	VOL gchar *utf;
	//check if message is already utf8
	if (g_utf8_validate (msg, -1, NULL))
		utf = msg;
	else
	//	utf = e2_utf8_from_locale (msg);
	{
		//convert to utf before inserting
		GError *error = NULL;
		utf = g_locale_to_utf8 (msg, -1, NULL, NULL, &error);
		if (error != NULL)
		{
			printd (WARN, "locale string to UTF8 conversion failed: %s", error->message);
			g_error_free (error);
			utf = e2_utf8_from_locale_fallback (msg);
		}
	}

	//ensure that there's an origin set
	if (origin == NULL)
		origin = "default";	//no translate
	VOL gboolean is_default_origin = g_str_equal (origin, "default");
	//flag whether this origin is new
	VOL gboolean is_new;
	//get the text buffer for the output pane
	VOL GtkTextBuffer *buffer = tab->buffer;
	VOL GtkTextIter start, end;

//	gdk_threads_enter ();
	//try to get insert mark
	//it exists if there has been output in this origin before
	VOL GtkTextMark *origin_mark = gtk_text_buffer_get_mark (buffer, origin);
	//if not, create it at the end
	if (origin_mark == NULL)
	{
		is_new = TRUE;
		gtk_text_buffer_get_end_iter (buffer, &end);
		origin_mark = gtk_text_buffer_create_mark (buffer, origin, &end, TRUE);
	}
	else
	{
		is_new = FALSE;
		//the default context always outputs to the end of the textview
		//(ie the output is not "glued together"
		if (is_default_origin)
		{
			gtk_text_buffer_get_end_iter (buffer, &end);
			gtk_text_buffer_move_mark (buffer, origin_mark, &end);
		}
		else
			gtk_text_buffer_get_iter_at_mark (buffer, &end, origin_mark);
	}
	//move the scroll helper mark
	//(it is used by the scroll callback to find out if the user is
	//following output)
	gtk_text_buffer_move_mark (buffer, tab->mark, &end);
	gtk_text_buffer_place_cursor (buffer, &end);

	//do we have to overwrite a previous message's characters in front of us?
	//(if a previous message had a carriage return '\r' in it)
	VOL gchar *mark_del_name = g_strconcat (origin, "-del", NULL);	//no translate
	VOL GtkTextMark *mark_del = gtk_text_buffer_get_mark (buffer, mark_del_name);
	if (mark_del != NULL)
	{
		VOL GtkTextIter iter_del;
		gtk_text_buffer_get_iter_at_mark (buffer, &iter_del, mark_del);
		gtk_text_buffer_delete (buffer, &end, &iter_del);
	}

	if (back > 0)
	{
		VOL gint _offset = gtk_text_iter_get_offset (&end);
		GtkTextIter backi;
		gtk_text_buffer_get_iter_at_offset (buffer, &backi, _offset - back);
		gtk_text_buffer_delete (buffer, &backi, &end);
		gtk_text_buffer_get_iter_at_offset (buffer, &end, _offset - back);
	}
	//delete the delete mark, it's only used one time
	if (mark_del != NULL)
		gtk_text_buffer_delete_mark (buffer, mark_del);

	VOL gint lfcount = 0;

	//if necessary, insert a newline character before the message because
	//it has a different context from the last one and the last one hasn't
	//printed one yet;
//	g_static_rec_mutex_lock (&print_mutex);
	if (tab->origin_lastime != NULL
		&& !g_str_equal (tab->origin_lastime, origin)
		&& !gtk_text_iter_starts_line (&end)
		&& (is_new || is_default_origin))
			lfcount = 1;
//	g_static_rec_mutex_unlock (&print_mutex);

	if (newline && (!gtk_text_iter_starts_line (&end)))
	{
		printd (DEBUG, "inserted additional newline because the message requested it");
		lfcount++;
	}

	VOL GtkTextMark *mark_end;
	if (lfcount > 0)
	{
		gtk_text_buffer_insert (buffer, &end, "\n", lfcount);
		mark_end = gtk_text_buffer_get_insert (buffer);
		gtk_text_buffer_get_iter_at_mark (buffer, &end, mark_end);
	}
	//AT LAST, WE PUT IT IN !
	gtk_text_buffer_insert (buffer, &end, utf, -1);
	//get start and end of text just inserted
	gtk_text_buffer_get_iter_at_mark (buffer, &start, origin_mark);
	mark_end = gtk_text_buffer_get_insert (buffer);
	gtk_text_buffer_get_iter_at_mark (buffer, &end, mark_end);
	if (first_tag != NULL)
	{
		//apply style-tags (if any) for added text
		va_list args;
		va_start (args, first_tag);
		VOL const gchar *tag_name = first_tag;
		while (tag_name != NULL)
		{
			VOL GtkTextTag *tag = gtk_text_tag_table_lookup (buffer->tag_table, tag_name);
/* tags are all pre-configured
			if (tag == NULL)
				printd (WARN, "%s: no tag with name '%s'!", G_STRLOC, tag_name);
			else */
				gtk_text_buffer_apply_tag (buffer, tag, &start, &end);
			tag_name = va_arg (args, const gchar*);
		}
		va_end (args);
	}
/*	when there is a context after the current one, the font weight of
	the later one (eg bold) is sometimes (eg current has error then normal)
	used instead of the proper font (normal)
	EVEN IF the inserted text is explicitly set to normal style
	probably a gtk bug as the font color is correct
	the following is a workaround */
	else //if (!gtk_text_iter_is_end (&end))
	{
		//gtk_text_buffer_remove_all_tags (buffer, &start, &end);
		VOL GSList *tags = gtk_text_iter_get_tags (&start);
		if (tags != NULL)
		{
			gtk_text_buffer_remove_all_tags (buffer, &start, &end);
			g_slist_free (tags);
		}
	}

	if (newline)
	{	//trailing \n
		gtk_text_buffer_insert (buffer, &end, "\n", 1);
		mark_end = gtk_text_buffer_get_insert (buffer);
		gtk_text_buffer_get_iter_at_mark (buffer, &end, mark_end);
	}

	//scroll down if necessary
	if (//scrolling to new output is in effect
		e2_option_bool_get_direct (app.output.opt_jump)
		&& (//manual scrolling has not moved the insert mark off-screen
			tab->onscreen
			//scroll regardless of whether manually scrolled away
			|| !e2_option_bool_get_direct (app.output.opt_jump_follow))
		&& (
			//added text is at the end of the buffer
			gtk_text_iter_is_end (&end)
			//don't care whether added text is at the end of the buffer
			|| !e2_option_bool_get_direct (app.output.opt_jump_end))
			//the end of the added text is not visible already
		&& _e2_output_iter_offscreen (tab->text, &end))
			gtk_text_view_scroll_to_mark (tab->text, mark_end, 0.0, TRUE, 0.1, 1.0);

	//if there was a carriage return, save a delete mark for the next message
	if ((line_back) && (!is_new))
		//CHECKME origin_mark moved in this case too ?
		gtk_text_buffer_create_mark (buffer, mark_del_name, &end, TRUE);
	else
		gtk_text_buffer_move_mark (buffer, origin_mark, &end);

//	gdk_threads_leave ();

	if (utf != msg)
		g_free ((gchar *)utf);
	if (free_message)
		g_free (msg);
	g_free ((gchar *)mark_del_name);

	//protect threaded clearing of origin_buffer
	g_static_rec_mutex_lock (&print_mutex);
	if (tab->origin_lastime == NULL
		|| !g_str_equal (tab->origin_lastime, origin))
	{
		//save the origin of the last message
		if (tab->origin_lastime != NULL)
			g_free (tab->origin_lastime);
		tab->origin_lastime = g_strdup (origin);
	}
	g_static_rec_mutex_unlock (&print_mutex);
}
#undef VOL
/**
@brief update several output pane textview settings to conform to current config parameters
Used only during window re-creation
@return
*/
void e2_output_update_style (void)
{
	const gchar *fntname = e2_utils_get_output_font ();
	PangoFontDescription *font_desc = pango_font_description_from_string
			(fntname);
	app.output.font_size = pango_font_description_get_size (font_desc);	//(pixels or points) * PANGO_SCALE

	GList *member;
	for (member = app.tabslist; member != NULL; member = member->next)
	{
		GtkTextView *tvw = ((E2_OutputTabRuntime *)member->data)->text;
		gtk_text_view_set_wrap_mode (tvw,
			e2_option_int_get ("output-wrap-mode"));
		gtk_text_view_set_left_margin (tvw,
			e2_option_int_get ("output-left-margin"));
		gtk_text_view_set_right_margin (tvw,
			e2_option_int_get ("output-right-margin"));
		gtk_widget_modify_font (GTK_WIDGET (tvw), font_desc);

		GtkTextBuffer *buf = ((E2_OutputTabRuntime *)member->data)->buffer;
		GtkTextTagTable *table = gtk_text_buffer_get_tag_table (buf);
		GtkTextTag *tag = gtk_text_tag_table_lookup (table, "green");
		g_object_set (G_OBJECT (tag), "foreground",
			e2_option_str_get ("color-positive"), NULL);
		tag = gtk_text_tag_table_lookup (table, "red");
		g_object_set (G_OBJECT (tag), "foreground",
			e2_option_str_get ("color-negative"), NULL);
		tag = gtk_text_tag_table_lookup (table, "grey");
		g_object_set (G_OBJECT (tag), "foreground",
			e2_option_str_get ("color-unimportant"), NULL);
		tag = gtk_text_tag_table_lookup (table, "small");
		g_object_set (G_OBJECT (tag), "size",
			(gint) (PANGO_SCALE_SMALL * app.output.font_size), NULL);
	}
	pango_font_description_free (font_desc);
}
/**
@brief create a new textview and buffer and some basic tags

@param rt runtime data struct for the tab (NOT app.tab)

@return scrolled window containing a textview
*/
static GtkWidget *_e2_output_create_view (E2_OutputTabRuntime *rt)
{
	//visible flag is set elswhere, depending on cache data
	//init some vars
	//at the outset, assume that the current content is on-screen
	rt->onscreen = TRUE;
	rt->mark = NULL;
	//create scrolled window
	rt->scroll = e2_widget_get_sw (GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC,
		GTK_SHADOW_OUT);

	//create text view
	rt->text = GTK_TEXT_VIEW (gtk_text_view_new ());
	gtk_container_add (GTK_CONTAINER (rt->scroll), GTK_WIDGET (rt->text));
	gtk_text_view_set_editable (rt->text, FALSE);
	gtk_text_view_set_cursor_visible (rt->text, FALSE);
//	allow focus so popup menu signal can happen, & can select text in the pane
//	GTK_WIDGET_UNSET_FLAGS (rt->text, GTK_CAN_FOCUS);

	gtk_text_view_set_wrap_mode (rt->text,
		e2_option_int_get ("output-wrap-mode"));
	gtk_text_view_set_left_margin (rt->text,
		e2_option_int_get ("output-left-margin"));
	gtk_text_view_set_right_margin (rt->text,
		e2_option_int_get ("output-right-margin"));
	const gchar *fntname = e2_utils_get_output_font ();
	PangoFontDescription *font_desc = pango_font_description_from_string
			(fntname);
	gtk_widget_modify_font (GTK_WIDGET (rt->text), font_desc);
	pango_font_description_free (font_desc);

	//signal used for "links" in the output pane
//	g_signal_connect (G_OBJECT (rt->text), "motion-notify-event",
//		G_CALLBACK (test_cb), NULL);
	gtk_widget_set_events (GTK_WIDGET (rt->text),
		gtk_widget_get_events (GTK_WIDGET (rt->text))
		| GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK);
	g_signal_connect (G_OBJECT (rt->text), "popup_menu",
		G_CALLBACK (_e2_output_popup_menu_cb), rt);
	g_signal_connect (G_OBJECT (rt->text), "button-press-event",
		G_CALLBACK (_e2_output_button_press_cb), rt);
//	g_signal_connect_after (G_OBJECT (rt->text), "button-release-event",
//		G_CALLBACK (_e2_output_button_release_cb), rt);
//YUK no "activate" available, nor so for any ancestor
	g_signal_connect_after (G_OBJECT (rt->text->vadjustment), "value-changed",
		G_CALLBACK (_e2_output_scrolled_cb), rt);

	gtk_widget_show (GTK_WIDGET (rt->text));

	rt->buffer = gtk_text_view_get_buffer (rt->text);

	gtk_text_buffer_create_tag (rt->buffer, "bold", "weight", PANGO_WEIGHT_BOLD, NULL);
	gtk_text_buffer_create_tag (rt->buffer, "italic", "style", PANGO_STYLE_ITALIC, NULL);
	gtk_text_buffer_create_tag (rt->buffer, "uline", "underline", PANGO_UNDERLINE_SINGLE, NULL);
	gtk_text_buffer_create_tag (rt->buffer, "small", "size",
			(gint) (PANGO_SCALE_SMALL * app.output.font_size), NULL);
	gtk_text_buffer_create_tag (rt->buffer, "blue", "foreground", "blue", NULL);
	gtk_text_buffer_create_tag (rt->buffer, "green", "foreground",
		e2_option_str_get ("color-positive"), NULL);
	gtk_text_buffer_create_tag (rt->buffer, "red", "foreground",
		e2_option_str_get ("color-negative"), NULL);
	gtk_text_buffer_create_tag (rt->buffer, "grey", "foreground",
		e2_option_str_get ("color-unimportant"), NULL);
/* these not needed unless output links are parsed
	gtk_text_buffer_create_tag (rt->tab.buffer, "link", "foreground", "blue",
		"underline", PANGO_UNDERLINE_NONE, NULL);
	gtk_text_buffer_create_tag (rt->tab.buffer, "link-active", "foreground", "blue",
		"underline", PANGO_UNDERLINE_SINGLE, NULL); */

	GtkTextIter end;
	gtk_text_buffer_get_end_iter (rt->buffer, &end);
	rt->mark = gtk_text_buffer_create_mark (rt->buffer,
		"internal-end-mark", &end, FALSE);

	//attach options
	rt->opt_wrap = e2_option_attach_value_changed ("output-wrap-mode",
		GTK_WIDGET (rt->text), _e2_output_set_op_wrap, rt);

	gtk_widget_show (rt->scroll);
	return rt->scroll;
}
/**
@brief create output pane

Create initial output pane - with initial no. of panes as per cache

@return notebook, one or more pages, each containing an output pane
*/
GtkWidget *e2_output_initialise (void)
{
	//create notebook for tabs, no callback until all pages created
	app.outbook = gtk_notebook_new ();
	GtkNotebook *book = GTK_NOTEBOOK (app.outbook);
	gtk_notebook_set_show_tabs (book, (app.tabcount > 1));
	gtk_notebook_popup_enable (book);
	gtk_notebook_set_tab_pos (book, GTK_POS_LEFT);
	gtk_notebook_set_scrollable (book, TRUE);
	gtk_notebook_set_show_border (book, FALSE);
	const gchar *fntname = e2_utils_get_output_font ();
	PangoFontDescription *font_desc = pango_font_description_from_string
			(fntname);
	app.output.font_size = pango_font_description_get_size (font_desc);//(pixels or points) * PANGO_SCALE
	pango_font_description_free (font_desc);
	if (app.output.font_size == 0)	//in case of invalid font name string
	{
		//set 10-point default
		app.output.font_size =
			(pango_font_description_get_size_is_absolute (font_desc)) ?
			10 * PANGO_SCALE * 96 / 72 :	//assume screen is 96 DPI
			10 * PANGO_SCALE;
	}

	printd (DEBUG, "stacked tab is at %x", &app.tab);
	//we're always going to have at least 1 tab, which gets the initial focus
	gint i;
	E2_OutputTabRuntime *tab = NULL;	//assignment for complier-warning prevention only
	//iterate backward so that we end with the first (default) tab
	for (i = app.tabcount ; i > 0 ; i--)
	{
		tab = ALLOCATE0 (E2_OutputTabRuntime);	//FIXME only deallocated by the user, not at session end
		CHECKALLOCATEDFATAL (tab);
//		printd (DEBUG, "created tab data at %x", tab);
		app.tabslist = g_list_prepend (app.tabslist, tab);
		GtkWidget *sw = _e2_output_create_view (tab);
		gchar *labltxt = g_strdup_printf ("%d", i);	//tab labels are numbers
		GtkWidget *label = gtk_label_new (labltxt);
		gtk_notebook_prepend_page (book, sw, label);
		g_free (labltxt);
#ifdef USE_GTK2_10
		tab->labelnum = i;	//save tab id for matching
		gtk_notebook_set_tab_reorderable (book, sw, TRUE);
# ifdef E2_TABS_DETACH
		gtk_notebook_set_tab_detachable (book, sw, TRUE);
#  ifdef USE_GTK2_12DND
		gtk_notebook_set_group (book, app.outbook);	//instance-specific pointer
#  else
		gtk_notebook_set_group_id (book, getpid());
#  endif
# endif
#endif
	}
	//last-added one becomes current
	//its current-page is set when the window is shown
	app.tab = *tab;
	curr_tab = tab;

#ifdef E2_TABS_DETACH
	//enable tab dragging to new windows
# ifdef USE_GTK2_12DND
	g_signal_connect (G_OBJECT (app.outbook), "create-window",
		G_CALLBACK (_e2_output_tab_drop_new), NULL);	//CHECKME user_data
# else
	//this is the gtk 2.10 approach
	gtk_notebook_set_window_creation_hook
		((GtkNotebookWindowCreationFunc) _e2_output_tab_drop_new,
		NULL, //CHECKME gpointer data
		NULL);	//(GDestroyNotify) destroy
# endif
	//enable processing of tabs being dragged back from a new window
	gtk_drag_dest_set (app.outbook, GTK_DEST_DEFAULT_DROP, target_table2, n_targets2,
		GDK_ACTION_MOVE);
	g_signal_connect (G_OBJECT (app.outbook), "drag-data-received",
		G_CALLBACK (_e2_output_tabdrag_data_received_cb), NULL);	//CHECKME user_data
	//capture clicks on tab labels
	g_signal_connect (G_OBJECT (app.outbook), "grab-focus",
		G_CALLBACK (_e2_output_grab_focus_cb), NULL);
//	g_signal_connect (G_OBJECT (app.outbook), "focus-in-event",
//		G_CALLBACK (_e2_output_focus_in_cb), NULL);
	//for unblocking
	g_signal_connect (G_OBJECT (app.outbook), "page-removed",
		G_CALLBACK (_e2_output_tabgone_cb), app.main_window);
#endif

	g_signal_connect (G_OBJECT (app.outbook), "switch-page",
		G_CALLBACK (_e2_output_tabchange_cb), NULL);	//no data
	//setup data common to all tabs
	//repetitive, but option data structs may move after options hash recreation,
	//and possibly without re-running the options init function
	app.output.opt_show_on_new = e2_option_get ("show-output-window-on-output");
//	app.output.opt_show_on_focus_in = e2_option_get ("command-line-show-output-on-focus-in"); UNUSED DIRECTLY
	app.output.opt_hide_on_focus_out = e2_option_get ("command-line-hide-output-on-focus-out");
	app.output.opt_jump = e2_option_get ("output-jump-new");
	app.output.opt_jump_follow = e2_option_get ("output-jump-new-following");
	app.output.opt_jump_end = e2_option_get ("output-jump-new-end");

	//setup mutex to protect threaded access to print-function static variables
	g_static_rec_mutex_init (&print_mutex);

	return app.outbook;
}
/**
@brief register actions related to output pane

@return
*/
void e2_output_actions_register (void)
{
	gchar *action_name = g_strconcat(_A(9),".",_A(29),NULL);
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_clear_action, NULL, FALSE);
	action_name = g_strconcat(_A(9),".",_A(68),NULL);
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_print_action, NULL, TRUE);
	action_name = g_strconcat(_A(9),".",_A(50),NULL);  //output.help
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_help_action, NULL, TRUE);
	action_name = g_strconcat(_A(9),".",_A(74),NULL);  //output.scrolldown
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_scroll_step, GINT_TO_POINTER (TRUE), TRUE);
	action_name = g_strconcat(_A(9),".",_A(75),NULL);  //output.scrollup
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_scroll_step, GINT_TO_POINTER (FALSE), TRUE);
	action_name = g_strconcat(_A(9),".",_A(64),NULL); //output.pagedown
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_scroll_page, GINT_TO_POINTER (TRUE), TRUE);
	action_name = g_strconcat(_A(9),".",_A(65),NULL); //output.pageup
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_scroll_page, GINT_TO_POINTER (FALSE), TRUE);
	action_name = g_strconcat(_A(9),".",_A(48),NULL); //output.goto bottom
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_scroll_all, GINT_TO_POINTER (TRUE), TRUE);
	action_name = g_strconcat(_A(9),".",_A(49),NULL); //output.goto top
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_scroll_all, GINT_TO_POINTER (FALSE), TRUE);
	action_name = g_strconcat (_A(9),".",_A(26),NULL); //output.add
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_tab_add, NULL, FALSE);
	action_name = g_strconcat (_A(9),".",_A(38),NULL); //output.delete
	e2_action_register_simple (action_name, E2_ACTION_TYPE_ITEM,
		_e2_output_tab_remove, NULL, FALSE);	//FIXME data
}
/**
@brief register options related to output pane

@return
*/
void e2_output_options_register (void)
{
	gchar *group_name = g_strconcat(_C(6),".",_C(27),":",_C(25),NULL); //_("commands.output:miscellaneous"
	e2_option_bool_register ("show-output-window-on-output", group_name,
		_("show output pane when a new message appears"),
		_("This will ensure you don't miss any messages"), NULL, FALSE,
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP);
	e2_option_bool_register ("command-line-show-output-on-focus-in", group_name,
		_("show output pane if the command line is focused"),
		_("This causes the output pane to be opened when you are about enter a command"),
		NULL, FALSE,
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_BUILDBARS);
	e2_option_bool_register ("command-line-hide-output-on-focus-out", group_name,
		_("hide output pane if the command line is unfocused"),
		_("This causes the output pane to be closed when you move focus away from the command line"),
		NULL, FALSE,
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_BUILDBARS);
	e2_option_bool_register ("fileop-show", group_name, _("show commands"),
 		_("This echoes the commands that are run and their exit value"), NULL, TRUE,
		E2_OPTION_FLAG_ADVANCED);
	e2_option_bool_register ("output-jump-new", group_name, _("scroll to new output"),
		_("This will automatically scroll the output pane content, to show new message(s)"), NULL, TRUE,
		E2_OPTION_FLAG_ADVANCED);
	e2_option_bool_register ("output-jump-new-following", group_name, _("only scroll when really following"),
		_("This stops automatic pane scrolling to new output if you've manually scrolled away"),
		"output-jump-new", TRUE,
		E2_OPTION_FLAG_ADVANCED);
	e2_option_bool_register ("output-jump-new-end", group_name, _("only scroll when new output is at the end"),
		_("This stops pane scrolling if a different process has displayed text after the current insert position"),
  		"output-jump-new", FALSE,
		E2_OPTION_FLAG_ADVANCED);
	const gchar *opt_wrap_mode[] = {_("none"), _("everywhere"), _("words"), NULL};
	group_name = g_strconcat(_C(6),".",_C(27),":",_C(37),NULL);	//_("commands.output:style"
	e2_option_sel_register ("output-wrap-mode", group_name, _("line wrap mode"),
		_("If mode is 'none', a horizontal scrollbar will be available. Mode 'words' will only break the line between words"),
		NULL, 2, opt_wrap_mode,
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP | E2_OPTION_FLAG_BUILDALL);
	e2_option_int_register ("output-left-margin", group_name, _("left margin (pixels)"),
		_("This is the left margin between the output-pane edge and the text in it"),
		NULL, 6, 0, 1000,
		E2_OPTION_FLAG_ADVANCED);
	e2_option_int_register ("output-right-margin", group_name, _("right margin (pixels)"),
		_("This is the right margin between the output-pane edge and the text in it"),
		 NULL, 2, 0, 1000,
		E2_OPTION_FLAG_ADVANCED);
	e2_option_bool_register ("custom-output-font",
		group_name, _("use custom font"),
		_("If activated, the font specified below will be used, instead of the theme default"),
		NULL, FALSE,
		E2_OPTION_FLAG_BASIC);  //no rebuild
	e2_option_font_register ("output-font", group_name, _("custom output font"),
		_("This is the font used for text in the output pane"), "custom-output-font", "Sans 10", 	//_I( font name
		E2_OPTION_FLAG_BASIC);  //no rebuild
//	group_name = g_strconcat(_C(32),".",_C(2),":",_C(27),NULL); //_("panes.other colors:output"
	group_name = g_strconcat(_C(6),".",_C(27),":",_C(2),NULL);	//_("commands.output:colors"
	//CHECKME which of these output colors really do need the whole window to be reconstructed ?
	e2_option_color_register ("color-positive", group_name, _("positive color"),
		_("This color is used for messages about successful operations or other 'positive events'"),
		NULL, "dark green",
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_FREEGROUP | E2_OPTION_FLAG_BUILDALL);
	e2_option_color_register ("color-negative", group_name, _("negative color"),
		_("This color is used for messages about unsuccessful operations or other 'negative events'"),
		NULL, "red",
		E2_OPTION_FLAG_ADVANCED  | E2_OPTION_FLAG_BUILDALL);
	e2_option_color_register ("color-unimportant", group_name, _("unimportant color"),
		_("This color is used for messages of minor importance or other miscellaneous events"),
		NULL, "light grey",
		E2_OPTION_FLAG_ADVANCED | E2_OPTION_FLAG_BUILDALL);

}
