/*	This file is for functions having to do with key bindings,
 *	descriptions, help commands and startup file.
 *
 *	written 11-feb-86 by Daniel Lawrence
 *
 * $Header: /usr/build/vile/vile/RCS/bind.c,v 1.183 1998/07/23 09:19:56 cmorgan Exp $
 *
 */

#ifdef _WIN32
# include <windows.h>
#endif

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

#define BI_DATA NBST_DATA
#include	"btree.h"

#define SHORT_CMD_LEN 4	/* command names longer than this are preferred
				over those shorter.  e.g. display "quit"
				instead of "q" if possible */

extern const int nametblsize;

static	KBIND *	kcode2kbind ( int code );

#if OPT_CASELESS
#define Strcmp(s,d)      cs_strcmp(case_insensitive, s, d)
#define StrNcmp(s,d,len) cs_strncmp(case_insensitive, s, d, len)
#else
#define Strcmp(s,d)      strcmp(s, d)
#define StrNcmp(s,d,len) strncmp(s, d, len)
#endif

#if OPT_REBIND
#define	isSpecialCmd(k) \
		( (k == &f_cntl_a_func)\
		||(k == &f_cntl_x_func)\
		||(k == &f_poundc_func)\
		||(k == &f_unarg_func)\
		||(k == &f_esc_func)\
		)

static	char *	kcod2prc (int c, char *seq);
static	int	install_bind (int c, const CMDFUNC *kcmd, const CMDFUNC **oldfunc);
static	int	key_to_bind ( const CMDFUNC *kcmd );
static	int	rebind_key ( int c, const CMDFUNC *kcmd );
static	int	strinc (const char *sourc, const char *sub);
static	int	unbindchar ( int c );
static	int	update_binding_list ( BUFFER *bp );
static	void	makebindlist (LIST_ARGS);
#endif	/* OPT_REBIND */

#if OPT_NAMEBST
static int kbd_complete_bst( int case_insensitive, int c, char *buf, unsigned *pos);
#else
#define kbd_complete_bst(case_insensitive, c, buf, pos) \
	kbd_complete(case_insensitive, c, buf, pos, \
			(const char *)&nametbl[0], sizeof(nametbl[0]))
#endif	/* OPT_NAMEBST */

#if OPT_EVAL || OPT_REBIND
static	const NTAB * fnc2ntab ( const CMDFUNC *cfp );
static	int	prc2kcod ( const char *kk );
#endif

#if OPT_REBIND
static	KBIND	*KeyBindings = kbindtbl;
#endif

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

#if OPT_NAMEBST
static BI_NODE*
new_namebst (BI_DATA *a)
{
	BI_NODE *p = typecalloc(BI_NODE);
	p->value = *a;
	if (!(a->n_flags & NBST_READONLY))
		BI_KEY(p) = strmalloc(a->bi_key);
	return p;
}

static void
old_namebst (BI_NODE *a)
{
	if (!(a->value.n_flags & NBST_READONLY))
		free(TYPECAST(char,BI_KEY(a)));
	free(a);
}

static void
dpy_namebst (BI_NODE *a GCC_UNUSED, int level GCC_UNUSED)
{
#if OPT_TRACE
	while (level-- > 0)
		TRACE((". "));
	TRACE(("%p -> %s (%d)\n", a, BI_KEY(a), a->balance));
#endif
}

static	BI_TREE namebst = {new_namebst, old_namebst, dpy_namebst};
#endif /* OPT_NAMEBST */

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

int
no_such_function(const char * fnp)
{
	mlforce("[No such function \"%s\"]", fnp != 0 ? fnp : "");
	return FALSE;
}

/* give me some help!!!! bring up a buffer and read the help file into it */
/* ARGSUSED */
int
help(int f GCC_UNUSED, int n GCC_UNUSED)
{
	register BUFFER *bp;	/* buffer pointer to help */
	char *fname;		/* ptr to file returned by flook() */
	int alreadypopped;

	/* first check if we are already here */
	bp = bfind(HELP_BufName, BFSCRTCH);
	if (bp == NULL)
		return FALSE;

	if (bp->b_active == FALSE) { /* never been used */
		fname = flook(helpfile, FL_ANYWHERE|FL_READABLE);
		if (fname == NULL) {
			mlforce("[Sorry, can't find the help information]");
			(void)zotbuf(bp);
			return(FALSE);
		}
		alreadypopped = (bp->b_nwnd != 0);
		/* and read the stuff in */
		if (readin(fname, 0, bp, TRUE) == FALSE ||
				popupbuff(bp) == FALSE) {
			(void)zotbuf(bp);
			return(FALSE);
		}
		set_bname(bp, HELP_BufName);
		set_rdonly(bp, fname, MDVIEW);

		make_local_b_val(bp,MDIGNCASE); /* easy to search, */
		set_b_val(bp,MDIGNCASE,TRUE);
		b_set_scratch(bp);
		if (!alreadypopped)
			shrinkwrap();
	}

	if (!swbuffer(bp))
		return FALSE;

	if (help_at >= 0) {
		if (!gotoline(TRUE, help_at))
			return FALSE;
		mlwrite("[Type '1G' to return to start of help information]");
		help_at = -1;  /* until zotbuf is called, we let normal
				DOT tracking keep our position */
	}
	return TRUE;
}

#if OPT_REBIND

#if OPT_TERMCHRS

	/* patch: this table and the corresponding initializations should be
	 * generated by 'mktbls'.  In any case, the table must be sorted to use
	 * name-completion on it.
	 */
static	const struct {
		const char *name;
		int	*value;
		char	how_to;
	} TermChrs[] = {
		{"backspace",		&backspc,	's'},
		{"interrupt",		&intrc,		's'},
		{"line-kill",		&killc,		's'},
		{"mini-edit",		&editc,		's'},
		{"name-complete",	&name_cmpl,	0},
		{"quote-next",		&quotec,	0},
		{"start-output",	&startc,	's'},
		{"stop-output",		&stopc,		's'},
		{"suspend",		&suspc,		's'},
		{"test-completions",	&test_cmpl,	0},
		{"word-kill",		&wkillc,	's'},
		{0}
	};

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

/* list the current chrs into the current buffer */
/* ARGSUSED */
static void
makechrslist(int dum1 GCC_UNUSED, void *ptr GCC_UNUSED)
{
	register int i;
	char	temp[NLINE];

	bprintf("--- Terminal Character Settings %*P\n", term.t_ncol-1, '-');
	for (i = 0; TermChrs[i].name != 0; i++) {
		bprintf("\n%s = %s",
			TermChrs[i].name,
			kcod2prc(*(TermChrs[i].value), temp));
	}
}

/*
 * Find a special-character definition, given the name
 */
static int
chr_lookup(const char *name)
{
	register int	j;
	for (j = 0; TermChrs[j].name != 0; j++)
		if (!strcmp(name, TermChrs[j].name))
			return j;
	return -1;
}

/*
 * The 'chr_complete()' and 'chr_eol()' functions are invoked from
 * 'kbd_reply()' to setup the mode-name completion and query displays.
 */
static int
chr_complete(int c, char *buf, unsigned *pos)
{
	return kbd_complete(FALSE, c, buf, pos, (const char *)&TermChrs[0],
		sizeof(TermChrs[0]));
}

static int
/*ARGSUSED*/
chr_eol(
	const char * buffer GCC_UNUSED,
	unsigned cpos GCC_UNUSED,
	int c GCC_UNUSED,
	int eolchar GCC_UNUSED)
{
	return isSpace(c);
}

#if OPT_UPBUFF
/* ARGSUSED */
static int
update_termchrs(BUFFER *bp GCC_UNUSED)
{
	return show_termchrs(FALSE,1);
}
#endif

/* ARGSUSED */
int
set_termchrs(int f GCC_UNUSED, int n GCC_UNUSED)
{
	register int s, j;
	static	TBUFF *name;
	int c;

	/* get the table-entry */
	tb_scopy(&name, "");
	if ((s = kbd_reply("Terminal setting: ", &name, chr_eol,
		' ', 0, chr_complete)) == TRUE) {

		j = chr_lookup(tb_values(name));
		switch (TermChrs[j].how_to) {
		case 's':
		default:
			c = key_to_bind((CMDFUNC *)0);
			if (c < 0)
				return(FALSE);
			*(TermChrs[j].value) = c;
			break;
		}
		update_scratch(TERMINALCHARS_BufName, update_termchrs);
	}
	return s;
}

/* ARGSUSED */
int
show_termchrs(int f GCC_UNUSED, int n GCC_UNUSED)
{
	return liststuff(TERMINALCHARS_BufName, FALSE, makechrslist, 0, (void *)0);
}
#endif /* OPT_TERMCHRS */

static void
ostring(	/* output a string of output characters */
char *s)	/* string to output */
{
	if (discmd)
		kbd_puts(s);
}

/* bindkey:	add a new key to the key binding table		*/

