root/usr.bin/tmux/prompt.c
/* $OpenBSD: prompt.c,v 1.5 2026/07/13 10:29:17 nicm Exp $ */

/*
 * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/types.h>
#include <sys/time.h>

#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "tmux.h"

struct prompt {
        char                     *string;
        struct utf8_data         *buffer;
        struct cmd_find_state     state;
        char                     *last;
        size_t                    index;

        prompt_input_cb           inputcb;
        prompt_free_cb            freecb;
        void                     *data;

        char                     *message_format;
        int                       keys;
        char                     *word_separators;
        struct grid_cell          style;
        struct grid_cell          command_style;
        char                     *style_str;
        char                     *command_style_str;
        enum screen_cursor_style  cstyle;
        enum screen_cursor_style  command_cstyle;
        int                       ccolour;
        int                       command_ccolour;
        int                       cmode;
        int                       command_cmode;

        enum prompt_type          type;
        int                       flags;
        int                       closed;

        u_int                     hindex[PROMPT_NTYPES];
        struct utf8_data         *copied;

        char                    **complete_list;
        u_int                     complete_size;
        char                     *complete_display;
};

struct prompt_layout {
        u_int                    area_x;
        u_int                    area_width;

        u_int                    content_x;
        u_int                    content_width;

        u_int                    label_width;
        u_int                    input_x;
        u_int                    cursor_x;

        u_int                    input_offset;
        u_int                    input_width;
};

static char     *prompt_complete(struct prompt *, const char *, u_int);
static void      prompt_clear_complete(struct prompt *);
static struct format_tree *prompt_format_tree(struct prompt *);
static char     *prompt_expand1(struct prompt *, struct format_tree *);
static void      prompt_effective_style(struct prompt *, struct style *,
                     struct format_tree *);
static void      prompt_layout(struct prompt *, u_int, u_int,
                     struct prompt_layout *, char **, struct style *);
static int       prompt_replace_complete(struct prompt *, const char *);

/* Get prompt flags as a string. */
static const char *
prompt_flags_to_string(int flags)
{
        static char     tmp[256];

        *tmp = '\0';
        if (flags & PROMPT_SINGLE)
                strlcat(tmp, "SINGLE,", sizeof tmp);
        if (flags & PROMPT_NUMERIC)
                strlcat(tmp, "NUMERIC,", sizeof tmp);
        if (flags & PROMPT_INCREMENTAL)
                strlcat(tmp, "INCREMENTAL,", sizeof tmp);
        if (flags & PROMPT_NOFORMAT)
                strlcat(tmp, "NOFORMAT,", sizeof tmp);
        if (flags & PROMPT_KEY)
                strlcat(tmp, "KEY,", sizeof tmp);
        if (flags & PROMPT_ACCEPT)
                strlcat(tmp, "ACCEPT,", sizeof tmp);
        if (flags & PROMPT_QUOTENEXT)
                strlcat(tmp, "QUOTENEXT,", sizeof tmp);
        if (flags & PROMPT_BSPACE_EXIT)
                strlcat(tmp, "BSPACE_EXIT,", sizeof tmp);
        if (flags & PROMPT_NOFREEZE)
                strlcat(tmp, "NOFREEZE,", sizeof tmp);
        if (flags & PROMPT_COMMANDMODE)
                strlcat(tmp, "COMMANDMODE,", sizeof tmp);
        if (flags & PROMPT_ISPANE)
                strlcat(tmp, "ISPANE,", sizeof tmp);
        if (flags & PROMPT_ISMODE)
                strlcat(tmp, "ISMODE,", sizeof tmp);
        if (flags & PROMPT_EDITARROWS)
                strlcat(tmp, "EDITARROWS,", sizeof tmp);
        if (*tmp != '\0')
                tmp[strlen(tmp) - 1] = '\0';
        return (tmp);
}

/* Set prompt options from session options. */
void
prompt_set_options(struct prompt_create_data *pd, struct session *s)
{
        struct options          *oo;
        struct grid_cell         gc;
        u_int                    n;

        if (s != NULL)
                oo = s->options;
        else
                oo = global_s_options;

        style_apply(&pd->style, oo, "message-style", NULL);
        style_apply(&pd->command_style, oo, "message-command-style", NULL);
        pd->style_str = options_get_string(oo, "message-style");
        pd->command_style_str = options_get_string(oo, "message-command-style");
        n = options_get_number(oo, "prompt-cursor-style");
        screen_set_cursor_style(n, &pd->cstyle, &pd->cmode);
        n = options_get_number(oo, "prompt-command-cursor-style");
        screen_set_cursor_style(n, &pd->command_cstyle, &pd->command_cmode);
        style_apply(&gc, oo, "prompt-cursor-colour", NULL);
        pd->ccolour = gc.fg;
        style_apply(&gc, oo, "prompt-command-cursor-colour", NULL);
        pd->command_ccolour = gc.fg;
        pd->message_format = options_get_string(oo, "message-format");
        pd->keys = options_get_number(oo, "status-keys");
        pd->word_separators = options_get_string(oo, "word-separators");
}

/* Create prompt. */
struct prompt *
prompt_create(const struct prompt_create_data *pd)
{
        struct prompt           *pr;
        struct format_tree      *ft;
        const char              *input = pd->input;
        char                    *tmp;

        pr = xcalloc(1, sizeof *pr);

        if (pd->fs != NULL) {
                ft = format_create_from_state(NULL, NULL, pd->fs);
                cmd_find_copy_state(&pr->state, pd->fs);
        } else {
                ft = format_create_defaults(NULL, NULL, NULL, NULL, NULL);
                cmd_find_clear_state(&pr->state, 0);
        }

        if (input == NULL)
                input = "";
        pr->string = xstrdup(pd->prompt);
        if (pd->flags & PROMPT_NOFORMAT)
                tmp = xstrdup(input);
        else
                tmp = format_expand_time(ft, input);
        if (pd->flags & PROMPT_INCREMENTAL) {
                pr->last = xstrdup(tmp);
                pr->buffer = utf8_fromcstr("");
        } else {
                pr->last = NULL;
                pr->buffer = utf8_fromcstr(tmp);
        }
        pr->index = utf8_strlen(pr->buffer);
        free(tmp);

        pr->inputcb = pd->inputcb;
        pr->freecb = pd->freecb;
        pr->data = pd->data;

        pr->flags = pd->flags;
        pr->type = pd->type;

        memcpy(&pr->style, &pd->style, sizeof pr->style);
        memcpy(&pr->command_style, &pd->command_style,
            sizeof pr->command_style);
        pr->style_str = xstrdup(pd->style_str);
        pr->command_style_str = xstrdup(pd->command_style_str);
        pr->cstyle = pd->cstyle;
        pr->command_cstyle = pd->command_cstyle;
        pr->ccolour = pd->ccolour;
        pr->command_ccolour = pd->command_ccolour;
        pr->cmode = pd->cmode;
        pr->command_cmode = pd->command_cmode;
        pr->message_format = xstrdup(pd->message_format);
        pr->keys = pd->keys;
        pr->word_separators = xstrdup(pd->word_separators);

        format_free(ft);
        return (pr);
}

