/*	INPUT:	Various input routines for vile
 *		written by Daniel Lawrence	5/9/86
 *		variously munged/massaged/relayered/slashed/burned
 *			since then. -pgf
 *
 *	term.getch()	raw 8-bit key from terminal driver.
 *
 *	sysmapped_c()	single "keystroke" -- may have SPEC bit, if it was
 *			a sytem-mapped function key.  calls term.getch().  these
 *			system-mapped keys will never map to a multi-char
 *			sequence.  the routine does have storage, to hold
 *			keystrokes gathered "in error".
 *
 *	tgetc()		fresh, pushed back, or recorded output of result of
 *			sysmapped_c() (i.e. dotcmd and the keyboard macros
 *			are recordedand played back at this level).  this is
 *			only called from mapgetc() in map.c
 *
 *	mapped_c()	(map.c) worker routine which will return a mapped
 *			or non-mapped character from the mapping engine.
 *			determines correct map, and uses its own pushback
 *			buffers on top of calls to tgetc() (see mapgetc/
 *			mapungetc).
 *
 *	mapped_keystroke() applies user-specified maps to user's input.
 *			correct map used depending on mode (insert, command,
 *			message-line).
 *
 *	keystroke()	returns pushback from mappings, the results of
 *			previous calls to mapped_keystroke().
 *
 *	keystroke8()	as above, but masks off any "wideness", i.e. SPEC bits.
 *
 *	keystroke_raw() as above, but recording is forced even if
 *			sysmapped_c() returns intrc. (old "tgetc(TRUE)")
 *
 *	kbd_seq()	the vile prefix keys (^X,^A,#) are checked for, and
 *			appropriate key pairs are turned into CTLA|c, CTLX|c,
 *			SPEC|c.
 *
 *
 *	term.typahead()	  true if a key is avail from term.getch().
 *	sysmapped_c_avail() "  if a key is avail from sysmapped_c() or below.
 *	tgetc_avail()     true if a key is avail from tgetc() or below.
 *	keystroke_avail() true if a key is avail from keystroke() or below.
 *
 * $Header: /usr/build/vile/vile/RCS/input.c,v 1.261 2003/07/27 19:06:52 tom Exp $
 *
 */

#include	"estruct.h"
#include	"edef.h"
#include	"nefunc.h"

#define	DEFAULT_REG	-1

#define INFINITE_LOOP_COUNT 1200

typedef struct _kstack {
    struct _kstack *m_link;
    int m_save;			/* old value of 'kbdmacactive'          */
    int m_indx;			/* index identifying this macro         */
    int m_rept;			/* the number of times to execute the macro */
    ITBUFF *m_kbdm;		/* the macro-text to execute            */
    ITBUFF *m_dots;		/* workspace for "." command            */
} KSTACK;

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

/*
 * FIXME:
 * Special hacks to convert null-terminated string to/from TBUFF, for interface
 * with functions that still expect these.
 */
#define StrToBuff(buf) (buf)->tb_used = strlen(tb_values(buf))
#define BuffToStr(buf) tb_values(buf)[tb_length(buf)] = EOS

/*--------------------------------------------------------------------------*/
static void finish_kbm(void);

static int kbdmacactive;	/* current mode */
static size_t kbdmaclength;	/* effective recording length */
static KSTACK *KbdStack;	/* keyboard/@-macros that are replaying */
static ITBUFF *KbdMacro;	/* keyboard macro, recorded     */
static int last_eolchar;	/* records last eolchar-match in 'kbd_string' */

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

/*
 * Returns a pointer to the buffer that we use for saving text to replay with
 * the "." command.
 */
static ITBUFF *
TempDot(int init)
{
    static ITBUFF *tmpcmd;	/* dot commands, 'til we're sure */

    if (kbdmacactive == PLAY) {
	if (init)
	    (void) itb_init(&(KbdStack->m_dots), esc_c);
	return KbdStack->m_dots;
    }
    if (init || (tmpcmd == 0))
	(void) itb_init(&tmpcmd, esc_c);
    return tmpcmd;
}

/*
 * Dummy function to use when 'kbd_string()' does not handle automatic completion
 */
/*ARGSUSED*/
int
no_completion(DONE_ARGS)
{
    (void) flags;
    (void) c;
    (void) buf;
    (void) pos;
    return FALSE;
}

/*
 * Complete names in a shell command using the filename-completion.  We'll have
 * to scan back to the beginning of the appropriate name since that module
 * expects only one name in a buffer.
 *
 * We only do shell-completion if filename-completion is configured.
 */
#if COMPLETE_FILES
static int doing_shell;
int
shell_complete(DONE_ARGS)
{
    int status;
    unsigned len = *pos;
    int base;
    int first = 0;

    TRACE(("shell_complete %d:'%s'\n", *pos, buf));
    if (isShellOrPipe(buf))
	first++;

    for (base = len; base > first;) {
	base--;
	if (isSpace(buf[base]) && (first || doing_shell)) {
	    base++;
	    break;
	} else if (buf[base] == '$') {
	    break;
	}
    }
    len -= base;
    status = path_completion(flags, c, buf + base, &len);
    *pos = len + base;

    return status;
}
#endif

/*
 * Ask a yes or no question in the message line. Return either TRUE, FALSE, or
 * ABORT. The ABORT status is returned if the user bumps out of the question
 * with an esc_c. Used any time a confirmation is required.
 */
int
mlyesno(const char *prompt)
{
    int result = ABORT;
    char c;			/* input character */

    /* in case this is right after a shell escape */
    if (update(TRUE)) {
	reading_msg_line = TRUE;
	for_ever {
	    mlforce("%s [y/n]? ", prompt);
	    c = (char) keystroke();	/* get the response */

	    if (ABORTED(c)) {	/* Bail out! */
		break;
	    }

	    if (c == 'y' || c == 'Y') {
		result = TRUE;
		break;
	    }

	    if (c == 'n' || c == 'N') {
		result = FALSE;
		break;
	    }
	}
	reading_msg_line = FALSE;
    }
    return result;
}

/*
 * Ask a simple question in the message line.  Return the single char response
 * if it was one of the valid responses.
 */
int
mlquickask(const char *prompt, const char *respchars, int *cp)
{
    int result = ABORT;

    if (update(TRUE)) {
	reading_msg_line = TRUE;
	for_ever {
	    mlforce("%s ", prompt);
	    *cp = keystroke();	/* get the response */

	    if (ABORTED(*cp)) {	/* Bail out! */
		break;
	    }

	    if (strchr(respchars, *cp)) {
		result = TRUE;
		break;
	    }

	    kbd_alarm();
	}
	reading_msg_line = FALSE;
    }
    return result;
}

/*
 * Prompt for a named-buffer (i.e., "register")
 */
int
mlreply_reg(const char *prompt,
	    char *cbuf,		/* 2-char buffer for register+eol */
	    int *retp,		/* => the register-name */
	    int at_dft)		/* default-value (e.g., for "@@" command) */
{
    register int status;
    register int c;

    if (clexec || isnamedcmd) {
	if ((status = mlreply(prompt, cbuf, 2)) != TRUE)
	    return status;
	c = cbuf[0];
    } else {
	c = keystroke();
	if (ABORTED(c))
	    return ABORT;
    }

    if (c == '@' && at_dft != -1) {
	c = at_dft;
    } else if (reg2index(c) < 0) {
	mlwarn("[Invalid register name]");
	return FALSE;
    }

    *retp = c;
    return TRUE;
}

/*
 * Prompt for a register-name and/or line-count (e.g., for the ":yank" and
 * ":put" commands).  The register-name, if given, is first.
 */