/* ARGSUSED */
int
bindkey(int f GCC_UNUSED, int n GCC_UNUSED)
{
	register const CMDFUNC *kcmd; /* ptr to the requested function to bind to */
	char cmd[NLINE];
	char *fnp;

	/* prompt the user to type in a key to bind */
	/* and get the function name to bind it to */
	fnp = kbd_engl("Bind function whose full name is: ", cmd);

	if (fnp == NULL || (kcmd = engl2fnc(fnp)) == NULL) {
		return no_such_function(fnp);
	}

	return rebind_key(key_to_bind(kcmd), kcmd);
}

/*
 * Prompt-for and return the key-code to bind.
 */
static int
key_to_bind(register const CMDFUNC *kcmd)
{
	char outseq[NLINE];	/* output buffer for keystroke sequence */
	register int c;

	mlprompt("...to keyboard sequence (type it exactly): ");

	/* get the command sequence to bind */
	if (clexec) {
		char tok[NSTRING];
		macarg(tok);	/* get the next token */
		c = prc2kcod(tok);
	} else {
		/* perhaps we only want a single key, not a sequence */
		/* 	(see more comments below) */
		if (isSpecialCmd(kcmd))
			c = keystroke();
		else
			c = kbd_seq();
	}

	if (c >= 0) {
		/* change it to something we can print as well */
		ostring(kcod2prc(c, outseq));
		hst_append_s(outseq, FALSE);
	} else {
		mlforce("[Not a proper key-sequence]");
	}
	return c;
}

/*
 * Given a key-code and a command-function pointer, rebind the key-code to
 * the command-function.
 */
static int
rebind_key (
register int	c,
register const CMDFUNC *kcmd)
{
	static const CMDFUNC ignored = { INIT_UNION( unimpl ) },
		*old = &ignored;
	return install_bind (c, kcmd, &old);
}

/*
 * Prefix-keys can be only bound to one value. This procedure tests the
 * argument 'kcmd' to see if it is a prefix key, and if so, unbinds the
 * key, and sets the corresponding global variable to the new value.
 * The calling procedure will then do the binding per se.
 */
static void
reset_prefix (
register int	c,
register const CMDFUNC *kcmd)
{
	if (isSpecialCmd(kcmd)) {
		register int j;
		/* search for an existing binding for the prefix key */
		if ((j = fnc2kcod(kcmd)) >= 0)
			(void)unbindchar(j);
		/* reset the appropriate global prefix variable */
		if (kcmd == &f_cntl_a_func)
			cntl_a = c;
		if (kcmd == &f_cntl_x_func)
			cntl_x = c;
		if (kcmd == &f_poundc_func)
			poundc = c;
		if (kcmd == &f_unarg_func)
			reptc = c;
		if (kcmd == &f_esc_func)
			abortc = c;
	}
}

/*
 * Bind a command-function pointer to a given key-code (saving the old
 * value of the function-pointer via an pointer given by the caller).
 */
static int
install_bind (
register int	c,
register const CMDFUNC *kcmd,
const CMDFUNC **oldfunc)
{
	register KBIND *kbp;	/* pointer into a binding table */

	if (c < 0)
		return FALSE;	/* not a legal key-code */

	/* if the function is a prefix key, i.e. we're changing the definition
		of a prefix key, then they typed a dummy function name, which
		has been translated into a dummy function pointer */
	*oldfunc = kcod2fnc(c);
	reset_prefix(-1, *oldfunc);
	reset_prefix(c, kcmd);

	if (!isspecial(c)) {
		asciitbl[c] = TYPECAST(CMDFUNC,kcmd);
	} else {
		if ((kbp = kcode2kbind(c)) != 0) { /* change it in place */
			kbp->k_cmd = kcmd;
		} else {
			if ((kbp = typealloc(KBIND)) == 0) {
				return no_memory("Key-Binding");
			}
			kbp->k_link = KeyBindings;
			kbp->k_code = (short)c;	/* add keycode */
			kbp->k_cmd  = kcmd;	/* and func pointer */
			KeyBindings = kbp;
		}
	}
	update_scratch(BINDINGLIST_BufName, update_binding_list);
	return(TRUE);
}

/* unbindkey:	delete a key from the key binding table	*/

/* ARGSUSED */
int
unbindkey(int f GCC_UNUSED, int n GCC_UNUSED)
{
	register int c;		/* command key to unbind */
	char outseq[NLINE];	/* output buffer for keystroke sequence */

	/* prompt the user to type in a key to unbind */
	mlprompt("Unbind this key sequence: ");

	/* get the command sequence to unbind */
	if (clexec) {
		char tok[NSTRING];
		macarg(tok);	/* get the next token */
		c = prc2kcod(tok);
		if (c < 0) {
			mlforce("[Illegal key-sequence \"%s\"]",tok);
			return FALSE;
		}
	} else {
		c = kbd_seq();
		if (c < 0) {
			mlforce("[Not a bindable key-sequence]");
			return(FALSE);
		}
	}

	/* change it to something we can print as well */
	ostring(kcod2prc(c, outseq));

	/* if it isn't bound, bitch */
	if (unbindchar(c) == FALSE) {
		mlforce("[Key not bound]");
		return(FALSE);
	}
	update_scratch(BINDINGLIST_BufName, update_binding_list);
	return(TRUE);
}

static int
unbindchar(int c)		/* command key to unbind */
{
    register KBIND *kbp;	/* pointer into the command table */
    register KBIND *skbp;	/* saved pointer into the command table */

    if (!isspecial(c))
    {
	if (asciitbl[c])
	{
	    asciitbl[c] = 0;
	    return TRUE;
	}

	return FALSE;
    }

    /* check first entry in KeyBindings table */
    kbp = skbp = KeyBindings;
    if (kbp->k_code == c)
    {
	KeyBindings = kbp->k_link;
	free(kbp);
	return TRUE;
    }

    /* check KeyBindings */
    while (kbp != kbindtbl)
    {
	if (kbp->k_code == c)
	{
	    /* relink previous */
	    skbp->k_link = kbp->k_link;
	    free(kbp);
	    return TRUE;
	}

	skbp = kbp;
	kbp = kbp->k_link;
    }

    /* nope, check kbindtbl */
    skbp = 0;
    for (skbp = 0; kbp->k_cmd; kbp++)
    {
	if (!skbp && kbp->k_code == c)
	    skbp = kbp;
    }

    /* not found */
    if (!skbp)
	return FALSE;

    --kbp; /* backup to the last legit entry */
    if (skbp != kbp)
    {
	/* copy the last entry to the current one */
	skbp->k_code = kbp->k_code;
	skbp->k_cmd  = kbp->k_cmd;
    }

    /* null out the last one */
    kbp->k_code = 0;
    kbp->k_cmd = 0;

    return TRUE;
}

/* describe bindings bring up a fake buffer and list the key bindings
		   into it with view mode			*/

/* remember whether we last did "apropos" or "describe-bindings" */
static char *last_apropos_string;
static CMDFLAGS last_whichcmds;
static int append_to_binding_list;

/* ARGSUSED */
static int
update_binding_list(BUFFER *bp GCC_UNUSED)
{
	return liststuff(BINDINGLIST_BufName, append_to_binding_list,
		makebindlist, (int)last_whichcmds, (void *)last_apropos_string);
}

/* ARGSUSED */
int
desbind(int f GCC_UNUSED, int n GCC_UNUSED)
{
	last_apropos_string = (char *)0;
	last_whichcmds = 0;

	return update_binding_list((BUFFER *)0);
}

/* ARGSUSED */
int
desmotions(int f GCC_UNUSED, int n GCC_UNUSED)
{
	last_apropos_string = (char *)0;
	last_whichcmds = MOTION;
	return update_binding_list((BUFFER *)0);
}

/* ARGSUSED */
int
desopers(int f GCC_UNUSED, int n GCC_UNUSED)
{
	last_apropos_string = (char *)0;
	last_whichcmds = OPER;
	return update_binding_list((BUFFER *)0);
}

/* ARGSUSED */
int
desapro(int f GCC_UNUSED, int n GCC_UNUSED)	/* Apropos (List functions that match a substring) */
{
	register int    s;
	static char mstring[NSTRING];	/* string to match cmd names to */

	s = mlreply("Apropos string: ", mstring, sizeof(mstring));
	if (s != TRUE)
		return(s);

	last_apropos_string = mstring;
	last_whichcmds = 0;
	return update_binding_list((BUFFER *)0);
}

static char described_cmd[NLINE+1];	/* string to match cmd names to */

/* ARGSUSED */
int
desfunc(int f GCC_UNUSED, int n GCC_UNUSED)	/* describe-function */
{
	register int    s;
	char *fnp;

	/* force an exact match by strinc() later on from makefuncdesc() */
	described_cmd[0] = '^';

	fnp = kbd_engl("Describe function whose full name is: ",
							described_cmd+1);
	if (fnp == NULL || engl2fnc(fnp) == NULL) {
		s = no_such_function(fnp);
	} else {
		last_apropos_string = described_cmd;
		last_whichcmds = 0;
		append_to_binding_list = TRUE;
		s = update_binding_list((BUFFER *)0);
		append_to_binding_list = FALSE;
	}
	return s;
}