/* Free prompt. */
void
prompt_free(struct prompt *pr)
{
        if (pr != NULL) {
                if (pr->freecb != NULL && pr->data != NULL)
                        pr->freecb(pr->data);
                free(pr->message_format);
                free(pr->style_str);
                free(pr->command_style_str);
                free(pr->word_separators);
                free(pr->last);
                free(pr->string);
                free(pr->buffer);
                free(pr->copied);
                prompt_clear_complete(pr);
                free(pr);
        }
}

/*
 * Fire the input callback. Returns one if the prompt is finished or zero if
 * still open.
 */
static int
prompt_fire_callback(struct prompt *pr, const char *s,
    enum prompt_key_result type, int *redraw)
{
        enum prompt_result      result;

        result = pr->inputcb(pr->data, s, type);
        if (result == PROMPT_CLOSE) {
                pr->closed = 1;
                return (1);
        }
        if (redraw != NULL)
                *redraw = 1;
        return (0);
}

/* Start incremental prompt. */
void
prompt_incremental_start(struct prompt *pr)
{
        char    *tmp, *cp;

        if (pr->flags & PROMPT_INCREMENTAL) {
                tmp = utf8_tocstr(pr->buffer);
                xasprintf(&cp, "=%s", tmp);
                prompt_fire_callback(pr, cp, PROMPT_KEY_HANDLED, NULL);
                free(cp);
                free(tmp);
        }
}

/* Update prompt. */
void
prompt_update(struct prompt *pr, const char *msg, const char *input)
{
        struct format_tree      *ft;
        char                    *tmp;

        if (cmd_find_valid_state(&pr->state))
                ft = format_create_from_state(NULL, NULL, &pr->state);
        else
                ft = format_create_defaults(NULL, NULL, NULL, NULL, NULL);

        free(pr->string);
        pr->string = xstrdup(msg);

        if (input == NULL)
                input = "";
        free(pr->buffer);
        if (pr->flags & PROMPT_NOFORMAT)
                tmp = xstrdup(input);
        else
                tmp = format_expand_time(ft, input);
        pr->buffer = utf8_fromcstr(tmp);
        pr->index = utf8_strlen(pr->buffer);
        free(tmp);

        memset(pr->hindex, 0, sizeof pr->hindex);
        pr->closed = 0;
        prompt_clear_complete(pr);

        format_free(ft);
}

/* Is this prompt closed? */
int
prompt_closed(struct prompt *pr)
{
        return (pr->closed);
}

/* Redraw character. Return 1 if can continue redrawing, 0 otherwise. */
static int
prompt_redraw_character(struct screen_write_ctx *ctx, u_int offset,
    u_int pwidth, u_int *width, struct grid_cell *gc,
    const struct utf8_data *ud)
{
        u_char  ch;

        if (*width < offset) {
                *width += ud->width;
                return (1);
        }
        if (*width >= offset + pwidth)
                return (0);
        *width += ud->width;
        if (*width > offset + pwidth)
                return (0);

        ch = *ud->data;
        if (ud->size == 1 && (ch <= 0x1f || ch == 0x7f)) {
                gc->data.data[0] = '^';
                gc->data.data[1] = (ch == 0x7f) ? '?' : ch|0x40;
                gc->data.size = gc->data.have = 2;
                gc->data.width = 2;
        } else
                utf8_copy(&gc->data, ud);
        screen_write_cell(ctx, gc);
        return (1);
}

/*
 * Redraw quote indicator '^' if necessary. Return 1 if can continue redrawing,
 * 0 otherwise.
 */
static int
prompt_redraw_quote(const struct prompt *pr, u_int pcursor, u_int input_x,
    struct screen_write_ctx *ctx, u_int offset, u_int pw, u_int *w,
    struct grid_cell *gc)
{
        struct utf8_data        ud;

        if (pr->flags & PROMPT_QUOTENEXT &&
            pcursor >= offset &&
            ctx->s->cx == input_x + pcursor - offset) {
                utf8_set(&ud, '^');
                return (prompt_redraw_character(ctx, offset, pw, w, gc, &ud));
        }
        return (1);
}

/* Draw the stored completion matches. */
static void
prompt_draw_complete(struct prompt *pr, struct screen_write_ctx *ctx, u_int ax,
    u_int aw, u_int cx, u_int py, const struct grid_cell *base)
{
        struct grid_cell         gc;
        struct utf8_data        *ud;
        u_int                    avail, width, i;

        if (pr->complete_display == NULL)
                return;
        if (pr->index != utf8_strlen(pr->buffer))
                return;
        if (cx < ax || cx - ax >= aw)
                return;
        avail = aw - (cx - ax);

        memcpy(&gc, base, sizeof gc);
        gc.attr |= GRID_ATTR_UNDERSCORE;
        screen_write_cursormove(ctx, cx, py, 0);

        width = 0;
        ud = utf8_fromcstr(pr->complete_display);
        for (i = 0; ud[i].size != 0; i++) {
                if (width + ud[i].width > avail)
                        break;
                utf8_copy(&gc.data, &ud[i]);
                screen_write_cell(ctx, &gc);
                width += ud[i].width;
        }
        free(ud);
}

/* Create the prompt format tree using the current input. */
static struct format_tree *
prompt_format_tree(struct prompt *pr)
{
        struct format_tree      *ft;
        char                    *tmp;

        if (cmd_find_valid_state(&pr->state))
                ft = format_create_from_state(NULL, NULL, &pr->state);
        else
                ft = format_create_defaults(NULL, NULL, NULL, NULL, NULL);
        tmp = utf8_tocstr(pr->buffer);
        format_add(ft, "prompt_input", "%s", tmp);
        free(tmp);

        format_add(ft, "prompt_flags", "%s", prompt_flags_to_string(pr->flags));
        format_add(ft, "prompt_type", "%s", prompt_type_string(pr->type));
        if (pr->flags & PROMPT_COMMANDMODE)
                format_add(ft, "command_prompt", "1");
        else
                format_add(ft, "command_prompt", "0");
        return (ft);
}