int
mlreply_reg_count(int state,	/* negative=register, positive=count, zero=either */
		  int *retp,	/* returns the register-index or line-count */
		  int *next)	/* returns 0/1=register, 2=count */
{
    register int status;
    char prompt[80];
    char expect[80];
    char buffer[10];
    UINT length;

    *expect = EOS;
    if (state <= 0)
	(void) strcat(expect, " register");
    if (state == 0)
	(void) strcat(expect, " or");
    if (state >= 0) {
	(void) strcat(expect, " line-count");
	length = sizeof(buffer);
    } else
	length = 2;

    (void) lsprintf(prompt, "Specify%s: ", expect);
    *buffer = EOS;
    status = kbd_string(prompt, buffer, length, ' ', 0, no_completion);

    if (status == TRUE) {
	if (state <= 0
	    && isAlpha(buffer[0])
	    && buffer[1] == EOS
	    && (*retp = reg2index(*buffer)) >= 0) {
	    *next = isUpper(*buffer) ? 1 : 0;
	} else if (state >= 0
		   && string_to_number(buffer, retp)
		   && *retp) {
	    *next = 2;
	} else {
	    mlforce("[Expected%s]", expect);
	    kbd_alarm();
	    status = ABORT;
	}
    }
    return status;
}

/*
 * Write a prompt into the message line, then read back a response.  Keep track
 * of the physical position of the cursor.  If we are in a keyboard macro throw
 * the prompt away, and return the remembered response.  This lets macros run
 * at full speed.  The reply is always terminated by a carriage return.  Handle
 * erase, kill, and abort keys.
 */
int
mlreply(const char *prompt, char *buf, UINT bufn)
{
    return kbd_string(prompt, buf, bufn, '\n', KBD_NORMAL, no_completion);
}

int
mlreply2(const char *prompt, TBUFF ** buf)
{
    return kbd_string2(prompt, buf, '\n', KBD_NORMAL, no_completion);
}

/* as above, but don't do anything to backslashes */
int
mlreply_no_bs(const char *prompt, char *buf, UINT bufn)
{
    int s;
#if COMPLETE_FILES
    doing_shell = TRUE;
    init_filec(FILECOMPLETION_BufName);
#endif
    s = kbd_string(prompt, buf, bufn, '\n', KBD_EXPAND | KBD_SHPIPE, shell_complete);
#if COMPLETE_FILES
    doing_shell = FALSE;
#endif
    return s;
}

/* as above, but neither expand nor do anything to backslashes */
int
mlreply_no_opts(const char *prompt, char *buf, UINT bufn)
{
    return kbd_string(prompt, buf, bufn, '\n', 0, no_completion);
}

/* the numbered buffer names increment each time they are referenced */
void
incr_dot_kregnum(void)
{
    if (dotcmdactive == PLAY) {
	register int c = itb_peek(dotcmd);
	if (isDigit(c) && c < '9')
	    itb_stuff(dotcmd, ++c);
    }
}

/*
 * Record a character for "." commands
 */
static void
record_dot_char(int c)
{
    if (dotcmdactive == RECORD) {
	ITBUFF *tmp = TempDot(FALSE);
	(void) itb_append(&tmp, c);
    }
}

/*
 * Record a character for kbd-macros
 */
static void
record_kbd_char(int c)
{
    if (dotcmdactive != PLAY && kbdmacactive == RECORD)
	(void) itb_append(&KbdMacro, c);
}

/* if we should preserve this input, do so */
static void
record_char(int c)
{
    record_dot_char(c);
    record_kbd_char(c);
}

/* get the next character of a replayed '.' or macro */
int
get_recorded_char(int eatit)	/* consume the character? */
{
    register int c = -1;
    register ITBUFF *buffer;

    if (dotcmdactive == PLAY) {

	if (interrupted()) {
	    dotcmdactive = 0;
	    return intrc;
	} else {

	    if (!itb_more(buffer = dotcmd)) {
		if (!eatit) {
		    if (dotcmdrep > 1)
			return itb_get(buffer, 0);
		} else {	/* at the end of last repetition?  */
		    if (--dotcmdrep < 1) {
			dotcmdactive = 0;
			(void) dotcmdbegin();
			/* immediately start recording
			 * again, just in case.
			 */
		    } else {
			/* reset the macro to the
			 * beginning for the next rep.
			 */
			itb_first(buffer);
		    }
		}
	    }

	    /* if there is some left... */
	    if (itb_more(buffer)) {
		if (eatit)
		    c = itb_next(buffer);
		else
		    c = itb_peek(buffer);
		return c;
	    }
	}
    }

    if (kbdmacactive == PLAY) {

	if (interrupted()) {
	    while (kbdmacactive == PLAY)
		finish_kbm();
	    return intrc;
	} else {

	    if (!itb_more(buffer = KbdStack->m_kbdm)) {
		if (--(KbdStack->m_rept) >= 1)
		    itb_first(buffer);
		else
		    finish_kbm();
	    }

	    if (kbdmacactive == PLAY) {
		buffer = KbdStack->m_kbdm;
		if (eatit)
		    record_dot_char(c = itb_next(buffer));
		else
		    c = itb_peek(buffer);
	    }
	}
    }

    return c;
}

void
unkeystroke(int c)
{
    mapungetc(c | NOREMAP);
}

int
mapped_keystroke(void)
{
    return lastkey = mapped_c(DOMAP, MAP_UNQUOTED);
}

int
keystroke(void)
{
    return lastkey = mapped_c(NODOMAP, MAP_UNQUOTED);
}

int
keystroke8(void)
{
    int c;
    for_ever {
	c = mapped_c(NODOMAP, MAP_UNQUOTED);
	if ((c & ~0xff) == 0)
	    return lastkey = c;
	kbd_alarm();
    }
}

int
keystroke_raw8(void)
{
    int c;
    for_ever {
	c = mapped_c(NODOMAP, MAP_QUOTED);
	if ((c & ~0xff) == 0)
	    return lastkey = c;
	kbd_alarm();
    }
}

static int
mapped_keystroke_raw(void)
{
    return lastkey = mapped_c(DOMAP, MAP_QUOTED);
}

int
keystroke_avail(void)
{
    return mapped_c_avail();
}

static ITBUFF *tgetc_ungottenchars = NULL;
static int tgetc_ungotcnt = 0;

void
tungetc(int c)
{
    (void) itb_append(&tgetc_ungottenchars, c);
    tgetc_ungotcnt++;
}

int
tgetc_avail(void)
{
    return tgetc_ungotcnt > 0 ||
	get_recorded_char(FALSE) != -1 ||
	sysmapped_c_avail();
}

int
tgetc(int quoted)
{
    register int c;		/* character read from terminal */

    if (tgetc_ungotcnt > 0) {
	tgetc_ungotcnt--;
	c = itb_last(tgetc_ungottenchars);
    } else if ((c = get_recorded_char(TRUE)) == -1) {
	/* read a character from the terminal */
	not_interrupted();
	if (setjmp(read_jmp_buf)) {
	    c = kcod2key(intrc);
	    TRACE(("setjmp/getc:%c (%#x)\n", c, c));
#if defined(BUG_LINUX_2_0_INTR) && DISP_TERMCAP
	    /*
	     * Linux bug (observed with kernels 1.2.13 & 2.0.0):
	     * when interrupted, the _next_ character that
	     * getchar() returns will be the same as the last
	     * character before the interrupt.  Eat it.
	     */
	    (void) ttgetc();
#endif
	} else {
	    (void) im_waiting(TRUE);
	    do {		/* if it's sysV style signals,
				   we want to try again, since this
				   must not have been SIGINT, but
				   was probably SIGWINCH */
		c = sysmapped_c();
	    } while (c == -1);
	}
	(void) im_waiting(FALSE);
	if (quoted || ((UINT) c != kcod2key(intrc)))
	    record_char(c);
    }

    /* return the character read from the terminal */
#if OPT_TRACE
    {
	char temp[NSTRING];
	TRACE(("tgetc(%s) = %s\n", (quoted
				    ? "quoted"
				    : "unquoted"),
	       kcod2prc(c, temp)));
    }
#endif
    return c;
}

