Merge remote-tracking branch 'origin/master' into redraw-damage-rectangles

# Conflicts:
#	server-client.c
#	window.c
This commit is contained in:
Michael Grant
2026-08-25 08:08:17 +01:00
33 changed files with 988 additions and 207 deletions

View File

@@ -17,7 +17,7 @@ jobs:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
if: github.repository == 'tmux/tmux'
timeout-minutes: 45
timeout-minutes: 300
strategy:
fail-fast: false

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */
/* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -712,6 +712,9 @@ client_dispatch_wait(struct imsg *imsg)
fprintf(stderr, "server version is too old for client\n");
proc_exit(client_proc);
break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
}
}
@@ -798,5 +801,8 @@ client_dispatch_attached(struct imsg *imsg)
system(data);
proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0);
break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */
/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */
/*
* Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -850,7 +850,18 @@ cmdq_error(struct cmdq_item *item, const char *fmt, ...)
if (c == NULL) {
cmd_get_source(cmd, &file, &line);
cfg_add_cause("%s:%u: %s", file, line, msg);
if (!cfg_finished) {
if (file != NULL)
cfg_add_cause("%s:%u: %s", file, line, msg);
else
cfg_add_cause("%s", msg);
} else {
if (file != NULL) {
server_add_message("message: %s:%u: %s", file,
line, msg);
} else
server_add_message("message: %s", msg);
}
} else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) {
server_add_message("%s message: %s", c->name, msg);
if (~c->flags & CLIENT_UTF8) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-select-pane.c,v 1.77 2026/07/17 12:42:51 nicm Exp $ */
/* $OpenBSD: cmd-select-pane.c,v 1.78 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -160,6 +160,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
char *title;
const char *style;
struct options_entry *o;
int visible, Zflag = args_has(args, 'Z');
if (entry == &cmd_last_pane_entry || args_has(args, 'l')) {
/*
@@ -185,14 +186,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
server_redraw_window_borders(lastwp->window);
server_status_window(lastwp->window);
} else {
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && lastwp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(lastwp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, lastwp);
if (window_set_active_pane(w, lastwp, 1)) {
cmd_find_from_winlink(current, wl, 0);
cmd_select_pane_redraw(w);
}
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
}
return (CMD_RETURN_NORMAL);
@@ -268,14 +273,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
if (wp == w->active)
return (CMD_RETURN_NORMAL);
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && wp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(wp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, wp);
if (window_set_active_pane(w, wp, 1))
cmd_find_from_winlink_pane(current, wl, wp, 0);
cmdq_insert_hook(s, item, current, "after-select-pane");
cmd_select_pane_redraw(w);
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
return (CMD_RETURN_NORMAL);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */
/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -100,6 +100,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "%s", cause);
goto fail;
}
free(bufname);
return (CMD_RETURN_NORMAL);
}
@@ -107,8 +108,10 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "no data specified");
goto fail;
}
if ((newsize = strlen(args_string(args, 0))) == 0)
if ((newsize = strlen(args_string(args, 0))) == 0) {
free(bufname);
return (CMD_RETURN_NORMAL);
}
if (args_has(args, 'a') && pb != NULL) {
olddata = paste_buffer_data(pb, &bufsize);
@@ -127,6 +130,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'w') && tc != NULL)
tty_set_selection(&tc->tty, "", bufdata, bufsize);
free(bufname);
return (CMD_RETURN_NORMAL);
fail:

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */
/* $OpenBSD: cmd-source-file.c,v 1.63 2026/08/18 08:05:05 nicm Exp $ */
/*
* Copyright (c) 2008 Tiago Cunha <me@tiagocunha.org>
@@ -50,6 +50,7 @@ const struct cmd_entry cmd_source_file_entry = {
struct cmd_source_file_data {
struct cmdq_item *item;
struct client *client;
int flags;
struct cmdq_item *after;
@@ -60,10 +61,24 @@ struct cmd_source_file_data {
u_int nfiles;
};
static enum cmd_retval
cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data)
static void
cmd_source_file_free_data(struct cmd_source_file_data *cdata)
{
struct client *c = cmdq_get_client(item);
u_int i;
for (i = 0; i < cdata->nfiles; i++)
free(cdata->files[i]);
free(cdata->files);
if (cdata->client != NULL)
server_client_unref(cdata->client);
free(cdata);
}
static enum cmd_retval
cmd_source_file_complete_cb(struct cmdq_item *item, void *data)
{
struct cmd_source_file_data *cdata = data;
struct client *c = cdata->client;
if (c == NULL) {
cmd_source_file_depth--;
@@ -74,36 +89,36 @@ cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data)
}
cfg_print_causes(item);
cmd_source_file_free_data(cdata);
return (CMD_RETURN_NORMAL);
}
static void
cmd_source_file_complete(struct client *c, struct cmd_source_file_data *cdata)
cmd_source_file_complete(struct cmd_source_file_data *cdata)
{
struct client *c = cdata->client;
struct cmdq_item *new_item;
u_int i;
if (cfg_finished) {
if (cdata->retval == CMD_RETURN_ERROR &&
c != NULL &&
c->session == NULL)
c->retval = 1;
new_item = cmdq_get_callback(cmd_source_file_complete_cb, NULL);
cmdq_insert_after(cdata->after, new_item);
if (!cfg_finished) {
cmd_source_file_free_data(cdata);
return;
}
for (i = 0; i < cdata->nfiles; i++)
free(cdata->files[i]);
free(cdata->files);
free(cdata);
if (cdata->retval == CMD_RETURN_ERROR &&
c != NULL &&
c->session == NULL)
c->retval = 1;
new_item = cmdq_get_callback(cmd_source_file_complete_cb, cdata);
cmdq_insert_after(cdata->after, new_item);
}
static void
cmd_source_file_done(struct client *c, const char *path, int error,
int closed, struct evbuffer *buffer, void *data)
cmd_source_file_done(__unused struct client *oc, const char *path,
int error, int closed, struct evbuffer *buffer, void *data)
{
struct cmd_source_file_data *cdata = data;
struct cmdq_item *item = cdata->item;
struct client *c = cdata->client;
void *bdata = EVBUFFER_DATA(buffer);
size_t bsize = EVBUFFER_LENGTH(buffer);
u_int n;
@@ -127,7 +142,7 @@ cmd_source_file_done(struct client *c, const char *path, int error,
if (n < cdata->nfiles)
file_read(c, cdata->files[n], cmd_source_file_done, cdata);
else {
cmd_source_file_complete(c, cdata);
cmd_source_file_complete(cdata);
cmdq_continue(item);
}
}
@@ -187,6 +202,9 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item)
cdata = xcalloc(1, sizeof *cdata);
cdata->item = item;
cdata->client = c;
if (c != NULL)
c->references++;
if (args_has(args, 'q'))
cdata->flags |= CMD_PARSE_QUIET;
@@ -249,7 +267,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item)
file_read(c, cdata->files[0], cmd_source_file_done, cdata);
retval = CMD_RETURN_WAIT;
} else
cmd_source_file_complete(c, cdata);
cmd_source_file_complete(cdata);
free(cwd);
return (retval);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-split-window.c,v 1.148 2026/08/07 08:10:53 nicm Exp $ */
/* $OpenBSD: cmd-split-window.c,v 1.150 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -40,8 +40,8 @@ const struct cmd_entry cmd_new_pane_entry = {
.name = "new-pane",
.alias = "newp",
.args = { "bB:Cc:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-bCdefhIklMOPvWZ] [-B border-lines] "
.args = { "AbB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-AbCdefhIkKLMOPvWZ] [-B border-lines] "
"[-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] "
@@ -89,6 +89,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state fs;
struct key_event *event = cmdq_get_event(item);
int input, empty, is_floating, flags = 0;
int restore_zoom = 0;
const char *template, *style, *value;
char *cause = NULL, *cp, *title;
const struct options_table_entry *oe;
@@ -96,10 +97,16 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
enum pane_lines lines;
u_int count = args_count(args);
if (window_active_pane_is_over_zoom(w))
restore_zoom = 1;
if (cmd_get_entry(self) == &cmd_new_pane_entry)
is_floating = !args_has(args, 'L');
else {
window_unzoom(w, 1);
if (!window_pane_is_visible(wp))
restore_zoom = 0;
if (!restore_zoom)
window_unzoom(w, 1);
is_floating = window_pane_is_floating(wp);
flags |= SPAWN_SPLIT;
}
@@ -133,7 +140,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'Z'))
flags |= SPAWN_ZOOM;
if (args_has(args, 'O'))
flags |= SPAWN_MODAL;
flags |= SPAWN_MODAL|SPAWN_FLOATOVERZOOM;
if (is_floating && args_has(args, 'A'))
flags |= SPAWN_FLOATOVERZOOM;
if ((w->flags & WINDOW_ZOOMED) && (flags & SPAWN_FLOATOVERZOOM))
restore_zoom = 1;
input = args_has(args, 'I');
if (input || (count == 1 && *args_string(args, 0) == '\0'))
@@ -169,6 +180,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (cause != NULL) {
cmdq_error(item, "%s", cause);
free(cause);
if (restore_zoom)
window_pop_zoom(w);
return (CMD_RETURN_ERROR);
}
@@ -202,6 +215,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
*/
goto fail;
}
if (args_has(args, 'K') && args_has(args, 'O'))
new_wp->flags |= PANE_CAPTUREALLKEYS;
if (args_has(args, 'C') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCLICK;
@@ -271,7 +286,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (~flags & SPAWN_DETACHED)
cmd_find_from_winlink_pane(current, wl, new_wp, 0);
if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) {
if (restore_zoom) {
window_pop_zoom(wp->window);
server_redraw_window(wp->window);
} else if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) {
window_pop_zoom(wp->window);
server_redraw_window(wp->window);
}
@@ -322,8 +340,9 @@ fail:
if (!is_floating)
layout_close_pane(new_wp);
window_remove_pane(wp->window, new_wp);
} else if (args_has(args, 'O'))
window_pop_modal_zoom(wp->window);
}
if (restore_zoom || (~flags & SPAWN_FLOATING))
window_pop_zoom(wp->window);
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */
/* $OpenBSD: cmd-switch-client.c,v 1.75 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -53,7 +53,7 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state target;
const char *tflag = args_get(args, 't');
enum cmd_find_type type;
int flags;
int flags, visible, Zflag = args_has(args, 'Z');
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct session *s;
@@ -139,11 +139,15 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_NORMAL);
if (wl != NULL && wp != NULL && wp != wl->window->active) {
w = wl->window;
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && wp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(wp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, wp);
window_set_active_pane(w, wp, 1);
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
}
if (wl != NULL) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */
/* $OpenBSD: control.c,v 1.66 2026/08/18 07:43:44 nicm Exp $ */
/*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -388,6 +388,25 @@ control_pause_pane(struct client *c, struct window_pane *wp)
}
}
/*
* Reset a pane after its buffer has been replaced: drop any output still
* queued from the old buffer and start again from the pane's own offset.
*/
void
control_reset_pane(struct client *c, struct window_pane *wp)
{
struct control_pane *cp;
if (c->control_state == NULL)
return;
cp = control_get_pane(c, wp);
if (cp == NULL)
return;
control_discard_pane(c, cp);
memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
}
/* Write an already-formatted line, queueing it behind %output if needed. */
static void
control_write_line(struct client *c, char *line)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */
/* $OpenBSD: format.c,v 1.413 2026/08/24 07:26:43 nicm Exp $ */
/*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -4387,7 +4387,7 @@ format_quote_shell(const char *s)
at = out = xmalloc(strlen(s) * 2 + 1);
for (cp = s; *cp != '\0'; cp++) {
if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL)
if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL)
*at++ = '\\';
*at++ = *cp;
}

View File

@@ -84,6 +84,11 @@ image_fallback(char **ret, u_int sx, u_int sy)
char *buf, *label;
u_int py, size, lsize;
if (sy == 0) {
*ret = xstrdup("");
return;
}
/* Allocate first line. */
lsize = xasprintf(&label, "SIXEL IMAGE (%ux%u)\r\n", sx, sy) + 1;
if (sx < lsize - 3)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 nicm Exp $ */
/* $OpenBSD: input.c,v 1.270 2026/08/17 20:04:00 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1978,7 +1978,7 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx)
screen_write_mode_clear(sctx, MODE_BRACKETPASTE);
break;
case 2026:
screen_write_stop_sync(ictx->wp);
screen_write_end_sync(sctx);
break;
case 2031:
screen_write_mode_clear(sctx, MODE_THEME_UPDATES);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: layout.c,v 1.96 2026/07/15 13:02:33 nicm Exp $ */
/* $OpenBSD: layout.c,v 1.97 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1652,7 +1652,10 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args,
return (NULL);
}
window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM));
if (window_active_pane_is_over_zoom(w))
window_push_zoom(w, 0, 1);
else
window_push_zoom(w, 1, (flags & SPAWN_ZOOM));
lc = layout_split_pane(wp, type, size, flags);
if (lc == NULL)
*cause = xstrdup("no space for a new pane");
@@ -1679,8 +1682,10 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args,
return (NULL);
}
if (flags & SPAWN_MODAL)
window_push_modal_zoom(w);
if (flags & SPAWN_FLOATOVERZOOM)
window_push_zoom(wp->window, 0, 1);
else if (window_active_pane_is_over_zoom(w))
window_push_zoom(wp->window, 0, 1);
else
window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM));
lcnew = layout_floating_pane(w, wp, &fg);

View File

@@ -105,6 +105,24 @@ drag()
sleep 1
}
meta_drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<8;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<40;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<8;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
check_ok new-session -d -s modal -x 80 -y 24 'cat'
@@ -127,12 +145,16 @@ case "$(fmt "$modal" '#{pane_flags}')" in
*O*) ;;
*) fail "modal pane flags do not include O" ;;
esac
case "$(fmt "$modal" '#{pane_flags}')" in
*A*) ;;
*) fail "modal pane flags do not include A" ;;
esac
case "$(fmt modal:0 '#{window_flags}')" in
*O*) ;;
*) fail "modal window flags do not include O" ;;
esac
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
check_fail "window already has a modal pane" \
new-pane -O -x 10 -y 4 'cat'
@@ -159,12 +181,14 @@ under=$($TMUX split-window -PF '#{pane_id}' -t "$p0" 'cat') ||
sleep 1
must_equal "$(fmt modal:0 '#{pane_id}')" "$modal"
must_equal "$(fmt "$under" '#{pane_active}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
float=$($TMUX new-pane -PF '#{pane_id}' -x 10 -y 4 -X 5 -Y 3 'cat') ||
fail "new floating pane under modal failed"
sleep 1
must_equal "$(fmt modal:0 '#{pane_id}')" "$modal"
must_equal "$(fmt "$float" '#{pane_active}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
check_ok new-window -d -t modal: -n other 'cat'
check_ok select-window -t modal:other
@@ -303,6 +327,164 @@ check_ok kill-pane -t "$modal"
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
must_equal "$(fmt modal:0 '#{pane_id}')" "$p0"
must_equal "$(fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 0:0
ignored=$($TMUX new-pane -KdPF '#{pane_id}' -t "$p0" 'cat') ||
fail "new-pane -K without -O failed"
check_ok kill-pane -t "$ignored"
$TMUX set -g @modal-prefix no
$TMUX set -g @modal-root no
$TMUX bind -n z set -g @modal-root yes
modal=$($TMUX new-pane -OKPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "new-pane -OK failed"
sleep 1
$TMUX2 send-keys -t "$OUTER" C-b x z Enter
sleep 1
must_equal "$($TMUX show -gv @modal-prefix)" no
must_equal "$($TMUX show -gv @modal-root)" no
case "$($TMUX capture-pane -pt "$modal")" in
*xz*) ;;
*) fail "keys did not reach key-capturing modal pane" ;;
esac
left=$(fmt "$modal" '#{pane_left}')
top=$(fmt "$modal" '#{pane_top}')
meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4))
new_left=$(fmt "$modal" '#{pane_left}')
new_top=$(fmt "$modal" '#{pane_top}')
[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] ||
fail "key-capturing modal pane did not move"
check_ok kill-pane -t "$modal"
sleep 1
# A nonmodal floating pane may remain above zoom, and switching between it and
# the zoomed tiled pane must not unzoom the window.
check_ok new-window -d -t modal: -n float-over-zoom 'cat'
base=$(fmt modal:float-over-zoom '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
check_ok resize-pane -Z -t "$base"
over=$($TMUX new-pane -APF '#{pane_id}' -t "$base" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "new-pane -A failed"
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1
case "$(fmt "$over" '#{pane_flags}')" in
*A*) ;;
*) fail "float-over-zoom pane flags do not include A" ;;
esac
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok select-pane -t "$base"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok select-pane -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
client=$($TMUX list-clients -F '#{client_name}' | head -1)
[ -n "$client" ] || fail "no client for switch-client test"
check_ok switch-client -c "$client" -t "$base"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok switch-client -c "$client" -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok kill-pane -t "$over"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok resize-pane -Z -t "$base"
ignored=$($TMUX new-pane -ALdPF '#{pane_id}' -t "$base" 'cat') ||
fail "new-pane -A -L failed"
case "$(fmt "$ignored" '#{pane_flags}')" in
*A*) fail "tiled pane flags include A" ;;
*) ;;
esac
# Existing floating panes are filtered when zoom begins: -A panes remain in the
# visible layout and ordinary floating panes do not.
check_ok new-window -d -t modal: -n existing-over-zoom 'cat'
base=$(fmt modal:existing-over-zoom '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "pre-existing new-pane -A failed"
under=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 15 -y 4 -X 2 -Y 2 'cat') ||
fail "pre-existing ordinary new-pane failed"
check_ok select-window -t modal:existing-over-zoom
check_ok select-pane -t "$base"
check_ok resize-pane -Z -t "$base"
must_equal "$(fmt "$over" '#{pane_floating_flag}')" 1
must_equal "$(fmt "$under" '#{pane_floating_flag}')" 0
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
# Geometry changed in the visible zoom layout is copied back when unzooming.
check_ok select-pane -t "$over"
left=$(fmt "$over" '#{pane_left}')
top=$(fmt "$over" '#{pane_top}')
meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4))
new_left=$(fmt "$over" '#{pane_left}')
new_top=$(fmt "$over" '#{pane_top}')
[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] ||
fail "float-over-zoom pane did not move"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok resize-pane -Z -t "$base"
must_equal "$(fmt "$over" '#{pane_left}:#{pane_top}')" \
"$new_left:$new_top"
must_equal "$(fmt "$under" '#{pane_floating_flag}')" 1
# Resizing with the over-zoom pane active restores the original zoom target.
check_ok resize-pane -Z -t "$base"
check_ok select-pane -t "$over"
check_ok resize-window -t modal:existing-over-zoom -x 90 -y 30
must_equal "$(fmt "$base" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}')" \
90x30:1:1
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1
# Natural pane exit uses a different removal path from kill-pane and must also
# preserve zoom.
dying=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 12 -y 4 -X 4 -Y 3 'true') ||
fail "short-lived new-pane -A failed"
i=0
while $TMUX list-panes -a -F '#{pane_id}' | grep -qx "$dying"; do
i=$((i + 1))
[ $i -gt 50 ] && fail "short-lived float-over-zoom pane did not exit"
sleep 0.1
done
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
# A pane with -A is also above a zoom target which was itself floating. The
# temporary tiled target must sit behind retained floating panes, then return
# to the normal floating z order when unzoomed.
check_ok new-window -d -t modal: -n floating-zoom-target 'cat'
base=$(fmt modal:floating-zoom-target '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
target=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 30 -y 10 -X 10 -Y 5 'cat') ||
fail "floating zoom target creation failed"
over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 15 -y 5 -X 15 -Y 8 'cat') ||
fail "float-over-zoom pane creation failed"
check_ok select-window -t modal:floating-zoom-target
check_ok select-pane -t "$target"
must_equal "$(fmt "$target" '#{pane_z}')" 0
must_equal "$(fmt "$over" '#{pane_z}')" 1
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$target" \
'#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_floating_flag}:#{pane_z}')" \
1:1:0:2
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0
check_ok select-pane -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$over" '#{pane_active}:#{pane_z}')" 1:0
must_equal "$(fmt "$target" '#{pane_floating_flag}:#{pane_z}')" 1:1
# If the target remains active, it returns to the front on unzoom.
check_ok select-pane -t "$target"
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$target" '#{pane_active}:#{pane_z}')" 1:0
must_equal "$(fmt "$over" '#{pane_z}')" 1
cleanup
exit 0

View File

@@ -19,7 +19,10 @@
# - respawn-pane/respawn-window refusing a live pane without -k, working on a
# dead pane (remain-on-exit) and killing with -k;
# - resize-pane -x/-y in cells and percent, -L/-R/-U/-D adjustments and -Z
# zoom/unzoom (including implicit unzoom on split).
# zoom/unzoom, layout round trips, zoom target selection, pane removal,
# window resizing and implicit unzoom on layout changes;
# - swap-pane preserving one or two zoomed windows, including cross-window
# swaps of hidden panes and zoom targets.
#
# window-ops.sh covers window-level commands and buffers.sh paste buffers.
@@ -144,7 +147,8 @@ check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# break-pane and join-pane.
# break-pane moves a pane to a new window; -P -F prints where it went and -n
# names the new window.
# names the new window. Moving a pane out of a zoomed window unzooms it first.
check_ok resize-pane -Z -t "$p0"
out=$($TMUX break-pane -d -P -F '#{window_index}:#{pane_id}' -n broken \
-s "$p1" -t P:)
if [ "$out" != "1:$p1" ]; then
@@ -153,10 +157,14 @@ if [ "$out" != "1:$p1" ]; then
fi
check_fmt 'P:1' '#{window_name}:#{window_panes}' 'broken:1'
check_fmt 'P:0' '#{window_panes}' '3'
check_fmt 'P:0' '#{window_zoomed_flag}' '0'
# join-pane -v moves it back (the source window, left empty, is destroyed).
# join-pane -v moves it back (the source window, left empty, is destroyed) and
# also unzooms the destination before changing its layout.
check_ok resize-pane -Z -t "$p0"
check_ok join-pane -d -v -s P:broken.0 -t "$p2"
check_fmt 'P:0' '#{window_panes}' '4'
check_fmt 'P:0' '#{window_zoomed_flag}' '0'
if $TMUX has-session -t P:broken 2>/dev/null; then
echo "Window 'broken' still exists after join-pane."
exit 1
@@ -299,6 +307,122 @@ check_fmt 'P:0' '#{window_panes}' '5'
p6=$($TMUX display-message -p -t P:0.2 '#{pane_id}')
check_ok kill-pane -t "$p6"
# Zoom and unzoom preserve the exact tiled layout. Selecting another pane
# without -Z unzooms, while -Z transfers zoom to the selected pane.
layout=$($TMUX display-message -p -t P:0 '#{window_layout}')
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -t "$p2"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1'
check_fmt P:0 '#{window_layout}' "$layout"
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -Z -t "$p2"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p2"
check_fmt P:0 '#{window_layout}' "$layout"
# Directional selection temporarily restores the full layout to find its
# neighbour, then follows the same unzoom or -Z transfer rules.
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -t "$p0"
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok select-pane -D -t "$p0"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1'
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -D -Z -t "$p0"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p2"
check_fmt P:0 '#{window_layout}' "$layout"
# The last-pane path has separate zoom handling, both with and without -Z.
check_ok select-pane -t "$p0"
check_ok select-pane -t "$p2"
check_ok resize-pane -Z -t "$p2"
check_ok select-pane -l -t P:0
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'0:0:1'
check_ok select-pane -t "$p0"
check_ok select-pane -t "$p2"
check_ok resize-pane -Z -t "$p2"
check_ok select-pane -l -Z -t P:0
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p0"
check_fmt P:0 '#{window_layout}' "$layout"
# Killing either a hidden ordinary pane or the zoom target unzooms.
check_ok new-window -d -t P:12 -n zoom-kill 'cat'
zk0=$($TMUX display-message -p -t P:12.0 '#{pane_id}')
zk1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat')
zk2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat')
check_ok resize-pane -Z -t "$zk0"
check_ok kill-pane -t "$zk1"
check_fmt "$zk0" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'2:0:0'
check_ok resize-pane -Z -t "$zk0"
check_ok kill-pane -t "$zk0"
check_fmt "$zk2" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'1:0:0'
check_ok kill-window -t P:12
# The same cases through natural process exit exercise server_destroy_pane.
check_ok new-window -d -t P:13 -n zoom-exit 'cat'
ze0=$($TMUX display-message -p -t P:13.0 '#{pane_id}')
ze1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat')
ze2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat')
check_ok resize-pane -Z -t "$ze0"
check_ok send-keys -t "$ze1" C-d
i=0
while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 2 ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "Hidden pane did not exit."; exit 1; }
sleep 0.1
done
check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
check_ok resize-pane -Z -t "$ze2"
check_ok send-keys -t "$ze2" C-d
i=0
while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 1 ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "Zoomed pane did not exit."; exit 1; }
sleep 0.1
done
check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
check_ok kill-window -t P:13
# resize-window restores both tiled and floating zoom targets after rebuilding
# the layout at the new size.
check_ok new-window -d -t P:14 -n zoom-resize 'cat'
zr0=$($TMUX display-message -p -t P:14.0 '#{pane_id}')
check_ok split-window -d -t P:14.0 'cat'
check_ok resize-pane -Z -t "$zr0"
check_ok resize-window -t P:14 -x 90 -y 30
check_fmt "$zr0" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'90x30:1:1'
check_ok resize-pane -Z -t "$zr0"
zrf=$($TMUX new-pane -dP -F '#{pane_id}' -t P:14 -x 20 -y 6 'cat')
zrf_size=$($TMUX display-message -p -t "$zrf" \
'#{pane_width}x#{pane_height}')
check_ok resize-pane -Z -t "$zrf"
check_ok resize-window -t P:14 -x 100 -y 32
check_fmt "$zrf" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'100x32:1:1'
check_ok resize-pane -Z -t "$zrf"
check_fmt "$zrf" '#{pane_floating_flag}:#{pane_width}x#{pane_height}' \
"1:$zrf_size"
check_ok kill-window -t P:14
# ---------------------------------------------------------------------------
# kill-pane.
@@ -307,8 +431,10 @@ check_ok kill-pane -t "$p3"
check_panes P:0 "0:$p0 1:$p2 2:$p1"
# -a kills every pane except the target.
check_ok resize-pane -Z -t "$p0"
check_ok kill-pane -a -t "$p0"
check_panes P:0 "0:$p0"
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
# Killing the last pane in a window kills the window.
check_ok new-window -d -t P:7 -n goner
@@ -516,6 +642,10 @@ check_panes P:3 "0:$r0 1:$r1 2:$r2"
# Swapping a pane with itself quietly does nothing.
check_ok swap-pane -d -s "$r1" -t "$r1"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_ok resize-pane -Z -t "$r1"
check_ok swap-pane -d -Z -s "$r1" -t "$r1"
check_fmt "$r1" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1'
check_ok resize-pane -Z -t "$r1"
# Panes can be swapped between different windows.
check_ok swap-pane -d -s "$o0" -t "$r1"
@@ -525,10 +655,38 @@ check_ok swap-pane -d -s "$r1" -t "$o0"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_panes P:5 "0:$o0"
# With both windows zoomed, -Z preserves each window's zoom when hidden panes
# are exchanged and when the zoom targets themselves are exchanged.
o1=$($TMUX split-window -dP -F '#{pane_id}' -t P:5.0)
check_ok resize-pane -Z -t "$r0"
check_ok resize-pane -Z -t "$o0"
check_ok swap-pane -d -Z -s "$r1" -t "$o1"
check_fmt "$r0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1'
check_fmt "$o0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1'
check_fmt "$r1" '#{window_name}' 'other'
check_fmt "$o1" '#{window_name}' 'swaps'
check_ok swap-pane -d -Z -s "$r0" -t "$o0"
check_fmt "$o0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1'
check_fmt "$r0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1'
check_ok resize-pane -Z -t "$o0"
check_ok resize-pane -Z -t "$r0"
# Restore the original panes before the remaining swap tests.
check_ok swap-pane -d -s "$r0" -t "$o0"
check_ok swap-pane -d -s "$r1" -t "$o1"
check_ok kill-pane -t "$o1"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_panes P:5 "0:$o0"
# -Z keeps the window zoomed across the swap.
check_ok resize-pane -Z -t "$r0"
check_ok swap-pane -d -Z -s "$r0" -t "$r1"
check_fmt 'P:3' '#{window_zoomed_flag}' '1'
check_fmt "$r0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1'
check_ok resize-pane -Z -t P:3
check_panes P:3 "0:$r1 1:$r0 2:$r2"

View File

@@ -0,0 +1,87 @@
#!/bin/sh
# respawn-pane frees the pane's buffer and creates an empty one. The pane
# offsets and each control client's offsets used to keep pointing into the old
# buffer, so if an attached control client had not caught up on the pane's
# output, the next read from the new process ran off the end of the new buffer
# and the server crashed in input_parse.
#
# Two ways for a client to be behind are tested: a control client whose output
# goes down a fifo that is never read (a second, healthy control client keeps
# the pane being read), and a healthy control client viewing a session that
# the pane's window has been moved out of.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)
FIFO=$DIR/fifo
SERVER=
mkfifo "$FIFO" || exit 1
cleanup() {
[ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null
$TMUX kill-server 2>/dev/null
exec 8<&- 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
alive() {
if ! kill -0 "$SERVER" 2>/dev/null; then
SERVER=
echo "server died after $1"
exit 1
fi
$TMUX has -t rt || exit 1
}
wait_clients() {
n=0
while [ $n -lt 50 ]; do
[ "$($TMUX lsc 2>/dev/null | wc -l)" -ge $1 ] && return
sleep 0.1
n=$((n + 1))
done
echo "control clients did not attach"; exit 1
}
# A detached session with a pane that writes a line every 10 milliseconds.
$TMUX -f/dev/null new -d -x 80 -y 24 -s rt \
'while :; do echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx; sleep 0.01; done' ||
exit 1
SERVER=$($TMUX display -pt rt '#{pid}')
# One control client with output down the unread fifo and one reading
# normally; stdin of both held open by sleep so they stay attached.
( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 &
exec 8<"$FIFO"
( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >/dev/null 2>&1 &
wait_clients 2
# Let the first client fall behind, then respawn the pane. The new process
# writes at once and the server must survive reading it.
sleep 3
$TMUX respawn-pane -k -t rt:0 'echo respawned; sleep 60' || exit 1
sleep 1
alive "respawn-pane with a lagging control client"
# Now a pane whose window is moved out of the session both clients view: their
# offsets for it stop advancing, so they fall behind until it is respawned.
$TMUX neww -d -t rt \
'while :; do echo yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy; sleep 0.01; done' ||
exit 1
sleep 1
$TMUX new -d -s other || exit 1
$TMUX movew -d -s rt:1 -t other: || exit 1
sleep 2
$TMUX respawn-pane -k -t other:1 'echo respawned; sleep 60' || exit 1
sleep 1
alive "respawn-pane on a window moved out of the clients' session"
exit 0

View File

@@ -14,8 +14,8 @@ DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -Lsync-output-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsync-output-outer-$$ -f/dev/null"
INNER="$TEST_TMUX -Li$$ -f/dev/null"
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
CLIENT_BYTES=$DIR/client-bytes
CONTROL=$DIR/control
EMITTER=$DIR/emitter.pl
@@ -112,7 +112,7 @@ $INNER set-option -g window-size manual || exit 1
$INNER set-option -as terminal-features '*:sync' || exit 1
$OUTER new-session -d -s outer -x 80 -y 24 \
"$TEST_TMUX -Lsync-output-inner-$$ -f/dev/null attach-session -t inner" ||
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1

View File

@@ -105,6 +105,21 @@ sleep 1
CLIENT=$($TMUX2 list-clients -F '#{client_name}' | head -1)
[ -n "$CLIENT" ] || fail "no inner client"
# A variation selector which widens the previous character must use window
# coordinates when checking visibility in a pane with a nonzero x offset.
$TMUX2 set -s variation-selector-always-wide on || exit 1
WINDOW=$($TMUX2 neww -dPF '#{window_id}' "exec sleep 100") || exit 1
PANE=$($TMUX2 splitw -dhPF '#{pane_id}' -t "$WINDOW" \
"exec sleep 100") || exit 1
$TMUX2 selectw -t "$WINDOW" || exit 1
$TMUX2 respawnp -k -t "$PANE" \
"printf '\nA\342\234\217\357\270\217B'; exec sleep 100" || exit 1
sleep 1
$TMUX2 capturep -p -t "$PANE" >$TMP || exit 1
EXPECTED=$(printf 'A\342\234\217\357\270\217B')
check_line 2 "$EXPECTED"
$TMUX2 killw -t "$WINDOW" || exit 1
# Long line, then short line: default cells after cellsize must clear stale text.
capture
check_line 1 "abcdefghijklmnopqrst"

View File

@@ -17,7 +17,8 @@
# unlink the last link without -k;
# - swap-window within and between sessions, -d keeping the active window,
# and the grouped-sessions error;
# - rotate-window -U/-D rotating pane positions;
# - rotate-window -U/-D rotating pane positions, with and without preserving
# zoom using -Z;
# - kill-window switching to the last (previously current) window, kill-window
# -a killing all other windows and the "-f only valid with -a" guard.
#
@@ -306,6 +307,24 @@ check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p1"
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{pane_index}:#{pane_id}' "0:$p0"
# Rotation without -Z unzooms. With -Z it preserves zoom and transfers it to
# the pane which arrives at the active position.
layout=$($TMUX display-message -p -t R:0 '#{window_layout}')
check_ok resize-pane -Z -t "$p0"
check_ok rotate-window -U -t R:0
check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'0:0:1'
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{window_layout}' "$layout"
check_ok resize-pane -Z -t "$p0"
check_ok rotate-window -U -Z -t R:0
check_fmt "$p1" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p1"
check_ok rotate-window -D -t R:0
check_fmt 'R:0' '#{window_layout}' "$layout"
# ---------------------------------------------------------------------------
# kill-window.

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: resize.c,v 1.58 2026/07/17 08:37:29 nicm Exp $ */
/* $OpenBSD: resize.c,v 1.59 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -42,8 +42,8 @@ resize_fire_window_resized(struct window *w, u_int old_sx, u_int old_sy)
void
resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
{
u_int old_sx = w->sx, old_sy = w->sy;
int zoomed;
struct window_pane *zwp;
u_int old_sx = w->sx, old_sy = w->sy;
/* Check size limits. */
if (sx < WINDOW_MINIMUM)
@@ -56,8 +56,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
sy = WINDOW_MAXIMUM;
/* If the window is zoomed, unzoom. */
zoomed = w->flags & WINDOW_ZOOMED;
if (zoomed)
zwp = window_zoomed_pane(w);
if (zwp != NULL)
window_unzoom(w, 1);
/* Resize the layout first. */
@@ -73,8 +73,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel)
sx, sy, w->layout_root->g.sx, w->layout_root->g.sy);
/* Restore the window zoom state. */
if (zoomed)
window_zoom(w->active);
if (zwp != NULL && window_has_pane(w, zwp))
window_zoom(zwp);
tty_update_window_offset(w);
server_redraw_window(w);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */
/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -314,12 +314,14 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
if (~ctx->flags & SCREEN_WRITE_SYNC) {
/*
* For the active pane or for an overlay (no pane), we want to
* only use synchronized updates if requested (commands that
* move the cursor); for other panes, always use it, since the
* cursor will have to move.
* For the active pane showing its base screen or for an
* overlay (no pane), only use synchronized updates if
* requested (commands that move the cursor); for other panes
* or a pane in a mode, always use it, since the cursor will
* have to move.
*/
if (ctx->wp != NULL && ctx->wp != ctx->wp->window->active)
if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active ||
ctx->wp->screen != &ctx->wp->base))
ttyctx->flags |= TTY_CTX_SYNC;
else {
if (ctx->wp == NULL)
@@ -347,10 +349,18 @@ screen_write_make_list(struct screen *s)
void
screen_write_free_list(struct screen *s)
{
u_int y;
struct screen_write_cline *cl;
struct screen_write_citem *ci, *ci1;
u_int y;
for (y = 0; y < screen_size_y(s); y++)
free(s->write_list[y].data);
for (y = 0; y < screen_size_y(s); y++) {
cl = &s->write_list[y];
TAILQ_FOREACH_SAFE(ci, &cl->items, entry, ci1) {
TAILQ_REMOVE(&cl->items, ci, entry);
screen_write_free_citem(ci);
}
free(cl->data);
}
free(s->write_list);
}
@@ -1068,6 +1078,19 @@ screen_write_stop_sync(struct window_pane *wp)
log_debug("%s: %%%u stopped sync mode", __func__, wp->id);
}
/* Flush pending output before clearing sync mode. */
void
screen_write_end_sync(struct screen_write_ctx *ctx)
{
struct window_pane *wp = ctx->wp;
if (wp == NULL)
return;
if (wp->base.mode & MODE_SYNC)
screen_write_collect_flush(ctx, 0, __func__);
screen_write_stop_sync(wp);
}
/* Cursor up by ny. */
void
screen_write_cursorup(struct screen_write_ctx *ctx, u_int ny)
@@ -2848,10 +2871,11 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
struct grid *gd = s->grid;
const struct utf8_data *ud = &gc->data;
struct options *oo = global_options;
u_int i, n, cx = s->cx, cy = s->cy, vis, yoff = 0;
u_int i, n, cx = s->cx, cy = s->cy, vis;
struct grid_cell last;
struct tty_ctx ttyctx;
int force_wide = 0, zero_width = 0;
int xoff = 0, yoff = 0;
struct visible_ranges *r;
/* Ignore U+3164 HANGUL_FILLER entirely. */
@@ -2945,9 +2969,11 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
* obscured in the middle, only on left or right, but there could be an
* empty range in the visible ranges so we add them all up.
*/
if (wp != NULL)
if (wp != NULL) {
xoff = wp->xoff;
yoff = wp->yoff;
r = window_visible_ranges(wp, cx - n, cy + yoff, n, NULL);
}
r = window_visible_ranges(wp, xoff + cx - n, cy + yoff, n, NULL);
for (i = 0, vis = 0; i < r->used; i++)
vis += r->ranges[i].nx;
if (vis < n) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.503 2026/08/17 07:56:56 nicm Exp $ */
/* $OpenBSD: server-client.c,v 1.507 2026/08/24 21:17:19 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -38,13 +38,14 @@ static void server_client_repeat_timer(int, short, void *);
static void server_client_click_timer(int, short, void *);
static void server_client_check_exit(struct client *, int);
static void server_client_exit_timer(int, short, void *);
static int server_client_check_redraw(struct client *);
static void server_client_check_redraw(struct client *);
static void server_client_check_modes(struct client *);
static void server_client_set_title(struct client *);
static void server_client_set_path(struct client *);
static void server_client_set_progress_bar(struct client *);
static void server_client_reset_state(struct client *);
static void server_client_update_latest(struct client *);
static int server_client_handle_dead_key(struct window_pane *, key_code);
static void server_client_dispatch(struct imsg *, void *);
static int server_client_dispatch_command(struct client *, struct imsg *);
static int server_client_dispatch_identify(struct client *, struct imsg *);
@@ -540,6 +541,7 @@ server_client_lost(struct client *c)
input_cancel_requests(c);
free(c->title);
free(c->path);
free((void *)c->cwd);
evtimer_del(&c->repeat_timer);
@@ -817,8 +819,7 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py,
} else {
/* Try the pane borders. */
TAILQ_FOREACH(fwp, &w->panes, entry) {
if ((w->flags & WINDOW_ZOOMED) &&
(~fwp->flags & PANE_ZOOMED))
if (!window_pane_is_visible(fwp))
continue;
if (window_pane_is_floating(fwp) &&
window_pane_get_pane_lines(fwp) == PANE_LINES_NONE)
@@ -1405,6 +1406,21 @@ server_client_repeat_time(struct client *c, struct key_binding *bd)
return (repeat);
}
/* Handle a key press on a dead pane waiting for a key. */
static int
server_client_handle_dead_key(struct window_pane *wp, key_code key)
{
if (wp == NULL ||
(~wp->flags & PANE_EXITED) ||
KEYC_IS_MOUSE(key) ||
KEYC_IS_PASTE(key) ||
options_get_number(wp->options, "remain-on-exit") != 3)
return (0);
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
return (1);
}
/*
* Handle data key input from client. This owns and can modify the key event it
* is given and is responsible for freeing it.
@@ -1501,6 +1517,14 @@ server_client_key_callback(struct cmdq_item *item, void *data)
server_client_is_assume_paste(c))
goto paste_key;
/* Forward keys directly if this pane is capturing all keys. */
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
(~wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
TAILQ_EMPTY(&wp->modes))
goto forward_key;
/*
* Work out the current key table. If the pane is in a mode, use
* the mode table instead of the default key table.
@@ -1665,15 +1689,8 @@ try_again:
}
forward_key:
if (wp != NULL &&
(wp->flags & PANE_EXITED) &&
!KEYC_IS_MOUSE(key) &&
!KEYC_IS_PASTE(key) &&
options_get_number(wp->options, "remain-on-exit") == 3) {
options_set_number(wp->options, "remain-on-exit", 0);
server_destroy_pane(wp, 0);
if (server_client_handle_dead_key(wp, key))
goto out;
}
if (c->flags & CLIENT_READONLY)
goto out;
if (wp != NULL)
@@ -1761,9 +1778,9 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
/*
* Key presses in overlay mode and the command prompt are a special
* case. The queue might be blocked so they need to be processed
* immediately rather than queued.
* Key presses in overlay mode, for panes capturing all keys and in the
* command prompt are a special case. The queue might be blocked so they
* need to be processed immediately rather than queued.
*/
if (~c->flags & CLIENT_READONLY) {
if (c->message_string != NULL) {
@@ -1783,6 +1800,21 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
server_client_clear_overlay(c);
wp = s->curw->window->active;
if (wp != NULL &&
(wp->flags & PANE_CAPTUREALLKEYS) &&
TAILQ_EMPTY(&wp->modes) &&
!KEYC_IS_MOUSE(event->key)) {
if (server_client_handle_dead_key(wp, event->key))
return (0);
if (~wp->flags & PANE_EXITED) {
window_pane_key(wp, c, s, s->curw, event->key,
&event->m);
return (0);
}
}
if (server_client_handle_menu_key(c, event))
return (0);
if (c->prompt != NULL) {
@@ -1883,25 +1915,19 @@ server_client_loop(void)
server_client_check_exit(c, 0);
if (c->session != NULL && c->session->curw != NULL) {
server_client_check_modes(c);
if (server_client_check_redraw(c))
c->session->curw->window->redraw_deferred = 1;
server_client_check_redraw(c);
server_client_reset_state(c);
}
}
/*
* Any windows will have been redrawn as part of clients, so clear
* their flags now. Neither window damage nor PANE_REDRAW/
* PANE_REDRAWSCROLLBAR are lossy to leave for a later pass, so only
* clear them once every client actually viewing the window has
* drawn (none deferred this pass, per the loop above), otherwise
* leave them for the deferred redraw timer to retry. Checking
* EVBUFFER_LENGTH(tty->out) here instead would be wrong: a redraw
* that was NOT deferred still just wrote fresh bytes into that same
* buffer, which libevent has not flushed to the fd yet, so it would
* look identical to a deferred one and nothing would ever clear.
* PANE_ACTIVITY is unrelated bookkeeping (activity monitoring, not
* redraw) and keeps its original unconditional-clear behaviour.
* their flags now. A client whose redraw was deferred this pass
* (waiting for outstanding tty output to drain) has already
* escalated to CLIENT_REDRAWWINDOW or CLIENT_REDRAWSCROLLBARS in
* server_client_check_redraw() to cover whatever it is about to
* lose here, so PANE_REDRAW/PANE_REDRAWSCROLLBAR and window damage
* can simply be cleared unconditionally.
*/
RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry) {
@@ -1909,15 +1935,10 @@ server_client_loop(void)
server_client_check_pane_resize(wp);
server_client_check_pane_buffer(wp);
}
wp->flags &= ~PANE_ACTIVITY;
if (!w->redraw_deferred)
wp->flags &= ~(PANE_REDRAW|
PANE_REDRAWSCROLLBAR);
wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR|
PANE_ACTIVITY);
}
if (!w->redraw_deferred)
redraw_free_damage(w);
w->redraw_deferred = 0;
redraw_free_damage(w);
check_window_name(w);
}
@@ -2474,11 +2495,8 @@ server_client_any_pane_redraw(struct client *c)
return (0);
}
/*
* Check for client redraws. Returns 1 if the redraw was deferred (waiting
* for outstanding tty output to drain) rather than actually performed.
*/
static int
/* Check for client redraws. */
static void
server_client_check_redraw(struct client *c)
{
struct session *s = c->session;
@@ -2491,7 +2509,7 @@ server_client_check_redraw(struct client *c)
size_t n;
if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
return (0);
return;
if (c->flags & CLIENT_ALLREDRAWFLAGS) {
log_debug("%s: redraw%s%s%s%s%s", c->name,
(c->flags & CLIENT_REDRAWWINDOW) ? " window" : "",
@@ -2503,25 +2521,24 @@ server_client_check_redraw(struct client *c)
/* Work out if a redraw is actually needed. */
needed = 0;
if (c->flags & CLIENT_ALLREDRAWFLAGS)
if (c->flags & (CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS))
needed = 1;
else if (server_client_any_pane_redraw(c))
needed = 1;
if (!needed) {
c->flags &= ~CLIENT_STATUSFORCE;
return (0);
return;
}
/*
* If there is outstanding data, defer the redraw until it has been
* consumed. We can just add a timer to get out of the event loop and
* end up back here. Unlike before, this does not need to escalate to
* a full CLIENT_REDRAWWINDOW redraw to avoid losing whatever is
* pending: server_client_loop() only clears PANE_REDRAW,
* PANE_REDRAWSCROLLBAR and window damage once a pass actually drew
* them (see the redraw_deferred handling there), so they are simply
* left in place and retried in their normal, narrowly-scoped form
* next time.
* end up back here. server_client_loop() clears PANE_REDRAW,
* PANE_REDRAWSCROLLBAR and window damage unconditionally every pass,
* so escalate to a coarser, persistent client flag that survives
* that clear and forces a full catch-up redraw once this client is
* unblocked, rather than trying to keep the fine-grained state
* around for a retry.
*/
n = EVBUFFER_LENGTH(tty->out);
if (n != 0 || (tty->flags & TTY_BLOCK)) {
@@ -2535,7 +2552,17 @@ server_client_check_redraw(struct client *c)
log_debug("redraw timer started");
evtimer_add(&ev, &tv);
}
return (1);
if (!TAILQ_EMPTY(&w->damage))
c->flags |= CLIENT_REDRAWWINDOW;
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW;
break;
}
if (wp->flags & PANE_REDRAWSCROLLBAR)
c->flags |= CLIENT_REDRAWSCROLLBARS;
}
return;
}
/* Unfreeze the tty and turn off the cursor. */
@@ -2553,7 +2580,8 @@ server_client_check_redraw(struct client *c)
log_debug("%s: redraw pane %%%u", __func__,
wp->id);
redraw_pane(c, wp);
} else if (wp->flags & PANE_REDRAWSCROLLBAR) {
} else if ((wp->flags & PANE_REDRAWSCROLLBAR) ||
(c->flags & CLIENT_REDRAWSCROLLBARS)) {
log_debug("%s: redraw scrollbar %%%u", __func__,
wp->id);
redraw_pane_scrollbar(c, wp);
@@ -2565,9 +2593,10 @@ server_client_check_redraw(struct client *c)
* redraw() decide a redraw is needed at all, independently of
* any CLIENT_ALLREDRAWFLAGS bit. Every current damage source
* happens to set one of those flags too, so the block below
* always consumes it - but fall back to consuming it here in
* case that ever stops holding, rather than silently
* dropping it when redraw_free_damage() clears it next loop.
* always consumes it - but consume it here too in case that
* ever stops holding, since server_client_loop() clears
* window damage unconditionally every pass regardless of
* whether it was actually drawn.
*/
if (!TAILQ_EMPTY(&w->damage) && (~c->flags & CLIENT_ALLREDRAWFLAGS))
redraw_client_damage(c);
@@ -2596,10 +2625,10 @@ server_client_check_redraw(struct client *c)
* All the redraw flags can now be cleared. Also record how many bytes
* were written.
*/
c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_STATUSFORCE);
c->flags &= ~(CLIENT_ALLREDRAWFLAGS|CLIENT_REDRAWSCROLLBARS|
CLIENT_STATUSFORCE);
c->redraw = EVBUFFER_LENGTH(tty->out);
log_debug("%s: redraw added %zu bytes", c->name, c->redraw);
return (0);
}
/* Set client title. */

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-fn.c,v 1.149 2026/07/27 14:25:46 nicm Exp $ */
/* $OpenBSD: server-fn.c,v 1.151 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -226,10 +226,11 @@ server_kill_pane(struct window_pane *wp)
server_kill_window(w, 1);
recalculate_sizes();
} else {
server_unzoom_window(w);
window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM);
server_client_remove_pane(wp);
layout_close_pane(wp);
window_remove_pane(w, wp);
window_pop_zoom(w);
server_redraw_window(w);
}
}
@@ -421,15 +422,17 @@ server_destroy_pane(struct window_pane *wp, int notify)
if (notify)
server_fire_pane_exit("pane-exited", wp);
server_unzoom_window(w);
window_push_zoom(w, 0, wp->flags & PANE_FLOATOVERZOOM);
server_client_remove_pane(wp);
layout_close_pane(wp);
window_remove_pane(w, wp);
if (TAILQ_EMPTY(&w->panes))
server_kill_window(w, 1);
else
else {
window_pop_zoom(w);
server_redraw_window(w);
}
}
static void
@@ -551,6 +554,7 @@ server_check_unattached(void)
continue;
break;
}
server_destroy_session(s);
session_destroy(s, 1, __func__);
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */
/* $OpenBSD: server.c,v 1.216 2026/08/18 07:24:49 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -129,6 +129,7 @@ server_create_socket(uint64_t flags, char **cause)
mask = umask(S_IXUSR|S_IRWXG|S_IRWXO);
if (bind(fd, (struct sockaddr *)&sa, sizeof sa) == -1) {
saved_errno = errno;
umask(mask);
close(fd);
errno = saved_errno;
goto fail;

29
spawn.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: spawn.c,v 1.50 2026/07/23 09:38:27 nicm Exp $ */
/* $OpenBSD: spawn.c,v 1.52 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -243,7 +243,7 @@ struct window_pane *
spawn_pane(struct spawn_context *sc, char **cause)
{
struct cmdq_item *item = sc->item;
struct client *c;
struct client *c, *loop;
struct session *s = sc->s;
struct session *ts;
struct window *w = sc->wl->window;
@@ -331,6 +331,20 @@ spawn_pane(struct spawn_context *sc, char **cause)
input_free(sc->wp0->ictx);
sc->wp0->ictx = NULL;
}
/*
* The old buffer is gone and the new one starts empty, so
* offsets into the old buffer no longer mean anything. Reset
* them, and drop output control clients still had queued.
*/
sc->wp0->offset.used = 0;
sc->wp0->base_offset = 0;
sc->wp0->pipe_offset.used = 0;
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->flags & CLIENT_CONTROL)
control_reset_pane(loop, sc->wp0);
}
new_wp = sc->wp0;
new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN);
} else {
@@ -346,6 +360,8 @@ spawn_pane(struct spawn_context *sc, char **cause)
}
if (sc->flags & SPAWN_FLOATING)
new_wp->layout_cell->flags |= LAYOUT_CELL_FLOATING;
if (sc->flags & SPAWN_FLOATOVERZOOM)
new_wp->flags |= PANE_FLOATOVERZOOM;
/*
* If window currently zoomed, window_set_active_pane calls
@@ -731,10 +747,10 @@ spawn_editor(struct client *c, const char *buf, size_t len,
lg.sy = w->sy * 9 / 10;
lg.xoff = w->sx / 2 - lg.sx / 2;
lg.yoff = w->sy / 2 - lg.sy / 2;
window_push_modal_zoom(w);
window_push_zoom(w, 0, 1);
lc = layout_floating_pane(w, NULL, &lg);
if (lc == NULL) {
window_pop_modal_zoom(w);
window_pop_zoom(w);
spawn_editor_free(es);
return (NULL);
}
@@ -751,17 +767,18 @@ spawn_editor(struct client *c, const char *buf, size_t len,
sc.environ = env;
sc.idx = -1;
sc.cwd = _PATH_TMP;
sc.flags = SPAWN_FLOATING|SPAWN_MODAL;
sc.flags = SPAWN_FLOATING|SPAWN_MODAL|SPAWN_FLOATOVERZOOM;
wp = spawn_pane(&sc, &cause);
free(cmd);
environ_free(env);
if (wp == NULL) {
free(cause);
window_pop_modal_zoom(w);
window_pop_zoom(w);
spawn_editor_free(es);
return (NULL);
}
window_pop_zoom(w);
options_set_number(wp->options, "remain-on-exit", 0);
es->pid = wp->pid;
wp->editor = es;

13
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1156 2026/08/17 14:47:41 nicm Exp $
.\" $OpenBSD: tmux.1,v 1.1158 2026/08/20 09:19:24 nicm Exp $
.\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\"
.Dd $Mdocdate: August 17 2026 $
.Dd $Mdocdate: August 20 2026 $
.Dt TMUX 1
.Os
.Sh NAME
@@ -3644,7 +3644,7 @@ but a different format may be specified with
.Fl F .
.Tg newp
.It Xo Ic new\-pane
.Op Fl bCdefhIkLMOPvWZ
.Op Fl AbCdefhIkKLMOPvWZ
.Op Fl B Ar border\-lines
.Op Fl c Ar start\-directory
.Op Fl e Ar environment
@@ -3693,14 +3693,17 @@ sets the pane border lines for floating panes; see
.Fl M
may be used when bound to a mouse drag key; the new floating pane is resized to
follow the mouse drag.
It has no effect with
.Fl L .
.Fl A
creates a floating pane which remains visible above a zoomed pane.
.Fl O
creates a modal pane.
A modal pane is always the active pane and prevents interaction with any other
panes while it is active.
A window can only have one modal pane and it must be a floating pane.
With
.Fl K ,
all keys, including the prefix key, are passed directly to the modal pane.
With
.Fl C ,
the modal pane is closed when the mouse is clicked outside it.
.Pp

19
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1424 2026/08/17 14:47:41 nicm Exp $ */
/* $OpenBSD: tmux.h,v 1.1430 2026/08/24 21:17:19 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1336,6 +1336,8 @@ struct window_pane {
#define PANE_CMDRUNNING 0x20000
#define PANE_ACTIVITY 0x40000
#define PANE_CLOSEONCLICK 0x80000
#define PANE_CAPTUREALLKEYS 0x100000
#define PANE_FLOATOVERZOOM 0x200000
bitstr_t *sync_dirty;
u_int sync_dirty_size;
@@ -1443,6 +1445,7 @@ struct window {
struct window_pane *active;
struct window_pane *modal;
struct window_pane *modal_last;
struct window_pane *was_zoomed;
struct window_panes last_panes;
struct window_panes z_index;
struct window_panes panes;
@@ -1468,7 +1471,6 @@ struct window {
struct redraw_damages damage;
u_int damage_count;
int redraw_deferred;
struct menu_data *menu;
u_int menu_last_px;
@@ -1489,7 +1491,6 @@ struct window {
#define WINDOW_ZOOMED 0x8
#define WINDOW_WASZOOMED 0x10
#define WINDOW_RESIZE 0x20
#define WINDOW_WASMODALZOOMED 0x40
#define WINDOW_ALERTFLAGS (WINDOW_BELL|WINDOW_ACTIVITY|WINDOW_SILENCE)
int alerts_queued;
@@ -1817,8 +1818,9 @@ struct tty {
#define TTY_WAITFG 0x2000
#define TTY_WAITBG 0x4000
#define TTY_BRACKETPASTE 0x8000
#define TTY_HAVESYNC 0x10000
#define TTY_ALL_REQUEST_FLAGS \
(TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA)
(TTY_HAVEDA|TTY_HAVEDA2|TTY_HAVEXDA|TTY_HAVESYNC)
int flags;
struct tty_term *term;
@@ -2293,7 +2295,7 @@ struct client {
#define CLIENT_STARTSERVER 0x10000000
#define CLIENT_REDRAWMENU 0x20000000
#define CLIENT_NOFORK 0x40000000
/* 0x80000000ULL unused */
#define CLIENT_REDRAWSCROLLBARS 0x80000000ULL
#define CLIENT_CONTROL_PAUSEAFTER 0x100000000ULL
#define CLIENT_CONTROL_WAITEXIT 0x200000000ULL
#define CLIENT_WINDOWSIZECHANGED 0x400000000ULL
@@ -2539,6 +2541,7 @@ struct spawn_context {
#define SPAWN_HORIZONTAL 0x200
#define SPAWN_SPLIT 0x400
#define SPAWN_MODAL 0x800
#define SPAWN_FLOATOVERZOOM 0x1000
};
/* Paste buffer. */
@@ -3606,6 +3609,7 @@ void screen_write_mode_set(struct screen_write_ctx *, int);
void screen_write_mode_clear(struct screen_write_ctx *, int);
void screen_write_start_sync(struct window_pane *);
void screen_write_stop_sync(struct window_pane *);
void screen_write_end_sync(struct screen_write_ctx *);
void screen_write_clear_dirty(struct window_pane *);
void screen_write_cursorup(struct screen_write_ctx *, u_int);
void screen_write_cursordown(struct screen_write_ctx *, u_int);
@@ -3738,8 +3742,8 @@ void window_resize(struct window *, u_int, u_int, int, int);
void window_pane_send_resize(struct window_pane *, u_int, u_int);
int window_zoom(struct window_pane *);
int window_unzoom(struct window *, int);
void window_push_modal_zoom(struct window *);
int window_pop_modal_zoom(struct window *);
int window_active_pane_is_over_zoom(struct window *);
struct window_pane *window_zoomed_pane(struct window *);
int window_push_zoom(struct window *, int, int);
int window_pop_zoom(struct window *);
void window_lost_pane(struct window *, struct window_pane *);
@@ -4046,6 +4050,7 @@ void control_set_pane_on(struct client *, struct window_pane *);
void control_set_pane_off(struct client *, struct window_pane *);
void control_continue_pane(struct client *, struct window_pane *);
void control_pause_pane(struct client *, struct window_pane *);
void control_reset_pane(struct client *, struct window_pane *);
void control_set_window_size(struct client *, u_int, u_int, u_int);
int control_get_window_size(struct client *, u_int, u_int *, u_int *);
void control_clear_window_size(struct client *, u_int);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-keys.c,v 1.213 2026/08/17 14:47:41 nicm Exp $ */
/* $OpenBSD: tty-keys.c,v 1.214 2026/08/18 09:01:20 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -59,6 +59,7 @@ static int tty_keys_device_attributes2(struct tty *, const char *, size_t,
size_t *);
static int tty_keys_extended_device_attributes(struct tty *, const char *,
size_t, size_t *);
static int tty_keys_sync(struct tty *, const char *, size_t, size_t *);
static int tty_keys_palette(struct tty *, const char *, size_t, size_t *);
/* A key tree entry. */
@@ -771,6 +772,17 @@ tty_keys_next(struct tty *tty)
goto partial_key;
}
/* Is this a synchronized update mode response? */
switch (tty_keys_sync(tty, buf, len, &size)) {
case 0: /* yes */
key = KEYC_UNKNOWN;
goto complete_key;
case -1: /* no, or not valid */
break;
case 1: /* partial */
goto partial_key;
}
/* Is this a primary device attributes response? */
switch (tty_keys_device_attributes(tty, buf, len, &size)) {
case 0: /* yes */
@@ -1521,6 +1533,54 @@ tty_keys_device_attributes(struct tty *tty, const char *buf, size_t len,
return (0);
}
/*
* Handle a synchronized update mode response. Returns 0 for success, -1 for
* failure, 1 for partial.
*/
static int
tty_keys_sync(struct tty *tty, const char *buf, size_t len, size_t *size)
{
struct client *c = tty->client;
static const char prefix[] = "\033[?2026;";
size_t i;
int status;
*size = 0;
if (tty->flags & TTY_HAVESYNC)
return (-1);
/* The response is always \033[?2026;Ps$y. */
for (i = 0; i < (sizeof prefix) - 1; i++) {
if (i == len)
return (1);
if (buf[i] != prefix[i])
return (-1);
}
if (i == len)
return (1);
if (buf[i] < '0' || buf[i] > '4')
return (-1);
status = buf[i++] - '0';
if (i == len)
return (1);
if (buf[i++] != '$')
return (-1);
if (i == len)
return (1);
if (buf[i++] != 'y')
return (-1);
*size = i;
if (status == 1 || status == 2 || status == 3) {
tty_parse_client_features(c, "sync", ",");
tty_update_features(tty);
}
log_debug("%s: received DECRPM %.*s", c->name, (int)*size, buf);
tty->flags |= TTY_HAVESYNC;
return (0);
}
/*
* Handle secondary device attributes input. Returns 0 for success, -1 for
* failure, 1 for partial.

4
tty.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty.c,v 1.478 2026/08/17 14:47:41 nicm Exp $ */
/* $OpenBSD: tty.c,v 1.479 2026/08/18 09:01:20 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -403,6 +403,8 @@ tty_send_requests(struct tty *tty)
tty_puts(tty, "\033[>c");
if (~tty->flags & TTY_HAVEXDA)
tty_puts(tty, "\033[>q");
if (~tty->flags & TTY_HAVESYNC)
tty_puts(tty, "\033[?2026$p");
tty_puts(tty, "\033]10;?\033\\\033]11;?\033\\");
tty->flags |= (TTY_WAITBG|TTY_WAITFG);
} else

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-buffer.c,v 1.51 2026/07/15 12:45:39 nicm Exp $ */
/* $OpenBSD: window-buffer.c,v 1.52 2026/08/24 21:19:40 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -567,9 +567,7 @@ window_buffer_edit_close_cb(char *buf, size_t len, void *arg)
}
oldbuf = paste_buffer_data(pb, &oldlen);
if (oldlen != '\0' &&
oldbuf[oldlen - 1] != '\n' &&
buf[len - 1] == '\n')
if (oldlen != 0 && oldbuf[oldlen - 1] != '\n' && buf[len - 1] == '\n')
len--;
if (len != 0)
paste_replace(pb, buf, len);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-copy.c,v 1.425 2026/08/05 12:23:25 nicm Exp $ */
/* $OpenBSD: window-copy.c,v 1.426 2026/08/19 19:55:04 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -4093,7 +4093,7 @@ window_copy_search_lr_regex(struct grid *gd, u_int *ppx, u_int *psx, u_int py,
endline = gd->hsize + gd->sy - 1;
pywrap = py;
while (buf != NULL &&
pywrap <= endline &&
pywrap < endline &&
len < WINDOW_COPY_SEARCH_MAX_LINE) {
gl = grid_get_line(gd, pywrap);
if (~gl->flags & GRID_LINE_WRAPPED)
@@ -4152,7 +4152,7 @@ window_copy_search_rl_regex(struct grid *gd, u_int *ppx, u_int *psx, u_int py,
endline = gd->hsize + gd->sy - 1;
pywrap = py;
while (buf != NULL &&
pywrap <= endline &&
pywrap < endline &&
len < WINDOW_COPY_SEARCH_MAX_LINE) {
gl = grid_get_line(gd, pywrap);
if (~gl->flags & GRID_LINE_WRAPPED)
@@ -4353,6 +4353,7 @@ window_copy_cstrtocellpos(struct grid *gd, u_int ncells, u_int *ppx, u_int *ppy,
break;
}
}
ncells = cell;
/* Locate starting cell. */
cell = 0;

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-tree.c,v 1.94 2026/07/14 17:17:18 nicm Exp $ */
/* $OpenBSD: window-tree.c,v 1.95 2026/08/24 07:06:58 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -642,6 +642,7 @@ window_tree_draw_session(struct window_tree_modedata *data, struct session *s,
}
free(label);
}
format_free(ft);
if (loop != end - 1) {
screen_write_cursormove(ctx, cx + offset + width, cy,
@@ -790,6 +791,7 @@ window_tree_draw_window(struct window_tree_modedata *data, struct session *s,
}
free(label);
}
format_free(ft);
if (loop != end - 1) {
screen_write_cursormove(ctx, cx + offset + width, cy,

142
window.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window.c,v 1.369 2026/07/29 14:06:32 nicm Exp $ */
/* $OpenBSD: window.c,v 1.373 2026/08/24 21:17:19 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -713,7 +713,7 @@ int
window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
{
struct window_pane *lastwp;
int was_zoomed;
int unzoomed;
log_debug("%s: pane %%%u", __func__, wp->id);
@@ -721,8 +721,8 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
return (0);
if (w->modal != NULL && wp != w->modal)
return (0);
was_zoomed = (w->flags & WINDOW_ZOOMED) != 0;
if (was_zoomed)
unzoomed = (w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp);
if (unzoomed)
window_unzoom(w, 1);
lastwp = w->active;
@@ -746,7 +746,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
* and status appearance changed, so avoid redrawing unaffected pane
* content.
*/
if (was_zoomed)
if (unzoomed)
server_redraw_window(w);
else {
server_redraw_window_borders(w);
@@ -934,13 +934,18 @@ window_zoom(struct window_pane *wp)
{
struct window *w = wp->window;
struct window_pane *wp1;
struct layout_cell *lc;
struct layout_geometry lg;
if (w->flags & WINDOW_ZOOMED)
return (-1);
if (window_count_panes(w, 1) == 1)
return (-1);
if (w->active != wp)
if (w->active != wp &&
(w->active == NULL ||
(~w->active->flags & PANE_FLOATOVERZOOM) ||
!window_pane_is_floating(w->active)))
window_set_active_pane(w, wp, 1);
wp->flags |= PANE_ZOOMED;
@@ -951,6 +956,22 @@ window_zoom(struct window_pane *wp)
w->saved_layout_root = w->layout_root;
layout_init(w, wp);
TAILQ_FOREACH(wp1, &w->panes, entry) {
lc = wp1->saved_layout_cell;
if (wp1 == wp ||
(~wp1->flags & PANE_FLOATOVERZOOM) ||
lc == NULL ||
(~lc->flags & LAYOUT_CELL_FLOATING))
continue;
memcpy(&lg, &lc->g, sizeof lg);
lc = layout_floating_pane(w, wp, &lg);
layout_assign_pane(lc, wp1, 0);
}
/* A floating zoom target is now tiled, so put it behind the floats. */
if (wp->saved_layout_cell->flags & LAYOUT_CELL_FLOATING) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_TAIL(&w->z_index, wp, zentry);
}
w->flags |= WINDOW_ZOOMED;
events_fire_window("window-zoomed", w);
events_fire_window("window-layout-changed", w);
@@ -962,11 +983,26 @@ window_zoom(struct window_pane *wp)
int
window_unzoom(struct window *w, int notify)
{
struct window_pane *wp;
struct window_pane *wp, *zoomed = NULL;
struct layout_cell *slc;
if (!(w->flags & WINDOW_ZOOMED))
if (~w->flags & WINDOW_ZOOMED)
return (-1);
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_ZOOMED)
zoomed = wp;
if (~wp->flags & PANE_FLOATOVERZOOM)
continue;
if (wp->flags & PANE_ZOOMED)
continue;
slc = wp->saved_layout_cell;
if (slc == NULL || wp->layout_cell == NULL)
continue;
memcpy(&slc->g, &wp->layout_cell->g, sizeof slc->g);
memcpy(&slc->fg, &wp->layout_cell->fg, sizeof slc->fg);
}
w->flags &= ~WINDOW_ZOOMED;
layout_free(w, 0);
w->layout_root = w->saved_layout_root;
@@ -977,6 +1013,22 @@ window_unzoom(struct window *w, int notify)
wp->saved_layout_cell = NULL;
wp->flags &= ~PANE_ZOOMED;
}
/* Put a floating zoom target back into the floating part of the list. */
if (zoomed != NULL && window_pane_is_floating(zoomed)) {
TAILQ_REMOVE(&w->z_index, zoomed, zentry);
if (zoomed == w->active)
TAILQ_INSERT_HEAD(&w->z_index, zoomed, zentry);
else {
TAILQ_FOREACH(wp, &w->z_index, zentry) {
if (!window_pane_is_floating(wp))
break;
}
if (wp == NULL)
TAILQ_INSERT_TAIL(&w->z_index, zoomed, zentry);
else
TAILQ_INSERT_BEFORE(wp, zoomed, zentry);
}
}
layout_fix_panes(w, NULL);
if (notify) {
@@ -988,48 +1040,69 @@ window_unzoom(struct window *w, int notify)
return (0);
}
void
window_push_modal_zoom(struct window *w)
struct window_pane *
window_zoomed_pane(struct window *w)
{
if (w->flags & WINDOW_ZOOMED)
w->flags |= WINDOW_WASMODALZOOMED;
else
w->flags &= ~WINDOW_WASMODALZOOMED;
window_unzoom(w, 1);
struct window_pane *wp;
if (~w->flags & WINDOW_ZOOMED)
return (NULL);
TAILQ_FOREACH_REVERSE(wp, &w->z_index, window_panes_zindex, zentry) {
if (wp->layout_cell != NULL && !window_pane_is_floating(wp))
return (wp);
}
return (NULL);
}
int
window_pop_modal_zoom(struct window *w)
window_active_pane_is_over_zoom(struct window *w)
{
struct window_pane *wp = w->active;
if (~w->flags & WINDOW_WASMODALZOOMED)
if (~w->flags & WINDOW_ZOOMED)
return (0);
w->flags &= ~WINDOW_WASMODALZOOMED;
if (wp != NULL && window_has_pane(w, wp))
return (window_zoom(wp) == 0);
return (0);
if (w->active == NULL)
return (0);
if (~w->active->flags & PANE_FLOATOVERZOOM)
return (0);
return (window_pane_is_floating(w->active));
}
int
window_push_zoom(struct window *w, int always, int flag)
{
struct window_pane *wp = window_zoomed_pane(w);
log_debug("%s: @%u %d", __func__, w->id,
flag && (w->flags & WINDOW_ZOOMED));
if (flag && (always || (w->flags & WINDOW_ZOOMED)))
w->flags |= WINDOW_WASZOOMED;
else
w->flags &= ~WINDOW_WASZOOMED;
if (w->flags & WINDOW_WASZOOMED)
w->was_zoomed = wp;
else
w->was_zoomed = NULL;
return (window_unzoom(w, 1) == 0);
}
int
window_pop_zoom(struct window *w)
{
struct window_pane *wp = w->was_zoomed;
log_debug("%s: @%u %d", __func__, w->id,
!!(w->flags & WINDOW_WASZOOMED));
if (w->flags & WINDOW_WASZOOMED)
return (window_zoom(w->active) == 0);
if (w->flags & WINDOW_WASZOOMED) {
w->flags &= ~WINDOW_WASZOOMED;
w->was_zoomed = NULL;
if (w->active != NULL &&
((~w->active->flags & PANE_FLOATOVERZOOM) ||
!window_pane_is_floating(w->active)))
wp = w->active;
if (wp == NULL || !window_has_pane(w, wp))
wp = w->active;
if (wp != NULL)
return (window_zoom(wp) == 0);
}
return (0);
}
@@ -1081,8 +1154,8 @@ window_lost_pane(struct window *w, struct window_pane *wp)
server_clear_marked();
if (wp == w->modal_last)
w->modal_last = NULL;
if (w->modal_last == NULL)
w->flags &= ~WINDOW_WASMODALZOOMED;
if (wp == w->was_zoomed)
w->was_zoomed = NULL;
window_pane_stack_remove(&w->last_panes, wp);
if (wp == w->active) {
@@ -1109,7 +1182,6 @@ window_lost_pane(struct window *w, struct window_pane *wp)
}
} else if (wp == w->modal) {
w->modal = w->modal_last = NULL;
w->flags &= ~WINDOW_WASMODALZOOMED;
}
redraw_invalidate_scene(w);
}
@@ -1117,13 +1189,9 @@ window_lost_pane(struct window *w, struct window_pane *wp)
void
window_remove_pane(struct window *w, struct window_pane *wp)
{
int pop = (wp == w->modal);
window_lost_pane(w, wp);
TAILQ_REMOVE(&w->panes, wp, entry);
TAILQ_REMOVE(&w->z_index, wp, zentry);
if (pop && window_pop_modal_zoom(w))
server_redraw_window(w);
redraw_invalidate_scene(w);
window_pane_destroy(wp);
}
@@ -1279,6 +1347,8 @@ window_pane_printable_flags(struct window_pane *wp)
flags[pos++] = 'Z';
if (window_pane_is_floating(wp))
flags[pos++] = 'F';
if (wp->flags & PANE_FLOATOVERZOOM)
flags[pos++] = 'A';
if (wp == w->modal)
flags[pos++] = 'O';
flags[pos] = '\0';
@@ -1424,6 +1494,8 @@ window_pane_scrollbar_overlay_visible(struct window_pane *wp)
void
window_pane_scrollbar_redraw(struct window_pane *wp)
{
if (!window_pane_scrollbar_visible(wp))
return;
if (window_pane_scrollbar_overlay_visible(wp)) {
wp->flags |= PANE_REDRAW;
return;
@@ -1491,16 +1563,16 @@ window_pane_free(struct window_pane *wp)
log_debug("pane %%%u freed (%d references)", wp->id, wp->references);
free(wp->searchstr);
screen_free(&wp->status_screen);
screen_free(&wp->base);
free(wp->r.ranges);
options_free(wp->options);
free((void *)wp->cwd);
free(wp->shell);
cmd_free_argv(wp->argc, wp->argv);
colour_palette_free(&wp->palette);
style_ranges_free(&wp->border_status_line.ranges);
free(wp->border_status_line.expanded);
free(wp);
}
@@ -1985,7 +2057,7 @@ window_pane_is_visible(struct window_pane *wp)
{
if (~wp->window->flags & WINDOW_ZOOMED)
return (1);
return (wp == wp->window->active);
return (wp->layout_cell != NULL);
}
int