/* Expand prompt string using the current input. */
static char *
prompt_expand1(struct prompt *pr, struct format_tree *ft)
{
        char                    *expanded, *prompt;

        prompt = format_expand_time(ft, pr->string);
        format_add(ft, "message", "%s", prompt);
        expanded = format_expand_time(ft, pr->message_format);
        free(prompt);
        return (expanded);
}

/* Get the effective message style for the current prompt. */
static void
prompt_effective_style(struct prompt *pr, struct style *sy,
    struct format_tree *ft)
{
        const char              *s;
        struct grid_cell        *gc;
        char                    *expanded;

        if (pr->flags & PROMPT_COMMANDMODE) {
                s = pr->command_style_str;
                gc = &pr->command_style;
        } else {
                s = pr->style_str;
                gc = &pr->style;
        }

        style_set(sy, gc);
        if (s != NULL) {
                expanded = format_expand_time(ft, s);
                if (style_parse(sy, &grid_default_cell, expanded) != 0)
                        style_set(sy, gc);
                free(expanded);
        }
}

/* Work out where the editable prompt content appears. */
static void
prompt_layout(struct prompt *pr, u_int ax, u_int aw, struct prompt_layout *pl,
    char **expanded, struct style *sy)
{
        char    *local = NULL;
        struct format_tree *ft;
        u_int    pcursor, pwidth, end, width, offset, avail;

        memset(pl, 0, sizeof *pl);
        pl->area_x = ax;
        pl->area_width = aw;

        ft = prompt_format_tree(pr);
        if (sy != NULL)
                prompt_effective_style(pr, sy, ft);
        if (expanded != NULL)
                *expanded = prompt_expand1(pr, ft);
        else {
                local = prompt_expand1(pr, ft);
                expanded = &local;
        }
        format_free(ft);

        if (aw == 0) {
                free(local);
                return;
        }

        pl->label_width = format_width(*expanded);
        if (pl->label_width > aw)
                pl->label_width = aw;

        pcursor = utf8_strwidth(pr->buffer, pr->index);
        pwidth = utf8_strwidth(pr->buffer, -1);
        if (pr->flags & PROMPT_QUOTENEXT)
                pwidth++;

        avail = aw - pl->label_width;
        if (avail == 0) {
                pl->input_offset = 0;
                pl->input_width = 0;
                pl->cursor_x = pl->label_width;
        } else {
                if (pcursor >= avail) {
                        offset = (pcursor - avail) + 1;
                        width = avail;
                } else {
                        offset = 0;
                        width = pwidth;
                }
                if (width > avail)
                        width = avail;

                pl->input_offset = offset;
                pl->input_width = width;
                pl->cursor_x = pl->label_width + pcursor - offset;
        }

        pl->content_width = pl->label_width + pl->input_width;
        if (pr->complete_display != NULL &&
            pr->index == utf8_strlen(pr->buffer) &&
            pl->cursor_x < aw) {
                avail = aw - pl->cursor_x;
                width = utf8_cstrwidth(pr->complete_display);
                if (width > avail)
                        width = avail;
                end = pl->cursor_x + width;
                if (end > pl->content_width)
                        pl->content_width = end;
        }
        if (pl->content_width > aw)
                pl->content_width = aw;

        if (sy != NULL) {
                switch (sy->align) {
                case STYLE_ALIGN_CENTRE:
                case STYLE_ALIGN_ABSOLUTE_CENTRE:
                        pl->content_x = ax + (aw - pl->content_width) / 2;
                        break;
                case STYLE_ALIGN_RIGHT:
                        pl->content_x = ax + aw - pl->content_width;
                        break;
                default:
                        pl->content_x = ax;
                        break;
                }
        } else
                pl->content_x = ax;

        pl->input_x = pl->content_x + pl->label_width;
        pl->cursor_x += pl->content_x;
        free(local);
}

/* Choose a completion from a mouse position. */
static enum prompt_key_result
prompt_mouse_complete(struct prompt *pr, u_int x, u_int cx, u_int ax, u_int aw,
    int *redraw)
{
        char    *replace;
        u_int    avail, clicked, end, i, start, width;

        if (pr->complete_display == NULL || pr->complete_size == 0)
                return (PROMPT_KEY_NOT_HANDLED);
        if (pr->index != utf8_strlen(pr->buffer))
                return (PROMPT_KEY_NOT_HANDLED);
        if (cx < ax || cx - ax >= aw || x < cx)
                return (PROMPT_KEY_NOT_HANDLED);

        avail = aw - (cx - ax);
        clicked = x - cx;
        width = utf8_cstrwidth(pr->complete_display);
        if (width > avail)
                width = avail;
        if (clicked >= width)
                return (PROMPT_KEY_NOT_HANDLED);

        end = 0;
        for (i = 0; i < pr->complete_size; i++) {
                start = end + 1;
                end = start + utf8_cstrwidth(pr->complete_list[i]);
                if (clicked < start || clicked >= end)
                        continue;

                xasprintf(&replace, "%s ", pr->complete_list[i]);
                if (prompt_replace_complete(pr, replace)) {
                        prompt_clear_complete(pr);
                        if (redraw != NULL)
                                *redraw = 1;
                }
                free(replace);
                return (PROMPT_KEY_HANDLED);
        }
        return (PROMPT_KEY_HANDLED);
}

/* Draw prompt. */
void
prompt_draw(struct prompt *pr, struct prompt_draw_data *pd)
{
        struct screen_write_ctx *ctx = pd->ctx;
        struct screen           *s = ctx->s;
        u_int                    ax = pd->area_x, py = pd->prompt_line, *cx;
        u_int                    aw = pd->area_width;
        struct grid_cell         gc;
        struct prompt_layout     pl;
        struct style             sy;
        u_int                    i, width, pcursor;
        char                    *expanded;

        if (pr->flags & PROMPT_COMMANDMODE) {
                s->default_cstyle = pr->command_cstyle;
                s->default_mode = pr->command_cmode;
                s->default_ccolour = pr->command_ccolour;
        } else {
                s->default_cstyle = pr->cstyle;
                s->default_mode = pr->cmode;
                s->default_ccolour = pr->ccolour;
        }

        prompt_layout(pr, ax, aw, &pl, &expanded, &sy);
        memcpy(&gc, &sy.gc, sizeof gc);
        cx = pd->cursor_x;
        *cx = pl.cursor_x;

        screen_write_cursormove(ctx, ax, py, 0);
        if (sy.fill != 8)
                screen_write_clearcharacter(ctx, aw, sy.fill);

        pcursor = utf8_strwidth(pr->buffer, pr->index);

        if (pl.content_width != 0) {
                screen_write_cursormove(ctx, pl.content_x, py, 0);
                if (pl.label_width != 0)
                        format_draw(ctx, &gc, pl.label_width, expanded, NULL,
                            0);

                screen_write_cursormove(ctx, pl.input_x, py, 0);
                width = 0;
                for (i = 0; pr->buffer[i].size != 0; i++) {
                        if (!prompt_redraw_quote(pr, pcursor, pl.input_x,
                            ctx, pl.input_offset, pl.input_width, &width,
                            &gc))
                                break;
                        if (!prompt_redraw_character(ctx, pl.input_offset,
                            pl.input_width, &width, &gc, &pr->buffer[i]))
                                break;
                }
                prompt_redraw_quote(pr, pcursor, pl.input_x, ctx,
                    pl.input_offset, pl.input_width, &width, &gc);

                prompt_draw_complete(pr, ctx, pl.content_x, pl.content_width,
                    pl.cursor_x, py, &gc);
        }
        free(expanded);
}

