/* shell.c - user defined shell commands 
   Copyright (C) 1996, 1997 Paul Sheer

   This program 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 2 of the License, or
   (at your option) any later version.

   This program 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 this program; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
   02111-1307, USA.
 */

#include <config.h>
#include <sys/types.h>
#if HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif
#ifndef WEXITSTATUS
#define WEXITSTATUS(stat_val) ((unsigned)(stat_val) >> 8)
#endif
#ifndef WIFEXITED
#define WIFEXITED(stat_val) (((stat_val) & 255) == 0)
#endif
#include <errno.h>
#ifdef HAVE_SYS_ERRNO_H
#include <sys/errno.h>
#endif
#include "editoptions.h"
#include "shell.h"
#include "pool.h"
#include "mad.h"

#define shell_error_dialog(h,t) CErrorDialog(e->widget->winid,20,20,h,"%s",t)

#undef gettext_noop
#define gettext_noop(x) x

int save_options_section (const char *file, const char *section, const char *text);
extern char *editor_options_file;
extern char *init_font;
extern Window main_window;

/* Here are the default example shells */
struct shell_cmd default_scripts[] =
{
    {
	" Sed ",
	"Sed...\tC-M-d",
	'~',
	XK_d,
	Mod1Mask | ControlMask,
	gettext_noop (" Enter sed arguments (see sed manpage) : "),
	SHELL_OPTION_SAVE_BLOCK | SHELL_OPTION_REQUEST_ARGUMENTS |
	SHELL_OPTION_DELETE_BLOCK | SHELL_OPTION_INSERT_STDOUT |
	SHELL_OPTION_DISPLAY_ERROR_FILE | SHELL_OPTION_CHECK_ERROR_FILE,
	0,
	"#!/bin/sh\n" \
	"cat %b | sed %a 2>%e\n"
    },
    {
	gettext_noop (" Indent "),
	gettext_noop ("'indent' C Formatter\tS-F9"),
	'~',
	XK_F9,
	ShiftMask,
	"",
	SHELL_OPTION_SAVE_BLOCK | SHELL_OPTION_DELETE_BLOCK |
	SHELL_OPTION_DISPLAY_ERROR_FILE | SHELL_OPTION_CHECK_ERROR_FILE |
	SHELL_OPTION_INSERT_BLOCK_FILE,
	0,
	"#!/bin/sh\n" \
	"indent -kr -pcs %b 2>%e\n"
    },
    {
	gettext_noop (" Sort "),
	gettext_noop ("Sort...\tM-t"),
	'~',
	XK_t,
	Mod1Mask,
	gettext_noop (" Enter sort options (see sort manpage) : "),
	SHELL_OPTION_SAVE_BLOCK | SHELL_OPTION_REQUEST_ARGUMENTS |
	SHELL_OPTION_DELETE_BLOCK | SHELL_OPTION_INSERT_STDOUT |
	SHELL_OPTION_DISPLAY_ERROR_FILE | SHELL_OPTION_CHECK_ERROR_FILE,
	0,
	"#!/bin/sh\n" \
	"sort %a %b 2>%e\n"
    },
    {
	" Ispell ",
	gettext_noop ("'ispell' Spell Check\tC-p"),
	'~',
	XK_p,
	ControlMask,
	"",
	SHELL_OPTION_SAVE_BLOCK | SHELL_OPTION_DELETE_BLOCK |
	SHELL_OPTION_INSERT_BLOCK_FILE,
	0,
	"#!/bin/sh\n" \
	XTERM_CMD " -e ispell %b\n"
    },
    {
	" Make ",
	gettext_noop ("Run make in curr. dir\tM-F7"),
	'~',
	XK_F7,
	Mod1Mask,
	"",
	SHELL_OPTION_DISPLAY_STDOUT_CONTINUOUS |
	SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS,
	0,
	"#!/bin/sh\n" \
	"cd %d\n" \
	"make\n" \
	"echo Done\n"
    },
    {
	" Latex ",
	"Latex\tC-M-l",
	'~',
	XK_l,
	Mod1Mask | ControlMask,
	"",
	SHELL_OPTION_DISPLAY_STDOUT_CONTINUOUS |
	SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS,
	0,
	"#!/bin/sh\n" \
	"cd %p\n" \
	"latex %f\n" \
	"echo Done\n"
    },
    {
	" Latex to PS ",
	"Latex to PS\tC-M-p",
	'~',
	XK_p,
	Mod1Mask | ControlMask,
	"",
	SHELL_OPTION_DISPLAY_STDOUT_CONTINUOUS |
	SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS,
	0,
	"#!/bin/sh\n" \
	"cd %p\n" \
	"latex %f\n" \
	"dvips %n.dvi\n" \
	"echo Done\n"
    },
    {
	" Xdvi ",
	"Xdvi\tC-M-d",
	'~',
	XK_d,
	Mod1Mask | ControlMask,
	"",
	SHELL_OPTION_RUN_IN_BACKGROUND,
	0,
	"#!/bin/sh\n" \
	"cd %p\n" \
	"xdvi %n.dvi 2>%e\n"
    },
    {
	" GhostView ",
	"GhostView\tC-M-g",
	'~',
	XK_g,
	Mod1Mask | ControlMask,
	"",
	SHELL_OPTION_RUN_IN_BACKGROUND,
	0,
	"#!/bin/sh\n" \
	"cd %p\n" \
	"ghostview %n.ps 2>%e\n"
    },
    {
	gettext_noop (" Run Application "),
	gettext_noop ("Run Application...\tC-M-a"),
	'~',
	XK_a,
	Mod1Mask | ControlMask,
	" Enter command : ",
	SHELL_OPTION_REQUEST_ARGUMENTS |
	SHELL_OPTION_RUN_IN_BACKGROUND,
	0,
	"#!/bin/sh\n" \
	"%a 2>%e\n"
    },
    {
	gettext_noop (" Terminal Application "),
	gettext_noop ("Run Terminal App...\tC-M-e"),
	'~',
	XK_e,
	Mod1Mask | ControlMask,
	" Enter command : ",
	SHELL_OPTION_REQUEST_ARGUMENTS |
	SHELL_OPTION_RUN_IN_BACKGROUND,
	0,
	"#!/bin/sh\n" \
	XTERM_CMD " -e %a\n"
    },
    {
	" Xterm ",
	"Xterm\tC-M-t",
	'~',
	XK_t,
	Mod1Mask | ControlMask,
	"",
	SHELL_OPTION_RUN_IN_BACKGROUND,
	0,
	"#!/bin/sh\n" \
	XTERM_CMD "\n"
    },
    {
	gettext_noop (" C Run "),
	gettext_noop ("Run this file\tC-M-r"),
	'~',
	XK_r,
	Mod1Mask | ControlMask,
	"",
	SHELL_OPTION_RUN_IN_BACKGROUND,
	0,
	"#!/bin/sh\n" \
	XTERM_CMD " -e %p/%n\n"
    },
    {
	gettext_noop (" C Compile "),
	"Cc\tC-M-c",
	'~',
	XK_c,
	Mod1Mask | ControlMask,
	"",
	SHELL_OPTION_DISPLAY_STDOUT_CONTINUOUS |
	SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS,
	0,
	"#!/bin/sh\n" \
	"cd %p\n" \
	"cc -g -Wall -o %n %f\n" \
	"echo Done\n"
    }
};