/* ARGSUSED */
int
deskey(int f GCC_UNUSED, int n GCC_UNUSED)	/* describe the command for a certain key */
{
	register int c;		/* key to describe */
	char outseq[NSTRING];	/* output buffer for command sequence */
	const NTAB *nptr;	/* name table pointer */
	int s;

	/* prompt the user to type us a key to describe */
	mlprompt("Describe the function bound to this key sequence: ");

	/* get the command sequence to describe
	   change it to something we can print as well */

	/* check to see if we are executing a command line */
	if (clexec) {
		char tok[NSTRING];
		macarg(tok);	/* get the next token */
		c = prc2kcod(tok);
		if (c < 0) {
			mlforce("[Illegal key-sequence \"%s\"]",tok);
			return(FALSE);
		}
	} else {
		c = kbd_seq_nomap();
		if (c < 0) {
			mlforce("[Not a bindable key-sequence]");
			return(FALSE);
		}
	}

	(void)kcod2prc(c, outseq);
	hst_append_s(outseq, FALSE); /* cannot replay this, but can see it */

	/* find the right ->function */
	if ((nptr = fnc2ntab(kcod2fnc(c))) == NULL) {
		mlwrite("Key sequence '%s' is not bound to anything.",
					outseq);
		return TRUE;
	}

	/* describe it */
	described_cmd[0] = '^';
	(void)strcpy(described_cmd + 1, nptr->n_name);
	last_apropos_string = described_cmd;
	last_whichcmds = 0;
	append_to_binding_list = TRUE;
	s = update_binding_list((BUFFER *)0);
	append_to_binding_list = FALSE;

	mlwrite("Key sequence '%s' is bound to function \"%s\"",
				outseq, nptr->n_name);

	return s;
}

/* returns a name in double-quotes */
static char *
quoted(char *dst, const char *src)
{
	return strcat(strcat(strcpy(dst, "\""), src), "\"");
}

/* returns the number of columns used by the given string */
static unsigned
converted_len(register char *buffer)
{
	register unsigned len = 0, c;
	while ((c = *buffer++) != EOS) {
		if (c == '\t')
			len |= 7;
		len++;
	}
	return len;
}

/* force the buffer to a tab-stop if needed */
static char *
to_tabstop(char *buffer)
{
	register unsigned cpos = converted_len(buffer);
	if (cpos & 7)
		(void)strcat(buffer, "\t");
	return skip_string(buffer);
}

/* convert a key binding, padding to the next multiple of 8 columns */
static void
convert_kcode(int c, char *buffer)
{
	(void)kcod2prc(c, to_tabstop(buffer));
}

#if OPT_NAMEBST
struct bindlist_data {
    int mask;		/* oper/motion mask */
    int min;		/* minimum key length */
    char *apropos;	/* key check */
};

static int
btree_walk(BI_NODE *node, int (*func)(BI_NODE *, const void *),
	   const void *data)
{
    if (node)
    {
	if (btree_walk(BI_LEFT(node), func, data))
	    return 1;

	if (BI_KEY(node))
	    func(node, data);

	if (btree_walk(BI_RIGHT(node), func, data))
	    return 1;
    }

    return 0;
}

static int
clearflag_func(BI_NODE *n, const void *d GCC_UNUSED)
{
    n->value.n_flags &= ~NBST_DONE;
    return 0;
}

static int
addsynonym_func(BI_NODE *node, const void *d)
{
    const CMDFUNC *func = (const CMDFUNC *)d;
    static char outseq[NLINE];	/* output buffer for text */

    if (node->value.n_cmd == func &&
	!(node->value.n_flags & NBST_DONE))
    {
	strcpy(outseq, "  or\t");
	quoted(outseq+5, BI_KEY(node));
	if (!addline(curbp, outseq, -1))
	    return 1;
    }

    return 0;
}

static int
makebind_func(BI_NODE *node, const void *d)
{
    const struct bindlist_data *data = (const struct bindlist_data *)d;
    static KBIND *kbp;		/* pointer into a key binding table */
    static char outseq[NLINE];	/* output buffer for keystroke sequence */
    const CMDFUNC *cmd = node->value.n_cmd;
    register int i;

    /* has this been listed? */
    if (node->value.n_flags & NBST_DONE)
	return 0;

    /* are we interested in this type of command? */
    if (data->mask && !(cmd->c_flags & data->mask))
	return 0;

    /* try to avoid alphabetizing by the real short names */
    if (data->min && (int) strlen(BI_KEY(node)) <= data->min)
	return 0;

    /* if we are executing an apropos command
       and current string doesn't include the search string */
    if (data->apropos && !strinc(BI_KEY(node), data->apropos))
	return 0;

    /* add in the command name */
    quoted(outseq, BI_KEY(node));
    while (converted_len(outseq) < 32)
	strcat(outseq, "\t");

    /* look in the simple ascii binding table first */
    for (i = 0; i < N_chars; i++)
	if (asciitbl[i] == cmd)
	    convert_kcode(i, outseq);

    /* then look in the multi-key table */
#if OPT_REBIND
    for (kbp = KeyBindings; kbp != kbindtbl; kbp = kbp->k_link)
    {
	if (kbp->k_cmd == cmd)
	    convert_kcode(kbp->k_code, outseq);
    }
#endif
    for (kbp = kbindtbl; kbp->k_cmd; kbp++)
	if (kbp->k_cmd == cmd)
	    convert_kcode(kbp->k_code, outseq);

    /* dump the line */
    if (!addline(curbp, outseq, -1))
	    return 1;

    node->value.n_flags |= NBST_DONE;

    /* add synonyms */
    btree_walk(&namebst.head, addsynonym_func, cmd);

#if OPT_ONLINEHELP
    if (cmd->c_help && *cmd->c_help)
	lsprintf(outseq, "  (%s %s )",
	    (cmd->c_flags & MOTION) ? "motion: " :
	    (cmd->c_flags & OPER)   ? "operator: " : "",
	    cmd->c_help);
	else
	    lsprintf(outseq, "  ( no help for this command )");

    if (!addline(curbp, outseq, -1))
	return 1;

    if (cmd->c_flags & GLOBOK)
	if (!addline(curbp, "  (may follow global command)", -1))
	    return 1;
#endif
    /* blank separator */
    if (!addline(curbp, "", -1))
	return 1;

    return 0;
}

/* build a binding list (limited or full) */
/* ARGSUSED */
static void
makebindlist(
int whichmask,
void *mstring)		/* match string if partial list, NULL to list all */
{
    struct bindlist_data data;

    data.mask = whichmask;
    data.min = SHORT_CMD_LEN;
    data.apropos = (char *)mstring;

    /* let us know this is in progress */
    mlwrite("[Building binding list]");

    /* clear the NBST_DONE flag */
    btree_walk(&namebst.head, clearflag_func, 0);

    /* create binding list */
    if (btree_walk(&namebst.head, makebind_func, &data))
	return;

    /* catch entries with no synonym > SHORT_CMD_LEN */
    data.min = 0;
    if (btree_walk(&namebst.head, makebind_func, &data))
	return;

    mlerase();	/* clear the message line */
}
#else /* OPT_NAMEBST */
/* fully describe a function into the current buffer, given a pointer to
 * its name table entry */
static int
makefuncdesc(int j, char *listed)
{
	register KBIND *kbp;	/* pointer into a key binding table */
	int i;
	const CMDFUNC *cmd = nametbl[j].n_cmd;
	char outseq[NLINE];	/* output buffer for keystroke sequence */

	/* add in the command name */
	(void)quoted(outseq, nametbl[j].n_name);
	while (converted_len(outseq) < 32)
		(void)strcat(outseq, "\t");

	/* look in the simple ascii binding table first */
	for (i = 0; i < N_chars; i++)
		if (asciitbl[i] == cmd)
			convert_kcode(i, outseq);

	/* then look in the multi-key table */
#if OPT_REBIND
	for (kbp = KeyBindings; kbp != kbindtbl; kbp = kbp->k_link) {
		if (kbp->k_cmd == cmd)
			convert_kcode(kbp->k_code, outseq);
	}
#endif
	for (kbp = kbindtbl; kbp->k_cmd; kbp++)
		if (kbp->k_cmd == cmd)
			convert_kcode(kbp->k_code, outseq);

	/* dump the line */
	if (!addline(curbp,outseq,-1))
		return FALSE;

	/* then look for synonyms */
	(void)strcpy(outseq, "  or\t");
	for (i = 0; nametbl[i].n_name != 0; i++) {
		/* if it's the one we're on, skip */
		if (i == j)
			continue;
		/* if it's already been listed, skip */
		if (listed[i])
			continue;
		/* if it's not a synonym, skip */
		if (nametbl[i].n_cmd != cmd)
			continue;
		(void)quoted(outseq+5, nametbl[i].n_name);
		if (!addline(curbp,outseq,-1))
			return FALSE;
	}

#if OPT_ONLINEHELP
	if (cmd->c_help && cmd->c_help[0])
		(void)lsprintf(outseq,"  (%s %s )",
		(cmd->c_flags & MOTION) ? "motion: " :
			(cmd->c_flags & OPER) ? "operator: " : "",
		cmd->c_help);
	else
		(void)lsprintf(outseq,"  ( no help for this command )");
	if (!addline(curbp,outseq,-1))
		return FALSE;
	if (cmd->c_flags & GLOBOK) {
		if (!addline(curbp,"  (may follow global command)",-1))
			return FALSE;
	}
#endif
	/* blank separator */
	if (!addline(curbp,"",-1))
		return FALSE;

	return TRUE;
}