/* Move cursor in prompt from a mouse position. */
enum prompt_key_result
prompt_mouse(struct prompt *pr, u_int x, u_int ax, u_int aw, int *redraw)
{
        struct utf8_data        *ud;
        enum prompt_key_result   result;
        struct prompt_layout     pl;
        struct style             sy;
        char                    *expanded;
        u_int                    pwidth, width, target;
        size_t                   idx;

        if (x < ax || x >= ax + aw)
                return (PROMPT_KEY_NOT_HANDLED);

        prompt_layout(pr, ax, aw, &pl, &expanded, &sy);
        free(expanded);

        if (pl.input_width == 0)
                return (PROMPT_KEY_HANDLED);

        pwidth = utf8_strwidth(pr->buffer, -1);
        if (pr->flags & PROMPT_QUOTENEXT)
                pwidth++;

        result = prompt_mouse_complete(pr, x, pl.cursor_x, pl.content_x,
            pl.content_width, redraw);
        if (result != PROMPT_KEY_NOT_HANDLED)
                return (result);

        if (x <= pl.input_x)
                target = pl.input_offset;
        else
                target = pl.input_offset + x - pl.input_x;
        if (target > pwidth)
                target = pwidth;

        width = 0;
        for (idx = 0; pr->buffer[idx].size != 0; idx++) {
                ud = &pr->buffer[idx];
                if (width >= target)
                        break;
                width += ud->width;
        }
        if (idx == pr->index)
                return (PROMPT_KEY_HANDLED);

        pr->index = idx;
        prompt_clear_complete(pr);
        if (redraw != NULL)
                *redraw = 1;

        return (PROMPT_KEY_HANDLED);
}

/* Is this a separator? */
static int
prompt_in_list(const char *ws, const struct utf8_data *ud)
{
        if (ud->size != 1 || ud->width != 1)
                return (0);
        return (strchr(ws, *ud->data) != NULL);
}

/* Is this a space? */
static int
prompt_space(const struct utf8_data *ud)
{
        if (ud->size != 1 || ud->width != 1)
                return (0);
        return (*ud->data == ' ');
}

/* Is this a keypad key? */
static key_code
prompt_keypad_key(key_code key)
{
        if (key & KEYC_MASK_MODIFIERS)
                return (key);

        switch (key) {
        case KEYC_KP_SLASH:
                return ('/');
        case KEYC_KP_STAR:
                return ('*');
        case KEYC_KP_MINUS:
                return ('-');
        case KEYC_KP_SEVEN:
                return ('7');
        case KEYC_KP_EIGHT:
                return ('8');
        case KEYC_KP_NINE:
                return ('9');
        case KEYC_KP_PLUS:
                return ('+');
        case KEYC_KP_FOUR:
                return ('4');
        case KEYC_KP_FIVE:
                return ('5');
        case KEYC_KP_SIX:
                return ('6');
        case KEYC_KP_ONE:
                return ('1');
        case KEYC_KP_TWO:
                return ('2');
        case KEYC_KP_THREE:
                return ('3');
        case KEYC_KP_ENTER:
                return ('\r');
        case KEYC_KP_ZERO:
                return ('0');
        case KEYC_KP_PERIOD:
                return ('.');
        }
        return (key);
}

/*
 * Translate key from vi to emacs. Return 0 to drop key, 1 to process the key
 * as an emacs key; return 2 to append to the buffer. Set *redraw if the
 * translation changed something the host needs to redraw (such as switching
 * between insert and command mode).
 */
static int
prompt_translate_key(struct prompt *pr, key_code key, key_code *new_key,
    int *redraw)
{
        if (~pr->flags & PROMPT_COMMANDMODE) {
                switch (key) {
                case 'a'|KEYC_CTRL:
                case 'c'|KEYC_CTRL:
                case 'e'|KEYC_CTRL:
                case 'g'|KEYC_CTRL:
                case 'h'|KEYC_CTRL:
                case '\011': /* Tab */
                case 'k'|KEYC_CTRL:
                case 'n'|KEYC_CTRL:
                case 'p'|KEYC_CTRL:
                case 't'|KEYC_CTRL:
                case 'u'|KEYC_CTRL:
                case 'v'|KEYC_CTRL:
                case 'w'|KEYC_CTRL:
                case 'y'|KEYC_CTRL:
                case '\n':
                case '\r':
                case KEYC_LEFT|KEYC_CTRL:
                case KEYC_RIGHT|KEYC_CTRL:
                case KEYC_BSPACE:
                case KEYC_DC:
                case KEYC_DOWN:
                case KEYC_END:
                case KEYC_HOME:
                case KEYC_LEFT:
                case KEYC_RIGHT:
                case KEYC_UP:
                        *new_key = key;
                        return (1);
                case '\033': /* Escape */
                case '['|KEYC_CTRL:
                        pr->flags |= PROMPT_COMMANDMODE;
                        if (pr->index != 0)
                                pr->index--;
                        *redraw = 1;
                        return (0);
                }
                *new_key = key;
                return (2);
        }

        switch (key) {
        case KEYC_BSPACE:
                *new_key = KEYC_LEFT;
                return (1);
        case 'A':
        case 'I':
        case 'C':
        case 's':
        case 'a':
                pr->flags &= ~PROMPT_COMMANDMODE;
                *redraw = 1;
                break; /* switch mode and... */
        case 'S':
                pr->flags &= ~PROMPT_COMMANDMODE;
                *redraw = 1;
                *new_key = 'u'|KEYC_CTRL;
                return (1);
        case 'i':
                pr->flags &= ~PROMPT_COMMANDMODE;
                *redraw = 1;
                return (0);
        case '\033': /* Escape */
        case '['|KEYC_CTRL:
                return (0);
        }

        switch (key) {
        case 'A':
        case '$':
                *new_key = KEYC_END;
                return (1);
        case 'I':
        case '0':
        case '^':
                *new_key = KEYC_HOME;
                return (1);
        case 'C':
        case 'D':
                *new_key = 'k'|KEYC_CTRL;
                return (1);
        case KEYC_BSPACE:
        case 'X':
                *new_key = KEYC_BSPACE;
                return (1);
        case 'b':
                *new_key = 'b'|KEYC_META;
                return (1);
        case 'B':
                *new_key = 'B'|KEYC_VI;
                return (1);
        case 'd':
                *new_key = 'u'|KEYC_CTRL;
                return (1);
        case 'e':
                *new_key = 'e'|KEYC_VI;
                return (1);
        case 'E':
                *new_key = 'E'|KEYC_VI;
                return (1);
        case 'w':
                *new_key = 'w'|KEYC_VI;
                return (1);
        case 'W':
                *new_key = 'W'|KEYC_VI;
                return (1);
        case 'p':
                *new_key = 'y'|KEYC_CTRL;
                return (1);
        case 'q':
                *new_key = 'c'|KEYC_CTRL;
                return (1);
        case 's':
        case KEYC_DC:
        case 'x':
                *new_key = KEYC_DC;
                return (1);
        case KEYC_DOWN:
        case 'j':
                *new_key = KEYC_DOWN;
                return (1);
        case KEYC_LEFT:
        case 'h':
                *new_key = KEYC_LEFT;
                return (1);
        case 'a':
        case KEYC_RIGHT:
        case 'l':
                *new_key = KEYC_RIGHT;
                return (1);
        case KEYC_UP:
        case 'k':
                *new_key = KEYC_UP;
                return (1);
        case 'h'|KEYC_CTRL:
        case 'c'|KEYC_CTRL:
        case '\n':
        case '\r':
                return (1);
        }
        return (0);
}