char *hme (char *text);
char *substitute_strings (char *text, char *cmdline_options, char *editor_file, char *dnd_major_type, char *dnd_minor_type, char *dnd_data_file);

/* {{{ dynamic display of shell output in a dialog box */

#define MAX_RUNNING_SHELLS 32
static struct running_shell {
    pid_t shell_pid;
    int shell_pipe;
    POOL *shell_pool;
    char *shell_name;
    CWidget *w;
    int killme;
} running_shell[MAX_RUNNING_SHELLS];

static void kill_process (pid_t p)
{
    if (p)
	kill (p, SIGKILL);
}


/* one of these must be non-zero */
static int find_shell (pid_t p, char *name, CWidget * w)
{
    int i;
    for (i = 0; i < MAX_RUNNING_SHELLS; i++) {
	if (p)
	    if (running_shell[i].shell_pid == p)
		return i;
	if (name && running_shell[i].shell_name)
	    if (*name)
		if (!strcmp (name, running_shell[i].shell_name))
		    return i;
	if (w)
	    if ((unsigned long) w == (unsigned long) running_shell[i].w)
		return i;
    }
    return -1;
}

static int new_shell (char *name)
{
    int i;
    i = find_shell (0, name, 0);
    if (i < 0) {
	for (i = 0; i < MAX_RUNNING_SHELLS; i++)
	    if (!running_shell[i].shell_name) {
		memset (&running_shell[i], 0, sizeof (struct running_shell));
		running_shell[i].shell_pipe = -1;
		running_shell[i].shell_name = name;	/* FIXME: strdup() then free() later - static is ok for now */
		return i;
	    }
    }
    return -1;
}

void text_free (void *x)
{
    if (x)
	free (x);
}

static void shell_pool_update (int fd, fd_set * reading, fd_set * writing, fd_set * error, void *data);

char *shell_free_pool (int i)
{
    char *s = 0;
    if (i < 0)
	return 0;
    if (running_shell[i].shell_pool)
	s = (char *) pool_break (running_shell[i].shell_pool);
    if (running_shell[i].shell_pipe >= 0) {
	CRemoveWatch (running_shell[i].shell_pipe , shell_pool_update, WATCH_READING);
	close (running_shell[i].shell_pipe);
    }
    memset (&running_shell[i], 0, sizeof (struct running_shell));
    running_shell[i].shell_pipe = -1;
    return s;
}

/* kills an executing shell whose output is being dynamically displayed */
void set_to_kill (pid_t p)
{
    int i;
    i = find_shell (p, 0, 0);
    if (i >= 0)
	running_shell[i].killme = 1;
}

/* kills an executing shell whose output is being dynamically displayed */
static int kill_shell (pid_t p, char *name, CWidget * w)
{
    int i;
    i = find_shell (p, name, w);
    if (i < 0)
	return -1;
    kill_process (running_shell[i].shell_pid);
    set_to_kill (running_shell[i].shell_pid);
    return i;
}

static int restart_shell (pid_t p, char *name, CWidget * w)
{
    int i;
    i = find_shell (p, name, w);
    if (i < 0)
	return new_shell (name);
    if (running_shell[i].shell_pipe >= 0) {
	CRemoveWatch (running_shell[i].shell_pipe, shell_pool_update, WATCH_READING);
	close (running_shell[i].shell_pipe);
    }
    kill_process (running_shell[i].shell_pid);
    running_shell[i].shell_pid = 0;
    running_shell[i].shell_pipe = -1;
    running_shell[i].killme = 0;
    if (running_shell[i].shell_pool) {
	pool_break (running_shell[i].shell_pool);
	running_shell[i].shell_pool = 0;
    }
    return i;
}

void goto_error (char *message, int raise_wm_window);

/* if you double click on a line of gcc output, this will take you to the file */
static int goto_file_callback (CWidget * w, XEvent * x, CEvent * c)
{
    if (c->double_click || (c->command == CK_Enter && !c->handled)) {
	int width;
	char *q;
	width = w->options & TEXTBOX_WRAP ? (w->width - TEXTBOX_BDR) / FONT_MEAN_WIDTH : 32000;
	q = strline (w->text, strmovelines (w->text, w->current, w->cursor - w->firstline, width));
	goto_error (q, 1);
    }
    return 0;
}

