diff --git a/Makefile b/Makefile index cfab9c7bb..ec1f2ff56 100644 --- a/Makefile +++ b/Makefile @@ -78,6 +78,7 @@ SRCS= alerts.c \ environ.c \ file.c \ format.c \ + fuzzy.c \ format-draw.c \ grid-reader.c \ grid-view.c \ @@ -133,6 +134,7 @@ SRCS= alerts.c \ window-clock.c \ window-copy.c \ window-customize.c \ + window-switch.c \ window-visible.c \ window-tree.c \ window.c \ @@ -145,7 +147,7 @@ CDIAGFLAGS+= -Wundef -Wbad-function-cast -Winline -Wcast-align CFLAGS += -I${.CURDIR} -LDADD= -lutil -lcurses -levent -lm -DPADD= ${LIBUTIL} ${LIBCURSES} ${LIBEVENT} ${LIBM} +LDADD= -lutil -lcurses -levent -lm +DPADD= ${LIBUTIL} ${LIBCURSES} ${LIBEVENT} ${LIBM} .include diff --git a/cmd-choose-tree.c b/cmd-choose-tree.c index 87c29419c..4a6bd152b 100644 --- a/cmd-choose-tree.c +++ b/cmd-choose-tree.c @@ -84,6 +84,19 @@ const struct cmd_entry cmd_customize_mode_entry = { .exec = cmd_choose_tree_exec }; +const struct cmd_entry cmd_switch_mode_entry = { + .name = "switch-mode", + .alias = NULL, + + .args = { "F:kst:wZ", 0, 1, cmd_choose_tree_args_parse }, + .usage = "[-kswZ] [-F format] " CMD_TARGET_PANE_USAGE " [command]", + + .target = { 't', CMD_FIND_PANE, 0 }, + + .flags = 0, + .exec = cmd_choose_tree_exec +}; + static enum args_parse_type cmd_choose_tree_args_parse(__unused struct args *args, __unused u_int idx, __unused char **cause) @@ -116,6 +129,8 @@ cmd_choose_tree_exec(struct cmd *self, struct cmdq_item *item) mode = &window_client_mode; } else if (cmd_get_entry(self) == &cmd_customize_mode_entry) mode = &window_customize_mode; + else if (cmd_get_entry(self) == &cmd_switch_mode_entry) + mode = &window_switch_mode; else mode = &window_tree_mode; diff --git a/cmd.c b/cmd.c index d20b87366..a2d3fad70 100644 --- a/cmd.c +++ b/cmd.c @@ -116,6 +116,7 @@ extern const struct cmd_entry cmd_suspend_client_entry; extern const struct cmd_entry cmd_swap_pane_entry; extern const struct cmd_entry cmd_swap_window_entry; extern const struct cmd_entry cmd_switch_client_entry; +extern const struct cmd_entry cmd_switch_mode_entry; extern const struct cmd_entry cmd_unbind_key_entry; extern const struct cmd_entry cmd_unlink_window_entry; extern const struct cmd_entry cmd_wait_for_entry; @@ -209,6 +210,7 @@ const struct cmd_entry *cmd_table[] = { &cmd_swap_pane_entry, &cmd_swap_window_entry, &cmd_switch_client_entry, + &cmd_switch_mode_entry, &cmd_unbind_key_entry, &cmd_unlink_window_entry, &cmd_wait_for_entry, diff --git a/format.c b/format.c index 75b00e2bf..8af01342d 100644 --- a/format.c +++ b/format.c @@ -4537,6 +4537,47 @@ format_build_modifiers(struct format_expand_state *es, const char **s, return (list); } +/* Match using the fuzzy matcher. */ +static char * +format_match_fuzzy(const char *pattern, const char *text, int positions) +{ + struct evbuffer *buffer; + bitstr_t *bs; + char *value; + size_t size; + u_int i, width; + + width = format_width(text); + if (width == 0) + width = 1; + bs = fuzzy_match(pattern, text, width, NULL); + if (bs == NULL) + return (xstrdup(positions ? "" : "0")); + + if (!positions) { + free(bs); + return (xstrdup("1")); + } + + buffer = evbuffer_new(); + if (buffer == NULL) + fatalx("out of memory"); + for (i = 0; i < width; i++) { + if (!bit_test(bs, i)) + continue; + if (EVBUFFER_LENGTH(buffer) != 0) + evbuffer_add(buffer, ",", 1); + evbuffer_add_printf(buffer, "%u", i); + } + if ((size = EVBUFFER_LENGTH(buffer)) != 0) + value = xmemdup(EVBUFFER_DATA(buffer), size); + else + value = xstrdup(""); + evbuffer_free(buffer); + free(bs); + return (value); +} + /* Match against an fnmatch(3) pattern or regular expression. */ static char * format_match(struct format_modifier *fm, const char *pattern, const char *text) @@ -4547,6 +4588,10 @@ format_match(struct format_modifier *fm, const char *pattern, const char *text) if (fm->argc >= 1) s = fm->argv[0]; + if (strchr(s, 'p') != NULL) + return (format_match_fuzzy(pattern, text, 1)); + if (strchr(s, 'z') != NULL) + return (format_match_fuzzy(pattern, text, 0)); if (strchr(s, 'r') == NULL) { if (strchr(s, 'i') != NULL) flags |= FNM_CASEFOLD; diff --git a/fuzzy.c b/fuzzy.c new file mode 100644 index 000000000..00fc90687 --- /dev/null +++ b/fuzzy.c @@ -0,0 +1,655 @@ +/* $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 "tmux.h" + +/* + * Fuzzy matching in the style of fzf. The pattern is split into groups by | + * and each group is split on spaces into terms. A row matches if any group + * matches; within a group all positive terms must match and all inverse terms + * must not match. + * + * Plain positive terms are fuzzy subsequences. A leading ' makes a term an + * exact substring match, ^ anchors a term at the start and $ anchors it at + * the end. A leading ! inverts the term. Plain inverse terms are exact + * substring matches rather than inverse fuzzy matches, like fzf. + * + * Both the pattern and the text are UTF-8. The text may contain tmux style + * directives (#[...]); these and their contents are invisible to matching and + * occupy no columns, but align= styles do move the surrounding text and are + * accounted for exactly as format_draw lays it out (the no-list layout, see + * format_draw_none). Matching is smart-case: case is ignored unless the pattern + * contains an uppercase character (ASCII case folding only; other characters + * are compared exactly by their UTF-8 data). + * + * On a match a bitstr_t of the requested display width is returned with a bit + * set for every column occupied by a matched character, so the caller can + * highlight them; NULL is returned if there is no match. A cheap fzf-style + * score (matches at the start, after word boundaries and in contiguous runs + * score higher) is also produced so callers can rank best-match-first. + */ + +#define FUZZY_BONUS_EXACT 1000 +#define FUZZY_BONUS_PREFIX 200 +#define FUZZY_BONUS_SUFFIX 100 +#define FUZZY_BONUS_START 12 +#define FUZZY_BONUS_BOUNDARY 8 +#define FUZZY_BONUS_CONSECUTIVE 6 +#define FUZZY_PENALTY_LEADING 1 +#define FUZZY_PENALTY_LEADING_MAX 10 +#define FUZZY_PENALTY_GAP 1 + +/* A single visible character of the text. */ +struct fuzzy_char { + enum style_align align; + struct utf8_data ud; /* original UTF-8 data */ + u_int width; /* display width */ + u_int offset; /* within its alignment */ +}; + +/* One parsed query term. */ +struct fuzzy_term { + int inverse; + int exact; + int prefix; + int suffix; + const char *text; + size_t len; +}; + +/* Is this character a word boundary, so a match after it scores higher? */ +static int +fuzzy_is_boundary(const struct utf8_data *ud) +{ + static const char *boundary = " -_/.:"; + + if (ud->size != 1) + return (0); + return (strchr(boundary, ud->data[0]) != NULL); +} + +/* + * Compare two characters, folding ASCII case if wanted. UTF-8 is compared + * directly without case folding. + */ +static int +fuzzy_char_equal(const struct utf8_data *a, const struct utf8_data *b, int fold) +{ + if (fold && + a->size == 1 && + b->size == 1 && + a->data[0] < 0x80 && + b->data[0] < 0x80) + return (tolower(a->data[0]) == tolower(b->data[0])); + return (a->size == b->size && memcmp(a->data, b->data, a->size) == 0); +} + +/* Map a style alignment onto one of the four layout columns. */ +static enum style_align +fuzzy_align(enum style_align align) +{ + if (align == STYLE_ALIGN_DEFAULT) + return (STYLE_ALIGN_LEFT); + return (align); +} + +/* Add a visible character to the array, updating the alignment width. */ +static void +fuzzy_add(struct fuzzy_char **cs, u_int *ncs, u_int *alloc, enum style_align a, + const struct utf8_data *ud, u_int *widths) +{ + struct fuzzy_char *fc; + + if (*ncs == *alloc) { + *alloc = (*alloc == 0) ? 64 : *alloc * 2; + *cs = xreallocarray(*cs, *alloc, sizeof **cs); + } + fc = &(*cs)[(*ncs)++]; + fc->align = a; + memcpy(&fc->ud, ud, sizeof fc->ud); + fc->width = ud->width; + fc->offset = widths[a]; + widths[a] += ud->width; +} + +/* Decode a character as UTF-8. */ +static const char * +fuzzy_decode_one(const char *cp, const char *end, struct utf8_data *ud) +{ + enum utf8_state more; + const char *start = cp; + + if ((more = utf8_open(ud, (u_char)*cp)) == UTF8_MORE) { + while (++cp != end && more == UTF8_MORE) + more = utf8_append(ud, (u_char)*cp); + if (more == UTF8_DONE) + return (cp); + cp = start; + } + utf8_set(ud, (u_char)*cp); + return (cp + 1); +} + +/* + * Scan the text into an array of visible characters, skipping styles and + * recording the alignment and intra-alignment offset of each. Returns the + * array and its length and fills in the per-alignment widths. + */ +static struct fuzzy_char * +fuzzy_scan(const char *text, u_int *ncs, u_int *widths) +{ + struct fuzzy_char *cs = NULL; + u_int alloc = 0, n, leading, i; + enum style_align current = STYLE_ALIGN_LEFT; + struct style sy; + const char *cp = text, *textend = text + strlen(text); + const char *end; + struct utf8_data ud, hash, bracket; + char *tmp; + + *ncs = 0; + memset(widths, 0, sizeof *widths * (STYLE_ALIGN_ABSOLUTE_CENTRE + 1)); + style_set(&sy, &grid_default_cell); + utf8_set(&hash, '#'); + utf8_set(&bracket, '['); + + while (*cp != '\0') { + /* Handle a run of #s, which may introduce a style. */ + if (*cp == '#') { + for (n = 0; cp[n] == '#'; n++) + /* nothing */; + if (cp[n] != '[') { + /* Escaped #s: ##->#, so half (rounded up). */ + leading = (n % 2 == 0) ? n / 2 : n / 2 + 1; + for (i = 0; i < leading; i++) { + fuzzy_add(&cs, ncs, &alloc, current, + &hash, widths); + } + cp += n; + continue; + } + + /* Even count: all #s escaped, the [ is literal. */ + for (i = 0; i < n / 2; i++) + fuzzy_add(&cs, ncs, &alloc, current, &hash, + widths); + if (n % 2 == 0) { + fuzzy_add(&cs, ncs, &alloc, current, &bracket, + widths); + cp += n + 1; + continue; + } + + /* Odd count: this is a style, find and parse it. */ + end = format_skip(cp + n + 1, "]"); + if (end == NULL) + break; + tmp = xstrndup(cp + n + 1, end - (cp + n + 1)); + if (style_parse(&sy, &grid_default_cell, tmp) == 0) + current = fuzzy_align(sy.align); + free(tmp); + cp = end + 1; + continue; + } + + /* Decode one character, multibyte or single byte. */ + cp = fuzzy_decode_one(cp, textend, &ud); + + /* + * Skip non-printable single bytes (control characters and raw + * bytes left over from a failed decode); keep printable ASCII + * and any decoded UTF-8. + */ + if (ud.size == 1 && (ud.data[0] <= 0x1f || ud.data[0] >= 0x7f)) + continue; + fuzzy_add(&cs, ncs, &alloc, current, &ud, widths); + } + return (cs); +} + +/* + * Work out the display column of a visible character given the trimmed widths + * and start columns of each alignment. Returns 0 and sets the column if the + * character is visible, otherwise returns -1. + */ +static int +fuzzy_column(const struct fuzzy_char *fc, const u_int *start, const u_int *src, + const u_int *vis, u_int *column) +{ + enum style_align a = fc->align; + + if (fc->offset < src[a] || fc->offset >= src[a] + vis[a]) + return (-1); + *column = start[a] + (fc->offset - src[a]); + return (0); +} + +/* Decode a UTF-8 term into an array of characters. */ +static u_int +fuzzy_decode(const char *tok, size_t len, struct utf8_data *out) +{ + const char *cp = tok, *end = tok + len; + u_int n = 0; + + while (cp != end) + cp = fuzzy_decode_one(cp, end, &out[n++]); + return (n); +} + +/* Add the score for a fuzzy token matched at the given positions. */ +static int +fuzzy_score_positions(const u_int *pos, u_int npos, const struct fuzzy_char *cs) +{ + u_int i, gap, span; + int score = 0; + + if (npos == 0) + return (0); + if (pos[0] == 0) + score += FUZZY_BONUS_START; + else { + if (fuzzy_is_boundary(&cs[pos[0] - 1].ud)) + score += FUZZY_BONUS_BOUNDARY; + if (pos[0] < FUZZY_PENALTY_LEADING_MAX) + score -= pos[0] * FUZZY_PENALTY_LEADING; + else { + score -= FUZZY_PENALTY_LEADING_MAX * + FUZZY_PENALTY_LEADING; + } + } + for (i = 1; i < npos; i++) { + if (pos[i] == pos[i - 1] + 1) + score += FUZZY_BONUS_CONSECUTIVE; + else if (fuzzy_is_boundary(&cs[pos[i] - 1].ud)) + score += FUZZY_BONUS_BOUNDARY; + } + span = pos[npos - 1] - pos[0] + 1; + gap = span - npos; + score -= gap * FUZZY_PENALTY_GAP; + return (score); +} + +/* + * Match a token as a subsequence of the visible characters. Returns if the + * token matches. + */ +static int +fuzzy_match_fuzzy(const struct utf8_data *tok, u_int toklen, + struct fuzzy_char *cs, u_int ncs, int fold, int *score, char *matched) +{ + u_int pi, ci, *pos; + int found, value; + + if (toklen == 0 || ncs == 0) + return (0); + pos = xcalloc(toklen, sizeof *pos); + + /* First find a subsequence from the start. */ + ci = 0; + for (pi = 0; pi < toklen; pi++) { + while (ci != ncs && + !fuzzy_char_equal(&tok[pi], &cs[ci].ud, fold)) + ci++; + if (ci == ncs) { + free(pos); + return (0); + } + pos[pi] = ci++; + } + + /* Then compact it backwards to prefer a shorter span. */ + ci = pos[toklen - 1]; + for (pi = toklen; pi > 0; pi--) { + found = 0; + for (;;) { + if (fuzzy_char_equal(&tok[pi - 1], &cs[ci].ud, fold)) { + pos[pi - 1] = ci; + found = 1; + break; + } + if (ci == 0) + break; + ci--; + } + if (!found) { + free(pos); + return (0); + } + if (pi != 1) + ci--; + } + + value = fuzzy_score_positions(pos, toklen, cs); + *score += value; + for (pi = 0; pi < toklen; pi++) + matched[pos[pi]] = 1; + free(pos); + return (1); +} + +/* Score an exact, prefix or suffix match. */ +static int +fuzzy_score_exact(u_int start, u_int toklen, u_int ncs, + const struct fuzzy_char *cs, int prefix, int suffix) +{ + int score; + + score = FUZZY_BONUS_EXACT + toklen * FUZZY_BONUS_CONSECUTIVE; + if (prefix) + score += FUZZY_BONUS_PREFIX; + if (suffix) + score += FUZZY_BONUS_SUFFIX; + if (start == 0) + score += FUZZY_BONUS_START; + else if (fuzzy_is_boundary(&cs[start - 1].ud)) + score += FUZZY_BONUS_BOUNDARY; + if (start < FUZZY_PENALTY_LEADING_MAX) + score -= start * FUZZY_PENALTY_LEADING; + else + score -= FUZZY_PENALTY_LEADING_MAX * FUZZY_PENALTY_LEADING; + if (!prefix && !suffix) + score -= ncs - (start + toklen); + return (score); +} + +/* Match an exact, prefix or suffix term against the visible characters. */ +static int +fuzzy_match_exact(const struct utf8_data *tok, u_int toklen, + struct fuzzy_char *cs, u_int ncs, int fold, int prefix, int suffix, + int *score, char *matched) +{ + u_int start, end, i, j, best = 0; + int ok, found = 0, value, bestscore = 0; + + if (toklen == 0 || toklen > ncs) + return (0); + + if (prefix && suffix) { + if (toklen != ncs) + return (0); + start = 0; + end = 1; + } else if (prefix) { + start = 0; + end = 1; + } else if (suffix) { + start = ncs - toklen; + end = start + 1; + } else { + start = 0; + end = ncs - toklen + 1; + } + + for (i = start; i < end; i++) { + ok = 1; + for (j = 0; j < toklen; j++) { + if (!fuzzy_char_equal(&tok[j], &cs[i + j].ud, fold)) { + ok = 0; + break; + } + } + if (!ok) + continue; + value = fuzzy_score_exact(i, toklen, ncs, cs, prefix, suffix); + if (!found || value > bestscore) { + found = 1; + best = i; + bestscore = value; + } + } + if (!found) + return (0); + *score += bestscore; + if (matched != NULL) { + for (i = 0; i < toklen; i++) + matched[best + i] = 1; + } + return (1); +} + +/* Parse one term. */ +static int +fuzzy_parse_term(const char *start, const char *end, struct fuzzy_term *term) +{ + memset(term, 0, sizeof *term); + if (start == end) + return (0); + if (*start == '!') { + term->inverse = 1; + start++; + } + if (start == end) + return (0); + if (*start == '\'') { + term->exact = 1; + start++; + } else if (*start == '^') { + term->exact = 1; + term->prefix = 1; + start++; + } + if (start == end) + return (0); + if (end[-1] == '$') { + term->exact = 1; + term->suffix = 1; + end--; + } + if (start == end) + return (0); + + if (term->inverse) + term->exact = 1; + term->text = start; + term->len = end - start; + return (1); +} + +/* Match one parsed term. */ +static int +fuzzy_match_term(const struct fuzzy_term *term, struct utf8_data *tok, + struct fuzzy_char *cs, u_int ncs, int fold, int *score, char *matched) +{ + u_int toklen; + int value = 0, matched_term; + + toklen = fuzzy_decode(term->text, term->len, tok); + if (term->exact) { + matched_term = fuzzy_match_exact(tok, toklen, cs, ncs, fold, + term->prefix, term->suffix, &value, + term->inverse ? NULL : matched); + } else { + matched_term = fuzzy_match_fuzzy(tok, toklen, cs, ncs, fold, + &value, term->inverse ? NULL : matched); + } + + if (term->inverse) + return (!matched_term); + if (!matched_term) + return (0); + *score += value; + return (1); +} + +/* Match one AND group of terms. */ +static int +fuzzy_match_group(const char *start, const char *end, struct utf8_data *tok, + struct fuzzy_char *cs, u_int ncs, int fold, int *score, char *matched) +{ + const char *cp = start, *sp; + struct fuzzy_term term; + int any = 0; + + *score = 0; + while (cp != end) { + while (cp != end && *cp == ' ') + cp++; + if (cp == end) + break; + sp = cp; + while (cp != end && *cp != ' ') + cp++; + if (!fuzzy_parse_term(sp, cp, &term)) + return (0); + any = 1; + if (!fuzzy_match_term(&term, tok, cs, ncs, fold, score, + matched)) + return (0); + } + return (any); +} + +/* + * Fuzzy match pattern against text, which is drawn into a region of the given + * display width. Returns a bitstr_t of width bits with a bit set for each + * column occupied by a matched character, or NULL if there is no match. A + * higher returned score is better. + */ +bitstr_t * +fuzzy_match(const char *pattern, const char *text, u_int width, u_int *score) +{ + struct fuzzy_char *cs; + char *matched = NULL, *best = NULL, *groupmatched; + struct utf8_data *tok; + bitstr_t *mask; + u_int ncs, i, j, column; + u_int widths[STYLE_ALIGN_ABSOLUTE_CENTRE + 1]; + u_int start[STYLE_ALIGN_ABSOLUTE_CENTRE + 1]; + u_int src[STYLE_ALIGN_ABSOLUTE_CENTRE + 1]; + u_int vis[STYLE_ALIGN_ABSOLUTE_CENTRE + 1]; + u_int wl, wc, wr, wa; + const char *cp, *sp; + int bestscore = 0, groupscore, found = 0, fold; + + if (width == 0) + return (NULL); + + /* An empty query matches everything, with nothing highlighted. */ + for (cp = pattern; *cp == ' ' || *cp == '|'; cp++) + /* nothing */; + if (*cp == '\0') { + if (score != NULL) + *score = 0; + return (bit_alloc(width)); + } + + /* Smart-case: fold unless the pattern has an uppercase character. */ + fold = 1; + for (cp = pattern; *cp != '\0'; cp++) { + if (*cp >= 'A' && *cp <= 'Z') { + fold = 0; + break; + } + } + + /* Scan the text into visible characters. */ + cs = fuzzy_scan(text, &ncs, widths); + matched = xcalloc(ncs == 0 ? 1 : ncs, sizeof *matched); + best = xcalloc(ncs == 0 ? 1 : ncs, sizeof *best); + tok = xreallocarray(NULL, strlen(pattern) + 1, sizeof *tok); + + /* Match each |-separated group and keep the best-scoring one. */ + cp = pattern; + while (*cp != '\0') { + while (*cp == ' ' || *cp == '|') + cp++; + if (*cp == '\0') + break; + sp = cp; + while (*cp != '\0' && *cp != '|') + cp++; + memset(matched, 0, ncs == 0 ? 1 : ncs); + groupmatched = matched; + if (fuzzy_match_group(sp, cp, tok, cs, ncs, fold, + &groupscore, groupmatched)) { + if (!found || groupscore > bestscore) { + found = 1; + bestscore = groupscore; + memcpy(best, matched, ncs == 0 ? 1 : ncs); + } + } + } + free(tok); + if (!found) { + free(best); + free(matched); + free(cs); + return (NULL); + } + + /* + * Work out the trimmed widths and start columns of each alignment, + * mirroring format_draw_none. + */ + wl = widths[STYLE_ALIGN_LEFT]; + wc = widths[STYLE_ALIGN_CENTRE]; + wr = widths[STYLE_ALIGN_RIGHT]; + wa = widths[STYLE_ALIGN_ABSOLUTE_CENTRE]; + while (wl + wc + wr > width) { + if (wc > 0) + wc--; + else if (wr > 0) + wr--; + else + wl--; + } + if (wa > width) + wa = width; + + start[STYLE_ALIGN_LEFT] = 0; + src[STYLE_ALIGN_LEFT] = 0; + vis[STYLE_ALIGN_LEFT] = wl; + + start[STYLE_ALIGN_RIGHT] = width - wr; + src[STYLE_ALIGN_RIGHT] = widths[STYLE_ALIGN_RIGHT] - wr; + vis[STYLE_ALIGN_RIGHT] = wr; + + start[STYLE_ALIGN_CENTRE] = + wl + ((width - wr) - wl) / 2 - wc / 2; + src[STYLE_ALIGN_CENTRE] = widths[STYLE_ALIGN_CENTRE] / 2 - wc / 2; + vis[STYLE_ALIGN_CENTRE] = wc; + + start[STYLE_ALIGN_ABSOLUTE_CENTRE] = (width - wa) / 2; + src[STYLE_ALIGN_ABSOLUTE_CENTRE] = 0; + vis[STYLE_ALIGN_ABSOLUTE_CENTRE] = wa; + + /* Set a bit for each column of each matched character. */ + mask = bit_alloc(width); + for (i = 0; i < ncs; i++) { + if (!best[i]) + continue; + if (fuzzy_column(&cs[i], start, src, vis, &column) != 0) + continue; + for (j = 0; j < cs[i].width && column + j < width; j++) + bit_set(mask, column + j); + } + + free(best); + free(matched); + free(cs); + + if (score != NULL) + *score = (bestscore < 0) ? 0 : (u_int)bestscore; + return (mask); +} diff --git a/key-bindings.c b/key-bindings.c index 02ec14dfc..e5a58a94e 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -402,6 +402,8 @@ key_bindings_init(void) "bind -N 'Redraw the current client' r { refresh-client }", "bind -N 'Choose a session from a list' s { choose-tree -Zs }", "bind -N 'Show a clock' t { clock-mode }", + "bind -N 'Switch to a window' Tab { new-pane -E -x75% -y30% -X0 -Y0; move-pane -P bottom-centre; switch-mode -wk }", + "bind -N 'Switch to a session' BTab { new-pane -E -x75% -y30% -X0 -Y0; move-pane -P bottom-centre; switch-mode -sk }", "bind -N 'Choose a window from a list' w { choose-tree -Zw }", "bind -N 'Kill the active pane' x { confirm-before -p\"kill-pane #P? (y/n)\" kill-pane }", "bind -N 'Zoom the active pane' z { resize-pane -Z }", diff --git a/mode-tree.c b/mode-tree.c index afa04bd0f..fae0f3d8a 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -794,8 +794,8 @@ mode_tree_no_tag(struct mode_tree_item *mti) } /* - * Set the alignment of mti->name: -1 to align left, 0 (default) to not align, - * or 1 to align right. + * Set the alignment of the item name: -1 to align left, 0 (default) to not + * align, or 1 to align right. */ void mode_tree_align(struct mode_tree_item *mti, int align) @@ -1144,6 +1144,7 @@ mode_tree_set_prompt(struct mode_tree_data *mtd, struct client *c, mtp = xcalloc(1, sizeof *mtp); mtp->mtd = mtd; + mtp->c = c; mtp->inputcb = inputcb; mtp->freecb = freecb; mtp->data = data; diff --git a/options-table.c b/options-table.c index f73335681..ddd8b4634 100644 --- a/options-table.c +++ b/options-table.c @@ -1673,6 +1673,15 @@ const struct options_table_entry options_table[] = { "history when clearing the whole screen." }, + { .name = "switch-mode-match-style", + .type = OPTIONS_TABLE_STRING, + .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, + .default_str = "bg=cyan fg=black", + .flags = OPTIONS_TABLE_IS_STYLE, + .separator = ",", + .text = "Style of matched characters in switch mode." + }, + { .name = "synchronize-panes", .type = OPTIONS_TABLE_FLAG, .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, diff --git a/prompt.c b/prompt.c index 4612a2e5f..1ab447ebe 100644 --- a/prompt.c +++ b/prompt.c @@ -98,6 +98,8 @@ prompt_flags_to_string(int flags) 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); @@ -536,8 +538,6 @@ prompt_mouse(struct prompt *pr, u_int x, u_int ax, u_int aw, int *redraw) 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; @@ -1060,11 +1060,14 @@ prompt_check_move(struct prompt *pr, key_code key) switch (key) { case KEYC_UP: case KEYC_DOWN: - case KEYC_LEFT: - case KEYC_RIGHT: 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); } diff --git a/tmux.1 b/tmux.1 index 3df69e546..58d055cff 100644 --- a/tmux.1 +++ b/tmux.1 @@ -360,6 +360,8 @@ Choose the current window interactively. Kill the current pane. .It z Toggle zoom state of the current pane. +.It Tab +Choose a new window and session by fuzzy matching. .It { Move floating pane to top-left corner. .It } @@ -3086,6 +3088,51 @@ The .Ic customize-mode command works only if at least one client is attached. .It Xo +.Ic switch\-mode +.Op Fl kswZ +.Op Fl F Ar format +.Op Fl t Ar target\-pane +.Op Ar command +.Xc +Put a pane into switch mode, where a session or window may be chosen +interactively from a list. +Each session or window is shown on one line and the list is narrowed by typing: +the typed text is matched against each item with fuzzy +matching and only matching items are shown, sorted by how well they match. +.Fl s +lists sessions (the default) and +.Fl w +lists windows. +.Fl Z +zooms the pane. +The following keys may be used in switch mode: +.Bl -column "KeyXXX" "Function" -offset indent +.It Sy "Key" Ta Sy "Function" +.It Li "Enter" Ta "Choose the selected item" +.It Li "Up" Ta "Select the previous item" +.It Li "Down" Ta "Select the next item" +.It Li "Escape" Ta "Exit mode" +.El +.Pp +After a session or window is chosen, the first instance of +.Ql %% +and all instances of +.Ql %1 +are replaced by the target in +.Ar command +and the result executed as a command. +If +.Ar command +is not given, "switch\-client \-Zt \[aq]%%\[aq]" is used. +.Fl F +specifies the format for each item in the list. +.Fl k +kills the pane when the mode is exited. +.Pp +The appearance of matched characters is controlled by the +.Ic switch\-mode\-match\-style +option. +.It Xo .Tg displayp .Ic display\-panes .Op Fl bN @@ -6100,6 +6147,15 @@ is enabled. When the entire screen is cleared and this option is on, scroll the contents of the screen into history before clearing it. .Pp +.It Ic switch\-mode\-match\-style Ar style +Set the style of characters matched by the filter in +.Ic switch\-mode . +For how to specify +.Ar style , +see the +.Sx STYLES +section. +.Pp .It Xo Ic synchronize\-panes .Op Ic on | off .Xc @@ -6492,13 +6548,36 @@ An optional argument specifies flags: .Ql r means the pattern is a regular expression instead of the default .Xr glob 7 -pattern and +pattern; .Ql i -means to ignore case. +means to ignore case; +.Ql z +means to do a fuzzy match; +.Ql p +is like +.Ql z +but returns a list of matched positions. +A fuzzy match matches plain terms as sequences where each character must appear +in order but not necessarily consecutively; terms beginning with +.Ql ' +are exact substring matches, +.Ql ^ +anchors a term at the start, +.Ql $ +anchors it at the end, +.Ql ! +inverts a term and +.Ql | +separates alternative groups. For example: .Ql #{m:*foo*,#{host}} or -.Ql #{m/ri:\[ha]A,MYVAR} . +.Ql #{m/ri:\[ha]A,MYVAR} +or +.Ql #{m/z:dev bash,dev:1 bash} +or +.Ql #{m/z:^dev | ^prod,prod:1 ssh} . +.Pp A .Ql C performs a search for a diff --git a/tmux.h b/tmux.h index c6ddaf6e7..d80e9b793 100644 --- a/tmux.h +++ b/tmux.h @@ -2054,6 +2054,7 @@ typedef void (*prompt_free_cb)(void *); #define PROMPT_COMMANDMODE 0x200 #define PROMPT_ISPANE 0x400 #define PROMPT_ISMODE 0x800 +#define PROMPT_EDITARROWS 0x1000 /* Prompt create data. */ struct prompt_create_data { @@ -3269,6 +3270,9 @@ void colour_palette_from_option(struct colour_palette *, struct options *); const char *attributes_tostring(int); int attributes_fromstring(const char *); +/* fuzzy.c */ +bitstr_t *fuzzy_match(const char *, const char *, u_int, u_int *); + /* grid.c */ extern const struct grid_cell grid_default_cell; void grid_empty_line(struct grid *, u_int, u_int); @@ -3725,6 +3729,9 @@ extern const struct window_mode window_buffer_mode; /* window-tree.c */ extern const struct window_mode window_tree_mode; +/* window-switch.c */ +extern const struct window_mode window_switch_mode; + /* window-clock.c */ extern const struct window_mode window_clock_mode; extern const char window_clock_table[14][5][5]; diff --git a/window-switch.c b/window-switch.c new file mode 100644 index 000000000..99ea19985 --- /dev/null +++ b/window-switch.c @@ -0,0 +1,636 @@ +/* $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 "tmux.h" + +static struct screen *window_switch_init(struct window_mode_entry *, + struct cmd_find_state *, struct args *); +static void window_switch_free(struct window_mode_entry *); +static void window_switch_resize(struct window_mode_entry *, u_int, + u_int); +static void window_switch_key(struct window_mode_entry *, + struct client *, struct session *, + struct winlink *, key_code, struct mouse_event *); +static enum prompt_result window_switch_prompt_callback(void *, const char *, + enum prompt_key_result); + +#define WINDOW_SWITCH_DEFAULT_COMMAND "switch-client -Zt '%%'" + +#define WINDOW_SWITCH_DEFAULT_FORMAT \ + "#{?window_format," \ + "#{window_name} " \ + "#[dim]#{session_name}:#{window_index}#{window_flags}#[default] " \ + "#[dim]#{pane_current_command}#[default] " \ + "#[dim]#{?#{!=:#{pane_title},#{host_short}},#{pane_title},}#[default]" \ + "," \ + "#{session_name} " \ + "#[dim]#{session_windows} windows#[default] " \ + "#{?session_attached,attached,#[dim]detached#[default]} " \ + "#[dim]#{window_name}#[default]" \ + "}" + +const struct window_mode window_switch_mode = { + .name = "switch-mode", + .default_format = WINDOW_SWITCH_DEFAULT_FORMAT, + + .init = window_switch_init, + .free = window_switch_free, + .resize = window_switch_resize, + .key = window_switch_key, +}; + +enum window_switch_type { + WINDOW_SWITCH_TYPE_SESSION, + WINDOW_SWITCH_TYPE_WINDOW +}; + +struct window_switch_itemdata { + enum window_switch_type type; + int session; + int winlink; + + uint64_t tag; + char *text; + bitstr_t *match; + + u_int score; + u_int order; +}; + +struct window_switch_modedata { + struct window_pane *wp; + struct screen screen; + int zoomed; + + char *format; + char *command; + + enum window_switch_type type; + char *filter; + struct prompt *prompt; + u_int prompt_cx; + + struct window_switch_itemdata **item_list; + u_int item_size; + + struct window_switch_itemdata **matches; + u_int matches_size; + + u_int current; + u_int offset; +}; + +static void +window_switch_free_item(struct window_switch_itemdata *item) +{ + free(item->match); + free(item->text); + free(item); +} + +static struct window_switch_itemdata * +window_switch_add_item(struct window_switch_modedata *data) +{ + struct window_switch_itemdata *item; + + data->item_list = xreallocarray(data->item_list, data->item_size + 1, + sizeof *data->item_list); + item = data->item_list[data->item_size++] = xcalloc(1, sizeof *item); + return (item); +} + +static void +window_switch_add_session(struct window_switch_modedata *data, + struct session *s, u_int *order) +{ + struct window_switch_itemdata *item; + struct format_tree *ft; + + ft = format_create(NULL, NULL, FORMAT_NONE, 0); + format_defaults(ft, NULL, s, NULL, NULL); + + item = window_switch_add_item(data); + item->type = WINDOW_SWITCH_TYPE_SESSION; + item->session = s->id; + item->winlink = -1; + item->tag = (uint64_t)s; + item->order = (*order)++; + item->text = format_expand(ft, data->format); + + format_free(ft); +} + +static void +window_switch_add_window(struct window_switch_modedata *data, + struct winlink *wl, u_int *order) +{ + struct window_switch_itemdata *item; + struct format_tree *ft; + + ft = format_create(NULL, NULL, FORMAT_NONE, 0); + format_defaults(ft, NULL, wl->session, wl, NULL); + + item = window_switch_add_item(data); + item->type = WINDOW_SWITCH_TYPE_WINDOW; + item->session = wl->session->id; + item->winlink = wl->idx; + item->tag = (uint64_t)wl; + item->order = (*order)++; + item->text = format_expand(ft, data->format); + + format_free(ft); +} + +static int +window_switch_compare(const void *a0, const void *b0) +{ + struct window_switch_itemdata *const *a = a0; + struct window_switch_itemdata *const *b = b0; + + if ((*a)->score > (*b)->score) + return (-1); + if ((*a)->score < (*b)->score) + return (1); + if ((*a)->order < (*b)->order) + return (-1); + if ((*a)->order > (*b)->order) + return (1); + return (0); +} + +static void +window_switch_build(struct window_switch_modedata *data) +{ + struct window_switch_itemdata *item, **m = NULL; + const char *f = data->filter; + u_int ns, nw, i, n = 0, order = 0; + u_int sx = screen_size_x(&data->screen); + struct session **sl; + struct winlink **wl; + struct sort_criteria sort_crit; + + sort_crit.order = SORT_NAME; + sort_crit.reversed = 0; + + for (i = 0; i < data->item_size; i++) + window_switch_free_item(data->item_list[i]); + free(data->item_list); + data->item_list = NULL; + data->item_size = 0; + + switch (data->type) { + case WINDOW_SWITCH_TYPE_SESSION: + sl = sort_get_sessions(&ns, &sort_crit); + for (i = 0; i < ns; i++) + window_switch_add_session(data, sl[i], &order); + break; + case WINDOW_SWITCH_TYPE_WINDOW: + wl = sort_get_winlinks(&nw, &sort_crit); + for (i = 0; i < nw; i++) + window_switch_add_window(data, wl[i], &order); + break; + } + + for (i = 0; i < data->item_size; i++) { + item = data->item_list[i]; + if (*f == '\0') { + m = xreallocarray(m, n + 1, sizeof *m); + m[n++] = item; + continue; + } + + item->match = fuzzy_match(f, item->text, sx, &item->score); + if (item->match == NULL) + continue; + m = xreallocarray(m, n + 1, sizeof *m); + m[n++] = item; + } + qsort(m, n, sizeof *m, window_switch_compare); + + free(data->matches); + data->matches = m; + data->matches_size = n; +} + +static u_int +window_switch_visible(struct window_switch_modedata *data) +{ + u_int sy = screen_size_y(&data->screen); + + if (sy <= 1) + return (0); + return (sy - 1); +} + +static void +window_switch_set_current(struct window_switch_modedata *data, u_int current) +{ + u_int visible = window_switch_visible(data); + + if (data->matches_size == 0) { + data->current = 0; + data->offset = 0; + return; + } + + if (current > data->matches_size - 1) + current = data->matches_size - 1; + data->current = current; + + if (data->current < data->offset) + data->offset = data->current; + else if (visible != 0 && data->current >= data->offset + visible) + data->offset = data->current - visible + 1; +} + +static void +window_switch_draw_screen(struct window_mode_entry *wme) +{ + struct window_pane *wp = wme->wp; + struct window_switch_modedata *data = wme->data; + struct options *oo = wp->options; + struct screen_write_ctx ctx; + struct screen *s = &data->screen; + u_int sx = screen_size_x(s), i, j; + u_int sy = screen_size_y(s), visible, idx; + struct window_switch_itemdata *item; + struct grid_cell mgc, sgc, gc; + const struct grid_cell *dgc = &grid_default_cell; + struct prompt_draw_data pdd; + screen_write_start(&ctx, s); + screen_write_clearscreen(&ctx, 8); + + if (sy <= 1) { + screen_write_stop(&ctx); + return; + } + + style_apply(&mgc, oo, "switch-mode-match-style", NULL); + style_apply(&sgc, oo, "mode-style", NULL); + + visible = window_switch_visible(data); + for (i = 0; i < visible; i++) { + idx = data->offset + i; + if (idx >= data->matches_size) + break; + item = data->matches[idx]; + + screen_write_cursormove(&ctx, 0, i, 0); + if (idx != data->current) + format_draw(&ctx, dgc, sx, item->text, NULL, 0); + else { + screen_write_clearendofline(&ctx, sgc.bg); + format_draw(&ctx, &sgc, sx, item->text, NULL, 0); + } + + if (item->match == NULL) + continue; + for (j = 0; j < sx; j++) { + if (!bit_test(item->match, j)) + continue; + grid_get_cell(s->grid, j, i, &gc); + gc.attr = mgc.attr; + gc.fg = mgc.fg; + gc.bg = mgc.bg; + screen_write_cursormove(&ctx, j, i, 0); + screen_write_cell(&ctx, &gc); + } + } + + if (data->prompt != NULL) { + pdd.ctx = &ctx; + pdd.cursor_x = &data->prompt_cx; + pdd.area_x = 0; + pdd.area_width = sx; + pdd.prompt_line = sy - 1; + s->mode |= MODE_CURSOR; + prompt_draw(data->prompt, &pdd); + screen_write_cursormove(&ctx, data->prompt_cx, sy - 1, 0); + } + screen_write_stop(&ctx); +} + +static struct screen * +window_switch_init(struct window_mode_entry *wme, + struct cmd_find_state *fs, struct args *args) +{ + struct window_pane *wp = wme->wp; + struct window_switch_modedata *data; + struct screen *s; + struct prompt_create_data pd; + + wme->data = data = xcalloc(1, sizeof *data); + data->wp = wp; + + if (args_has(args, 'w')) + data->type = WINDOW_SWITCH_TYPE_WINDOW; + else + data->type = WINDOW_SWITCH_TYPE_SESSION; + + data->filter = xstrdup(""); + if (args == NULL || !args_has(args, 'F')) + data->format = xstrdup(WINDOW_SWITCH_DEFAULT_FORMAT); + else + data->format = xstrdup(args_get(args, 'F')); + if (args == NULL || args_count(args) == 0) + data->command = xstrdup(WINDOW_SWITCH_DEFAULT_COMMAND); + else + data->command = xstrdup(args_string(args, 0)); + + memset(&pd, 0, sizeof pd); + prompt_set_options(&pd, fs->s); + pd.fs = fs; + pd.prompt = "(search) "; + pd.input = ""; + pd.type = PROMPT_TYPE_SEARCH; + pd.flags = PROMPT_INCREMENTAL|PROMPT_NOFORMAT|PROMPT_ISMODE| + PROMPT_EDITARROWS; + pd.inputcb = window_switch_prompt_callback; + pd.data = data; + data->prompt = prompt_create(&pd); + prompt_update(data->prompt, "(search) ", data->filter); + + if (!args_has(args, 'Z')) + data->zoomed = -1; + else { + data->zoomed = (wp->window->flags & WINDOW_ZOOMED); + if (!data->zoomed && window_zoom(wp) == 0) + server_redraw_window(wp->window); + } + + s = &data->screen; + screen_init(s, screen_size_x(&wp->base), screen_size_y(&wp->base), 0); + + window_switch_build(data); + prompt_incremental_start(data->prompt); + window_switch_draw_screen(wme); + + return (s); +} + +static void +window_switch_free(struct window_mode_entry *wme) +{ + struct window_switch_modedata *data = wme->data; + u_int i; + + if (data->zoomed == 0) + server_unzoom_window(wme->wp->window); + + for (i = 0; i < data->item_size; i++) + window_switch_free_item(data->item_list[i]); + free(data->item_list); + + free(data->matches); + free(data->filter); + prompt_free(data->prompt); + free(data->format); + free(data->command); + screen_free(&data->screen); + + free(data); +} + +static void +window_switch_resize(struct window_mode_entry *wme, u_int sx, u_int sy) +{ + struct window_switch_modedata *data = wme->data; + struct screen *s = &data->screen; + + screen_resize(s, sx, sy, 0); + window_switch_build(data); + window_switch_set_current(data, data->current); + window_switch_draw_screen(wme); +} + +static int +window_switch_run_command(struct window_switch_modedata *data, struct client *c) +{ + struct window_switch_itemdata *item; + struct cmd_find_state fs; + struct session *s; + struct winlink *wl; + char *target = NULL; + struct cmdq_state *state; + char *command, *error; + enum cmd_parse_status status; + + if (data->matches_size == 0) + return (0); + item = data->matches[data->current]; + + cmd_find_clear_state(&fs, 0); + switch (item->type) { + case WINDOW_SWITCH_TYPE_SESSION: + s = session_find_by_id(item->session); + if (s != NULL) { + xasprintf(&target, "=%s:", s->name); + cmd_find_from_session(&fs, s, 0); + } + break; + case WINDOW_SWITCH_TYPE_WINDOW: + s = session_find_by_id(item->session); + if (s != NULL) { + wl = winlink_find_by_index(&s->windows, item->winlink); + if (s != NULL && wl != NULL) { + xasprintf(&target, "=%s:%u.", s->name, wl->idx); + cmd_find_from_winlink(&fs, wl, 0); + } + } + break; + } + if (target == NULL) + return (0); + + command = cmd_template_replace(data->command, target, 1); + if (command != NULL && *command != '\0') { + state = cmdq_new_state(&fs, NULL, 0); + status = cmd_parse_and_append(command, NULL, c, state, &error); + if (status == CMD_PARSE_ERROR) { + if (c != NULL) { + *error = toupper((u_char)*error); + status_message_set(c, -1, 1, 0, 0, "%s", error); + } + free(error); + } + cmdq_free_state(state); + } + free(command); + free(target); + return (1); +} + +static enum prompt_result +window_switch_prompt_callback(void *arg, const char *s, + enum prompt_key_result key) +{ + struct window_switch_modedata *data = arg; + + if (key != PROMPT_KEY_HANDLED) + return (PROMPT_CONTINUE); + + if (s == NULL) + s = ""; + else if (*s != '\0') + s++; + + free(data->filter); + data->filter = xstrdup(s); + window_switch_build(data); + data->current = 0; + data->offset = 0; + + return (PROMPT_CONTINUE); +} + +static void +window_switch_key(struct window_mode_entry *wme, struct client *c, + __unused struct session *s, __unused struct winlink *wl, key_code key, + struct mouse_event *m) +{ + struct window_pane *wp = wme->wp; + struct window_switch_modedata *data = wme->data; + u_int visible, current = data->current; + u_int x, y, size = data->matches_size; + enum prompt_key_result result; + int redraw = 0; + + if (KEYC_IS_MOUSE(key)) { + if (m == NULL || cmd_mouse_at(wp, m, &x, &y, 0) != 0) + return; + if (data->prompt != NULL && screen_size_y(&data->screen) != 0 && + y == screen_size_y(&data->screen) - 1 && + MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_1 && !MOUSE_DRAG(m->b) && + !MOUSE_RELEASE(m->b)) { + result = prompt_mouse(data->prompt, x, 0, + screen_size_x(&data->screen), &redraw); + if (redraw || result == PROMPT_KEY_HANDLED) { + window_switch_draw_screen(wme); + wp->flags |= PANE_REDRAW; + } + return; + } + switch (key) { + case KEYC_WHEELUP_PANE: + if (size != 0 && current != 0) + window_switch_set_current(data, current - 1); + goto moved; + case KEYC_WHEELDOWN_PANE: + if (size != 0 && current != size - 1) + window_switch_set_current(data, current + 1); + goto moved; + case KEYC_MOUSEDOWN1_PANE: + case KEYC_DOUBLECLICK1_PANE: + if (y >= window_switch_visible(data) || + data->offset + y >= size) + return; + window_switch_set_current(data, data->offset + y); + if (key == KEYC_DOUBLECLICK1_PANE) { + if (window_switch_run_command(data, c)) + window_pane_reset_mode(wp); + return; + } + goto moved; + } + return; + } + + switch (key) { + case 'p'|KEYC_CTRL: + case 'k'|KEYC_CTRL: + key = KEYC_UP; + break; + case 'n'|KEYC_CTRL: + case 'j'|KEYC_CTRL: + key = KEYC_DOWN; + break; + } + + switch (key) { + case '\r': + if (window_switch_run_command(data, c)) + window_pane_reset_mode(wp); + return; + case '\033': /* Escape */ + case '['|KEYC_CTRL: + case 'c'|KEYC_CTRL: + case 'g'|KEYC_CTRL: + window_pane_reset_mode(wp); + return; + } + + if (data->prompt != NULL) { + result = prompt_key(data->prompt, key, &redraw); + if (redraw) { + window_switch_draw_screen(wme); + wp->flags |= PANE_REDRAW; + } + if (result == PROMPT_KEY_HANDLED || + result == PROMPT_KEY_NOT_HANDLED) + return; + current = data->current; + size = data->matches_size; + } + + switch (key) { + case KEYC_UP: + if (size == 0) + goto moved; + if (current == 0) + window_switch_set_current(data, size - 1); + else + window_switch_set_current(data, current - 1); + goto moved; + case KEYC_DOWN: + if (size == 0) + goto moved; + if (current == size - 1) + window_switch_set_current(data, 0); + else + window_switch_set_current(data, current + 1); + goto moved; + case KEYC_PPAGE: + visible = window_switch_visible(data); + if (current >= visible) + window_switch_set_current(data, current - visible); + else + window_switch_set_current(data, 0); + goto moved; + case KEYC_NPAGE: + visible = window_switch_visible(data); + window_switch_set_current(data, current + visible); + goto moved; + case KEYC_HOME: + window_switch_set_current(data, 0); + goto moved; + case KEYC_END: + if (size > 0) + window_switch_set_current(data, size - 1); + goto moved; + } + +moved: + window_switch_draw_screen(wme); + wp->flags |= PANE_REDRAW; +}