/* Paste into prompt. */
static int
prompt_paste(struct prompt *pr)
{
        struct paste_buffer     *pb;
        const char              *bufdata;
        size_t                   size, n, bufsize;
        u_int                    i;
        struct utf8_data        *ud, *udp;
        enum utf8_state          more;

        size = utf8_strlen(pr->buffer);
        if (pr->copied != NULL) {
                ud = pr->copied;
                n = utf8_strlen(pr->copied);
        } else {
                if ((pb = paste_get_top(NULL)) == NULL)
                        return (0);
                bufdata = paste_buffer_data(pb, &bufsize);
                ud = udp = xreallocarray(NULL, bufsize + 1, sizeof *ud);
                for (i = 0; i != bufsize; /* nothing */) {
                        more = utf8_open(udp, bufdata[i]);
                        if (more == UTF8_MORE) {
                                while (++i != bufsize && more == UTF8_MORE)
                                        more = utf8_append(udp, bufdata[i]);
                                if (more == UTF8_DONE) {
                                        udp++;
                                        continue;
                                }
                                i -= udp->have;
                        }
                        if (bufdata[i] <= 31 || bufdata[i] >= 127)
                                break;
                        utf8_set(udp, bufdata[i]);
                        udp++;
                        i++;
                }
                udp->size = 0;
                n = udp - ud;
        }
        if (n != 0) {
                pr->buffer = xreallocarray(pr->buffer, size + n + 1,
                    sizeof *pr->buffer);
                if (pr->index == size) {
                        memcpy(pr->buffer + pr->index, ud,
                            n * sizeof *pr->buffer);
                        pr->index += n;
                        pr->buffer[pr->index].size = 0;
                } else {
                        memmove(pr->buffer + pr->index + n,
                            pr->buffer + pr->index,
                            (size + 1 - pr->index) *
                            sizeof *pr->buffer);
                        memcpy(pr->buffer + pr->index, ud,
                            n * sizeof *pr->buffer);
                        pr->index += n;
                }
        }
        if (ud != pr->copied)
                free(ud);
        return (1);
}

/* Finish completion. */
static int
prompt_replace_complete(struct prompt *pr, const char *s)
{
        char                     word[64], *allocated = NULL;
        size_t                   size, n, off, idx, used;
        struct utf8_data        *first, *last, *ud;

        /* Work out where the cursor currently is. */
        idx = pr->index;
        if (idx != 0)
                idx--;
        size = utf8_strlen(pr->buffer);

        /* Find the word we are in. */
        first = &pr->buffer[idx];
        while (first > pr->buffer && !prompt_space(first))
                first--;
        while (first->size != 0 && prompt_space(first))
                first++;
        last = &pr->buffer[idx];
        while (last->size != 0 && !prompt_space(last))
                last++;
        while (last > pr->buffer && prompt_space(last))
                last--;
        if (last->size != 0)
                last++;
        if (last < first)
                return (0);
        if (s == NULL) {
                used = 0;
                for (ud = first; ud < last; ud++) {
                        if (used + ud->size >= sizeof word)
                                break;
                        memcpy(word + used, ud->data, ud->size);
                        used += ud->size;
                }
                if (ud != last)
                        return (0);
                word[used] = '\0';
        }

        /* Try to complete it. */
        if (s == NULL) {
                allocated = prompt_complete(pr, word, first - pr->buffer);
                if (allocated == NULL)
                        return (0);
                s = allocated;
        }

        /* Trim out word. */
        n = size - (last - pr->buffer) + 1; /* with \0 */
        memmove(first, last, n * sizeof *pr->buffer);
        size -= last - first;

        /* Insert the new word. */
        size += strlen(s);
        off = first - pr->buffer;
        pr->buffer = xreallocarray(pr->buffer, size + 1,
            sizeof *pr->buffer);
        first = pr->buffer + off;
        memmove(first + strlen(s), first, n * sizeof *pr->buffer);
        for (idx = 0; idx < strlen(s); idx++)
                utf8_set(&first[idx], s[idx]);
        pr->index = (first - pr->buffer) + strlen(s);

        free(allocated);
        return (1);
}