static char *nm (int i, char *a, char *b, char *c)
{
    static char id[36];
    sprintf (id, "%.3d%s", i, a);
    if (b) {
	strcat (id, ".");
	strcat (id, b);
	if (c) {
	    strcat (id, ".");
	    strcat (id, c);
	}
    }
    return id;
}

/* if the dynamic output dialog's tick button is click, then terminate: */
static int display_file_callback (CWidget * w, XEvent * x, CEvent * c)
{
    shell_free_pool (kill_shell (0, 0, CIdent (nm (atoi (w->ident), "shelldisplaytext", "text", 0))));
    CDestroyWidget (nm (atoi (w->ident), "shelldisplaytext", 0, 0));
    return 1;
}

#if 0
    fd_set reading;
    struct timeval tv;
    FD_ZERO (&reading);
    for (i = 0; i < MAX_RUNNING_SHELLS; i++) {
	if (running_shell[i].shell_pipe >= 0 && running_shell[i].shell_name) {
	    FD_SET (running_shell[i].shell_pipe, &reading);
	    if (n < (int) running_shell[i].shell_pipe)
		n = running_shell[i].shell_pipe;
	}
    }
    if (n < 0) {
	CAddCallback ("AlarmCallback", 0);
	return 0;
    }
    tv.tv_sec = 0;
    tv.tv_usec = 0;
#endif


#define CHUNK 8192
extern char per_char[256];

static void shell_pool_update (int fd, fd_set * reading, fd_set * writing, fd_set * error, void *data)
{
    CWidget *w;
    struct running_shell *r;
    int i, count = 0, do_redraw = 0;
    r = &running_shell[i = (int) data];
    if (r->shell_pipe != fd) {
	printf ("huh??\n");
    }
/* first check if we are still on the screen */
    w = CIdent (nm (i, "shelldisplaytext", "text", 0));
    if (!w) {
	shell_free_pool (kill_shell (0, 0, w));
	return;
    }
/* is the buffer initialised? : */
    if (!r->shell_pool)
	r->shell_pool = pool_init ();
/* read a little */
    for (;;) {
	int c, j;
	unsigned char *p;
	if (pool_freespace (r->shell_pool) < CHUNK + 1) {
	    pool_advance (r->shell_pool, CHUNK + 1);
	    do_redraw = 1;	/* must redraw cause pool has changed */
	}
	do {
	    c = read (fd, pool_current (r->shell_pool), CHUNK);
	} while (c < 0 && errno == EINTR);
/* translate unreadables */
	for (j = 0, p = (unsigned char *) pool_current (r->shell_pool); j < c; j++, p++) {
	    if (*p == '\t')
		*p = ' ';
	    else if (*p == '\n')
		*p = '\n';
	    else if (!per_char[*p])
		*p = '?';
	}
	if (c <= 0)
	    break;
	count += c;
	pool_current (r->shell_pool) += c;
	break;
    }
    pool_null (r->shell_pool);	/* adds a zero to the end */
/* do we need to refresh? : */
    if (count || do_redraw) {
/* we do a manual appending of the text for optimization purposes, instead of CRedrawTextbox */
	w->text = (char *) pool_start (r->shell_pool);
	w->numlines += strcountlines (w->text + w->textlength, 0, 2000000000, w->options & TEXTBOX_WRAP ? (w->width - TEXTBOX_BDR) / FONT_MEAN_WIDTH : 32000);
	w->textlength = pool_length (r->shell_pool);
/* only redraw if we crossed a newline */
	if (strchr (w->text + w->textlength - count, '\n')) {
	    CExpose (w->ident);
	    if (w->winid != CGetFocus ())
		if (w->numlines > w->firstline + (w->height / FONT_PIX_PER_LINE - 1))
		    CSetTextboxPos (w, TEXT_SET_LINE, w->numlines - (w->height / FONT_PIX_PER_LINE - 1));
	}
    }
    if (!count && r->killme) {
	pool_break (r->shell_pool);
	r->shell_pool = 0;
	CRemoveWatch (fd, shell_pool_update, WATCH_READING);
	close (fd);
	r->shell_pipe = -1;
    }
}

/* draws a textbox dialog for showing the shells output */
static void shell_display_output (char *text, const char *heading, int (*select_line_callback) (CWidget *, XEvent *, CEvent *), char *name)
{
    int i;
    if (!text)
	text = "";
    i = find_shell (0, name, 0);
    if (i >= 0 && CIdent (nm (i, "shelldisplaytext", 0, 0))) {	/* exists ? */
	CWidget *w;
	w = CIdent (nm (i, "shelldisplaytext", "text", 0));
	free (w->text);
	w->text = 0;
	CRedrawTextbox (nm (i, "shelldisplaytext", "text", 0), text, 0);
	CRedrawText (nm (i, "shelldisplaytext", "header", 0), heading);
	CFocus (CIdent (nm (i, "shelldisplaytext", "text", 0)));
	CRaiseWMWindow (nm (i, "shelldisplaytext", 0, 0));
    } else {
	int x, y;
	Window win;
	CWidget *w;
	win = CDrawMainWindow (nm (i, "shelldisplaytext", 0, 0), heading);
	CGetHintPos (&x, &y);
	running_shell[i].w = w = CDrawTextbox (nm (i, "shelldisplaytext", "text", 0), win, x, y, 80 * FONT_MEAN_WIDTH + 7, 25 * FONT_PIX_PER_LINE + 6, 0, 0, text, TEXTBOX_WRAP | TEXTBOX_NO_STRDUP);
/* Toolhint */
	CSetToolHint (nm (i, "shelldisplaytext", "text", 0), _ ("Double click on file:line type messages to goto the\nfile and line number.  Note that the file will not\nauto-load unless there is a full path in the message."));
	w->position |= POSITION_HEIGHT | POSITION_WIDTH;
	(CIdent (nm (i, "shelldisplaytext", "text", "vsc")))->position |= POSITION_HEIGHT | POSITION_RIGHT;
	CGetHintPos (0, &y);
	(CDrawPixmapButton (nm (i, "shelldisplaytext", "done", 0), win, 0, y, PIXMAP_BUTTON_TICK))->position = POSITION_BOTTOM | POSITION_CENTRE;
/* Toolhint */
	CSetToolHint (nm (i, "shelldisplaytext", "done", 0), _ ("Kill the running script"));
	CCentre (nm (i, "shelldisplaytext", "done", 0));
	CSetSizeHintPos (nm (i, "shelldisplaytext", 0, 0));
	CSetWindowResizable (nm (i, "shelldisplaytext", 0, 0), FONT_MEAN_WIDTH * 15, FONT_PIX_PER_LINE * 15, 1600, 1200);	/* minimum and maximum sizes */
	CMapDialog (nm (i, "shelldisplaytext", 0, 0));
	CAddCallback (nm (i, "shelldisplaytext", "text", 0), select_line_callback);
	CAddCallback (nm (i, "shelldisplaytext", "done", 0), display_file_callback);
	CFocus (CIdent (nm (i, "shelldisplaytext", "done", 0)));
    }
}