/*
 * Get a command sequence (multiple keystrokes) from the keyboard.  Process all
 * applicable prefix keys.  Set lastcmd for commands which want stuttering.
 */
int
kbd_seq(void)
{
    int c;			/* fetched keystroke */

    int prefix = 0;		/* accumulate prefix */

    c = mapped_keystroke();

    if (c == cntl_a) {
	prefix = CTLA;
	c = keystroke();
    } else if (c == cntl_x) {
	prefix = CTLX;
	c = keystroke();
    } else if (c == poundc) {
	prefix = SPEC;
	c = keystroke();
    }

    c |= prefix;

    /* otherwise, just return it */
    return (lastcmd = c);
}

/*
 * Get a command-key, suppressing the mapping
 */
int
kbd_seq_nomap(void)
{
    unkeystroke(keystroke());
    return kbd_seq();
}

/* get a string consisting of inclchartype characters from the current
	position.  if inclchartype is 0, return everything to eol */
int
screen_string(char *buf, int bufn, CHARTYPE inclchartype)
{
    static TBUFF *temp;
    int rc;
    int len;

    if ((rc = screen2tbuff(&temp, inclchartype)) == TRUE) {
	if ((len = tb_length(temp)) >= bufn)
	    len = bufn - 1;
	strncpy0(buf, tb_values(temp), len + 1);
    } else {
	*buf = EOS;
    }
    return rc;
}

/* get a string consisting of inclchartype characters from the current
	position.  if inclchartype is 0, return everything to eol */
int
screen2tbuff(TBUFF ** result, CHARTYPE inclchartype)
{
    int i = 0;
    MARK save_dot;
    int first = -1;
    int last = -1;
    int whole_line = 0;

    save_dot = DOT;

    tb_init(result, EOS);
    /* if from gototag(), grab from the beginning of the string */
    if (b_val(curbp, MDTAGWORD)
	&& inclchartype == vl_ident) {
	whole_line = 1;
    } else if (b_is_directory(curbp)
	       && inclchartype == SCREEN_STRING) {
	whole_line = 1;
	inclchartype = (CHARTYPE) ~ 0;
    }

    if (whole_line
	&& DOT.o > 0
	&& istype(inclchartype, char_at(DOT))) {
	while (DOT.o > 0) {
	    DOT.o--;
	    if (!istype(inclchartype, char_at(DOT))) {
		DOT.o++;
		break;
	    }
	}
    }
    while (!is_at_end_of_line(DOT)) {
	last = CharOf(char_at(DOT));
	if (first < 0)
	    first = last;
#if OPT_WIDE_CTYPES
	if (i == 0) {
	    if (inclchartype & vl_scrtch) {
		if (first != SCRTCH_LEFT[0])
		    inclchartype &= ~vl_scrtch;
	    }
	    if (inclchartype & vl_shpipe) {
		if (first != SHPIPE_LEFT[0])
		    inclchartype &= ~vl_shpipe;
	    }
	}

	/* allow "[!command]" */
	if ((inclchartype & vl_scrtch)
	    && (i == 1)
	    && (last == SHPIPE_LEFT[0])) {
	    /*EMPTY */ ;
	    /* guard against things like "[Buffer List]" on VMS */
	} else if ((inclchartype & vl_pathn)
		   && !ispath(last)
		   && (inclchartype == vl_pathn)) {
	    break;
	} else
#endif
	if (inclchartype && !istype(inclchartype, last))
	    break;
	tb_append(result, last);
	DOT.o++;
	i++;
#if OPT_WIDE_CTYPES
	if (inclchartype & vl_scrtch) {
	    if ((inclchartype & vl_pathn)
		&& ispath(char_at(DOT)))
		continue;
	    if (last == SCRTCH_RIGHT[0])
		break;
	}
#endif
    }

#if OPT_WIDE_CTYPES
#if OPT_VMS_PATH
    if (inclchartype & vl_pathn) {
	;			/* override conflict with "[]" */
    } else
#endif
    if (inclchartype & vl_scrtch) {
	if (last != SCRTCH_RIGHT[0])
	    tb_init(result, EOS);
    }
#endif

    DOT = save_dot;

    return tb_length(*result) != 0;
}

/*
 * Returns the character that ended the last call on 'kbd_string()'
 */
int
end_string(void)
{
    return last_eolchar;
}

void
set_end_string(int c)
{
    last_eolchar = c;
}

/*
 * Returns an appropriate delimiter for /-commands, based on the end of the
 * last reply.  That is, in a command such as
 *
 *	:s/first/last/
 *
 * we will get prompts for
 *
 *	:s/	/-delimiter saved in 'end_string()'
 *	first/
 *	last/
 *
 * If a newline is used at any stage, subsequent delimiters are forced to a
 * newline.
 */
int
kbd_delimiter(void)
{
    register int c = '\n';

    if (isnamedcmd) {
	register int d = end_string();
	if (isPunct(d))
	    c = d;
    }
    return c;
}

/*
 * If we are NOT processing MSDOS pathnames, we may have literal backslashes in
 * pathnames and buffer names.  Escape those by doubling them.
 */
char *
add_backslashes(char *text)
{
    static TBUFF *temp = 0;
    int n;

    if (strchr(text, BACKSLASH) != 0) {
	temp = tb_init(&temp, EOS);
	for (n = 0; text[n] != EOS; ++n) {
	    if (text[n] == BACKSLASH)
		temp = tb_append(&temp, BACKSLASH);
	    temp = tb_append(&temp, text[n]);
	}
	temp = tb_append(&temp, EOS);
	text = tb_values(temp);
    }
    return text;
}

/*
 * Make sure that the buffer will have extra space when passing it to functions
 * that don't know it's a TBUFF.
 */
static char *
tbreserve(TBUFF ** buf)
{
    char *result = tb_values(tb_alloc(buf, tb_length(*buf) + NSTRING));
    BuffToStr(*buf);
    return result;
}

/* turn \X into X */
static void
remove_backslashes(TBUFF * buf)
{
    register char *cp = tb_values(buf);
    register size_t s, d;

    for (s = d = 0; s < tb_length(buf);) {
	if (cp[s] == BACKSLASH)
	    s++;
	cp[d++] = cp[s++];
    }

    buf->tb_used = d;
}

/* count backslashes so we can tell at any point whether we have the current
 * position escaped by one.
 */
static UINT
countBackSlashes(TBUFF * buf, UINT len)
{
    char *buffer = tb_values(buf);
    register UINT count;

    if (len && buffer[len - 1] == BACKSLASH) {
	count = 1;
	while (count + 1 <= len &&
	       buffer[len - 1 - count] == BACKSLASH)
	    count++;
    } else {
	count = 0;
    }
    return count;
}

static void
showChar(int c)
{
    int save_expand;

    if (!vl_echo)
	return;

    save_expand = kbd_expand;
    kbd_expand = 1;		/* show all controls */
    if (qpasswd)
	c = '*';
    kbd_putc(c);
    kbd_expand = save_expand;
}

static void
show1Char(int c)
{
    if (!vl_echo)
	return;
    showChar(c);
    kbd_flush();
}

/*
 * Macro result is true if the buffer is a normal :-command with a leading "!",
 * or is invoked from one of the places that supplies an implicit "!" for shell
 * commands.
 */
#define editingShellCmd(buf,options) \
	 (((options & KBD_EXPCMD) && isShellOrPipe(buf)) \
	|| (options & KBD_SHPIPE))

