Add switch-mode a fast switcher with fuzzy searching, bound to Tab (for

windows) or BTab (S-Tab, for sessions) by default.
This commit is contained in:
nicm
2026-06-26 14:40:30 +00:00
parent 973c4ab569
commit 575f84ce0f
12 changed files with 1467 additions and 11 deletions

View File

@@ -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 <bsd.prog.mk>

View File

@@ -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;

2
cmd.c
View File

@@ -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,

View File

@@ -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;

655
fuzzy.c Normal file
View File

@@ -0,0 +1,655 @@
/* $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 <ctype.h>
#include <stdlib.h>
#include <string.h>
#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);
}

View File

@@ -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 }",

View File

@@ -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;

View File

@@ -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,

View File

@@ -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);
}

85
tmux.1
View File

@@ -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

7
tmux.h
View File

@@ -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];

636
window-switch.c Normal file
View File

@@ -0,0 +1,636 @@
/* $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 <ctype.h>
#include <stdlib.h>
#include <string.h>
#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;
}