/* }}} dynamic display of shell output in a dialog box */

static char *hme_i (char *h, int i)
{
    static char s[MAX_PATH_LEN];
    sprintf (s, "%s-%d", hme (h), i);
    return s;
}

/* returns non-zero on error */
int execute_background_display_output (char *title, char *s, char *name)
{
    char *argv[] =
    {0, 0};
    pid_t p;
    int i;
    i = restart_shell (0, name, 0);
    argv[0] = hme_i (SCRIPT_FILE, i);
    savefile (argv[0], s, strlen (s), 0777);
    if ((p = triple_pipe_open (0, &running_shell[i].shell_pipe, 0, 1, argv[0], argv)) < 0)
	return 1;
    running_shell[i].shell_pid = p;
    if (!running_shell[i].shell_pool) {
	running_shell[i].shell_pool = pool_init ();
	pool_null (running_shell[i].shell_pool);
    }
    shell_display_output ((char *) pool_start (running_shell[i].shell_pool), title, goto_file_callback, name);
    CAddWatch (running_shell[i].shell_pipe, shell_pool_update, WATCH_READING, (void *) i);
    return 0;
}

/*
   Executes the shell in the background. The "Insert File" options
   will be ignored if this is called.
 */
static int execute_background_shell (struct shell_cmd *s, char *script, char *name)
{
    char *argv[] =
    {0, 0};
    pid_t p = 0;
    int i;

    i = restart_shell (0, name, 0);
    argv[0] = hme_i (SCRIPT_FILE, i);
    savefile (argv[0], script, strlen (script), 0777);

    if (s->options & SHELL_OPTION_DISPLAY_STDOUT_CONTINUOUS) {
	if ((p = triple_pipe_open (0, &running_shell[i].shell_pipe, 0,
	   (s->options & SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS) ? 1 : 0,
				   hme_i (SCRIPT_FILE, i), argv)) < 0)
	    return 0;
    } else {
	if (!(s->options & SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS)) {	/* no output desired, just run in background */
	    switch (fork ()) {
	    case 0:{
		    int nulldevice_wr, nulldevice_rd;
		    nulldevice_wr = open ("/dev/null", O_WRONLY);
		    nulldevice_rd = open ("/dev/null", O_RDONLY);
		    close (0);
		    dup (nulldevice_rd);
		    close (1);
		    dup (nulldevice_wr);
		    close (2);
		    dup (nulldevice_wr);
		    set_signal_handlers_to_default ();
		    execlp (argv[0], argv[0], 0);
		    exit (0);	/* should never reach */
		}
	    case -1:
		return 0;
	    default:
		return 1;
	    }
	}
	if ((p = triple_pipe_open (0, 0, &running_shell[i].shell_pipe, 0, hme_i (SCRIPT_FILE, i), argv)) < 0)
	    return 0;
    }
    running_shell[i].shell_pid = p;

    if (!running_shell[i].shell_pool) {
	running_shell[i].shell_pool = pool_init ();
	pool_null (running_shell[i].shell_pool);
    }
    shell_display_output ((char *) pool_start (running_shell[i].shell_pool), catstrs (s->name, _ (" Output "), 0), goto_file_callback, name);
    CAddWatch (running_shell[i].shell_pipe, shell_pool_update, WATCH_READING, (void *) i);
    return 1;
}

char *read_pipe (int fd, int *len);

/*
   Returns shell_output on success, 0 on error. Result must be free'd.
   Unlike the above routine, this blocks waiting for the shell to exit.
 */
static char *execute_foreground_shell (struct shell_cmd *s, char *script)
{
    pid_t p = 0;
    char *argv[] =
    {0, 0};
    int shell_foreground_pipe = -1;
    char *t;

    argv[0] = hme (SCRIPT_FILE);
    savefile (argv[0], script, strlen (script), 0777);
    if (s->options & SHELL_OPTION_INSERT_STDOUT) {
	if ((p = triple_pipe_open (0, &shell_foreground_pipe, 0, (s->options & SHELL_OPTION_INSERT_STDERR) ? 1 : 0, hme (SCRIPT_FILE), argv)) < 0)
	    return 0;
	t = read_pipe (shell_foreground_pipe, 0);
    } else {
	if ((p = triple_pipe_open (0, 0, &shell_foreground_pipe, 0, hme (SCRIPT_FILE), argv)) < 0)
	    return 0;
	t = read_pipe (shell_foreground_pipe, 0);
	if (!(s->options & SHELL_OPTION_INSERT_STDERR)) {
	    if (t)
		free (t);
	    t = (char *) strdup ("");
	}
    }
    if (shell_foreground_pipe >= 0)
	close (shell_foreground_pipe);
    kill_process (p);
    return t;
}

int edit_save_block (WEdit * edit, const char *filename, long start, long finish);