/* expand a single character (only used on interactive input) */
static int
expandChar(TBUFF ** buf,
	   unsigned *position,
	   int c,
	   KBD_OPTIONS options)
{
    register int cpos = *position;
    register char *cp;
    register BUFFER *bp;
    char str[NFILEN];
    char *buffer = tb_values(*buf);
    int shell = editingShellCmd(buffer, options);
    int expand = ((options & KBD_EXPAND) || shell);
    int exppat = (options & KBD_EXPPAT);

    /* Are we allowed to expand anything? */
    if (!(expand || exppat || shell))
	return FALSE;

    /* Is this a character that we know about? */
    if (strchr(global_g_val_ptr(GVAL_EXPAND_CHARS), c) == 0)
	return FALSE;

    switch (c) {
    case EXPC_THIS:
    case EXPC_THAT:
	if (!expand)
	    return FALSE;

	bp = (c == EXPC_THIS) ? curbp : find_alt();
	if (bp == 0) {
	    kbd_alarm();	/* complain a little */
	    return FALSE;	/* ...and let the user type it as-is */
	}

	cp = bp->b_fname;
	if (isInternalName(cp)) {
	    cp = bp->b_bname;
	} else if (!global_g_val(GMDEXPAND_PATH)) {
	    cp = shorten_path(strcpy(str, cp), FALSE);
#if OPT_MSDOS_PATH
	    /* always use backslashes if invoking external prog */
	    if (shell)
		cp = SL_TO_BSL(cp);
#endif
	}

	break;

    case EXPC_TOKEN:
	if (!expand)
	    return FALSE;

	if (screen_string(str, sizeof(str), vl_pathn))
	    cp = str;
	else
	    cp = NULL;

	break;

    case EXPC_RPAT:
	if (!exppat)
	    return FALSE;

	if (tb_length(replacepat))
	    cp = tb_values(replacepat);
	else
	    cp = NULL;

	break;

    case EXPC_SHELL:
	if (!shell)
	    return FALSE;

#ifdef only_expand_first_bang
	/* without this check, do as vi does -- expand '!'
	   to previous command anywhere it's typed */
	if (cpos > (buffer[0] == '!'))
	    return FALSE;
#endif
	cp = tb_values(tb_save_shell[!isShellOrPipe(buffer)]);
	if (cp != NULL && isShellOrPipe(cp))
	    cp++;		/* skip the '!' */
	break;

    default:
	return FALSE;
    }

    if (cp != NULL) {
	while ((c = *cp++) != EOS) {
	    tb_insert(buf, cpos++, c);
	    showChar(c);
	}
	kbd_flush();
    }
    *position = cpos;
    return TRUE;
}

/*
 * Returns true for the (presumably control-chars) that we use for line edits
 */
int
is_edit_char(int c)
{
    return (isreturn(c)
	    || isbackspace(c)
	    || (c == wkillc)
	    || (c == killc));
}

/*
 * Erases the response from the screen for 'kbd_string()'
 */
void
kbd_kill_response(TBUFF * buffer, unsigned *position, int c)
{
    char *buf = tb_values(buffer);
    TBUFF *tmp = 0;
    int cpos = *position;
    UINT mark = cpos;

    tmp = tb_copy(&tmp, buffer);
    while (cpos > 0) {
	cpos--;
	kbd_erase();
	if (c == wkillc) {
	    if (!isSpace(buf[cpos])) {
		if (cpos > 0 && isSpace(buf[cpos - 1]))
		    break;
	    }
	}

	if (c != killc && c != wkillc)
	    break;
    }
    if (vl_echo)
	kbd_flush();

    *position = cpos;
    buffer->tb_used = cpos;
    if (mark < tb_length(tmp)) {
	tb_bappend(&buffer, tb_values(tmp) + mark, tb_length(tmp) - mark);
    }
    tb_free(&tmp);
}

/*
 * Display the default response for 'kbd_string()', escaping backslashes if
 * necessary.
 */
int
kbd_show_response(TBUFF ** dst,	/* string with escapes */
		  char *src,	/* string w/o escapes */
		  unsigned bufn,	/* # of chars we read from 'src[]' */
		  int eolchar,
		  KBD_OPTIONS options)
{
    register unsigned k;

    /* add backslash escapes in front of volatile characters */
    tb_init(dst, 0);

    for (k = 0; k < bufn; k++) {
	register int c = src[k];

	if ((c == BACKSLASH) || (c == eolchar && eolchar != '\n')) {
	    if (options & KBD_QUOTES)
		tb_append(dst, BACKSLASH);	/* add extra */
	} else if (strchr(global_g_val_ptr(GVAL_EXPAND_CHARS), c) != 0) {
	    if (c == EXPC_RPAT && !(options & KBD_EXPPAT))
		/*EMPTY */ ;
	    else if (c == EXPC_SHELL && !(options & KBD_SHPIPE))
		/*EMPTY */ ;
	    else if ((options & KBD_QUOTES)
		     && (options & KBD_EXPAND))
		tb_append(dst, BACKSLASH);	/* add extra */
	}
	tb_append(dst, c);
    }

    /* put out the default response, which is in the buffer */
    kbd_init();
    for (k = 0; k < tb_length(*dst); k++) {
	showChar(tb_values(*dst)[k]);
    }
    if (vl_echo)
	kbd_flush();
    return tb_length(*dst);
}

/* default function for 'edithistory()' */
int
/*ARGSUSED*/
eol_history(const char *buffer GCC_UNUSED, unsigned cpos GCC_UNUSED, int c, int eolchar)
{
    if (isPrint(eolchar) || eolchar == '\r') {
	if (c == eolchar || (eolchar == '\r' && c == '\n'))
	    return TRUE;
    }
    return FALSE;
}

/*
 * Store a one-level push-back of the shell command text. This allows simple
 * prompt/substitution of shell commands, while keeping the "!" and text
 * separate for the command decoder.
 */
static int pushed_back;
static int pushback_flg;
static char *pushback_ptr;

void
kbd_pushback(TBUFF * buf, int skip)
{
    static TBUFF *PushBack;
    char *buffer = tb_values(buf);	/* FIXME */

    TRACE(("kbd_pushback(%s,%d)\n", tb_visible(buf), skip));
    if (macroize(&PushBack, buf, skip)) {
	pushed_back = TRUE;
	pushback_flg = clexec;
	pushback_ptr = execstr;
	clexec = TRUE;
	execstr = tb_values(PushBack);
	buffer[skip] = EOS;
	buf->tb_used = skip;
    }
}

int
kbd_is_pushed_back(void)
{
    return clexec && pushed_back;
}

/* A generalized prompt/reply function which allows the caller to specify an
 * additional terminator as well as '\n'.  Both are accepted.  Assumes the
 * buffer already contains a valid (possibly null) string to use as the default
 * response.
 */
int
kbd_string(const char *prompt,	/* put this out first */
	   char *extbuf,	/* the caller's (possibly full) buffer */
	   unsigned bufn,	/* the length of  " */
	   int eolchar,		/* char we can terminate on, in addition to '\n' */
	   KBD_OPTIONS options,	/* KBD_EXPAND/KBD_QUOTES, etc. */
	   int (*complete) (DONE_ARGS))		/* handles completion */
{
    int code;
    static TBUFF *temp;

    options &= ~KBD_0CHAR;	/* this func is for null-terminated strings! */
    tb_scopy(&temp, extbuf);
    code = kbd_reply(prompt, &temp, eol_history, eolchar, options, complete);
    if (bufn > tb_length(temp))
	bufn = tb_length(temp);
    if (bufn != 0)
	memcpy(extbuf, tb_values(temp), bufn);
    extbuf[bufn - 1] = EOS;
    return code;
}

int
kbd_string2(const char *prompt,	/* put this out first */
	    TBUFF ** result,	/* the caller's (possibly full) buffer */
	    int eolchar,	/* char we can terminate on, in addition to '\n' */
	    KBD_OPTIONS options,	/* KBD_EXPAND/KBD_QUOTES, etc. */
	    int (*complete) (DONE_ARGS))	/* handles completion */
{
    return kbd_reply(prompt, result, eol_history, eolchar, options, complete);
}

/*
 * Prompt for strings, used for @"interactive" variables, and the &query,
 * &dquery, and &qpasswd functions.
 */
