/* $OpenBSD$ */ /* * Copyright (c) 2026 Nicholas Marriott * * 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 #include #include #include #include #include #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"); }