/* This is called from the envokation dialog below */
static int run_shell (WEdit * e, struct shell_cmd *s, char *cmdline_options, char *name)
{
    struct stat st;
    long start_mark, end_mark;
    char *script;
    char *output = 0;
    int i;

    i = find_shell (0, name, 0);
    if (!(s->options & (SHELL_OPTION_DISPLAY_STDOUT_CONTINUOUS |
			SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS)))	/* --> these kill any running script automatically */
	if (running_shell[i].shell_pid) {
	    shell_error_dialog (_ (" Shell script "), _ (" A script is already running "));
	    return -1;
	}
    if (s->options & SHELL_OPTION_SAVE_BLOCK) {
	eval_marks (e, &start_mark, &end_mark);
	edit_save_block (e, hme (BLOCK_FILE), start_mark, end_mark);
    }
    if (s->options & SHELL_OPTION_SAVE_EDITOR_FILE)
	if (!edit_save_file (e, catstrs (e->dir, e->filename, 0)))
	    if (!edit_save_as_cmd (e))
		return -1;

    script = substitute_strings (s->script,
	  cmdline_options, catstrs (e->dir, e->filename, 0), "", "", "");

    if ((s->options & (SHELL_OPTION_DISPLAY_STDOUT_CONTINUOUS |
		       SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS |
		       SHELL_OPTION_RUN_IN_BACKGROUND))) {
	if (!execute_background_shell (s, script, name)) {
	    shell_error_dialog (_ (" Shell script "), get_sys_error (_ (" Error trying to pipe script. ")));
	    return -1;
	}
	if (script)
	    free (script);
	return 0;
    } else {
	CHourGlass (main_window);
	output = execute_foreground_shell (s, script);
	CUnHourGlass (main_window);
	if (!output) {
	    shell_error_dialog (_ (" Shell script "), get_sys_error (_ (" Error trying to pipe script. ")));
	    if (script)
		free (script);
	    return -1;
	}
    }
    if (script)
	free (script);
    script = 0;

    if (s->options & SHELL_OPTION_CHECK_ERROR_FILE) {
	if (stat (hme (ERROR_FILE), &st) == 0) {
	    if (st.st_size) {
		char *error;
		if (s->options & SHELL_OPTION_DISPLAY_ERROR_FILE) {
		    error = loadfile (hme (ERROR_FILE), 0);
		    if (error) {
			CTextboxMessageDialog (main_window, 20, 20, 80, 20, catstrs (s->name, _ (" Error "), 0), error, 0);
			free (error);
		    }
		}
		return -1;
	    }
	}
    }
    if (s->options & SHELL_OPTION_DELETE_BLOCK)
	if (edit_block_delete_cmd (e))
	    return 1;
    if (output)
	if (*output)
	    for (i = strlen (output) - 1; i >= 0; i--)
		edit_insert_ahead (e, output[i]);
    if (output)
	free (output);

    if (s->options & SHELL_OPTION_INSERT_TEMP_FILE)
	if (!edit_insert_file (e, hme (TEMP_FILE)))
	    shell_error_dialog (_ (" Shell script "), get_sys_error (_ (" Error trying to insert temp file. ")));

    if (s->options & SHELL_OPTION_INSERT_BLOCK_FILE)
	if (!edit_insert_file (e, hme (BLOCK_FILE)))
	    shell_error_dialog (_ (" Shell script "), get_sys_error (_ (" Error trying to insert block file. ")));

    if (s->options & SHELL_OPTION_INSERT_CLIP_FILE)
	if (!edit_insert_file (e, hme (CLIP_FILE)))
	    shell_error_dialog (_ (" Shell script "), get_sys_error (_ (" Error trying to insert clip file. ")));

    if (s->options & SHELL_OPTION_DISPLAY_ERROR_FILE)
	if (stat (hme (ERROR_FILE), &st) == 0)
	    if (st.st_size) {	/* no error messages */
		char *error;
		error = loadfile (hme (ERROR_FILE), 0);
		if (error) {
		    CTextboxMessageDialog (main_window, 20, 20, 80, 25, s->name, error, 0);
		    free (error);
		}
	    }
    return 0;
}

int edit_execute_cmd (WEdit * edit, int command, int char_for_insertion);

/*
   Main entry point. Request args if option is set and calls run_shell.
   Returns 0 on success, -1 on error and 1 on cancel.
 */
static int run_shell_dialog (WEdit * e, struct shell_cmd *s)
{
    char *cmdline_options;
    int r;
    long start_mark, end_mark;
    if (!s)
	return -1;
    if (s->options & SHELL_OPTION_SAVE_BLOCK)
	if (eval_marks (e, &start_mark, &end_mark)) {
	    shell_error_dialog (_ (" Shell Script "), _ (" Script requires some text to be highlighted. "));
	    return 1;
	}
    if (s->options & SHELL_OPTION_REQUEST_ARGUMENTS) {
	char *p, *q;
	p = (char *) strdup (s->name);	/* create a name for the input dialog by stripping spaces */
	for (q = p; *q; q++)
	    if (*q == ' ')
		memmove (q, q + 1, strlen (q));
	if (strlen (p) > 20)
	    p[20] = 0;
	cmdline_options = CInputDialog (p, 0, 0, 0, 40 * FONT_MEAN_WIDTH, s->last_options ? s->last_options : "", s->name, s->prompt);
	free (p);
	if (!cmdline_options)
	    return 1;
	if (s->last_options)
	    free (s->last_options);
	s->last_options = cmdline_options;
    } else {
	cmdline_options = "";
    }
    r = run_shell (e, s, cmdline_options, s->menu);
    CRefreshEditor (e);
    return r;
}

