Files
tmux/prompt.c
nicm 51d037e881 Major rework of prompts. The basic prompt mechanics (draw, editing, etc)
are now wrapped up in prompt*.c and do not depend on a client. These
functions are used to provide the original client prompt but also to
allow panes to have their own prompts, which works much much better for
floating panes. The mode prompts for both the tree modes and copy mode
are switched over to be per pane.

There are some visible changes (some of these may be changed if they
don't seem to be working well):

- Prompts in modes now appear in the bottom line, covering whatever
  content was there.

- command-prompt has a -P flag to open a pane prompt.

- Because they cover the content, the default style for prompts in modes
  now does not fill the entire line; the main command prompt stays the
  same.

- The old completion menu has gone, and completions are now shown after
  the text. Builtin aliases are no longer completed.

- Clicking the mouse on the prompt now moves the cursor or selects a
  completion.
2026-06-25 11:39:11 +00:00

1596 lines
36 KiB
C

/* $OpenBSD$ */
/*
* 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;
enum screen_cursor_style cstyle;
enum screen_cursor_style command_cstyle;
int 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;
};
static char *prompt_complete(struct prompt *, const char *, u_int);
static void prompt_clear_complete(struct prompt *);
static char *prompt_expand(struct prompt *);
static int prompt_replace_complete(struct prompt *, const char *);
static u_int prompt_width(struct prompt *, u_int);
/* 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 (*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;
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);
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);
pd->ccolour = options_get_number(oo, "prompt-cursor-colour");
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->cstyle = pd->cstyle;
pr->command_cstyle = pd->command_cstyle;
pr->ccolour = pd->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->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,
struct screen_write_ctx *ctx, u_int offset, u_int pwidth, u_int *width,
struct grid_cell *gc)
{
struct utf8_data ud;
if (pr->flags & PROMPT_QUOTENEXT && ctx->s->cx == pcursor + 1) {
utf8_set(&ud, '^');
return (prompt_redraw_character(ctx, offset, pwidth,
width, 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);
}
/* Expand prompt string using the current input. */
static char *
prompt_expand(struct prompt *pr)
{
struct format_tree *ft;
char *expanded, *prompt, *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));
prompt = format_expand_time(ft, pr->string);
format_add(ft, "message", "%s", prompt);
if (pr->flags & PROMPT_COMMANDMODE)
format_add(ft, "command_prompt", "1");
else
format_add(ft, "command_prompt", "0");
expanded = format_expand_time(ft, pr->message_format);
free(prompt);
format_free(ft);
return (expanded);
}
/* Work out the width used by the prompt string. */
static u_int
prompt_width(struct prompt *pr, u_int aw)
{
char *expanded;
u_int start;
expanded = prompt_expand(pr);
start = format_width(expanded);
if (start > aw)
start = aw;
free(expanded);
return (start);
}
/* 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;
u_int aw = pd->area_width, *cx = pd->cursor_x;
struct grid_cell gc;
u_int i, offset, left, start, width;
u_int pcursor, pwidth;
char *expanded;
/* Choose the cursor colour and style for this prompt. */
if (pr->flags & PROMPT_COMMANDMODE) {
memcpy(&gc, &pr->command_style, sizeof gc);
s->default_cstyle = pr->command_cstyle;
s->default_mode = pr->command_cmode;
} else {
memcpy(&gc, &pr->style, sizeof gc);
s->default_cstyle = pr->cstyle;
s->default_mode = pr->cmode;
}
s->default_ccolour = pr->ccolour;
expanded = prompt_expand(pr);
start = format_width(expanded);
if (start > aw)
start = aw;
*cx = ax + start;
screen_write_cursormove(ctx, ax, py, 0);
format_draw(ctx, &gc, aw, expanded, NULL, 0);
screen_write_cursormove(ctx, ax + start, py, 0);
free(expanded);
left = aw - start;
if (left == 0)
return;
pcursor = utf8_strwidth(pr->buffer, pr->index);
pwidth = utf8_strwidth(pr->buffer, -1);
if (pr->flags & PROMPT_QUOTENEXT)
pwidth++;
if (pcursor >= left) {
/*
* The cursor would be outside the screen so start drawing
* with it on the right.
*/
offset = (pcursor - left) + 1;
pwidth = left;
} else
offset = 0;
if (pwidth > left)
pwidth = left;
*cx = ax + start + pcursor - offset;
width = 0;
for (i = 0; pr->buffer[i].size != 0; i++) {
if (!prompt_redraw_quote(pr, pcursor, ctx, offset, pwidth,
&width, &gc))
break;
if (!prompt_redraw_character(ctx, offset, pwidth, &width, &gc,
&pr->buffer[i]))
break;
}
prompt_redraw_quote(pr, pcursor, ctx, offset, pwidth, &width, &gc);
prompt_draw_complete(pr, ctx, ax, aw, *cx, py, &gc);
}
/* 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;
u_int cx, start, left, pcursor, pwidth, offset, width;
u_int target;
size_t idx;
if (x < ax || x >= ax + aw)
return (PROMPT_KEY_NOT_HANDLED);
if (pr->flags & PROMPT_INCREMENTAL)
return (PROMPT_KEY_HANDLED);
start = prompt_width(pr, aw);
left = aw - start;
if (left == 0)
return (PROMPT_KEY_HANDLED);
pcursor = utf8_strwidth(pr->buffer, pr->index);
pwidth = utf8_strwidth(pr->buffer, -1);
if (pr->flags & PROMPT_QUOTENEXT)
pwidth++;
if (pcursor >= left)
offset = (pcursor - left) + 1;
else
offset = 0;
cx = ax + start + pcursor - offset;
result = prompt_mouse_complete(pr, x, cx, ax, aw, redraw);
if (result != PROMPT_KEY_NOT_HANDLED)
return (result);
if (x <= ax + start)
target = offset;
else
target = offset + x - (ax + start);
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_LEFT:
case KEYC_RIGHT:
case KEYC_PPAGE:
case KEYC_NPAGE:
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");
}