/* build a binding list (limited or full) */
/* ARGSUSED */
static void
makebindlist(
int whichmask,
void *mstring)		/* match string if partial list, NULL to list all */
{
	int pass;
	int j;
	int ok = TRUE;		/* reset if out-of-memory, etc. */
	char *listed = typecallocn(char, (ALLOC_T)nametblsize);

	if (listed == 0) {
		(void)no_memory(BINDINGLIST_BufName);
		return;
	}

	/* let us know this is in progress */
	mlwrite("[Building binding list]");

	/* build the contents of this window, inserting it line by line */
	for (pass = 0; pass < 2; pass++) {
	    for (j = 0; nametbl[j].n_name != 0; j++) {

		/* if we've already described this one, move on */
		if (listed[j])
			continue;

		/* are we interested in this type of command? */
		if (whichmask && !(nametbl[j].n_cmd->c_flags & whichmask))
			continue;

		/* try to avoid alphabetizing by the real short names */
		if (pass == 0 && (int)strlen(nametbl[j].n_name) <= SHORT_CMD_LEN)
			continue;

		/* if we are executing an apropos command
		   and current string doesn't include the search string */
		if (mstring
		 && (strinc(nametbl[j].n_name, (char *)mstring) == FALSE))
			continue;

		ok = makefuncdesc(j, listed);
		if (!ok)
			break;

		listed[j] = TRUE; /* mark it as already listed */
	    }
	}

	if (ok)
		mlerase();	/* clear the message line */
	free(listed);
}
#endif /* OPT_NAMEBST */

/* much like the "standard" strstr, but if the substring starts
	with a '^', we discard it and force an exact match.  */
static int
strinc(			/* does source include sub? */
const char *sourc,	/* string to search in */
const char *sub)	/* substring to look for */
{
	const char *sp;		/* ptr into source */
	const char *nxtsp;	/* next ptr into source */
	const char *tp;		/* ptr into substring */
	int exact = (*sub == '^');

	if (exact)
		sub++;

	/* for each character in the source string */
	sp = sourc;
	while (*sp) {
		tp = sub;
		nxtsp = sp;

		/* is the substring here? */
		while (*tp) {
			if (*nxtsp++ != *tp)
				break;
			tp++;
		}

		if ((*tp == EOS) && (!exact || *nxtsp == EOS))
			return(TRUE);

		if (exact) /* we only get one chance */
			break;

		/* no, onward */
		sp++;
	}
	return(FALSE);
}

#endif /* OPT_REBIND */


/*	Look up the existence of a file along the normal or PATH
	environment variable. Look first in the HOME directory if
	asked and possible
*/

char *
flook(
char *fname,		/* base file name to search for */
UINT hflag)		/* Look in the HOME environment variable first? */
{
	register char *home;	/* path to home directory */
#if ENVFUNC && OPT_PATHLOOKUP
	register const char *path; /* environmental PATH variable */
#endif
	static char fspec[NSTRING];	/* full path spec to search */
#if SYS_VMS
	register char *sp;	/* pointer into path spec */
	static TBUFF *myfiles;
#endif
	int	mode = (hflag & (FL_EXECABLE|FL_WRITEABLE|FL_READABLE));

	/* take care of special cases */
	if (!fname || !fname[0] || isSpace(fname[0]))
		return NULL;
	else if (isShellOrPipe(fname))
		return fname;

	if (hflag & FL_HERE) {
		if (ffaccess(fname, mode)) {
			return(fname);
		}
	}

#if ENVFUNC

	if (hflag & FL_HOME) {
		home = getenv("HOME");
		if (home != NULL) {
			/* try home dir file spec */
			if (ffaccess(pathcat(fspec,home,fname), mode)) {
				return(fspec);
			}
		}
	}

#endif	/* ENVFUNC */

	if (hflag & FL_EXECDIR) { /* is it where we found the executable? */
		if (exec_pathname
		 && exec_pathname[0] != EOS
		 && ffaccess(pathcat(fspec, exec_pathname, fname), mode))
			return(fspec);
	}

	if (hflag & FL_TABLE) {
		/* then look it up via the table method */
		path = startup_path;
		while ((path = parse_pathlist(path, fspec)) != 0) {
			if (ffaccess(pathcat(fspec, fspec, fname), mode)) {
				return(fspec);
			}
		}
	}

	if (hflag & FL_PATH) {

#if ENVFUNC
#if OPT_PATHLOOKUP
		/* then look along $PATH */
#if SYS_VMS
		/* On VAX/VMS, the PATH environment variable is only the
		 * current-dir.  Fake up an acceptable alternative.
		 */
		if (!tb_length(myfiles)) {
			char	mypath[NFILEN];

			(void)strcpy(mypath, prog_arg);
			if ((sp = vms_pathleaf(mypath)) == mypath)
				(void)strcpy(mypath, current_directory(FALSE));
			else
				*sp = EOS;

			if (!tb_init(&myfiles, EOS)
			 || !tb_sappend(&myfiles, mypath)
			 || !tb_sappend(&myfiles, ",SYS$SYSTEM:,SYS$LIBRARY:")
			 || !tb_append(&myfiles, EOS))
			return NULL;
		}
		path = tb_values(myfiles);
#else	/* UNIX or MSDOS */
		path = getenv("PATH");	/* get the PATH variable */
#endif
		while ((path = parse_pathlist(path, fspec)) != 0) {
			if (ffaccess(pathcat(fspec, fspec, fname), mode)) {
				return(fspec);
			}
		}
#endif	/* OPT_PATHLOOKUP */
#endif	/* ENVFUNC */

	}

	return NULL;	/* no such luck */
}

/* translate a keycode to its binding-string */
char *
kcod2pstr(
int c,		/* sequence to translate */
char *seq)	/* destination string for sequence */
{
	seq[0] = (char)kcod2escape_seq(c, &seq[1]);
	return seq;
}

/* Translate a 16-bit keycode to a string that will replay into the same
 * code.
 */
int
kcod2escape_seq (
int	c,
char *	ptr)
{
	char	*base = ptr;

	/* ...just for completeness */
	if (c & CTLA)		*ptr++ = (char)cntl_a;
	else if (c & CTLX)	*ptr++ = (char)cntl_x;
	else if (c & SPEC)	*ptr++ = (char)poundc;
#if SYS_WINNT
#define ALTPLUS   "Alt+"
#define CTRLPLUS  "Ctrl+"
#define SHIFTPLUS "Shift+"
#define W32INSERT "Insert"
	else if (c & W32_KEY)
	{
		if (c & W32_SHIFT)
		{
			strcpy(ptr, SHIFTPLUS);
			ptr += sizeof(SHIFTPLUS) - 1;
		}
		if (c & W32_CTRL)
		{
			strcpy(ptr, CTRLPLUS);
			ptr += sizeof(CTRLPLUS) - 1;
		}
		if (c & W32_ALT)
		{
			strcpy(ptr, ALTPLUS);
			ptr += sizeof(ALTPLUS) - 1;
		}
		c &= W32_NOMOD;
		if (c == VK_INSERT)
		{
			strcpy(ptr, W32INSERT);
			ptr += sizeof(W32INSERT) - 1;
		}
		else
			*ptr++ = c;    /* Pickup <modifier>+...<single_char> */
		*ptr = EOS;
		return (int)(ptr - base);
	}
#endif
	*ptr++ = (char)c;
	*ptr = EOS;
	return (int)(ptr - base);
}


/* translates a binding string into printable form */
#if OPT_REBIND
static char *
bytes2prc(char *dst, char *src, int n)
{
	char	*base = dst;
	register int	c;
	register const char *tmp;

	for ( ; n != 0; dst++, src++, n--) {

		c = *src;

		tmp = 0;

		if (c & HIGHBIT) {
			*dst++ = 'M';
			*dst++ = '-';
			c &= ~HIGHBIT;
		}
		if (c == ' ') {
			tmp = "<space>";
		} else if (isCntrl(c)) {
			*dst++ = '^';
			*dst = tocntrl(c);
		} else {
			*dst = (char)c;
		}

		if (tmp != 0) {
			while ((*dst++ = *tmp++) != EOS)
				;
			dst -= 2;	/* point back to last nonnull */
		}

		if (n > 1) {
			*++dst = '-';
		}
	}
	*dst = EOS;
	return base;
}

