/* $OpenBSD$ */ /* * Copyright (c) 2026 Nicholas Marriott * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include "tmux.h" static struct screen *window_switch_init(struct window_mode_entry *, struct cmd_find_state *, struct args *); static void window_switch_free(struct window_mode_entry *); static void window_switch_resize(struct window_mode_entry *, u_int, u_int); static void window_switch_key(struct window_mode_entry *, struct client *, struct session *, struct winlink *, key_code, struct mouse_event *); static enum prompt_result window_switch_prompt_callback(void *, const char *, enum prompt_key_result); #define WINDOW_SWITCH_DEFAULT_COMMAND "switch-client -Zt '%%'" #define WINDOW_SWITCH_DEFAULT_FORMAT \ "#{?window_format," \ "#{window_name} " \ "#[dim]#{session_name}:#{window_index}#{window_flags}#[default] " \ "#[dim]#{pane_current_command}#[default] " \ "#[dim]#{?#{!=:#{pane_title},#{host_short}},#{pane_title},}#[default]" \ "," \ "#{session_name} " \ "#[dim]#{session_windows} windows#[default] " \ "#{?session_attached,attached,#[dim]detached#[default]} " \ "#[dim]#{window_name}#[default]" \ "}" const struct window_mode window_switch_mode = { .name = "switch-mode", .default_format = WINDOW_SWITCH_DEFAULT_FORMAT, .init = window_switch_init, .free = window_switch_free, .resize = window_switch_resize, .key = window_switch_key, }; enum window_switch_type { WINDOW_SWITCH_TYPE_SESSION, WINDOW_SWITCH_TYPE_WINDOW }; struct window_switch_itemdata { enum window_switch_type type; int session; int winlink; uint64_t tag; char *text; bitstr_t *match; u_int score; u_int order; }; struct window_switch_modedata { struct window_pane *wp; struct screen screen; int zoomed; char *format; char *command; enum window_switch_type type; char *filter; struct prompt *prompt; u_int prompt_cx; struct window_switch_itemdata **item_list; u_int item_size; struct window_switch_itemdata **matches; u_int matches_size; u_int current; u_int offset; }; static void window_switch_free_item(struct window_switch_itemdata *item) { free(item->match); free(item->text); free(item); } static struct window_switch_itemdata * window_switch_add_item(struct window_switch_modedata *data) { struct window_switch_itemdata *item; data->item_list = xreallocarray(data->item_list, data->item_size + 1, sizeof *data->item_list); item = data->item_list[data->item_size++] = xcalloc(1, sizeof *item); return (item); } static void window_switch_add_session(struct window_switch_modedata *data, struct session *s, u_int *order) { struct window_switch_itemdata *item; struct format_tree *ft; ft = format_create(NULL, NULL, FORMAT_NONE, 0); format_defaults(ft, NULL, s, NULL, NULL); item = window_switch_add_item(data); item->type = WINDOW_SWITCH_TYPE_SESSION; item->session = s->id; item->winlink = -1; item->tag = (uint64_t)s; item->order = (*order)++; item->text = format_expand(ft, data->format); format_free(ft); } static void window_switch_add_window(struct window_switch_modedata *data, struct winlink *wl, u_int *order) { struct window_switch_itemdata *item; struct format_tree *ft; ft = format_create(NULL, NULL, FORMAT_NONE, 0); format_defaults(ft, NULL, wl->session, wl, NULL); item = window_switch_add_item(data); item->type = WINDOW_SWITCH_TYPE_WINDOW; item->session = wl->session->id; item->winlink = wl->idx; item->tag = (uint64_t)wl; item->order = (*order)++; item->text = format_expand(ft, data->format); format_free(ft); } static int window_switch_compare(const void *a0, const void *b0) { struct window_switch_itemdata *const *a = a0; struct window_switch_itemdata *const *b = b0; if ((*a)->score > (*b)->score) return (-1); if ((*a)->score < (*b)->score) return (1); if ((*a)->order < (*b)->order) return (-1); if ((*a)->order > (*b)->order) return (1); return (0); } static void window_switch_build(struct window_switch_modedata *data) { struct window_switch_itemdata *item, **m = NULL; const char *f = data->filter; u_int ns, nw, i, n = 0, order = 0; u_int sx = screen_size_x(&data->screen); struct session **sl; struct winlink **wl; struct sort_criteria sort_crit; sort_crit.order = SORT_NAME; sort_crit.reversed = 0; for (i = 0; i < data->item_size; i++) window_switch_free_item(data->item_list[i]); free(data->item_list); data->item_list = NULL; data->item_size = 0; switch (data->type) { case WINDOW_SWITCH_TYPE_SESSION: sl = sort_get_sessions(&ns, &sort_crit); for (i = 0; i < ns; i++) window_switch_add_session(data, sl[i], &order); break; case WINDOW_SWITCH_TYPE_WINDOW: wl = sort_get_winlinks(&nw, &sort_crit); for (i = 0; i < nw; i++) window_switch_add_window(data, wl[i], &order); break; } for (i = 0; i < data->item_size; i++) { item = data->item_list[i]; if (*f == '\0') { m = xreallocarray(m, n + 1, sizeof *m); m[n++] = item; continue; } item->match = fuzzy_match(f, item->text, sx, &item->score); if (item->match == NULL) continue; m = xreallocarray(m, n + 1, sizeof *m); m[n++] = item; } qsort(m, n, sizeof *m, window_switch_compare); free(data->matches); data->matches = m; data->matches_size = n; } static u_int window_switch_visible(struct window_switch_modedata *data) { u_int sy = screen_size_y(&data->screen); if (sy <= 1) return (0); return (sy - 1); } static void window_switch_set_current(struct window_switch_modedata *data, u_int current) { u_int visible = window_switch_visible(data); if (data->matches_size == 0) { data->current = 0; data->offset = 0; return; } if (current > data->matches_size - 1) current = data->matches_size - 1; data->current = current; if (data->current < data->offset) data->offset = data->current; else if (visible != 0 && data->current >= data->offset + visible) data->offset = data->current - visible + 1; } static void window_switch_draw_screen(struct window_mode_entry *wme) { struct window_pane *wp = wme->wp; struct window_switch_modedata *data = wme->data; struct options *oo = wp->options; struct screen_write_ctx ctx; struct screen *s = &data->screen; u_int sx = screen_size_x(s), i, j; u_int sy = screen_size_y(s), visible, idx; struct window_switch_itemdata *item; struct grid_cell mgc, sgc, gc; const struct grid_cell *dgc = &grid_default_cell; struct prompt_draw_data pdd; screen_write_start(&ctx, s); screen_write_clearscreen(&ctx, 8); if (sy <= 1) { screen_write_stop(&ctx); return; } style_apply(&mgc, oo, "switch-mode-match-style", NULL); style_apply(&sgc, oo, "mode-style", NULL); visible = window_switch_visible(data); for (i = 0; i < visible; i++) { idx = data->offset + i; if (idx >= data->matches_size) break; item = data->matches[idx]; screen_write_cursormove(&ctx, 0, i, 0); if (idx != data->current) format_draw(&ctx, dgc, sx, item->text, NULL, 0); else { screen_write_clearendofline(&ctx, sgc.bg); format_draw(&ctx, &sgc, sx, item->text, NULL, 0); } if (item->match == NULL) continue; for (j = 0; j < sx; j++) { if (!bit_test(item->match, j)) continue; grid_get_cell(s->grid, j, i, &gc); gc.attr = mgc.attr; gc.fg = mgc.fg; gc.bg = mgc.bg; screen_write_cursormove(&ctx, j, i, 0); screen_write_cell(&ctx, &gc); } } if (data->prompt != NULL) { pdd.ctx = &ctx; pdd.cursor_x = &data->prompt_cx; pdd.area_x = 0; pdd.area_width = sx; pdd.prompt_line = sy - 1; s->mode |= MODE_CURSOR; prompt_draw(data->prompt, &pdd); screen_write_cursormove(&ctx, data->prompt_cx, sy - 1, 0); } screen_write_stop(&ctx); } static struct screen * window_switch_init(struct window_mode_entry *wme, struct cmd_find_state *fs, struct args *args) { struct window_pane *wp = wme->wp; struct window_switch_modedata *data; struct screen *s; struct prompt_create_data pd; wme->data = data = xcalloc(1, sizeof *data); data->wp = wp; if (args_has(args, 'w')) data->type = WINDOW_SWITCH_TYPE_WINDOW; else data->type = WINDOW_SWITCH_TYPE_SESSION; data->filter = xstrdup(""); if (args == NULL || !args_has(args, 'F')) data->format = xstrdup(WINDOW_SWITCH_DEFAULT_FORMAT); else data->format = xstrdup(args_get(args, 'F')); if (args == NULL || args_count(args) == 0) data->command = xstrdup(WINDOW_SWITCH_DEFAULT_COMMAND); else data->command = xstrdup(args_string(args, 0)); memset(&pd, 0, sizeof pd); prompt_set_options(&pd, fs->s); pd.fs = fs; pd.prompt = "(search) "; pd.input = ""; pd.type = PROMPT_TYPE_SEARCH; pd.flags = PROMPT_INCREMENTAL|PROMPT_NOFORMAT|PROMPT_ISMODE| PROMPT_EDITARROWS; pd.inputcb = window_switch_prompt_callback; pd.data = data; data->prompt = prompt_create(&pd); prompt_update(data->prompt, "(search) ", data->filter); if (!args_has(args, 'Z')) data->zoomed = -1; else { data->zoomed = (wp->window->flags & WINDOW_ZOOMED); if (!data->zoomed && window_zoom(wp) == 0) server_redraw_window(wp->window); } s = &data->screen; screen_init(s, screen_size_x(&wp->base), screen_size_y(&wp->base), 0); window_switch_build(data); prompt_incremental_start(data->prompt); window_switch_draw_screen(wme); return (s); } static void window_switch_free(struct window_mode_entry *wme) { struct window_switch_modedata *data = wme->data; u_int i; if (data->zoomed == 0) server_unzoom_window(wme->wp->window); for (i = 0; i < data->item_size; i++) window_switch_free_item(data->item_list[i]); free(data->item_list); free(data->matches); free(data->filter); prompt_free(data->prompt); free(data->format); free(data->command); screen_free(&data->screen); free(data); } static void window_switch_resize(struct window_mode_entry *wme, u_int sx, u_int sy) { struct window_switch_modedata *data = wme->data; struct screen *s = &data->screen; screen_resize(s, sx, sy, 0); window_switch_build(data); window_switch_set_current(data, data->current); window_switch_draw_screen(wme); } static int window_switch_run_command(struct window_switch_modedata *data, struct client *c) { struct window_switch_itemdata *item; struct cmd_find_state fs; struct session *s; struct winlink *wl; char *target = NULL; struct cmdq_state *state; char *command, *error; enum cmd_parse_status status; if (data->matches_size == 0) return (0); item = data->matches[data->current]; cmd_find_clear_state(&fs, 0); switch (item->type) { case WINDOW_SWITCH_TYPE_SESSION: s = session_find_by_id(item->session); if (s != NULL) { xasprintf(&target, "=%s:", s->name); cmd_find_from_session(&fs, s, 0); } break; case WINDOW_SWITCH_TYPE_WINDOW: s = session_find_by_id(item->session); if (s != NULL) { wl = winlink_find_by_index(&s->windows, item->winlink); if (s != NULL && wl != NULL) { xasprintf(&target, "=%s:%u.", s->name, wl->idx); cmd_find_from_winlink(&fs, wl, 0); } } break; } if (target == NULL) return (0); command = cmd_template_replace(data->command, target, 1); if (command != NULL && *command != '\0') { state = cmdq_new_state(&fs, NULL, 0); status = cmd_parse_and_append(command, NULL, c, state, &error); if (status == CMD_PARSE_ERROR) { if (c != NULL) { *error = toupper((u_char)*error); status_message_set(c, -1, 1, 0, 0, "%s", error); } free(error); } cmdq_free_state(state); } free(command); free(target); return (1); } static enum prompt_result window_switch_prompt_callback(void *arg, const char *s, enum prompt_key_result key) { struct window_switch_modedata *data = arg; if (key != PROMPT_KEY_HANDLED) return (PROMPT_CONTINUE); if (s == NULL) s = ""; else if (*s != '\0') s++; free(data->filter); data->filter = xstrdup(s); window_switch_build(data); data->current = 0; data->offset = 0; return (PROMPT_CONTINUE); } static void window_switch_key(struct window_mode_entry *wme, struct client *c, __unused struct session *s, __unused struct winlink *wl, key_code key, struct mouse_event *m) { struct window_pane *wp = wme->wp; struct window_switch_modedata *data = wme->data; u_int visible, current = data->current; u_int x, y, size = data->matches_size; enum prompt_key_result result; int redraw = 0; if (KEYC_IS_MOUSE(key)) { if (m == NULL || cmd_mouse_at(wp, m, &x, &y, 0) != 0) return; if (data->prompt != NULL && screen_size_y(&data->screen) != 0 && y == screen_size_y(&data->screen) - 1 && MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_1 && !MOUSE_DRAG(m->b) && !MOUSE_RELEASE(m->b)) { result = prompt_mouse(data->prompt, x, 0, screen_size_x(&data->screen), &redraw); if (redraw || result == PROMPT_KEY_HANDLED) { window_switch_draw_screen(wme); wp->flags |= PANE_REDRAW; } return; } switch (key) { case KEYC_WHEELUP_PANE: if (size != 0 && current != 0) window_switch_set_current(data, current - 1); goto moved; case KEYC_WHEELDOWN_PANE: if (size != 0 && current != size - 1) window_switch_set_current(data, current + 1); goto moved; case KEYC_MOUSEDOWN1_PANE: case KEYC_DOUBLECLICK1_PANE: if (y >= window_switch_visible(data) || data->offset + y >= size) return; window_switch_set_current(data, data->offset + y); if (key == KEYC_DOUBLECLICK1_PANE) { if (window_switch_run_command(data, c)) window_pane_reset_mode(wp); return; } goto moved; } return; } switch (key) { case 'p'|KEYC_CTRL: case 'k'|KEYC_CTRL: key = KEYC_UP; break; case 'n'|KEYC_CTRL: case 'j'|KEYC_CTRL: key = KEYC_DOWN; break; } switch (key) { case '\r': if (window_switch_run_command(data, c)) window_pane_reset_mode(wp); return; case '\033': /* Escape */ case '['|KEYC_CTRL: case 'c'|KEYC_CTRL: case 'g'|KEYC_CTRL: window_pane_reset_mode(wp); return; } if (data->prompt != NULL) { result = prompt_key(data->prompt, key, &redraw); if (redraw) { window_switch_draw_screen(wme); wp->flags |= PANE_REDRAW; } if (result == PROMPT_KEY_HANDLED || result == PROMPT_KEY_NOT_HANDLED) return; current = data->current; size = data->matches_size; } switch (key) { case KEYC_UP: if (size == 0) goto moved; if (current == 0) window_switch_set_current(data, size - 1); else window_switch_set_current(data, current - 1); goto moved; case KEYC_DOWN: if (size == 0) goto moved; if (current == size - 1) window_switch_set_current(data, 0); else window_switch_set_current(data, current + 1); goto moved; case KEYC_PPAGE: visible = window_switch_visible(data); if (current >= visible) window_switch_set_current(data, current - visible); else window_switch_set_current(data, 0); goto moved; case KEYC_NPAGE: visible = window_switch_visible(data); window_switch_set_current(data, current + visible); goto moved; case KEYC_HOME: window_switch_set_current(data, 0); goto moved; case KEYC_END: if (size > 0) window_switch_set_current(data, size - 1); goto moved; } moved: window_switch_draw_screen(wme); wp->flags |= PANE_REDRAW; }