mirror of
https://github.com/tmux/tmux.git
synced 2026-07-03 10:12:31 +00:00
Merge branch 'obsd-master'
This commit is contained in:
@@ -162,6 +162,7 @@ dist_tmux_SOURCES = \
|
||||
file.c \
|
||||
format.c \
|
||||
format-draw.c \
|
||||
fuzzy.c \
|
||||
grid-reader.c \
|
||||
grid-view.c \
|
||||
grid.c \
|
||||
@@ -217,6 +218,7 @@ dist_tmux_SOURCES = \
|
||||
window-clock.c \
|
||||
window-copy.c \
|
||||
window-customize.c \
|
||||
window-switch.c \
|
||||
window-tree.c \
|
||||
window-visible.c \
|
||||
window.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;
|
||||
|
||||
|
||||
2
cmd.c
2
cmd.c
@@ -115,6 +115,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;
|
||||
@@ -208,6 +209,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,
|
||||
|
||||
45
format.c
45
format.c
@@ -4555,6 +4555,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)
|
||||
@@ -4565,6 +4606,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
655
fuzzy.c
Normal 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);
|
||||
}
|
||||
@@ -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 }",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1672,6 +1672,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,
|
||||
|
||||
11
prompt.c
11
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);
|
||||
}
|
||||
|
||||
85
tmux.1
85
tmux.1
@@ -362,6 +362,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 }
|
||||
@@ -3088,6 +3090,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
|
||||
@@ -6102,6 +6149,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
|
||||
@@ -6494,13 +6550,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
7
tmux.h
@@ -2095,6 +2095,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 {
|
||||
@@ -3314,6 +3315,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);
|
||||
@@ -3774,6 +3778,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
636
window-switch.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user