/* translate a 10-bit keycode to its printable name (like "M-j")  */
static char *
kcod2prc(
int c,		/* sequence to translate */
char *seq)	/* destination string for sequence */
{
	char	temp[NSTRING];
	(void)kcod2pstr(c,temp);
#if SYS_WINNT
	if (c & W32_KEY)
	{
		/* Translation is complete, by defn. */

		strcpy(seq, temp + 1);
		return (seq);
	}
#endif
	return bytes2prc(seq, temp + 1, (int)*temp);
}
#endif


/* kcode2kbind: translate a 10-bit key-binding to the table-pointer
 */
static KBIND *
kcode2kbind(register int code)
{
	register KBIND	*kbp;	/* pointer into a binding table */

#if OPT_REBIND
	for (kbp = KeyBindings; kbp != kbindtbl; kbp = kbp->k_link) {
		if (kbp->k_code == code)
			return kbp;
	}
#endif
	for (kbp = kbindtbl; kbp->k_cmd; kbp++) {
		if (kbp->k_code == code)
			return kbp;
	}
	return 0;
}

/* kcod2fnc:  translate a 10-bit keycode to a function pointer */
/*	(look a key binding up in the binding table)		*/
const CMDFUNC *
kcod2fnc(
int c)	/* key to find what is bound to it */
{
	if (isspecial(c)) {
		register KBIND *kp = kcode2kbind(c);
		return (kp != 0) ? kp->k_cmd : 0;
	}
	return asciitbl[c];
}

/* fnc2kcod: translate a function pointer to a keycode */
int
fnc2kcod(const CMDFUNC *f)
{
	register KBIND *kbp;
	register int	c;

	for (c = 0; c < N_chars; c++)
		if (f == asciitbl[c])
			return c;

#if OPT_REBIND
	for (kbp = KeyBindings; kbp != kbindtbl; kbp = kbp->k_link) {
		if (kbp->k_cmd == f)
			return kbp->k_code;
	}
#endif
	for (kbp = kbindtbl; kbp->k_cmd != 0; kbp++) {
		if (kbp->k_cmd == f)
			return kbp->k_code;
	}

	return -1;	/* none found */
}

/* fnc2pstr: translate a function pointer to a pascal-string that a user
	could enter.  returns a pointer to a static array */
#if DISP_X11
char *
fnc2pstr(const CMDFUNC *f)
{
	register int	c;
	static char seq[10];

	c = fnc2kcod(f);

	if (c == -1)
		return NULL;

	return kcod2pstr(c, seq);
}
#endif

/* fnc2engl: translate a function pointer to the english name for
		that function
*/
#if OPT_EVAL || OPT_REBIND
static const NTAB *
fnc2ntab(const CMDFUNC *cfp)
{
	register const NTAB *nptr;	/* pointer into the name table */
	register const NTAB *shortnptr = NULL; /* pointer into the name table */

	/* skim through the table, looking for a match */
	for (nptr = nametbl; nptr->n_cmd; nptr++) {
		if (nptr->n_cmd == cfp) {
			/* if it's a long name, return it */
			if ((int)strlen(nptr->n_name) > SHORT_CMD_LEN)
				return nptr;
			/* remember the first short name, in case there's
				no long name */
			if (!shortnptr)
				shortnptr = nptr;
		}
	}
	if (shortnptr)
		return shortnptr;

	return NULL;
}

static char *
fnc2engl(const CMDFUNC *cfp)
{
	register const NTAB *nptr = fnc2ntab(cfp);
	return nptr ? nptr->n_name : 0;
}

#endif

/* engl2fnc: match name to a function in the names table
	translate english name to function pointer
 		 return any match or NULL if none
 */
#define BINARY_SEARCH_IS_BROKEN 0
#if OPT_NAMEBST
const CMDFUNC *
engl2fnc(const char *fname)	/* name to attempt to match */
{
	BI_NODE *n = btree_pmatch(BI_RIGHT(&namebst.head), TRUE, fname);

	if (n == NULL) return NULL;
	else return n->value.n_cmd;
}
#else
#if BINARY_SEARCH_IS_BROKEN  /* then use the old linear look-up */
const CMDFUNC *
engl2fnc(const char *fname)	/* name to attempt to match */
{
	register NTAB *nptr;	/* pointer to entry in name binding table */
	register SIZE_T len = strlen(fname);

	if (len != 0) {	/* scan through the table, returning any match */
		nptr = nametbl;
		while (nptr->n_cmd != NULL) {
			if (strncmp(fname, nptr->n_name, len) == 0)
				return nptr->n_cmd;
			++nptr;
		}
	}
	return NULL;
}
#else
/* this runs 10 times faster for 'nametbl[]' */
const CMDFUNC *
engl2fnc(const char *fname)	/* name to attempt to match */
{
	int lo, hi, cur;
	int r;
	register SIZE_T len = strlen(fname);

	if (len == 0)
		return NULL;

	/* scan through the table, returning any match */
	lo = 0;
	hi = nametblsize - 2;	/* don't want last entry -- it's NULL */

	while (lo <= hi) {
		cur = (lo + hi) >> 1;
		if ((r = strncmp(fname, nametbl[cur].n_name, len)) == 0) {
			/* Now find earliest matching entry */
			while (cur > lo
			    && strncmp(fname, nametbl[cur-1].n_name, len) == 0)
				cur--;
			return nametbl[cur].n_cmd;

		} else if (r > 0) {
			lo = cur+1;
		} else {
			hi = cur-1;
		}
	}
	return NULL;
}
#endif	/* binary vs linear */
#endif  /* OPT_NAMEBST */

/* prc2kcod: translate printable code to 10 bit keycode */
#if OPT_EVAL || OPT_REBIND
static int
prc2kcod(
const char *kk)		/* name of key to translate to Command key form */
{
	register UINT c;	/* key sequence to return */
	register UINT pref = 0;	/* key prefixes */
	register int len = strlen(kk);
	register const UCHAR *k = (const UCHAR *)kk;

	if (len > 3 && *(k+2) == '-') {
		if (*k == '^') {
			if (isCntrl(cntl_a) && *(k+1) == toalpha(cntl_a))
				pref = CTLA;
			if (isCntrl(cntl_x) && *(k+1) == toalpha(cntl_x))
				pref = CTLX;
			if (isCntrl(poundc) && *(k+1) == toalpha(poundc))
				pref = SPEC;
		} else if (!strncmp((const char *)k, "FN", (SIZE_T)2)) {
			pref = SPEC;
		}
		if (pref != 0)
			k += 3;
	} else if (len > 2 && !strncmp((const char *)k, "M-", (SIZE_T)2)) {
		pref = HIGHBIT;
		k += 2;
	} else if (len > 1) {
		if (*k == cntl_a)
			pref = CTLA;
		else if (*k == cntl_x)
			pref = CTLX;
		else if (*k == poundc)
			pref = SPEC;
		if (pref != 0) {
			k++;
			if (len > 2 && *k == '-')
				k++;
		}
	}

	/* a control char? */
	if (*k == '^' && *(k+1) != EOS) {
		c = *(k+1);
		if (isLower(c)) c = toUpper(c);
		c = tocntrl(c);
		k += 2;
	} else {		/* any single char, control or not */
		c = *k++;
	}

	if (*k != EOS)		/* we should have eaten the whole thing */
		return -1;

	return (int)(pref|c);
}
#endif


#if OPT_EVAL
/* translate printable code (like "M-r") to english command name */
const char *
prc2engl(		/* string key name to binding name.... */
const char *skey)	/* name of key to get binding for */
{
	const char *bindname;
	int c;

	c = prc2kcod(skey);
	if (c < 0)
		return "ERROR";

	bindname = fnc2engl(kcod2fnc(c));
	if (bindname == NULL)
		bindname = "ERROR";

	return bindname;
}
#endif

/*
 * Get an english command name from the user
 */
char *
kbd_engl(
const char *prompt,	/* null pointer to splice calls */
char *buffer)
{
	if (kbd_engl_stat(prompt, buffer, 0) == TRUE)
		return buffer;
	return NULL;
}

/* sound the alarm! */
void
kbd_alarm(void)
{
	TRACE(("BEEP\n"))

	if (global_g_val(GMDERRORBELLS)) {
		TTbeep();
		TTflush();
	}
	warnings++;
}