/* Prompt forward to the next beginning of a word. */
static void
prompt_forward_word(struct prompt *pr, size_t size, int vi,
    const char *separators)
{
        size_t           idx = pr->index;
        int              word_is_separators;

        /* In emacs mode, skip until the first non-whitespace character. */
        if (!vi) {
                while (idx != size && prompt_space(&pr->buffer[idx]))
                        idx++;
        }

        /* Can't move forward if we're already at the end. */
        if (idx == size) {
                pr->index = idx;
                return;
        }

        /* Determine the current character class (separators or not). */
        word_is_separators = prompt_in_list(separators, &pr->buffer[idx]) &&
            !prompt_space(&pr->buffer[idx]);

        /* Skip ahead until the first space or opposite character class. */
        do {
                idx++;
                if (prompt_space(&pr->buffer[idx])) {
                        /* In vi mode, go to the start of the next word. */
                        if (vi) {
                                while (idx != size &&
                                    prompt_space(&pr->buffer[idx]))
                                        idx++;
                        }
                        break;
                }
        } while (idx != size && word_is_separators == prompt_in_list(
            separators, &pr->buffer[idx]));

        pr->index = idx;
}

/* Prompt forward to the next end of a word. */
static void
prompt_end_word(struct prompt *pr, size_t size, const char *separators)
{
        size_t           idx = pr->index;
        int              word_is_separators;

        /* Can't move forward if we're already at the end. */
        if (idx == size)
                return;

        /* Find the next word. */
        do {
                idx++;
                if (idx == size) {
                        pr->index = idx;
                        return;
                }
        } while (prompt_space(&pr->buffer[idx]));

        /* Determine the character class (separators or not). */
        word_is_separators = prompt_in_list(separators,
            &pr->buffer[idx]);

        /* Skip ahead until the next space or opposite character class. */
        do {
                idx++;
                if (idx == size)
                        break;
        } while (!prompt_space(&pr->buffer[idx]) &&
            word_is_separators == prompt_in_list(separators, &pr->buffer[idx]));

        /* Back up to the previous character to stop at the end of the word. */
        pr->index = idx - 1;
}

/* Prompt backward to the previous beginning of a word. */
static void
prompt_backward_word(struct prompt *pr, const char *separators)
{
        size_t  idx = pr->index;
        int     word_is_separators;

        /* Find non-whitespace. */
        while (idx != 0) {
                --idx;
                if (!prompt_space(&pr->buffer[idx]))
                        break;
        }
        word_is_separators = prompt_in_list(separators,
            &pr->buffer[idx]);

        /* Find the character before the beginning of the word. */
        while (idx != 0) {
                --idx;
                if (prompt_space(&pr->buffer[idx]) ||
                    word_is_separators != prompt_in_list(separators,
                    &pr->buffer[idx])) {
                        /* Go back to the word. */
                        idx++;
                        break;
                }
        }
        pr->index = idx;
}

/* Fire input callback when done. */
static enum prompt_key_result
prompt_done(struct prompt *pr, const char *s, int *redraw)
{
        if (prompt_fire_callback(pr, s, PROMPT_KEY_CLOSE, redraw))
                return (PROMPT_KEY_CLOSE);
        return (PROMPT_KEY_HANDLED);
}

/* Check for a movement key. */
static enum prompt_key_result
prompt_check_move(struct prompt *pr, key_code key)
{
        char    *s;

        if (~pr->flags & PROMPT_INCREMENTAL)
                return (PROMPT_KEY_NOT_HANDLED);
        switch (key) {
        case KEYC_UP:
        case KEYC_DOWN:
        case KEYC_PPAGE:
        case KEYC_NPAGE:
                break;
        case KEYC_LEFT:
        case KEYC_RIGHT:
                if (pr->flags & PROMPT_EDITARROWS)
                        return (PROMPT_KEY_NOT_HANDLED);
                break;
        default:
                return (PROMPT_KEY_NOT_HANDLED);
        }
        s = utf8_tocstr(pr->buffer);
        if (prompt_fire_callback(pr, s, PROMPT_KEY_MOVE, NULL)) {
                free(s);
                return (PROMPT_KEY_CLOSE);
        }
        free(s);
        return (PROMPT_KEY_MOVE);
}