static struct shell_cmd *scripts[MAX_NUM_SCRIPTS] =
{0, 0, 0, 0, 0, 0, 0, 0,
 0, 0, 0, 0, 0, 0, 0, 0,
 0, 0, 0, 0, 0, 0, 0, 0,
 0, 0, 0, 0, 0, 0, 0, 0,
 0, 0, 0, 0, 0, 0, 0, 0,
 0, 0, 0, 0, 0, 0, 0, 0,
 0, 0, 0, 0, 0, 0, 0, 0,
 0, 0, 0, 0, 0, 0, 0, 0};

/* loads from options file */
void load_scripts ()
{
    char *s = 0, *p, *q;
    unsigned char *r;
    int i = 0, n;

    memset (&running_shell, 0, sizeof (running_shell));

    s = get_options_section (editor_options_file, "[Shell Scripts]");
    if (!s) {
	goto load_default_scripts;
    } else {
	if (!*s)
	    goto load_default_scripts;
    }

    p = q = s;
    for (i = 0; i < MAX_NUM_SCRIPTS; i++) {
	if (!*q || *q == '\n')
	    break;
	q = strchr (p, '\n');
	if (!q)
	    break;
	*q++ = 0;
	scripts[i] = CMalloc (sizeof (struct shell_cmd));
	memset (scripts[i], 0, sizeof (struct shell_cmd));
	strncpy (scripts[i]->name, p, 39);
	p = q;

	q = strchr (p, '\n');
	*q++ = 0;
	strncpy (scripts[i]->menu, p, 39);
	p = q;

	q = strchr (p, '\n');
	*q++ = 0;
	scripts[i]->menu_hot_key = *p;
	p = q;

	q = strchr (p, '\n');
	*q++ = 0;
	scripts[i]->key = (KeySym) atoi (p);
	p = q;

	q = strchr (p, '\n');
	*q++ = 0;
	scripts[i]->keyboard_state = atoi (p);
	p = q;

	q = strchr (p, '\n');
	*q++ = 0;
	strncpy (scripts[i]->prompt, p, 159);
	p = q;

	q = strchr (p, '\n');
	*q++ = 0;
	scripts[i]->options = atoi (p);
	p = q;

	q = strchr (p, '\n');
	*q++ = 0;
	scripts[i]->last_options = (char *) strdup (p);
	p = q;

	q = strchr (p, '\n');
	*q++ = 0;
	scripts[i]->script = (char *) strdup (p);
	for (r = (unsigned char *) scripts[i]->script; *r; r++)
	    if (*r == 129)
		*r = '\n';
	p = q;
    }

  load_default_scripts:

    n = sizeof (default_scripts) / sizeof (struct shell_cmd);

    if (i < n) {
	for (; i < n; i++) {
	    scripts[i] = CMalloc (sizeof (struct shell_cmd));
	    memset (scripts[i], 0, sizeof (struct shell_cmd));
	    memcpy (scripts[i], &default_scripts[i], sizeof (struct shell_cmd));
	    scripts[i]->script = (char *) strdup (default_scripts[i].script);
	}
    }
    if (s)
	free (s);
}

/* saves to options file */
static void save_scripts (void)
{
    char *s, *p;
    int i = 0, n;

    p = s = CMalloc (65536);	/* make longer if overwrites */
    while (scripts[i]) {
	unsigned char *t, *r;
	t = (unsigned char *) strdup (scripts[i]->script);
	for (r = t; *r; r++)
	    if (*r == '\n')
		*r = 129;	/* replace newlines with 129 */
	sprintf (p, "%s\n%s\n%c\n%d\n%d\n%s\n%d\n%s\n%s\n%n",
		 scripts[i]->name ? scripts[i]->name : 0,
		 scripts[i]->menu ? scripts[i]->menu : 0,
	       scripts[i]->menu_hot_key ? scripts[i]->menu_hot_key : ' ',
		 (int) scripts[i]->key,
		 (int) scripts[i]->keyboard_state,
		 scripts[i]->prompt ? scripts[i]->prompt : "",
		 (int) scripts[i]->options,
		 scripts[i]->last_options ? scripts[i]->last_options : "",
		 t, &n);
	free (t);
	p += n;
	i++;
    }
    *p++ = '\n';
    *p = 0;
    save_options_section (editor_options_file, "[Shell Scripts]", s);
    free (s);
}


#define N_ITEMS 4

extern CWidget *edit[];
extern int current_edit;

/* straight from the menu */
static void script_menu_callback (unsigned long ignored)
{
    int i;
    i = (CIdent ("menu.scripts"))->current - N_ITEMS;
    run_shell_dialog (edit[current_edit]->editor, scripts[i]);
}

/* this is called from edit_translate_key.c */
int get_script_number_from_key (unsigned int state, KeySym keysym)
{
    int i;
    for (i = 0; i < MAX_NUM_SCRIPTS; i++) {
	if (!scripts[i])
	    break;
	if (keysym < 256)
	    keysym = my_lower_case (keysym);
	if (scripts[i]->keyboard_state == state && scripts[i]->key == keysym)
	    return i;
    }
    return -1;
}

/* This is called from the editor: see main.c: edit_set_user_command (execute_script); */
void execute_script (WEdit * e, int i)
{
    run_shell_dialog (e, scripts[i]);
}

/* Updates updates the menu when a new shell has been added or removed */
void update_script_menu_items ()
{
    int i, n;
    n = (CIdent ("menu.scripts"))->numlines + 2;
    for (i = N_ITEMS; i < n; i++)
	CRemoveMenuItemNumber ("menu.scripts", N_ITEMS);

    for (n = 0; n < MAX_NUM_SCRIPTS; n++) {
	if (!scripts[n])
	    break;
	CAddMenuItem ("menu.scripts", scripts[n]->menu, scripts[n]->menu_hot_key, script_menu_callback, 0);
    }
}

#define NUM_OPTS 14