/* put a character to the keyboard-prompt */
void
kbd_putc(int c)
{
	BUFFER *savebp;
	WINDOW *savewp;
	MARK savemk;

	beginDisplay();
	savebp = curbp;
	savewp = curwp;
	curbp = bminip;
	curwp = wminip;
	savemk = MK;
	MK = DOT;
	if ((kbd_expand <= 0) && isreturn(c)) {
		kbd_erase_to_end(0);
	} else {
		if ((kbd_expand < 0) && (c == '\t')) {
			(void)linsert(1,' ');
		} else {
			(void)linsert(1,c);
		}
		if (! is_header_line(DOT,curbp) && !is_at_end_of_line(DOT))
			forwchar(TRUE,1); /* END OF LINE HACK */
#ifdef DEBUG
		TRACE(("mini:%2d:%.*s\n", llength(DOT.l), llength(DOT.l), DOT.l->l_text));
#endif
	}
	curbp = savebp;
	curwp = savewp;
	MK = savemk;
	endofDisplay();
}

/* put a string to the keyboard-prompt */
void
kbd_puts(const char *s)
{
	while (*s)
		kbd_putc(*s++);
}

/* erase a character from the display by wiping it out */
void
kbd_erase(void)
{
	BUFFER *savebp;
	WINDOW *savewp;
	MARK savemk;

	if (!disinp)
	    return;

	beginDisplay();
	savebp = curbp;
	savewp = curwp;
	curbp = bminip;
	curwp = wminip;
	savemk = MK;
	MK = DOT;
	if (DOT.o > 0) {
		DOT.o -= 1;
		ldelete(1, FALSE);
	}
#ifdef DEBUG
	TRACE(("MINI:%2d:%.*s\n", llength(DOT.l), llength(DOT.l), DOT.l->l_text));
#endif
	curbp = savebp;
	curwp = savewp;
	MK = savemk;
	endofDisplay();
}

void
kbd_erase_to_end(int column)
{
	BUFFER *savebp;
	WINDOW *savewp;
	MARK savemk;

	if (!disinp)
	    return;

	beginDisplay();
	savebp = curbp;
	savewp = curwp;
	curbp = bminip;
	curwp = wminip;
	savemk = MK;
	MK = DOT;
	if (llength(DOT.l) > 0) {
		DOT.o = column;
		ldelete(llength(DOT.l) - DOT.o, FALSE);
		TRACE(("NULL:%2d:%.*s\n", llength(DOT.l), llength(DOT.l), DOT.l->l_text));
	}
	curbp = savebp;
	curwp = savewp;
	MK = savemk;
	endofDisplay();
}


#if OPT_CASELESS
static int
cs_strcmp(
int case_insensitive,
const char *s1,
const char *s2)
{
	if (case_insensitive)
		return stricmp(s1, s2);
	return strcmp(s1, s2);
}

static int
cs_strncmp(
int case_insensitive,
const char *s1,
const char *s2,
SIZE_T n)
{
	if (case_insensitive)
		return strnicmp(s1, s2, n);
	return strncmp(s1, s2, n);
}
#endif	/* OPT_CASELESS */

/* definitions for name-completion */
#define	NEXT_DATA(p)	((p)+size_entry)
#define	PREV_DATA(p)	((p)-size_entry)

#ifdef	lint
static	/*ARGSUSED*/
const char *	THIS_NAME(const char *p) { return 0; }
#else
#define	THIS_NAME(p)	(*TYPECAST(const char *const,p))
#endif
#define	NEXT_NAME(p)	THIS_NAME(NEXT_DATA(p))

/*
 * Scan down until we no longer match the current input, or reach the end of
 * the symbol table.
 */
/*ARGSUSED*/
static const char *
skip_partial(
int	case_insensitive GCC_UNUSED,
char	*buf,
SIZE_T	len,
const char *table,
SIZE_T	size_entry)
{
	register const char * next = NEXT_DATA(table);
	register const char *	sp;

	while ((sp = THIS_NAME(next)) != 0) {
		if (StrNcmp(buf, sp, len) != 0)
			break;
		next = NEXT_DATA(next);
	}
	return next;
}

/*
 * Shows a partial-match.  This is invoked in the symbol table at a partial
 * match, and the user wants to know what characters could be typed next.
 * If there is more than one possibility, they are shown in square-brackets.
 * If there is only one possibility, it is shown in curly-braces.
 */
static void
show_partial(
int	case_insensitive,
char	*buf,
SIZE_T	len,
const char *table,
SIZE_T	size_entry)
{
	register const char *next = skip_partial(case_insensitive, buf, len, table, size_entry);
	register const char *last = PREV_DATA(next);
	register int	c;

	if (THIS_NAME(table)[len] == THIS_NAME(last)[len]) {
		kbd_putc('{');
		while ((c = THIS_NAME(table)[len]) != 0) {
			if (c == THIS_NAME(last)[len]) {
				kbd_putc(c);
				len++;
			} else
				break;
		}
		kbd_putc('}');
	}
	if (next != NEXT_DATA(table)) {
		c = TESTC;	/* shouldn't be in the table! */
		kbd_putc('[');
		while (table != next) {
			register const char *sp = THIS_NAME(table);
			if (c != sp[len]) {
				c = sp[len];
				kbd_putc(c ? c : '$');
			}
			table = NEXT_DATA(table);
		}
		kbd_putc(']');
	}
	kbd_flush();
}

#if OPT_POPUPCHOICE
/*
 * makecmpllist is called from liststuff to display the possible completions.
 */
struct compl_rec {
    char *buf;
    SIZE_T len;
    const char *table;
    SIZE_T size_entry;
};

#ifdef lint
#define c2ComplRec(c) ((struct compl_rec *)0)
#else
#define c2ComplRec(c) ((struct compl_rec *)c)
#endif

/*ARGSUSED*/
static void
makecmpllist(
    int case_insensitive,
    void *cinfop)
{
    char * buf		= c2ComplRec(cinfop)->buf;
    SIZE_T len		= c2ComplRec(cinfop)->len;
    const char * first	= c2ComplRec(cinfop)->table;
    SIZE_T size_entry	= c2ComplRec(cinfop)->size_entry;
    register const char *last = skip_partial(case_insensitive, buf, len, first, size_entry);
    register const char *p;
    SIZE_T maxlen;
    int slashcol;
    int cmpllen;
    int cmplcols;
    int cmplrows;
    int nentries;
    int i, j;

    for (p = NEXT_DATA(first), maxlen = strlen(THIS_NAME(first));
         p != last;
	 p = NEXT_DATA(p)) {
	SIZE_T l = strlen(THIS_NAME(p));
	if (l > maxlen)
	    maxlen = l;
    }

    slashcol = (int)(pathleaf(buf) - buf);
    if (slashcol != 0) {
        char b[NLINE];
        (void)strncpy(b, buf, (SIZE_T)slashcol);
        (void)strncpy(&b[slashcol], &(THIS_NAME(first))[slashcol],
			(len-slashcol));
        b[slashcol+(len-slashcol)] = EOS;
        bprintf("Completions prefixed by %s:\n", b);
    }

    cmplcols = term.t_ncol / (maxlen - slashcol + 1);

    if (cmplcols == 0)
	cmplcols = 1;

    nentries = (int)(last - first) / size_entry;
    cmplrows = nentries / cmplcols;
    cmpllen  = term.t_ncol / cmplcols;
    if (cmplrows * cmplcols < nentries)
	cmplrows++;

    for (i = 0; i < cmplrows; i++) {
	for (j = 0; j < cmplcols; j++) {
	    int idx = cmplrows * j + i;
	    if (idx < nentries) {
		const char *s = THIS_NAME(first+(idx*size_entry))+slashcol;
		if (j == cmplcols-1)
		    bprintf("%s\n", s);
		else
		    bprintf("%*s", cmpllen, s);
	    }
	    else {
		bprintf("\n");
		break;
	    }
	}
    }
}

/*
 * Pop up a window and show the possible completions.
 */
static void
show_completions(
int	case_insensitive,
char	*buf,
SIZE_T	len,
const char *table,
SIZE_T	size_entry)
{
    struct compl_rec cinfo;
    BUFFER *bp;
    int alreadypopped = 0;

    /*
     * Find out if completions buffer exists; so we can take the time to
     * shrink/grow the window to the latest size.
     */
    if ((bp = find_b_name(COMPLETIONS_BufName)) != NULL) {
	alreadypopped = (bp->b_nwnd != 0);
    }

    cinfo.buf = buf;
    cinfo.len = len;
    cinfo.table = table;
    cinfo.size_entry = size_entry;
    liststuff(COMPLETIONS_BufName, FALSE, makecmpllist, case_insensitive, (void *) &cinfo);

    if (alreadypopped)
	shrinkwrap();

    (void)update(TRUE);
}

/*
 * Scroll the completions window wrapping around back to the beginning
 * of the buffer once it has been completely scrolled.  If the completions
 * buffer is missing for some reason, we will call show_completions to pop
 * it (back) up.
 */
