Merge branch 'master' into command_parser

This commit is contained in:
Nicholas Marriott
2026-08-07 16:08:26 +01:00
33 changed files with 919 additions and 210 deletions

View File

@@ -16,6 +16,7 @@ concurrency:
jobs: jobs:
action: action:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'tmux/tmux'
steps: steps:
- uses: dessant/lock-threads@v6 - uses: dessant/lock-threads@v6
with: with:

View File

@@ -16,6 +16,7 @@ jobs:
regress: regress:
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }} runs-on: ${{ matrix.runner }}
if: github.repository == 'tmux/tmux'
timeout-minutes: 45 timeout-minutes: 45
strategy: strategy:

16
cfg.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cfg.c,v 1.90 2026/07/14 17:17:17 nicm Exp $ */ /* $OpenBSD: cfg.c,v 1.91 2026/08/03 13:38:42 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -221,13 +221,15 @@ cfg_print_causes(struct cmdq_item *item)
{ {
struct client *c = cmdq_get_client(item); struct client *c = cmdq_get_client(item);
u_int i; u_int i;
char *cause;
for (i = 0; i < cfg_ncauses; i++) { for (i = 0; i < cfg_ncauses; i++) {
cause = cfg_causes[i];
if (c != NULL && (c->flags & CLIENT_CONTROL)) if (c != NULL && (c->flags & CLIENT_CONTROL))
control_write(c, "%%config-error %s", cfg_causes[i]); control_notify_write(c, "%%config-error %s", cause);
else else
cmdq_print(item, "%s", cfg_causes[i]); cmdq_print(item, "%s", cause);
free(cfg_causes[i]); free(cause);
} }
free(cfg_causes); free(cfg_causes);
@@ -242,14 +244,16 @@ cfg_show_causes(struct session *s)
struct window_pane *wp; struct window_pane *wp;
struct window_mode_entry *wme; struct window_mode_entry *wme;
u_int i; u_int i;
char *cause;
if (cfg_ncauses == 0) if (cfg_ncauses == 0)
return; return;
if (c != NULL && (c->flags & CLIENT_CONTROL)) { if (c != NULL && (c->flags & CLIENT_CONTROL)) {
for (i = 0; i < cfg_ncauses; i++) { for (i = 0; i < cfg_ncauses; i++) {
control_write(c, "%%config-error %s", cfg_causes[i]); cause = cfg_causes[i];
free(cfg_causes[i]); control_notify_write(c, "%%config-error %s", cause);
free(cause);
} }
goto out; goto out;
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-join-pane.c,v 1.73 2026/07/17 15:24:30 nicm Exp $ */ /* $OpenBSD: cmd-join-pane.c,v 1.74 2026/08/03 20:29:52 nicm Exp $ */
/* /*
* Copyright (c) 2011 George Nachman <tmux@georgester.com> * Copyright (c) 2011 George Nachman <tmux@georgester.com>
@@ -431,25 +431,23 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
if (cmd_get_entry(self) == &cmd_move_pane_entry) { if (cmd_get_entry(self) == &cmd_move_pane_entry) {
if (args_has(args, 'M')) if (args_has(args, 'M'))
return (cmd_join_pane_mouse_update(item)); return (cmd_join_pane_mouse_update(item));
if (!window_pane_is_floating(dst_wp)) { if (args_has(args, 'P') ||
cmdq_error(item, "pane is not floating"); args_has(args, 'z') ||
return (CMD_RETURN_ERROR); args_has(args, 'X') ||
}
if ((s = args_get(args, 'P')) != NULL) {
server_unzoom_window(dst_w);
return (cmd_join_pane_place(item, dst_wl, dst_wp, s));
}
if ((s = args_get(args, 'z')) != NULL) {
server_unzoom_window(dst_w);
return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s));
}
if (args_has(args, 'X') ||
args_has(args, 'Y') || args_has(args, 'Y') ||
args_has(args, 'U') || args_has(args, 'U') ||
args_has(args, 'D') || args_has(args, 'D') ||
args_has(args, 'L') || args_has(args, 'L') ||
args_has(args, 'R')) { args_has(args, 'R')) {
if (!window_pane_is_floating(dst_wp)) {
cmdq_error(item, "pane is not floating");
return (CMD_RETURN_ERROR);
}
server_unzoom_window(dst_w); server_unzoom_window(dst_w);
if ((s = args_get(args, 'P')) != NULL)
return (cmd_join_pane_place(item, dst_wl, dst_wp, s));
if ((s = args_get(args, 'z')) != NULL)
return (cmd_join_pane_zindex(item, dst_wl, dst_wp, s));
return (cmd_join_pane_move(item, args, dst_wl, dst_wp)); return (cmd_join_pane_move(item, args, dst_wl, dst_wp));
} }
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-queue.c,v 1.121 2026/07/14 17:17:17 nicm Exp $ */ /* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 nicm Exp $ */
/* /*
* Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -773,7 +773,7 @@ cmdq_guard(struct cmdq_item *item, const char *guard, int flags)
u_int number = item->number; u_int number = item->number;
if (c != NULL && (c->flags & CLIENT_CONTROL)) if (c != NULL && (c->flags & CLIENT_CONTROL))
control_write(c, "%%%s %ld %u %d", guard, t, number, flags); control_write_guard(c, guard, t, number, flags);
} }
/* Show message from command. */ /* Show message from command. */

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-split-window.c,v 1.146 2026/07/21 12:28:43 nicm Exp $ */ /* $OpenBSD: cmd-split-window.c,v 1.148 2026/08/07 08:10:53 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -99,6 +99,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (cmd_get_entry(self) == &cmd_new_pane_entry) if (cmd_get_entry(self) == &cmd_new_pane_entry)
is_floating = !args_has(args, 'L'); is_floating = !args_has(args, 'L');
else { else {
window_unzoom(w, 1);
is_floating = window_pane_is_floating(wp); is_floating = window_pane_is_floating(wp);
flags |= SPAWN_SPLIT; flags |= SPAWN_SPLIT;
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control-notify.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ /* $OpenBSD: control-notify.c,v 1.38 2026/08/03 13:38:42 nicm Exp $ */
/* /*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -44,7 +44,8 @@ control_pane_mode_changed_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue; continue;
control_write(c, "%%pane-mode-changed %%%u", wp->id); control_notify_write(c, "%%pane-mode-changed %%%u",
wp->id);
} }
return; return;
} }
@@ -53,8 +54,9 @@ control_pane_mode_changed_cb(__unused const char *name,
if (value == NULL) if (value == NULL)
return; return;
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
control_write(c, "%%pane-mode-changed %s", value); continue;
control_notify_write(c, "%%pane-mode-changed %s", value);
} }
free(value); free(value);
} }
@@ -92,7 +94,7 @@ control_window_layout_changed_cb(__unused const char *name,
continue; continue;
s = c->session; s = c->session;
if (winlink_find_by_window_id(&s->windows, w->id) != NULL) if (winlink_find_by_window_id(&s->windows, w->id) != NULL)
control_write(c, "%s", cp); control_notify_write(c, "%s", cp);
} }
free(cp); free(cp);
} }
@@ -111,7 +113,7 @@ control_window_pane_changed_cb(__unused const char *name,
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue; continue;
control_write(c, "%%window-pane-changed @%u %%%u", w->id, control_notify_write(c, "%%window-pane-changed @%u %%%u", w->id,
w->active->id); w->active->id);
} }
} }
@@ -133,9 +135,11 @@ control_window_unlinked_cb(__unused const char *name, struct event_payload *ep,
cs = c->session; cs = c->session;
if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) if (winlink_find_by_window_id(&cs->windows, w->id) != NULL)
control_write(c, "%%window-close @%u", w->id); control_notify_write(c, "%%window-close @%u", w->id);
else else {
control_write(c, "%%unlinked-window-close @%u", w->id); control_notify_write(c, "%%unlinked-window-close @%u",
w->id);
}
} }
} }
@@ -156,9 +160,11 @@ control_window_linked_cb(__unused const char *name, struct event_payload *ep,
cs = c->session; cs = c->session;
if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) if (winlink_find_by_window_id(&cs->windows, w->id) != NULL)
control_write(c, "%%window-add @%u", w->id); control_notify_write(c, "%%window-add @%u", w->id);
else else {
control_write(c, "%%unlinked-window-add @%u", w->id); control_notify_write(c, "%%unlinked-window-add @%u",
w->id);
}
} }
} }
@@ -179,11 +185,11 @@ control_window_renamed_cb(__unused const char *name, struct event_payload *ep,
cs = c->session; cs = c->session;
if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) { if (winlink_find_by_window_id(&cs->windows, w->id) != NULL) {
control_write(c, "%%window-renamed @%u %s", w->id, control_notify_write(c, "%%window-renamed @%u %s",
w->name);
} else {
control_write(c, "%%unlinked-window-renamed @%u %s",
w->id, w->name); w->id, w->name);
} else {
control_notify_write(c,
"%%unlinked-window-renamed @%u %s", w->id, w->name);
} }
} }
} }
@@ -206,11 +212,12 @@ control_client_session_changed_cb(__unused const char *name,
continue; continue;
if (cc == c) { if (cc == c) {
control_write(c, "%%session-changed $%u %s", s->id, control_notify_write(c, "%%session-changed $%u %s",
s->name); s->id, s->name);
} else { } else {
control_write(c, "%%client-session-changed %s $%u %s", control_notify_write(c,
cc->name, s->id, s->name); "%%client-session-changed %s $%u %s", cc->name,
s->id, s->name);
} }
} }
} }
@@ -226,8 +233,9 @@ control_client_detached_cb(__unused const char *name, struct event_payload *ep,
if (cc == NULL) if (cc == NULL)
return; return;
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
control_write(c, "%%client-detached %s", cc->name); continue;
control_notify_write(c, "%%client-detached %s", cc->name);
} }
} }
@@ -244,8 +252,8 @@ control_session_renamed_cb(__unused const char *name, struct event_payload *ep,
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue; continue;
control_notify_write(c, "%%session-renamed $%u %s", s->id,
control_write(c, "%%session-renamed $%u %s", s->id, s->name); s->name);
} }
} }
@@ -259,8 +267,7 @@ control_session_created_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue; continue;
control_notify_write(c, "%%sessions-changed");
control_write(c, "%%sessions-changed");
} }
} }
@@ -274,8 +281,7 @@ control_session_closed_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue; continue;
control_notify_write(c, "%%sessions-changed");
control_write(c, "%%sessions-changed");
} }
} }
@@ -298,9 +304,8 @@ control_session_window_changed_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue; continue;
control_notify_write(c, "%%session-window-changed $%u @%u",
control_write(c, "%%session-window-changed $%u @%u", s->id, s->id, s->curw->window->id);
s->curw->window->id);
} }
} }
@@ -317,8 +322,7 @@ control_paste_buffer_changed_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue; continue;
control_notify_write(c, "%%paste-buffer-changed %s", pbname);
control_write(c, "%%paste-buffer-changed %s", pbname);
} }
} }
@@ -335,8 +339,7 @@ control_paste_buffer_deleted_cb(__unused const char *name,
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c))
continue; continue;
control_notify_write(c, "%%paste-buffer-deleted %s", pbname);
control_write(c, "%%paste-buffer-deleted %s", pbname);
} }
} }

176
control.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.62 2026/07/17 08:37:29 nicm Exp $ */ /* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 nicm Exp $ */
/* /*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -53,6 +53,17 @@ struct control_block {
TAILQ_ENTRY(control_block) all_entry; TAILQ_ENTRY(control_block) all_entry;
}; };
/*
* A notification line deferred because it was generated while a command's
* %begin/%end guard block was open. Notifications must never appear inside a
* guard block, so they are held here and flushed once the block closes.
*/
struct control_line {
char *line;
TAILQ_ENTRY(control_line) entry;
};
/* Control client pane. */ /* Control client pane. */
struct control_pane { struct control_pane {
u_int pane; u_int pane;
@@ -102,6 +113,13 @@ struct control_state {
struct bufferevent *write_event; struct bufferevent *write_event;
struct monitor_set *subs; struct monitor_set *subs;
/*
* Depth of open %begin/%end guard blocks and notifications deferred
* until the outermost block closes.
*/
int guard_depth;
TAILQ_HEAD(, control_line) deferred;
}; };
/* Low and high watermarks. */ /* Low and high watermarks. */
@@ -352,7 +370,7 @@ control_continue_pane(struct client *c, struct window_pane *wp)
cp->flags &= ~CONTROL_PANE_PAUSED; cp->flags &= ~CONTROL_PANE_PAUSED;
memcpy(&cp->offset, &wp->offset, sizeof cp->offset); memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
memcpy(&cp->queued, &wp->offset, sizeof cp->queued); memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
control_write(c, "%%continue %%%u", wp->id); control_notify_write(c, "%%continue %%%u", wp->id);
} }
} }
@@ -366,52 +384,120 @@ control_pause_pane(struct client *c, struct window_pane *wp)
if (~cp->flags & CONTROL_PANE_PAUSED) { if (~cp->flags & CONTROL_PANE_PAUSED) {
cp->flags |= CONTROL_PANE_PAUSED; cp->flags |= CONTROL_PANE_PAUSED;
control_discard_pane(c, cp); control_discard_pane(c, cp);
control_write(c, "%%pause %%%u", wp->id); control_notify_write(c, "%%pause %%%u", wp->id);
} }
} }
/* Write a line. */ /* Write an already-formatted line, queueing it behind %output if needed. */
static void printflike(2, 0) static void
control_vwrite(struct client *c, const char *fmt, va_list ap) control_write_line(struct client *c, char *line)
{
struct control_state *cs = c->control_state;
char *s;
xvasprintf(&s, fmt, ap);
log_debug("%s: %s: writing line: %s", __func__, c->name, s);
bufferevent_write(cs->write_event, s, strlen(s));
bufferevent_write(cs->write_event, "\n", 1);
bufferevent_enable(cs->write_event, EV_WRITE);
free(s);
}
/* Write a line. */
void
control_write(struct client *c, const char *fmt, ...)
{ {
struct control_state *cs = c->control_state; struct control_state *cs = c->control_state;
struct control_block *cb; struct control_block *cb;
va_list ap;
va_start(ap, fmt);
if (TAILQ_EMPTY(&cs->all_blocks)) { if (TAILQ_EMPTY(&cs->all_blocks)) {
control_vwrite(c, fmt, ap); log_debug("%s: %s: writing line: %s", __func__, c->name, line);
va_end(ap); bufferevent_write(cs->write_event, line, strlen(line));
bufferevent_write(cs->write_event, "\n", 1);
bufferevent_enable(cs->write_event, EV_WRITE);
free(line);
return; return;
} }
cb = xcalloc(1, sizeof *cb); cb = xcalloc(1, sizeof *cb);
xvasprintf(&cb->line, fmt, ap); cb->line = line;
TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry); TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
cb->t = get_timer(); cb->t = get_timer();
log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line); log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line);
bufferevent_enable(cs->write_event, EV_WRITE); bufferevent_enable(cs->write_event, EV_WRITE);
}
/* Flush notifications that were deferred while a guard block was open. */
static void
control_flush_deferred(struct client *c)
{
struct control_state *cs = c->control_state;
struct control_line *cl, *cl1;
TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) {
TAILQ_REMOVE(&cs->deferred, cl, entry);
control_write_line(c, cl->line);
free(cl);
}
}
/*
* Write a line of command output or error text. This is a sink for arbitrary
* user-controlled text (command output, capture-pane, error messages), so it
* must never try to interpret the content: guard tracking is done only in
* control_write_guard.
*/
void
control_write(struct client *c, const char *fmt, ...)
{
va_list ap;
char *line;
va_start(ap, fmt);
xvasprintf(&line, fmt, ap);
va_end(ap); va_end(ap);
control_write_line(c, line);
}
/*
* Write a %begin, %end or %error guard around a command's output. This is the
* only place guard lines are produced, so the block depth is maintained here;
* when the outermost block closes any deferred notifications are flushed after
* it. "guard" is always one of the fixed strings from cmdq_guard, never user
* text.
*/
void
control_write_guard(struct client *c, const char *guard, long t, u_int number,
int flags)
{
struct control_state *cs = c->control_state;
char *line;
if (strcmp(guard, "begin") == 0)
cs->guard_depth++;
xasprintf(&line, "%%%s %ld %u %d", guard, t, number, flags);
control_write_line(c, line);
if (strcmp(guard, "begin") != 0 && cs->guard_depth > 0 &&
--cs->guard_depth == 0)
control_flush_deferred(c);
}
/*
* Write a notification line. Notifications must never appear inside a command's
* %begin/%end guard block, so if one is open the line is deferred until it
* closes.
*/
void
control_notify_write(struct client *c, const char *fmt, ...)
{
struct control_state *cs = c->control_state;
struct control_line *cl;
va_list ap;
char *line;
va_start(ap, fmt);
xvasprintf(&line, fmt, ap);
va_end(ap);
if (cs->guard_depth == 0) {
control_write_line(c, line);
return;
}
log_debug("%s: %s: deferring notification: %s", __func__, c->name,
line);
cl = xcalloc(1, sizeof *cl);
cl->line = line;
TAILQ_INSERT_TAIL(&cs->deferred, cl, entry);
} }
/* Check age for this pane. */ /* Check age for this pane. */
@@ -438,7 +524,7 @@ control_check_age(struct client *c, struct window_pane *wp,
return (0); return (0);
cp->flags |= CONTROL_PANE_PAUSED; cp->flags |= CONTROL_PANE_PAUSED;
control_discard_pane(c, cp); control_discard_pane(c, cp);
control_write(c, "%%pause %%%u", wp->id); control_notify_write(c, "%%pause %%%u", wp->id);
} else { } else {
if (age < CONTROL_MAXIMUM_AGE) if (age < CONTROL_MAXIMUM_AGE)
return (0); return (0);
@@ -811,14 +897,17 @@ control_sub_change(struct monitor_change *change, __unused void *data)
if (wp != NULL) { if (wp != NULL) {
w = wp->window; w = wp->window;
control_write(c, "%%subscription-changed %s $%u @%u %u %%%u : %s", control_notify_write(c,
"%%subscription-changed %s $%u @%u %u %%%u : %s",
change->name, s->id, w->id, wl->idx, wp->id, change->value); change->name, s->id, w->id, wl->idx, wp->id, change->value);
} else if (wl != NULL) { } else if (wl != NULL) {
w = wl->window; w = wl->window;
control_write(c, "%%subscription-changed %s $%u @%u %u - : %s", control_notify_write(c,
"%%subscription-changed %s $%u @%u %u - : %s",
change->name, s->id, w->id, wl->idx, change->value); change->name, s->id, w->id, wl->idx, change->value);
} else { } else {
control_write(c, "%%subscription-changed %s $%u - - - : %s", control_notify_write(c,
"%%subscription-changed %s $%u - - - : %s",
change->name, s->id, change->value); change->name, s->id, change->value);
} }
} }
@@ -841,6 +930,7 @@ control_start(struct client *c)
RB_INIT(&cs->windows); RB_INIT(&cs->windows);
TAILQ_INIT(&cs->pending_list); TAILQ_INIT(&cs->pending_list);
TAILQ_INIT(&cs->all_blocks); TAILQ_INIT(&cs->all_blocks);
TAILQ_INIT(&cs->deferred);
cs->subs = monitor_create_client(c, control_sub_change, NULL); cs->subs = monitor_create_client(c, control_sub_change, NULL);
cs->read_event = bufferevent_new(c->fd, control_read_callback, cs->read_event = bufferevent_new(c->fd, control_read_callback,
@@ -884,6 +974,19 @@ control_discard(struct client *c)
bufferevent_disable(cs->read_event, EV_READ); bufferevent_disable(cs->read_event, EV_READ);
} }
/* Discard all tmux-owned queued control blocks and stop writing. */
void
control_discard_all(struct client *c)
{
struct control_state *cs = c->control_state;
struct control_block *cb, *cb1;
control_discard(c);
TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1)
control_free_block(cs, cb);
bufferevent_disable(cs->write_event, EV_WRITE);
}
/* Stop control mode. */ /* Stop control mode. */
void void
control_stop(struct client *c) control_stop(struct client *c)
@@ -891,12 +994,19 @@ control_stop(struct client *c)
struct control_state *cs = c->control_state; struct control_state *cs = c->control_state;
struct control_block *cb, *cb1; struct control_block *cb, *cb1;
struct control_window *cw, *cw1; struct control_window *cw, *cw1;
struct control_line *cl, *cl1;
if (cs == NULL) if (cs == NULL)
return; return;
monitor_destroy(cs->subs); monitor_destroy(cs->subs);
TAILQ_FOREACH_SAFE(cl, &cs->deferred, entry, cl1) {
TAILQ_REMOVE(&cs->deferred, cl, entry);
free(cl->line);
free(cl);
}
if (~c->flags & CLIENT_CONTROLCONTROL) if (~c->flags & CLIENT_CONTROLCONTROL)
bufferevent_free(cs->write_event); bufferevent_free(cs->write_event);
bufferevent_free(cs->read_event); bufferevent_free(cs->read_event);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.409 2026/07/26 15:21:53 nicm Exp $ */ /* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 nicm Exp $ */
/* /*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -2101,6 +2101,60 @@ format_cb_synchronized_output_flag(struct format_tree *ft)
return (NULL); return (NULL);
} }
/* Callback for pane_private_modes. */
static void *
format_cb_pane_private_modes(struct format_tree *ft)
{
static const struct {
int mode;
int number;
} table[] = {
{ MODE_KCURSOR, 1 }, /* DECCKM */
{ MODE_ORIGIN, 6 }, /* DECOM */
{ MODE_WRAP, 7 }, /* DECAWM */
{ MODE_CURSOR_BLINKING, 12 }, /* cursor blinking */
{ MODE_CURSOR, 25 }, /* DECTCEM */
{ MODE_MOUSE_STANDARD, 1000 }, /* mouse normal tracking */
{ MODE_MOUSE_BUTTON, 1002 }, /* mouse button tracking */
{ MODE_MOUSE_ALL, 1003 }, /* mouse any tracking */
{ MODE_FOCUSON, 1004 }, /* focus reporting */
{ MODE_MOUSE_UTF8, 1005 }, /* mouse: UTF-8 */
{ MODE_MOUSE_SGR, 1006 }, /* mouse: SGR */
{ MODE_BRACKETPASTE, 2004 }, /* bracketed paste */
{ MODE_SYNC, 2026 }, /* synchronized output */
{ MODE_THEME_UPDATES, 2031 }, /* theme update notifications */
};
int mode;
char *value = NULL, *tmp;
u_int i;
if (ft->wp == NULL)
return (NULL);
mode = ft->wp->base.mode;
for (i = 0; i < nitems(table); i++) {
if (~mode & table[i].mode)
continue;
/*
* Only report cursor blinking when set by the application, not
* when it comes from the cursor-style option.
*/
if (table[i].mode == MODE_CURSOR_BLINKING &&
(~mode & MODE_CURSOR_BLINKING_SET))
continue;
if (value == NULL)
xasprintf(&value, "%d", table[i].number);
else {
xasprintf(&tmp, "%s,%d", value, table[i].number);
free(value);
value = tmp;
}
}
if (value == NULL)
return (xstrdup(""));
return (value);
}
/* Callback for pane_active. */ /* Callback for pane_active. */
static void * static void *
format_cb_pane_active(struct format_tree *ft) format_cb_pane_active(struct format_tree *ft)
@@ -2628,20 +2682,23 @@ format_cb_pane_unzoomed_width(struct format_tree *ft)
{ {
struct window_pane *wp = ft->wp; struct window_pane *wp = ft->wp;
struct layout_cell *lc; struct layout_cell *lc;
int sb_w, sb_pad; int saved, sb_w, sb_pad;
u_int sx; u_int sx;
if (wp == NULL) if (wp == NULL)
return (NULL); return (NULL);
lc = wp->saved_layout_cell; lc = wp->saved_layout_cell;
saved = (lc != NULL);
if (lc == NULL) if (lc == NULL)
lc = wp->layout_cell; lc = wp->layout_cell;
if (lc == NULL) if (lc == NULL)
return (NULL); return (NULL);
sx = lc->g.sx; sx = lc->g.sx;
if (window_pane_scrollbar_reserve(wp)) { if ((saved && !SCREEN_IS_ALTERNATE(&wp->base) &&
wp->window->sb == PANE_SCROLLBARS_ALWAYS) ||
(!saved && window_pane_scrollbar_reserve(wp))) {
sb_w = wp->scrollbar_style.width; sb_w = wp->scrollbar_style.width;
sb_pad = wp->scrollbar_style.pad; sb_pad = wp->scrollbar_style.pad;
if (sb_w < 1) if (sb_w < 1)
@@ -3810,6 +3867,9 @@ static const struct format_table_entry format_table[] = {
{ "pane_pipe_pid", FORMAT_TABLE_STRING, { "pane_pipe_pid", FORMAT_TABLE_STRING,
format_cb_pane_pipe_pid format_cb_pane_pipe_pid
}, },
{ "pane_private_modes", FORMAT_TABLE_STRING,
format_cb_pane_private_modes
},
{ "pane_right", FORMAT_TABLE_STRING, { "pane_right", FORMAT_TABLE_STRING,
format_cb_pane_right format_cb_pane_right
}, },
@@ -4197,6 +4257,8 @@ format_log_debug_cb(const char *key, const char *value, void *arg)
void void
format_log_debug(struct format_tree *ft, const char *prefix) format_log_debug(struct format_tree *ft, const char *prefix)
{ {
if (log_get_level() == 0)
return;
format_each(ft, format_log_debug_cb, (void *)prefix); format_each(ft, format_log_debug_cb, (void *)prefix);
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: grid-reader.c,v 1.10 2026/05/17 13:12:21 nicm Exp $ */ /* $OpenBSD: grid-reader.c,v 1.11 2026/07/29 17:42:56 nicm Exp $ */
/* /*
* Copyright (c) 2020 Anindya Mukherjee <anindya49@hotmail.com> * Copyright (c) 2020 Anindya Mukherjee <anindya49@hotmail.com>
@@ -54,11 +54,8 @@ grid_reader_cursor_right(struct grid_reader *gr, int wrap, int all, int onemore)
px = gr->gd->sx; px = gr->gd->sx;
else if (onemore) else if (onemore)
px = grid_reader_line_length(gr); px = grid_reader_line_length(gr);
else { else
px = grid_reader_line_length(gr); px = grid_line_limit(gr->gd, gr->cy);
if (px != 0)
px--;
}
if (wrap && gr->cx >= px && gr->cy < gr->gd->hsize + gr->gd->sy - 1) { if (wrap && gr->cx >= px && gr->cy < gr->gd->hsize + gr->gd->sy - 1) {
grid_reader_cursor_start_of_line(gr, 0); grid_reader_cursor_start_of_line(gr, 0);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: grid-view.c,v 1.38 2026/01/22 08:55:01 nicm Exp $ */ /* $OpenBSD: grid-view.c,v 1.39 2026/08/03 12:58:53 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -47,9 +47,9 @@ grid_view_set_cell(struct grid *gd, u_int px, u_int py,
/* Set padding. */ /* Set padding. */
void void
grid_view_set_padding(struct grid *gd, u_int px, u_int py) grid_view_set_padding(struct grid *gd, u_int px, u_int py, int bg)
{ {
grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py)); grid_set_padding(gd, grid_view_x(gd, px), grid_view_y(gd, py), bg);
} }
/* Set cells. */ /* Set cells. */

30
grid.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: grid.c,v 1.154 2026/07/20 11:16:33 nicm Exp $ */ /* $OpenBSD: grid.c,v 1.156 2026/08/03 12:58:53 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -682,9 +682,13 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc)
/* Set padding at position. */ /* Set padding at position. */
void void
grid_set_padding(struct grid *gd, u_int px, u_int py) grid_set_padding(struct grid *gd, u_int px, u_int py, int bg)
{ {
grid_set_cell(gd, px, py, &grid_padding_cell); struct grid_cell gc;
memcpy(&gc, &grid_padding_cell, sizeof gc);
gc.bg = bg;
grid_set_cell(gd, px, py, &gc);
} }
/* Set cells at position. */ /* Set cells at position. */
@@ -1681,6 +1685,26 @@ grid_line_length(struct grid *gd, u_int py)
return (px); return (px);
} }
/* Get last position on line, not including padding. */
u_int
grid_line_limit(struct grid *gd, u_int py)
{
struct grid_cell gc;
u_int px;
px = grid_line_length(gd, py);
if (px == 0)
return (0);
px--;
while (px > 0) {
grid_get_cell(gd, px, py, &gc);
if (~gc.flags & GRID_FLAG_PADDING)
break;
px--;
}
return (px);
}
/* Check if character is in set. */ /* Check if character is in set. */
int int
grid_in_set(struct grid *gd, u_int px, u_int py, const char *set) grid_in_set(struct grid *gd, u_int px, u_int py, const char *set)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: key-bindings.c,v 1.190 2026/07/22 19:23:59 nicm Exp $ */ /* $OpenBSD: key-bindings.c,v 1.192 2026/08/06 09:05:04 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -419,6 +419,7 @@ key_bindings_init(void)
"bind -N 'Spread panes out evenly' E { select-layout -E }", "bind -N 'Spread panes out evenly' E { select-layout -E }",
"bind -N 'Switch to the last client' L { switch-client -l }", "bind -N 'Switch to the last client' L { switch-client -l }",
"bind -N 'Clear the marked pane' M { select-pane -M }", "bind -N 'Clear the marked pane' M { select-pane -M }",
"bind -N 'Change the pane title' T { command-prompt -I'#T' { select-pane -T '%%' } }",
"bind -N 'Enter copy mode' [ { copy-mode }", "bind -N 'Enter copy mode' [ { copy-mode }",
"bind -N 'Paste the most recent paste buffer' ] { paste-buffer -p }", "bind -N 'Paste the most recent paste buffer' ] { paste-buffer -p }",
"bind -N 'Create a new window' c { new-window }", "bind -N 'Create a new window' c { new-window }",
@@ -593,7 +594,7 @@ key_bindings_init(void)
"bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }", "bind -Tcopy-mode g { command-prompt -P -p'(goto line)' { send -X goto-line -- '%%' } }",
"bind -Tcopy-mode n { send -X search-again }", "bind -Tcopy-mode n { send -X search-again }",
"bind -Tcopy-mode q { send -X cancel }", "bind -Tcopy-mode q { send -X cancel }",
"bind -Tcopy-mode r { send -X refresh-toggle }", "bind -Tcopy-mode r { send -X refresh-now }",
"bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }",
"bind -Tcopy-mode Home { send -X start-of-line }", "bind -Tcopy-mode Home { send -X start-of-line }",
"bind -Tcopy-mode End { send -X end-of-line }", "bind -Tcopy-mode End { send -X end-of-line }",
@@ -703,7 +704,7 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi n { send -X search-again }", "bind -Tcopy-mode-vi n { send -X search-again }",
"bind -Tcopy-mode-vi o { send -X other-end }", "bind -Tcopy-mode-vi o { send -X other-end }",
"bind -Tcopy-mode-vi q { send -X cancel }", "bind -Tcopy-mode-vi q { send -X cancel }",
"bind -Tcopy-mode-vi r { send -X refresh-toggle }", "bind -Tcopy-mode-vi r { send -X refresh-now }",
"bind -Tcopy-mode-vi t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }", "bind -Tcopy-mode-vi t { command-prompt -P -1p'(jump to forward)' { send -X jump-to-forward -- '%%' } }",
"bind -Tcopy-mode-vi v { send -X rectangle-toggle }", "bind -Tcopy-mode-vi v { send -X rectangle-toggle }",
"bind -Tcopy-mode-vi w { send -X next-word }", "bind -Tcopy-mode-vi w { send -X next-word }",

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: mode-tree.c,v 1.100 2026/07/14 19:07:03 nicm Exp $ */ /* $OpenBSD: mode-tree.c,v 1.101 2026/08/05 07:50:21 nicm Exp $ */
/* /*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -47,7 +47,7 @@ enum mode_tree_preview {
"#[acs]x" MODE_TREE_PREFIX_STYLE " }," \ "#[acs]x" MODE_TREE_PREFIX_STYLE " }," \
"#{mode_tree_repeat}}" \ "#{mode_tree_repeat}}" \
"#{?mode_tree_branch," \ "#{?mode_tree_branch," \
"#[acs]#{?mode_tree_last,mq,tq}" MODE_TREE_PREFIX_STYLE "> ,}" \ "#[acs]#{?mode_tree_last,mq,tq}+" MODE_TREE_PREFIX_STYLE " ,}" \
"#{?mode_tree_has_children," \ "#{?mode_tree_has_children," \
"#{?mode_tree_expanded,#[fg=themered]-" MODE_TREE_PREFIX_STYLE " ," \ "#{?mode_tree_expanded,#[fg=themered]-" MODE_TREE_PREFIX_STYLE " ," \
"#[fg=themegreen]+" MODE_TREE_PREFIX_STYLE " }," \ "#[fg=themegreen]+" MODE_TREE_PREFIX_STYLE " }," \

6
proc.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: proc.c,v 1.31 2026/06/08 21:38:19 nicm Exp $ */ /* $OpenBSD: proc.c,v 1.32 2026/08/04 13:16:03 claudio Exp $ */
/* /*
* Copyright (c) 2015 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2015 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -79,7 +79,7 @@ static void
proc_event_cb(__unused int fd, short events, void *arg) proc_event_cb(__unused int fd, short events, void *arg)
{ {
struct tmuxpeer *peer = arg; struct tmuxpeer *peer = arg;
ssize_t n; int n;
struct imsg imsg; struct imsg imsg;
if (!(peer->flags & PEER_BAD) && (events & EV_READ)) { if (!(peer->flags & PEER_BAD) && (events & EV_READ)) {
@@ -88,7 +88,7 @@ proc_event_cb(__unused int fd, short events, void *arg)
return; return;
} }
for (;;) { for (;;) {
if ((n = imsg_get(&peer->ibuf, &imsg)) == -1) { if ((n = imsgbuf_get(&peer->ibuf, &imsg)) == -1) {
peer->dispatchcb(NULL, peer->arg); peer->dispatchcb(NULL, peer->arg);
return; return;
} }

View File

@@ -0,0 +1,77 @@
#!/bin/sh
# A control client whose terminal has stopped accepting output (for example
# its ssh connection or terminal emulator died) never drains the output that
# server_client_check_exit waits on before sending MSG_EXIT, and may then
# never close its socket either. This must not prevent the server from
# exiting: after kill-server the server previously stayed alive forever with
# server_exit set, closing every new connection immediately, so every new
# client failed with "server exited unexpectedly" until the server was killed
# by hand. The server now bounds the exit handshake: it discards output it
# can never deliver and drops the client if it still does not close.
#
# The stalled terminal is simulated by a fifo whose read end is held open but
# never read.
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
# A detached session whose pane floods printable output forever.
$TMUX -f/dev/null new -d -x 80 -y 24 -s rt 'cat /dev/zero | tr "\000" x' || exit 1
SERVER=$($TMUX display -pt rt '#{pid}')
# Attach a control client with output down the fifo; stdin held open by sleep
# so it stays attached. The fifo fills and is never read, so a backlog forms
# in the server that can never be delivered.
( sleep 60 ) | $TMUX -f/dev/null -C attach -t rt >"$FIFO" 2>&1 &
exec 8<"$FIFO"
n=0
while [ $n -lt 50 ]; do
$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . && break
sleep 0.1
n=$((n + 1))
done
$TMUX lsc -F '#{client_name}' 2>/dev/null | grep -q . ||
{ echo "control client did not attach"; exit 1; }
# Let the pane flood fill the fifo so the client's output is stuck.
sleep 2
# Ask the server to exit. This sets CLIENT_EXIT on the stalled client, whose
# output can never drain.
$TMUX kill-server 2>/dev/null
# The server must exit within the exit timeout (10 seconds) plus slack,
# rather than staying in limbo forever rejecting new clients.
n=0
while [ $n -lt 100 ]; do
if ! kill -0 "$SERVER" 2>/dev/null; then
SERVER=
exit 0
fi
sleep 0.2
n=$((n + 1))
done
echo "server did not exit after kill-server; new clients see:"
$TMUX ls 2>&1
exit 1

158
regress/control-notify-guard.sh Executable file
View File

@@ -0,0 +1,158 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMPDIR=$(mktemp -d)
IN="$TMPDIR/in"
OUT="$TMPDIR/out"
BADCFG="$TMPDIR/bad.conf"
PID=
cleanup()
{
exec 3>&- 2>/dev/null
[ -n "$PID" ] && kill "$PID" 2>/dev/null
$TMUX kill-server 2>/dev/null
rm -rf "$TMPDIR"
}
trap cleanup EXIT
fail()
{
echo "$1" >&2
[ -s "$OUT" ] && cat "$OUT" >&2
exit 1
}
wait_for()
{
pattern=$1
timeout=${2:-60}
i=0
while [ "$i" -lt "$timeout" ]; do
grep -F -- "$pattern" "$OUT" >/dev/null 2>&1 && return 0
sleep 0.1
i=$((i + 1))
done
fail "missing: $pattern"
}
wait_for_count()
{
pattern=$1
expected=$2
timeout=${3:-60}
i=0
while [ "$i" -lt "$timeout" ]; do
count=$(grep -F -c -- "$pattern" "$OUT" 2>/dev/null)
[ "$count" -ge "$expected" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "missing count $expected for: $pattern"
}
send()
{
printf '%s\n' "$1" >&3 || fail "failed to send: $1"
}
# A notification is a standalone protocol message. It must never be part of
# the response between the real %begin and matching %end or %error for a
# command. Ignore guard-looking command output while a real guard is open.
check_guards()
{
awk '
function notification(line) {
return line ~ /^%(sessions-changed|session-changed |unlinked-window-add )/ ||
line ~ /^%(pause |continue |config-error )/
}
!open && $0 ~ /^%begin [0-9]+ [0-9]+ [0-9]+$/ {
open = 1
time = $2
number = $3
flags = $4
next
}
open && ($0 == "%end " time " " number " " flags ||
$0 == "%error " time " " number " " flags) {
open = 0
next
}
notification($0) {
seen[$1]++
if (open) {
print "notification inside guard: " $0 > "/dev/stderr"
bad = 1
}
}
END {
if (open) {
print "unterminated command guard" > "/dev/stderr"
bad = 1
}
if (seen["%sessions-changed"] < 2 ||
seen["%session-changed"] < 1 ||
seen["%unlinked-window-add"] < 2 ||
seen["%pause"] < 1 || seen["%continue"] < 1 ||
seen["%config-error"] < 1) {
print "missing expected notification" > "/dev/stderr"
bad = 1
}
exit bad
}' "$OUT" || fail "bad control protocol notification ordering"
}
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -s guard -x 80 -y 24 || exit 1
pane=$($TMUX display-message -p -t guard:0.0 '#{pane_id}') || exit 1
mkfifo "$IN" || exit 1
: >"$OUT"
$TMUX -C attach-session -t guard <"$IN" >"$OUT" 2>&1 &
PID=$!
exec 3>"$IN"
# Attaching generates a session notification synchronously from inside the
# attach command. It must follow the closing guard.
wait_for '%session-changed '
# Guard-looking output is arbitrary command output, not protocol state. Make
# new-session both generate notifications and print an unmatched fake %begin;
# it must not leave those notifications permanently deferred.
send "set-option -g @fake-begin '%begin 1 2 3'"
send "new-session -d -P -F '#{@fake-begin}' -s fake-begin"
wait_for_count '%sessions-changed' 1
# Likewise, fake %end output from a command that generates multiple
# notifications must not flush them before the command actually ends.
send "set-option -g @fake-end '%end 1 2 3'"
send "new-session -d -P -F '#{@fake-end}' -s fake-end"
wait_for_count '%sessions-changed' 2
# Exercise notification paths outside control-notify.c.
send "refresh-client -A '$pane:pause'"
wait_for "%pause $pane"
send "refresh-client -A '$pane:continue'"
wait_for "%continue $pane"
printf '%s\n' 'not-a-command' >"$BADCFG"
send "source-file '$BADCFG'"
wait_for '%config-error '
# A final command guarantees all earlier command guards have completed before
# the transcript is checked.
send "display-message -p 'guard-check-done'"
wait_for 'guard-check-done'
check_guards
exit 0

View File

@@ -114,5 +114,30 @@ $TMUX send-keys -X next-space-end
$TMUX send-keys -X copy-selection $TMUX send-keys -X copy-selection
[ "$($TMUX show-buffer)" = "500xyz" ] || exit 1 [ "$($TMUX show-buffer)" = "500xyz" ] || exit 1
# Test that vi cursor movement does not stop on the padding cell of a wide
# character at the end of a line.
$TMUX kill-server 2>/dev/null
sleep 1
$TMUX new -d -x20 -y5 \
"printf 'abc中\nxyz\n'; exec cat" || exit 1
$TMUX set-window-option -g mode-keys vi
$TMUX copy-mode
$TMUX send-keys -X history-top
$TMUX send-keys -X start-of-line
$TMUX send-keys -X cursor-right
$TMUX send-keys -X cursor-right
$TMUX send-keys -X cursor-right
[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] ||
exit 1
$TMUX send-keys -X cursor-right
[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "0,1" ] ||
exit 1
$TMUX send-keys -X cursor-left
[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "3,0" ] ||
exit 1
$TMUX send-keys -X cursor-left
[ "$($TMUX display -p '#{copy_cursor_x},#{copy_cursor_y}')" = "2,0" ] ||
exit 1
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null
exit 0 exit 0

View File

@@ -192,13 +192,13 @@ check_fail "invalid window name: $(printf 'a\377b')" \
break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P: break-pane -d -n "$(printf 'a\377b')" -s "$p1" -t P:
# join-pane can move a pane from one window to another without destroying # join-pane can move a pane from one window to another without destroying
# the source window if other panes remain. (On this branch move-pane is # the source window if other panes remain. move-pane does the same when no
# reserved for floating panes, covered by floating-pane-geometry.sh.) # floating-pane movement flags are given.
check_ok new-window -d -t P:5 -n other check_ok new-window -d -t P:5 -n other
check_ok join-pane -d -s "$p1" -t P:5.0 check_ok join-pane -d -s "$p1" -t P:5.0
check_fmt 'P:5' '#{window_panes}' '2' check_fmt 'P:5' '#{window_panes}' '2'
check_fmt 'P:0' '#{window_panes}' '3' check_fmt 'P:0' '#{window_panes}' '3'
check_ok join-pane -d -v -s "$p1" -t "$p2" check_ok move-pane -d -v -s "$p1" -t "$p2"
check_fmt 'P:0' '#{window_panes}' '4' check_fmt 'P:0' '#{window_panes}' '4'
check_fmt 'P:5' '#{window_panes}' '1' check_fmt 'P:5' '#{window_panes}' '1'

View File

@@ -35,9 +35,9 @@ check_alive()
# A pipe-pane -I child may write after the pane process has exited. With # A pipe-pane -I child may write after the pane process has exited. With
# remain-on-exit, the pane stays around but its bufferevent has been freed. # remain-on-exit, the pane stays around but its bufferevent has been freed.
check_ok new-session -d -s pipe -x 80 -y 24 'sleep 0.2' check_ok new-session -d -s pipe -x 80 -y 24 'sleep 1'
check_ok set-option -t pipe:0 remain-on-exit on check_ok set-option -t pipe:0 remain-on-exit on
check_ok pipe-pane -t pipe:0.0 -I 'sleep 0.6; printf x' check_ok pipe-pane -t pipe:0.0 -I 'sleep 2; printf x'
i=0 i=0
while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do
@@ -46,7 +46,7 @@ while [ "$($TMUX display-message -p -t pipe:0.0 '#{pane_dead}')" != "1" ]; do
sleep 0.1 sleep 0.1
done done
sleep 0.7 sleep 2
check_alive check_alive
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null

49
regress/server-socket-error.sh Executable file
View File

@@ -0,0 +1,49 @@
#!/bin/sh
# Server socket creation failures must produce a nonzero client exit status.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
# Root bypasses directory mode bits, so it cannot trigger this failure.
[ "$(id -u)" -eq 0 ] && exit 0
TMP=$(mktemp -d) || exit 1
SOCKET="$TMP/ro/socket"
cleanup()
{
chmod 700 "$TMP/ro" 2>/dev/null
rm -rf "$TMP"
}
fail()
{
echo "$1" >&2
cleanup
exit 1
}
mkdir "$TMP/ro" || exit 1
chmod 500 "$TMP/ro" || exit 1
trap cleanup 0 1 15
check_failure()
{
output=$($TEST_TMUX -S "$SOCKET" -f/dev/null "$@" 2>&1)
retval=$?
[ "$retval" -ne 0 ] || fail "tmux $* exited zero: $output"
case "$output" in
"error creating $SOCKET ("*) ;;
*) fail "tmux $* produced unexpected error: $output" ;;
esac
}
check_failure start-server
check_failure new-session -d
exit 0

43
regress/tab-cell-background.sh Executable file
View File

@@ -0,0 +1,43 @@
#!/bin/sh
# When a write breaks up a wide character or a tab, the columns it covered must
# keep the background colour, both in the grid and on the terminal. The second
# server is attached to the first so that what is actually sent is checked, not
# just what tmux has stored.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP" 0 1 15
# 1. Write inside a tab. 2. Overwrite the right half of a wide character.
# 3. Two tabs of different colours, the first partly deleted so that padding
# from both ends up next to each other, then written over.
# The sleep is so that the client is attached before anything is written and
# the changes are sent as they happen rather than as one redraw.
$TMUX2 -f/dev/null new -d -x24 -y3 "
sleep 2
printf '\033[44m\033[K\t\033[4GX'
printf '\033[2;1H\033[44m\033[K\344\270\226\033[2GX'
printf '\033[3;1H\033[42m\033[K\t\033[43m\033[K\t\033[0m\033[4G\033[8P\rx'
cat" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX -f/dev/null new -x24 -y4 -d "$TMUX2 attach" || exit 1
sleep 4
$TMUX capturep -pe -S0 -E2 >$TMP || exit 1
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
printf '\033[44m X\n X\n\033[49mx\033[42m \033[43m \033[49m\n' |
cmp - $TMP || exit 1
exit 0

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.285 2026/07/26 09:02:08 nicm Exp $ */ /* $OpenBSD: screen-write.c,v 1.286 2026/08/03 12:58:53 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -2497,15 +2497,62 @@ screen_write_collect_insert_clear(struct screen_write_ctx *ctx, u_int px,
} }
} }
/*
* Clear a cell that is being broken up by a write over part of it, keeping the
* background so the columns it covered do not lose their colour.
*/
static void
screen_write_clear_cell(struct grid *gd, u_int px, u_int py)
{
struct grid_cell gc;
int bg;
grid_view_get_cell(gd, px, py, &gc);
bg = gc.bg;
memcpy(&gc, &grid_default_cell, sizeof gc);
gc.bg = bg;
grid_view_set_cell(gd, px, py, &gc);
}
/*
* Insert clears for a range of cells already cleared in the grid. Adjacent
* cells may have come from different characters and so have different
* backgrounds, so this is done in runs of the same colour.
*/
static void
screen_write_insert_clears(struct screen_write_ctx *ctx, u_int px, u_int nx)
{
struct screen *s = ctx->s;
struct grid_cell gc;
u_int xx, start = px, n;
int bg = 8;
for (xx = px; xx < px + nx; xx++) {
grid_view_get_cell(s->grid, xx, s->cy, &gc);
if (xx == start)
bg = gc.bg;
else if (gc.bg != bg) {
n = xx - start;
log_debug("%s: from %u, size %u", __func__, start, n);
screen_write_collect_insert_clear(ctx, start, n, bg);
start = xx;
bg = gc.bg;
}
}
log_debug("%s: from %u, size %u", __func__, start, xx - start);
screen_write_collect_insert_clear(ctx, start, xx - start, bg);
}
/* Finish and store collected cells. */ /* Finish and store collected cells. */
void void
screen_write_collect_end(struct screen_write_ctx *ctx) screen_write_collect_end(struct screen_write_ctx *ctx)
{ {
struct screen *s = ctx->s; struct screen *s = ctx->s;
struct screen_write_citem *ci = ctx->item, *bci = NULL, *aci; struct screen_write_citem *ci = ctx->item;
struct screen_write_cline *cl = &s->write_list[s->cy]; struct screen_write_cline *cl = &s->write_list[s->cy];
struct grid_cell gc; struct grid_cell gc;
u_int xx; u_int xx, bx = 0, bnx = 0;
if (ci->used == 0) if (ci->used == 0)
return; return;
@@ -2521,8 +2568,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
grid_view_get_cell(s->grid, xx, s->cy, &gc); grid_view_get_cell(s->grid, xx, s->cy, &gc);
if (~gc.flags & GRID_FLAG_PADDING) if (~gc.flags & GRID_FLAG_PADDING)
break; break;
grid_view_set_cell(s->grid, xx, s->cy, screen_write_clear_cell(s->grid, xx, s->cy);
&grid_default_cell);
log_debug("%s: padding erased (before) at %u (cx %u)", log_debug("%s: padding erased (before) at %u (cx %u)",
__func__, xx, s->cx); __func__, xx, s->cx);
} }
@@ -2531,20 +2577,12 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
grid_view_get_cell(s->grid, 0, s->cy, &gc); grid_view_get_cell(s->grid, 0, s->cy, &gc);
if (gc.data.width > 1 || if (gc.data.width > 1 ||
(gc.flags & GRID_FLAG_PADDING)) { (gc.flags & GRID_FLAG_PADDING)) {
grid_view_set_cell(s->grid, xx, s->cy, screen_write_clear_cell(s->grid, xx, s->cy);
&grid_default_cell);
log_debug("%s: padding erased (before) at %u " log_debug("%s: padding erased (before) at %u "
"(cx %u)", __func__, xx, s->cx); "(cx %u)", __func__, xx, s->cx);
} }
} bx = xx;
if (xx != s->cx) { bnx = s->cx - xx;
bci = ctx->item;
bci->type = CLEAR;
bci->x = xx;
bci->bg = 8;
bci->used = s->cx - xx;
log_debug("%s: padding erased (before): from %u, "
"size %u", __func__, bci->x, bci->used);
} }
} }
@@ -2555,28 +2593,20 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x,
ci->used); ci->used);
if (bci != NULL) if (bnx != 0)
screen_write_collect_insert(ctx, bci); screen_write_insert_clears(ctx, bx, bnx);
screen_write_set_cursor(ctx, s->cx + ci->used, -1); screen_write_set_cursor(ctx, s->cx + ci->used, -1);
for (xx = s->cx; xx < screen_size_x(s); xx++) { for (xx = s->cx; xx < screen_size_x(s); xx++) {
grid_view_get_cell(s->grid, xx, s->cy, &gc); grid_view_get_cell(s->grid, xx, s->cy, &gc);
if (~gc.flags & GRID_FLAG_PADDING) if (~gc.flags & GRID_FLAG_PADDING)
break; break;
grid_view_set_cell(s->grid, xx, s->cy, &grid_default_cell); screen_write_clear_cell(s->grid, xx, s->cy);
log_debug("%s: padding erased (after) at %u (cx %u)", log_debug("%s: padding erased (after) at %u (cx %u)", __func__,
__func__, xx, s->cx); xx, s->cx);
}
if (xx != s->cx) {
aci = ctx->item;
aci->type = CLEAR;
aci->x = s->cx;
aci->bg = 8;
aci->used = xx - s->cx;
log_debug("%s: padding erased (after): from %u, size %u",
__func__, aci->x, aci->used);
screen_write_collect_insert(ctx, aci);
} }
if (xx != s->cx)
screen_write_insert_clears(ctx, s->cx, xx - s->cx);
} }
/* Write cell data, collecting if necessary. */ /* Write cell data, collecting if necessary. */
@@ -2704,7 +2734,7 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
*/ */
for (xx = s->cx + 1; xx < s->cx + width; xx++) { for (xx = s->cx + 1; xx < s->cx + width; xx++) {
log_debug("%s: new padding at %u,%u", __func__, xx, s->cy); log_debug("%s: new padding at %u,%u", __func__, xx, s->cy);
grid_view_set_padding(gd, xx, s->cy); grid_view_set_padding(gd, xx, s->cy, gc->bg);
skip = 0; skip = 0;
} }
@@ -2916,7 +2946,7 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
/* Set the new cell. */ /* Set the new cell. */
grid_view_set_cell(gd, cx - n, cy, &last); grid_view_set_cell(gd, cx - n, cy, &last);
if (force_wide) if (force_wide)
grid_view_set_padding(gd, cx - 1, cy); grid_view_set_padding(gd, cx - 1, cy, last.bg);
/* /*
* Check if all of this character is visible. No character will be * Check if all of this character is visible. No character will be
@@ -2985,12 +3015,12 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc,
if (~tmp_gc.flags & GRID_FLAG_PADDING) if (~tmp_gc.flags & GRID_FLAG_PADDING)
break; break;
log_debug("%s: padding at %u,%u", __func__, xx, s->cy); log_debug("%s: padding at %u,%u", __func__, xx, s->cy);
grid_view_set_cell(gd, xx, s->cy, &grid_default_cell); screen_write_clear_cell(gd, xx, s->cy);
} }
/* Overwrite the character at the start of this padding. */ /* Overwrite the character at the start of this padding. */
log_debug("%s: character at %u,%u", __func__, xx, s->cy); log_debug("%s: character at %u,%u", __func__, xx, s->cy);
grid_view_set_cell(gd, xx, s->cy, &grid_default_cell); screen_write_clear_cell(gd, xx, s->cy);
done = 1; done = 1;
} }
@@ -3008,17 +3038,7 @@ screen_write_overwrite(struct screen_write_ctx *ctx, struct grid_cell *gc,
break; break;
log_debug("%s: overwrite at %u,%u", __func__, xx, log_debug("%s: overwrite at %u,%u", __func__, xx,
s->cy); s->cy);
if (gc->flags & GRID_FLAG_TAB) { screen_write_clear_cell(gd, xx, s->cy);
memcpy(&tmp_gc, gc, sizeof tmp_gc);
memset(tmp_gc.data.data, 0,
sizeof tmp_gc.data.data);
*tmp_gc.data.data = ' ';
tmp_gc.data.width = tmp_gc.data.size =
tmp_gc.data.have = 1;
grid_view_set_cell(gd, xx, s->cy, &tmp_gc);
} else
grid_view_set_cell(gd, xx, s->cy,
&grid_default_cell);
done = 1; done = 1;
} }
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.500 2026/07/28 13:17:45 nicm Exp $ */ /* $OpenBSD: server-client.c,v 1.502 2026/08/04 11:18:22 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -36,7 +36,8 @@ static void server_client_check_window_resize(struct window *);
static key_code server_client_check_mouse(struct client *, struct key_event *); static key_code server_client_check_mouse(struct client *, struct key_event *);
static void server_client_repeat_timer(int, short, void *); static void server_client_repeat_timer(int, short, void *);
static void server_client_click_timer(int, short, void *); static void server_client_click_timer(int, short, void *);
static void server_client_check_exit(struct client *); static void server_client_check_exit(struct client *, int);
static void server_client_exit_timer(int, short, void *);
static void 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_check_modes(struct client *);
static void server_client_set_title(struct client *); static void server_client_set_title(struct client *);
@@ -309,6 +310,7 @@ server_client_create(int fd)
evtimer_set(&c->repeat_timer, server_client_repeat_timer, c); evtimer_set(&c->repeat_timer, server_client_repeat_timer, c);
evtimer_set(&c->click_timer, server_client_click_timer, c); evtimer_set(&c->click_timer, server_client_click_timer, c);
evtimer_set(&c->exit_timer, server_client_exit_timer, c);
c->click_wp = -1; c->click_wp = -1;
@@ -525,6 +527,7 @@ server_client_lost(struct client *c)
evtimer_del(&c->repeat_timer); evtimer_del(&c->repeat_timer);
evtimer_del(&c->click_timer); evtimer_del(&c->click_timer);
evtimer_del(&c->exit_timer);
if (event_initialized(&c->cycle_timer)) if (event_initialized(&c->cycle_timer))
evtimer_del(&c->cycle_timer); evtimer_del(&c->cycle_timer);
@@ -1849,7 +1852,7 @@ server_client_loop(void)
/* Check clients. */ /* Check clients. */
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
server_client_check_exit(c); server_client_check_exit(c, 0);
if (c->session != NULL && c->session->curw != NULL) { if (c->session != NULL && c->session->curw != NULL) {
server_client_check_modes(c); server_client_check_modes(c);
server_client_check_redraw(c); server_client_check_redraw(c);
@@ -2285,9 +2288,37 @@ server_client_click_timer(__unused int fd, __unused short events, void *data)
c->flags &= ~(CLIENT_DOUBLECLICK|CLIENT_TRIPLECLICK); c->flags &= ~(CLIENT_DOUBLECLICK|CLIENT_TRIPLECLICK);
} }
/* Check if client should be exited. */ /* Start client exit timer. */
static void static void
server_client_check_exit(struct client *c) server_client_start_exit_timer(struct client *c)
{
struct timeval tv = { .tv_sec = 10 };
if (!evtimer_pending(&c->exit_timer, NULL))
evtimer_add(&c->exit_timer, &tv);
}
/* Exit timer has expired: stop waiting for the client. */
static void
server_client_exit_timer(__unused int fd, __unused short events, void *data)
{
struct client *c = data;
if (c->flags & (CLIENT_DEAD|CLIENT_SUSPENDED))
return;
if (c->flags & CLIENT_EXITED) {
log_debug("%s: %s took too long to exit", __func__, c->name);
server_client_lost(c);
} else if (c->flags & CLIENT_EXIT) {
log_debug("%s: %s took too long to flush", __func__, c->name);
server_client_check_exit(c, 1);
}
}
/* Check if client should be exited, abandoning buffered output if forced. */
static void
server_client_check_exit(struct client *c, int force)
{ {
struct client_file *cf; struct client_file *cf;
const char *name = c->exit_session; const char *name = c->exit_session;
@@ -2300,16 +2331,29 @@ server_client_check_exit(struct client *c)
return; return;
if (c->flags & CLIENT_CONTROL) { if (c->flags & CLIENT_CONTROL) {
if (force)
control_discard_all(c);
else {
control_discard(c); control_discard(c);
if (!control_all_done(c)) if (!control_all_done(c)) {
server_client_start_exit_timer(c);
return; return;
} }
}
}
if (!force) {
RB_FOREACH(cf, client_files, &c->files) { RB_FOREACH(cf, client_files, &c->files) {
if (EVBUFFER_LENGTH(cf->buffer) != 0) if (EVBUFFER_LENGTH(cf->buffer) != 0) {
server_client_start_exit_timer(c);
return; return;
} }
}
}
c->flags |= CLIENT_EXITED; c->flags |= CLIENT_EXITED;
evtimer_del(&c->exit_timer);
server_client_start_exit_timer(c);
switch (c->exit_type) { switch (c->exit_type) {
case CLIENT_EXIT_RETURN: case CLIENT_EXIT_RETURN:
if (c->exit_message != NULL) if (c->exit_message != NULL)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server.c,v 1.214 2026/07/10 13:38:45 nicm Exp $ */ /* $OpenBSD: server.c,v 1.215 2026/08/03 10:07:57 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -240,6 +240,7 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base,
if (cause != NULL) { if (cause != NULL) {
if (c != NULL) { if (c != NULL) {
c->exit_message = cause; c->exit_message = cause;
c->retval = 1;
c->flags |= CLIENT_EXIT; c->flags |= CLIENT_EXIT;
} else { } else {
fprintf(stderr, "%s\n", cause); fprintf(stderr, "%s\n", cause);
@@ -398,6 +399,7 @@ server_accept(int fd, short events, __unused void *data)
c = server_client_create(newfd); c = server_client_create(newfd);
if (!server_acl_join(c)) { if (!server_acl_join(c)) {
c->exit_message = xstrdup("access not allowed"); c->exit_message = xstrdup("access not allowed");
c->retval = 1;
c->flags |= CLIENT_EXIT; c->flags |= CLIENT_EXIT;
} }
} }

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: session.c,v 1.106 2026/07/13 13:01:14 nicm Exp $ */ /* $OpenBSD: session.c,v 1.107 2026/08/05 07:35:35 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -709,8 +709,10 @@ session_group_synchronize1(struct session *target, struct session *s)
/* Fix up the current window. */ /* Fix up the current window. */
if (s->curw != NULL) if (s->curw != NULL)
s->curw = winlink_find_by_index(&s->windows, s->curw->idx); s->curw = winlink_find_by_index(&s->windows, s->curw->idx);
else else if (target->curw != NULL)
s->curw = winlink_find_by_index(&s->windows, target->curw->idx); s->curw = winlink_find_by_index(&s->windows, target->curw->idx);
if (s->curw == NULL)
s->curw = RB_MIN(winlinks, &s->windows);
/* Fix up the last window stack. */ /* Fix up the last window stack. */
memcpy(&old_lastw, &s->lastw, sizeof old_lastw); memcpy(&old_lastw, &s->lastw, sizeof old_lastw);

13
tmux.1
View File

@@ -1,4 +1,4 @@
.\" $OpenBSD: tmux.1,v 1.1152 2026/07/29 08:58:33 nicm Exp $ .\" $OpenBSD: tmux.1,v 1.1155 2026/08/06 09:05:04 nicm Exp $
.\" .\"
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> .\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
.\" .\"
@@ -14,7 +14,7 @@
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
.\" .\"
.Dd $Mdocdate: July 29 2026 $ .Dd $Mdocdate: August 6 2026 $
.Dt TMUX 1 .Dt TMUX 1
.Os .Os
.Sh NAME .Sh NAME
@@ -356,6 +356,8 @@ Force redraw of the attached client.
Select a new session for the attached client interactively. Select a new session for the attached client interactively.
.It t .It t
Show the time. Show the time.
.It T
Change the current pane title.
.It w .It w
Choose the current window interactively. Choose the current window interactively.
.It x .It x
@@ -2341,10 +2343,14 @@ the bottom and is paused while a selection is in progress.
.Xc .Xc
Turn off automatic refresh of the content from the pane. Turn off automatic refresh of the content from the pane.
.It Xo .It Xo
.Ic refresh\-toggle .Ic refresh\-now
(vi: r) (vi: r)
(emacs: r) (emacs: r)
.Xc .Xc
Refresh the content from the pane once.
.It Xo
.Ic refresh\-toggle
.Xc
Toggle automatic refresh of the content from the pane. Toggle automatic refresh of the content from the pane.
.It Xo .It Xo
.Ic scroll\-bottom .Ic scroll\-bottom
@@ -7426,6 +7432,7 @@ The following variables are available, where appropriate:
.It Li "pane_pid" Ta "" Ta "PID of first process in pane" .It Li "pane_pid" Ta "" Ta "PID of first process in pane"
.It Li "pane_pipe" Ta "" Ta "1 if pane is being piped" .It Li "pane_pipe" Ta "" Ta "1 if pane is being piped"
.It Li "pane_pipe_pid" Ta "" Ta "PID of pipe process, if any" .It Li "pane_pipe_pid" Ta "" Ta "PID of pipe process, if any"
.It Li "pane_private_modes" Ta "" Ta "List of enabled DECSET private modes"
.It Li "pane_pb_state" Ta "" Ta "Pane progress bar state, one of hidden, normal, error, indeterminate, paused (can be set by application)" .It Li "pane_pb_state" Ta "" Ta "Pane progress bar state, one of hidden, normal, error, indeterminate, paused (can be set by application)"
.It Li "pane_pb_progress" Ta "" Ta "Pane progress bar progress percentage (can be set by application)" .It Li "pane_pb_progress" Ta "" Ta "Pane progress bar progress percentage (can be set by application)"
.It Li "pane_right" Ta "" Ta "Right of pane" .It Li "pane_right" Ta "" Ta "Right of pane"

15
tmux.h
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1416 2026/07/27 19:15:58 nicm Exp $ */ /* $OpenBSD: tmux.h,v 1.1422 2026/08/05 08:54:56 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1197,6 +1197,8 @@ struct window_mode {
int flags; int flags;
#define WINDOW_MODE_HIDE_PANE_STATUS 0x1 #define WINDOW_MODE_HIDE_PANE_STATUS 0x1
#define WINDOW_MODE_NO_STACK 0x2 #define WINDOW_MODE_NO_STACK 0x2
#define WINDOW_MODE_FILL_WINDOW 0x4
#define WINDOW_MODE_HIDE_SCROLLBARS 0x8
struct screen *(*init)(struct window_mode_entry *, struct screen *(*init)(struct window_mode_entry *,
struct cmdq_item *, struct cmd_find_state *, struct cmdq_item *, struct cmd_find_state *,
@@ -1732,6 +1734,7 @@ struct tty_term {
#define TERM_RGBCOLOURS 0x10 #define TERM_RGBCOLOURS 0x10
#define TERM_VT100LIKE 0x20 #define TERM_VT100LIKE 0x20
#define TERM_SIXEL 0x40 #define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80
int flags; int flags;
LIST_ENTRY(tty_term) entry; LIST_ENTRY(tty_term) entry;
@@ -2217,6 +2220,8 @@ struct client {
struct event click_timer; struct event click_timer;
int click_loc; int click_loc;
int click_wp; int click_wp;
struct event exit_timer;
u_int click_button; u_int click_button;
struct mouse_event click_event; struct mouse_event click_event;
@@ -3442,7 +3447,7 @@ void grid_clear_history(struct grid *);
const struct grid_line *grid_peek_line(struct grid *, u_int); const struct grid_line *grid_peek_line(struct grid *, u_int);
void grid_get_cell(struct grid *, u_int, u_int, struct grid_cell *); void grid_get_cell(struct grid *, u_int, u_int, struct grid_cell *);
void grid_set_cell(struct grid *, u_int, u_int, const struct grid_cell *); void grid_set_cell(struct grid *, u_int, u_int, const struct grid_cell *);
void grid_set_padding(struct grid *, u_int, u_int); void grid_set_padding(struct grid *, u_int, u_int, int);
void grid_set_cells(struct grid *, u_int, u_int, const struct grid_cell *, void grid_set_cells(struct grid *, u_int, u_int, const struct grid_cell *,
const char *, size_t); const char *, size_t);
struct grid_line *grid_get_line(struct grid *, u_int); struct grid_line *grid_get_line(struct grid *, u_int);
@@ -3459,6 +3464,7 @@ void grid_reflow(struct grid *, u_int);
void grid_wrap_position(struct grid *, u_int, u_int, u_int *, u_int *); void grid_wrap_position(struct grid *, u_int, u_int, u_int *, u_int *);
void grid_unwrap_position(struct grid *, u_int *, u_int *, u_int, u_int); void grid_unwrap_position(struct grid *, u_int *, u_int *, u_int, u_int);
u_int grid_line_length(struct grid *, u_int); u_int grid_line_length(struct grid *, u_int);
u_int grid_line_limit(struct grid *, u_int);
int grid_in_set(struct grid *, u_int, u_int, const char *); int grid_in_set(struct grid *, u_int, u_int, const char *);
/* grid-reader.c */ /* grid-reader.c */
@@ -3486,7 +3492,7 @@ void grid_reader_cursor_back_to_indentation(struct grid_reader *);
void grid_view_get_cell(struct grid *, u_int, u_int, struct grid_cell *); void grid_view_get_cell(struct grid *, u_int, u_int, struct grid_cell *);
void grid_view_set_cell(struct grid *, u_int, u_int, void grid_view_set_cell(struct grid *, u_int, u_int,
const struct grid_cell *); const struct grid_cell *);
void grid_view_set_padding(struct grid *, u_int, u_int); void grid_view_set_padding(struct grid *, u_int, u_int, int);
void grid_view_set_cells(struct grid *, u_int, u_int, void grid_view_set_cells(struct grid *, u_int, u_int,
const struct grid_cell *, const char *, size_t); const struct grid_cell *, const char *, size_t);
void grid_view_clear_history(struct grid *, u_int); void grid_view_clear_history(struct grid *, u_int);
@@ -3968,6 +3974,7 @@ time_t monitor_get_fire_time(struct monitor_set *, const char *);
/* control.c */ /* control.c */
void control_discard(struct client *); void control_discard(struct client *);
void control_discard_all(struct client *);
void control_start(struct client *); void control_start(struct client *);
void control_ready(struct client *); void control_ready(struct client *);
void control_stop(struct client *); void control_stop(struct client *);
@@ -3983,6 +3990,8 @@ struct window_pane_offset *control_pane_offset(struct client *,
struct window_pane *, int *); struct window_pane *, int *);
void control_reset_offsets(struct client *); void control_reset_offsets(struct client *);
void printflike(2, 3) control_write(struct client *, const char *, ...); void printflike(2, 3) control_write(struct client *, const char *, ...);
void printflike(2, 3) control_notify_write(struct client *, const char *, ...);
void control_write_guard(struct client *, const char *, long, u_int, int);
void control_write_output(struct client *, struct window_pane *); void control_write_output(struct client *, struct window_pane *);
int control_all_done(struct client *); int control_all_done(struct client *);
void control_add_sub(struct client *, const char *, enum monitor_type, int, void control_add_sub(struct client *, const char *, enum monitor_type, int,

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: tty-term.c,v 1.106 2026/06/13 09:17:29 nicm Exp $ */ /* $OpenBSD: tty-term.c,v 1.107 2026/08/05 08:54:56 nicm Exp $ */
/* /*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -31,6 +31,7 @@
#include "tmux.h" #include "tmux.h"
static char *tty_term_strip(const char *); static char *tty_term_strip(const char *);
static void tty_term_validate(struct tty_term *);
struct tty_terms tty_terms = LIST_HEAD_INITIALIZER(tty_terms); struct tty_terms tty_terms = LIST_HEAD_INITIALIZER(tty_terms);
@@ -519,6 +520,26 @@ tty_term_apply_overrides(struct tty_term *term)
acs = "a#j+k+l+m+n+o-p-q-r-s-t+u+v+w+x|y<z>~."; acs = "a#j+k+l+m+n+o-p-q-r-s-t+u+v+w+x|y<z>~.";
for (; acs[0] != '\0' && acs[1] != '\0'; acs += 2) for (; acs[0] != '\0' && acs[1] != '\0'; acs += 2)
term->acs[(u_char) acs[0]][0] = acs[1]; term->acs[(u_char) acs[0]][0] = acs[1];
tty_term_validate(term);
}
static void
tty_term_validate(struct tty_term *term)
{
struct tty_code *code = &term->codes[TTYC_MS];
if (code->type != TTYCODE_STRING)
return;
if (*tty_term_string_ss(term, TTYC_MS, "c", "?") != '\0') {
term->flags &= ~TERM_INVALIDMS;
return;
}
log_debug("removing invalid Ms capability");
term->flags |= TERM_INVALIDMS;
free(code->value.string);
code->type = TTYCODE_NONE;
} }
struct tty_term * struct tty_term *

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-client.c,v 1.47 2026/07/14 17:17:18 nicm Exp $ */ /* $OpenBSD: window-client.c,v 1.48 2026/08/05 08:54:56 nicm Exp $ */
/* /*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -113,14 +113,16 @@ static const char *window_client_info_lines[] = {
"#{?#{I/c:kmous},,#[align=right]unavailable: [kmous] missing}", "#{?#{I/c:kmous},,#[align=right]unavailable: [kmous] missing}",
"#[fg=themelightgrey]set-clipboard #[#{E:tree-mode-border-style},acs]x#[default] " "#[fg=themelightgrey]set-clipboard #[#{E:tree-mode-border-style},acs]x#[default] "
"#{?#{!=:#{set-clipboard},off},#{?#{I/f:clipboard},," "#{?#{!=:#{set-clipboard},off},#{?#{I/c:Ms},,"
"#[fg=themered]}#{set-clipboard},#[fg=themelightgrey]off} " "#[fg=themered]}#{set-clipboard},#[fg=themelightgrey]off} "
"#{?#{I/f:clipboard},,#[align=right]unavailable: [Ms] missing}", "#{?#{I/c:Ms},,#[align=right]unavailable: [Ms] "
"#{?clipboard_invalid,invalid,missing}}",
"#[fg=themelightgrey]get-clipboard #[#{E:tree-mode-border-style},acs]x#[default] " "#[fg=themelightgrey]get-clipboard #[#{E:tree-mode-border-style},acs]x#[default] "
"#{?#{!=:#{get-clipboard},off},#{?#{I/f:clipboard},," "#{?#{!=:#{get-clipboard},off},#{?#{I/c:Ms},,"
"#[fg=themered]}#{get-clipboard},#[fg=themelightgrey]off} " "#[fg=themered]}#{get-clipboard},#[fg=themelightgrey]off} "
"#{?#{I/f:clipboard},,#[align=right]unavailable: [Ms] missing}", "#{?#{I/c:Ms},,#[align=right]unavailable: [Ms] "
"#{?clipboard_invalid,invalid,missing}}",
"#[fg=themelightgrey]focus-events #[#{E:tree-mode-border-style},acs]x#[default] " "#[fg=themelightgrey]focus-events #[#{E:tree-mode-border-style},acs]x#[default] "
"#{?focus-events,#{?#{I/f:focus},,#[fg=themered]}on,#[fg=themelightgrey]off} " "#{?focus-events,#{?#{I/f:focus},,#[fg=themered]}on,#[fg=themelightgrey]off} "
@@ -273,6 +275,10 @@ window_client_draw_info(__unused void *modedata, void *itemdata,
char *expanded; char *expanded;
ft = format_create_defaults(NULL, c, NULL, NULL, NULL); ft = format_create_defaults(NULL, c, NULL, NULL, NULL);
if (c->tty.term->flags & TERM_INVALIDMS)
format_add(ft, "clipboard_invalid", "1");
else
format_add(ft, "clipboard_invalid", "0");
screen_write_cursormove(ctx, cx, cy, 0); screen_write_cursormove(ctx, cx, cy, 0);
for (i = 0; i < nitems(window_client_info_lines); i++) { for (i = 0; i < nitems(window_client_info_lines); i++) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-copy.c,v 1.423 2026/07/21 11:52:13 nicm Exp $ */ /* $OpenBSD: window-copy.c,v 1.425 2026/08/05 12:23:25 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -54,6 +54,7 @@ static void window_copy_redraw_screen(struct window_mode_entry *);
static void window_copy_do_refresh(struct window_mode_entry *, int); static void window_copy_do_refresh(struct window_mode_entry *, int);
static void window_copy_refresh_timer(int, short, void *); static void window_copy_refresh_timer(int, short, void *);
static void window_copy_refresh_arm(struct window_mode_entry *); static void window_copy_refresh_arm(struct window_mode_entry *);
static int window_copy_refresh_allowed(struct window_mode_entry *);
static void window_copy_refresh_start(struct window_mode_entry *); static void window_copy_refresh_start(struct window_mode_entry *);
static void window_copy_refresh_stop(struct window_mode_entry *); static void window_copy_refresh_stop(struct window_mode_entry *);
static void window_copy_style_changed(struct window_mode_entry *); static void window_copy_style_changed(struct window_mode_entry *);
@@ -3036,6 +3037,20 @@ window_copy_refresh_arm(struct window_mode_entry *wme)
evtimer_add(&data->refresh_timer, &tv); evtimer_add(&data->refresh_timer, &tv);
} }
static int
window_copy_refresh_allowed(struct window_mode_entry *wme)
{
struct window_copy_mode_data *data = wme->data;
/*
* Do not refresh a view of another pane (copy-mode -s): the source may
* disappear and changes are not tracked on this pane.
*/
if (data->viewmode || wme->swp != wme->wp)
return (0);
return (1);
}
static void static void
window_copy_refresh_timer(__unused int fd, __unused short events, void *arg) window_copy_refresh_timer(__unused int fd, __unused short events, void *arg)
{ {
@@ -3070,11 +3085,7 @@ window_copy_refresh_start(struct window_mode_entry *wme)
{ {
struct window_copy_mode_data *data = wme->data; struct window_copy_mode_data *data = wme->data;
/* if (!window_copy_refresh_allowed(wme) || data->refresh_active)
* Do not refresh a view of another pane (copy-mode -s): the source may
* disappear and changes are not tracked on this pane.
*/
if (data->viewmode || wme->swp != wme->wp || data->refresh_active)
return; return;
data->refresh_active = 1; data->refresh_active = 1;
window_copy_refresh_arm(wme); window_copy_refresh_arm(wme);
@@ -3089,6 +3100,25 @@ window_copy_refresh_stop(struct window_mode_entry *wme)
evtimer_del(&data->refresh_timer); evtimer_del(&data->refresh_timer);
} }
static enum window_copy_cmd_action
window_copy_cmd_refresh_now(struct window_copy_cmd_state *cs)
{
struct window_mode_entry *wme = cs->wme;
struct window_copy_mode_data *data = wme->data;
struct window_pane *wp = wme->wp;
int follow;
if (!window_copy_refresh_allowed(wme))
return (WINDOW_COPY_CMD_NOTHING);
follow = (data->oy == 0 &&
data->cy == screen_size_y(&data->screen) - 1);
window_copy_do_refresh(wme, follow);
wp->flags &= ~PANE_UNSEENCHANGES;
return (WINDOW_COPY_CMD_REDRAW);
}
static enum window_copy_cmd_action static enum window_copy_cmd_action
window_copy_cmd_refresh_on(struct window_copy_cmd_state *cs) window_copy_cmd_refresh_on(struct window_copy_cmd_state *cs)
{ {
@@ -3631,6 +3661,12 @@ static const struct {
.clear = WINDOW_COPY_CMD_CLEAR_NEVER, .clear = WINDOW_COPY_CMD_CLEAR_NEVER,
.f = window_copy_cmd_refresh_off .f = window_copy_cmd_refresh_off
}, },
{ .command = "refresh-now",
.args = { "", 0, 0, NULL },
.flags = WINDOW_COPY_CMD_FLAG_READONLY,
.clear = WINDOW_COPY_CMD_CLEAR_NEVER,
.f = window_copy_cmd_refresh_now
},
{ .command = "refresh-toggle", { .command = "refresh-toggle",
.args = { "", 0, 0, NULL }, .args = { "", 0, 0, NULL },
.flags = WINDOW_COPY_CMD_FLAG_READONLY, .flags = WINDOW_COPY_CMD_FLAG_READONLY,
@@ -6215,16 +6251,13 @@ static u_int
window_copy_cursor_limit(struct window_mode_entry *wme, u_int py, window_copy_cursor_limit(struct window_mode_entry *wme, u_int py,
int allow_onemore) int allow_onemore)
{ {
struct window_copy_mode_data *data = wme->data;
struct options *oo = wme->wp->window->options; struct options *oo = wme->wp->window->options;
u_int len;
len = window_copy_find_length(wme, py);
if (allow_onemore || if (allow_onemore ||
options_get_number(oo, "mode-keys") != MODEKEY_VI) options_get_number(oo, "mode-keys") != MODEKEY_VI)
return (len); return (window_copy_find_length(wme, py));
if (len == 0) return (grid_line_limit(data->backing->grid, py));
return (0);
return (len - 1);
} }
static void static void

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window-panes.c,v 1.3 2026/07/19 19:53:11 nicm Exp $ */ /* $OpenBSD: window-panes.c,v 1.4 2026/07/29 14:06:32 nicm Exp $ */
/* /*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -35,7 +35,8 @@ static void window_panes_key(struct window_mode_entry *,
const struct window_mode window_panes_mode = { const struct window_mode window_panes_mode = {
.name = "panes-mode", .name = "panes-mode",
.flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK, .flags = WINDOW_MODE_HIDE_PANE_STATUS|WINDOW_MODE_NO_STACK|
WINDOW_MODE_FILL_WINDOW|WINDOW_MODE_HIDE_SCROLLBARS,
.init = window_panes_init, .init = window_panes_init,
.free = window_panes_free, .free = window_panes_free,

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: window.c,v 1.368 2026/07/23 09:38:27 nicm Exp $ */ /* $OpenBSD: window.c,v 1.369 2026/07/29 14:06:32 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -2460,11 +2460,21 @@ window_pane_mode(struct window_pane *wp)
int int
window_pane_show_scrollbar(struct window_pane *wp) window_pane_show_scrollbar(struct window_pane *wp)
{ {
struct window *w = wp->window;
struct window_mode_entry *wme;
if (SCREEN_IS_ALTERNATE(&wp->base)) if (SCREEN_IS_ALTERNATE(&wp->base))
return (0); return (0);
if (wp->window->sb == PANE_SCROLLBARS_ALWAYS || if ((w->flags & WINDOW_ZOOMED) && w->active != NULL) {
wp->window->sb == PANE_SCROLLBARS_AUTOHIDE || wme = TAILQ_FIRST(&w->active->modes);
(wp->window->sb == PANE_SCROLLBARS_MODAL && if (wme != NULL &&
(wme->mode->flags & WINDOW_MODE_HIDE_SCROLLBARS)) {
return (0);
}
}
if (w->sb == PANE_SCROLLBARS_ALWAYS ||
w->sb == PANE_SCROLLBARS_AUTOHIDE ||
(w->sb == PANE_SCROLLBARS_MODAL &&
window_pane_mode(wp) != WINDOW_PANE_NO_MODE)) window_pane_mode(wp) != WINDOW_PANE_NO_MODE))
return (1); return (1);
return (0); return (0);