/* Handle keys in prompt. */
enum prompt_key_result
prompt_key(struct prompt *pr, key_code key, int *redraw)
{
        char                    *s, *cp, prefix = '=';
        const char              *histstr, *ks;
        size_t                   size, idx;
        struct utf8_data         tmp;
        enum prompt_key_result   result = PROMPT_KEY_HANDLED;
        int                      word_is_separators;

        pr->closed = 0;

        /*
         * Drop any inline completion matches; the Tab handler rebuilds them if
         * completion is still applicable.
         */
        prompt_clear_complete(pr);

        if (pr->flags & PROMPT_KEY) {
                ks = key_string_lookup_key(key, 0);
                if (!prompt_fire_callback(pr, ks, PROMPT_KEY_CLOSE, NULL))
                        pr->closed = 1;
                return (PROMPT_KEY_CLOSE);
        }
        size = utf8_strlen(pr->buffer);

        key &= ~KEYC_MASK_FLAGS;
        key = prompt_keypad_key(key);

        if (pr->flags & PROMPT_NUMERIC) {
                if (key >= '0' && key <= '9')
                        goto append_key;
                s = utf8_tocstr(pr->buffer);
                if (!prompt_fire_callback(pr, s, PROMPT_KEY_CLOSE, NULL))
                        pr->closed = 1;
                free(s);
                return (PROMPT_KEY_NOT_HANDLED);
        }

        if (pr->flags & (PROMPT_SINGLE|PROMPT_QUOTENEXT)) {
                if ((key & KEYC_MASK_KEY) == KEYC_BSPACE)
                        key = 0x7f;
                else if ((key & KEYC_MASK_KEY) > 0x7f) {
                        if (!KEYC_IS_UNICODE(key))
                                return (PROMPT_KEY_HANDLED);
                        key &= KEYC_MASK_KEY;
                } else
                        key &= (key & KEYC_CTRL) ? 0x1f : KEYC_MASK_KEY;
                pr->flags &= ~PROMPT_QUOTENEXT;
                goto append_key;
        }

        if (pr->keys == MODEKEY_VI) {
                switch (prompt_translate_key(pr, key, &key, redraw)) {
                case 1:
                        goto process_key;
                case 2:
                        goto append_key;
                default:
                        return (PROMPT_KEY_HANDLED);
                }
        }

process_key:
        result = prompt_check_move(pr, key);
        if (result != PROMPT_KEY_NOT_HANDLED)
                return (result);
        result = PROMPT_KEY_HANDLED;

        switch (key) {
        case KEYC_LEFT:
        case 'b'|KEYC_CTRL:
                if (pr->index > 0) {
                        pr->index--;
                        break;
                }
                break;
        case KEYC_RIGHT:
        case 'f'|KEYC_CTRL:
                if (pr->index < size) {
                        pr->index++;
                        break;
                }
                break;
        case KEYC_HOME:
        case 'a'|KEYC_CTRL:
                if (pr->index != 0) {
                        pr->index = 0;
                        break;
                }
                break;
        case KEYC_END:
        case 'e'|KEYC_CTRL:
                if (pr->index != size) {
                        pr->index = size;
                        break;
                }
                break;
        case '\011': /* Tab */
                if (prompt_replace_complete(pr, NULL))
                        goto changed;
                break;
        case KEYC_BSPACE:
        case 'h'|KEYC_CTRL:
                if (pr->flags & PROMPT_BSPACE_EXIT && size == 0)
                        return (prompt_done(pr, NULL, redraw));
                if (pr->index != 0) {
                        if (pr->index == size)
                                pr->buffer[--pr->index].size = 0;
                        else {
                                memmove(pr->buffer + pr->index - 1,
                                    pr->buffer + pr->index,
                                    (size + 1 - pr->index) *
                                    sizeof *pr->buffer);
                                pr->index--;
                        }
                        goto changed;
                }
                break;
        case KEYC_DC:
        case 'd'|KEYC_CTRL:
                if (pr->index != size) {
                        memmove(pr->buffer + pr->index,
                            pr->buffer + pr->index + 1,
                            (size + 1 - pr->index) *
                            sizeof *pr->buffer);
                        goto changed;
                }
                break;
        case 'u'|KEYC_CTRL:
                pr->buffer[0].size = 0;
                pr->index = 0;
                goto changed;
        case 'k'|KEYC_CTRL:
                if (pr->index < size) {
                        pr->buffer[pr->index].size = 0;
                        goto changed;
                }
                break;
        case 'w'|KEYC_CTRL:
                /* Find non-whitespace. */
                idx = pr->index;
                while (idx != 0) {
                        idx--;
                        if (!prompt_space(&pr->buffer[idx]))
                                break;
                }
                word_is_separators = prompt_in_list(pr->word_separators,
                    &pr->buffer[idx]);

                /* Find the character before the beginning of the word. */
                while (idx != 0) {
                        idx--;
                        if (prompt_space(&pr->buffer[idx]) ||
                            word_is_separators != prompt_in_list(
                            pr->word_separators, &pr->buffer[idx])) {
                                /* Go back to the word. */
                                idx++;
                                break;
                        }
                }

                free(pr->copied);
                pr->copied = xcalloc(sizeof *pr->buffer,
                    (pr->index - idx) + 1);
                memcpy(pr->copied, pr->buffer + idx,
                    (pr->index - idx) * sizeof *pr->buffer);

                memmove(pr->buffer + idx, pr->buffer + pr->index,
                    (size + 1 - pr->index) * sizeof *pr->buffer);
                memset(pr->buffer + size - (pr->index - idx), '\0',
                    (pr->index - idx) * sizeof *pr->buffer);
                pr->index = idx;

                goto changed;
        case KEYC_RIGHT|KEYC_CTRL:
        case 'f'|KEYC_META:
                prompt_forward_word(pr, size, 0, pr->word_separators);
                goto changed;
        case 'E'|KEYC_VI:
                prompt_end_word(pr, size, "");
                goto changed;
        case 'e'|KEYC_VI:
                prompt_end_word(pr, size, pr->word_separators);
                goto changed;
        case 'W'|KEYC_VI:
                prompt_forward_word(pr, size, 1, "");
                goto changed;
        case 'w'|KEYC_VI:
                prompt_forward_word(pr, size, 1, pr->word_separators);
                goto changed;
        case 'B'|KEYC_VI:
                prompt_backward_word(pr, "");
                goto changed;
        case KEYC_LEFT|KEYC_CTRL:
        case 'b'|KEYC_META:
                prompt_backward_word(pr, pr->word_separators);
                goto changed;
        case KEYC_UP:
        case 'p'|KEYC_CTRL:
                histstr = prompt_up_history(pr->hindex,
                    pr->type);
                if (histstr == NULL)
                        break;
                free(pr->buffer);
                pr->buffer = utf8_fromcstr(histstr);
                pr->index = utf8_strlen(pr->buffer);
                goto changed;
        case KEYC_DOWN:
        case 'n'|KEYC_CTRL:
                histstr = prompt_down_history(pr->hindex, pr->type);
                if (histstr == NULL)
                        break;
                free(pr->buffer);
                pr->buffer = utf8_fromcstr(histstr);
                pr->index = utf8_strlen(pr->buffer);
                goto changed;
        case 'y'|KEYC_CTRL:
                if (prompt_paste(pr))
                        goto changed;
                break;
        case 't'|KEYC_CTRL:
                idx = pr->index;
                if (idx < size)
                        idx++;
                if (idx >= 2) {
                        utf8_copy(&tmp, &pr->buffer[idx - 2]);
                        utf8_copy(&pr->buffer[idx - 2], &pr->buffer[idx - 1]);
                        utf8_copy(&pr->buffer[idx - 1], &tmp);
                        pr->index = idx;
                        goto changed;
                }
                break;
        case '\r':
        case '\n':
                s = utf8_tocstr(pr->buffer);
                if (*s != '\0')
                        prompt_add_history(s, pr->type);
                result = prompt_done(pr, s, redraw);
                free(s);
                return (result);
        case '\033': /* Escape */
        case '['|KEYC_CTRL:
        case 'c'|KEYC_CTRL:
        case 'g'|KEYC_CTRL:
                return (prompt_done(pr, NULL, redraw));
        case 'r'|KEYC_CTRL:
                if (~pr->flags & PROMPT_INCREMENTAL)
                        break;
                if (pr->buffer[0].size == 0) {
                        prefix = '=';
                        free(pr->buffer);
                        pr->buffer = utf8_fromcstr(pr->last);
                        pr->index = utf8_strlen(pr->buffer);
                } else
                        prefix = '-';
                goto changed;
        case 's'|KEYC_CTRL:
                if (~pr->flags & PROMPT_INCREMENTAL)
                        break;
                if (pr->buffer[0].size == 0) {
                        prefix = '=';
                        free(pr->buffer);
                        pr->buffer = utf8_fromcstr(pr->last);
                        pr->index = utf8_strlen(pr->buffer);
                } else
                        prefix = '+';
                goto changed;
        case 'v'|KEYC_CTRL:
                pr->flags |= PROMPT_QUOTENEXT;
                break;
        default:
                goto append_key;
        }

        *redraw = 1;
        return (PROMPT_KEY_HANDLED);

append_key:
        if (key <= 0x7f) {
                utf8_set(&tmp, key);
                if (key <= 0x1f || key == 0x7f)
                        tmp.width = 2;
        } else if (KEYC_IS_UNICODE(key))
                utf8_to_data(key, &tmp);
        else
                return (PROMPT_KEY_HANDLED);

        pr->buffer = xreallocarray(pr->buffer, size + 2,
            sizeof *pr->buffer);

        if (pr->index == size) {
                utf8_copy(&pr->buffer[pr->index], &tmp);
                pr->index++;
                pr->buffer[pr->index].size = 0;
        } else {
                memmove(pr->buffer + pr->index + 1,
                    pr->buffer + pr->index,
                    (size + 1 - pr->index) *
                    sizeof *pr->buffer);
                utf8_copy(&pr->buffer[pr->index], &tmp);
                pr->index++;
        }

        if (pr->flags & PROMPT_SINGLE) {
                if (utf8_strlen(pr->buffer) != 1) {
                        pr->closed = 1;
                        result = PROMPT_KEY_CLOSE;
                } else {
                        s = utf8_tocstr(pr->buffer);
                        result = prompt_done(pr, s, redraw);
                        free(s);
                }
        }

changed:
        *redraw = 1;
        if (pr->flags & PROMPT_INCREMENTAL) {
                s = utf8_tocstr(pr->buffer);
                xasprintf(&cp, "%c%s", prefix, s);
                prompt_fire_callback(pr, cp, PROMPT_KEY_HANDLED, NULL);
                free(cp);
                free(s);
        }
        return (result);
}