char *
user_reply(const char *prompt, const char *dft_val)
{
    static TBUFF *replbuf, *passwdbuf;

    TBUFF **ptb;
    int save_vl_msgs, save_clexec, status;

    save_vl_msgs = vl_msgs;
    vl_msgs = TRUE;

    save_clexec = clexec;
    clexec = FALSE;

    if (!qpasswd)
	ptb = &replbuf;
    else {
	/* 
	 * Querying user for a password.  Store result in an alternate buffer,
	 * the value of which is _not_ reused (so that it can't be recalled via
	 * &query or &dquery or an @ var).
	 */
	if (passwdbuf)
	    tb_free(&passwdbuf);
	ptb = &passwdbuf;
    }

    if (dft_val != error_val) {
	TRACE(("user_reply, given default value %s\n", dft_val));
	tb_scopy(ptb, dft_val);
    }

    status = kbd_reply(prompt,
		       ptb,
		       eol_history,
		       '\n',
		       KBD_EXPAND | KBD_QUOTES,
		       no_completion);

    vl_msgs = save_vl_msgs;
    clexec = save_clexec;

    if (status == ABORT)
	return NULL;
    else			/* FIXME: assumes EOS added by kbd_reply */
	return tb_values(*ptb);
}

#define isMiniMotion(f) (((f) & MOTION) && ((f) & (MINIBUF)))

/*
 * We use the editc character to toggle between insert/command mode in the
 * minibuffer.  This is normally bound to ^G (the position command).
 */
static int
isMiniEdit(int c)
{
    const CMDFUNC *cfp = ((miniedit)
			  ? CommandKeyBinding(c)
			  : InsertKeyBinding(c));
    return ((c == editc) || ((cfp != 0) && isMiniMotion(cfp->c_flags)));
}

/*
 * Shift the minibuffer left/right to keep the cursor visible, as well as the
 * prompt, if that's feasible.
 */
static void
shiftMiniBuffer(int offs)
{
    static const int slack = 5;
    int shift = w_val(wminip, WVAL_SIDEWAYS);
    int adjust;
    BUFFER *savebp;
    WINDOW *savewp;
    MARK savemk;

    beginDisplay();
    savebp = curbp;
    savewp = curwp;
    savemk = MK;

    curbp = bminip;
    curwp = wminip;

    adjust = offs - (shift + LastMsgCol - 1);
    if (adjust >= 0)
	mvrightwind(TRUE, 1 + adjust);
    else if (shift > 0 && (slack + adjust) < 0)
	mvleftwind(TRUE, slack - adjust);

    curbp = savebp;
    curwp = savewp;
    MK = savemk;

    kbd_flush();
    endofDisplay();
}

static int
fakeKeyCode(const CMDFUNC * f)
{
    return (miniedit
	    ? fnc2kcod(f)
	    : fnc2kins(f));
}

static int
editMinibuffer(TBUFF ** buf, unsigned *cpos, int c, int margin, int quoted)
{
    int edited = FALSE;
    const CMDFUNC *cfp;
    int save_insertmode = insertmode;
    BUFFER *savebp;
    WINDOW *savewp;
    MARK savemk;

    beginDisplay();
    savebp = curbp;
    savewp = curwp;
    curbp = bminip;
    curwp = wminip;
    savemk = MK;
    ++no_minimsgs;

    TRACE((T_CALLED
	   "editMiniBuffer(%d:%s) called with c=%#x, miniedit=%d, dot=%d\n",
	   *cpos, tb_visible(*buf),
	   c, miniedit, DOT.o));

    cfp = ((miniedit)
	   ? CommandKeyBinding(c)
	   : InsertKeyBinding(c));

    /* Use editc (normally ^G) to toggle insert/command mode */
    if (c == editc && !quoted) {
	miniedit = !miniedit;
    } else if ((miniedit || isSpecial(c))
	       && (cfp != 0) && (cfp->c_flags & MINIBUF) != 0) {
	int first = *cpos + margin;
	int old_clexec = clexec;
	int old_named = isnamedcmd;
	int old_margin = b_left_margin(bminip);
	int old_shape = regionshape;

	/*
	 * Reset flags that might cause a recursion into the prompt/reply
	 * code.
	 */
	clexec = 0;
	isnamedcmd = 0;

	/*
	 * Set limits so we don't edit the prompt, and allow us to move the
	 * cursor with the arrow keys just past the end of line.
	 */
	b_set_left_margin(bminip, margin);
	DOT.o = llength(DOT.l);
	linsert(1, ' ');	/* pad the line so we can move */

	DOT.o = first;
	MK = DOT;
	curwp->w_line = DOT;
	regionshape = EXACT;	/* operdel(), etc., do not set this */
	(void) execute(cfp, FALSE, 1);
	insertmode = save_insertmode;
	edited = TRUE;

	llength(DOT.l) -= 1;	/* strip the padding */
	b_set_left_margin(bminip, old_margin);

	clexec = old_clexec;
	isnamedcmd = old_named;
	regionshape = old_shape;

	/*
	 * Cheat a little, since we may have used an alias for
	 * '$', which can set the offset just past the end of
	 * line.
	 */
	if (DOT.o > llength(DOT.l)) {
	    DOT.o = llength(DOT.l);
	}
	*cpos = DOT.o - margin;

	/*
	 * Copy the data back from the minibuffer into our working TBUFF.
	 */
	tb_init(buf, EOS);
	if (llength(DOT.l) > margin)
	    tb_bappend(buf, lvalue(DOT.l) + margin, llength(DOT.l) - margin);

	/*
	 * Below are some workarounds for making it appear that we're doing the
	 * right thing for certain non-motion commands.  They allow us to revert to
	 * insert-mode (the default), moving the cursor first as expected.
	 */
    } else if (cfp == &f_insert) {
	miniedit = !miniedit;
    } else if (miniedit && cfp == &f_insertbol) {
	edited = editMinibuffer(buf, cpos, fakeKeyCode(&f_firstnonwhite),
				margin, quoted);
	miniedit = FALSE;
    } else if (miniedit && cfp == &f_appendeol) {
	edited = editMinibuffer(buf, cpos, fakeKeyCode(&f_gotoeol),
				margin, quoted);
	miniedit = FALSE;
    } else if (miniedit && cfp == &f_append) {
	edited = editMinibuffer(buf, cpos, fakeKeyCode(&f_forwchar_to_eol),
				margin, quoted);
	miniedit = FALSE;
	/*
	 * Reject other non-motion commands for now.  We haven't a good way to
	 * update the minibuffer if someone inserts a newline, so we couldn't
	 * implement insert except as a special case (see above).
	 */
    } else if (isSpecial(c) || (miniedit && (cfp != 0))) {
	kbd_alarm();
    } else {
	LINE *lp = DOT.l;
	miniedit = FALSE;
	if (!vl_echo || qpasswd) {
	    char tmp = (char) c;
	    tb_bappend(buf, &tmp, 1);
	    if (qpasswd)
		show1Char(c);
	} else if (llength(lp) >= margin) {
	    show1Char(c);
	    tb_init(buf, EOS);
	    tb_bappend(buf, lvalue(lp) + margin, llength(lp) - margin);
	}
	*cpos += 1;
	edited = TRUE;
    }

    shiftMiniBuffer(DOT.o);

    if (*cpos > tb_length(*buf))
	*cpos = tb_length(*buf);

    TRACE(("...editMiniBuffer(%d:%s) returns %d, miniedit=%d, dot=%d\n",
	   *cpos, tb_visible(*buf),
	   edited, miniedit, DOT.o));

    --no_minimsgs;
    curbp = savebp;
    curwp = savewp;
    MK = savemk;
    kbd_flush();
    endofDisplay();

    returnCode(edited);
}

/*
 * Read a character quoted, e.g., by ^V.  If we allow multiple characters (the
 * normal case), check if the character is the beginning of a number.  Decode
 * numbers in hex, octal or decimal.  If inscreen, show the progress on the
 * message line.
 */