/* Edits a scripts: returns 1 on cancel, 0 on success, -1 on error */
int edit_scripts_dialog (Window parent, int x, int y, struct shell_cmd *s)
{
    int i, r = 0;
    CState state;
    struct {
	char *name;
	char *toolhint;
	int flag;
	CWidget *w;
    } options[] =
    {
	{
	    gettext_noop ("Save block on commence"), gettext_noop ("Save the current highlighted text to %b before executing"), SHELL_OPTION_SAVE_BLOCK, 0
	},
	{
	    gettext_noop ("Save editor file on commence"), gettext_noop ("Save the entire edit buffer to %f before executing"), SHELL_OPTION_SAVE_EDITOR_FILE, 0
	},
	{
	    gettext_noop ("Prompt for arguments on commence"), gettext_noop ("Prompt the user for a string to be replaced with %a"), SHELL_OPTION_REQUEST_ARGUMENTS, 0
	},
	{
	    gettext_noop ("Display script's stdout continuously"), gettext_noop ("Dynamically view the scripts output"), SHELL_OPTION_DISPLAY_STDOUT_CONTINUOUS, 0
	},
	{
	    gettext_noop ("Display script's stderr continuously"), gettext_noop ("Dynamically view the scripts output"), SHELL_OPTION_DISPLAY_STDERR_CONTINUOUS, 0
	},
	{
	    gettext_noop ("Run in background"), "", SHELL_OPTION_RUN_IN_BACKGROUND, 0
	},
	{
	    gettext_noop ("Delete block on commence"), gettext_noop ("Use when you want to replace highlighted text"), SHELL_OPTION_DELETE_BLOCK, 0
	},
	{
	    gettext_noop ("Insert temp file on completion"), gettext_noop ("Types out file %t's contents on completion of the script"), SHELL_OPTION_INSERT_TEMP_FILE, 0
	},
	{
	    gettext_noop ("Insert block file on completion"), gettext_noop ("Types out file %b's contents on completion of the script"), SHELL_OPTION_INSERT_BLOCK_FILE, 0
	},
	{
	    gettext_noop ("Insert clip file on completion"), gettext_noop ("Types out file %c's contents on completion of the script"), SHELL_OPTION_INSERT_CLIP_FILE, 0
	},
	{
	    gettext_noop ("Insert stdout on completion"), gettext_noop ("Types out the script's output"), SHELL_OPTION_INSERT_STDOUT, 0
	},
	{
	    gettext_noop ("Insert stderr on completion"), gettext_noop ("Types out the script's output"), SHELL_OPTION_INSERT_STDERR, 0
	},
	{
	    gettext_noop ("Display error file"), gettext_noop ("Displays %e on completion of the script"), SHELL_OPTION_DISPLAY_ERROR_FILE, 0
	},
	{
	    gettext_noop ("Subs only if error file is empty"), gettext_noop ("Only replace the highlighted text if %e is empty"), SHELL_OPTION_CHECK_ERROR_FILE, 0
	}
    };
    int xs, ys, x2, o, yu;
    char hot[2] = "\0\0";
    Window win;
    CWidget *w;
    CEvent cw;

    hot[0] = s->menu_hot_key;

    CBackupState (&state);
    CDisable ("*");

    win = CDrawHeadedDialog ("shellopt", parent, x, y, " Script Control ");
    CGetHintPos (&x, &y);
    yu = y;
    CDrawText ("shellopt.namet", win, x, y, " Name : ");
    CGetHintPos (&x2, 0);
    CDrawTextInput ("shellopt.name", win, x2, y, FONT_MEAN_WIDTH * 27, AUTO_HEIGHT, 39, s->name);
/* Toolhint */
    CSetToolHint ("shellopt.namet", _ ("Name to appear in the listbox"));
    CSetToolHint ("shellopt.name", _ ("Name to appear in the listbox"));

    CGetHintPos (0, &y);
    CDrawText ("shellopt.menut", win, x, y, " Menu item : ");
    CGetHintPos (&x2, 0);
    CDrawTextInput ("shellopt.menu", win, x2, y, FONT_MEAN_WIDTH * 27, AUTO_HEIGHT, 39, s->menu);
/* Toolhint */
    CSetToolHint ("shellopt.menut", _ ("Menu item entry with hot-key"));
    CSetToolHint ("shellopt.menu", _ ("Menu item entry with hot-key"));
    CGetHintPos (0, &y);
    CDrawText ("shellopt.hott", win, x, y, " Menu hotkey : ");
    CGetHintPos (&x2, 0);
    CDrawTextInput ("shellopt.hot", win, x2, y, FONT_MEAN_WIDTH * 10, AUTO_HEIGHT, 3, hot);
/* Toolhint */
    CSetToolHint ("shellopt.hott", _ ("Letter to underline"));
    CSetToolHint ("shellopt.hot", _ ("Letter to underline"));
    CGetHintPos (0, &y);
    CDrawText ("shellopt.promptt", win, x, y, " Argument prompt : ");
    CGetHintPos (&x2, 0);
    CDrawTextInput ("shellopt.prompt", win, x2, y, FONT_MEAN_WIDTH * 24, AUTO_HEIGHT, 159, s->prompt);
/* Toolhint */
    CSetToolHint ("shellopt.promptt", _ ("Message for %a prompting"));
    CSetToolHint ("shellopt.prompt", _ ("Message for %a prompting"));
    CGetHintPos (0, &y);
    xs = x;
    ys = y;
    o = (32 - (FONT_PIX_PER_LINE + 2 + TEXT_RELIEF * 2)) / 2;
    for (i = 0; i < NUM_OPTS / 2; i++) {
	options[i].w = CDrawSwitch (catstrs ("shellopt.", itoa (i), 0), win, x, y, (s->options & options[i].flag) ? 1 : 0, _ (options[i].name), 0);
	CSetToolHint (options[i].w->ident, _ (options[i].toolhint));
	CSetToolHint (catstrs (options[i].w->ident, ".label", 0), _ (options[i].toolhint));
	CGetHintPos (0, &y);
    }
    x = xs + CStringWidth (_ (options[3].name)) + 32 + WIDGET_SPACING * 2 + 10;
    y = ys;
    for (i = NUM_OPTS / 2; i < NUM_OPTS; i++) {
	options[i].w = CDrawSwitch (catstrs ("shellopt.", itoa (i), 0), win, x, y, (s->options & options[i].flag) ? 1 : 0, _ (options[i].name), 0);
	CSetToolHint (options[i].w->ident, _ (options[i].toolhint));
	CSetToolHint (catstrs (options[i].w->ident, ".label", 0), _ (options[i].toolhint));
	CGetHintPos (0, &y);
    }
    w = CDrawEditor ("shellopt.edit", win, xs, y,
		     77 * FONT_MEAN_WIDTH, 10 * FONT_PIX_PER_LINE, s->script, 0, 0, EDITOR_NO_TEXT | EDITOR_NO_FILE, strlen (s->script));
/* Toolhint */
    CSetToolHint ("shellopt.edit", _ ("Enter your shell script here"));

    get_hint_limits (&x, &y);
    x -= WIDGET_SPACING * 2 + TICK_BUTTON_WIDTH * 2;
    CDrawPixmapButton ("shellopt.ok", win, x, yu, PIXMAP_BUTTON_TICK);
/* Toolhint */
    CSetToolHint ("shellopt.ok", _ ("Press to accept. You will then be\nprompted for the hot-key combination"));
    CGetHintPos (&x, 0);
    CDrawPixmapButton ("shellopt.cancel", win, x, yu, PIXMAP_BUTTON_CROSS);
/* Toolhint */
    CSetToolHint ("shellopt.cancel", _ ("Abort operation"));
    CSetSizeHintPos ("shellopt");
    CFocus (CIdent ("shellopt.name"));
    CMapDialog ("shellopt");

    while (1) {
	CNextEvent (0, &cw);
	if (!CIdent ("shellopt"))	/* destroyed by WM close */
	    break;
	if (!strcmp (cw.ident, "shellopt.cancel")) {
	    r = 1;
	    break;
	}
	if (!strcmp (cw.ident, "shellopt.ok")) {
	    XEvent *p;
	    p = CRawkeyQuery ((CIdent ("shellopt.ok"))->mainid, 20, 20, _ (" Script Edit "), _ (" Press the key combination to envoke the script : "));
	    if (!p)
		continue;
	    s->key = CKeySym (p);
	    s->keyboard_state = p->xkey.state;
	    s->options = 0;
	    for (i = 0; i < NUM_OPTS; i++)
		if (options[i].w->keypressed)
		    s->options |= options[i].flag;
	    strncpy (s->name, (CIdent ("shellopt.name"))->text, 39);
	    strncpy (s->menu, (CIdent ("shellopt.menu"))->text, 39);
	    strncpy (s->prompt, (CIdent ("shellopt.prompt"))->text, 159);
	    s->menu_hot_key = (CIdent ("shellopt.hot"))->text[0];
	    if (s->script)
		free (s->script);
	    s->script = edit_get_buffer_as_text (w->editor);
	    break;
	}
    }
    CDestroyWidget ("shellopt");
    CRestoreState (&state);
    return r;
}