/* Add to completion list. */
static void
prompt_complete_add(char ***list, u_int *size, const char *s)
{
        u_int   i;

        for (i = 0; i < *size; i++) {
                if (strcmp((*list)[i], s) == 0)
                        return;
        }
        *list = xreallocarray(*list, (*size) + 1, sizeof **list);
        (*list)[(*size)++] = xstrdup(s);
}

/* Build completion list. */
static char **
prompt_complete_commands(u_int *size, const char *s)
{
        char                            **list = NULL, *tmp;
        const char                      *value, *cp;
        const struct cmd_entry          **cmdent;
        size_t                           slen = strlen(s), valuelen;
        struct options_entry            *o;
        struct options_array_item       *a;

        *size = 0;
        for (cmdent = cmd_table; *cmdent != NULL; cmdent++) {
                if (strncmp((*cmdent)->name, s, slen) == 0)
                        prompt_complete_add(&list, size, (*cmdent)->name);
        }
        o = options_get_only(global_options, "command-alias");
        if (o != NULL) {
                a = options_array_first(o);
                while (a != NULL) {
                        value = options_array_item_value(a)->string;
                        if ((cp = strchr(value, '=')) == NULL)
                                goto next;
                        valuelen = cp - value;
                        if (slen > valuelen || strncmp(value, s, slen) != 0)
                                goto next;

                        xasprintf(&tmp, "%.*s", (int)valuelen, value);
                        prompt_complete_add(&list, size, tmp);
                        free(tmp);

                next:
                        a = options_array_next(a);
                }
        }
        return (list);
}

/* Find longest prefix. */
static char *
prompt_complete_prefix(char **list, u_int size)
{
        char     *out;
        u_int     i;
        size_t    j;

        if (list == NULL || size == 0)
                return (NULL);
        out = xstrdup(list[0]);
        for (i = 1; i < size; i++) {
                for (j = 0; out[j] != '\0' && list[i][j] != '\0'; j++) {
                        if (out[j] != list[i][j])
                                break;
                }
                out[j] = '\0';
        }
        return (out);
}

/* Sort complete list. */
static int
prompt_complete_sort(const void *a, const void *b)
{
        const char      **aa = (const char **)a, **bb = (const char **)b;

        return (strcmp(*aa, *bb));
}

/* Free the stored inline completion matches. */
static void
prompt_clear_complete(struct prompt *pr)
{
        u_int   i;

        for (i = 0; i < pr->complete_size; i++)
                free(pr->complete_list[i]);
        free(pr->complete_list);
        pr->complete_list = NULL;
        pr->complete_size = 0;

        free(pr->complete_display);
        pr->complete_display = NULL;
}

/*
 * Store the match list for inline display and build the dim suffix string: a
 * leading space then the matches separated by spaces.
 */
static void
prompt_store_complete(struct prompt *pr, char **list, u_int size)
{
        char    *display, *cp;
        u_int    i;

        prompt_clear_complete(pr);
        pr->complete_list = list;
        pr->complete_size = size;

        display = xstrdup("");
        for (i = 0; i < size; i++) {
                xasprintf(&cp, "%s %s", display, list[i]);
                free(display);
                display = cp;
        }
        pr->complete_display = display;
}

/*
 * Complete word. Returns the text to insert when a unique match or a longer
 * common prefix is available; otherwise stores the match list for inline
 * display (and returns NULL) or returns NULL if there is nothing to do.
 */
static char *
prompt_complete(struct prompt *pr, const char *word, u_int offset)
{
        char    **list = NULL, *out = NULL;
        u_int     size = 0, i;

        if (pr->type != PROMPT_TYPE_COMMAND || offset != 0 ||
            *word == '\0')
                return (NULL);

        list = prompt_complete_commands(&size, word);
        if (size == 0) {
                free(list);
                return (NULL);
        }
        qsort(list, size, sizeof *list, prompt_complete_sort);
        for (i = 0; i < size; i++)
                log_debug("complete %u: %s", i, list[i]);

        if (size == 1)
                xasprintf(&out, "%s ", list[0]);
        else
                out = prompt_complete_prefix(list, size);
        if (out != NULL && strcmp(word, out) == 0) {
                free(out);
                out = NULL;
        }

        if (out != NULL || size <= 1) {
                /* Inserting (or nothing to show): drop the list. */
                for (i = 0; i < size; i++)
                        free(list[i]);
                free(list);
                return (out);
        }

        /* Multiple matches but nothing to insert: keep them for redraw. */
        prompt_store_complete(pr, list, size);
        return (NULL);
}


/* Return the type of the prompt as an enum. */
enum prompt_type
prompt_type(const char *type)
{
        u_int   i;

        for (i = 0; i < PROMPT_NTYPES; i++) {
                if (strcmp(type, prompt_type_string(i)) == 0)
                        return (i);
        }
        return (PROMPT_TYPE_INVALID);
}

/* Get prompt type as a string. */
const char *
prompt_type_string(enum prompt_type type)
{
        switch (type) {
        case PROMPT_TYPE_COMMAND:
                return ("command");
        case PROMPT_TYPE_SEARCH:
                return ("search");
        case PROMPT_TYPE_INVALID:
                return ("invalid");
        }
        return ("unknown");
}