static void
scroll_completions(
    int		case_insensitive,
    char	*buf,
    SIZE_T	len,
    const char	*table,
    SIZE_T	size_entry)
{
    BUFFER *bp = find_b_name(COMPLETIONS_BufName);
    if (bp == NULL)
	show_completions(case_insensitive, buf, len, table, size_entry);
    else {
	LINEPTR lp;
	swbuffer(bp);
	(void)gotoeos(FALSE, 1);
	lp = DOT.l;
	(void)forwhpage(FALSE, 1);
	if (lp == DOT.l)
	    (void)gotobob(FALSE, 0);
	(void)update(TRUE);
    }
}

void
popdown_completions(void)
{
    BUFFER *bp;
    if ((bp = find_b_name(COMPLETIONS_BufName)) != NULL)
	zotwp(bp);
}
#endif /* OPT_POPUPCHOICE */

/*
 * Attempt to partial-complete the string, char at a time
 */
static SIZE_T
fill_partial(
int	case_insensitive GCC_UNUSED,
char	*buf,
SIZE_T	pos,
const char *first,
const char *last,
SIZE_T	size_entry)
{
	register const char *p;
	register SIZE_T	n = pos;
	const char *this_name = THIS_NAME(first);

#if 0 /* case insensitive reply correction doesn't work reliably yet */
	if (!clexec && case_insensitive) {
		int spos = pos;

		while (spos > 0 && buf[spos - 1] != SLASHC) {
			kbd_erase();
			spos--;
		}
		while (spos < pos) {
			kbd_putc(this_name[spos]);
			spos++;
		}
	}
#endif

	for_ever {
		buf[n] = this_name[n];	/* add the next char in */
		buf[n+1] = EOS;

		/* scan through the candidates */
		for (p = NEXT_DATA(first); p != last; p = NEXT_DATA(p)) {
			if (StrNcmp(&THIS_NAME(p)[n], &buf[n], 1) != 0) {
				buf[n] = EOS;
				if (n == pos
#if OPT_POPUPCHOICE
# if OPT_ENUM_MODES
				 && !global_g_val(GVAL_POPUP_CHOICES)
# else
				 && !global_g_val(GMDPOPUP_CHOICES)
# endif
#endif
				)
					kbd_alarm();
				kbd_flush(); /* force out alarm or partial completion */
				return n;
			}
		}

		if (!clexec)
			kbd_putc(buf[n]); /* add the character */
		n++;
	}
}

static	int	testcol;	/* records the column when TESTC is decoded */
#if OPT_POPUPCHOICE
/*
 * cmplcol is used to record the column number (on the message line) after
 * name completion.  Its value is used to decide whether or not to display
 * a completion list if the name completion character (tab) is pressed
 * twice in succession.  Once the completion list has been displayed, its
 * value will be changed to the additive inverse of the column number in
 * order to determine whether to scroll if tab is pressed yet again.  We
 * assume that 0 will never be a valid column number.  So long as we always
 * display some sort of prompt prior to reading from the message line, this
 * is a good assumption.
 */
static	int	cmplcol = 0;
#endif

/*
 * Initializes the name-completion logic
 */
void
kbd_init(void)
{
	testcol = -1;
}

/*
 * Returns the current length of the minibuffer
 */
int
kbd_length(void)
{
	if (wminip != 0
	 && wminip->w_dot.l != 0
	 && llength(wminip->w_dot.l) > 0)
		return llength(wminip->w_dot.l);
	return 0;
}

/*
 * Erases the display that was shown in response to TESTC
 */
void
kbd_unquery(void)
{
	beginDisplay();
#if OPT_POPUPCHOICE
	if (cmplcol != kbd_length() && -cmplcol != kbd_length())
		cmplcol = 0;
#endif
	if (testcol >= 0) {
		while (kbd_length() > testcol)
			kbd_erase();
		kbd_flush();
		testcol = -1;
	}
	endofDisplay();
}

/*
 * This is invoked to find the closest name to complete from the current buffer
 * contents.
 */
int
kbd_complete(
int	case_insensitive,
int	c,		/* TESTC, NAMEC or isreturn() */
char	*buf,
unsigned *pos,
const char *table,
SIZE_T	size_entry)
{
	register SIZE_T cpos = *pos;
	register const char *nbp; /* first ptr to entry in name binding table */
	int status = FALSE;
#if OPT_POPUPCHOICE
# if OPT_ENUM_MODES
	int gvalpopup_choices = global_g_val(GVAL_POPUP_CHOICES);
# else
	int gvalpopup_choices = global_g_val(GMDPOPUP_CHOICES);
# endif
#endif

	kbd_init();		/* nothing to erase */
	buf[cpos] = EOS;	/* terminate it for us */
	nbp = table;		/* scan for matches */

	while (THIS_NAME(nbp) != NULL) {
		if (StrNcmp(buf,  THIS_NAME(nbp), strlen(buf)) == 0) {
			testcol = kbd_length();
			/* a possible match! exact? no more than one? */
#if OPT_POPUPCHOICE
			if (!clexec && c == NAMEC && cmplcol == -kbd_length()) {
				scroll_completions(case_insensitive, buf, cpos, nbp, size_entry);
				return FALSE;
			}
#endif
			if (c == TESTC) {
				show_partial(case_insensitive, buf, cpos, nbp, size_entry);
			}
			else if (Strcmp(buf,  THIS_NAME(nbp)) == 0 || /* exact? */
				NEXT_NAME(nbp) == NULL ||
				StrNcmp(buf, NEXT_NAME(nbp), strlen(buf)) != 0)
			{
				/* exact or only one like it.  print it */
				if (!clexec) {
#if 0 /* case insensitive reply correction doesn't work reliably yet */
					if (case_insensitive) {
						int spos = cpos;

						while (spos > 0 && buf[spos - 1] != SLASHC) {
							kbd_erase();
							spos--;
						}
						kbd_puts(THIS_NAME(nbp) + spos);
					}
					else
#endif
						kbd_puts(THIS_NAME(nbp) + cpos);
					kbd_flush();
					testcol = kbd_length();
				}
				if (c != NAMEC)  /* put it back */
					unkeystroke(c);
				/* return complete name */
				(void)strncpy0(buf, THIS_NAME(nbp),
						(SIZE_T)(NLINE - 1));
				*pos = strlen(buf);
#if OPT_POPUPCHOICE
				if (gvalpopup_choices != POPUP_CHOICES_OFF
				 && !clexec && (c == NAMEC))
					status = FALSE;
				else
#endif
					status = TRUE;
			}
			else {
				/* try for a partial match against the list */
				*pos = fill_partial(case_insensitive, buf, cpos, nbp,
					skip_partial(case_insensitive, buf, cpos, nbp, size_entry),
					size_entry);
				testcol = kbd_length();
			}
#if OPT_POPUPCHOICE
# if OPT_ENUM_MODES
			if (!clexec
			 && gvalpopup_choices != POPUP_CHOICES_OFF
			 && c == NAMEC
			 && *pos == cpos) {
				if (gvalpopup_choices == POPUP_CHOICES_IMMED
				 || cmplcol == kbd_length()) {
					show_completions(case_insensitive, buf, cpos, nbp, size_entry);
					cmplcol = -kbd_length();
				}
				else
					cmplcol = kbd_length();
			}
			else
				cmplcol = 0;
# else
			if (!clexec && gvalpopup_choices
			 && c == NAMEC && *pos == cpos) {
				show_completions(case_insensitive, buf, cpos, nbp, size_entry);
				cmplcol = -kbd_length();
			}
			else
				cmplcol = 0;
# endif
#endif
			return status;
		}
		nbp = NEXT_DATA(nbp);
	}

#if OPT_POPUPCHOICE
	cmplcol = 0;
#endif
	kbd_alarm();	/* no match */
	buf[*pos = cpos] = EOS;
	return FALSE;
}

/*
 * Test a buffer to see if it looks like a shift-command, which may have
 * repeated characters (but they must all be the same).
 */
static int
is_shift_cmd(
const char *buffer,
unsigned cpos)
{
	register int c = *buffer;
	if (isRepeatable(c)) {
		while (--cpos != 0)
			if (*(++buffer) != c)
				return FALSE;
		return TRUE;
	}
	return FALSE;
}

/*
 * The following mess causes the command to terminate if:
 *
 *	we've got the eolchar
 *		-or-
 *	we're in the first few chars and we're switching from punctuation
 *	(i.e., delimiters) to non-punctuation (i.e., characters that are part
 *	of command-names), or vice-versa.  oh yeah -- '-' isn't punctuation
 *	today, and '!' isn't either, in one direction, at any rate.
 *	All this allows things like:
 *		: e#
 *		: e!%
 *		: !ls
 *		: q!
 *		: up-line
 *	to work properly.
 *
 *	If we pass this "if" with c != NAMEC, then c is ungotten below,
 *	so it can be picked up by the commands argument getter later.
 */

#define ismostpunct(c) (isPunct(c) && (c) != '-')

