Files
tmux-tmux/window-switch.c
2026-06-30 18:19:26 +01:00

642 lines
16 KiB
C

/* $OpenBSD$ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <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;
struct cmd_parse_tree *tree;
struct cmdq_item *new_item;
char *target = NULL;
struct cmdq_state *state;
char *command, *error;
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);
tree = cmd_parse_from_string(command, NULL, &error);
if (tree == NULL) {
if (c != NULL) {
*error = toupper((u_char)*error);
status_message_set(c, -1, 1, 0, 0, "%s", error);
}
free(error);
} else {
new_item = cmd_invoke_get(tree, state, NULL);
cmdq_append(c, new_item);
cmd_parse_free(tree);
}
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;
}