int edit_script (Window parent, int x, int y, int which_script)
{
    return edit_scripts_dialog (parent, x, y, scripts[which_script]);
}

static char *get_a_line (void *data, int line)
{
    static char t[128];
    struct shell_cmd **s;
    s = data;
    strcpy (t, s[line]->name);
    return t;
}

int script_list_box_dialog (Window parent, int x, int y, char *heading)
{
    int n;

    for (n = 0; n < MAX_NUM_SCRIPTS; n++)
	if (!scripts[n])
	    break;

    return CListboxDialog (parent, x, y, 30, 10, heading, 0, 0, n, get_a_line, scripts);
}

/* straight from menu */
void edit_a_script_cmd (unsigned long ignored)
{
    int i;
    i = script_list_box_dialog (main_window, 40, 40, _ (" Pick a Script to Edit "));
    if (i >= 0) {
	if (!edit_script (CRoot, 20, 20, i))
	    save_scripts ();
	update_script_menu_items ();
    }
}

void delete_script (int i)
{
    if (scripts[i]) {
	if (scripts[i]->script)
	    free (scripts[i]->script);
	if (scripts[i]->last_options)
	    free (scripts[i]->last_options);
	free (scripts[i]);
	scripts[i] = 0;
	memmove (scripts + i, scripts + i + 1, (MAX_NUM_SCRIPTS - i - 1) * sizeof (struct shell_cmd *));
    }
}

/* called on application shutdown */
void free_all_scripts (void)
{
    while (scripts[0])
	delete_script (0);
}

/* straight from menu */
void delete_a_script_cmd (unsigned long ignored)
{
    int i;
    i = script_list_box_dialog (main_window, 20, 20, _ (" Pick a Script to Delete "));
    if (i >= 0) {
	delete_script (i);
	save_scripts ();
	update_script_menu_items ();
    }
}

/* straight from menu */
void new_script_cmd (unsigned long ignored)
{
    int n;
    for (n = 0; n < MAX_NUM_SCRIPTS; n++)
	if (!scripts[n])
	    break;
    if (n > MAX_NUM_SCRIPTS - 2) {
	CErrorDialog (0, 0, 0, _ (" New Script "), \
		      _ (" Max number of scripts has been reached, \n" \
		      " increase MAX_NUM_SCRIPTS in the file shell.h "));
	return;
    }
    scripts[n] = CMalloc (sizeof (struct shell_cmd));
    memset (scripts[n], 0, sizeof (struct shell_cmd));
    scripts[n]->script = (char *) strdup ("#!/bin/sh\n");
    if (edit_script (main_window, 20, 20, n)) {
	free (scripts[n]);
	scripts[n] = 0;
    } else
	save_scripts ();
    update_script_menu_items ();
}