int
read_quoted(int count, int inscreen)
{
    int c, digs, base, i, num, delta;
    const char *str;

    i = digs = 0;
    num = 0;

    c = keystroke_raw8();
    if (count <= 0)
	return c;

    /* accumulate up to 3 digits */
    if (isDigit(c) || c == 'x') {
	if (!inscreen) {
	    kbd_putc(c);
	    kbd_flush();
	}
	if (c == '0') {
	    digs = 4;		/* including the leading '0' */
	    base = 8;
	    str = "octal";
	} else if (c == 'x') {
	    digs = 3;		/* including the leading 'x' */
	    base = 16;
	    c = '0';
	    str = "hex";
	} else {
	    digs = 3;
	    base = 10;
	    str = "decimal";
	}
	do {
	    if (isbackspace(c)) {
		num /= base;
		if (--i < 0)
		    break;
	    } else {
		if ((c >= 'a') && (c <= 'f'))
		    delta = ('a' - 10);
		else if ((c >= 'A') && (c <= 'F'))
		    delta = ('A' - 10);
		else if (isDigit(c)
			 && (c >= '0') && (c <= base + '0'))
		    delta = '0';
		else
		    break;
		num = num * base + c - delta;
		i++;
	    }

	    if (inscreen) {
		mlwrite("Enter %s digits... %d", str, num);
	    } else if (i > 1) {
		kbd_putc(c);
		kbd_flush();
	    }

	    if (i >= digs)
		break;
	    (void) update(FALSE);
	    c = keystroke_raw8();
	} while (isbackspace(c) ||
		 (isDigit(c) && base >= 10) ||
		 (base == 8 && c < '8') ||
		 (base == 16 && (c >= 'a' && c <= 'f')) ||
		 (base == 16 && (c >= 'A' && c <= 'F')));
    }

    if (inscreen) {
	mlerase();
    } else {
	for (delta = i; delta > 0; delta--)
	    kbd_erase();
	kbd_flush();
    }

    if (c >= 0) {
	if (i == 0)		/* Did we start a number? */
	    return c;
	else if (i < digs)	/* any other character will be pushed back */
	    unkeystroke(c);
    }

    return (i > 0) ? (num & 0xff) : -1;
}

static int
may_complete(TBUFF * data, KBD_OPTIONS options)
{
    int result = ABORT;

    if ((options & KBD_MAYBEC) != 0) {
	result = SORTOFTRUE;
    } else if ((options & KBD_MAYBEC2) != 0
	       && tb_length(data) != 0
	       && (*tb_values(data) == '%')) {
	result = SORTOFTRUE;
    }
    return result;
}

/*
 * Same as 'kbd_string()', except for adding the 'endfunc' parameter.
 *
 * Returns:
 *	ABORT - abort character given (usually ESC)
 *	SORTOFTRUE - backspace from empty-buffer
 *	TRUE - buffer is not empty
 *	FALSE - buffer is empty
 */
