From 4d1ab1ba2a8404500624a199a0ad53c52cc95e0b Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 8 Jun 2026 21:38:19 +0000 Subject: [PATCH 01/10] Allow ACLs to use groups as well as users, GitHub issue 4917. --- cmd-server-access.c | 94 ++++++++++--------- proc.c | 12 ++- server-acl.c | 222 ++++++++++++++++++++++++++------------------ tmux.1 | 38 ++++++-- tmux.h | 14 +-- 5 files changed, 231 insertions(+), 149 deletions(-) diff --git a/cmd-server-access.c b/cmd-server-access.c index 43cf4d5f8..4ec61af3c 100644 --- a/cmd-server-access.c +++ b/cmd-server-access.c @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -37,67 +38,71 @@ const struct cmd_entry cmd_server_access_entry = { .name = "server-access", .alias = NULL, - .args = { "adlrw", 0, 1, NULL }, - .usage = "[-adlrw] " CMD_TARGET_PANE_USAGE " [user]", + .args = { "adglrw", 0, 1, NULL }, + .usage = "[-adglrw] " CMD_TARGET_PANE_USAGE " [user|group]", .flags = CMD_CLIENT_CANFAIL, .exec = cmd_server_access_exec }; static enum cmd_retval -cmd_server_access_deny(struct cmdq_item *item, struct passwd *pw) +cmd_server_access_deny(struct cmdq_item *item, id_t id, int flags, + const char *type, const char *name) { - struct client *loop; - struct server_acl_user *user; - uid_t uid; - - if ((user = server_acl_user_find(pw->pw_uid)) == NULL) { - cmdq_error(item, "user %s not found", pw->pw_name); + if (!server_acl_find(id, flags)) { + cmdq_error(item, "%s %s not found", type, name); return (CMD_RETURN_ERROR); } - TAILQ_FOREACH(loop, &clients, entry) { - uid = proc_get_peer_uid(loop->peer); - if (uid == server_acl_get_uid(user)) { - loop->exit_message = xstrdup("access not allowed"); - loop->flags |= CLIENT_EXIT; - } - } - server_acl_user_deny(pw->pw_uid); - + server_acl_deny(id, flags); return (CMD_RETURN_NORMAL); } static enum cmd_retval cmd_server_access_exec(struct cmd *self, struct cmdq_item *item) { - struct args *args = cmd_get_args(self); struct client *c = cmdq_get_target_client(item); - char *name; - struct passwd *pw = NULL; + char *arg; + const char *name, *type = NULL; + struct passwd *pw; + struct group *gr; + id_t id; + int flags = 0; if (args_has(args, 'l')) { server_acl_display(item); return (CMD_RETURN_NORMAL); } if (args_count(args) == 0) { - cmdq_error(item, "missing user argument"); + cmdq_error(item, "missing user or group argument"); return (CMD_RETURN_ERROR); } - name = format_single(item, args_string(args, 0), c, NULL, NULL, NULL); - if (*name != '\0') - pw = getpwnam(name); - if (pw == NULL) { - cmdq_error(item, "unknown user: %s", name); - free(name); + arg = format_single(item, args_string(args, 0), c, NULL, NULL, NULL); + if (args_has(args, 'g')) { + if ((gr = getgrnam(arg)) != NULL) { + type = "group"; + id = gr->gr_gid; + name = gr->gr_name; + flags |= SERVER_ACL_IS_GROUP; + } + } else { + if ((pw = getpwnam(arg)) != NULL) { + type = "user"; + id = pw->pw_uid; + name = pw->pw_name; + } + } + if (type == NULL) { + cmdq_error(item, "unknown %s: %s", type, arg); + free(arg); return (CMD_RETURN_ERROR); } - free(name); + free(arg); - if (pw->pw_uid == 0 || pw->pw_uid == getuid()) { + if ((~flags & SERVER_ACL_IS_GROUP) && (id == 0 || id == getuid())) { cmdq_error(item, "%s owns the server, can't change access", - pw->pw_name); + name); return (CMD_RETURN_ERROR); } @@ -111,36 +116,35 @@ cmd_server_access_exec(struct cmd *self, struct cmdq_item *item) } if (args_has(args, 'd')) - return (cmd_server_access_deny(item, pw)); + return (cmd_server_access_deny(item, id, flags, type, name)); if (args_has(args, 'a')) { - if (server_acl_user_find(pw->pw_uid) != NULL) { - cmdq_error(item, "user %s is already added", - pw->pw_name); + if (server_acl_find(id, flags)) { + cmdq_error(item, "%s %s is already added", type, name); return (CMD_RETURN_ERROR); } - server_acl_user_allow(pw->pw_uid); + server_acl_allow(id, flags); /* Do not return - allow -r or -w with -a. */ } else if (args_has(args, 'r') || args_has(args, 'w')) { - /* -r or -w implies -a if user does not exist. */ - if (server_acl_user_find(pw->pw_uid) == NULL) - server_acl_user_allow(pw->pw_uid); + /* -r or -w implies -a if the entry does not exist. */ + if (!server_acl_find(id, flags)) + server_acl_allow(id, flags); } if (args_has(args, 'w')) { - if (server_acl_user_find(pw->pw_uid) == NULL) { - cmdq_error(item, "user %s not found", pw->pw_name); + if (!server_acl_find(id, flags)) { + cmdq_error(item, "%s %s not found", type, name); return (CMD_RETURN_ERROR); } - server_acl_user_allow_write(pw->pw_uid); + server_acl_allow_write(id, flags); return (CMD_RETURN_NORMAL); } if (args_has(args, 'r')) { - if (server_acl_user_find(pw->pw_uid) == NULL) { - cmdq_error(item, "user %s not found", pw->pw_name); + if (!server_acl_find(id, flags)) { + cmdq_error(item, "%s %s not found", type, name); return (CMD_RETURN_ERROR); } - server_acl_user_deny_write(pw->pw_uid); + server_acl_deny_write(id, flags); return (CMD_RETURN_NORMAL); } diff --git a/proc.c b/proc.c index 7408528e2..21a747525 100644 --- a/proc.c +++ b/proc.c @@ -56,6 +56,7 @@ struct tmuxpeer { struct imsgbuf ibuf; struct event event; uid_t uid; + gid_t gid; int flags; #define PEER_BAD 0x1 @@ -297,7 +298,6 @@ proc_add_peer(struct tmuxproc *tp, int fd, void (*dispatchcb)(struct imsg *, void *), void *arg) { struct tmuxpeer *peer; - gid_t gid; peer = xcalloc(1, sizeof *peer); peer->parent = tp; @@ -310,8 +310,10 @@ proc_add_peer(struct tmuxproc *tp, int fd, imsgbuf_allow_fdpass(&peer->ibuf); event_set(&peer->event, fd, EV_READ, proc_event_cb, peer); - if (getpeereid(fd, &peer->uid, &gid) != 0) + if (getpeereid(fd, &peer->uid, &peer->gid) != 0) { peer->uid = (uid_t)-1; + peer->gid = (gid_t)-1; + } log_debug("add peer %p: %d (%p)", peer, fd, arg); TAILQ_INSERT_TAIL(&tp->peers, peer, entry); @@ -380,3 +382,9 @@ proc_get_peer_uid(struct tmuxpeer *peer) { return (peer->uid); } + +gid_t +proc_get_peer_gid(struct tmuxpeer *peer) +{ + return (peer->gid); +} diff --git a/server-acl.c b/server-acl.c index 26f2490d2..db95a3c6b 100644 --- a/server-acl.c +++ b/server-acl.c @@ -22,6 +22,7 @@ #include #include +#include #include #include #include @@ -29,158 +30,203 @@ #include "tmux.h" -struct server_acl_user { - uid_t uid; +struct server_acl_entry { + id_t id; - int flags; -#define SERVER_ACL_READONLY 0x1 + int flags; - RB_ENTRY(server_acl_user) entry; + RB_ENTRY(server_acl_entry) entry; }; static int -server_acl_cmp(struct server_acl_user *user1, struct server_acl_user *user2) +server_acl_cmp(struct server_acl_entry *entry1, + struct server_acl_entry *entry2) { - if (user1->uid < user2->uid) + if ((entry1->flags ^ entry2->flags) & SERVER_ACL_IS_GROUP) { + if (entry1->flags & SERVER_ACL_IS_GROUP) + return (1); return (-1); - return (user1->uid > user2->uid); + } + + if (entry1->id < entry2->id) + return (-1); + return (entry1->id > entry2->id); } -RB_HEAD(server_acl_entries, server_acl_user) server_acl_entries; -RB_GENERATE_STATIC(server_acl_entries, server_acl_user, entry, server_acl_cmp); +RB_HEAD(server_acl_entries, server_acl_entry) server_acl_entries; +RB_GENERATE_STATIC(server_acl_entries, server_acl_entry, entry, server_acl_cmp); -/* Initialize server_acl tree. */ +static struct server_acl_entry * +server_acl_entry_find(id_t id, int flags) +{ + struct server_acl_entry find = { + .id = id, + .flags = flags & SERVER_ACL_IS_GROUP + }; + + return (RB_FIND(server_acl_entries, &server_acl_entries, &find)); +} + +static struct server_acl_entry * +server_acl_check(struct client *c) +{ + struct server_acl_entry *entry; + uid_t uid; + gid_t gid; + + uid = proc_get_peer_uid(c->peer); + if (uid == (uid_t)-1) + return (NULL); + + entry = server_acl_entry_find(uid, 0); + if (entry != NULL) + return (entry); + + gid = proc_get_peer_gid(c->peer); + if (gid == (gid_t)-1) + return (NULL); + + return (server_acl_entry_find(gid, SERVER_ACL_IS_GROUP)); +} + +static void +server_acl_update(void) +{ + struct server_acl_entry *entry; + struct client *c; + + TAILQ_FOREACH(c, &clients, entry) { + entry = server_acl_check(c); + if (entry == NULL) { + c->exit_message = xstrdup("access not allowed"); + c->flags |= CLIENT_EXIT; + } else if (entry->flags & SERVER_ACL_READONLY) + c->flags |= CLIENT_READONLY; + else + c->flags &= ~CLIENT_READONLY; + } +} + +/* Initialize ACL tree. */ void server_acl_init(void) { RB_INIT(&server_acl_entries); if (getuid() != 0) - server_acl_user_allow(0); - server_acl_user_allow(getuid()); + server_acl_allow(0, 0); + server_acl_allow(getuid(), 0); } -/* Find user entry. */ -struct server_acl_user* -server_acl_user_find(uid_t uid) +/* Check if an ACL entry exists. */ +int +server_acl_find(id_t id, int flags) { - struct server_acl_user find = { .uid = uid }; - - return (RB_FIND(server_acl_entries, &server_acl_entries, &find)); + return (server_acl_entry_find(id, flags) != NULL); } /* Display the tree. */ void server_acl_display(struct cmdq_item *item) { - struct server_acl_user *loop; + struct server_acl_entry *loop; struct passwd *pw; + struct group *gr; const char *name; + char type; RB_FOREACH(loop, server_acl_entries, &server_acl_entries) { - if (loop->uid == 0) - continue; - if ((pw = getpwuid(loop->uid)) != NULL) - name = pw->pw_name; + if (~loop->flags & SERVER_ACL_IS_GROUP) { + if (loop->id == 0) + continue; + if ((pw = getpwuid(loop->id)) != NULL) + name = pw->pw_name; + else + name = "unknown"; + type = 'U'; + } else { + if ((gr = getgrgid(loop->id)) != NULL) + name = gr->gr_name; + else + name = "unknown"; + type = 'G'; + } + if (loop->flags & SERVER_ACL_READONLY) + cmdq_print(item, "%s (%c,R)", name, type); else - name = "unknown"; - if (loop->flags == SERVER_ACL_READONLY) - cmdq_print(item, "%s (R)", name); - else - cmdq_print(item, "%s (W)", name); + cmdq_print(item, "%s (%c,W)", name, type); } } -/* Allow a user. */ +/* Allow an ACL entry. */ void -server_acl_user_allow(uid_t uid) +server_acl_allow(id_t id, int flags) { - struct server_acl_user *user; + struct server_acl_entry *entry; - user = server_acl_user_find(uid); - if (user == NULL) { - user = xcalloc(1, sizeof *user); - user->uid = uid; - RB_INSERT(server_acl_entries, &server_acl_entries, user); + entry = server_acl_entry_find(id, flags); + if (entry == NULL) { + entry = xcalloc(1, sizeof *entry); + entry->id = id; + entry->flags = flags & SERVER_ACL_IS_GROUP; + RB_INSERT(server_acl_entries, &server_acl_entries, entry); } } -/* Deny a user (remove from the tree). */ +/* Deny an ACL entry (remove it from the tree). */ void -server_acl_user_deny(uid_t uid) +server_acl_deny(id_t id, int flags) { - struct server_acl_user *user; + struct server_acl_entry *entry; - user = server_acl_user_find(uid); - if (user != NULL) { - RB_REMOVE(server_acl_entries, &server_acl_entries, user); - free(user); + entry = server_acl_entry_find(id, flags); + if (entry != NULL) { + RB_REMOVE(server_acl_entries, &server_acl_entries, entry); + free(entry); + server_acl_update(); } } -/* Allow this user write access. */ +/* Allow this ACL entry write access. */ void -server_acl_user_allow_write(uid_t uid) +server_acl_allow_write(id_t id, int flags) { - struct server_acl_user *user; - struct client *c; + struct server_acl_entry *entry; - user = server_acl_user_find(uid); - if (user == NULL) + entry = server_acl_entry_find(id, flags); + if (entry == NULL) return; - user->flags &= ~SERVER_ACL_READONLY; - - TAILQ_FOREACH(c, &clients, entry) { - uid = proc_get_peer_uid(c->peer); - if (uid != (uid_t)-1 && uid == user->uid) - c->flags &= ~CLIENT_READONLY; - } + entry->flags &= ~SERVER_ACL_READONLY; + server_acl_update(); } -/* Deny this user write access. */ +/* Deny this ACL entry write access. */ void -server_acl_user_deny_write(uid_t uid) +server_acl_deny_write(id_t id, int flags) { - struct server_acl_user *user; - struct client *c; + struct server_acl_entry *entry; - user = server_acl_user_find(uid); - if (user == NULL) + entry = server_acl_entry_find(id, flags); + if (entry == NULL) return; - user->flags |= SERVER_ACL_READONLY; - - TAILQ_FOREACH(c, &clients, entry) { - uid = proc_get_peer_uid(c->peer); - if (uid != (uid_t)-1 && uid == user->uid) - c->flags |= CLIENT_READONLY; - } + entry->flags |= SERVER_ACL_READONLY; + server_acl_update(); } /* - * Check if the client's UID exists in the ACL list and if so, set as read only - * if needed. Return false if the user does not exist. + * Check if the client's UID or GID exists in the ACL list and if so, set as + * read only if needed. UID entries take precedence over GID entries. Return + * false if no entry exists. */ int server_acl_join(struct client *c) { - struct server_acl_user *user; - uid_t uid; + struct server_acl_entry *entry; - uid = proc_get_peer_uid(c->peer); - if (uid == (uid_t)-1) + entry = server_acl_check(c); + if (entry == NULL) return (0); - - user = server_acl_user_find(uid); - if (user == NULL) - return (0); - if (user->flags & SERVER_ACL_READONLY) + if (entry->flags & SERVER_ACL_READONLY) c->flags |= CLIENT_READONLY; return (1); } - -/* Get UID for user entry. */ -uid_t -server_acl_get_uid(struct server_acl_user *user) -{ - return (user->uid); -} diff --git a/tmux.1 b/tmux.1 index 57746ac78..fa308e7e9 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1574,35 +1574,57 @@ option. Rename the session to .Ar new\-name . .It Xo Ic server\-access -.Op Fl adlrw -.Op Ar user +.Op Fl adglrw +.Op Ar user | group .Xc Change the access or read/write permission of -.Ar user . +.Ar user +or +.Ar group . The user running the .Nm server (its owner) and the root user cannot be changed and are always permitted access. +.Fl g +changes a group rather than a user. .Pp .Fl a and .Fl d -are used to give or revoke access for the specified user. -If the user is already attached, the +are used to give or revoke access for the specified user or group. +If a client is already attached, the .Fl d -flag causes their clients to be detached. +flag causes it to be detached unless it is still permitted by another entry. .Pp .Fl r and .Fl w change the permissions for -.Ar user : +.Ar user +or +.Ar group : .Fl r -makes their clients read-only and +makes matching clients read-only and .Fl w writable. .Fl l lists current access permissions. +User entries are shown with +.Ql U , +group entries with +.Ql G , +and read-only or writable entries with +.Ql R +or +.Ql W , +for example +.Ql user1 (U,W) +or +.Ql testgroup (G,R) . +If both a user and group entry match a client, the user entry takes +precedence. +Only the effective group ID of the client is used, not its supplementary +groups. .Pp By default, the access list is empty and .Nm diff --git a/tmux.h b/tmux.h index b618b3be9..7cd6262e4 100644 --- a/tmux.h +++ b/tmux.h @@ -2386,6 +2386,7 @@ void proc_flush_peer(struct tmuxpeer *); void proc_toggle_log(struct tmuxproc *); pid_t proc_fork_and_daemon(int *); uid_t proc_get_peer_uid(struct tmuxpeer *); +gid_t proc_get_peer_gid(struct tmuxpeer *); /* cfg.c */ extern int cfg_finished; @@ -3787,15 +3788,16 @@ struct window_pane *spawn_pane(struct spawn_context *, char **); char *regsub(const char *, const char *, const char *, int); /* server-acl.c */ +#define SERVER_ACL_READONLY 0x1 +#define SERVER_ACL_IS_GROUP 0x2 void server_acl_init(void); -struct server_acl_user *server_acl_user_find(uid_t); +int server_acl_find(id_t, int); void server_acl_display(struct cmdq_item *); -void server_acl_user_allow(uid_t); -void server_acl_user_deny(uid_t); -void server_acl_user_allow_write(uid_t); -void server_acl_user_deny_write(uid_t); +void server_acl_allow(id_t, int); +void server_acl_deny(id_t, int); +void server_acl_allow_write(id_t, int); +void server_acl_deny_write(id_t, int); int server_acl_join(struct client *); -uid_t server_acl_get_uid(struct server_acl_user *); /* hyperlink.c */ u_int hyperlinks_put(struct hyperlinks *, const char *, From a6a06c5aa614fe992041a2f1139c01c4f4fc4bcc Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 8 Jun 2026 21:46:19 +0000 Subject: [PATCH 02/10] Fix squash-groups skipping every session, GitHub issue 5180 from Bryce Miller. --- window-tree.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/window-tree.c b/window-tree.c index 7a0db7c94..dadc473a2 100644 --- a/window-tree.c +++ b/window-tree.c @@ -355,6 +355,7 @@ window_tree_build(void *modedata, struct sort_criteria *sort_crit, uint64_t *tag, const char *filter) { struct window_tree_modedata *data = modedata; + int squash_groups = data->squash_groups; struct session *s, **l; struct session_group *sg, *current; u_int n, i; @@ -370,15 +371,14 @@ window_tree_build(void *modedata, struct sort_criteria *sort_crit, l = sort_get_sessions(&n, sort_crit); if (n == 0) return; - s = l[n - 1]; for (i = 0; i < n; i++) { - if (data->squash_groups && - (sg = session_group_contains(s)) != NULL) { + s = l[i]; + if (squash_groups && (sg = session_group_contains(s)) != NULL) { if ((sg == current && s != data->fs.s) || (sg != current && s != TAILQ_FIRST(&sg->sessions))) continue; } - window_tree_build_session(l[i], modedata, sort_crit, filter); + window_tree_build_session(s, modedata, sort_crit, filter); } switch (data->type) { From fd10db5a6212a1db54a7762b099d1cc17f2062e7 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 8 Jun 2026 23:06:21 +0000 Subject: [PATCH 03/10] Fix mouse events on tiled pane status line - when panes share a border, prefer the pane for which the border is the status line. With Dane Jensen. --- server-client.c | 8 ++------ window.c | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/server-client.c b/server-client.c index a819e6f9e..280e9afa1 100644 --- a/server-client.c +++ b/server-client.c @@ -699,12 +699,8 @@ server_client_check_mouse_in_pane(struct window_pane *wp, int px, int py, bdr_bottom = fwp->yoff + fwp->sy; if (py == bdr_bottom) break; - if (window_pane_is_floating(wp)) { - /* Floating pane, check top border. */ - bdr_top = fwp->yoff - 1; - if (py == bdr_top) - break; - } + if (py == bdr_top) + break; } } if (fwp != NULL) diff --git a/window.c b/window.c index ddd1d6f0d..10adaa450 100644 --- a/window.c +++ b/window.c @@ -621,12 +621,32 @@ window_get_active_at(struct window *w, u_int x, u_int y) pane_status = options_get_number(w->options, "pane-border-status"); + if (pane_status == PANE_STATUS_TOP) { + /* + * Prefer a pane's top border status line over the pane above's + * bottom border. + */ + TAILQ_FOREACH(wp, &w->z_index, zentry) { + if (!window_pane_visible(wp) || window_pane_is_floating(wp)) + continue; + + window_pane_full_size_offset(wp, &xoff, &yoff, &sx, &sy); + if ((int)x < xoff || x > xoff + sx) + continue; + if ((int)y == yoff - 1) + return (wp); + } + } + TAILQ_FOREACH(wp, &w->z_index, zentry) { if (!window_pane_visible(wp)) continue; window_pane_full_size_offset(wp, &xoff, &yoff, &sx, &sy); if (!window_pane_is_floating(wp)) { - /* Tiled - to and including bottom or right border. */ + /* + * Tiled - to and including the right border, excluding + * the bottom border. + */ if ((int)x < xoff || x > xoff + sx) continue; if (pane_status == PANE_STATUS_TOP) { From 27c58c764d4534855f6cafcf895d265197697901 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 9 Jun 2026 08:11:53 +0000 Subject: [PATCH 04/10] Do not use NULL arguments, bug spotted by Dane Jensen. --- window.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/window.c b/window.c index 10adaa450..eb895b9f2 100644 --- a/window.c +++ b/window.c @@ -1233,7 +1233,7 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, TAILQ_INSERT_HEAD(&wp->modes, wme, entry); wme->screen = wme->mode->init(wme, fs, args); } - wme->kill = args_has(args, 'k'); + wme->kill = args != NULL ? args_has(args, 'k') : 0; wp->screen = wme->screen; wp->flags |= (PANE_REDRAW|PANE_REDRAWSCROLLBAR|PANE_CHANGED); From d5c91963486e26043c7ca947d07ebd127e567dec Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 9 Jun 2026 09:11:05 +0000 Subject: [PATCH 05/10] Add -B to new-pane/split-window to block until the command exits, like run-shell -b. --- cmd-split-window.c | 14 ++++++++++++-- server.c | 2 ++ tmux.1 | 13 ++++++++++++- tmux.h | 2 ++ window.c | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index cc6427b50..98a4072d5 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -39,7 +39,7 @@ const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", .alias = "newp", - .args = { "bc:de:EfF:hIkl:Lm:p:PR:s:S:t:T:vx:X:y:Y:Z", 0, -1, NULL }, + .args = { "bBc:de:EfF:hIkl:Lm:p:PR:s:S:t:T:vx:X:y:Y:Z", 0, -1, NULL }, .usage = "[-bdefhIklPvZ] [-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -57,7 +57,7 @@ const struct cmd_entry cmd_split_window_entry = { .name = "split-window", .alias = "splitw", - .args = { "bc:de:EfF:hIkl:m:p:PR:s:S:t:T:vZ", 0, -1, NULL }, + .args = { "bBc:de:EfF:hIkl:m:p:PR:s:S:t:T:vZ", 0, -1, NULL }, .usage = "[-bdefhIklPvZ] [-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -245,5 +245,15 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) environ_free(sc.environ); if (input) return (CMD_RETURN_WAIT); + + if (args_has(args, 'B')) { + /* + * With -B, block this command queue item until the pane's + * command exits; window_pane_block_finish will be called to + * continue it. + */ + new_wp->block_item = item; + return (CMD_RETURN_WAIT); + } return (CMD_RETURN_NORMAL); } diff --git a/server.c b/server.c index b50bb0b64..ff4d165a1 100644 --- a/server.c +++ b/server.c @@ -491,6 +491,8 @@ server_child_exited(pid_t pid, int status) log_debug("%%%u exited", wp->id); wp->flags |= PANE_EXITED; + window_pane_block_finish(wp); + if (window_pane_destroy_ready(wp)) server_destroy_pane(wp, 1); break; diff --git a/tmux.1 b/tmux.1 index fa308e7e9..d2163335e 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3380,7 +3380,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bdefhIkPvZ +.Op Fl bBdefhIkPvZ .Op Fl c Ar start\-directory .Op Fl e Ar environment .Op Fl F Ar format @@ -3456,6 +3456,17 @@ but also sets the option for this pane to .Ar message . .Pp +.Fl B +blocks until +.Ar shell\-command +exits, then returns its exit status. +For example: +.Bd -literal -offset indent +$ tmux new-pane -B 'vi afile' +$ echo $? +0 +.Ed +.Pp .Fl E , or an empty .Ar shell\-command , diff --git a/tmux.h b/tmux.h index 7cd6262e4..e80047dcd 100644 --- a/tmux.h +++ b/tmux.h @@ -1262,6 +1262,7 @@ struct window_pane { char tty[TTY_NAME_MAX]; int status; struct timeval dead_time; + struct cmdq_item *block_item; /* new-pane -B: waiting for pane exit */ int fd; struct bufferevent *event; @@ -3372,6 +3373,7 @@ struct window *window_find_by_id(u_int); void window_update_activity(struct window *); struct window *window_create(u_int, u_int, u_int, u_int); void window_pane_set_event(struct window_pane *); +void window_pane_block_finish(struct window_pane *); struct window_pane *window_get_active_at(struct window *, u_int, u_int); struct window_pane *window_find_string(struct window *, const char *); int window_has_floating_panes(struct window *); diff --git a/window.c b/window.c index eb895b9f2..914c34d7f 100644 --- a/window.c +++ b/window.c @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -382,6 +383,13 @@ window_pane_destroy_ready(struct window_pane *wp) if (~wp->flags & PANE_EXITED) return (0); + + /* + * If a command queue item is blocked on this pane, wait for the + * child's exit status before destroying it. + */ + if (wp->block_item != NULL && (~wp->flags & PANE_STATUSREADY)) + return (0); return (1); } @@ -1077,12 +1085,38 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) return (wp); } +void +window_pane_block_finish(struct window_pane *wp) +{ + struct cmdq_item *item = wp->block_item; + struct client *c; + int retval = 0; + + if (item == NULL) + return; + wp->block_item = NULL; + + if (wp->flags & PANE_STATUSREADY) { + if (WIFEXITED(wp->status)) + retval = WEXITSTATUS(wp->status); + else if (WIFSIGNALED(wp->status)) + retval = WTERMSIG(wp->status) + 128; + } + + c = cmdq_get_client(item); + if (c != NULL && c->session == NULL) + c->retval = retval; + cmdq_continue(item); +} + static void window_pane_destroy(struct window_pane *wp) { struct window_pane_resize *r; struct window_pane_resize *r1; + window_pane_block_finish(wp); + window_pane_reset_mode_all(wp); free(wp->searchstr); From a42e425d4437e046e29aac8324759ce67030971a Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 9 Jun 2026 11:49:36 +0000 Subject: [PATCH 06/10] Two fixes for RI codepoints. Firstly, do not combine more than two of them - previously we were ending up with four codepoints in one cell which tmux believed to be width 2, but terminals considered width 4. Secondly, invalidate cursor position before redrawing the cell when the second codepoint is received, terminals vary in how they manage backspace and cursor movement across these characters, so it is better to use absolute rather than relative positioning. GitHub issue 4853. --- tty.c | 2 ++ utf8-combined.c | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/tty.c b/tty.c index e3c620be3..1d4e25df9 100644 --- a/tty.c +++ b/tty.c @@ -1979,6 +1979,8 @@ tty_cmd_cell(struct tty *tty, const struct tty_ctx *ctx) tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower); tty_margin_off(tty); + if (ctx->flags & TTY_CTX_CELL_INVALIDATE) + tty_invalidate(tty); tty_cursor_pane_unless_wrap(tty, ctx, ctx->ocx, ctx->ocy); tty_cell(tty, ctx->cell, &ctx->defaults, ctx->palette, diff --git a/utf8-combined.c b/utf8-combined.c index 65ecf9cdc..923342b43 100644 --- a/utf8-combined.c +++ b/utf8-combined.c @@ -82,6 +82,23 @@ utf8_is_hangul_filler(const struct utf8_data *ud) return (memcmp(ud->data, "\343\205\244", 3) == 0); } +/* Count regional indicator characters. */ +static u_int +utf8_regional_count(const struct utf8_data *ud) +{ + u_int count = 0, i; + + for (i = 0; i + 4 <= ud->size; i++) { + if (ud->data[i] == 0xf0 && + ud->data[i + 1] == 0x9f && + ud->data[i + 2] == 0x87 && + ud->data[i + 3] >= 0xa6 && + ud->data[i + 3] <= 0xbf) + count++; + } + return (count); +} + /* Should these two characters combine? */ int utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add) @@ -94,8 +111,13 @@ utf8_should_combine(const struct utf8_data *with, const struct utf8_data *add) return (0); /* Regional indicators. */ - if ((a >= 0x1F1E6 && a <= 0x1F1FF) && (w >= 0x1F1E6 && w <= 0x1F1FF)) + if ((a >= 0x1F1E6 && a <= 0x1F1FF) && (w >= 0x1F1E6 && w <= 0x1F1FF)) { + if (utf8_regional_count(with) != 1) + return (0); + if (utf8_regional_count(add) != 1) + return (0); return (1); + } /* Emoji skin tone modifiers. */ switch (a) { From 4a3f380fd25a34e79b058e97a156162d5ca6a8f9 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 9 Jun 2026 12:24:59 +0000 Subject: [PATCH 07/10] Add -f for filter with -a to kill-pane, kill-window, kill-session. GitHub issue 4782. --- cmd-kill-pane.c | 74 ++++++++++++++++++++++------ cmd-kill-session.c | 63 ++++++++++++++++++++---- cmd-kill-window.c | 117 +++++++++++++++++++++++++++++++-------------- tmux.1 | 21 ++++++++ 4 files changed, 215 insertions(+), 60 deletions(-) diff --git a/cmd-kill-pane.c b/cmd-kill-pane.c index fc3eb1b77..ed8336430 100644 --- a/cmd-kill-pane.c +++ b/cmd-kill-pane.c @@ -27,13 +27,17 @@ */ static enum cmd_retval cmd_kill_pane_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_kill_pane_all(struct cmdq_item *, const char *); +static int cmd_kill_pane_filter(struct cmdq_item *, + struct session *, struct winlink *, + struct window_pane *, const char *); const struct cmd_entry cmd_kill_pane_entry = { .name = "kill-pane", .alias = "killp", - .args = { "at:", 0, 0, NULL }, - .usage = "[-a] " CMD_TARGET_PANE_USAGE, + .args = { "af:t:", 0, 0, NULL }, + .usage = "[-a] [-f filter] " CMD_TARGET_PANE_USAGE, .target = { 't', CMD_FIND_PANE, 0 }, @@ -46,22 +50,19 @@ cmd_kill_pane_exec(struct cmd *self, struct cmdq_item *item) { struct args *args = cmd_get_args(self); struct cmd_find_state *target = cmdq_get_target(item); + struct session *s = target->s; struct winlink *wl = target->wl; - struct window_pane *loopwp, *tmpwp, *wp = target->wp; + struct window_pane *wp = target->wp; + const char *filter = args_get(args, 'f'); - if (args_has(args, 'a')) { - server_unzoom_window(wl->window); - TAILQ_FOREACH_SAFE(loopwp, &wl->window->panes, entry, tmpwp) { - if (loopwp == wp) - continue; - server_client_remove_pane(loopwp); - layout_close_pane(loopwp); - window_remove_pane(wl->window, loopwp); - } - server_redraw_window(wl->window); - return (CMD_RETURN_NORMAL); + if (filter != NULL && !args_has(args, 'a')) { + cmdq_error(item, "-f only valid with -a"); + return (CMD_RETURN_ERROR); } + if (args_has(args, 'a')) + return (cmd_kill_pane_all(item, filter)); + if (wp == NULL) { cmdq_error(item, "no active pane to kill"); return (CMD_RETURN_ERROR); @@ -69,3 +70,48 @@ cmd_kill_pane_exec(struct cmd *self, struct cmdq_item *item) server_kill_pane(wp); return (CMD_RETURN_NORMAL); } + +static enum cmd_retval +cmd_kill_pane_all(struct cmdq_item *item, const char *filter) +{ + struct cmd_find_state *target = cmdq_get_target(item); + struct session *s = target->s; + struct winlink *wl = target->wl; + struct window_pane *wp = target->wp; + struct window_pane *loopwp, *tmpwp; + + server_unzoom_window(wl->window); + TAILQ_FOREACH_SAFE(loopwp, &wl->window->panes, entry, tmpwp) { + if (loopwp == wp) + continue; + if (!cmd_kill_pane_filter(item, s, wl, loopwp, filter)) + continue; + server_client_remove_pane(loopwp); + layout_close_pane(loopwp); + window_remove_pane(wl->window, loopwp); + } + server_redraw_window(wl->window); + return (CMD_RETURN_NORMAL); +} + +static int +cmd_kill_pane_filter(struct cmdq_item *item, struct session *s, + struct winlink *wl, struct window_pane *wp, const char *filter) +{ + struct format_tree *ft; + char *expanded; + int flag; + + if (filter == NULL) + return (1); + + ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); + format_defaults(ft, NULL, s, wl, wp); + + expanded = format_expand(ft, filter); + flag = format_true(expanded); + free(expanded); + + format_free(ft); + return (flag); +} diff --git a/cmd-kill-session.c b/cmd-kill-session.c index 7d3ae9849..22c37b2c5 100644 --- a/cmd-kill-session.c +++ b/cmd-kill-session.c @@ -28,13 +28,16 @@ */ static enum cmd_retval cmd_kill_session_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_kill_session_all(struct cmdq_item *, const char *); +static int cmd_kill_session_filter(struct cmdq_item *, + struct session *, const char *); const struct cmd_entry cmd_kill_session_entry = { .name = "kill-session", .alias = NULL, - .args = { "aCgt:", 0, 0, NULL }, - .usage = "[-aCg] " CMD_TARGET_SESSION_USAGE, + .args = { "aCgf:t:", 0, 0, NULL }, + .usage = "[-aCg] [-f filter] " CMD_TARGET_SESSION_USAGE, .target = { 't', CMD_FIND_SESSION, 0 }, @@ -50,6 +53,12 @@ cmd_kill_session_exec(struct cmd *self, struct cmdq_item *item) struct session *s = target->s, *sloop, *stmp; struct session_group *sg; struct winlink *wl; + const char *filter = args_get(args, 'f'); + + if (filter != NULL && (!args_has(args, 'a') || args_has(args, 'C'))) { + cmdq_error(item, "-f only valid with -a"); + return (CMD_RETURN_ERROR); + } if (args_has(args, 'C')) { RB_FOREACH(wl, winlinks, &s->windows) { @@ -57,14 +66,9 @@ cmd_kill_session_exec(struct cmd *self, struct cmdq_item *item) wl->flags &= ~WINLINK_ALERTFLAGS; } server_redraw_session(s); - } else if (args_has(args, 'a')) { - RB_FOREACH_SAFE(sloop, sessions, &sessions, stmp) { - if (sloop != s) { - server_destroy_session(sloop); - session_destroy(sloop, 1, __func__); - } - } - } else if (args_has(args, 'g') && + } else if (args_has(args, 'a')) + return (cmd_kill_session_all(item, filter)); + else if (args_has(args, 'g') && (sg = session_group_contains(s)) != NULL) { TAILQ_FOREACH_SAFE(sloop, &sg->sessions, gentry, stmp) { server_destroy_session(sloop); @@ -76,3 +80,42 @@ cmd_kill_session_exec(struct cmd *self, struct cmdq_item *item) } return (CMD_RETURN_NORMAL); } + +static enum cmd_retval +cmd_kill_session_all(struct cmdq_item *item, const char *filter) +{ + struct session *s = cmdq_get_target(item)->s; + struct session *sloop, *stmp; + + RB_FOREACH_SAFE(sloop, sessions, &sessions, stmp) { + if (sloop == s) + continue; + if (!cmd_kill_session_filter(item, sloop, filter)) + continue; + server_destroy_session(sloop); + session_destroy(sloop, 1, __func__); + } + return (CMD_RETURN_NORMAL); +} + +static int +cmd_kill_session_filter(struct cmdq_item *item, struct session *s, + const char *filter) +{ + struct format_tree *ft; + char *expanded; + int flag; + + if (filter == NULL) + return (1); + + ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); + format_defaults(ft, NULL, s, NULL, NULL); + + expanded = format_expand(ft, filter); + flag = format_true(expanded); + free(expanded); + + format_free(ft); + return (flag); +} diff --git a/cmd-kill-window.c b/cmd-kill-window.c index f5ff05f84..4353c1c1f 100644 --- a/cmd-kill-window.c +++ b/cmd-kill-window.c @@ -25,13 +25,16 @@ */ static enum cmd_retval cmd_kill_window_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_kill_window_all(struct cmdq_item *, const char *); +static int cmd_kill_window_filter(struct cmdq_item *, + struct session *, struct winlink *, const char *); const struct cmd_entry cmd_kill_window_entry = { .name = "kill-window", .alias = "killw", - .args = { "at:", 0, 0, NULL }, - .usage = "[-a] " CMD_TARGET_WINDOW_USAGE, + .args = { "af:t:", 0, 0, NULL }, + .usage = "[-a] [-f filter] " CMD_TARGET_WINDOW_USAGE, .target = { 't', CMD_FIND_WINDOW, 0 }, @@ -57,10 +60,15 @@ cmd_kill_window_exec(struct cmd *self, struct cmdq_item *item) { struct args *args = cmd_get_args(self); struct cmd_find_state *target = cmdq_get_target(item); - struct winlink *wl = target->wl, *loop; + struct winlink *wl = target->wl; struct window *w = wl->window; struct session *s = target->s; - u_int found; + const char *filter = args_get(args, 'f'); + + if (filter != NULL && !args_has(args, 'a')) { + cmdq_error(item, "-f only valid with -a"); + return (CMD_RETURN_ERROR); + } if (cmd_get_entry(self) == &cmd_unlink_window_entry) { if (!args_has(args, 'k') && !session_is_linked(s, w)) { @@ -72,39 +80,76 @@ cmd_kill_window_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } - if (args_has(args, 'a')) { - if (RB_PREV(winlinks, &s->windows, wl) == NULL && - RB_NEXT(winlinks, &s->windows, wl) == NULL) - return (CMD_RETURN_NORMAL); - - /* Kill all windows except the current one. */ - do { - found = 0; - RB_FOREACH(loop, winlinks, &s->windows) { - if (loop->window != wl->window) { - server_kill_window(loop->window, 0); - found++; - break; - } - } - } while (found != 0); - - /* - * If the current window appears in the session more than once, - * kill it as well. - */ - found = 0; - RB_FOREACH(loop, winlinks, &s->windows) { - if (loop->window == wl->window) - found++; - } - if (found > 1) - server_kill_window(wl->window, 0); - - server_renumber_all(); - return (CMD_RETURN_NORMAL); - } + if (args_has(args, 'a')) + return (cmd_kill_window_all(item, filter)); server_kill_window(wl->window, 1); return (CMD_RETURN_NORMAL); } + +static enum cmd_retval +cmd_kill_window_all(struct cmdq_item *item, const char *filter) +{ + struct cmd_find_state *target = cmdq_get_target(item); + struct session *s = target->s; + struct winlink *wl = target->wl; + struct winlink *loop; + u_int found, kill_current; + + if (RB_PREV(winlinks, &s->windows, wl) == NULL && + RB_NEXT(winlinks, &s->windows, wl) == NULL) + return (CMD_RETURN_NORMAL); + + /* Kill all windows except the current one. */ + do { + found = 0; + RB_FOREACH(loop, winlinks, &s->windows) { + if (loop->window != wl->window && + cmd_kill_window_filter(item, s, loop, filter)) { + server_kill_window(loop->window, 0); + found++; + break; + } + } + } while (found != 0); + + /* + * If the current window appears in the session more than once, kill it + * as well if it matches the filter. + */ + found = kill_current = 0; + RB_FOREACH(loop, winlinks, &s->windows) { + if (loop->window == wl->window) { + found++; + if (cmd_kill_window_filter(item, s, loop, filter)) + kill_current = 1; + } + } + if (kill_current && found > 1) + server_kill_window(wl->window, 0); + + server_renumber_all(); + return (CMD_RETURN_NORMAL); +} + +static int +cmd_kill_window_filter(struct cmdq_item *item, struct session *s, + struct winlink *wl, const char *filter) +{ + struct format_tree *ft; + char *expanded; + int flag; + + if (filter == NULL) + return (1); + + ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); + format_defaults(ft, NULL, s, wl, NULL); + + expanded = format_expand(ft, filter); + flag = format_true(expanded); + free(expanded); + + format_free(ft); + return (flag); +} diff --git a/tmux.1 b/tmux.1 index d2163335e..c4236526c 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1203,6 +1203,7 @@ Kill the server and clients and destroy all sessions. .It Xo Ic kill\-session .Op Fl aCg +.Op Fl f Ar filter .Op Fl t Ar target\-session .Xc Destroy the given session, closing any windows linked to it and no other @@ -1210,6 +1211,12 @@ sessions, and detaching all clients attached to it. If .Fl a is given, all sessions but the specified one is killed. +When +.Fl a +is given, +.Fl f +specifies a filter. +Only sessions for which the filter is true are killed. If .Fl g is given and the session is in a session group, all sessions in the group are @@ -3115,6 +3122,7 @@ the marked pane is used rather than the current pane. .Tg killp .It Xo Ic kill\-pane .Op Fl a +.Op Fl f Ar filter .Op Fl t Ar target\-pane .Xc .D1 Pq alias: Ic killp @@ -3124,9 +3132,16 @@ The .Fl a option kills all but the pane given with .Fl t . +When +.Fl a +is given, +.Fl f +specifies a filter. +Only panes for which the filter is true are killed. .Tg killw .It Xo Ic kill\-window .Op Fl a +.Op Fl f Ar filter .Op Fl t Ar target\-window .Xc .D1 Pq alias: Ic killw @@ -3137,6 +3152,12 @@ The .Fl a option kills all but the window given with .Fl t . +When +.Fl a +is given, +.Fl f +specifies a filter. +Only windows for which the filter is true are killed. .Tg lastp .It Xo Ic last\-pane .Op Fl deZ From 538688486eb5cafab9d14723b7abe5655322da55 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 9 Jun 2026 12:51:57 +0000 Subject: [PATCH 08/10] Clarify -a for kill-window, kill-session. --- tmux.1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tmux.1 b/tmux.1 index c4236526c..f6b24fde8 100644 --- a/tmux.1 +++ b/tmux.1 @@ -3130,7 +3130,7 @@ Destroy the given pane. If no panes remain in the containing window, it is also destroyed. The .Fl a -option kills all but the pane given with +option kills all panes in the window except the pane given with .Fl t . When .Fl a @@ -3150,7 +3150,7 @@ Kill the current window or the window at removing it from any sessions to which it is linked. The .Fl a -option kills all but the window given with +option kills all windows in the session except the window given with .Fl t . When .Fl a From b3748687500e94f7480df79ce09010a330b7a7b1 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 9 Jun 2026 12:57:40 +0000 Subject: [PATCH 09/10] Add missing headers. --- cmd-kill-session.c | 2 ++ cmd-kill-window.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/cmd-kill-session.c b/cmd-kill-session.c index 22c37b2c5..4bf36d785 100644 --- a/cmd-kill-session.c +++ b/cmd-kill-session.c @@ -18,6 +18,8 @@ #include +#include + #include "tmux.h" /* diff --git a/cmd-kill-window.c b/cmd-kill-window.c index 4353c1c1f..0253605f1 100644 --- a/cmd-kill-window.c +++ b/cmd-kill-window.c @@ -18,6 +18,8 @@ #include +#include + #include "tmux.h" /* From 34dd42dc7fe550b555d46340f32466f9ade0bfa9 Mon Sep 17 00:00:00 2001 From: nicm Date: Tue, 9 Jun 2026 12:58:40 +0000 Subject: [PATCH 10/10] Use name as marker for failure not type (which must be set to be in the error). --- cmd-server-access.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd-server-access.c b/cmd-server-access.c index 4ec61af3c..895e69417 100644 --- a/cmd-server-access.c +++ b/cmd-server-access.c @@ -63,7 +63,7 @@ cmd_server_access_exec(struct cmd *self, struct cmdq_item *item) struct args *args = cmd_get_args(self); struct client *c = cmdq_get_target_client(item); char *arg; - const char *name, *type = NULL; + const char *name = NULL, *type; struct passwd *pw; struct group *gr; id_t id; @@ -80,20 +80,20 @@ cmd_server_access_exec(struct cmd *self, struct cmdq_item *item) arg = format_single(item, args_string(args, 0), c, NULL, NULL, NULL); if (args_has(args, 'g')) { + type = "group"; if ((gr = getgrnam(arg)) != NULL) { - type = "group"; id = gr->gr_gid; name = gr->gr_name; flags |= SERVER_ACL_IS_GROUP; } } else { + type = "user"; if ((pw = getpwnam(arg)) != NULL) { - type = "user"; id = pw->pw_uid; name = pw->pw_name; } } - if (type == NULL) { + if (name == NULL) { cmdq_error(item, "unknown %s: %s", type, arg); free(arg); return (CMD_RETURN_ERROR);