static int
eol_command(
const char * buffer,
unsigned cpos,
int	c,
int	eolchar)
{
	/*
	 * Handle special case of repeated-character implying repeat-count
	 */
	if (is_shift_cmd(buffer, cpos) && (c == *buffer))
		return TRUE;

	/*
	 * Shell-commands aren't complete until the line is complete.
	 */
	if ((cpos != 0) && isShellOrPipe(buffer))
		return isreturn(c);

	return	(c == eolchar)
	  ||	(
		  cpos != 0 &&  cpos < 3
	      &&(
		  (!ismostpunct(c)
		&&  ismostpunct(buffer[cpos-1])
		  )
		|| ((c != '!' && ismostpunct(c))
		  && (buffer[cpos-1] == '!' || !ismostpunct(buffer[cpos-1]))
		  )
		)
	      );
}

/*
 * This procedure is invoked from 'kbd_string()' to setup the command-name
 * completion and query displays.
 */
static int
cmd_complete(
int	c,
char	*buf,
unsigned *pos)
{
	register int status;
#if OPT_HISTORY
	/*
	 * If the user scrolled back in 'edithistory()', the text may be a
	 * repeated-shift command, which won't match the command-table (e.g.,
	 * ">>>").
	 */
	if ((*pos > 1) && is_shift_cmd(buf, *pos)) {
		unsigned len = 1;
		char	tmp[NLINE];
		tmp[0] = *buf;
		tmp[1] = EOS;
		status = cmd_complete(c, tmp, &len);
	} else
#endif
	 if ((*pos != 0) && isShellOrPipe(buf)) {
#if COMPLETE_FILES
		status = shell_complete(c, buf, pos);
#else
		status = isreturn(c);
		if (c != NAMEC)
			unkeystroke(c);
#endif
	} else {
		status = kbd_complete_bst(FALSE, c, buf, pos);
	}
	return status;
}

int
kbd_engl_stat(const char *prompt, char	*buffer, int stated)
{
	int	kbd_flags = KBD_EXPCMD|KBD_NULLOK|((NAMEC != ' ') ? 0 : KBD_MAYBEC);
	int	code;
	static	TBUFF *temp;
	ALLOC_T	len = NLINE;

	tb_scopy(&temp, "");
#if COMPLETE_FILES
	init_filec(FILECOMPLETION_BufName);
#endif
	kbd_flags |= stated;
	code = kbd_reply(
		prompt,		/* no-prompt => splice */
		&temp,		/* in/out buffer */
		eol_command,
		' ',		/* eolchar */
		kbd_flags,	/* allow blank-return */
		cmd_complete);
	if (len > tb_length(temp))
		len = tb_length(temp);
	strncpy0(buffer, tb_values(temp), len);
	return code;
}

#if OPT_NAMEBST
int
insert_namebst(const char *name, const CMDFUNC *cmd, int ro)
{
    BI_DATA temp, *p;

    if ((p = btree_search(&namebst, name)) != 0)
    {
	if ((p->n_flags & NBST_READONLY) && !ro)
	{
	    mlforce("[Cannot redefine %s]", name);
	    return FALSE;
	}

	if (!delete_namebst(name, TRUE))
	    return FALSE;
    }

    temp.bi_key     = name;
    temp.n_cmd      = cmd;
    temp.n_flags    = ro ? NBST_READONLY : 0;

    return (btree_insert(&namebst, &temp) != 0);
}

/*
 * Lookup a name in the binary-search tree, remove it if found
 */
int
delete_namebst(const char *name, int release)
{
    BI_DATA *p = btree_search(&namebst, name);

    /* not a named procedure */
    if (!p)
	return TRUE;

    if (p->n_flags & NBST_READONLY)
    {
	mlforce("BUG: btree entry %s is readonly", name);
	return FALSE;
    }

    /* we may have to free some stuff */
    if (p && release)
    {
	int i;
	int redo;

	/* remove ascii bindings */
	for (i = 0; i < N_chars; i++)
	    if (asciitbl[i] == p->n_cmd)
		asciitbl[i] = 0;

	/* then look in the multi-key table */
#if OPT_REBIND
	do {
	    register KBIND *kbp;
	    redo = FALSE;
	    for (kbp = KeyBindings; kbp != kbindtbl; kbp = kbp->k_link)
		if (kbp->k_cmd == p->n_cmd)
		{
		    unbindchar(kbp->k_code);
		    redo = TRUE;
		    break;
		}
	} while (redo);
#endif
	do {
	    register KBIND *kbp;
	    redo = FALSE;
	    for (kbp = kbindtbl; kbp->k_cmd; kbp++)
		if (kbp->k_cmd == p->n_cmd)
		{
		    unbindchar(kbp->k_code);
		    redo = TRUE;
		    break;
		}
	} while (redo);

	/* free stuff */
#if OPT_PERL
	if (p->n_cmd->c_flags & CMD_PERL)
	    perl_free_sub(CMD_U_PERL(p->n_cmd));
#endif

	free(TYPECAST(char,p->n_cmd->c_help));
	free(TYPECAST(char,p->n_cmd));
    }

    return btree_delete(&namebst, name);
}

/*
 * If we're renaming a procedure to another "procedure" name (i.e., bracketed),
 * rename it in the name-completion table.  Otherwise, simply remove it from the
 * name-completions.
 */
int
rename_namebst(const char *oldname, const char *newname)
{
    BI_DATA *prior;
    char name[NBUFN];

    /* not a named procedure */
    if (!(prior = btree_search(&namebst, oldname)))
	return TRUE;

    /* remove the entry if the new name is not a procedure (bracketed) */
    if (!is_scratchname(newname))
	return delete_namebst(oldname, TRUE);

    /* add the new name */
    strip_brackets(name, newname);
    if ((insert_namebst(name, prior->n_cmd,
		       prior->n_flags & NBST_READONLY)) != TRUE)
	return FALSE;

    /* delete the old (but don't free the data) */
    return delete_namebst(oldname, FALSE);
}

int
search_namebst(const char *name)
{
	return (btree_search(&namebst, name) != 0);
}

/*
 * Build the initial name binary search tree.  Since the nametbl is sorted we
 * do this in a binary-search manner to get a balanced tree.
 */
void
build_namebst(const NTAB *nptr, int lo, int hi)
{
	for (; lo < hi; lo++)
		if (!insert_namebst(nptr[lo].n_name, nptr[lo].n_cmd, TRUE))
			tidy_exit(BADEXIT);
}

/*
 * This is invoked to find the closest name to complete from the current buffer
 * contents.
 */
static int
kbd_complete_bst(
int	case_insensitive GCC_UNUSED,
int	c,		/* TESTC, NAMEC or isreturn() */
char	*buf,
unsigned *pos)
{
	register unsigned cpos = *pos;
	int status = FALSE;
	const char **nptr;

	kbd_init();		/* nothing to erase */
	buf[cpos] = EOS;	/* terminate it for us */

	if ((nptr = btree_parray(&namebst, buf, cpos)) != 0) {
		status = kbd_complete(FALSE, c, buf, pos, (char *)nptr, sizeof(*nptr));
		free((char *)nptr);
	} else
		kbd_alarm();
	return status;
}
#endif /* OPT_NAMEBST */

#if OPT_MENUS
/* FIXME: reuse logic from makefuncdesc() */
char *give_accelerator ( char *bname )
{
	size_t 			i, n;
	register KBIND 	*kbp;
	const CMDFUNC 	*cmd;
	static char 	outseq[NLINE];

	for (n=0; nametbl[n].n_name != 0; n++)
	{
		if (!strcmp(nametbl[n].n_name, bname))
		{
			cmd = nametbl[n].n_cmd;

			outseq[0] = '\0';

			for (i = 0; i < N_chars; i++)
			{
				if (asciitbl[i] == cmd)
					convert_kcode(i, outseq);
			}

			for (kbp = KeyBindings; kbp != kbindtbl; kbp = kbp->k_link) {
				if (kbp->k_cmd == cmd)
					convert_kcode(kbp->k_code, outseq);
			}

#if 0
			for (kbp = kbindtbl; kbp->k_cmd; kbp++)
				if (kbp->k_cmd == cmd)
					convert_kcode(kbp->k_code, outseq);
#endif

			/* Replace \t by ' ' */
			for (i=0; i<strlen(outseq); i++)
			{
				if (outseq[i] == '\t')
					outseq[i] = ' ';
			}

			return outseq;
		}
	}

	return NULL;
}
#endif /* OPT_MENUS */

#if NO_LEAKS
void
bind_leaks(void)
{
#if OPT_REBIND
	while (KeyBindings != kbindtbl) {
		KBIND *kbp = KeyBindings;
		KeyBindings = kbp->k_link;
		free((char *)kbp);
	}
#endif
#if OPT_NAMEBST
	btree_freeup(&namebst);
#endif
}
#endif	/* NO_LEAKS */