int
kbd_reply(const char *prompt,	/* put this out first */
	  TBUFF ** extbuf,	/* the caller's (possibly full) buffer */
	  int (*endfunc) (EOL_ARGS),	/* parsing with 'eolchar' delimiter */
	  int eolchar,		/* char we can terminate on, in addition to '\n' */
	  KBD_OPTIONS options,	/* KBD_EXPAND/KBD_QUOTES */
	  int (*complete) (DONE_ARGS))	/* handles completion */
{
    int c;
    int done;
    unsigned cpos;		/* current position */
    int status;
    int shell;
    int margin;
    size_t save_len;

    int quotef;			/* are we quoting the next char? */
    UINT backslashes;		/* are we quoting the next expandable char? */
    UINT dontmap = (options & KBD_NOMAP);
    int firstch = TRUE;
    int lastch;
    unsigned newpos;
    TBUFF *buf = 0;
    const char *result;

    TRACE((T_CALLED "kbd_reply(prompt=%s, extbuf=%s, options=%#x)\n",
	   TRACE_NULL(prompt),
	   tb_visible(*extbuf),
	   options));
    TRACE(("clexec=%d, pushed_back=%d\n", clexec, pushed_back));

    miniedit = FALSE;
    set_end_string(EOS);	/* ...in case we don't set it elsewhere */
    tb_unput(*extbuf);		/* FIXME: trim null */

    if (clexec) {
	int actual;

	/*
	 * A "^W!cat" (control/W followed by a shell command) will arrive at
	 * this point with "!" in extbuf, and the command in execstr.  Glue the
	 * two back together so they're seen as a shell command which should
	 * not be name-completed.
	 */
	if (tb_length(*extbuf) == 1
	    && isShellOrPipe(tb_values(*extbuf)))
	    options |= KBD_REGLUE | KBD_SHPIPE;

	tbreserve(extbuf);
	TRACE(("...getting token from: %s\n", str_visible0(execstr)));
	execstr = ((options & KBD_REGLUE) != 0 && pushed_back)
	    ? get_token2(execstr, extbuf, eolchar, &actual)
	    : get_token(execstr, extbuf, eolchar, &actual);
	StrToBuff(*extbuf);	/* FIXME: get_token should use TBUFF */
	TRACE(("...got token, result:  %s\n", tb_visible(*extbuf)));
	status = (tb_length(*extbuf) != 0);
	if (status) {		/* i.e., we got some input */
#if !SMALLER
	    /* FIXME: may have nulls */
	    if ((options & KBD_LOWERC))
		(void) mklower(tb_values(*extbuf));
	    else if ((options & KBD_UPPERC))
		(void) mkupper(tb_values(*extbuf));
#endif
	    if (!(options & KBD_NOEVAL)) {
		buf = tb_copy(&buf, *extbuf);
		tb_append(&buf, EOS);	/* FIXME: for tokval() */
		result = tokval(tb_values(buf));

		/*
		 * Check for special return values from tokval().  It may be
		 * the result of an expression.  If an error was detected
		 * during evaluation, we will see the magic error_val.  That's
		 * a reason to stop here.
		 *
		 * If the result is a string, tokval() will return a quoted
		 * version of it, to avoid confusing recursive calls of
		 * run_func().  But we do not want quotes here, just the
		 * contents.  Dequote it if we started with a function and got
		 * a quoted string.
		 */
		if (result == error_val) {
		    result = "";
		    status = ABORT;
		} else if (toktyp(tb_values(buf)) == TOK_FUNCTION &&
			   toktyp(result) == TOK_QUOTSTR) {
		    ++result;
		}

		(void) tb_scopy(extbuf, result);
		tb_free(&buf);
		tb_unput(*extbuf);	/* trim the null */
	    }
	    if (complete != no_completion
		&& !editingShellCmd(tb_values(*extbuf), options)) {
		cpos =
		    newpos = tb_length(*extbuf);
		if ((options & KBD_MAYBEC2)
		    && tb_length(*extbuf) > 1
		    && (*tb_values(*extbuf) == '%')) {
		    status = TRUE;
		    tb_put(extbuf, cpos, EOS);
		} else if ((*complete) (options, NAMEC, tbreserve(extbuf), &newpos)) {
		    StrToBuff(*extbuf);
		} else {
		    status = may_complete(*extbuf, options);
		    tb_put(extbuf, cpos, EOS);
		}
	    }
	    /*
	     * Splice for multi-part command
	     */
	    if (actual != EOS) {
		set_end_string(actual);
	    } else {
		set_end_string('\n');
	    }
	}
	if (pushed_back && (*execstr == EOS)) {
	    pushed_back = FALSE;
	    clexec = pushback_flg;
	    execstr = pushback_ptr;
#if	OPT_HISTORY
	    if (!pushback_flg) {
		hst_append(*extbuf, EOS);
	    }
#endif
	}
	if (tb_length(*extbuf) == 0
	    || tb_values(*extbuf)[tb_length(*extbuf) - 1] != EOS)
	    tb_append(extbuf, EOS);	/* FIXME */
	TRACE(("reply1:(status=%d, length=%d):%s\n", status,
	       (int) tb_length(*extbuf),
	       tb_visible(*extbuf)));
	returnCode(status);
    }

    quotef = FALSE;
    reading_msg_line = TRUE;

    /* ask for the input string with the prompt */
    if (prompt != 0)
	mlprompt("%s", prompt);

    /*
     * Use the length of the prompt (plus whatever led up to it) as the
     * left-margin for editing.  We'll automatically scroll the edited
     * field left/right within the margins.
     */
    margin = llength(wminip->w_dot.l);
    cpos = kbd_show_response(&buf,
			     tb_values(*extbuf),
			     tb_length(*extbuf),
			     eolchar,
			     options);
    backslashes = 0;		/* by definition, there is an even
				   number of backslashes */
    c = -1;			/* initialize 'lastch' */

    for_ever {
	int EscOrQuo = (quotef ||
			((backslashes & 1) != 0));

	lastch = c;

	/*
	 * Get a character from the user. If not quoted, treat escape
	 * sequences as a single (16-bit) special character.
	 */
	if (quotef)
	    c = read_quoted(1, FALSE);
	else if (dontmap)
	    /* this looks wrong, but isn't.  no mapping will happen
	       anyway, since we're on the command line.  we want SPEC
	       keys to be expanded to #c, but no further.  this does
	       that */
	    c = mapped_keystroke_raw();
	else
	    c = keystroke();

	/* Ignore nulls if the calling function is not prepared to process
	 * them.  We want to be able to search for nulls, but must not use them
	 * in filenames, for example.  (We don't support name-completion with
	 * embedded nulls, either).
	 */
	if ((c == 0 && c != esc_c) && !(options & KBD_0CHAR))
	    continue;

	/* if we echoed ^V, erase it now */
	if (quotef) {
	    firstch = FALSE;
	    kbd_erase();
	    kbd_flush();
	}

	/* If it is a <ret>, change it to a <NL> */
	if (c == '\r' && !quotef)
	    c = '\n';

	/*
	 * If they hit the line terminator (i.e., newline or unescaped
	 * eolchar), finish up.
	 *
	 * Don't allow newlines in the string -- they cause real problems,
	 * especially when searching for patterns containing them -pgf
	 */
	done = FALSE;
	if (c == '\n') {
	    done = TRUE;
	} else if (!EscOrQuo
		   && !is_edit_char(c)
		   && !isMiniEdit(c)) {
	    BuffToStr(buf);
	    if ((*endfunc) (tb_values(buf), tb_length(buf), c, eolchar)) {
		done = TRUE;
	    }
	}

	if (complete != no_completion) {
	    kbd_unquery();
	    shell = editingShellCmd(tb_values(buf), options);
	    if (done && (options & KBD_NULLOK) && cpos == 0)
		/*EMPTY */ ;
	    else if ((done && (may_complete(buf, options) == ABORT))
		     || (!EscOrQuo
			 && !(shell && isPrint(c))
			 && (c == TESTC || c == NAMEC))) {
		/*
		 * If we're going to force name completion, put the cursor at
		 * the end of the line so the name completion code works
		 * properly.  Do this by erasing the remainder of the line and
		 * reentering it.
		 */
		if (cpos < tb_length(buf)) {
		    kbd_erase_to_end(wminip->w_dot.o);
		    while (cpos < tb_length(buf)) {
			show1Char(tb_values(buf)[cpos++]);
		    }
		}
		if (shell && isreturn(c)) {
		    /*EMPTY */ ;
		} else if ((*complete) (options, c, tbreserve(&buf), &cpos)) {
		    done = TRUE;
		    StrToBuff(buf);	/* FIXME */
		    if (c != NAMEC)	/* cancel the unget */
			(void) keystroke();
		} else {
		    StrToBuff(buf);	/* FIXME */
		    if (done) {	/* stay til matched! */
			kbd_unquery();
			(void) ((*complete) (options, TESTC,
					     tbreserve(&buf), &cpos));
		    }
		    firstch = FALSE;
		    continue;
		}
	    }
	}

	if (done) {
	    set_end_string(c);
	    if (options & KBD_QUOTES)
		remove_backslashes(buf);	/* take out quoters */

	    save_len = tb_length(buf);
	    hst_append(buf, eolchar);
	    (void) tb_copy(extbuf, buf);
	    status = (tb_length(*extbuf) != 0);

	    /* If this is a shell command, push-back the actual text to
	     * separate the "!" from the command.  Note that the history
	     * command tries to do this already, but cannot for some special
	     * cases, e.g., the user types
	     *      :execute-named-command !ls -l
	     * which is equivalent to
	     *      :!ls -l
	     */
	    if (status == TRUE	/* ...we have some text */
		&& (options & KBD_EXPCMD)
#if	OPT_HISTORY
		&& (tb_length(buf) == save_len)		/* history didn't split it */
#endif
		&& isShellOrPipe(tb_values(buf))) {
		kbd_pushback(*extbuf, 1);
	    }
	    break;
	}
#if	OPT_HISTORY
	if (!EscOrQuo
	    && edithistory(&buf, &cpos, &c, options, endfunc, eolchar)) {
	    backslashes = countBackSlashes(buf, cpos);
	    firstch = TRUE;
	    continue;
	} else
#endif
	    /*
	     * If editc and esc_c are the same, don't abort unless the
	     * user presses it twice in a row.
	     */
	    if ((c != editc || c == lastch)
		&& ABORTED(c)
		&& !quotef
		&& !dontmap) {
	    tb_init(&buf, esc_c);
	    status = esc_func(FALSE, 1);
	    break;
	} else if ((isbackspace(c) ||
		    c == wkillc ||
		    c == killc) && !quotef) {

	    if (prompt == 0 && c == killc)
		cpos = 0;

	    if (cpos == 0) {
		if (prompt)
		    mlerase();
		if (isbackspace(c)) {	/* splice calls */
		    unkeystroke(c);
		    status = SORTOFTRUE;
		} else {
		    status = FALSE;
		}
		break;
	    }

	    kbd_kill_response(buf, &cpos, c);

	    /*
	     * If we backspaced to erase the buffer, it's ok to return to the
	     * caller, who would be the :-line parser, so we can do something
	     * reasonable with the address specification.
	     */
	    if (cpos == 0
		&& isbackspace(c)
		&& (options & KBD_STATED)) {
		status = SORTOFTRUE;
		break;
	    }
	    backslashes = countBackSlashes(buf, cpos);

	} else if (firstch == TRUE
		   && !quotef
		   && !isMiniEdit(c)
		   && !isSpecial(c)) {
	    /* clean the buffer on the first char typed */
	    unkeystroke(c);
	    kbd_kill_response(buf, &cpos, killc);
	    backslashes = countBackSlashes(buf, cpos);

	} else if (c == quotec && !quotef) {
	    quotef = TRUE;
	    show1Char(c);
	    continue;		/* keep firstch==TRUE */

	} else if (EscOrQuo || !expandChar(&buf, &cpos, c, options)) {
	    if (c == BACKSLASH)
		backslashes++;
	    else
		backslashes = 0;
	    quotef = FALSE;

#if !SMALLER
	    if (!isSpecial(c)) {
		if ((options & KBD_LOWERC)
		    && isUpper(c))
		    c = toLower(c);
		else if ((options & KBD_UPPERC)
			 && isLower(c))
		    c = toUpper(c);
	    }
#endif
	    if (!editMinibuffer(&buf, &cpos, c, margin, EscOrQuo))
		continue;	/* keep firstch==TRUE */
	}
	firstch = FALSE;
    }
#if OPT_POPUPCHOICE
    popdown_completions();
#endif
    miniedit = FALSE;
    reading_msg_line = FALSE;
    tb_free(&buf);
    shiftMiniBuffer(0);

    TRACE(("reply:(status=%d, length=%d):%s\n", status,
	   (int) tb_length(*extbuf),
	   tb_visible(*extbuf)));
    tb_append(extbuf, EOS);	/* FIXME */
    returnCode(status);
}

/*
 * Begin recording the dot command macro.
 * Set up variables and return.
 * we use a temporary accumulator, in case this gets stopped prematurely
 */
int
dotcmdbegin(void)
{
    /* never record a playback */
    if (dotcmdactive != PLAY) {
	(void) TempDot(TRUE);
	dotcmdactive = RECORD;
	return TRUE;
    }
    return FALSE;
}

/*
 * Finish dot command, and copy it to the real holding area
 */
int
dotcmdfinish(void)
{
    if (dotcmdactive == RECORD) {
	ITBUFF *tmp = TempDot(FALSE);
	if (itb_length(tmp) == 0	/* keep the old value */
	    || itb_copy(&dotcmd, tmp) != 0) {
	    itb_first(dotcmd);
	    dotcmdactive = 0;
	    return TRUE;
	}
    }
    return FALSE;
}

/* stop recording a dot command,
	probably because the command is not re-doable */
void
dotcmdstop(void)
{
    if (dotcmdactive == RECORD)
	dotcmdactive = 0;
}

/*
 * Execute the '.' command, by putting us in PLAY mode.
 * The command argument is the number of times to loop. Quit as soon as a
 * command gets an error. Return TRUE if all ok, else FALSE.
 */
int
dotcmdplay(int f, int n)
{
    if (!f)
	n = dotcmdarg ? dotcmdcnt : 1;
    else if (n < 0)
	return TRUE;

    if (f)			/* we definitely have an argument */
	dotcmdarg = TRUE;
    /* else
       leave dotcmdarg alone; */

    if (dotcmdactive || itb_length(dotcmd) == 0) {
	dotcmdactive = 0;
	dotcmdarg = FALSE;
	return FALSE;
    }

    if (n == 0)
	n = 1;

    dotcmdcnt = dotcmdrep = n;	/* remember how many times to execute */
    dotcmdactive = PLAY;	/* put us in play mode */
    itb_first(dotcmd);		/*    at the beginning */

    if (ukb != 0)		/* save our kreg, if one was specified */
	dotcmdkreg = ukb;
    else			/* use our old one, if it wasn't */
	ukb = dotcmdkreg;

    return TRUE;
}

/*
 * Test if we are replaying either '.' command, or keyboard macro.
 */
int
kbd_replaying(int match)
{
    if (dotcmdactive == PLAY) {
	/*
	 * Force a false-return if we are in insert-mode and have
	 * only one character to display.
	 */
	if (match
	    && insertmode == INSMODE_INS
	    && b_val(curbp, MDSHOWMAT)
	    && KbdStack == 0
	    && (dotcmd->itb_last + 1 >= dotcmd->itb_used)) {
	    return FALSE;
	}
	return TRUE;
    }
    return (kbdmacactive == PLAY);
}

int
kbd_mac_recording(void)
{
    return (kbdmacactive == RECORD);
}

/*
 * If we're recording a keyboard macro, remember the length before the
 * start/stop function is called, so we can ignore the last few characters that
 * actually turn the recording off when replaying it.
 */
void
kbd_mac_check(void)
{
    if (kbdmacactive == RECORD) {
	kbdmaclength = itb_length(KbdMacro);
    }
}

/* translate the keyboard-macro into readable form */
void
get_kbd_macro(TBUFF ** rp)
{
    char temp[80];
    unsigned n, last, len;

    *rp = tb_init(rp, EOS);
    if ((last = itb_length(KbdMacro)) != 0) {
	for (n = 0; n < last; n++) {
	    len = kcod2escape_seq(itb_get(KbdMacro, n), temp);
	    *rp = tb_bappend(rp, temp, len);
	}
    }
}

/* ARGSUSED */
int
kbd_mac_startstop(int f GCC_UNUSED, int n GCC_UNUSED)
{
    if (kbdmacactive) {
	if (kbdmacactive == RECORD) {
	    mlwrite("[Macro recording stopped]");
	    kbdmacactive = 0;
	    if (KbdMacro != 0
		&& itb_length(KbdMacro) > kbdmaclength)
		KbdMacro->itb_used = kbdmaclength;
	    upmode();
	    return TRUE;
	}

	/* note that for PLAY, we do nothing -- that makes the
	 * '^X-)' at the of the recorded buffer a no-op during
	 * playback */
	return TRUE;
    } else {
	mlwrite("[Macro recording started]");
	kbdmacactive = RECORD;
	upmode();
	return (itb_init(&KbdMacro, esc_c) != 0);
    }

}

/*
 * Execute a macro.
 * The command argument is the number of times to loop. Quit as soon as a
 * command gets an error. Return TRUE if all ok, else FALSE.
 */
/* ARGSUSED */
int
kbd_mac_exec(int f GCC_UNUSED, int n)
{
    if (!kbdmacactive) {
	if (n <= 0)
	    return TRUE;
	return start_kbm(n, DEFAULT_REG, KbdMacro);
    }

    mlforce("[Can't execute macro unless idle]");
    return FALSE;
}

/* ARGSUSED */
int
kbd_mac_save(int f GCC_UNUSED, int n GCC_UNUSED)
{
    TBUFF *p = 0;
    unsigned j, len;

    ksetup();
    get_kbd_macro(&p);
    if ((len = tb_length(p)) != 0) {
	for (j = 0; j < len; j++)
	    if (!kinsert(tb_get(p, j)))
		break;
    }
    tb_free(&p);
    kdone();
    mlwrite("[Keyboard macro saved in register %c.]", index2reg(ukb));
    return TRUE;
}

/*
 * Test if the given macro has already been started.
 */
int
kbm_started(int macnum, int force)
{
    if (force || (kbdmacactive == PLAY)) {
	register KSTACK *sp;
	for (sp = KbdStack; sp != 0; sp = sp->m_link) {
	    if (sp->m_indx == macnum) {
		while (kbdmacactive == PLAY)
		    finish_kbm();
		mlwarn("[Error: currently executing %s%c]",
		       macnum == DEFAULT_REG
		       ? "macro" : "register ",
		       index2reg(macnum));
		return TRUE;
	    }
	}
    }
    return FALSE;
}

/*
 * Start playback of the given keyboard command-string
 */
int
start_kbm(int n,		/* # of times to repeat */
	  int macnum,		/* register to execute */
	  ITBUFF * ptr)		/* data to interpret */
{
    KSTACK *sp;
    ITBUFF *tp = 0;

    if (interrupted())
	return FALSE;

    if (kbdmacactive == RECORD && KbdStack != 0)
	return TRUE;

    if (itb_length(ptr)
	&& (sp = typealloc(KSTACK)) != 0
	&& itb_copy(&tp, ptr) != 0) {

	/* make a copy of the macro in case recursion alters it */
	itb_first(tp);

	sp->m_save = kbdmacactive;
	sp->m_indx = macnum;
	sp->m_rept = n;
	sp->m_kbdm = tp;
	sp->m_link = KbdStack;

	KbdStack = sp;
	kbdmacactive = PLAY;	/* start us in play mode */

	/* save data for "." on the same stack */
	sp->m_dots = 0;
	if (dotcmdactive == PLAY) {
	    dotcmd = 0;
	    dotcmdactive = RECORD;
	}
	return (itb_init(&dotcmd, esc_c) != 0
		&& itb_init(&(sp->m_dots), esc_c) != 0);
    }
    return FALSE;
}

/*
 * Finish a macro begun via 'start_kbm()'
 */
static void
finish_kbm(void)
{
    if (kbdmacactive == PLAY) {
	register KSTACK *sp = KbdStack;

	kbdmacactive = 0;
	if (sp != 0) {
	    kbdmacactive = sp->m_save;
	    KbdStack = sp->m_link;

	    itb_free(&(sp->m_kbdm));
	    itb_free(&(sp->m_dots));
	    free((char *) sp);
	}
    }
}
