From d29aa12117a7348f04d60350f8f2794b7087e016 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 13:38:45 +0000 Subject: [PATCH 01/47] Replace the notification system with events. Events can carry a payload of additional payload (to reduce problems with lifetime of objects) and are delivered to one or more event sinks. This is more powerful and reduces the complex dependencies between control mode and hooks. Events are now used for hooks, control mode notifications and for monitors (set-hook -B). wait-for can now wait for an event to fire (-E flag, with -F to for filter), with -v to print the payload, as well as listing (-l) waiting clients on an event and forcing one to wake (-w). A few additional hooks are also now available (pane-created, pane-resized, etc) and some of the existing ones have additional format variables available. --- Makefile | 6 +- alerts.c | 8 +- arguments.c | 2 +- attributes.c | 2 +- cfg.c | 2 +- client.c | 2 +- cmd-attach-session.c | 4 +- cmd-bind-key.c | 2 +- cmd-break-pane.c | 19 +- cmd-capture-pane.c | 2 +- cmd-choose-tree.c | 2 +- cmd-command-prompt.c | 2 +- cmd-confirm-before.c | 2 +- cmd-copy-mode.c | 2 +- cmd-detach-client.c | 2 +- cmd-display-menu.c | 2 +- cmd-display-message.c | 2 +- cmd-display-panes.c | 2 +- cmd-find-window.c | 2 +- cmd-find.c | 2 +- cmd-has-session.c | 46 --- cmd-if-shell.c | 2 +- cmd-join-pane.c | 14 +- cmd-kill-pane.c | 2 +- cmd-kill-server.c | 2 +- cmd-kill-session.c | 2 +- cmd-kill-window.c | 2 +- cmd-list-buffers.c | 2 +- cmd-list-clients.c | 2 +- cmd-list-commands.c | 2 +- cmd-list-keys.c | 2 +- cmd-list-panes.c | 2 +- cmd-list-sessions.c | 2 +- cmd-list-windows.c | 2 +- cmd-list.c | 103 ------ cmd-load-buffer.c | 2 +- cmd-lock-server.c | 2 +- cmd-move-window.c | 2 +- cmd-new-session.c | 4 +- cmd-new-window.c | 2 +- cmd-parse.y | 2 +- cmd-paste-buffer.c | 2 +- cmd-pipe-pane.c | 2 +- cmd-queue.c | 70 +--- cmd-refresh-client.c | 2 +- cmd-rename-session.c | 13 +- cmd-rename-window.c | 2 +- cmd-resize-pane.c | 4 +- cmd-resize-window.c | 2 +- cmd-respawn-pane.c | 2 +- cmd-respawn-window.c | 2 +- cmd-rotate-window.c | 2 +- cmd-run-shell.c | 2 +- cmd-save-buffer.c | 2 +- cmd-select-layout.c | 4 +- cmd-select-pane.c | 111 +++++-- cmd-select-window.c | 2 +- cmd-send-keys.c | 2 +- cmd-send-prefix.c | 57 ---- cmd-server-access.c | 2 +- cmd-set-buffer.c | 2 +- cmd-set-environment.c | 2 +- cmd-set-option.c | 68 +++- cmd-show-buffer.c | 112 ------- cmd-show-environment.c | 2 +- cmd-show-messages.c | 2 +- cmd-show-options.c | 4 +- cmd-show-prompt-history.c | 2 +- cmd-source-file.c | 2 +- cmd-split-window.c | 11 +- cmd-swap-pane.c | 6 +- cmd-swap-window.c | 2 +- cmd-switch-client.c | 2 +- cmd-unbind-key.c | 2 +- cmd-wait-for.c | 261 ++++++++++++++- cmd.c | 2 +- colour.c | 2 +- control-notify.c | 214 ++++++++---- control.c | 2 +- environ.c | 2 +- events-payload.c | 672 ++++++++++++++++++++++++++++++++++++++ events.c | 199 +++++++++++ file.c | 2 +- format-draw.c | 2 +- format.c | 2 +- fuzzy.c | 2 +- grid-reader.c | 2 +- grid-view.c | 2 +- grid.c | 2 +- hooks.c | 461 ++++++++++++++++++++++++++ hyperlinks.c | 2 +- input-keys.c | 2 +- input.c | 27 +- job.c | 2 +- key-bindings.c | 2 +- key-string.c | 2 +- layout-custom.c | 4 +- layout-set.c | 14 +- layout.c | 6 +- log.c | 2 +- menu.c | 2 +- mode-key.c | 261 --------------- mode-tree.c | 2 +- monitor.c | 4 +- names.c | 2 +- notify.c | 516 ----------------------------- options-table.c | 11 +- options.c | 4 +- paste.c | 24 +- popup.c | 2 +- proc.c | 2 +- procname.c | 2 +- prompt-history.c | 2 +- prompt.c | 2 +- regsub.c | 2 +- resize.c | 6 +- screen-redraw.c | 2 +- screen-write.c | 2 +- screen.c | 2 +- server-acl.c | 2 +- server-client.c | 14 +- server-fn.c | 36 +- server.c | 4 +- session.c | 16 +- sort.c | 2 +- spawn.c | 49 ++- status.c | 2 +- style.c | 2 +- tmux-protocol.h | 2 +- tmux.1 | 101 +++++- tmux.c | 2 +- tmux.h | 126 +++++-- tty-acs.c | 2 +- tty-draw.c | 2 +- tty-features.c | 2 +- tty-keys.c | 6 +- tty-term.c | 2 +- tty.c | 2 +- utf8-combined.c | 2 +- utf8.c | 2 +- window-border.c | 2 +- window-buffer.c | 2 +- window-client.c | 2 +- window-clock.c | 2 +- window-copy.c | 6 +- window-customize.c | 2 +- window-switch.c | 2 +- window-tree.c | 2 +- window-visible.c | 2 +- window.c | 158 +++++++-- xmalloc.c | 2 +- xmalloc.h | 2 +- xterm-keys.c | 252 -------------- 153 files changed, 2543 insertions(+), 1787 deletions(-) delete mode 100644 cmd-has-session.c delete mode 100644 cmd-list.c delete mode 100644 cmd-send-prefix.c delete mode 100644 cmd-show-buffer.c create mode 100644 events-payload.c create mode 100644 events.c create mode 100644 hooks.c delete mode 100644 mode-key.c delete mode 100644 notify.c delete mode 100644 xterm-keys.c diff --git a/Makefile b/Makefile index 165d0d47c..d73312013 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -# $OpenBSD$ +# $OpenBSD: Makefile,v 1.120 2026/07/10 13:38:45 nicm Exp $ PROG= tmux SRCS= alerts.c \ @@ -76,6 +76,8 @@ SRCS= alerts.c \ control-notify.c \ control.c \ environ.c \ + events.c \ + events-payload.c \ file.c \ format.c \ fuzzy.c \ @@ -83,6 +85,7 @@ SRCS= alerts.c \ grid-reader.c \ grid-view.c \ grid.c \ + hooks.c \ hyperlinks.c \ input-keys.c \ input.c \ @@ -97,7 +100,6 @@ SRCS= alerts.c \ mode-tree.c \ monitor.c \ names.c \ - notify.c \ options-table.c \ options.c \ paste.c \ diff --git a/alerts.c b/alerts.c index a2b4d6541..824fb7381 100644 --- a/alerts.c +++ b/alerts.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: alerts.c,v 1.35 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott @@ -206,7 +206,7 @@ alerts_check_bell(struct window *w) } if (!alerts_action_applies(wl, "bell-action")) continue; - notify_winlink("alert-bell", wl); + events_fire_winlink("alert-bell", wl); if (s->flags & SESSION_ALERTED) continue; @@ -242,7 +242,7 @@ alerts_check_activity(struct window *w) } if (!alerts_action_applies(wl, "activity-action")) continue; - notify_winlink("alert-activity", wl); + events_fire_winlink("alert-activity", wl); if (s->flags & SESSION_ALERTED) continue; @@ -278,7 +278,7 @@ alerts_check_silence(struct window *w) } if (!alerts_action_applies(wl, "silence-action")) continue; - notify_winlink("alert-silence", wl); + events_fire_winlink("alert-silence", wl); if (s->flags & SESSION_ALERTED) continue; diff --git a/arguments.c b/arguments.c index cab40aa6e..baafd6d52 100644 --- a/arguments.c +++ b/arguments.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: arguments.c,v 1.66 2026/06/26 09:54:56 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott diff --git a/attributes.c b/attributes.c index b0c9ef518..d853b8db0 100644 --- a/attributes.c +++ b/attributes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: attributes.c,v 1.13 2026/07/02 08:51:05 nicm Exp $ */ /* * Copyright (c) 2009 Joshua Elsasser diff --git a/cfg.c b/cfg.c index 450ab88fb..7a79a6025 100644 --- a/cfg.c +++ b/cfg.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cfg.c,v 1.89 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/client.c b/client.c index fcc49c395..1b7fdb043 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: client.c,v 1.165 2025/04/25 12:25:32 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-attach-session.c b/cmd-attach-session.c index 32b1e0cbd..050c7ca38 100644 --- a/cmd-attach-session.c +++ b/cmd-attach-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-attach-session.c,v 1.91 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -163,7 +163,7 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag, if (~c->flags & CLIENT_CONTROL) proc_send(c->peer, MSG_READY, -1, NULL, 0); - notify_client("client-attached", c); + events_fire_client("client-attached", c); c->flags |= CLIENT_ATTACHED; } diff --git a/cmd-bind-key.c b/cmd-bind-key.c index 01f8e9614..0d7636ff7 100644 --- a/cmd-bind-key.c +++ b/cmd-bind-key.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-bind-key.c,v 1.47 2025/04/09 07:03:04 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 3891534a0..8d85c57b2 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-break-pane.c,v 1.72 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -80,7 +80,7 @@ cmd_break_pane_float(struct cmdq_item *item, struct args *args, window_set_active_pane(w, wp, 1); layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -99,7 +99,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) struct session *dst_s = target->s; struct window_pane *wp = source->wp; struct window *w = wl->window; - char *newname, *cause, *cp; + char *cause, *cp; int idx = target->idx, before; const char *template, *name = args_get(args, 'n'); @@ -151,6 +151,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) layout_close_pane(wp); w = wp->window = window_create(w->sx, w->sy, w->xpixel, w->ypixel); + window_add_ref(w, __func__); options_set_parent(wp->options, w->options); wp->flags |= (PANE_STYLECHANGED|PANE_THEMECHANGED); TAILQ_INSERT_HEAD(&w->panes, wp, entry); @@ -158,12 +159,11 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) w->active = wp; w->latest = tc; - if (name == NULL) { - newname = default_window_name(w); - window_set_name(w, newname, 0); - free(newname); - } else { - window_set_name(w, name, 0); + free(w->name); + if (name == NULL) + w->name = default_window_name(w); + else { + w->name = clean_name(name, 0); options_set_number(w->options, "automatic-rename", 0); } @@ -174,6 +174,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) if (idx == -1) idx = -1 - options_get_number(dst_s->options, "base-index"); wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ + window_remove_ref(w, __func__); if (!args_has(args, 'd')) { session_select(dst_s, wl->idx); cmd_find_from_session(current, dst_s, 0); diff --git a/cmd-capture-pane.c b/cmd-capture-pane.c index febf29697..d83590f90 100644 --- a/cmd-capture-pane.c +++ b/cmd-capture-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-capture-pane.c,v 1.67 2026/07/02 10:34:14 nicm Exp $ */ /* * Copyright (c) 2009 Jonathan Alvarado diff --git a/cmd-choose-tree.c b/cmd-choose-tree.c index 4a6bd152b..f9ce7d3b7 100644 --- a/cmd-choose-tree.c +++ b/cmd-choose-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-choose-tree.c,v 1.57 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2012 Thomas Adam diff --git a/cmd-command-prompt.c b/cmd-command-prompt.c index 045ef0b12..ac1117347 100644 --- a/cmd-command-prompt.c +++ b/cmd-command-prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-command-prompt.c,v 1.75 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-confirm-before.c b/cmd-confirm-before.c index 54bc9a2a1..fda3af772 100644 --- a/cmd-confirm-before.c +++ b/cmd-confirm-before.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-confirm-before.c,v 1.61 2026/06/24 10:55:39 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-copy-mode.c b/cmd-copy-mode.c index cfa60f59f..051cb58a2 100644 --- a/cmd-copy-mode.c +++ b/cmd-copy-mode.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-copy-mode.c,v 1.53 2026/07/08 08:07:42 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-detach-client.c b/cmd-detach-client.c index b5a4cf1c1..82ce9a00b 100644 --- a/cmd-detach-client.c +++ b/cmd-detach-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-detach-client.c,v 1.39 2026/05/22 15:22:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-display-menu.c b/cmd-display-menu.c index 97973b8bd..c2d83ff27 100644 --- a/cmd-display-menu.c +++ b/cmd-display-menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-menu.c,v 1.51 2026/07/07 09:45:09 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/cmd-display-message.c b/cmd-display-message.c index 3138d9223..f786006c3 100644 --- a/cmd-display-message.c +++ b/cmd-display-message.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-message.c,v 1.65 2026/02/23 08:46:57 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-display-panes.c b/cmd-display-panes.c index f8991dfe1..176238a1f 100644 --- a/cmd-display-panes.c +++ b/cmd-display-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-display-panes.c,v 1.56 2026/06/25 16:32:42 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-find-window.c b/cmd-find-window.c index 560998370..28a8949cd 100644 --- a/cmd-find-window.c +++ b/cmd-find-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-find-window.c,v 1.56 2023/12/27 20:42:01 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-find.c b/cmd-find.c index 7ce714583..5173591f9 100644 --- a/cmd-find.c +++ b/cmd-find.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-find.c,v 1.86 2026/07/02 08:47:25 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott diff --git a/cmd-has-session.c b/cmd-has-session.c deleted file mode 100644 index a873b2046..000000000 --- a/cmd-has-session.c +++ /dev/null @@ -1,46 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2007 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include "tmux.h" - -/* - * Cause client to report an error and exit with 1 if session doesn't exist. - */ - -enum cmd_retval cmd_has_session_exec(struct cmd *, struct cmd_q *); - -const struct cmd_entry cmd_has_session_entry = { - "has-session", "has", - "t:", 0, 0, - CMD_TARGET_SESSION_USAGE, - 0, - cmd_has_session_exec -}; - -enum cmd_retval -cmd_has_session_exec(struct cmd *self, struct cmd_q *cmdq) -{ - struct args *args = self->args; - - if (cmd_find_session(cmdq, args_get(args, 't'), 0) == NULL) - return (CMD_RETURN_ERROR); - - return (CMD_RETURN_NORMAL); -} diff --git a/cmd-if-shell.c b/cmd-if-shell.c index f0c3637cc..49b92a996 100644 --- a/cmd-if-shell.c +++ b/cmd-if-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-if-shell.c,v 1.86 2025/08/01 09:05:51 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 122505d81..1b290a01e 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-join-pane.c,v 1.69 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -187,7 +187,7 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl, lc->g.yoff = yoff; layout_fix_panes(w, NULL); } - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -257,7 +257,7 @@ cmd_join_pane_move(struct cmdq_item *item, struct args *args, lc->g.xoff = xoff; lc->g.yoff = yoff; layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -359,7 +359,7 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl, else TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -398,7 +398,7 @@ cmd_join_pane_tile(struct cmdq_item *item, struct args *args, struct window *w, window_set_active_pane(w, wp, 1); layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); @@ -502,8 +502,8 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) if (window_count_panes(src_w, 1) == 0) server_kill_window(src_w, 1); else - notify_window("window-layout-changed", src_w); - notify_window("window-layout-changed", dst_w); + events_fire_window("window-layout-changed", src_w); + events_fire_window("window-layout-changed", dst_w); return (CMD_RETURN_NORMAL); } diff --git a/cmd-kill-pane.c b/cmd-kill-pane.c index 836551f01..f634a7cd7 100644 --- a/cmd-kill-pane.c +++ b/cmd-kill-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-pane.c,v 1.34 2026/06/09 21:22:22 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-kill-server.c b/cmd-kill-server.c index 7bb79e3d4..8b70be3d8 100644 --- a/cmd-kill-server.c +++ b/cmd-kill-server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-server.c,v 1.20 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-kill-session.c b/cmd-kill-session.c index 4bf36d785..7edd2a30a 100644 --- a/cmd-kill-session.c +++ b/cmd-kill-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-session.c,v 1.31 2026/06/09 12:57:40 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-kill-window.c b/cmd-kill-window.c index 0253605f1..e7a6ec32e 100644 --- a/cmd-kill-window.c +++ b/cmd-kill-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-kill-window.c,v 1.30 2026/06/09 12:57:40 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-buffers.c b/cmd-list-buffers.c index 347c69341..82ee58947 100644 --- a/cmd-list-buffers.c +++ b/cmd-list-buffers.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-buffers.c,v 1.41 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-clients.c b/cmd-list-clients.c index ae235d83c..77f3912ca 100644 --- a/cmd-list-clients.c +++ b/cmd-list-clients.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-clients.c,v 1.42 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-commands.c b/cmd-list-commands.c index 0b0f912d8..5e9c4430c 100644 --- a/cmd-list-commands.c +++ b/cmd-list-commands.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-commands.c,v 1.15 2026/02/24 08:22:13 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-keys.c b/cmd-list-keys.c index 7aa38c61e..e8617c2e3 100644 --- a/cmd-list-keys.c +++ b/cmd-list-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-keys.c,v 1.78 2026/07/01 13:12:17 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-panes.c b/cmd-list-panes.c index 094ca850c..54c8db7d3 100644 --- a/cmd-list-panes.c +++ b/cmd-list-panes.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-panes.c,v 1.40 2026/06/01 14:01:09 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-list-sessions.c b/cmd-list-sessions.c index 79a1ab620..07a622979 100644 --- a/cmd-list-sessions.c +++ b/cmd-list-sessions.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-sessions.c,v 1.36 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list-windows.c b/cmd-list-windows.c index 927976398..40f8c2db5 100644 --- a/cmd-list-windows.c +++ b/cmd-list-windows.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-list-windows.c,v 1.50 2026/02/27 08:25:12 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-list.c b/cmd-list.c deleted file mode 100644 index 82ffe55c0..000000000 --- a/cmd-list.c +++ /dev/null @@ -1,103 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2009 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include -#include - -#include "tmux.h" - -static u_int cmd_list_next_group = 1; - -struct cmd_list * -cmd_list_new(void) -{ - struct cmd_list *cmdlist; - - cmdlist = xcalloc(1, sizeof *cmdlist); - cmdlist->references = 1; - cmdlist->group = cmd_list_next_group++; - TAILQ_INIT(&cmdlist->list); - return (cmdlist); -} - -void -cmd_list_append(struct cmd_list *cmdlist, struct cmd *cmd) -{ - cmd->group = cmdlist->group; - TAILQ_INSERT_TAIL(&cmdlist->list, cmd, qentry); -} - -void -cmd_list_move(struct cmd_list *cmdlist, struct cmd_list *from) -{ - struct cmd *cmd, *cmd1; - - TAILQ_FOREACH_SAFE(cmd, &from->list, qentry, cmd1) { - TAILQ_REMOVE(&from->list, cmd, qentry); - TAILQ_INSERT_TAIL(&cmdlist->list, cmd, qentry); - } - cmdlist->group = cmd_list_next_group++; -} - -void -cmd_list_free(struct cmd_list *cmdlist) -{ - struct cmd *cmd, *cmd1; - - if (--cmdlist->references != 0) - return; - - TAILQ_FOREACH_SAFE(cmd, &cmdlist->list, qentry, cmd1) { - TAILQ_REMOVE(&cmdlist->list, cmd, qentry); - cmd_free(cmd); - } - - free(cmdlist); -} - -char * -cmd_list_print(struct cmd_list *cmdlist, int escaped) -{ - struct cmd *cmd; - char *buf, *this; - size_t len; - - len = 1; - buf = xcalloc(1, len); - - TAILQ_FOREACH(cmd, &cmdlist->list, qentry) { - this = cmd_print(cmd); - - len += strlen(this) + 4; - buf = xrealloc(buf, len); - - strlcat(buf, this, len); - if (TAILQ_NEXT(cmd, qentry) != NULL) { - if (escaped) - strlcat(buf, " \\; ", len); - else - strlcat(buf, " ; ", len); - } - - free(this); - } - - return (buf); -} diff --git a/cmd-load-buffer.c b/cmd-load-buffer.c index 82ac2fd47..df35b82c1 100644 --- a/cmd-load-buffer.c +++ b/cmd-load-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-load-buffer.c,v 1.66 2025/10/28 07:32:26 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-lock-server.c b/cmd-lock-server.c index bd61dcffd..1feac03f8 100644 --- a/cmd-lock-server.c +++ b/cmd-lock-server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-lock-server.c,v 1.31 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-move-window.c b/cmd-move-window.c index 4b90e70f8..1cd4f10a8 100644 --- a/cmd-move-window.c +++ b/cmd-move-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-move-window.c,v 1.34 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-new-session.c b/cmd-new-session.c index ba02c72d4..56d4f9779 100644 --- a/cmd-new-session.c +++ b/cmd-new-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-new-session.c,v 1.152 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -325,7 +325,7 @@ cmd_new_session_exec(struct cmd *self, struct cmdq_item *item) session_group_synchronize_to(s); session_select(s, RB_MIN(winlinks, &s->windows)->idx); } - notify_session("session-created", s); + events_fire_session("session-created", s); /* * Set the client to the new session. If a command client exists, it is diff --git a/cmd-new-window.c b/cmd-new-window.c index a54aaaa20..d1e2ab4e5 100644 --- a/cmd-new-window.c +++ b/cmd-new-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-new-window.c,v 1.103 2026/07/08 08:07:42 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-parse.y b/cmd-parse.y index 0b32f5d34..29b7780e0 100644 --- a/cmd-parse.y +++ b/cmd-parse.y @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-parse.y,v 1.58 2026/04/27 12:31:11 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/cmd-paste-buffer.c b/cmd-paste-buffer.c index 03fcb0d40..fbe598b04 100644 --- a/cmd-paste-buffer.c +++ b/cmd-paste-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-paste-buffer.c,v 1.44 2026/03/04 07:19:32 tb Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-pipe-pane.c b/cmd-pipe-pane.c index 13fe679e3..f18added6 100644 --- a/cmd-pipe-pane.c +++ b/cmd-pipe-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-pipe-pane.c,v 1.63 2026/04/28 08:47:55 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-queue.c b/cmd-queue.c index a6dfc5925..463d82b3f 100644 --- a/cmd-queue.c +++ b/cmd-queue.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-queue.c,v 1.120 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -351,74 +351,53 @@ cmdq_insert_after(struct cmdq_item *after, struct cmdq_item *item) /* Insert a hook. */ void -cmdq_insert_hook(struct session *s, struct cmdq_item *item, +cmdq_insert_hook(__unused struct session *s, struct cmdq_item *item, struct cmd_find_state *current, const char *fmt, ...) { - struct cmdq_state *state = item->state; struct cmd *cmd = item->cmd; struct args *args = cmd_get_args(cmd); struct args_entry *ae; struct args_value *av; - struct options *oo; + struct event_payload *ep; va_list ap; char *name, tmp[32], flag, *arguments; u_int i; const char *value; - struct cmdq_item *new_item; - struct cmdq_state *new_state; - struct options_entry *o; - struct options_array_item *a; - struct cmd_list *cmdlist; if (item->state->flags & CMDQ_STATE_NOHOOKS) return; - if (s == NULL) - oo = global_s_options; - else - oo = s->options; va_start(ap, fmt); xvasprintf(&name, fmt, ap); va_end(ap); - o = options_get(oo, name); - if (o == NULL) { - free(name); - return; - } - log_debug("running hook %s (parent %p)", name, item); - - /* - * The hooks get a new state because they should not update the current - * target or formats for any subsequent commands. - */ - new_state = cmdq_new_state(current, &state->event, CMDQ_STATE_NOHOOKS); - cmdq_add_format(new_state, "hook", "%s", name); + ep = event_payload_create(); + if (current != NULL) + event_payload_set_target(ep, current); + event_payload_set_pointer(ep, "_cmdq_item", item, NULL, NULL); arguments = args_print(args); - cmdq_add_format(new_state, "hook_arguments", "%s", arguments); + event_payload_set_string(ep, "arguments", "%s", arguments); free(arguments); for (i = 0; i < args_count(args); i++) { - xsnprintf(tmp, sizeof tmp, "hook_argument_%d", i); - cmdq_add_format(new_state, tmp, "%s", args_string(args, i)); + xsnprintf(tmp, sizeof tmp, "argument_%u", i); + event_payload_set_string(ep, tmp, "%s", args_string(args, i)); } flag = args_first(args, &ae); while (flag != 0) { value = args_get(args, flag); - if (value == NULL) { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c", flag); - cmdq_add_format(new_state, tmp, "1"); - } else { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c", flag); - cmdq_add_format(new_state, tmp, "%s", value); - } + xsnprintf(tmp, sizeof tmp, "flag_%c", flag); + if (value == NULL) + event_payload_set_string(ep, tmp, "1"); + else + event_payload_set_string(ep, tmp, "%s", value); i = 0; av = args_first_value(args, flag); while (av != NULL) { - xsnprintf(tmp, sizeof tmp, "hook_flag_%c_%d", flag, i); - cmdq_add_format(new_state, tmp, "%s", av->string); + xsnprintf(tmp, sizeof tmp, "flag_%c_%u", flag, i); + event_payload_set_string(ep, tmp, "%s", av->string); i++; av = args_next_value(av); } @@ -426,20 +405,7 @@ cmdq_insert_hook(struct session *s, struct cmdq_item *item, flag = args_next(&ae); } - a = options_array_first(o); - while (a != NULL) { - cmdlist = options_array_item_value(a)->cmdlist; - if (cmdlist != NULL) { - new_item = cmdq_get_command(cmdlist, new_state); - if (item != NULL) - item = cmdq_insert_after(item, new_item); - else - item = cmdq_append(NULL, new_item); - } - a = options_array_next(a); - } - - cmdq_free_state(new_state); + events_fire(name, ep); free(name); } diff --git a/cmd-refresh-client.c b/cmd-refresh-client.c index 6f22da83e..da2a27976 100644 --- a/cmd-refresh-client.c +++ b/cmd-refresh-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-refresh-client.c,v 1.54 2026/07/05 08:24:00 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-rename-session.c b/cmd-rename-session.c index b399483b8..35719c564 100644 --- a/cmd-rename-session.c +++ b/cmd-rename-session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rename-session.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,6 +49,8 @@ cmd_rename_session_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 event_payload *ep; + struct cmd_find_state fs; char *newname, *tmp; tmp = format_single_from_target(item, args_string(args, 0)); @@ -69,13 +71,20 @@ cmd_rename_session_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } + ep = event_payload_create(); + cmd_find_from_session(&fs, s, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", s); + event_payload_set_string(ep, "old_name", "%s", s->name); + event_payload_set_string(ep, "new_name", "%s", newname); + RB_REMOVE(sessions, &sessions, s); free(s->name); s->name = newname; RB_INSERT(sessions, &sessions, s); server_status_session(s); - notify_session("session-renamed", s); + events_fire("session-renamed", ep); return (CMD_RETURN_NORMAL); } diff --git a/cmd-rename-window.c b/cmd-rename-window.c index 8ab9b658a..f55f8a18f 100644 --- a/cmd-rename-window.c +++ b/cmd-rename-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rename-window.c,v 1.30 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-resize-pane.c b/cmd-resize-pane.c index 3ce805115..01ddf424a 100644 --- a/cmd-resize-pane.c +++ b/cmd-resize-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-resize-pane.c,v 1.67 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -182,7 +182,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item) if (lc->parent != NULL) layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); return (CMD_RETURN_NORMAL); diff --git a/cmd-resize-window.c b/cmd-resize-window.c index c420451cf..a89750607 100644 --- a/cmd-resize-window.c +++ b/cmd-resize-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-resize-window.c,v 1.10 2023/06/30 13:19:32 nicm Exp $ */ /* * Copyright (c) 2018 Nicholas Marriott diff --git a/cmd-respawn-pane.c b/cmd-respawn-pane.c index bf058a267..66a5a0be4 100644 --- a/cmd-respawn-pane.c +++ b/cmd-respawn-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-respawn-pane.c,v 1.40 2026/07/03 16:09:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-respawn-window.c b/cmd-respawn-window.c index e67757e1b..e10469649 100644 --- a/cmd-respawn-window.c +++ b/cmd-respawn-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-respawn-window.c,v 1.51 2026/07/03 16:09:49 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-rotate-window.c b/cmd-rotate-window.c index c7459a9ae..d62fd0fa4 100644 --- a/cmd-rotate-window.c +++ b/cmd-rotate-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-rotate-window.c,v 1.34 2026/06/22 08:47:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-run-shell.c b/cmd-run-shell.c index 6114f68a1..b440828fb 100644 --- a/cmd-run-shell.c +++ b/cmd-run-shell.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-run-shell.c,v 1.91 2026/06/01 08:27:37 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-save-buffer.c b/cmd-save-buffer.c index d4266231f..903016c5d 100644 --- a/cmd-save-buffer.c +++ b/cmd-save-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-save-buffer.c,v 1.57 2025/10/28 07:32:26 nicm Exp $ */ /* * Copyright (c) 2009 Tiago Cunha diff --git a/cmd-select-layout.c b/cmd-select-layout.c index 417797472..741dc63f4 100644 --- a/cmd-select-layout.c +++ b/cmd-select-layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-layout.c,v 1.43 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -139,7 +139,7 @@ changed: free(oldlayout); recalculate_sizes(); server_redraw_window(w); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (CMD_RETURN_NORMAL); error: diff --git a/cmd-select-pane.c b/cmd-select-pane.c index d146ea6cf..91a36cddb 100644 --- a/cmd-select-pane.c +++ b/cmd-select-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-pane.c,v 1.76 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -80,6 +80,69 @@ cmd_select_pane_redraw(struct window *w) } } +static enum cmd_retval +cmd_select_pane_marked_pane(struct cmd *self, struct cmdq_item *item) +{ + struct args *args = cmd_get_args(self); + struct cmd_find_state *target = cmdq_get_target(item); + struct event_payload *ep; + struct cmd_find_state fs; + struct winlink *wl = target->wl; + struct window_pane *wp = target->wp, *lwp = NULL, *mwp; + struct session *s = target->s; + + if (args_has(args, 'm') && !window_pane_is_visible(wp)) + return (CMD_RETURN_NORMAL); + if (server_check_marked()) + lwp = marked_pane.wp; + + if (args_has(args, 'M') || server_is_marked(s, wl, wp)) + server_clear_marked(); + else + server_set_marked(s, wl, wp); + mwp = marked_pane.wp; + + ep = event_payload_create(); + if (mwp != NULL) + cmd_find_from_pane(&fs, mwp, 0); + else if (lwp != NULL) + cmd_find_from_pane(&fs, lwp, 0); + else + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + if (mwp != NULL) { + event_payload_set_pane(ep, "pane", mwp); + event_payload_set_pane(ep, "new_pane", mwp); + event_payload_set_window(ep, "window", mwp->window); + } else if (lwp != NULL) { + event_payload_set_pane(ep, "pane", lwp); + event_payload_set_window(ep, "window", lwp->window); + } else { + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + } + if (lwp != NULL) + event_payload_set_pane(ep, "old_pane", lwp); + event_payload_set_int(ep, "marked", mwp != NULL); + events_fire("marked-pane-changed", ep); + + if (lwp != NULL) { + lwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED|PANE_THEMECHANGED); + server_redraw_window_borders(lwp->window); + server_status_window(lwp->window); + } + if (mwp != NULL) { + mwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED|PANE_THEMECHANGED); + server_redraw_window_borders(mwp->window); + server_status_window(mwp->window); + } + if (window_pane_is_floating(wp)) { + window_redraw_active_switch(wp->window, wp); + window_set_active_pane(wp->window, wp, 1); + } + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) { @@ -88,10 +151,12 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) struct cmd_find_state *current = cmdq_get_current(item); struct cmd_find_state *target = cmdq_get_target(item); struct client *c = cmdq_get_client(item); + struct event_payload *ep; + struct cmd_find_state fs; struct winlink *wl = target->wl; struct window *w = wl->window; struct session *s = target->s; - struct window_pane *wp = target->wp, *activewp, *lastwp, *markedwp; + struct window_pane *wp = target->wp, *activewp, *lastwp; struct options *oo = wp->options; char *title; const char *style; @@ -134,38 +199,8 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_NORMAL); } - if (args_has(args, 'm') || args_has(args, 'M')) { - if (args_has(args, 'm') && !window_pane_is_visible(wp)) - return (CMD_RETURN_NORMAL); - if (server_check_marked()) - lastwp = marked_pane.wp; - else - lastwp = NULL; - - if (args_has(args, 'M') || server_is_marked(s, wl, wp)) - server_clear_marked(); - else - server_set_marked(s, wl, wp); - markedwp = marked_pane.wp; - - if (lastwp != NULL) { - lastwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED| - PANE_THEMECHANGED); - server_redraw_window_borders(lastwp->window); - server_status_window(lastwp->window); - } - if (markedwp != NULL) { - markedwp->flags |= (PANE_REDRAW|PANE_STYLECHANGED| - PANE_THEMECHANGED); - server_redraw_window_borders(markedwp->window); - server_status_window(markedwp->window); - } - if (window_pane_is_floating(wp)) { - window_redraw_active_switch(w, wp); - window_set_active_pane(w, wp, 1); - } - return (CMD_RETURN_NORMAL); - } + if (args_has(args, 'm') || args_has(args, 'M')) + return (cmd_select_pane_marked_pane(self, item)); style = args_get(args, 'P'); if (style != NULL) { @@ -218,7 +253,13 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'T')) { title = format_single_from_target(item, args_get(args, 'T')); if (screen_set_title(&wp->base, title, 0)) { - notify_pane("pane-title-changed", wp); + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); server_redraw_window_borders(wp->window); server_status_window(wp->window); } diff --git a/cmd-select-window.c b/cmd-select-window.c index 4aca3e60e..32b4821bf 100644 --- a/cmd-select-window.c +++ b/cmd-select-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-select-window.c,v 1.30 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-send-keys.c b/cmd-send-keys.c index 6be3f2ead..06043bddb 100644 --- a/cmd-send-keys.c +++ b/cmd-send-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-send-keys.c,v 1.81 2026/06/11 19:13:34 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/cmd-send-prefix.c b/cmd-send-prefix.c deleted file mode 100644 index 04556507b..000000000 --- a/cmd-send-prefix.c +++ /dev/null @@ -1,57 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2007 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include "tmux.h" - -/* - * Send prefix key as a key. - */ - -enum cmd_retval cmd_send_prefix_exec(struct cmd *, struct cmd_ctx *); - -const struct cmd_entry cmd_send_prefix_entry = { - "send-prefix", NULL, - "2t:", 0, 0, - "[-2] " CMD_TARGET_PANE_USAGE, - 0, - NULL, - NULL, - cmd_send_prefix_exec -}; - -enum cmd_retval -cmd_send_prefix_exec(struct cmd *self, struct cmd_ctx *ctx) -{ - struct args *args = self->args; - struct session *s; - struct window_pane *wp; - int key; - - if (cmd_find_pane(ctx, args_get(args, 't'), &s, &wp) == NULL) - return (CMD_RETURN_ERROR); - - if (args_has(args, '2')) - key = options_get_number(&s->options, "prefix2"); - else - key = options_get_number(&s->options, "prefix"); - window_pane_key(wp, s, key); - - return (CMD_RETURN_NORMAL); -} diff --git a/cmd-server-access.c b/cmd-server-access.c index 895e69417..e85a278a8 100644 --- a/cmd-server-access.c +++ b/cmd-server-access.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-server-access.c,v 1.6 2026/06/09 12:58:40 nicm Exp $ */ /* * Copyright (c) 2021 Dallas Lyons diff --git a/cmd-set-buffer.c b/cmd-set-buffer.c index eef2978e2..6e640fec6 100644 --- a/cmd-set-buffer.c +++ b/cmd-set-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-set-environment.c b/cmd-set-environment.c index 78c32452b..9a6552cb5 100644 --- a/cmd-set-environment.c +++ b/cmd-set-environment.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-environment.c,v 1.29 2025/04/09 06:27:43 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-set-option.c b/cmd-set-option.c index 1b6de1e86..158cd5fac 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-option.c,v 1.145 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -31,6 +31,8 @@ static enum args_parse_type cmd_set_option_args_parse(struct args *, u_int, char **); static enum cmd_retval cmd_set_option_exec(struct cmd *, struct cmdq_item *); +static enum cmd_retval cmd_set_hook_event_exec(struct cmd *, + struct cmdq_item *); static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *, struct args *, int); @@ -64,8 +66,8 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpRt:uB:w", 0, 2, cmd_set_option_args_parse }, - .usage = "[-agpRuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + .args = { "agpERt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpERuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -85,12 +87,58 @@ cmd_set_option_args_parse(struct args *args, u_int idx, return (ARGS_PARSE_STRING); } +static enum cmd_retval +cmd_set_hook_event_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 event_payload *ep; + struct client *c; + char *argument; + + if (args_count(args) == 0) { + cmdq_error(item, "missing argument"); + return (CMD_RETURN_ERROR); + } + if (args_count(args) != 1) { + cmdq_error(item, "too many arguments"); + return (CMD_RETURN_ERROR); + } + + argument = format_single_from_target(item, args_string(args, 0)); + if (*argument != '@') { + cmdq_error(item, "event name must start with @"); + free(argument); + return (CMD_RETURN_ERROR); + } + + ep = event_payload_create(); + event_payload_set_target(ep, target); + c = cmdq_get_client(item); + if (c != NULL) + event_payload_set_client(ep, "client", c); + if (target->s != NULL) + event_payload_set_session(ep, "session", target->s); + if (target->w != NULL) + event_payload_set_window(ep, "window", target->w); + if (target->wl != NULL) + event_payload_set_int(ep, "window_index", target->wl->idx); + else if (target->idx != -1) + event_payload_set_int(ep, "window_index", target->idx); + if (target->wp != NULL) + event_payload_set_pane(ep, "pane", target->wp); + events_fire(argument, ep); + free(argument); + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) { struct cmd_find_state *target = cmdq_get_target(item), fs; struct options *oo; struct options_entry *o; + struct session *s = NULL; char *cause = NULL, *name = NULL, *format = NULL; char *expanded = NULL, *newvalue = NULL; const char *value, *old; @@ -130,7 +178,7 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) cmd_find_copy_state(&fs, target); if (args_has(args, 'u')) { - notify_monitor_remove(oo, name); + hooks_monitor_remove(oo, name); goto out; } @@ -152,7 +200,11 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) } } - notify_monitor_add(item, oo, name, type, id, format, &fs, target->s); + if (oo != global_options && + oo != global_s_options && + oo != global_w_options) + s = target->s; + hooks_monitor_add(item, oo, name, type, id, format, &fs, s); out: free(newvalue); @@ -185,6 +237,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) int scope; window = (cmd_get_entry(self) == &cmd_set_window_option_entry); + if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'E')) + return (cmd_set_hook_event_exec(self, item)); if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'B')) return (cmd_set_hook_monitor_exec(item, args, window)); if (args_count(args) == 0) { @@ -197,7 +251,7 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) /* If set-hook -R, fire the hook straight away. */ if (cmd_get_entry(self) == &cmd_set_hook_entry && args_has(args, 'R')) { - notify_hook(item, argument); + hooks_run(item, argument); free(argument); return (CMD_RETURN_NORMAL); } @@ -289,6 +343,8 @@ cmd_set_option_exec(struct cmd *self, struct cmdq_item *item) goto fail; } options_set_string(oo, name, append, "%s", value); + if (cmd_get_entry(self) == &cmd_set_hook_entry) + hooks_add_event(name); } else if (array_key == NULL && !options_is_array(parent)) { error = options_from_string(oo, options_table_entry(parent), options_table_entry(parent)->name, value, diff --git a/cmd-show-buffer.c b/cmd-show-buffer.c deleted file mode 100644 index 0af82972c..000000000 --- a/cmd-show-buffer.c +++ /dev/null @@ -1,112 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2007 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include -#include - -#include "tmux.h" - -/* - * Show a paste buffer. - */ - -enum cmd_retval cmd_show_buffer_exec(struct cmd *, struct cmd_ctx *); - -const struct cmd_entry cmd_show_buffer_entry = { - "show-buffer", "showb", - "b:", 0, 0, - CMD_BUFFER_USAGE, - 0, - NULL, - NULL, - cmd_show_buffer_exec -}; - -enum cmd_retval -cmd_show_buffer_exec(struct cmd *self, struct cmd_ctx *ctx) -{ - struct args *args = self->args; - struct session *s; - struct paste_buffer *pb; - int buffer; - char *in, *buf, *ptr, *cause; - size_t size, len; - u_int width; - - if ((s = cmd_find_session(ctx, NULL, 0)) == NULL) - return (CMD_RETURN_ERROR); - - if (!args_has(args, 'b')) { - if ((pb = paste_get_top(&global_buffers)) == NULL) { - ctx->error(ctx, "no buffers"); - return (CMD_RETURN_ERROR); - } - } else { - buffer = args_strtonum(args, 'b', 0, INT_MAX, &cause); - if (cause != NULL) { - ctx->error(ctx, "buffer %s", cause); - free(cause); - return (CMD_RETURN_ERROR); - } - - pb = paste_get_index(&global_buffers, buffer); - if (pb == NULL) { - ctx->error(ctx, "no buffer %d", buffer); - return (CMD_RETURN_ERROR); - } - } - - size = pb->size; - if (size > SIZE_MAX / 4 - 1) - size = SIZE_MAX / 4 - 1; - in = xmalloc(size * 4 + 1); - strvisx(in, pb->data, size, VIS_OCTAL|VIS_TAB); - - width = s->sx; - if (ctx->cmdclient != NULL) - width = ctx->cmdclient->tty.sx; - - buf = xmalloc(width + 1); - len = 0; - - ptr = in; - do { - buf[len++] = *ptr++; - - if (len == width || buf[len - 1] == '\n') { - if (buf[len - 1] == '\n') - len--; - buf[len] = '\0'; - - ctx->print(ctx, "%s", buf); - len = 0; - } - } while (*ptr != '\0'); - - if (len != 0) { - buf[len] = '\0'; - ctx->print(ctx, "%s", buf); - } - free(buf); - - free(in); - - return (CMD_RETURN_NORMAL); -} diff --git a/cmd-show-environment.c b/cmd-show-environment.c index 3440c33e4..692f09c32 100644 --- a/cmd-show-environment.c +++ b/cmd-show-environment.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-environment.c,v 1.29 2025/04/09 06:27:43 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-show-messages.c b/cmd-show-messages.c index 7c656fa99..f9e198c4c 100644 --- a/cmd-show-messages.c +++ b/cmd-show-messages.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-messages.c,v 1.37 2025/11/18 08:37:54 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/cmd-show-options.c b/cmd-show-options.c index bff0634fe..308912e00 100644 --- a/cmd-show-options.c +++ b/cmd-show-options.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-options.c,v 1.73 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -216,7 +216,7 @@ cmd_show_hooks_print_monitor(struct cmdq_item *item, struct options_entry *o) { char *value; - value = notify_monitor_to_string(o); + value = hooks_monitor_to_string(o); if (value == NULL) return; cmdq_print(item, "%s", value); diff --git a/cmd-show-prompt-history.c b/cmd-show-prompt-history.c index 741d22588..5b6af76fb 100644 --- a/cmd-show-prompt-history.c +++ b/cmd-show-prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-show-prompt-history.c,v 1.5 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2021 Anindya Mukherjee diff --git a/cmd-source-file.c b/cmd-source-file.c index cbd53960b..e5dfba543 100644 --- a/cmd-source-file.c +++ b/cmd-source-file.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */ /* * Copyright (c) 2008 Tiago Cunha diff --git a/cmd-split-window.c b/cmd-split-window.c index 09cfbfa86..64fa6b877 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-split-window.c,v 1.141 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -84,6 +84,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) struct window *w = wl->window; struct window_pane *wp = target->wp, *new_wp = NULL; struct layout_cell *lc = NULL; + struct event_payload *ep; struct cmd_find_state fs; int input, empty, is_floating, flags = 0; const char *template, *style, *value; @@ -218,7 +219,13 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (args_has(args, 'T')) { title = format_single_from_target(item, args_get(args, 'T')); screen_set_title(&new_wp->base, title, 0); - notify_pane("pane-title-changed", new_wp); + ep = event_payload_create(); + cmd_find_from_pane(&fs, new_wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", new_wp); + event_payload_set_window(ep, "window", new_wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); free(title); } diff --git a/cmd-swap-pane.c b/cmd-swap-pane.c index 7f5160e65..da7ea3706 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-swap-pane.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -180,9 +180,9 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) redraw_invalidate_scene(dst_w); server_redraw_window(dst_w); - notify_window("window-layout-changed", src_w); + events_fire_window("window-layout-changed", src_w); if (src_w != dst_w) - notify_window("window-layout-changed", dst_w); + events_fire_window("window-layout-changed", dst_w); out: if (window_pop_zoom(src_w)) diff --git a/cmd-swap-window.c b/cmd-swap-window.c index d31353982..980da0395 100644 --- a/cmd-swap-window.c +++ b/cmd-swap-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-swap-window.c,v 1.29 2025/10/30 13:52:08 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-switch-client.c b/cmd-switch-client.c index 9865ac52c..177107b47 100644 --- a/cmd-switch-client.c +++ b/cmd-switch-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-unbind-key.c b/cmd-unbind-key.c index 6d91d7cca..f537fb576 100644 --- a/cmd-unbind-key.c +++ b/cmd-unbind-key.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-unbind-key.c,v 1.34 2021/08/21 10:22:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/cmd-wait-for.c b/cmd-wait-for.c index 8a6aa259e..0e4130fcd 100644 --- a/cmd-wait-for.c +++ b/cmd-wait-for.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-wait-for.c,v 1.23 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2013 Nicholas Marriott @@ -34,8 +34,8 @@ const struct cmd_entry cmd_wait_for_entry = { .name = "wait-for", .alias = "wait", - .args = { "LSU", 1, 1, NULL }, - .usage = "[-L|-S|-U] channel", + .args = { "EF:LSUlvw:", 1, 1, NULL }, + .usage = "[-ELSUlv] [-F format] [-w waiter] name", .flags = 0, .exec = cmd_wait_for_exec @@ -46,6 +46,17 @@ struct wait_item { TAILQ_ENTRY(wait_item) entry; }; +struct wait_event_item { + struct cmdq_item *item; + struct events_sink *sink; + char *name; + char *filter; + int verbose; + TAILQ_ENTRY(wait_event_item) entry; +}; +static TAILQ_HEAD(, wait_event_item) wait_event_items = + TAILQ_HEAD_INITIALIZER(wait_event_items); + struct wait_channel { const char *name; int locked; @@ -76,9 +87,28 @@ static enum cmd_retval cmd_wait_for_lock(struct cmdq_item *, const char *, struct wait_channel *); static enum cmd_retval cmd_wait_for_unlock(struct cmdq_item *, const char *, struct wait_channel *); +static enum cmd_retval cmd_wait_for_event(struct cmdq_item *, const char *, + struct args *); +static void cmd_wait_for_event_cb(const char *, + struct event_payload *, void *); +static void cmd_wait_for_event_free(struct wait_event_item *); +static enum cmd_retval cmd_wait_for_event_list(struct cmdq_item *, + const char *); +static enum cmd_retval cmd_wait_for_event_wake(struct cmdq_item *, + const char *, struct args *); +static enum cmd_retval cmd_wait_for_list(struct cmdq_item *, + struct wait_channel *); +static enum cmd_retval cmd_wait_for_wake(struct cmdq_item *, const char *, + struct args *, struct wait_channel *); static struct wait_channel *cmd_wait_for_add(const char *); static void cmd_wait_for_remove(struct wait_channel *); +static void cmd_wait_for_remove_empty( + struct wait_channel *); +static const char *cmd_wait_for_item_client_name( + struct cmdq_item *); +static const char *cmd_wait_for_client_name( + struct wait_event_item *); static struct wait_channel * cmd_wait_for_add(const char *name) @@ -117,16 +147,53 @@ cmd_wait_for_remove(struct wait_channel *wc) free(wc); } +static void +cmd_wait_for_remove_empty(struct wait_channel *wc) +{ + if (wc->locked || wc->woken) + return; + if (!TAILQ_EMPTY(&wc->waiters) || !TAILQ_EMPTY(&wc->lockers)) + return; + + log_debug("remove empty wait channel %s", wc->name); + + RB_REMOVE(wait_channels, &wait_channels, wc); + + free((void *)wc->name); + free(wc); +} + +static const char * +cmd_wait_for_item_client_name(struct cmdq_item *item) +{ + struct client *c = cmdq_get_client(item); + + if (c == NULL || c->name == NULL) + return (""); + return (c->name); +} + +static const char * +cmd_wait_for_client_name(struct wait_event_item *wei) +{ + return (cmd_wait_for_item_client_name(wei->item)); +} + static enum cmd_retval cmd_wait_for_exec(struct cmd *self, struct cmdq_item *item) { - struct args *args = cmd_get_args(self); + struct args *args = cmd_get_args(self); const char *name = args_string(args, 0); - struct wait_channel *wc, find; + struct wait_channel *wc, find = { .name = name }; + + if (args_has(args, 'E')) + return (cmd_wait_for_event(item, name, args)); - find.name = name; wc = RB_FIND(wait_channels, &wait_channels, &find); - + if (args_has(args, 'l')) + return (cmd_wait_for_list(item, wc)); + if (args_has(args, 'w')) + return (cmd_wait_for_wake(item, name, args, wc)); if (args_has(args, 'S')) return (cmd_wait_for_signal(item, name, wc)); if (args_has(args, 'L')) @@ -136,6 +203,179 @@ cmd_wait_for_exec(struct cmd *self, struct cmdq_item *item) return (cmd_wait_for_wait(item, name, wc)); } +static void +cmd_wait_for_event_print(struct wait_event_item *wei, struct event_payload *ep) +{ + struct event_payload_item *epi; + const char *key; + char *value; + + epi = event_payload_first(ep); + while (epi != NULL) { + key = event_payload_item_name(epi); + if (*key != '_') { + value = event_payload_item_print(epi); + cmdq_print(wei->item, "%s=%s", key, value); + free(value); + } + epi = event_payload_next(epi); + } +} + +static void +cmd_wait_for_event_cb(__unused const char *name, struct event_payload *ep, + void *item_data) +{ + struct wait_event_item *wei = item_data; + struct format_tree *ft; + char *expanded; + int flag; + + if (wei->verbose) + cmd_wait_for_event_print(wei, ep); + + if (wei->filter != NULL) { + ft = format_create(cmdq_get_client(wei->item), wei->item, + FORMAT_NONE, FORMAT_NOJOBS); + event_payload_add_formats(ep, ft, NULL); + expanded = format_expand(ft, wei->filter); + flag = format_true(expanded); + free(expanded); + format_free(ft); + + if (!flag) + return; + } + + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); +} + +static void +cmd_wait_for_event_free(struct wait_event_item *wei) +{ + events_remove_sink(wei->sink); + free(wei->name); + free(wei->filter); + free(wei); +} + +static enum cmd_retval +cmd_wait_for_event(struct cmdq_item *item, const char *name, struct args *args) +{ + struct wait_event_item *wei; + const char *filter = args_get(args, 'F'); + + if (!hooks_valid_event_name(name)) { + cmdq_error(item, "invalid event: %s", name); + return (CMD_RETURN_ERROR); + } + if (args_has(args, 'l')) + return (cmd_wait_for_event_list(item, name)); + if (args_has(args, 'w')) + return (cmd_wait_for_event_wake(item, name, args)); + + if (cmdq_get_client(item) == NULL) { + cmdq_error(item, "not able to wait"); + return (CMD_RETURN_ERROR); + } + + wei = xcalloc(1, sizeof *wei); + wei->item = item; + wei->name = xstrdup(name); + wei->filter = (filter != NULL ? xstrdup(filter) : NULL); + wei->verbose = args_has(args, 'v'); + wei->sink = events_add_sink(name, cmd_wait_for_event_cb, wei); + TAILQ_INSERT_TAIL(&wait_event_items, wei, entry); + + return (CMD_RETURN_WAIT); +} + +static enum cmd_retval +cmd_wait_for_event_list(struct cmdq_item *item, const char *name) +{ + struct wait_event_item *wei; + + TAILQ_FOREACH(wei, &wait_event_items, entry) { + if (strcmp(wei->name, name) == 0) + cmdq_print(item, "%s", cmd_wait_for_client_name(wei)); + } + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_wait_for_event_wake(struct cmdq_item *item, const char *name, + struct args *args) +{ + struct wait_event_item *wei, *wei1; + const char *client_name = args_get(args, 'w'); + + TAILQ_FOREACH_SAFE(wei, &wait_event_items, entry, wei1) { + if (strcmp(wei->name, name) != 0) + continue; + if (strcmp(cmd_wait_for_client_name(wei), client_name) != 0) + continue; + + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); + return (CMD_RETURN_NORMAL); + } + + cmdq_error(item, "waiter %s not found", client_name); + return (CMD_RETURN_ERROR); +} + +static enum cmd_retval +cmd_wait_for_list(struct cmdq_item *item, struct wait_channel *wc) +{ + struct wait_item *wi; + + if (wc == NULL) + return (CMD_RETURN_NORMAL); + + TAILQ_FOREACH(wi, &wc->waiters, entry) + cmdq_print(item, "%s", cmd_wait_for_item_client_name(wi->item)); + TAILQ_FOREACH(wi, &wc->lockers, entry) + cmdq_print(item, "%s", cmd_wait_for_item_client_name(wi->item)); + + return (CMD_RETURN_NORMAL); +} + +static enum cmd_retval +cmd_wait_for_wake(__unused struct cmdq_item *item, const char *name, + struct args *args, struct wait_channel *wc) +{ + struct wait_item *wi, *wi1; + const char *client_name = args_get(args, 'w'); + + if (wc != NULL) { + TAILQ_FOREACH_SAFE(wi, &wc->waiters, entry, wi1) { + name = cmd_wait_for_item_client_name(wi->item); + if (strcmp(name, client_name) != 0) + continue; + cmdq_continue(wi->item); + TAILQ_REMOVE(&wc->waiters, wi, entry); + free(wi); + cmd_wait_for_remove_empty(wc); + return (CMD_RETURN_NORMAL); + } + TAILQ_FOREACH_SAFE(wi, &wc->lockers, entry, wi1) { + name = cmd_wait_for_item_client_name(wi->item); + if (strcmp(name, client_name) != 0) + continue; + cmdq_continue(wi->item); + TAILQ_REMOVE(&wc->lockers, wi, entry); + free(wi); + cmd_wait_for_remove_empty(wc); + return (CMD_RETURN_NORMAL); + } + } + return (CMD_RETURN_NORMAL); +} + static enum cmd_retval cmd_wait_for_signal(__unused struct cmdq_item *item, const char *name, struct wait_channel *wc) @@ -245,6 +485,13 @@ cmd_wait_for_flush(void) { struct wait_channel *wc, *wc1; struct wait_item *wi, *wi1; + struct wait_event_item *wei, *wei1; + + TAILQ_FOREACH_SAFE(wei, &wait_event_items, entry, wei1) { + TAILQ_REMOVE(&wait_event_items, wei, entry); + cmdq_continue(wei->item); + cmd_wait_for_event_free(wei); + } RB_FOREACH_SAFE(wc, wait_channels, &wait_channels, wc1) { TAILQ_FOREACH_SAFE(wi, &wc->waiters, entry, wi1) { diff --git a/cmd.c b/cmd.c index a2d3fad70..b9b8f1a4a 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd.c,v 1.185 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/colour.c b/colour.c index f563fd5aa..411d77921 100644 --- a/colour.c +++ b/colour.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: colour.c,v 1.35 2026/07/06 14:29:10 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/control-notify.c b/control-notify.c index e85660297..6fec1683f 100644 --- a/control-notify.c +++ b/control-notify.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: control-notify.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -23,32 +23,56 @@ #include "tmux.h" +/* Should this client be sent events? */ #define CONTROL_SHOULD_NOTIFY_CLIENT(c) \ - ((c) != NULL && ((c)->flags & CLIENT_CONTROL) && \ + ((c) != NULL && \ + ((c)->flags & CLIENT_CONTROL) && \ (~(c)->flags & CLIENT_EXIT) && \ (c)->control_state != NULL) -void -control_notify_pane_mode_changed(int pane) +/* Notify control clients that pane mode changed. */ +static void +control_pane_mode_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; + struct window_pane *wp; + struct client *c; + char *value; - TAILQ_FOREACH(c, &clients, entry) { - if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) - continue; - - control_write(c, "%%pane-mode-changed %%%u", pane); + wp = event_payload_get_pane(ep, "pane"); + if (wp != NULL) { + TAILQ_FOREACH(c, &clients, entry) { + if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) + continue; + control_write(c, "%%pane-mode-changed %%%u", wp->id); + } + return; } + + value = event_payload_print(ep, "pane"); + if (value == NULL) + return; + TAILQ_FOREACH(c, &clients, entry) { + if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) + control_write(c, "%%pane-mode-changed %s", value); + } + free(value); } -void -control_notify_window_layout_changed(struct window *w) +/* Notify control clients that window layout changed. */ +static void +control_window_layout_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; - struct session *s; - struct winlink *wl; - const char *template; - char *cp; + struct client *c; + struct session *s; + struct winlink *wl; + struct window *w = event_payload_get_window(ep, "window"); + const char *template; + char *cp; + + if (w == NULL) + return; template = "%layout-change #{window_id} #{window_layout} " "#{window_visible_layout} #{window_raw_flags}"; @@ -73,12 +97,15 @@ control_notify_window_layout_changed(struct window *w) free(cp); } -void -control_notify_window_pane_changed(struct window *w) +/* Notify control clients that window pane changed. */ +static void +control_window_pane_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { struct client *c; + struct window *w = event_payload_get_window(ep, "window"); - if (w->active == NULL) + if (w == NULL || w->active == NULL) return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) @@ -89,12 +116,17 @@ control_notify_window_pane_changed(struct window *w) } } -void -control_notify_window_unlinked(__unused struct session *s, struct window *w) +/* Notify control clients that a window was unlinked. */ +static void +control_window_unlinked_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -107,12 +139,17 @@ control_notify_window_unlinked(__unused struct session *s, struct window *w) } } -void -control_notify_window_linked(__unused struct session *s, struct window *w) +/* Notify control clients that a window was linked. */ +static void +control_window_linked_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -125,12 +162,17 @@ control_notify_window_linked(__unused struct session *s, struct window *w) } } -void -control_notify_window_renamed(struct window *w) +/* Notify control clients that a window was renamed. */ +static void +control_window_renamed_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; - struct session *cs; + struct client *c; + struct session *cs; + struct window *w = event_payload_get_window(ep, "window"); + if (w == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) continue; @@ -146,13 +188,16 @@ control_notify_window_renamed(struct window *w) } } -void -control_notify_client_session_changed(struct client *cc) +/* Notify control clients that a client changed session. */ +static void +control_client_session_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { - struct client *c; - struct session *s; + struct client *cc = event_payload_get_client(ep, "client"); + struct client *c; + struct session *s; - if (cc->session == NULL) + if (cc == NULL || cc->session == NULL) return; s = cc->session; @@ -170,22 +215,32 @@ control_notify_client_session_changed(struct client *cc) } } -void -control_notify_client_detached(struct client *cc) +/* Notify control clients that a client detached. */ +static void +control_client_detached_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; + struct client *cc = event_payload_get_client(ep, "client"); + struct client *c; + if (cc == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (CONTROL_SHOULD_NOTIFY_CLIENT(c)) control_write(c, "%%client-detached %s", cc->name); } } -void -control_notify_session_renamed(struct session *s) +/* Notify control clients that a session was renamed. */ +static void +control_session_renamed_cb(__unused const char *name, struct event_payload *ep, + __unused void *sink_data) { - struct client *c; + struct session *s = event_payload_get_session(ep, "session"); + struct client *c; + if (s == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; @@ -194,8 +249,10 @@ control_notify_session_renamed(struct session *s) } } -void -control_notify_session_created(__unused struct session *s) +/* Notify control clients that sessions changed. */ +static void +control_session_created_cb(__unused const char *name, + __unused struct event_payload *ep, __unused void *sink_data) { struct client *c; @@ -207,8 +264,10 @@ control_notify_session_created(__unused struct session *s) } } -void -control_notify_session_closed(__unused struct session *s) +/* Notify control clients that sessions changed. */ +static void +control_session_closed_cb(__unused const char *name, + __unused struct event_payload *ep, __unused void *sink_data) { struct client *c; @@ -220,9 +279,12 @@ control_notify_session_closed(__unused struct session *s) } } -void -control_notify_session_window_changed(struct session *s) +/* Notify control clients that the current window changed. */ +static void +control_session_window_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + struct session *s = event_payload_get_session(ep, "session"); struct client *c; /* @@ -230,7 +292,7 @@ control_notify_session_window_changed(struct session *s) * session has been destroyed (which sets curw to NULL) but is kept * alive by the notification's reference. Skip the notification. */ - if (s->curw == NULL) + if (s == NULL || s->curw == NULL) return; TAILQ_FOREACH(c, &clients, entry) { @@ -242,28 +304,68 @@ control_notify_session_window_changed(struct session *s) } } -void -control_notify_paste_buffer_changed(const char *name) +/* Notify control clients that a paste buffer changed. */ +static void +control_paste_buffer_changed_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + const char *pbname = event_payload_get_string(ep, "paste_buffer"); struct client *c; + if (pbname == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%paste-buffer-changed %s", name); + control_write(c, "%%paste-buffer-changed %s", pbname); } } -void -control_notify_paste_buffer_deleted(const char *name) +/* Notify control clients that a paste buffer was deleted. */ +static void +control_paste_buffer_deleted_cb(__unused const char *name, + struct event_payload *ep, __unused void *sink_data) { + const char *pbname = event_payload_get_string(ep, "paste_buffer"); struct client *c; + if (pbname == NULL) + return; TAILQ_FOREACH(c, &clients, entry) { if (!CONTROL_SHOULD_NOTIFY_CLIENT(c)) continue; - control_write(c, "%%paste-buffer-deleted %s", name); + control_write(c, "%%paste-buffer-deleted %s", pbname); } } + +/* Build control event sinks. */ +void +control_build_events(void) +{ + /* Control event sink callbacks. */ + static struct { + const char *name; + events_cb cb; + } events[] = { + { "pane-mode-changed", control_pane_mode_changed_cb }, + { "window-layout-changed", control_window_layout_changed_cb }, + { "window-pane-changed", control_window_pane_changed_cb }, + { "window-unlinked", control_window_unlinked_cb }, + { "window-linked", control_window_linked_cb }, + { "window-renamed", control_window_renamed_cb }, + { "client-session-changed", control_client_session_changed_cb }, + { "client-detached", control_client_detached_cb }, + { "session-renamed", control_session_renamed_cb }, + { "session-created", control_session_created_cb }, + { "session-closed", control_session_closed_cb }, + { "session-window-changed", control_session_window_changed_cb }, + { "paste-buffer-changed", control_paste_buffer_changed_cb }, + { "paste-buffer-deleted", control_paste_buffer_deleted_cb } + }; + u_int i; + + for (i = 0; i < nitems(events); i++) + events_add_sink(events[i].name, events[i].cb, NULL); +} diff --git a/control.c b/control.c index c1cc79e25..220267b6c 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: control.c,v 1.60 2026/07/10 07:25:05 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott diff --git a/environ.c b/environ.c index f9bc8cab3..e4f94fd4b 100644 --- a/environ.c +++ b/environ.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: environ.c,v 1.31 2026/06/13 20:07:30 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/events-payload.c b/events-payload.c new file mode 100644 index 000000000..444836cf3 --- /dev/null +++ b/events-payload.c @@ -0,0 +1,672 @@ +/* $OpenBSD: events-payload.c,v 1.1 2026/07/10 13:38:45 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include +#include +#include +#include + +#include "tmux.h" + +/* Event payload item. */ +struct event_payload_item { + char *name; + enum event_payload_type type; + + union { + char *string; + time_t time; + int number; + u_int unsigned_number; + struct client *client; + struct session *session; + struct window *window; + struct window_pane *pane; + struct { + void *ptr; + event_payload_free_cb free_cb; + event_payload_print_cb print_cb; + } pointer; + }; + + RB_ENTRY(event_payload_item) entry; +}; + +RB_HEAD(event_payload_tree, event_payload_item); + +struct event_payload { + struct event_payload_tree items; + struct cmd_find_state target; +}; + +static int +event_payload_cmp(struct event_payload_item *epi1, + struct event_payload_item *epi2) +{ + return (strcmp(epi1->name, epi2->name)); +} +RB_GENERATE_STATIC(event_payload_tree, event_payload_item, entry, + event_payload_cmp); + +/* Find an item. */ +static struct event_payload_item * +event_payload_find(struct event_payload *ep, const char *name) +{ + struct event_payload_item find = { .name = (char *)name }; + + return (RB_FIND(event_payload_tree, &ep->items, &find)); +} + +/* Free the target in a payload. */ +static void +event_payload_free_target(struct event_payload *ep) +{ + struct cmd_find_state *target = &ep->target; + + if (target->s != NULL) + session_remove_ref(target->s, __func__); + if (target->w != NULL) + window_remove_ref(target->w, __func__); + if (target->wp != NULL) + window_pane_remove_ref(target->wp, __func__); + + cmd_find_clear_state(target, 0); +} + +/* Free the value in an item. */ +static void +event_payload_free_value(struct event_payload_item *epi) +{ + switch (epi->type) { + case EVENT_PAYLOAD_STRING: + free(epi->string); + break; + case EVENT_PAYLOAD_CLIENT: + server_client_unref(epi->client); + break; + case EVENT_PAYLOAD_SESSION: + session_remove_ref(epi->session, __func__); + break; + case EVENT_PAYLOAD_WINDOW: + window_remove_ref(epi->window, __func__); + break; + case EVENT_PAYLOAD_PANE: + window_pane_remove_ref(epi->pane, __func__); + break; + case EVENT_PAYLOAD_POINTER: + if (epi->pointer.free_cb != NULL) + epi->pointer.free_cb(epi->pointer.ptr); + break; + case EVENT_PAYLOAD_INT: + case EVENT_PAYLOAD_UINT: + case EVENT_PAYLOAD_TIME: + break; + } +} + +/* Set an item. */ +static void +event_payload_set_item(struct event_payload *ep, const char *name, + struct event_payload_item *new) +{ + struct event_payload_item *old; + + new->name = xstrdup(name); + old = RB_INSERT(event_payload_tree, &ep->items, new); + if (old != NULL) { + RB_REMOVE(event_payload_tree, &ep->items, old); + event_payload_free_value(old); + free(old->name); + free(old); + RB_INSERT(event_payload_tree, &ep->items, new); + } +} + +/* Create an event payload. */ +struct event_payload * +event_payload_create(void) +{ + struct event_payload *ep; + + ep = xcalloc(1, sizeof *ep); + RB_INIT(&ep->items); + cmd_find_clear_state(&ep->target, 0); + return (ep); +} + +/* Free an event payload. */ +void +event_payload_free(struct event_payload *ep) +{ + struct event_payload_item *epi, *epi1; + + if (ep != NULL) { + RB_FOREACH_SAFE(epi, event_payload_tree, &ep->items, epi1) { + RB_REMOVE(event_payload_tree, &ep->items, epi); + event_payload_free_value(epi); + free(epi->name); + free(epi); + } + event_payload_free_target(ep); + free(ep); + } +} + +/* Set the target. */ +void +event_payload_set_target(struct event_payload *ep, struct cmd_find_state *fs) +{ + struct cmd_find_state *target = &ep->target; + + event_payload_free_target(ep); + + if (fs->s != NULL) { + session_add_ref(fs->s, __func__); + target->s = fs->s; + } + if (fs->wl != NULL) { + target->idx = fs->wl->idx; + if (target->s == NULL) { + session_add_ref(fs->wl->session, __func__); + target->s = fs->wl->session; + } + } else + target->idx = -1; + if (fs->w != NULL) { + window_add_ref(fs->w, __func__); + target->w = fs->w; + } else if (fs->wl != NULL) { + window_add_ref(fs->wl->window, __func__); + target->w = fs->wl->window; + } + if (fs->wp != NULL) { + window_pane_add_ref(fs->wp, __func__); + target->wp = fs->wp; + } +} + +/* Get the target. */ +int +event_payload_get_target(struct event_payload *ep, struct cmd_find_state *fs) +{ + struct cmd_find_state *t = &ep->target; + struct winlink *wl = NULL; + int flags = fs->flags; + + if (t->idx != -1 && + t->s != NULL && + t->w != NULL && + session_alive(t->s)) { + wl = winlink_find_by_index(&t->s->windows, t->idx); + if (wl != NULL && wl->window != t->w) + wl = NULL; + } + + cmd_find_clear_state(fs, flags); + fs->s = t->s; + fs->w = t->w; + fs->wp = t->wp; + fs->wl = wl; + fs->idx = (wl != NULL ? wl->idx : -1); + if (cmd_find_valid_state(fs)) + return (1); + + if (wl != NULL && + t->wp != NULL && + window_has_pane(wl->window, t->wp)) { + cmd_find_from_winlink_pane(fs, wl, t->wp, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (t->wp != NULL && + cmd_find_from_pane(fs, t->wp, flags) == 0 && + cmd_find_valid_state(fs)) + return (1); + + if (wl != NULL) { + cmd_find_from_winlink(fs, wl, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (t->s != NULL && + t->w != NULL && + session_alive(t->s) && + cmd_find_from_session_window(fs, t->s, t->w, flags) == 0 && + cmd_find_valid_state(fs)) + return (1); + + if (t->s != NULL && session_alive(t->s)) { + cmd_find_from_session(fs, t->s, flags); + if (cmd_find_valid_state(fs)) + return (1); + } + + if (cmd_find_from_nothing(fs, flags) == 0) + return (1); + + cmd_find_clear_state(fs, flags); + return (0); +} + +/* Set a string item. */ +void +event_payload_set_string(struct event_payload *ep, const char *name, + const char *fmt, ...) +{ + struct event_payload_item *epi; + va_list ap; + + va_start(ap, fmt); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_STRING; + xvasprintf(&epi->string, fmt, ap); + event_payload_set_item(ep, name, epi); + + va_end(ap); +} + +/* Set a time item. */ +void +event_payload_set_time(struct event_payload *ep, const char *name, + time_t value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_TIME; + epi->time = value; + event_payload_set_item(ep, name, epi); +} + +/* Set a number item. */ +void +event_payload_set_int(struct event_payload *ep, const char *name, int value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_INT; + epi->number = value; + event_payload_set_item(ep, name, epi); +} + +/* Set an unsigned number item. */ +void +event_payload_set_uint(struct event_payload *ep, const char *name, u_int value) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_UINT; + epi->unsigned_number = value; + event_payload_set_item(ep, name, epi); +} + +/* Set a client item. */ +void +event_payload_set_client(struct event_payload *ep, const char *name, + struct client *c) +{ + struct event_payload_item *epi; + + c->references++; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_CLIENT; + epi->client = c; + event_payload_set_item(ep, name, epi); +} + +/* Set a session item. */ +void +event_payload_set_session(struct event_payload *ep, const char *name, + struct session *s) +{ + struct event_payload_item *epi; + + session_add_ref(s, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_SESSION; + epi->session = s; + event_payload_set_item(ep, name, epi); +} + +/* Set a window item. */ +void +event_payload_set_window(struct event_payload *ep, const char *name, + struct window *w) +{ + struct event_payload_item *epi; + + window_add_ref(w, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_WINDOW; + epi->window = w; + event_payload_set_item(ep, name, epi); +} + +/* Set a pane item. */ +void +event_payload_set_pane(struct event_payload *ep, const char *name, + struct window_pane *wp) +{ + struct event_payload_item *epi; + + window_pane_add_ref(wp, __func__); + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_PANE; + epi->pane = wp; + event_payload_set_item(ep, name, epi); +} + +/* Set a pointer item. */ +void +event_payload_set_pointer(struct event_payload *ep, const char *name, + void *ptr, event_payload_free_cb free_cb, event_payload_print_cb print_cb) +{ + struct event_payload_item *epi; + + epi = xcalloc(1, sizeof *epi); + epi->type = EVENT_PAYLOAD_POINTER; + epi->pointer.ptr = ptr; + epi->pointer.free_cb = free_cb; + epi->pointer.print_cb = print_cb; + event_payload_set_item(ep, name, epi); +} + +/* Get a string item. */ +const char * +event_payload_get_string(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_STRING) + return (NULL); + return (epi->string); +} + +/* Print a payload item. */ +static void +event_payload_add_item(struct event_payload_item *epi, struct evbuffer *evb) +{ + switch (epi->type) { + case EVENT_PAYLOAD_STRING: + evbuffer_add_printf(evb, "%s", epi->string); + break; + case EVENT_PAYLOAD_TIME: + evbuffer_add_printf(evb, "%lld", (long long)epi->time); + break; + case EVENT_PAYLOAD_INT: + evbuffer_add_printf(evb, "%d", epi->number); + break; + case EVENT_PAYLOAD_UINT: + evbuffer_add_printf(evb, "%u", epi->unsigned_number); + break; + case EVENT_PAYLOAD_CLIENT: + evbuffer_add_printf(evb, "%s", epi->client->name); + break; + case EVENT_PAYLOAD_SESSION: + evbuffer_add_printf(evb, "$%u", epi->session->id); + break; + case EVENT_PAYLOAD_WINDOW: + evbuffer_add_printf(evb, "@%u", epi->window->id); + break; + case EVENT_PAYLOAD_PANE: + evbuffer_add_printf(evb, "%%%u", epi->pane->id); + break; + case EVENT_PAYLOAD_POINTER: + if (epi->pointer.print_cb != NULL) + epi->pointer.print_cb(epi->pointer.ptr, evb); + else + evbuffer_add_printf(evb, "%p", epi->pointer.ptr); + break; + } +} + +/* Print a payload item. */ +char * +event_payload_item_print(struct event_payload_item *epi) +{ + struct evbuffer *evb; + char *value = NULL; + size_t size; + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + event_payload_add_item(epi, evb); + if ((size = EVBUFFER_LENGTH(evb)) != 0) + value = xmemdup(EVBUFFER_DATA(evb), size); + else + value = xstrdup(""); + evbuffer_free(evb); + return (value); +} + +/* Print an item value. */ +char * +event_payload_print(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL) + return (NULL); + return (event_payload_item_print(epi)); +} + +/* Add payload items as formats. */ +void +event_payload_add_formats(struct event_payload *ep, struct format_tree *ft, + const char *prefix) +{ + struct event_payload_item *epi; + char *name, *value; + const char *key; + + if (prefix == NULL) + prefix = ""; + + RB_FOREACH(epi, event_payload_tree, &ep->items) { + key = epi->name; + if (*key == '_') + continue; + + value = event_payload_item_print(epi); + xasprintf(&name, "%s%s", prefix, key); + format_add(ft, name, "%s", value); + free(name); + free(value); + + if (epi->type == EVENT_PAYLOAD_SESSION) { + xasprintf(&name, "%s%s_name", prefix, key); + format_add(ft, name, "%s", epi->session->name); + free(name); + } else if (epi->type == EVENT_PAYLOAD_WINDOW) { + xasprintf(&name, "%s%s_name", prefix, key); + format_add(ft, name, "%s", epi->window->name); + free(name); + } + } +} + +/* Get the first payload item. */ +struct event_payload_item * +event_payload_first(struct event_payload *ep) +{ + return (RB_MIN(event_payload_tree, &ep->items)); +} + +/* Get the next payload item. */ +struct event_payload_item * +event_payload_next(struct event_payload_item *epi) +{ + return (RB_NEXT(event_payload_tree, , epi)); +} + +/* Get a payload item name. */ +const char * +event_payload_item_name(struct event_payload_item *epi) +{ + return (epi->name); +} + +/* Get a payload item type. */ +enum event_payload_type +event_payload_item_type(struct event_payload_item *epi) +{ + return (epi->type); +} + +/* Log a payload. */ +void +event_payload_log(struct event_payload *ep, const char *fmt, ...) +{ + struct event_payload_item *epi; + struct evbuffer *evb; + va_list ap; + char *prefix; + + va_start(ap, fmt); + xvasprintf(&prefix, fmt, ap); + va_end(ap); + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + if (ep != NULL) { + RB_FOREACH(epi, event_payload_tree, &ep->items) { + if (EVBUFFER_LENGTH(evb) != 0) + evbuffer_add_printf(evb, ", "); + evbuffer_add_printf(evb, "%s=", epi->name); + event_payload_add_item(epi, evb); + } + } + log_debug("%s%.*s", prefix, (int)EVBUFFER_LENGTH(evb), + (char *)EVBUFFER_DATA(evb)); + evbuffer_free(evb); + free(prefix); +} + +/* Get a time item. */ +time_t +event_payload_get_time(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_TIME) + return (0); + return (epi->time); +} + +/* Get a number item. */ +int +event_payload_get_int(struct event_payload *ep, const char *name, int *value) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_INT) + return (-1); + *value = epi->number; + return (0); +} + +/* Get an unsigned number item. */ +int +event_payload_get_uint(struct event_payload *ep, const char *name, u_int *value) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_UINT) + return (-1); + *value = epi->unsigned_number; + return (0); +} + +/* Get a client item. */ +struct client * +event_payload_get_client(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_CLIENT) + return (NULL); + return (epi->client); +} + +/* Get a session item. */ +struct session * +event_payload_get_session(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_SESSION) + return (NULL); + return (epi->session); +} + +/* Get a window item. */ +struct window * +event_payload_get_window(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_WINDOW) + return (NULL); + return (epi->window); +} + +/* Get a pane item. */ +struct window_pane * +event_payload_get_pane(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_PANE) + return (NULL); + return (epi->pane); +} + +/* Get a pointer item. */ +void * +event_payload_get_pointer(struct event_payload *ep, const char *name) +{ + struct event_payload_item *epi; + + epi = event_payload_find(ep, name); + if (epi == NULL || epi->type != EVENT_PAYLOAD_POINTER) + return (NULL); + return (epi->pointer.ptr); +} diff --git a/events.c b/events.c new file mode 100644 index 000000000..8d6e3eed1 --- /dev/null +++ b/events.c @@ -0,0 +1,199 @@ +/* $OpenBSD: events.c,v 1.1 2026/07/10 13:38:45 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* Event sink. */ +struct events_sink { + char *name; + events_cb cb; + void *data; + int dead; + u_int generation; + + TAILQ_ENTRY(events_sink) entry; +}; + +TAILQ_HEAD(events_sinks, events_sink); +static struct events_sinks events_sinks = TAILQ_HEAD_INITIALIZER(events_sinks); + +static u_int events_dispatching; +static u_int events_generation; + +/* Free an event sink. */ +static void +events_free_sink(struct events_sink *es) +{ + TAILQ_REMOVE(&events_sinks, es, entry); + free(es->name); + free(es); +} + +/* Free dead event sinks. */ +static void +events_free_dead(void) +{ + struct events_sink *es, *es1; + + TAILQ_FOREACH_SAFE(es, &events_sinks, entry, es1) { + if (es->dead) + events_free_sink(es); + } +} + +/* Add an event sink. */ +struct events_sink * +events_add_sink(const char *name, events_cb cb, void *data) +{ + struct events_sink *es; + + es = xcalloc(1, sizeof *es); + es->name = xstrdup(name); + es->cb = cb; + es->data = data; + es->generation = ++events_generation; + + TAILQ_INSERT_TAIL(&events_sinks, es, entry); + return (es); +} + +/* Remove an event sink. */ +void +events_remove_sink(struct events_sink *es) +{ + if (es != NULL && !es->dead) { + if (events_dispatching != 0) + es->dead = 1; + else + events_free_sink(es); + } +} + +/* Fire an event. */ +void +events_fire(const char *name, struct event_payload *ep) +{ + struct events_sink *es; + u_int generation = events_generation; + + event_payload_set_string(ep, "event", "%s", name); + + if (log_get_level() != 0) + event_payload_log(ep, "%s: %s: ", __func__, name); + + events_dispatching++; + TAILQ_FOREACH(es, &events_sinks, entry) { + if (es->dead || es->generation > generation) + continue; + if (strcmp(es->name, name) == 0) + es->cb(name, ep, es->data); + } + if (--events_dispatching == 0) + events_free_dead(); + event_payload_free(ep); +} + +/* Fire a client event. */ +void +events_fire_client(const char *name, struct client *c) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_client(&fs, c, 0); + event_payload_set_target(ep, &fs); + event_payload_set_client(ep, "client", c); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.w != NULL) + event_payload_set_window(ep, "window", fs.w); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + else if (fs.idx != -1) + event_payload_set_int(ep, "window_index", fs.idx); + if (fs.wp != NULL) + event_payload_set_pane(ep, "pane", fs.wp); + events_fire(name, ep); +} + +/* Fire a session event. */ +void +events_fire_session(const char *name, struct session *s) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + if (session_alive(s)) { + cmd_find_from_session(&fs, s, 0); + event_payload_set_target(ep, &fs); + } + event_payload_set_session(ep, "session", s); + events_fire(name, ep); +} + +/* Fire a window event. */ +void +events_fire_window(const char *name, struct window *w) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_window(&fs, w, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + events_fire(name, ep); +} + +/* Fire a pane event. */ +void +events_fire_pane(const char *name, struct window_pane *wp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + events_fire(name, ep); +} + +/* Fire a winlink event. */ +void +events_fire_winlink(const char *name, struct winlink *wl) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_winlink(&fs, wl, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", wl->session); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + events_fire(name, ep); +} diff --git a/file.c b/file.c index 38a692e0d..a9c393c04 100644 --- a/file.c +++ b/file.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: file.c,v 1.20 2026/05/17 10:54:01 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/format-draw.c b/format-draw.c index 47bf192e6..ea1da421a 100644 --- a/format-draw.c +++ b/format-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: format-draw.c,v 1.33 2026/06/29 17:08:52 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/format.c b/format.c index 7ec4f06b9..da374d580 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: format.c,v 1.402 2026/07/09 06:33:19 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott diff --git a/fuzzy.c b/fuzzy.c index 00fc90687..83d34715c 100644 --- a/fuzzy.c +++ b/fuzzy.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: fuzzy.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/grid-reader.c b/grid-reader.c index 421c25181..9b40def6c 100644 --- a/grid-reader.c +++ b/grid-reader.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid-reader.c,v 1.10 2026/05/17 13:12:21 nicm Exp $ */ /* * Copyright (c) 2020 Anindya Mukherjee diff --git a/grid-view.c b/grid-view.c index 655ec9e46..86317526f 100644 --- a/grid-view.c +++ b/grid-view.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid-view.c,v 1.38 2026/01/22 08:55:01 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/grid.c b/grid.c index aabc3cf3f..878c01bab 100644 --- a/grid.c +++ b/grid.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: grid.c,v 1.153 2026/07/02 08:51:05 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/hooks.c b/hooks.c new file mode 100644 index 000000000..0ac64b8bd --- /dev/null +++ b/hooks.c @@ -0,0 +1,461 @@ +/* $OpenBSD: hooks.c,v 1.12 2026/07/10 13:38:45 nicm Exp $ */ + +/* + * Copyright (c) 2026 Nicholas Marriott + * Copyright (c) 2012 George Nachman + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER + * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING + * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include +#include + +#include "tmux.h" + +/* Hook monitor state owned by an option entry. */ +struct hook_monitor { + struct options *oo; + + struct monitor_set *set; + struct events_sink *sink; + struct cmd_find_state fs; + + enum monitor_type type; + int id; + char *format; +}; + +/* Hook command data built from an event payload. */ +struct hooks_data { + const char *name; + struct cmd_find_state fs; + struct format_tree *formats; + struct options *oo; + struct client *client; + int expand; +}; + +/* Hook event sink registered for a notify event name. */ +struct hooks_event { + char *name; + struct events_sink *sink; + TAILQ_ENTRY(hooks_event) entry; +}; +TAILQ_HEAD(hooks_events, hooks_event); +static struct hooks_events hooks_events = TAILQ_HEAD_INITIALIZER(hooks_events); + +/* Insert one hook command list. */ +static struct cmdq_item * +hooks_insert_one(struct cmdq_item *item, struct hooks_data *hd, + struct cmd_list *cmdlist, struct cmdq_state *state) +{ + struct cmdq_item *new_item; + char *s; + + if (cmdlist == NULL) + return (item); + if (log_get_level() != 0) { + s = cmd_list_print(cmdlist, 0); + log_debug("%s: hook %s is: %s", __func__, hd->name, s); + free(s); + } + new_item = cmdq_get_command(cmdlist, state); + if (item != NULL) + return (cmdq_insert_after(item, new_item)); + return (cmdq_append(NULL, new_item)); +} + +/* Parse a hook command. */ +static struct cmd_parse_result * +hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs, + const char *value) +{ + struct cmd_parse_result *pr; + struct format_tree *ft; + char *expanded; + + if (!hd->expand) + return (cmd_parse_from_string(value, NULL)); + + ft = format_create_defaults(NULL, hd->client, fs->s, fs->wl, fs->wp); + if (hd->formats != NULL) + format_merge(ft, hd->formats); + expanded = format_expand(ft, value); + format_free(ft); + + pr = cmd_parse_from_string(expanded, NULL); + free(expanded); + return (pr); +} + +/* Insert commands for a hook. */ +static void +hooks_insert(struct cmdq_item *item, struct hooks_data *hd) +{ + struct cmd_find_state fs; + struct options *oo; + struct cmdq_state *state; + struct options_entry *o; + struct options_array_item *a; + struct cmd_list *cmdlist; + const char *value; + struct cmd_parse_result *pr; + + log_debug("%s: inserting hook %s", __func__, hd->name); + + cmd_find_clear_state(&fs, 0); + if (cmd_find_empty_state(&hd->fs) || !cmd_find_valid_state(&hd->fs)) + cmd_find_from_nothing(&fs, 0); + else + cmd_find_copy_state(&fs, &hd->fs); + + if (hd->oo != NULL) { + oo = hd->oo; + o = options_get_only(oo, hd->name); + } else { + if (fs.s == NULL) + oo = global_s_options; + else + oo = fs.s->options; + o = options_get(oo, hd->name); + if (o == NULL && fs.wp != NULL) { + oo = fs.wp->options; + o = options_get(oo, hd->name); + } + if (o == NULL && fs.wl != NULL) { + oo = fs.wl->window->options; + o = options_get(oo, hd->name); + } + } + if (o == NULL) { + log_debug("%s: hook %s not found", __func__, hd->name); + return; + } + + if (item == NULL) + state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); + else { + state = cmdq_new_state(&fs, cmdq_get_event(item), + CMDQ_STATE_NOHOOKS); + } + cmdq_add_formats(state, hd->formats); + + if (*hd->name == '@') { + value = options_get_string(oo, hd->name); + pr = hooks_parse(hd, &fs, value); + switch (pr->status) { + case CMD_PARSE_ERROR: + log_debug("%s: can't parse hook %s: %s", __func__, + hd->name, pr->error); + free(pr->error); + break; + case CMD_PARSE_SUCCESS: + hooks_insert_one(item, hd, pr->cmdlist, state); + break; + } + } else { + a = options_array_first(o); + while (a != NULL) { + if (hd->expand) { + value = options_array_item_value(a)->string; + pr = hooks_parse(hd, &fs, value); + switch (pr->status) { + case CMD_PARSE_ERROR: + if (pr->error != NULL) { + cmdq_error(item, "%s", + pr->error); + } + break; + case CMD_PARSE_SUCCESS: + item = hooks_insert_one(item, hd, + pr->cmdlist, state); + break; + } + } else { + cmdlist = options_array_item_value(a)->cmdlist; + item = hooks_insert_one(item, hd, cmdlist, + state); + } + a = options_array_next(a); + } + } + + cmdq_free_state(state); +} + +/* Insert commands for a hook event. */ +static void +hooks_insert_event(struct cmdq_item *item, const char *name, + struct event_payload *ep, struct options *oo, int expand) +{ + struct hooks_data hd; + struct format_tree *ft; + struct client *c; + + if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + return; + + c = event_payload_get_client(ep, "client"); + ft = format_create(c, item, FORMAT_NONE, FORMAT_NOJOBS); + event_payload_add_formats(ep, ft, "hook_"); + format_add(ft, "hook", "%s", name); + format_log_debug(ft, __func__); + + memset(&hd, 0, sizeof hd); + hd.name = name; + cmd_find_clear_state(&hd.fs, 0); + event_payload_get_target(ep, &hd.fs); + hd.formats = ft; + hd.oo = oo; + hd.client = c; + hd.expand = expand; + + hooks_insert(item, &hd); + format_free(ft); +} + +/* Handle an event for hooks. */ +static void +hooks_event_cb(const char *name, struct event_payload *ep, + __unused void *sink_data) +{ + struct cmdq_item *item; + + if (event_payload_get_pointer(ep, "_hook_monitor") != NULL) + return; + + item = event_payload_get_pointer(ep, "_cmdq_item"); + if (item != NULL) { + hooks_insert_event(item, name, ep, NULL, 0); + return; + } + + item = cmdq_running(NULL); + if (item == NULL || (~cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) + hooks_insert_event(NULL, name, ep, NULL, 0); +} + +/* Add a hook event sink. */ +void +hooks_add_event(const char *name) +{ + struct hooks_event *he; + + TAILQ_FOREACH(he, &hooks_events, entry) { + if (strcmp(he->name, name) == 0) + return; + } + + he = xcalloc(1, sizeof *he); + he->name = xstrdup(name); + he->sink = events_add_sink(name, hooks_event_cb, NULL); + TAILQ_INSERT_TAIL(&hooks_events, he, entry); +} + +/* Return if an event name can be fired through the hooks path. */ +int +hooks_valid_event_name(const char *name) +{ + const struct options_table_entry *oe; + + if (*name == '@') + return (1); + oe = options_search(name); + return (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); +} + +/* Add hook event sinks for all built-in hooks. */ +void +hooks_build_events(void) +{ + const struct options_table_entry *oe; + + for (oe = options_table; oe->name != NULL; oe++) { + if (oe->flags & OPTIONS_TABLE_IS_HOOK) + hooks_add_event(oe->name); + } +} + +/* Run a hook immediately. */ +void +hooks_run(struct cmdq_item *item, const char *name) +{ + struct cmd_find_state *target = cmdq_get_target(item); + struct hooks_data hd = { 0 }; + + hd.name = name; + cmd_find_copy_state(&hd.fs, target); + hd.client = cmdq_get_client(item); + + hd.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); + format_add(hd.formats, "hook", "%s", name); + format_log_debug(hd.formats, __func__); + + hooks_insert(item, &hd); + format_free(hd.formats); +} + +/* Free a hook monitor. */ +void +hooks_monitor_free(void *data) +{ + struct hook_monitor *hm = data; + + events_remove_sink(hm->sink); + monitor_destroy(hm->set); + free(hm->format); + free(hm); +} + +/* Remove a hook monitor. */ +void +hooks_monitor_remove(struct options *oo, const char *name) +{ + struct options_entry *o; + struct hook_monitor *hm; + + o = options_get_only(oo, name); + if (o == NULL) + return; + + hm = options_get_monitor_data(o); + if (hm != NULL) { + options_set_monitor_data(o, NULL); + hooks_monitor_free(hm); + } +} + +/* Handle a hook monitor event. */ +static void +hooks_monitor_hook_cb(const char *name, struct event_payload *ep, + void *sink_data) +{ + struct hook_monitor *hm = sink_data; + + if (event_payload_get_pointer(ep, "_hook_monitor") == hm) + hooks_insert_event(cmdq_running(NULL), name, ep, hm->oo, 1); +} + +/* Fire a hook monitor event. */ +static void +hooks_monitor_cb(struct monitor_change *change, void *data) +{ + struct hook_monitor *hm = data; + struct event_payload *ep; + struct winlink *wl = change->wl; + struct window_pane *wp = change->wp; + struct cmd_find_state fs; + + ep = event_payload_create(); + event_payload_set_pointer(ep, "_hook_monitor", data, NULL, NULL); + + cmd_find_clear_state(&fs, 0); + if (wl != NULL && wp != NULL && wp->window == wl->window) + cmd_find_from_winlink_pane(&fs, wl, wp, 0); + else if (wl != NULL) + cmd_find_from_winlink(&fs, wl, 0); + else if (wp != NULL) + cmd_find_from_pane(&fs, wp, 0); + else if (change->s != NULL) + cmd_find_from_session(&fs, change->s, 0); + else + cmd_find_copy_state(&fs, &hm->fs); + event_payload_set_target(ep, &fs); + + if (change->value != NULL) + event_payload_set_string(ep, "value", "%s", change->value); + else + event_payload_set_string(ep, "value", "%s", ""); + if (change->last != NULL) + event_payload_set_string(ep, "last", "%s", change->last); + else + event_payload_set_string(ep, "last", "%s", ""); + + if (change->c != NULL) + event_payload_set_client(ep, "client", change->c); + if (change->s != NULL) + event_payload_set_session(ep, "session", change->s); + if (wl != NULL) { + if (change->s == NULL) + event_payload_set_session(ep, "session", + wl->session); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + } + if (wp != NULL) { + event_payload_set_pane(ep, "pane", wp); + if (wl == NULL) + event_payload_set_window(ep, "window", wp->window); + } + + events_fire(change->name, ep); +} + +/* Add a hook monitor. */ +void +hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, + const char *name, enum monitor_type type, int id, const char *format, + struct cmd_find_state *fs, struct session *s) +{ + struct options_entry *o; + struct hook_monitor *hm; + + hooks_monitor_remove(oo, name); + o = options_get_only(oo, name); + if (o == NULL) + o = options_set_string(oo, name, 0, "%s", ""); + + hm = xcalloc(1, sizeof *hm); + hm->oo = oo; + cmd_find_copy_state(&hm->fs, fs); + hm->type = type; + hm->id = id; + hm->format = xstrdup(format); + hm->set = monitor_create_session(s, hooks_monitor_cb, hm); + hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm); + options_set_monitor_data(o, hm); + monitor_add(hm->set, name, type, id, format, 0); +} + +/* Convert a hook monitor to its value. */ +char * +hooks_monitor_to_string(struct options_entry *o) +{ + struct hook_monitor *hm = options_get_monitor_data(o); + const char *name = options_name(o); + char *s; + + if (hm == NULL) + return (NULL); + + switch (hm->type) { + case MONITOR_SESSION: + xasprintf(&s, "%s::%s", name, hm->format); + break; + case MONITOR_PANE: + xasprintf(&s, "%s:%%%d:%s", name, hm->id, hm->format); + break; + case MONITOR_ALL_PANES: + xasprintf(&s, "%s:%%*:%s", name, hm->format); + break; + case MONITOR_WINDOW: + xasprintf(&s, "%s:@%d:%s", name, hm->id, hm->format); + break; + case MONITOR_ALL_WINDOWS: + xasprintf(&s, "%s:@*:%s", name, hm->format); + break; + } + return (s); +} diff --git a/hyperlinks.c b/hyperlinks.c index a71827272..1820fdad2 100644 --- a/hyperlinks.c +++ b/hyperlinks.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: hyperlinks.c,v 1.5 2026/06/29 16:44:06 nicm Exp $ */ /* * Copyright (c) 2021 Will diff --git a/input-keys.c b/input-keys.c index 277c9e71e..3208a33c3 100644 --- a/input-keys.c +++ b/input-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: input-keys.c,v 1.114 2026/06/15 21:47:01 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/input.c b/input.c index 2438ea504..bfab97a7b 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: input.c,v 1.265 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -174,6 +174,8 @@ static void input_osc_110(struct input_ctx *, const char *); static void input_osc_111(struct input_ctx *, const char *); static void input_osc_112(struct input_ctx *, const char *); static void input_osc_133(struct input_ctx *, const char *); +static void input_fire_pane_title_changed(struct window_pane *, + const char *); /* Transition entry/exit handlers. */ static void input_clear(struct input_ctx *); @@ -210,6 +212,21 @@ static int input_end_bel(struct input_ctx *); /* Command table comparison function. */ static int input_table_compare(const void *, const void *); +static void +input_fire_pane_title_changed(struct window_pane *wp, const char *title) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_string(ep, "new_title", "%s", title); + events_fire("pane-title-changed", ep); +} + /* Command table entry. */ struct input_table_entry { int ch; @@ -2161,7 +2178,7 @@ input_csi_dispatch_winops(struct input_ctx *ictx) screen_pop_title(sctx->s); if (wp == NULL) break; - notify_pane("pane-title-changed", wp); + events_fire_pane("pane-title-changed", wp); server_redraw_window_borders(w); server_status_window(w); break; @@ -2663,7 +2680,7 @@ input_exit_osc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, p, 1)) { - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, p); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -2741,7 +2758,7 @@ input_exit_apc(struct input_ctx *ictx) if (wp != NULL && options_get_number(wp->options, "allow-set-title") && screen_set_title(sctx->s, ictx->input_buf, 1)) { - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, ictx->input_buf); server_redraw_window_borders(wp->window); server_status_window(wp->window); } @@ -3252,7 +3269,7 @@ input_osc_52(struct input_ctx *ictx, const char *p) screen_write_start_pane(&ctx, wp, NULL); screen_write_setselection(&ctx, clip, out, outlen); screen_write_stop(&ctx); - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); paste_add(NULL, out, outlen); } } diff --git a/job.c b/job.c index 5ad179ffb..8400ea584 100644 --- a/job.c +++ b/job.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: job.c,v 1.74 2025/09/08 11:21:56 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/key-bindings.c b/key-bindings.c index c73fd7c33..92d417c5b 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: key-bindings.c,v 1.186 2026/07/07 12:30:36 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/key-string.c b/key-string.c index 3df9da4b9..ac7a1bf89 100644 --- a/key-string.c +++ b/key-string.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: key-string.c,v 1.77 2026/03/31 11:46:43 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/layout-custom.c b/layout-custom.c index 65fb3780b..9df0448ff 100644 --- a/layout-custom.c +++ b/layout-custom.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout-custom.c,v 1.37 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott @@ -286,7 +286,7 @@ layout_parse(struct window *w, const char *layout, char **cause) recalculate_sizes(); layout_print_cell(tiled_lc, __func__, 0); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (0); diff --git a/layout-set.c b/layout-set.c index 2f164b01c..7e944d32f 100644 --- a/layout-set.c +++ b/layout-set.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout-set.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -183,7 +183,7 @@ layout_set_even(struct window *w, enum layout_type type) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -292,7 +292,7 @@ layout_set_main_h(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -389,7 +389,7 @@ layout_set_main_h_mirrored(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -486,7 +486,7 @@ layout_set_main_v(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -584,7 +584,7 @@ layout_set_main_v_mirrored(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } @@ -706,6 +706,6 @@ layout_set_tiled(struct window *w) layout_print_cell(w->layout_root, __func__, 1); window_resize(w, lcroot->g.sx, lcroot->g.sy, -1, -1); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); server_redraw_window(w); } diff --git a/layout.c b/layout.c index 507dd63c1..177d2f4cf 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout.c,v 1.93 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -957,7 +957,7 @@ layout_resize_layout(struct window *w, struct layout_cell *lc, /* Fix cell offsets. */ layout_fix_offsets(w); layout_fix_panes(w, NULL); - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); } /* Resize a single pane within the layout. */ @@ -1502,7 +1502,7 @@ layout_close_pane(struct window_pane *wp) layout_fix_offsets(w); layout_fix_panes(w, NULL); } - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); } /* Spread out cells inside a parent cell. */ diff --git a/log.c b/log.c index ede6e2577..e596088b2 100644 --- a/log.c +++ b/log.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: log.c,v 1.33 2026/05/17 16:02:33 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/menu.c b/menu.c index 257452fd7..ec7b9781b 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: menu.c,v 1.68 2026/07/07 09:45:09 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/mode-key.c b/mode-key.c deleted file mode 100644 index 4245d173b..000000000 --- a/mode-key.c +++ /dev/null @@ -1,261 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2008 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include - -#include "tmux.h" - -/* - * Mode keys. These are the key bindings used when editing (status prompt), and - * in the modes. They are split into two sets of three tables, one set of three - * for vi and the other for emacs key bindings. The three tables are for - * editing, for menu-like modes (choice, more), and for copy modes (copy, - * scroll). - * - * The fixed tables of struct mode_key_entry below are the defaults: they are - * built into a tree of struct mode_key_binding by mode_key_init_trees, which - * can then be modified. - * - * vi command mode is handled by having a mode flag in the struct which allows - * two sets of bindings to be swapped between. A couple of editing commands - * (any matching MODEKEYEDIT_SWITCHMODE*) are special-cased to do this. - */ - -/* Command to string mapping. */ -struct mode_key_cmdstr { - enum mode_key_cmd cmd; - const char *name; -}; - -/* Entry in the default mode key tables. */ -struct mode_key_entry { - key_code key; - enum mode_key_cmd cmd; -}; - -/* Choice keys command strings. */ -static const struct mode_key_cmdstr mode_key_cmdstr_choice[] = { - { MODEKEYCHOICE_BACKSPACE, "backspace" }, - { MODEKEYCHOICE_BOTTOMLINE, "bottom-line"}, - { MODEKEYCHOICE_CANCEL, "cancel" }, - { MODEKEYCHOICE_CHOOSE, "choose" }, - { MODEKEYCHOICE_DOWN, "down" }, - { MODEKEYCHOICE_ENDOFLIST, "end-of-list"}, - { MODEKEYCHOICE_PAGEDOWN, "page-down" }, - { MODEKEYCHOICE_PAGEUP, "page-up" }, - { MODEKEYCHOICE_SCROLLDOWN, "scroll-down" }, - { MODEKEYCHOICE_SCROLLUP, "scroll-up" }, - { MODEKEYCHOICE_STARTNUMBERPREFIX, "start-number-prefix" }, - { MODEKEYCHOICE_STARTOFLIST, "start-of-list"}, - { MODEKEYCHOICE_TOPLINE, "top-line"}, - { MODEKEYCHOICE_TREE_COLLAPSE, "tree-collapse" }, - { MODEKEYCHOICE_TREE_COLLAPSE_ALL, "tree-collapse-all" }, - { MODEKEYCHOICE_TREE_EXPAND, "tree-expand" }, - { MODEKEYCHOICE_TREE_EXPAND_ALL, "tree-expand-all" }, - { MODEKEYCHOICE_TREE_TOGGLE, "tree-toggle" }, - { MODEKEYCHOICE_UP, "up" }, - - { 0, NULL } -}; - -/* vi choice selection keys. */ -static const struct mode_key_entry mode_key_vi_choice[] = { - { '0' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '1' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '2' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '3' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '4' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '5' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '6' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '7' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '8' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '9' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '\002' /* C-b */, MODEKEYCHOICE_PAGEUP }, - { '\003' /* C-c */, MODEKEYCHOICE_CANCEL }, - { '\005' /* C-e */, MODEKEYCHOICE_SCROLLDOWN }, - { '\006' /* C-f */, MODEKEYCHOICE_PAGEDOWN }, - { '\031' /* C-y */, MODEKEYCHOICE_SCROLLUP }, - { '\n', MODEKEYCHOICE_CHOOSE }, - { '\r', MODEKEYCHOICE_CHOOSE }, - { 'j', MODEKEYCHOICE_DOWN }, - { 'k', MODEKEYCHOICE_UP }, - { 'q', MODEKEYCHOICE_CANCEL }, - { KEYC_HOME, MODEKEYCHOICE_STARTOFLIST }, - { 'g', MODEKEYCHOICE_STARTOFLIST }, - { 'H', MODEKEYCHOICE_TOPLINE }, - { 'L', MODEKEYCHOICE_BOTTOMLINE }, - { 'G', MODEKEYCHOICE_ENDOFLIST }, - { KEYC_END, MODEKEYCHOICE_ENDOFLIST }, - { KEYC_BSPACE, MODEKEYCHOICE_BACKSPACE }, - { KEYC_DOWN | KEYC_CTRL, MODEKEYCHOICE_SCROLLDOWN }, - { KEYC_DOWN, MODEKEYCHOICE_DOWN }, - { KEYC_NPAGE, MODEKEYCHOICE_PAGEDOWN }, - { KEYC_PPAGE, MODEKEYCHOICE_PAGEUP }, - { KEYC_UP | KEYC_CTRL, MODEKEYCHOICE_SCROLLUP }, - { KEYC_UP, MODEKEYCHOICE_UP }, - { ' ', MODEKEYCHOICE_TREE_TOGGLE }, - { KEYC_LEFT, MODEKEYCHOICE_TREE_COLLAPSE }, - { KEYC_RIGHT, MODEKEYCHOICE_TREE_EXPAND }, - { KEYC_LEFT | KEYC_CTRL, MODEKEYCHOICE_TREE_COLLAPSE_ALL }, - { KEYC_RIGHT | KEYC_CTRL, MODEKEYCHOICE_TREE_EXPAND_ALL }, - { KEYC_MOUSEDOWN1_PANE, MODEKEYCHOICE_CHOOSE }, - { KEYC_MOUSEDOWN3_PANE, MODEKEYCHOICE_TREE_TOGGLE }, - { KEYC_WHEELUP_PANE, MODEKEYCHOICE_UP }, - { KEYC_WHEELDOWN_PANE, MODEKEYCHOICE_DOWN }, - - { KEYC_NONE, -1 } -}; -struct mode_key_tree mode_key_tree_vi_choice; - -/* emacs choice selection keys. */ -static const struct mode_key_entry mode_key_emacs_choice[] = { - { '0' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '1' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '2' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '3' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '4' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '5' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '6' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '7' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '8' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '9' | KEYC_ESCAPE, MODEKEYCHOICE_STARTNUMBERPREFIX }, - { '\003' /* C-c */, MODEKEYCHOICE_CANCEL }, - { '\016' /* C-n */, MODEKEYCHOICE_DOWN }, - { '\020' /* C-p */, MODEKEYCHOICE_UP }, - { '\026' /* C-v */, MODEKEYCHOICE_PAGEDOWN }, - { '\033' /* Escape */, MODEKEYCHOICE_CANCEL }, - { '\n', MODEKEYCHOICE_CHOOSE }, - { '\r', MODEKEYCHOICE_CHOOSE }, - { 'q', MODEKEYCHOICE_CANCEL }, - { 'v' | KEYC_ESCAPE, MODEKEYCHOICE_PAGEUP }, - { KEYC_HOME, MODEKEYCHOICE_STARTOFLIST }, - { '<' | KEYC_ESCAPE, MODEKEYCHOICE_STARTOFLIST }, - { 'R' | KEYC_ESCAPE, MODEKEYCHOICE_TOPLINE }, - { '>' | KEYC_ESCAPE, MODEKEYCHOICE_ENDOFLIST }, - { KEYC_END, MODEKEYCHOICE_ENDOFLIST }, - { KEYC_BSPACE, MODEKEYCHOICE_BACKSPACE }, - { KEYC_DOWN | KEYC_CTRL, MODEKEYCHOICE_SCROLLDOWN }, - { KEYC_DOWN, MODEKEYCHOICE_DOWN }, - { KEYC_NPAGE, MODEKEYCHOICE_PAGEDOWN }, - { KEYC_PPAGE, MODEKEYCHOICE_PAGEUP }, - { KEYC_UP | KEYC_CTRL, MODEKEYCHOICE_SCROLLUP }, - { KEYC_UP, MODEKEYCHOICE_UP }, - { ' ', MODEKEYCHOICE_TREE_TOGGLE }, - { KEYC_LEFT, MODEKEYCHOICE_TREE_COLLAPSE }, - { KEYC_RIGHT, MODEKEYCHOICE_TREE_EXPAND }, - { KEYC_LEFT | KEYC_CTRL, MODEKEYCHOICE_TREE_COLLAPSE_ALL }, - { KEYC_RIGHT | KEYC_CTRL, MODEKEYCHOICE_TREE_EXPAND_ALL }, - { KEYC_MOUSEDOWN1_PANE, MODEKEYCHOICE_CHOOSE }, - { KEYC_MOUSEDOWN3_PANE, MODEKEYCHOICE_TREE_TOGGLE }, - { KEYC_WHEELUP_PANE, MODEKEYCHOICE_UP }, - { KEYC_WHEELDOWN_PANE, MODEKEYCHOICE_DOWN }, - - { KEYC_NONE, -1 } -}; -struct mode_key_tree mode_key_tree_emacs_choice; - -/* Table mapping key table names to default settings and trees. */ -static const struct mode_key_table mode_key_tables[] = { - { "vi-choice", mode_key_cmdstr_choice, - &mode_key_tree_vi_choice, mode_key_vi_choice }, - { "emacs-choice", mode_key_cmdstr_choice, - &mode_key_tree_emacs_choice, mode_key_emacs_choice }, - - { NULL, NULL, NULL, NULL } -}; - -RB_GENERATE(mode_key_tree, mode_key_binding, entry, mode_key_cmp); - -int -mode_key_cmp(struct mode_key_binding *mbind1, struct mode_key_binding *mbind2) -{ - if (mbind1->key < mbind2->key) - return (-1); - if (mbind1->key > mbind2->key) - return (1); - return (0); -} - -const char * -mode_key_tostring(const struct mode_key_cmdstr *cmdstr, enum mode_key_cmd cmd) -{ - for (; cmdstr->name != NULL; cmdstr++) { - if (cmdstr->cmd == cmd) - return (cmdstr->name); - } - return (NULL); -} - -enum mode_key_cmd -mode_key_fromstring(const struct mode_key_cmdstr *cmdstr, const char *name) -{ - for (; cmdstr->name != NULL; cmdstr++) { - if (strcasecmp(cmdstr->name, name) == 0) - return (cmdstr->cmd); - } - return (MODEKEY_NONE); -} - -const struct mode_key_table * -mode_key_findtable(const char *name) -{ - const struct mode_key_table *mtab; - - for (mtab = mode_key_tables; mtab->name != NULL; mtab++) { - if (strcasecmp(name, mtab->name) == 0) - return (mtab); - } - return (NULL); -} - -void -mode_key_init_trees(void) -{ - const struct mode_key_table *mtab; - const struct mode_key_entry *ment; - struct mode_key_binding *mbind; - - for (mtab = mode_key_tables; mtab->name != NULL; mtab++) { - RB_INIT(mtab->tree); - for (ment = mtab->table; ment->key != KEYC_NONE; ment++) { - mbind = xmalloc(sizeof *mbind); - mbind->key = ment->key; - mbind->cmd = ment->cmd; - RB_INSERT(mode_key_tree, mtab->tree, mbind); - } - } -} - -void -mode_key_init(struct mode_key_data *mdata, struct mode_key_tree *mtree) -{ - mdata->tree = mtree; -} - -enum mode_key_cmd -mode_key_lookup(struct mode_key_data *mdata, key_code key) -{ - struct mode_key_binding *mbind, mtmp; - - mtmp.key = key; - if ((mbind = RB_FIND(mode_key_tree, mdata->tree, &mtmp)) == NULL) - return (MODEKEY_OTHER); - return (mbind->cmd); -} diff --git a/mode-tree.c b/mode-tree.c index 59d9e4fa9..ef859b15e 100644 --- a/mode-tree.c +++ b/mode-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: mode-tree.c,v 1.99 2026/07/06 12:08:52 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/monitor.c b/monitor.c index 6d1a747fd..514af4e3c 100644 --- a/monitor.c +++ b/monitor.c @@ -1,7 +1,7 @@ -/* $OpenBSD$ */ +/* $OpenBSD: monitor.c,v 1.5 2026/07/10 13:38:45 nicm Exp $ */ /* - * Copyright (c) 2026 Nicholas Marriott + * Copyright (c) 2026 Nicholas Marriott * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above diff --git a/names.c b/names.c index cee310366..aa7e6006e 100644 --- a/names.c +++ b/names.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: names.c,v 1.48 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/notify.c b/notify.c deleted file mode 100644 index 50bb55022..000000000 --- a/notify.c +++ /dev/null @@ -1,516 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2012 George Nachman - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include -#include - -#include -#include - -#include "tmux.h" - -struct notify_entry { - const char *name; - struct cmd_find_state fs; - struct format_tree *formats; - struct options *oo; - - struct client *client; - struct session *session; - struct window *window; - int pane; - const char *pbname; - int expand; -}; - -struct notify_monitor { - struct options *oo; - - struct monitor_set *set; - struct cmd_find_state fs; - - enum monitor_type type; - int id; - char *format; -}; - -static struct cmdq_item * -notify_insert_one_hook(struct cmdq_item *item, struct notify_entry *ne, - struct cmd_list *cmdlist, struct cmdq_state *state) -{ - struct cmdq_item *new_item; - char *s; - - if (cmdlist == NULL) - return (item); - if (log_get_level() != 0) { - s = cmd_list_print(cmdlist, 0); - log_debug("%s: hook %s is: %s", __func__, ne->name, s); - free(s); - } - new_item = cmdq_get_command(cmdlist, state); - if (item != NULL) - return (cmdq_insert_after(item, new_item)); - return (cmdq_append(NULL, new_item)); -} - -static struct cmd_parse_result * -notify_parse_hook(struct notify_entry *ne, struct cmd_find_state *fs, - const char *value) -{ - struct cmd_parse_result *pr; - struct format_tree *ft; - char *expanded; - - if (!ne->expand) - return (cmd_parse_from_string(value, NULL)); - - ft = format_create_defaults(NULL, ne->client, fs->s, fs->wl, fs->wp); - if (ne->formats != NULL) - format_merge(ft, ne->formats); - expanded = format_expand(ft, value); - format_free(ft); - - pr = cmd_parse_from_string(expanded, NULL); - free(expanded); - return (pr); -} - -static void -notify_insert_hook(struct cmdq_item *item, struct notify_entry *ne) -{ - struct cmd_find_state fs; - struct options *oo; - struct cmdq_state *state; - struct options_entry *o; - struct options_array_item *a; - struct cmd_list *cmdlist; - const char *value; - struct cmd_parse_result *pr; - - log_debug("%s: inserting hook %s", __func__, ne->name); - - cmd_find_clear_state(&fs, 0); - if (cmd_find_empty_state(&ne->fs) || !cmd_find_valid_state(&ne->fs)) - cmd_find_from_nothing(&fs, 0); - else - cmd_find_copy_state(&fs, &ne->fs); - - if (ne->oo != NULL) { - oo = ne->oo; - o = options_get_only(oo, ne->name); - } else { - if (fs.s == NULL) - oo = global_s_options; - else - oo = fs.s->options; - o = options_get(oo, ne->name); - if (o == NULL && fs.wp != NULL) { - oo = fs.wp->options; - o = options_get(oo, ne->name); - } - if (o == NULL && fs.wl != NULL) { - oo = fs.wl->window->options; - o = options_get(oo, ne->name); - } - } - if (o == NULL) { - log_debug("%s: hook %s not found", __func__, ne->name); - return; - } - - state = cmdq_new_state(&fs, NULL, CMDQ_STATE_NOHOOKS); - cmdq_add_formats(state, ne->formats); - - if (*ne->name == '@') { - value = options_get_string(oo, ne->name); - pr = notify_parse_hook(ne, &fs, value); - switch (pr->status) { - case CMD_PARSE_ERROR: - log_debug("%s: can't parse hook %s: %s", __func__, - ne->name, pr->error); - free(pr->error); - break; - case CMD_PARSE_SUCCESS: - notify_insert_one_hook(item, ne, pr->cmdlist, state); - break; - } - } else { - a = options_array_first(o); - while (a != NULL) { - if (ne->expand) { - value = options_array_item_value(a)->string; - pr = notify_parse_hook(ne, &fs, value); - switch (pr->status) { - case CMD_PARSE_ERROR: - if (pr->error != NULL) - cmdq_error(item, "%s", pr->error); - break; - case CMD_PARSE_SUCCESS: - item = notify_insert_one_hook(item, ne, - pr->cmdlist, state); - break; - } - } else { - cmdlist = options_array_item_value(a)->cmdlist; - item = notify_insert_one_hook(item, ne, cmdlist, - state); - } - a = options_array_next(a); - } - } - - cmdq_free_state(state); -} - -static enum cmd_retval -notify_callback(struct cmdq_item *item, void *data) -{ - struct notify_entry *ne = data; - - log_debug("%s: %s", __func__, ne->name); - - if (strcmp(ne->name, "pane-mode-changed") == 0) - control_notify_pane_mode_changed(ne->pane); - if (strcmp(ne->name, "window-layout-changed") == 0) - control_notify_window_layout_changed(ne->window); - if (strcmp(ne->name, "window-pane-changed") == 0) - control_notify_window_pane_changed(ne->window); - if (strcmp(ne->name, "window-unlinked") == 0) - control_notify_window_unlinked(ne->session, ne->window); - if (strcmp(ne->name, "window-linked") == 0) - control_notify_window_linked(ne->session, ne->window); - if (strcmp(ne->name, "window-renamed") == 0) - control_notify_window_renamed(ne->window); - if (strcmp(ne->name, "client-session-changed") == 0) - control_notify_client_session_changed(ne->client); - if (strcmp(ne->name, "client-detached") == 0) - control_notify_client_detached(ne->client); - if (strcmp(ne->name, "session-renamed") == 0) - control_notify_session_renamed(ne->session); - if (strcmp(ne->name, "session-created") == 0) - control_notify_session_created(ne->session); - if (strcmp(ne->name, "session-closed") == 0) - control_notify_session_closed(ne->session); - if (strcmp(ne->name, "session-window-changed") == 0) - control_notify_session_window_changed(ne->session); - if (strcmp(ne->name, "paste-buffer-changed") == 0) - control_notify_paste_buffer_changed(ne->pbname); - if (strcmp(ne->name, "paste-buffer-deleted") == 0) - control_notify_paste_buffer_deleted(ne->pbname); - - notify_insert_hook(item, ne); - - if (ne->client != NULL) - server_client_unref(ne->client); - if (ne->session != NULL) - session_remove_ref(ne->session, __func__); - if (ne->window != NULL) - window_remove_ref(ne->window, __func__); - - if (ne->fs.s != NULL) - session_remove_ref(ne->fs.s, __func__); - - format_free(ne->formats); - free((void *)ne->name); - free((void *)ne->pbname); - free(ne); - - return (CMD_RETURN_NORMAL); -} - -void -notify_monitor_free(void *data) -{ - struct notify_monitor *nhm = data; - - monitor_destroy(nhm->set); - free(nhm->format); - free(nhm); -} - -void -notify_monitor_remove(struct options *oo, const char *name) -{ - struct options_entry *o; - struct notify_monitor *nhm; - - o = options_get_only(oo, name); - if (o == NULL) - return; - - nhm = options_get_monitor_data(o); - if (nhm != NULL) { - options_set_monitor_data(o, NULL); - notify_monitor_free(nhm); - } -} - -static void -notify_monitor_cb(struct monitor_change *change, void *data) -{ - struct notify_monitor *nhm = data; - struct notify_entry ne; - struct cmdq_item *item; - struct window *w; - - item = cmdq_running(NULL); - if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) - return; - - memset(&ne, 0, sizeof ne); - ne.name = change->name; - ne.oo = nhm->oo; - ne.client = change->c; - ne.expand = 1; - if (change->wp != NULL && change->wl != NULL) - cmd_find_from_winlink_pane(&ne.fs, change->wl, change->wp, 0); - else if (change->wl != NULL) - cmd_find_from_winlink(&ne.fs, change->wl, 0); - else if (change->s != NULL) - cmd_find_from_session(&ne.fs, change->s, 0); - else - cmd_find_copy_state(&ne.fs, &nhm->fs); - ne.formats = format_create(change->c, item, FORMAT_NONE, FORMAT_NOJOBS); - format_add(ne.formats, "hook", "%s", change->name); - format_add(ne.formats, "hook_value", "%s", change->value); - format_add(ne.formats, "hook_last", "%s", - change->last == NULL ? "" : change->last); - if (change->s != NULL) { - format_add(ne.formats, "hook_session", "$%u", change->s->id); - format_add(ne.formats, "hook_session_name", "%s", change->s->name); - } - if (change->wl != NULL) { - w = change->wl->window; - format_add(ne.formats, "hook_window", "@%u", w->id); - format_add(ne.formats, "hook_window_name", "%s", w->name); - format_add(ne.formats, "hook_window_index", "%d", change->wl->idx); - } - if (change->wp != NULL) { - format_add(ne.formats, "hook_pane", "%%%u", change->wp->id); - } - - notify_insert_hook(item, &ne); - format_free(ne.formats); -} - -void -notify_monitor_add(__unused struct cmdq_item *item, struct options *oo, - const char *name, enum monitor_type type, int id, const char *format, - struct cmd_find_state *fs, struct session *s) -{ - struct options_entry *o; - struct notify_monitor *nhm; - - notify_monitor_remove(oo, name); - o = options_get_only(oo, name); - if (o == NULL) - o = options_set_string(oo, name, 0, "%s", ""); - - nhm = xcalloc(1, sizeof *nhm); - nhm->oo = oo; - cmd_find_copy_state(&nhm->fs, fs); - nhm->type = type; - nhm->id = id; - nhm->format = xstrdup(format); - nhm->set = monitor_create_session(s, notify_monitor_cb, nhm); - options_set_monitor_data(o, nhm); - monitor_add(nhm->set, name, type, id, format, 0); -} - -/* Convert a hook monitor to its value. */ -char * -notify_monitor_to_string(struct options_entry *o) -{ - struct notify_monitor *nhm = options_get_monitor_data(o); - const char *name = options_name(o); - char *s; - - if (nhm == NULL) - return (NULL); - - switch (nhm->type) { - case MONITOR_SESSION: - xasprintf(&s, "%s::%s", name, nhm->format); - break; - case MONITOR_PANE: - xasprintf(&s, "%s:%%%d:%s", name, nhm->id, nhm->format); - break; - case MONITOR_ALL_PANES: - xasprintf(&s, "%s:%%*:%s", name, nhm->format); - break; - case MONITOR_WINDOW: - xasprintf(&s, "%s:@%d:%s", name, nhm->id, nhm->format); - break; - case MONITOR_ALL_WINDOWS: - xasprintf(&s, "%s:@*:%s", name, nhm->format); - break; - } - return (s); -} - -static void -notify_add(const char *name, struct cmd_find_state *fs, struct client *c, - struct session *s, struct window *w, struct window_pane *wp, - const char *pbname) -{ - struct notify_entry *ne; - struct cmdq_item *item; - - item = cmdq_running(NULL); - if (item != NULL && (cmdq_get_flags(item) & CMDQ_STATE_NOHOOKS)) - return; - - ne = xcalloc(1, sizeof *ne); - ne->name = xstrdup(name); - - ne->client = c; - ne->session = s; - ne->window = w; - ne->pane = (wp != NULL ? (int)wp->id : -1); - ne->pbname = (pbname != NULL ? xstrdup(pbname) : NULL); - - ne->formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); - format_add(ne->formats, "hook", "%s", name); - if (c != NULL) - format_add(ne->formats, "hook_client", "%s", c->name); - if (s != NULL) { - format_add(ne->formats, "hook_session", "$%u", s->id); - format_add(ne->formats, "hook_session_name", "%s", s->name); - } - if (w != NULL) { - format_add(ne->formats, "hook_window", "@%u", w->id); - format_add(ne->formats, "hook_window_name", "%s", w->name); - } - if (wp != NULL) { - format_add(ne->formats, "hook_pane", "%%%d", wp->id); - format_add(ne->formats, "hook_window", "@%u", wp->window->id); - format_add(ne->formats, "hook_window_name", "%s", - wp->window->name); - } - format_log_debug(ne->formats, __func__); - - if (c != NULL) - c->references++; - if (s != NULL) - session_add_ref(s, __func__); - if (w != NULL) - window_add_ref(w, __func__); - - cmd_find_copy_state(&ne->fs, fs); - if (ne->fs.s != NULL) /* cmd_find_valid_state needs session */ - session_add_ref(ne->fs.s, __func__); - - cmdq_append(NULL, cmdq_get_callback(notify_callback, ne)); -} - -void -notify_hook(struct cmdq_item *item, const char *name) -{ - struct cmd_find_state *target = cmdq_get_target(item); - struct notify_entry ne; - - memset(&ne, 0, sizeof ne); - - ne.name = name; - cmd_find_copy_state(&ne.fs, target); - - ne.client = cmdq_get_client(item); - ne.session = target->s; - ne.window = target->w; - ne.pane = (target->wp != NULL ? (int)target->wp->id : -1); - - ne.formats = format_create(NULL, NULL, 0, FORMAT_NOJOBS); - format_add(ne.formats, "hook", "%s", name); - format_log_debug(ne.formats, __func__); - - notify_insert_hook(item, &ne); - format_free(ne.formats); -} - -void -notify_client(const char *name, struct client *c) -{ - struct cmd_find_state fs; - - cmd_find_from_client(&fs, c, 0); - notify_add(name, &fs, c, NULL, NULL, NULL, NULL); -} - -void -notify_session(const char *name, struct session *s) -{ - struct cmd_find_state fs; - - if (session_alive(s)) - cmd_find_from_session(&fs, s, 0); - else - cmd_find_from_nothing(&fs, 0); - notify_add(name, &fs, NULL, s, NULL, NULL, NULL); -} - -void -notify_winlink(const char *name, struct winlink *wl) -{ - struct cmd_find_state fs; - - cmd_find_from_winlink(&fs, wl, 0); - notify_add(name, &fs, NULL, wl->session, wl->window, NULL, NULL); -} - -void -notify_session_window(const char *name, struct session *s, struct window *w) -{ - struct cmd_find_state fs; - - cmd_find_from_session_window(&fs, s, w, 0); - notify_add(name, &fs, NULL, s, w, NULL, NULL); -} - -void -notify_window(const char *name, struct window *w) -{ - struct cmd_find_state fs; - - cmd_find_from_window(&fs, w, 0); - notify_add(name, &fs, NULL, NULL, w, NULL, NULL); -} - -void -notify_pane(const char *name, struct window_pane *wp) -{ - struct cmd_find_state fs; - - cmd_find_from_pane(&fs, wp, 0); - notify_add(name, &fs, NULL, NULL, NULL, wp, NULL); -} - -void -notify_paste_buffer(const char *pbname, int deleted) -{ - struct cmd_find_state fs; - - cmd_find_clear_state(&fs, 0); - if (deleted) { - notify_add("paste-buffer-deleted", &fs, NULL, NULL, NULL, NULL, - pbname); - } else { - notify_add("paste-buffer-changed", &fs, NULL, NULL, NULL, NULL, - pbname); - } -} diff --git a/options-table.c b/options-table.c index b1ab705d5..082ed06ff 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: options-table.c,v 1.232 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1933,11 +1933,18 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-light-theme", ""), OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), + OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-created", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-in", ""), OPTIONS_TABLE_PANE_HOOK("pane-focus-out", ""), OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", ""), + OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", ""), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), + OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), @@ -1949,6 +1956,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-renamed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-resized", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-unzoomed", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-zoomed", ""), OPTIONS_TABLE_HOOK("window-unlinked", ""), { .name = NULL } diff --git a/options.c b/options.c index 3b81ed57d..b5238bc69 100644 --- a/options.c +++ b/options.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: options.c,v 1.90 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott @@ -410,7 +410,7 @@ options_remove(struct options_entry *o) else options_value_free(o, &o->value); if (o->monitor_data != NULL) - notify_monitor_free(o->monitor_data); + hooks_monitor_free(o->monitor_data); RB_REMOVE(options_tree, &oo->tree, o); free((void *)o->name); free(o); diff --git a/paste.c b/paste.c index 6b8a1cb47..0b9069f92 100644 --- a/paste.c +++ b/paste.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: paste.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -44,6 +44,16 @@ static int paste_cmp_times(const struct paste_buffer *, const struct paste_buffer *); RB_GENERATE_STATIC(paste_time_tree, paste_buffer, time_entry, paste_cmp_times); +static void +paste_fire_event(const char *name, const char *pbname) +{ + struct event_payload *ep; + + ep = event_payload_create(); + event_payload_set_string(ep, "paste_buffer", "%s", pbname); + events_fire(name, ep); +} + static int paste_cmp_names(const struct paste_buffer *a, const struct paste_buffer *b) { @@ -138,7 +148,7 @@ paste_get_name(const char *name) void paste_free(struct paste_buffer *pb) { - notify_paste_buffer(pb->name, 1); + paste_fire_event("paste-buffer-deleted", pb->name); RB_REMOVE(paste_name_tree, &paste_by_name, pb); RB_REMOVE(paste_time_tree, &paste_by_time, pb); @@ -196,7 +206,7 @@ paste_add(const char *prefix, char *data, size_t size) RB_INSERT(paste_name_tree, &paste_by_name, pb); RB_INSERT(paste_time_tree, &paste_by_time, pb); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); } /* Rename a paste buffer. */ @@ -254,8 +264,8 @@ paste_rename(const char *oldname, const char *newname, char **cause) RB_INSERT(paste_name_tree, &paste_by_name, pb); - notify_paste_buffer(oldname, 1); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-deleted", oldname); + paste_fire_event("paste-buffer-changed", pb->name); return (0); } @@ -312,7 +322,7 @@ paste_set(char *data, size_t size, const char *name, char **cause) RB_INSERT(paste_name_tree, &paste_by_name, pb); RB_INSERT(paste_time_tree, &paste_by_time, pb); - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); return (0); } @@ -325,7 +335,7 @@ paste_replace(struct paste_buffer *pb, char *data, size_t size) pb->data = data; pb->size = size; - notify_paste_buffer(pb->name, 0); + paste_fire_event("paste-buffer-changed", pb->name); } /* Convert start of buffer into a nice string. */ diff --git a/popup.c b/popup.c index 913c6c527..c43b46d0a 100644 --- a/popup.c +++ b/popup.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: popup.c,v 1.75 2026/07/09 07:32:58 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott diff --git a/proc.c b/proc.c index 21a747525..194371236 100644 --- a/proc.c +++ b/proc.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: proc.c,v 1.31 2026/06/08 21:38:19 nicm Exp $ */ /* * Copyright (c) 2015 Nicholas Marriott diff --git a/procname.c b/procname.c index 7c0872fd0..0ef484fb8 100644 --- a/procname.c +++ b/procname.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: procname.c,v 1.20 2022/02/22 17:35:01 deraadt Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/prompt-history.c b/prompt-history.c index ac99bdb95..ad1ab4047 100644 --- a/prompt-history.c +++ b/prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: prompt-history.c,v 1.1 2026/06/25 11:39:11 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/prompt.c b/prompt.c index 1ab447ebe..6187a7a12 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: prompt.c,v 1.4 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/regsub.c b/regsub.c index 9e33a88ab..c49c72bb9 100644 --- a/regsub.c +++ b/regsub.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: regsub.c,v 1.10 2026/07/08 11:04:51 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott diff --git a/resize.c b/resize.c index 19a940d61..c8257cec0 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: resize.c,v 1.56 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -60,8 +60,8 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) tty_update_window_offset(w); server_redraw_window(w); - notify_window("window-layout-changed", w); - notify_window("window-resized", w); + events_fire_window("window-layout-changed", w); + events_fire_window("window-resized", w); w->flags &= ~WINDOW_RESIZE; } diff --git a/screen-redraw.c b/screen-redraw.c index 70a830e6a..10a5acaa9 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen-redraw.c,v 1.150 2026/07/09 07:32:58 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/screen-write.c b/screen-write.c index 4d4f90900..a7a16149c 100644 --- a/screen-write.c +++ b/screen-write.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen-write.c,v 1.283 2026/07/09 07:35:05 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/screen.c b/screen.c index 8b988adee..4c754360c 100644 --- a/screen.c +++ b/screen.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: screen.c,v 1.105 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/server-acl.c b/server-acl.c index db95a3c6b..03e758af5 100644 --- a/server-acl.c +++ b/server-acl.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-acl.c,v 1.3 2026/06/08 21:38:19 nicm Exp $ */ /* * Copyright (c) 2021 Holland Schutte, Jayson Morberg diff --git a/server-client.c b/server-client.c index 8344ae190..86360de20 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-client.c,v 1.486 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -433,7 +433,7 @@ server_client_set_session(struct client *c, struct session *s) alerts_check_session(s); tty_update_client_offset(c); status_timer_start(c); - notify_client("client-session-changed", c); + events_fire_client("client-session-changed", c); server_redraw_client(c); } @@ -468,7 +468,7 @@ server_client_lost(struct client *c) if (c->flags & CLIENT_ATTACHED) { server_client_attached_lost(c); - notify_client("client-detached", c); + events_fire_client("client-detached", c); } if (c->flags & CLIENT_CONTROL) @@ -1288,7 +1288,7 @@ server_client_update_latest(struct client *c) if (options_get_number(w->options, "window-size") == WINDOW_SIZE_LATEST) recalculate_size(w, 0); - notify_client("client-active", c); + events_fire_client("client-active", c); } /* Get repeat time. */ @@ -2472,7 +2472,7 @@ server_client_dispatch(struct imsg *imsg, void *arg) c->overlay_resize(c, c->overlay_data); server_redraw_client(c); if (c->session != NULL) - notify_client("client-resized", c); + events_fire_client("client-resized", c); break; case MSG_EXITING: if (datalen != 0) @@ -3087,10 +3087,10 @@ server_client_report_theme(struct client *c, enum client_theme theme) if (theme == THEME_LIGHT) { c->theme = THEME_LIGHT; - notify_client("client-light-theme", c); + events_fire_client("client-light-theme", c); } else { c->theme = THEME_DARK; - notify_client("client-dark-theme", c); + events_fire_client("client-dark-theme", c); } /* diff --git a/server-fn.c b/server-fn.c index 7560e9da2..e3823dcbb 100644 --- a/server-fn.c +++ b/server-fn.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-fn.c,v 1.147 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -30,6 +30,31 @@ #include "tmux.h" static void server_destroy_session_group(struct session *); +static void server_fire_pane_exit(const char *, struct window_pane *); + +static void +server_fire_pane_exit(const char *name, struct window_pane *wp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + int status = wp->status; + const char *signame; + + if (WIFSIGNALED(status)) + signame = sig2name(WTERMSIG(status)); + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + if (WIFEXITED(status)) + event_payload_set_int(ep, "exit_status", WEXITSTATUS(status)); + else if (WIFSIGNALED(status)) + event_payload_set_string(ep, "exit_signal", "%s", signame); + event_payload_set_int(ep, "exit_success", status == 0); + events_fire(name, ep); +} void server_redraw_client(struct client *c) @@ -203,6 +228,7 @@ server_kill_window(struct window *w, int renumber) struct session *s, *s1; struct winlink *wl; + window_add_ref(w, __func__); RB_FOREACH_SAFE(s, sessions, &sessions, s1) { if (!session_has(s, w)) continue; @@ -220,6 +246,7 @@ server_kill_window(struct window *w, int renumber) server_renumber_session(s); } recalculate_sizes(); + window_remove_ref(w, __func__); } void @@ -273,8 +300,7 @@ server_link_window(struct session *src, struct winlink *srcwl, * Can't use session_detach as it will destroy session * if this makes it empty. */ - notify_session_window("window-unlinked", dst, - dstwl->window); + events_fire_winlink("window-unlinked", dstwl); dstwl->flags &= ~WINLINK_ALERTFLAGS; winlink_stack_remove(&dst->lastw, dstwl); winlink_remove(&dst->windows, dstwl); @@ -348,7 +374,7 @@ server_destroy_pane(struct window_pane *wp, int notify) gettimeofday(&wp->dead_time, NULL); if (notify) - notify_pane("pane-died", wp); + server_fire_pane_exit("pane-died", wp); s = options_get_string(wp->options, "remain-on-exit-format"); if (*s != '\0') { @@ -371,7 +397,7 @@ server_destroy_pane(struct window_pane *wp, int notify) } if (notify) - notify_pane("pane-exited", wp); + server_fire_pane_exit("pane-exited", wp); server_unzoom_window(w); server_client_remove_pane(wp); diff --git a/server.c b/server.c index edabc8a79..6fe7f2c7b 100644 --- a/server.c +++ b/server.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server.c,v 1.214 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -212,6 +212,8 @@ server_start(struct tmuxproc *client, uint64_t flags, struct event_base *base, TAILQ_INIT(&clients); RB_INIT(&sessions); key_bindings_init(); + control_build_events(); + hooks_build_events(); TAILQ_INIT(&message_log); gettimeofday(&start_time, NULL); diff --git a/session.c b/session.c index 41909d2a6..9c23c4b50 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: session.c,v 1.105 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -208,7 +208,7 @@ session_destroy(struct session *s, int notify, const char *from) RB_REMOVE(sessions, &sessions, s); if (notify) - notify_session("session-closed", s); + events_fire_session("session-closed", s); free(s->tio); @@ -221,7 +221,7 @@ session_destroy(struct session *s, int notify, const char *from) winlink_stack_remove(&s->lastw, TAILQ_FIRST(&s->lastw)); while (!RB_EMPTY(&s->windows)) { wl = RB_ROOT(&s->windows); - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_remove(&s->windows, wl); } @@ -332,7 +332,7 @@ session_attach(struct session *s, struct window *w, int idx, char **cause) } wl->session = s; winlink_set_window(wl, w); - notify_session_window("window-linked", s, w); + events_fire_winlink("window-linked", wl); session_group_synchronize_from(s); return (wl); @@ -348,7 +348,7 @@ session_detach(struct session *s, struct winlink *wl) session_next(s, 0); wl->flags &= ~WINLINK_ALERTFLAGS; - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_stack_remove(&s->lastw, wl); winlink_remove(&s->windows, wl); @@ -495,7 +495,7 @@ session_set_current(struct session *s, struct winlink *wl) winlink_clear_flags(wl); window_update_activity(wl->window); tty_update_window_offset(wl->window); - notify_session("session-window-changed", s); + events_fire_session("session-window-changed", s); return (0); } @@ -659,7 +659,7 @@ session_group_synchronize1(struct session *target, struct session *s) wl2 = winlink_add(&s->windows, wl->idx); wl2->session = s; winlink_set_window(wl2, wl->window); - notify_session_window("window-linked", s, wl2->window); + events_fire_winlink("window-linked", wl2); wl2->flags |= wl->flags & WINLINK_ALERTFLAGS; } @@ -685,7 +685,7 @@ session_group_synchronize1(struct session *target, struct session *s) wl = RB_ROOT(&old_windows); wl2 = winlink_find_by_window_id(&s->windows, wl->window->id); if (wl2 == NULL) - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_remove(&old_windows, wl); } } diff --git a/sort.c b/sort.c index c416b8425..31f6e82f1 100644 --- a/sort.c +++ b/sort.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: sort.c,v 1.9 2026/06/29 07:45:09 nicm Exp $ */ /* * Copyright (c) 2026 Dane Jensen diff --git a/spawn.c b/spawn.c index 09eec948d..21fc25937 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: spawn.c,v 1.47 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -73,6 +73,45 @@ spawn_log(const char *from, struct spawn_context *sc) log_debug("%s: s=$%u %s idx=%d", from, s->id, tmp, sc->idx); } +static void +spawn_fire_pane_created(struct spawn_context *sc, struct window_pane *wp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + char *cmd = NULL; + const char *cwd = wp->cwd; + + ep = event_payload_create(); + cmd_find_from_winlink_pane(&fs, sc->wl, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", sc->s); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_int(ep, "window_index", sc->wl->idx); + event_payload_set_pane(ep, "pane", wp); + + if (wp->argc != 0) + cmd = cmd_stringify_argv(wp->argc, wp->argv); + if (cmd != NULL && *cmd != '\0') + event_payload_set_string(ep, "pane_command", "%s", cmd); + else if (wp->shell != NULL) + event_payload_set_string(ep, "pane_command", "%s", wp->shell); + free(cmd); + + if (cwd != NULL) + event_payload_set_string(ep, "pane_current_path", "%s", cwd); + + if (sc->flags & SPAWN_EMPTY) + event_payload_set_int(ep, "created_empty", 1); + else + event_payload_set_int(ep, "created_empty", 0); + if (sc->flags & SPAWN_RESPAWN) + event_payload_set_int(ep, "created_respawn", 1); + else + event_payload_set_int(ep, "created_respawn", 0); + + events_fire("pane-created", ep); +} + struct winlink * spawn_window(struct spawn_context *sc, char **cause) { @@ -133,7 +172,7 @@ spawn_window(struct spawn_context *sc, char **cause) * if this makes it empty. */ wl->flags &= ~WINLINK_ALERTFLAGS; - notify_session_window("window-unlinked", s, wl->window); + events_fire_winlink("window-unlinked", wl); winlink_stack_remove(&s->lastw, wl); winlink_remove(&s->windows, wl); @@ -193,7 +232,7 @@ spawn_window(struct spawn_context *sc, char **cause) /* Fire notification if new window. */ if (~sc->flags & SPAWN_RESPAWN) - notify_session_window("window-linked", s, w); + events_fire_winlink("window-linked", sc->wl); session_group_synchronize_from(s); return (sc->wl); @@ -499,8 +538,8 @@ complete: sigprocmask(SIG_SETMASK, &oldset, NULL); window_pane_set_event(new_wp); - environ_free(child); + spawn_fire_pane_created(sc, new_wp); if (sc->flags & SPAWN_RESPAWN) return (new_wp); @@ -511,7 +550,7 @@ complete: window_set_active_pane(w, new_wp, 1); } if (~sc->flags & SPAWN_NONOTIFY) - notify_window("window-layout-changed", w); + events_fire_window("window-layout-changed", w); return (new_wp); } diff --git a/status.c b/status.c index 8f7c209e1..2f3c8f11e 100644 --- a/status.c +++ b/status.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: status.c,v 1.273 2026/07/06 14:29:10 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/style.c b/style.c index 2ca5e37af..55d08fe75 100644 --- a/style.c +++ b/style.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: style.c,v 1.47 2026/06/29 17:08:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/tmux-protocol.h b/tmux-protocol.h index 3cf00c094..d823ce7e2 100644 --- a/tmux-protocol.h +++ b/tmux-protocol.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux-protocol.h,v 1.2 2023/01/06 07:09:27 nicm Exp $ */ /* * Copyright (c) 2021 Nicholas Marriott diff --git a/tmux.1 b/tmux.1 index 50d398b7c..215878dea 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD$ +.\" $OpenBSD: tmux.1,v 1.1131 2026/07/10 13:38:45 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate$ +.Dd $Mdocdate: July 10 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -6323,11 +6323,7 @@ For example, this could be used to write to a log file: set\-hook \-g command\-error "run\-shell \\"echo 'a tmux command failed' >>/tmp/log\\"" .Ed .Pp -All the notifications listed in the -.Sx CONTROL MODE -section are hooks (without any arguments), except -.Ic %exit . -The following additional hooks are available: +The following hooks are available in addition to after hooks: .Bl -tag -width "XXXXXXXXXXXXXXXXXXXXXX" .It alert\-activity Run when a window has activity. @@ -6346,11 +6342,11 @@ Run when a client becomes the latest active client of its session. .It client\-attached Run when a client is attached. .It client\-detached -Run when a client is detached +Run when a client is detached. .It client\-focus\-in -Run when focus enters a client +Run when focus enters a client. .It client\-focus\-out -Run when focus exits a client +Run when focus exits a client. .It client\-resized Run when a client is resized. .It client\-session\-changed @@ -6361,6 +6357,10 @@ Run when a client switches to a light theme. Run when a client switches to a dark theme. .It command\-error Run when a command fails. +.It marked\-pane\-changed +Run when the marked pane is set or cleared. +.It pane\-created +Run when a pane is created or respawned. .It pane\-died Run when the program running in a pane exits, but .Ic remain\-on\-exit @@ -6375,20 +6375,38 @@ option is on. Run when the focus exits a pane, if the .Ic focus\-events option is on. +.It pane\-mode\-changed +Run when a pane changes mode. +.It pane\-mode\-entered +Run when a pane enters a mode. +.It pane\-mode\-exited +Run when a pane exits a mode. +.It pane\-prompt\-closed +Run when a prompt in a pane is closed. +.It pane\-prompt\-opened +Run when a prompt is opened in a pane. +.It pane\-resized +Run when a pane is resized. .It pane\-set\-clipboard Run when the terminal clipboard is set using the .Xr xterm 1 escape sequence. -.It session\-created -Run when a new session created. +.It pane\-title\-changed +Run when a pane title is changed. .It session\-closed -Run when a session closed. +Run when a session is closed. +.It session\-created +Run when a new session is created. .It session\-renamed Run when a session is renamed. +.It session\-window\-changed +Run when a session changes its active window. .It window\-layout\-changed Run when a window layout is changed. .It window\-linked Run when a window is linked into a session. +.It window\-pane\-changed +Run when a window changes its active pane. .It window\-renamed Run when a window is renamed. .It window\-resized @@ -6396,6 +6414,10 @@ Run when a window is resized. This may be after the .Ar client\-resized hook is run. +.It window\-unzoomed +Run when a window is unzoomed. +.It window\-zoomed +Run when a window is zoomed. .It window\-unlinked Run when a window is unlinked from a session. .El @@ -6403,13 +6425,15 @@ Run when a window is unlinked from a session. Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook -.Op Fl agpRuw +.Op Fl agpERuw .Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name .Op Ar command .Xc Without +.Fl E +or .Fl R , sets (or with .Fl u @@ -6450,6 +6474,14 @@ the subscription named by is removed. .Pp With +.Fl E , +fire the user event named +.Ar hook\-name . +.Ar hook\-name +must begin with +.Ql @ . +.Pp +With .Fl R , run .Ar hook\-name @@ -8560,8 +8592,10 @@ or the current pane if omitted) after the command finishes. If the command fails, the exit status is also displayed. .Tg wait .It Xo Ic wait\-for -.Op Fl L | S | U -.Ar channel +.Op Fl ELSUlv +.Op Fl F Ar format +.Op Fl w Ar waiter +.Ar name .Xc .D1 Pq alias: Ic wait When used without options, prevents the client from exiting until woken using @@ -8574,6 +8608,41 @@ is used, the channel is locked and any clients that try to lock the same channel are made to wait until the channel is unlocked with .Ic wait\-for .Fl U . +.Pp +With +.Fl E , +.Nm +waits for the next event with +.Ar name . +Events include hook and notification names, and user +.Ql @ +events generated by +.Ic set-hook +.Fl E +or +.Ic set-hook +.Fl B . +If +.Fl F +is given, +.Ar format +must also be true. +If +.Fl v +is given, event payload keys are printed (whether or not +.Ar format +is true). +.Pp +.Fl l +list the waiters for +.Ar name +and +.Fl w +wakes +.Ar waiter +on +.Ar name +immediately. .El .Sh EXIT MESSAGES When a diff --git a/tmux.c b/tmux.c index c08110c95..8a90c3ff9 100644 --- a/tmux.c +++ b/tmux.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux.c,v 1.221 2026/06/29 18:17:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/tmux.h b/tmux.h index b153310cf..076f4bf7c 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux.h,v 1.1394 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -48,6 +48,9 @@ struct cmdq_state; struct cmds; struct control_state; struct environ; +struct event_payload; +struct event_payload_item; +struct events_sink; struct format_job_tree; struct format_tree; struct hyperlinks_uri; @@ -2302,6 +2305,23 @@ struct monitor_change { }; typedef void (*monitor_cb)(struct monitor_change *, void *); +/* Event payload type. */ +enum event_payload_type { + EVENT_PAYLOAD_STRING, + EVENT_PAYLOAD_TIME, + EVENT_PAYLOAD_INT, + EVENT_PAYLOAD_UINT, + EVENT_PAYLOAD_CLIENT, + EVENT_PAYLOAD_SESSION, + EVENT_PAYLOAD_WINDOW, + EVENT_PAYLOAD_PANE, + EVENT_PAYLOAD_POINTER +}; + +/* Event payload callbacks. */ +typedef void (*event_payload_free_cb)(void *); +typedef void (*event_payload_print_cb)(void *, struct evbuffer *); + /* Key binding and key table. */ struct key_binding { key_code key; @@ -2611,29 +2631,78 @@ char *format_grid_hyperlink(struct grid *, u_int, u_int, struct screen *); char *format_grid_line(struct grid *, u_int); -/* format-draw.c */ -void format_draw(struct screen_write_ctx *, - const struct grid_cell *, u_int, const char *, - struct style_ranges *, int); -u_int format_width(const char *); -char *format_trim_left(const char *, u_int); -char *format_trim_right(const char *, u_int); +/* events-payload.c */ +struct event_payload *event_payload_create(void); +void event_payload_free(struct event_payload *); +void printflike(2, 3) event_payload_log(struct event_payload *, const char *, + ...); +char *event_payload_item_print(struct event_payload_item *); +void event_payload_set_target(struct event_payload *, + struct cmd_find_state *); +int event_payload_get_target(struct event_payload *, + struct cmd_find_state *); +void printflike(3, 4) event_payload_set_string(struct event_payload *, + const char *, const char *, ...); +void event_payload_set_time(struct event_payload *, const char *, time_t); +void event_payload_set_int(struct event_payload *, const char *, int); +void event_payload_set_uint(struct event_payload *, const char *, u_int); +void event_payload_set_client(struct event_payload *, const char *, + struct client *); +void event_payload_set_session(struct event_payload *, const char *, + struct session *); +void event_payload_set_window(struct event_payload *, const char *, + struct window *); +void event_payload_set_pane(struct event_payload *, const char *, + struct window_pane *); +void event_payload_set_pointer(struct event_payload *, const char *, void *, + event_payload_free_cb, event_payload_print_cb); +const char *event_payload_get_string(struct event_payload *, const char *); +char *event_payload_print(struct event_payload *, const char *); +void event_payload_add_formats(struct event_payload *, + struct format_tree *, const char *); +struct event_payload_item *event_payload_first(struct event_payload *); +struct event_payload_item *event_payload_next(struct event_payload_item *); +const char *event_payload_item_name(struct event_payload_item *); +enum event_payload_type event_payload_item_type(struct event_payload_item *); +time_t event_payload_get_time(struct event_payload *, const char *); +int event_payload_get_int(struct event_payload *, const char *, int *); +int event_payload_get_uint(struct event_payload *, const char *, u_int *); +struct client *event_payload_get_client(struct event_payload *, const char *); +struct session *event_payload_get_session(struct event_payload *, const char *); +struct window *event_payload_get_window(struct event_payload *, const char *); +struct window_pane *event_payload_get_pane(struct event_payload *, + const char *); +void *event_payload_get_pointer(struct event_payload *, const char *); -/* notify.c */ -void notify_hook(struct cmdq_item *, const char *); -void notify_monitor_add(struct cmdq_item *, struct options *, +/* events.c */ +typedef void (*events_cb)(const char *, struct event_payload *, void *); +struct events_sink *events_add_sink(const char *, events_cb, void *); +void events_remove_sink(struct events_sink *); +void events_fire(const char *, struct event_payload *); +void events_fire_client(const char *, struct client *); +void events_fire_session(const char *, struct session *); +void events_fire_window(const char *, struct window *); +void events_fire_pane(const char *, struct window_pane *); +void events_fire_winlink(const char *, struct winlink *); + +/* format-draw.c */ +void format_draw(struct screen_write_ctx *, const struct grid_cell *, + u_int, const char *, struct style_ranges *, int); +u_int format_width(const char *); +char *format_trim_left(const char *, u_int); +char *format_trim_right(const char *, u_int); + +/* hooks.c */ +void hooks_add_event(const char *); +int hooks_valid_event_name(const char *); +void hooks_build_events(void); +void hooks_run(struct cmdq_item *, const char *); +void hooks_monitor_add(struct cmdq_item *, struct options *, const char *, enum monitor_type, int, const char *, struct cmd_find_state *, struct session *); -void notify_monitor_remove(struct options *, const char *); -void notify_monitor_free(void *); -char *notify_monitor_to_string(struct options_entry *); -void notify_client(const char *, struct client *); -void notify_session(const char *, struct session *); -void notify_winlink(const char *, struct winlink *); -void notify_session_window(const char *, struct session *, struct window *); -void notify_window(const char *, struct window *); -void notify_pane(const char *, struct window_pane *); -void notify_paste_buffer(const char *, int); +void hooks_monitor_remove(struct options *, const char *); +void hooks_monitor_free(void *); +char *hooks_monitor_to_string(struct options_entry *); /* options.c */ struct options *options_create(struct options *); @@ -3855,20 +3924,7 @@ void control_add_sub(struct client *, const char *, enum monitor_type, int, void control_remove_sub(struct client *, const char *); /* control-notify.c */ -void control_notify_pane_mode_changed(int); -void control_notify_window_layout_changed(struct window *); -void control_notify_window_pane_changed(struct window *); -void control_notify_window_unlinked(struct session *, struct window *); -void control_notify_window_linked(struct session *, struct window *); -void control_notify_window_renamed(struct window *); -void control_notify_client_session_changed(struct client *); -void control_notify_client_detached(struct client *); -void control_notify_session_renamed(struct session *); -void control_notify_session_created(struct session *); -void control_notify_session_closed(struct session *); -void control_notify_session_window_changed(struct session *); -void control_notify_paste_buffer_changed(const char *); -void control_notify_paste_buffer_deleted(const char *); +void control_build_events(void); /* session.c */ extern struct sessions sessions; diff --git a/tty-acs.c b/tty-acs.c index 3dab31b6f..eedb79c2f 100644 --- a/tty-acs.c +++ b/tty-acs.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-acs.c,v 1.13 2023/08/08 07:19:48 nicm Exp $ */ /* * Copyright (c) 2010 Nicholas Marriott diff --git a/tty-draw.c b/tty-draw.c index 825d81e49..d47be66bc 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-draw.c,v 1.14 2026/06/19 10:38:29 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/tty-features.c b/tty-features.c index ea6769dc6..614c63f62 100644 --- a/tty-features.c +++ b/tty-features.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-features.c,v 1.40 2026/07/01 06:17:58 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott diff --git a/tty-keys.c b/tty-keys.c index d6a2610ea..61e541165 100644 --- a/tty-keys.c +++ b/tty-keys.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-keys.c,v 1.210 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1008,10 +1008,10 @@ complete_key: if (key == KEYC_FOCUS_OUT) { c->flags &= ~CLIENT_FOCUSED; window_update_focus(c->session->curw->window); - notify_client("client-focus-out", c); + events_fire_client("client-focus-out", c); } else if (key == KEYC_FOCUS_IN) { c->flags |= CLIENT_FOCUSED; - notify_client("client-focus-in", c); + events_fire_client("client-focus-in", c); window_update_focus(c->session->curw->window); } diff --git a/tty-term.c b/tty-term.c index bea34fc42..c8ceb2e56 100644 --- a/tty-term.c +++ b/tty-term.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty-term.c,v 1.106 2026/06/13 09:17:29 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/tty.c b/tty.c index ce5012191..66ca0d6cf 100644 --- a/tty.c +++ b/tty.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tty.c,v 1.476 2026/06/26 11:36:22 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/utf8-combined.c b/utf8-combined.c index 923342b43..02abc3518 100644 --- a/utf8-combined.c +++ b/utf8-combined.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: utf8-combined.c,v 1.10 2026/06/09 11:49:36 nicm Exp $ */ /* * Copyright (c) 2023 Nicholas Marriott diff --git a/utf8.c b/utf8.c index 4d6ebcbd8..e634b43f1 100644 --- a/utf8.c +++ b/utf8.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: utf8.c,v 1.71 2026/05/12 09:37:25 nicm Exp $ */ /* * Copyright (c) 2008 Nicholas Marriott diff --git a/window-border.c b/window-border.c index 5420f4b97..0419015f4 100644 --- a/window-border.c +++ b/window-border.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-border.c,v 1.1 2026/06/22 08:47:46 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/window-buffer.c b/window-buffer.c index 6798e2027..f3116f8da 100644 --- a/window-buffer.c +++ b/window-buffer.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-buffer.c,v 1.49 2026/06/26 08:10:49 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/window-client.c b/window-client.c index 5a8c71862..64e38cb81 100644 --- a/window-client.c +++ b/window-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-client.c,v 1.46 2026/07/04 22:09:06 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/window-clock.c b/window-clock.c index cf7040009..acd4382b3 100644 --- a/window-clock.c +++ b/window-clock.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-clock.c,v 1.34 2026/06/25 16:32:42 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott diff --git a/window-copy.c b/window-copy.c index 8b117be92..997cb8888 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-copy.c,v 1.413 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -5764,7 +5764,7 @@ window_copy_copy_buffer(struct window_mode_entry *wme, const char *prefix, screen_write_setselection(&ctx, "", buf, len); screen_write_stop(&ctx); wp->flags |= redraw; - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); } if (set_paste) @@ -5849,7 +5849,7 @@ window_copy_append_selection(struct window_mode_entry *wme) screen_write_start_pane(&ctx, wp, NULL); screen_write_setselection(&ctx, "", buf, len); screen_write_stop(&ctx); - notify_pane("pane-set-clipboard", wp); + events_fire_pane("pane-set-clipboard", wp); } pb = paste_get_top(&bufname); diff --git a/window-customize.c b/window-customize.c index 09da37f2e..9f84540ed 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-customize.c,v 1.30 2026/07/06 14:40:57 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott diff --git a/window-switch.c b/window-switch.c index 99ea19985..d458f30af 100644 --- a/window-switch.c +++ b/window-switch.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-switch.c,v 1.1 2026/06/26 14:40:30 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott diff --git a/window-tree.c b/window-tree.c index 2d15eb536..1b19602fe 100644 --- a/window-tree.c +++ b/window-tree.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-tree.c,v 1.93 2026/07/04 22:09:06 nicm Exp $ */ /* * Copyright (c) 2017 Nicholas Marriott diff --git a/window-visible.c b/window-visible.c index f46967ec7..360e4fc88 100644 --- a/window-visible.c +++ b/window-visible.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-visible.c,v 1.4 2026/06/29 19:03:34 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott diff --git a/window.c b/window.c index 109f002ff..198296da4 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window.c,v 1.359 2026/07/10 13:38:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -75,8 +75,8 @@ static struct window_pane *window_pane_create(struct window *, u_int, u_int, static void window_pane_destroy(struct window_pane *); static void window_pane_free(struct window_pane *); static void window_pane_scrollbar_timer(int, short, void *); -static void window_pane_full_size_offset(struct window_pane *wp, - int *xoff, int *yoff, u_int *sx, u_int *sy); +static void window_pane_full_size_offset(struct window_pane *, int *, int *, + u_int *, u_int *); RB_GENERATE(windows, window, entry, window_cmp); RB_GENERATE(winlinks, winlink, entry, winlink_cmp); @@ -88,6 +88,7 @@ struct window_pane_prompt { status_prompt_input_cb inputcb; prompt_free_cb freecb; void *data; + enum prompt_type type; }; int @@ -96,6 +97,78 @@ window_cmp(struct window *w1, struct window *w2) return (w1->id - w2->id); } +static void +window_fire_renamed(struct window *w, const char *old_name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_window(&fs, w, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + event_payload_set_string(ep, "old_name", "%s", old_name); + event_payload_set_string(ep, "new_name", "%s", w->name); + events_fire("window-renamed", ep); +} + +static void +window_fire_pane_changed(struct window *w, struct window_pane *wp, + struct window_pane *lastwp) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_pane(ep, "new_pane", wp); + if (lastwp != NULL) + event_payload_set_pane(ep, "old_pane", lastwp); + events_fire("window-pane-changed", ep); +} + +static void +window_fire_pane_mode_changed(const char *name, struct window_pane *wp, + const char *previous, const char *current, int entered) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + + if (current != NULL) + event_payload_set_string(ep, "current_mode", "%s", current); + if (previous != NULL) + event_payload_set_string(ep, "previous_mode", "%s", previous); + event_payload_set_int(ep, "mode_entered", entered); + + events_fire(name, ep); +} + +static void +window_fire_pane_prompt(const char *name, struct window_pane *wp, + enum prompt_type type) +{ + struct event_payload *ep; + struct cmd_find_state fs; + const char *type_string = prompt_type_string(type); + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_string(ep, "prompt_type", "%s", type_string); + events_fire(name, ep); +} + int winlink_cmp(struct winlink *wl1, struct winlink *wl2) { @@ -446,13 +519,15 @@ window_pane_remove_ref(struct window_pane *wp, const char *from) void window_set_name(struct window *w, const char *new_name, int untrusted) { - char *name; + char *last, *name; name = clean_name(new_name, untrusted); if (name != NULL) { + last = xstrdup(w->name); free(w->name); w->name = name; - notify_window("window-renamed", w); + window_fire_renamed(w, last); + free(last); } } @@ -553,13 +628,13 @@ window_pane_update_focus(struct window_pane *wp) log_debug("%s: %%%u focus out", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[O", 3); - notify_pane("pane-focus-out", wp); + events_fire_pane("pane-focus-out", wp); wp->flags &= ~PANE_FOCUSED; } else if (focused && (~wp->flags & PANE_FOCUSED)) { log_debug("%s: %%%u focus in", __func__, wp->id); if (wp->base.mode & MODE_FOCUSON) bufferevent_write(wp->event, "\033[I", 3); - notify_pane("pane-focus-in", wp); + events_fire_pane("pane-focus-in", wp); wp->flags |= PANE_FOCUSED; } else log_debug("%s: %%%u focus unchanged", __func__, wp->id); @@ -595,7 +670,7 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify) server_redraw_window(w); if (notify) - notify_window("window-pane-changed", w); + window_fire_pane_changed(w, w->active, lastwp); return (1); } @@ -785,7 +860,8 @@ window_zoom(struct window_pane *wp) w->saved_layout_root = w->layout_root; layout_init(w, wp); w->flags |= WINDOW_ZOOMED; - notify_window("window-layout-changed", w); + events_fire_window("window-zoomed", w); + events_fire_window("window-layout-changed", w); redraw_invalidate_scene(w); return (0); @@ -811,8 +887,10 @@ window_unzoom(struct window *w, int notify) } layout_fix_panes(w, NULL); - if (notify) - notify_window("window-layout-changed", w); + if (notify) { + events_fire_window("window-unzoomed", w); + events_fire_window("window-layout-changed", w); + } redraw_invalidate_scene(w); return (0); @@ -894,7 +972,7 @@ window_lost_pane(struct window *w, struct window_pane *wp) if (w->active != NULL) { window_pane_stack_remove(&w->last_panes, w->active); w->active->flags |= PANE_CHANGED; - notify_window("window-pane-changed", w); + events_fire_window("window-pane-changed", w); window_update_focus(w); } } @@ -1223,9 +1301,9 @@ window_pane_destroy(struct window_pane *wp) window_pane_wait_finish(wp); spawn_editor_finish(wp); - window_pane_clear_prompt(wp); RB_REMOVE(window_pane_tree, &all_window_panes, wp); wp->flags |= PANE_DESTROYED; + window_pane_clear_prompt(wp); window_pane_free_modes(wp); screen_write_clear_dirty(wp); @@ -1352,6 +1430,8 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) { struct window_mode_entry *wme; struct window_pane_resize *r; + struct event_payload *ep; + struct cmd_find_state fs; if (sx == wp->sx && sy == wp->sy) return; @@ -1374,6 +1454,17 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy) wme = TAILQ_FIRST(&wp->modes); if (wme != NULL && wme->mode->resize != NULL) wme->mode->resize(wme, sx, sy); + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_uint(ep, "width", sx); + event_payload_set_uint(ep, "height", sy); + event_payload_set_uint(ep, "old_width", r->osx); + event_payload_set_uint(ep, "old_height", r->osy); + events_fire("pane-resized", ep); } int @@ -1383,9 +1474,13 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, { struct window_mode_entry *wme; struct window *w = wp->window; + const char *name = mode->name, *p = NULL; - if (!TAILQ_EMPTY(&wp->modes) && TAILQ_FIRST(&wp->modes)->mode == mode) - return (1); + if (!TAILQ_EMPTY(&wp->modes)) { + if (TAILQ_FIRST(&wp->modes)->mode == mode) + return (1); + p = TAILQ_FIRST(&wp->modes)->mode->name; + } TAILQ_FOREACH(wme, &wp->modes, entry) { if (wme->mode == mode) @@ -1411,7 +1506,9 @@ window_pane_set_mode(struct window_pane *wp, struct window_pane *swp, server_redraw_window_borders(wp->window); server_status_window(wp->window); - notify_pane("pane-mode-changed", wp); + + window_fire_pane_mode_changed("pane-mode-entered", wp, p, name, 1); + window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 1); return (0); } @@ -1422,11 +1519,13 @@ window_pane_reset_mode(struct window_pane *wp) struct window_mode_entry *wme, *next; struct window *w = wp->window; int kill; + const char *name, *p; if (TAILQ_EMPTY(&wp->modes)) return; wme = TAILQ_FIRST(&wp->modes); + p = wme->mode->name; kill = wme->kill; TAILQ_REMOVE(&wp->modes, wme, entry); wme->mode->free(wme); @@ -1443,13 +1542,16 @@ window_pane_reset_mode(struct window_pane *wp) if (next->mode->resize != NULL) next->mode->resize(next, wp->sx, wp->sy); } + name = (next == NULL ? NULL : next->mode->name); wp->flags |= (PANE_REDRAW|PANE_REDRAWSCROLLBAR|PANE_CHANGED); layout_fix_panes(w, NULL); server_redraw_window_borders(wp->window); server_status_window(wp->window); - notify_pane("pane-mode-changed", wp); + + window_fire_pane_mode_changed("pane-mode-exited", wp, p, name, 0); + window_fire_pane_mode_changed("pane-mode-changed", wp, p, name, 0); if (kill) server_kill_pane(wp); @@ -1512,6 +1614,7 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wpp->inputcb = inputcb; wpp->freecb = freecb; wpp->data = data; + wpp->type = type; memset(&pd, 0, sizeof pd); prompt_set_options(&pd, s); @@ -1529,19 +1632,28 @@ window_pane_set_prompt(struct window_pane *wp, struct client *c, wp->flags |= PANE_REDRAW; prompt_incremental_start(wp->prompt); + window_fire_pane_prompt("pane-prompt-opened", wp, type); } /* Close a pane prompt. */ void window_pane_clear_prompt(struct window_pane *wp) { - struct prompt *prompt = wp->prompt; + struct prompt *prompt = wp->prompt; + struct window_pane_prompt *wpp = wp->prompt_data; + enum prompt_type type = PROMPT_TYPE_INVALID; - if (prompt == NULL) - return; - wp->prompt = NULL; - prompt_free(prompt); - wp->flags |= PANE_REDRAW; + if (prompt != NULL) { + if (wpp != NULL) + type = wpp->type; + + wp->prompt = NULL; + prompt_free(prompt); + wp->flags |= PANE_REDRAW; + + if (~wp->flags & PANE_DESTROYED) + window_fire_pane_prompt("pane-prompt-closed", wp, type); + } } /* Does this pane have an open prompt? */ diff --git a/xmalloc.c b/xmalloc.c index 14e805ed9..b60b9dbee 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: xmalloc.c,v 1.14 2026/06/18 10:56:22 nicm Exp $ */ /* * Author: Tatu Ylonen diff --git a/xmalloc.h b/xmalloc.h index 93a4155e2..c9257b438 100644 --- a/xmalloc.h +++ b/xmalloc.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: xmalloc.h,v 1.5 2026/06/18 10:56:22 nicm Exp $ */ /* * Author: Tatu Ylonen diff --git a/xterm-keys.c b/xterm-keys.c deleted file mode 100644 index b10c10db7..000000000 --- a/xterm-keys.c +++ /dev/null @@ -1,252 +0,0 @@ -/* $OpenBSD$ */ - -/* - * Copyright (c) 2009 Nicholas Marriott - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER - * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING - * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include - -#include - -#include "tmux.h" - -/* - * xterm-style function keys append one of the following values before the last - * character: - * - * 2 Shift - * 3 Alt - * 4 Shift + Alt - * 5 Ctrl - * 6 Shift + Ctrl - * 7 Alt + Ctrl - * 8 Shift + Alt + Ctrl - * - * Rather than parsing them, just match against a table. - * - * There are three forms for F1-F4 (\\033O_P and \\033O1;_P and \\033[1;_P). - * We accept any but always output the latter (it comes first in the table). - */ - -static int xterm_keys_match(const char *, const char *, size_t, size_t *, - key_code *); -static int xterm_keys_modifiers(const char *, size_t, size_t *, - key_code *); - -struct xterm_keys_entry { - key_code key; - const char *template; -}; - -static const struct xterm_keys_entry xterm_keys_table[] = { - { KEYC_F1, "\033[1;_P" }, - { KEYC_F1, "\033O1;_P" }, - { KEYC_F1, "\033O_P" }, - { KEYC_F2, "\033[1;_Q" }, - { KEYC_F2, "\033O1;_Q" }, - { KEYC_F2, "\033O_Q" }, - { KEYC_F3, "\033[1;_R" }, - { KEYC_F3, "\033O1;_R" }, - { KEYC_F3, "\033O_R" }, - { KEYC_F4, "\033[1;_S" }, - { KEYC_F4, "\033O1;_S" }, - { KEYC_F4, "\033O_S" }, - { KEYC_F5, "\033[15;_~" }, - { KEYC_F6, "\033[17;_~" }, - { KEYC_F7, "\033[18;_~" }, - { KEYC_F8, "\033[19;_~" }, - { KEYC_F9, "\033[20;_~" }, - { KEYC_F10, "\033[21;_~" }, - { KEYC_F11, "\033[23;_~" }, - { KEYC_F12, "\033[24;_~" }, - { KEYC_UP, "\033[1;_A" }, - { KEYC_DOWN, "\033[1;_B" }, - { KEYC_RIGHT, "\033[1;_C" }, - { KEYC_LEFT, "\033[1;_D" }, - { KEYC_HOME, "\033[1;_H" }, - { KEYC_END, "\033[1;_F" }, - { KEYC_PPAGE, "\033[5;_~" }, - { KEYC_NPAGE, "\033[6;_~" }, - { KEYC_IC, "\033[2;_~" }, - { KEYC_DC, "\033[3;_~" }, - - { '!', "\033[27;_;33~" }, - { '#', "\033[27;_;35~" }, - { '(', "\033[27;_;40~" }, - { ')', "\033[27;_;41~" }, - { '+', "\033[27;_;43~" }, - { ',', "\033[27;_;44~" }, - { '-', "\033[27;_;45~" }, - { '.', "\033[27;_;46~" }, - { '0', "\033[27;_;48~" }, - { '1', "\033[27;_;49~" }, - { '2', "\033[27;_;50~" }, - { '3', "\033[27;_;51~" }, - { '4', "\033[27;_;52~" }, - { '5', "\033[27;_;53~" }, - { '6', "\033[27;_;54~" }, - { '7', "\033[27;_;55~" }, - { '8', "\033[27;_;56~" }, - { '9', "\033[27;_;57~" }, - { ':', "\033[27;_;58~" }, - { ';', "\033[27;_;59~" }, - { '<', "\033[27;_;60~" }, - { '=', "\033[27;_;61~" }, - { '>', "\033[27;_;62~" }, - { '?', "\033[27;_;63~" }, - { '\'', "\033[27;_;39~" }, - { '\r', "\033[27;_;13~" }, - { '\t', "\033[27;_;9~" }, -}; - -/* - * Match key against buffer, treating _ as a wildcard. Return -1 for no match, - * 0 for match, 1 if the end of the buffer is reached (need more data). - */ -static int -xterm_keys_match(const char *template, const char *buf, size_t len, - size_t *size, key_code *modifiers) -{ - size_t pos; - int retval; - - *modifiers = 0; - - if (len == 0) - return (0); - - pos = 0; - do { - if (*template == '_') { - retval = xterm_keys_modifiers(buf, len, &pos, - modifiers); - if (retval != 0) - return (retval); - continue; - } - if (buf[pos] != *template) - return (-1); - pos++; - } while (*++template != '\0' && pos != len); - - if (*template != '\0') /* partial */ - return (1); - - *size = pos; - return (0); -} - -/* Find modifiers from buffer. */ -static int -xterm_keys_modifiers(const char *buf, size_t len, size_t *pos, - key_code *modifiers) -{ - u_int flags; - - if (len - *pos < 2) - return (1); - - if (buf[*pos] < '0' || buf[*pos] > '9') - return (-1); - flags = buf[(*pos)++] - '0'; - if (buf[*pos] >= '0' && buf[*pos] <= '9') - flags = (flags * 10) + (buf[(*pos)++] - '0'); - flags -= 1; - - *modifiers = 0; - if (flags & 1) - *modifiers |= KEYC_SHIFT; - if (flags & 2) - *modifiers |= KEYC_ESCAPE; - if (flags & 4) - *modifiers |= KEYC_CTRL; - if (flags & 8) - *modifiers |= KEYC_ESCAPE; - return (0); -} - -/* - * Lookup key from a buffer against the table. Returns 0 for found (and the - * key), -1 for not found, 1 for partial match. - */ -int -xterm_keys_find(const char *buf, size_t len, size_t *size, key_code *key) -{ - const struct xterm_keys_entry *entry; - u_int i; - int matched; - key_code modifiers; - - for (i = 0; i < nitems(xterm_keys_table); i++) { - entry = &xterm_keys_table[i]; - - matched = xterm_keys_match(entry->template, buf, len, size, - &modifiers); - if (matched == -1) - continue; - if (matched == 0) - *key = (entry->key|modifiers|KEYC_XTERM); - return (matched); - } - return (-1); -} - -/* Lookup a key number from the table. */ -char * -xterm_keys_lookup(key_code key) -{ - const struct xterm_keys_entry *entry; - u_int i; - key_code modifiers; - char *out; - - modifiers = 1; - if (key & KEYC_SHIFT) - modifiers += 1; - if (key & KEYC_ESCAPE) - modifiers += 2; - if (key & KEYC_CTRL) - modifiers += 4; - - /* - * If the key has no modifiers, return NULL and let it fall through to - * the normal lookup. - */ - if (modifiers == 1) - return (NULL); - - /* - * If this has the escape modifier, but was not originally an xterm - * key, it may be a genuine escape + key. So don't pass it through as - * an xterm key or programs like vi may be confused. - */ - if ((key & (KEYC_ESCAPE|KEYC_XTERM)) == KEYC_ESCAPE) - return (NULL); - - /* Otherwise, find the key in the table. */ - key &= KEYC_MASK_KEY; - for (i = 0; i < nitems(xterm_keys_table); i++) { - entry = &xterm_keys_table[i]; - if (key == entry->key) - break; - } - if (i == nitems(xterm_keys_table)) - return (NULL); - - /* Copy the template and replace the modifier. */ - out = xstrdup(entry->template); - out[strcspn(out, "_")] = '0' + modifiers; - return (out); -} From 512255b9a7b37fd571d33e6ecfaeb2ba4039fc2e Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 15:20:06 +0000 Subject: [PATCH 02/47] Add formats and events for OSC 133 commmands, as well as a -T flag to set-hook -B to only fire when the format is true. --- cmd-set-option.c | 12 +++-- format.c | 123 ++++++++++++++++++++++++++++++++++++++++++++++- hooks.c | 6 +-- input.c | 87 ++++++++++++++++++++++++++++++--- monitor.c | 13 +++-- options-table.c | 5 +- tmux.1 | 29 ++++++++++- tmux.h | 14 ++++-- window.c | 3 +- 9 files changed, 263 insertions(+), 29 deletions(-) diff --git a/cmd-set-option.c b/cmd-set-option.c index 158cd5fac..335a54760 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-set-option.c,v 1.145 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-set-option.c,v 1.146 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -66,8 +66,8 @@ const struct cmd_entry cmd_set_hook_entry = { .name = "set-hook", .alias = NULL, - .args = { "agpERt:uB:w", 0, 2, cmd_set_option_args_parse }, - .usage = "[-agpERuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " + .args = { "agpERTt:uB:w", 0, 2, cmd_set_option_args_parse }, + .usage = "[-agpERTuw] [-B name:what:format] " CMD_TARGET_PANE_USAGE " " "[hook] [command]", .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, @@ -143,7 +143,7 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) char *expanded = NULL, *newvalue = NULL; const char *value, *old; enum monitor_type type; - int id, scope; + int id, scope, flags = 0; if (args_count(args) > 1) { cmdq_error(item, "too many arguments"); @@ -204,7 +204,9 @@ cmd_set_hook_monitor_exec(struct cmdq_item *item, struct args *args, int window) oo != global_s_options && oo != global_w_options) s = target->s; - hooks_monitor_add(item, oo, name, type, id, format, &fs, s); + if (args_has(args, 'T')) + flags |= MONITOR_NOTIFY_TRUE; + hooks_monitor_add(item, oo, name, type, id, format, flags, &fs, s); out: free(newvalue); diff --git a/format.c b/format.c index da374d580..6175f0d0d 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD: format.c,v 1.402 2026/07/09 06:33:19 nicm Exp $ */ +/* $OpenBSD: format.c,v 1.403 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2173,6 +2173,106 @@ format_cb_pane_dead_time(struct format_tree *ft) return (NULL); } +/* Callback for pane_last_output_time. */ +static void * +format_cb_pane_last_output_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_output_time != 0) { + tv.tv_sec = wp->last_output_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_last_prompt_time. */ +static void * +format_cb_pane_last_prompt_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->last_prompt_time != 0) { + tv.tv_sec = wp->last_prompt_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_start_time. */ +static void * +format_cb_pane_command_start_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_start_time != 0) { + tv.tv_sec = wp->cmd_start_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_end_time. */ +static void * +format_cb_pane_command_end_time(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + static struct timeval tv; + + if (wp != NULL && wp->cmd_end_time != 0) { + tv.tv_sec = wp->cmd_end_time; + tv.tv_usec = 0; + return (&tv); + } + return (NULL); +} + +/* Callback for pane_command_running. */ +static void * +format_cb_pane_command_running(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL) + return (format_printf("%d", !!(wp->flags & PANE_CMDRUNNING))); + return (NULL); +} + +/* Callback for pane_command_duration. */ +static void * +format_cb_pane_command_duration(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + time_t end; + + if (wp == NULL || wp->cmd_start_time == 0) + return (NULL); + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = wp->cmd_end_time; + if (end < wp->cmd_start_time) + end = wp->cmd_start_time; + return (format_printf("%lld", (long long)(end - wp->cmd_start_time))); +} + +/* Callback for pane_command_status. */ +static void * +format_cb_pane_command_status(struct format_tree *ft) +{ + struct window_pane *wp = ft->wp; + + if (wp != NULL && wp->cmd_status != -1) + return (format_printf("%d", wp->cmd_status)); + return (NULL); +} + /* Callback for pane_format. */ static void * format_cb_pane_format(struct format_tree *ft) @@ -3485,6 +3585,21 @@ static const struct format_table_entry format_table[] = { { "pane_bottom", FORMAT_TABLE_STRING, format_cb_pane_bottom }, + { "pane_command_duration", FORMAT_TABLE_STRING, + format_cb_pane_command_duration + }, + { "pane_command_end_time", FORMAT_TABLE_TIME, + format_cb_pane_command_end_time + }, + { "pane_command_running", FORMAT_TABLE_STRING, + format_cb_pane_command_running + }, + { "pane_command_start_time", FORMAT_TABLE_TIME, + format_cb_pane_command_start_time + }, + { "pane_command_status", FORMAT_TABLE_STRING, + format_cb_pane_command_status + }, { "pane_current_command", FORMAT_TABLE_STRING, format_cb_current_command }, @@ -3536,6 +3651,12 @@ static const struct format_table_entry format_table[] = { { "pane_last", FORMAT_TABLE_STRING, format_cb_pane_last }, + { "pane_last_output_time", FORMAT_TABLE_TIME, + format_cb_pane_last_output_time + }, + { "pane_last_prompt_time", FORMAT_TABLE_TIME, + format_cb_pane_last_prompt_time + }, { "pane_left", FORMAT_TABLE_STRING, format_cb_pane_left }, diff --git a/hooks.c b/hooks.c index 0ac64b8bd..fd55c7273 100644 --- a/hooks.c +++ b/hooks.c @@ -1,4 +1,4 @@ -/* $OpenBSD: hooks.c,v 1.12 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: hooks.c,v 1.13 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -407,7 +407,7 @@ hooks_monitor_cb(struct monitor_change *change, void *data) void hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, const char *name, enum monitor_type type, int id, const char *format, - struct cmd_find_state *fs, struct session *s) + int flags, struct cmd_find_state *fs, struct session *s) { struct options_entry *o; struct hook_monitor *hm; @@ -426,7 +426,7 @@ hooks_monitor_add(__unused struct cmdq_item *item, struct options *oo, hm->set = monitor_create_session(s, hooks_monitor_cb, hm); hm->sink = events_add_sink(name, hooks_monitor_hook_cb, hm); options_set_monitor_data(o, hm); - monitor_add(hm->set, name, type, id, format, 0); + monitor_add(hm->set, name, type, id, format, flags); } /* Convert a hook monitor to its value. */ diff --git a/input.c b/input.c index bfab97a7b..fe7297dae 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.265 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.266 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1034,6 +1034,8 @@ input_parse_pane(struct window_pane *wp) size_t new_size; new_data = window_pane_get_new_data(wp, &wp->offset, &new_size); + if (new_size != 0) + wp->last_output_time = time(NULL); input_parse_buffer(wp, new_data, new_size); window_pane_update_used_data(wp, &wp->offset, new_size); } @@ -3126,7 +3128,8 @@ input_osc_12(struct input_ctx *ictx, const char *p) c = ictx->ctx.s->ccolour; if (c == -1) c = ictx->ctx.s->default_ccolour; - input_osc_colour_reply(ictx, 1, 12, 0, c, ictx->input_end); + input_osc_colour_reply(ictx, 1, 12, 0, c, + ictx->input_end); } return; } @@ -3146,24 +3149,92 @@ input_osc_112(struct input_ctx *ictx, const char *p) screen_set_cursor_colour(ictx->ctx.s, -1); } +/* Fire an OSC 133 command event. */ +static void +input_fire_command_event(struct window_pane *wp, const char *name) +{ + struct event_payload *ep; + struct cmd_find_state fs; + time_t tstart = wp->cmd_start_time, end; + time_t tend = wp->cmd_end_time; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + event_payload_set_window(ep, "window", wp->window); + event_payload_set_pane(ep, "pane", wp); + + if (wp->cmd_status != -1) + event_payload_set_int(ep, "command_status", wp->cmd_status); + if (tstart != 0) + event_payload_set_time(ep, "command_start_time", tstart); + if (tend != 0) + event_payload_set_time(ep, "command_end_time", tend); + + if (tstart != 0) { + if (wp->flags & PANE_CMDRUNNING) + end = time(NULL); + else + end = tend; + if (end < tstart) + end = tstart; + end -= tstart; + event_payload_set_uint(ep, "command_duration", end); + } + + events_fire(name, ep); +} + /* Handle the OSC 133 sequence. */ static void input_osc_133(struct input_ctx *ictx, const char *p) { + struct window_pane *wp = ictx->wp; struct grid *gd = ictx->ctx.s->grid; u_int line = ictx->ctx.s->cy + gd->hsize; - struct grid_line *gl; + struct grid_line *gl = NULL; + const char *errstr; + int status; - if (line > gd->hsize + gd->sy - 1) - return; - gl = grid_get_line(gd, line); + if (line <= gd->hsize + gd->sy - 1) + gl = grid_get_line(gd, line); switch (*p) { case 'A': - gl->flags |= GRID_LINE_START_PROMPT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_PROMPT; + if (wp != NULL) { + wp->last_prompt_time = time(NULL); + events_fire_pane("pane-shell-prompt", wp); + } break; case 'C': - gl->flags |= GRID_LINE_START_OUTPUT; + if (gl != NULL) + gl->flags |= GRID_LINE_START_OUTPUT; + if (wp != NULL) { + wp->cmd_start_time = time(NULL); + wp->cmd_end_time = 0; + wp->flags |= PANE_CMDRUNNING; + wp->cmd_status = -1; + input_fire_command_event(wp, "pane-command-started"); + } + break; + case 'D': + if (wp != NULL) { + wp->cmd_end_time = time(NULL); + wp->flags &= ~PANE_CMDRUNNING; + wp->cmd_status = -1; + if (p[1] == ';' && p[2] != '\0') { + status = strtonum(p + 2, 0, INT_MAX, &errstr); + if (errstr == NULL) + wp->cmd_status = status; + } + input_fire_command_event(wp, "pane-command-finished"); + } break; } } diff --git a/monitor.c b/monitor.c index 514af4e3c..d6338e878 100644 --- a/monitor.c +++ b/monitor.c @@ -1,4 +1,4 @@ -/* $OpenBSD: monitor.c,v 1.5 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: monitor.c,v 1.6 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -53,7 +53,7 @@ struct monitor_item { enum monitor_type type; u_int id; - u_int flags; + int flags; char *last; struct monitor_panes panes; @@ -198,7 +198,9 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, { if (*last == NULL) { *last = value; - if (me->flags & MONITOR_NOTIFY_INITIAL) + if ((me->flags & MONITOR_NOTIFY_INITIAL) && + ((~me->flags & MONITOR_NOTIFY_TRUE) || + format_true(value))) monitor_report(ms, me, s, wl, wp, value, NULL); return; } @@ -208,7 +210,8 @@ monitor_check_value(struct monitor_set *ms, struct monitor_item *me, return; } - monitor_report(ms, me, s, wl, wp, value, *last); + if ((~me->flags & MONITOR_NOTIFY_TRUE) || format_true(value)) + monitor_report(ms, me, s, wl, wp, value, *last); free(*last); *last = value; } @@ -621,7 +624,7 @@ fail: /* Add a subscription. */ void monitor_add(struct monitor_set *ms, const char *name, enum monitor_type type, - int id, const char *format, u_int flags) + int id, const char *format, int flags) { struct monitor_item *me, find = { .name = (char *)name }; struct timeval tv = { .tv_sec = 1 }; diff --git a/options-table.c b/options-table.c index 082ed06ff..49cf9730f 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.232 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.233 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1934,6 +1934,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), + OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), OPTIONS_TABLE_PANE_HOOK("pane-created", ""), OPTIONS_TABLE_PANE_HOOK("pane-died", ""), OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), @@ -1946,6 +1948,7 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), + OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", ""), OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), OPTIONS_TABLE_HOOK("session-created", ""), diff --git a/tmux.1 b/tmux.1 index 215878dea..262187889 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1131 2026/07/10 13:38:45 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1132 2026/07/10 15:20:06 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -6359,6 +6359,17 @@ Run when a client switches to a dark theme. Run when a command fails. .It marked\-pane\-changed Run when the marked pane is set or cleared. +.It pane\-command\-finished +Run when an OSC 133 command finishes in a pane. +The event payload includes +.Ql hook_command_start_time , +.Ql hook_command_end_time , +.Ql hook_command_duration +and +.Ql hook_command_status +if present. +.It pane\-command\-started +Run when an OSC 133 command starts in a pane. .It pane\-created Run when a pane is created or respawned. .It pane\-died @@ -6391,6 +6402,8 @@ Run when a pane is resized. Run when the terminal clipboard is set using the .Xr xterm 1 escape sequence. +.It pane\-shell\-prompt +Run when an OSC 133 shell prompt starts in a pane. .It pane\-title\-changed Run when a pane title is changed. .It session\-closed @@ -6425,7 +6438,7 @@ Run when a window is unlinked from a session. Hooks are managed with these commands: .Bl -tag -width Ds .It Xo Ic set\-hook -.Op Fl agpERuw +.Op Fl agpERTuw .Op Fl B Ar name:what:format .Op Fl t Ar target\-pane .Ar hook\-name @@ -6456,6 +6469,11 @@ is the hook to run, selects the session, pane, all panes, window, or all windows, and .Ar format is expanded once a second. +With +.Fl T , +the hook is run only when +.Ar format +is true. For monitor hooks, .Ar name must begin with @@ -7191,6 +7209,11 @@ The following variables are available, where appropriate: .It Li "pane_at_top" Ta "" Ta "1 if pane is at the top of window" .It Li "pane_bg" Ta "" Ta "Pane background colour" .It Li "pane_bottom" Ta "" Ta "Bottom of pane" +.It Li "pane_command_duration" Ta "" Ta "Current or most recent OSC 133 command duration in seconds" +.It Li "pane_command_end_time" Ta "" Ta "Time most recent OSC 133 command ended" +.It Li "pane_command_running" Ta "" Ta "1 if an OSC 133 command is running" +.It Li "pane_command_start_time" Ta "" Ta "Time current or most recent OSC 133 command started" +.It Li "pane_command_status" Ta "" Ta "Exit status from most recent OSC 133 command" .It Li "pane_current_command" Ta "" Ta "Current command if available" .It Li "pane_current_path" Ta "" Ta "Current path if available" .It Li "pane_dead" Ta "" Ta "1 if pane is dead" @@ -7208,6 +7231,8 @@ The following variables are available, where appropriate: .It Li "pane_input_off" Ta "" Ta "1 if input to pane is disabled" .It Li "pane_key_mode" Ta "" Ta "Extended key reporting mode in this pane" .It Li "pane_last" Ta "" Ta "1 if last pane" +.It Li "pane_last_output_time" Ta "" Ta "Time pane last produced output" +.It Li "pane_last_prompt_time" Ta "" Ta "Time most recent OSC 133 prompt began" .It Li "pane_left" Ta "" Ta "Left of pane" .It Li "pane_marked" Ta "" Ta "1 if this is the marked pane" .It Li "pane_marked_set" Ta "" Ta "1 if a marked pane is set" diff --git a/tmux.h b/tmux.h index 076f4bf7c..b3a3a9796 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1394 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1395 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1267,6 +1267,7 @@ struct window_pane { #define PANE_UNSEENCHANGES 0x4000 #define PANE_REDRAWSCROLLBAR 0x8000 #define PANE_DESTROYED 0x10000 +#define PANE_CMDRUNNING 0x20000 bitstr_t *sync_dirty; u_int sync_dirty_size; @@ -1289,6 +1290,12 @@ struct window_pane { struct cmdq_item *wait_item; /* new-pane -W: waiting for pane exit */ struct spawn_editor_state *editor; + time_t last_output_time; + time_t last_prompt_time; + time_t cmd_start_time; + time_t cmd_end_time; + int cmd_status; + int fd; struct bufferevent *event; @@ -2293,6 +2300,7 @@ enum monitor_type { MONITOR_ALL_WINDOWS }; #define MONITOR_NOTIFY_INITIAL 0x1 +#define MONITOR_NOTIFY_TRUE 0x2 struct monitor_change { const char *name; const char *value; @@ -2699,7 +2707,7 @@ void hooks_build_events(void); void hooks_run(struct cmdq_item *, const char *); void hooks_monitor_add(struct cmdq_item *, struct options *, const char *, enum monitor_type, int, const char *, - struct cmd_find_state *, struct session *); + int, struct cmd_find_state *, struct session *); void hooks_monitor_remove(struct options *, const char *); void hooks_monitor_free(void *); char *hooks_monitor_to_string(struct options_entry *); @@ -3901,7 +3909,7 @@ void monitor_destroy(struct monitor_set *); int monitor_parse(const char *, char **, enum monitor_type *, int *, char **); void monitor_add(struct monitor_set *, const char *, enum monitor_type, int, - const char *, u_int); + const char *, int); void monitor_remove(struct monitor_set *, const char *); /* control.c */ diff --git a/window.c b/window.c index 198296da4..f6e1a1f23 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.359 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.360 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1177,6 +1177,7 @@ window_pane_create(struct window *w, u_int sx, u_int sy, u_int hlimit) wp->window = w; wp->options = options_create(w->options); wp->flags = PANE_STYLECHANGED; + wp->cmd_status = -1; wp->id = next_window_pane_id++; RB_INSERT(window_pane_tree, &all_window_panes, wp); From 7f2b0ae16efac76402055bd6a3b3b973f3eaa576 Mon Sep 17 00:00:00 2001 From: nicm Date: Fri, 10 Jul 2026 15:45:11 +0000 Subject: [PATCH 03/47] Do not make pty fds blocking again until all the data has been consumed or control mode clients can get stuck, GitHub issue 5356 from Ben Maurer. --- client.c | 29 +++++++++++------------------ control.c | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- tmux.h | 3 ++- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/client.c b/client.c index 1b7fdb043..f413dc819 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: client.c,v 1.165 2025/04/25 12:25:32 nicm Exp $ */ +/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -240,9 +240,8 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, pid_t ppid; enum msgtype msg; struct termios tio, saved_tio; - size_t size, linesize = 0; - ssize_t linelen; - char *line = NULL, **caps = NULL, *cause; + size_t size; + char **caps = NULL, *cause; u_int ncaps = 0; struct args_value *values; @@ -400,11 +399,6 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, client_exec(client_execshell, client_execcmd); } - /* Restore streams to blocking. */ - setblocking(STDIN_FILENO, 1); - setblocking(STDOUT_FILENO, 1); - setblocking(STDERR_FILENO, 1); - /* Print the exit message, if any, and exit. */ if (client_attached) { if (client_exitreason != CLIENT_EXIT_NONE) @@ -419,15 +413,8 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, else printf("%%exit\n"); fflush(stdout); - if (client_flags & CLIENT_CONTROL_WAITEXIT) { - setvbuf(stdin, NULL, _IOLBF, 0); - for (;;) { - linelen = getline(&line, &linesize, stdin); - if (linelen <= 1) - break; - } - free(line); - } + if (client_flags & CLIENT_CONTROL_WAITEXIT) + control_wait_exit(STDIN_FILENO); if (client_flags & CLIENT_CONTROLCONTROL) { printf("\033\\"); fflush(stdout); @@ -435,6 +422,12 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags, } } else if (client_exitreason != CLIENT_EXIT_NONE) fprintf(stderr, "%s\n", client_exit_message()); + + /* Restore the streams to blocking. */ + setblocking(STDIN_FILENO, 1); + setblocking(STDOUT_FILENO, 1); + setblocking(STDERR_FILENO, 1); + return (client_exitval); } diff --git a/control.c b/control.c index 220267b6c..8b4ad3884 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD: control.c,v 1.60 2026/07/10 07:25:05 nicm Exp $ */ +/* $OpenBSD: control.c,v 1.61 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott @@ -19,7 +19,9 @@ #include +#include #include +#include #include #include #include @@ -486,6 +488,53 @@ control_all_done(struct client *c) return (EVBUFFER_LENGTH(cs->write_event->output) == 0); } +/* + * Wait for the terminal to send an empty line or close, used by a control + * client after printing %exit so a wrapping terminal (such as iTerm2) can + * finish reading. + */ +void +control_wait_exit(int fd) +{ + struct pollfd pfd; + struct evbuffer *evb; + char *line; + int n; + + evb = evbuffer_new(); + if (evb == NULL) + fatalx("out of memory"); + + for (;;) { + line = evbuffer_readln(evb, NULL, EVBUFFER_EOL_LF); + if (line != NULL) { + if (*line == '\0') { /* empty line, stop */ + free(line); + break; + } + free(line); + continue; /* drain buffered lines first */ + } + + memset(&pfd, 0, sizeof pfd); + pfd.fd = fd; + pfd.events = POLLIN; + if (poll(&pfd, 1, INFTIM) == -1) { + if (errno == EINTR) + continue; + break; + } + + n = evbuffer_read(evb, fd, -1); + if (n == 0) + break; + if (n == -1 && errno != EAGAIN && errno != EINTR) + break; + } + + evbuffer_free(evb); +} + /* Flush all blocks until output. */ static void control_flush_all_blocks(struct client *c) diff --git a/tmux.h b/tmux.h index b3a3a9796..5031a581f 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1395 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1396 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3917,6 +3917,7 @@ void control_discard(struct client *); void control_start(struct client *); void control_ready(struct client *); void control_stop(struct client *); +void control_wait_exit(int); void control_set_pane_on(struct client *, struct window_pane *); void control_set_pane_off(struct client *, struct window_pane *); void control_continue_pane(struct client *, struct window_pane *); From 28a1231100b3e589400af8f3e5d2ff349e7b61e5 Mon Sep 17 00:00:00 2001 From: Yayo Razo Date: Sat, 11 Jul 2026 15:53:08 -0600 Subject: [PATCH 04/47] fix(screen): free alternate-screen images on screen destroy --- screen.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/screen.c b/screen.c index 62a079270..9f5177241 100644 --- a/screen.c +++ b/screen.c @@ -152,6 +152,10 @@ screen_reset_hyperlinks(struct screen *s) void screen_free(struct screen *s) { +#ifdef ENABLE_SIXEL + struct image *im; +#endif + free(s->sel); free(s->tabs); free(s->path); @@ -169,6 +173,14 @@ screen_free(struct screen *s) screen_free_titles(s); #ifdef ENABLE_SIXEL + /* + * Images saved when entering the alternate screen stay linked in the + * global list; move them back so they are freed and unlinked here, or + * a later eviction would write through this freed screen. + */ + TAILQ_CONCAT(&s->images, &s->saved_images, entry); + TAILQ_FOREACH(im, &s->images, entry) + im->list = &s->images; image_free_all(s); #endif } From 3ff24a8eb452272b72d627fb25fba38a6790b5ba Mon Sep 17 00:00:00 2001 From: Yayo Razo Date: Sat, 11 Jul 2026 14:22:48 -0600 Subject: [PATCH 05/47] fix(compat): guard buffer size multiplication in getline --- compat/getline.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/compat/getline.c b/compat/getline.c index 90437ccb9..feabdd43c 100644 --- a/compat/getline.c +++ b/compat/getline.c @@ -74,8 +74,14 @@ getdelim(char **buf, size_t *bufsiz, int delimiter, FILE *fp) } if (ptr + 2 >= eptr) { char *nbuf; - size_t nbufsiz = *bufsiz * 2; + size_t nbufsiz; ssize_t d = ptr - *buf; + + if (*bufsiz > SIZE_MAX / 2) { + errno = EOVERFLOW; + return -1; + } + nbufsiz = *bufsiz * 2; if ((nbuf = realloc(*buf, nbufsiz)) == NULL) return -1; *buf = nbuf; From b602dc57b1cdca30bb359f5a28c7e3840cbcd6a5 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Sun, 12 Jul 2026 17:24:37 +0100 Subject: [PATCH 06/47] github actions: automate updates --- .../workflows/update-openbsd-and-portable.yml | 165 ++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 .github/workflows/update-openbsd-and-portable.yml diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml new file mode 100644 index 000000000..a9e3e4e1b --- /dev/null +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -0,0 +1,165 @@ +name: Update OpenBSD tmux and portable + +on: + workflow_dispatch: + schedule: + - cron: "17 3 * * *" + +permissions: + contents: write + +concurrency: + group: update-openbsd-and-portable + cancel-in-progress: false + +env: + OPENBSD_SRC_URL: https://github.com/openbsd/src.git + OPENBSD_SRC_DIR: openbsd-src + CUTOVER_DIR: tmux-openbsd-cutover + PORTABLE_DIR: tmux-portable + PORTABLE_REPO: ${{ vars.TMUX_PORTABLE_REPO }} + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Configure Git identity + run: | + git config --global user.name "tmux update bot" + git config --global user.email "actions@github.com" + + - name: Install git-filter-repo + run: python3 -m pip install --user git-filter-repo + + - name: Checkout cutover repository + uses: actions/checkout@v4 + with: + path: ${{ env.CUTOVER_DIR }} + fetch-depth: 0 + token: ${{ secrets.UPDATE_TOKEN || github.token }} + + - name: Check portable repo setting + run: | + if [ -z "$PORTABLE_REPO" ]; then + echo "Set repository variable TMUX_PORTABLE_REPO, for example owner/tmux-portable." >&2 + exit 1 + fi + + - name: Checkout portable repository + uses: actions/checkout@v4 + with: + repository: ${{ env.PORTABLE_REPO }} + path: ${{ env.PORTABLE_DIR }} + fetch-depth: 0 + token: ${{ secrets.UPDATE_TOKEN }} + + - name: Restore OpenBSD source clone cache + id: openbsd-cache + uses: actions/cache/restore@v4 + with: + path: ${{ env.OPENBSD_SRC_DIR }} + key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} + restore-keys: | + openbsd-src-blobless-${{ runner.os }}- + + - name: Refresh OpenBSD source clone + run: | + set -eu + + if [ ! -d "$OPENBSD_SRC_DIR/.git" ]; then + git clone --filter=blob:none --no-tags --single-branch --branch master \ + "$OPENBSD_SRC_URL" "$OPENBSD_SRC_DIR" + fi + + cd "$OPENBSD_SRC_DIR" + git remote set-url origin "$OPENBSD_SRC_URL" + git fetch --filter=blob:none --no-tags origin master + git switch -C master origin/master + + - name: Filter OpenBSD tmux history + run: | + set -eu + + cd "$OPENBSD_SRC_DIR" + git switch -C tmux-openbsd origin/master + git filter-repo \ + --force \ + --refs tmux-openbsd \ + --path usr.bin/tmux/ \ + --path-rename usr.bin/tmux/: \ + --replace-refs delete-no-add + + git push "../$CUTOVER_DIR" tmux-openbsd:refs/heads/openbsd-git + + - name: Update cutover master + id: cutover + run: | + set -eu + + cd "$CUTOVER_DIR" + git switch master + + base_ref=refs/openbsd/import-base + if ! git rev-parse --verify -q "$base_ref" >/dev/null; then + echo "$base_ref is missing; initialise it before running updates." >&2 + exit 1 + fi + + old_base=$(git rev-parse "$base_ref") + new_base=$(git rev-parse openbsd-git) + + if [ "$old_base" = "$new_base" ]; then + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No new OpenBSD tmux commits to import." + exit 0 + fi + + if ! git merge-base --is-ancestor "$old_base" "$new_base"; then + echo "openbsd-git is not a descendant of $base_ref; aborting." >&2 + exit 1 + fi + + git cherry-pick "$old_base..$new_base" + git update-ref "$base_ref" "$new_base" + echo "changed=true" >> "$GITHUB_OUTPUT" + + - name: Push cutover repository + if: steps.cutover.outputs.changed == 'true' + run: | + set -eu + + cd "$CUTOVER_DIR" + git push origin master openbsd-git + + - name: Merge cutover into portable + if: steps.cutover.outputs.changed == 'true' + run: | + set -eu + + cd "$PORTABLE_DIR" + git config remote.tmux-openbsd.tagOpt --no-tags + + if git remote get-url tmux-openbsd >/dev/null 2>&1; then + git remote set-url tmux-openbsd "../$CUTOVER_DIR" + else + git remote add tmux-openbsd "../$CUTOVER_DIR" + fi + + git fetch --no-tags tmux-openbsd + git switch master + git merge --no-ff --log tmux-openbsd/master + + - name: Push portable repository + if: steps.cutover.outputs.changed == 'true' + run: | + set -eu + + cd "$PORTABLE_DIR" + git push origin master + + - name: Save OpenBSD source clone cache + if: always() + uses: actions/cache/save@v4 + with: + path: ${{ env.OPENBSD_SRC_DIR }} + key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} From 5e3db12c07b23cb99cf04df49922fc0854e81407 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 12 Jul 2026 21:02:30 +0100 Subject: [PATCH 07/47] No need for ASAN_OPTIONS. --- .github/workflows/regress.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/regress.yml b/.github/workflows/regress.yml index 050acc072..8f6633624 100644 --- a/.github/workflows/regress.yml +++ b/.github/workflows/regress.yml @@ -75,7 +75,6 @@ jobs: - name: test run: | cd regress - export ASAN_OPTIONS="abort_on_error=1:detect_leaks=0" ${{ matrix.make }} - name: logs From 8ac3fcedcaa61bc70a47aebdc50d503034ff54e5 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 12 Jul 2026 22:03:16 +0100 Subject: [PATCH 08/47] Do not reuse sockets so much to avoid problems with slow server shutdown. --- regress/command-order.sh | 6 +++--- regress/conf-syntax.sh | 7 +++++-- regress/control-client-size.sh | 7 ++++--- regress/new-session-size.sh | 6 +++--- regress/screen-redraw-popups.sh | 21 +++++++++++++++------ 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/regress/command-order.sh b/regress/command-order.sh index 9c957246a..cddd9b29e 100644 --- a/regress/command-order.sh +++ b/regress/command-order.sh @@ -4,11 +4,10 @@ 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 +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) -trap "rm -f $TMP" 0 1 15 +trap 'rm -f "$TMP"; $TMUX kill-server 2>/dev/null' 0 1 15 cat <$TMP new -sfoo -nfoo0; neww -nfoo1; neww -nfoo2 @@ -27,6 +26,7 @@ foo,foo1 foo,foo2 EOF +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" cat <$TMP new -sfoo -nfoo0 neww -nfoo1 diff --git a/regress/conf-syntax.sh b/regress/conf-syntax.sh index 4dbc384b3..562ac76ad 100644 --- a/regress/conf-syntax.sh +++ b/regress/conf-syntax.sh @@ -4,10 +4,13 @@ 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 +TMUX= +n=0 +trap '[ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null' 0 1 15 for i in conf/*.conf; do + n=$((n + 1)) + TMUX="$TEST_TMUX -LtestA$$-$n -f/dev/null" $TMUX -f/dev/null start \; source -n $i || exit 1 done diff --git a/regress/control-client-size.sh b/regress/control-client-size.sh index e6742608a..3099b873f 100644 --- a/regress/control-client-size.sh +++ b/regress/control-client-size.sh @@ -8,12 +8,11 @@ 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 +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) OUT=$(mktemp) -trap "rm -f $TMP $OUT" 0 1 15 +trap 'rm -f "$TMP" "$OUT"; $TMUX kill-server 2>/dev/null' 0 1 15 $TMUX -f/dev/null new -d || exit 1 sleep 1 @@ -26,6 +25,7 @@ $TMUX ls -F':#{window_width} #{window_height}' >>$OUT printf ":80 24\n:100 50\n"|cmp -s $OUT - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" $TMUX -f/dev/null new -d || exit 1 sleep 1 cat <$TMP @@ -37,6 +37,7 @@ $TMUX ls -F':#{window_width} #{window_height}' >>$OUT printf ":80 24\n:80 24\n"|cmp -s $OUT - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-3 -f/dev/null" cat <$TMP ls -F':#{window_width} #{window_height}' refresh -C 80,24 diff --git a/regress/new-session-size.sh b/regress/new-session-size.sh index 6a35abbbd..422ac69fb 100644 --- a/regress/new-session-size.sh +++ b/regress/new-session-size.sh @@ -6,11 +6,10 @@ 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 +TMUX="$TEST_TMUX -LtestA$$-1 -f/dev/null" TMP=$(mktemp) -trap "rm -f $TMP" 0 1 15 +trap 'rm -f "$TMP"; $TMUX kill-server 2>/dev/null' 0 1 15 $TMUX -f/dev/null new -d $TMP printf "80 24\n"|cmp -s $TMP - || exit 1 $TMUX kill-server 2>/dev/null +TMUX="$TEST_TMUX -LtestA$$-2 -f/dev/null" $TMUX -f/dev/null new -d -x 100 -y 50 $TMP diff --git a/regress/screen-redraw-popups.sh b/regress/screen-redraw-popups.sh index 8ad3ba2eb..26e0f7175 100644 --- a/regress/screen-redraw-popups.sh +++ b/regress/screen-redraw-popups.sh @@ -17,13 +17,19 @@ LC_ALL=C.UTF-8 export TERM LC_ALL [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) -TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" -TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" +TMUX= +TMUX2= +SETUP=0 RESULTS=screen-redraw-results TMP=$(mktemp) -trap "rm -f $TMP; $TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null" \ - 0 1 15 + +cleanup() { + rm -f "$TMP" + [ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null + [ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null +} +trap cleanup 0 1 15 fail() { echo "$*" >&2 @@ -46,8 +52,11 @@ C="sh -c 'i=0; while [ \$i -lt 13 ]; do printf \"POP%02d abcdefghij\n\" \$i; i=\ # setup: fresh inner window attached inside a fresh outer pane, 40x14. setup() { - $TMUX kill-server 2>/dev/null - $TMUX2 kill-server 2>/dev/null + [ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null + [ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null + SETUP=$((SETUP + 1)) + TMUX="$TEST_TMUX -LtestA$$-$SETUP -f/dev/null" + TMUX2="$TEST_TMUX -LtestB$$-$SETUP -f/dev/null" $TMUX2 new -d -x40 -y14 "$C" || exit 1 $TMUX2 set -g status off || exit 1 $TMUX2 set -g window-size manual || exit 1 From d77c9dc6aa021e4bc61f0da128c591af695e6466 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Sun, 12 Jul 2026 23:18:47 +0100 Subject: [PATCH 09/47] Tweak BIDI test. --- regress/screen-redraw-bidi.sh | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/regress/screen-redraw-bidi.sh b/regress/screen-redraw-bidi.sh index ee9c2fa16..aa8d21627 100644 --- a/regress/screen-redraw-bidi.sh +++ b/regress/screen-redraw-bidi.sh @@ -42,6 +42,25 @@ compare() { fi } +wait_for_pane() { + i=0 + while [ $i -lt 50 ]; do + case "$($TMUX2 capturep -p -t "$1")" in + *"$2"*) return ;; + esac + i=$((i + 1)) + sleep 0.1 + done + fail "pane $1 did not contain: $2" +} + +attach_scene() { + # Start with a fresh outer screen and attach only after the complete + # scene exists, so it receives one initial redraw. + $TMUX respawnp -k "$TMUX2 attach" || exit 1 + sleep 1 +} + new_scene() { $TMUX2 neww -d "sh -c 'printf \"BIDI-LEFT 12345\nBIDI-RIGHT 67890\"; exec sleep 100'" || exit 1 $TMUX2 selectw -t:\$ || exit 1 @@ -63,17 +82,19 @@ $TMUX new -d -x40 -y12 || exit 1 $TMUX set -g status off || exit 1 $TMUX set -g window-size manual || exit 1 $TMUX set -g default-terminal "tmux-256color" || exit 1 -$TMUX send -l "$TMUX2 attach" || exit 1 -$TMUX send Enter || exit 1 -sleep 1 # Single pane: content is wrapped in isolates. new_scene +wait_for_pane ':.0' 'BIDI-RIGHT 67890' +attach_scene compare bidi-single # Two panes: borders and both panes are isolated. new_scene $TMUX2 splitw -h "$C" || exit 1 +wait_for_pane ':.0' 'BIDI-RIGHT 67890' +wait_for_pane ':.1' 'BIDI-PANE 67890' +attach_scene compare bidi-split exit 0 From a662981f106b8c2ee9e4669f4134177a9f4966b3 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 07:08:05 +0100 Subject: [PATCH 10/47] Bump a time limit. --- regress/screen-redraw-tiled.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/regress/screen-redraw-tiled.sh b/regress/screen-redraw-tiled.sh index 3b32478f0..de7c57aff 100644 --- a/regress/screen-redraw-tiled.sh +++ b/regress/screen-redraw-tiled.sh @@ -49,16 +49,17 @@ compare() { fi } +C="sh -c 'exec sleep 1000'" + # Fresh inner window of fixed size, with one command running. new_scene() { $TMUX2 selectw -t:0 || exit 1 $TMUX2 kill-window -a 2>/dev/null - $TMUX2 neww -d "sh -c 'exec sleep 100'" || exit 1 + $TMUX2 neww -d "$C" || exit 1 $TMUX2 selectw -t:\$ || exit 1 $TMUX2 resizew -x40 -y14 || exit 1 } -C="sh -c 'exec sleep 100'" # Layouts. Each produces one kind of junction. Splits at fixed window size are # deterministic. From cc8e0f2f67cc8727bafc1dfde4c4387d7a11937c Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 08:50:49 +0100 Subject: [PATCH 11/47] actions: use published openbsd-git as base update --- .github/workflows/update-openbsd-and-portable.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index a9e3e4e1b..f79d17545 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -99,13 +99,12 @@ jobs: cd "$CUTOVER_DIR" git switch master - base_ref=refs/openbsd/import-base - if ! git rev-parse --verify -q "$base_ref" >/dev/null; then - echo "$base_ref is missing; initialise it before running updates." >&2 + if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then + echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 exit 1 fi - old_base=$(git rev-parse "$base_ref") + old_base=$(git rev-parse origin/openbsd-git) new_base=$(git rev-parse openbsd-git) if [ "$old_base" = "$new_base" ]; then @@ -115,12 +114,11 @@ jobs: fi if ! git merge-base --is-ancestor "$old_base" "$new_base"; then - echo "openbsd-git is not a descendant of $base_ref; aborting." >&2 + echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 exit 1 fi git cherry-pick "$old_base..$new_base" - git update-ref "$base_ref" "$new_base" echo "changed=true" >> "$GITHUB_OUTPUT" - name: Push cutover repository From 3612a9a605f494f52ea316e00d23336165103d74 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 12 Jul 2026 20:16:20 +0000 Subject: [PATCH 12/47] Add missing calloc check and use fatal. Prompted by a similar change from Yayo Razo. --- xmalloc.c | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/xmalloc.c b/xmalloc.c index b60b9dbee..c6a2f6f13 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -1,4 +1,4 @@ -/* $OpenBSD: xmalloc.c,v 1.14 2026/06/18 10:56:22 nicm Exp $ */ +/* $OpenBSD: xmalloc.c,v 1.15 2026/07/12 20:16:20 nicm Exp $ */ /* * Author: Tatu Ylonen @@ -32,8 +32,7 @@ xmalloc(size_t size) fatalx("xmalloc: zero size"); ptr = malloc(size); if (ptr == NULL) - fatalx("xmalloc: allocating %zu bytes: %s", - size, strerror(errno)); + fatal("xmalloc: allocating %zu bytes", size); return ptr; } @@ -44,10 +43,11 @@ xcalloc(size_t nmemb, size_t size) if (size == 0 || nmemb == 0) fatalx("xcalloc: zero size"); + if (SIZE_MAX / nmemb < size) + fatalx("xcalloc: nmemb * size > SIZE_MAX"); ptr = calloc(nmemb, size); if (ptr == NULL) - fatalx("xcalloc: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xcalloc: allocating %zu bytes", size * nmemb); return ptr; } @@ -66,8 +66,7 @@ xreallocarray(void *ptr, size_t nmemb, size_t size) fatalx("xreallocarray: zero size"); new_ptr = reallocarray(ptr, nmemb, size); if (new_ptr == NULL) - fatalx("xreallocarray: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xreallocarray: allocating %zu bytes", size * nmemb); return new_ptr; } @@ -80,8 +79,7 @@ xrecallocarray(void *ptr, size_t oldnmemb, size_t nmemb, size_t size) fatalx("xrecallocarray: zero size"); new_ptr = recallocarray(ptr, oldnmemb, nmemb, size); if (new_ptr == NULL) - fatalx("xrecallocarray: allocating %zu * %zu bytes: %s", - nmemb, size, strerror(errno)); + fatal("xrecallocarray: allocating %zu bytes", size * nmemb); return new_ptr; } @@ -91,7 +89,7 @@ xstrdup(const char *str) char *cp; if ((cp = strdup(str)) == NULL) - fatalx("xstrdup: %s", strerror(errno)); + fatal("xstrdup"); return cp; } @@ -101,7 +99,7 @@ xstrndup(const char *str, size_t maxlen) char *cp; if ((cp = strndup(str, maxlen)) == NULL) - fatalx("xstrndup: %s", strerror(errno)); + fatal("xstrndup"); return cp; } @@ -126,7 +124,6 @@ xasprintf(char **ret, const char *fmt, ...) va_start(ap, fmt); i = xvasprintf(ret, fmt, ap); va_end(ap); - return i; } @@ -136,10 +133,8 @@ xvasprintf(char **ret, const char *fmt, va_list ap) int i; i = vasprintf(ret, fmt, ap); - - if (i == -1) - fatalx("xasprintf: %s", strerror(errno)); - + if (i == -1 || *ret == NULL) + fatal("xvasprintf"); return i; } @@ -152,7 +147,6 @@ xsnprintf(char *str, size_t len, const char *fmt, ...) va_start(ap, fmt); i = xvsnprintf(str, len, fmt, ap); va_end(ap); - return i; } @@ -163,11 +157,8 @@ xvsnprintf(char *str, size_t len, const char *fmt, va_list ap) if (len > INT_MAX) fatalx("xsnprintf: len > INT_MAX"); - i = vsnprintf(str, len, fmt, ap); - if (i < 0 || i >= (int)len) fatalx("xsnprintf: overflow"); - return i; } From 0925c9846b66dc807b1dfd97bcbc690e0b890069 Mon Sep 17 00:00:00 2001 From: nicm Date: Sun, 12 Jul 2026 20:35:52 +0000 Subject: [PATCH 13/47] Use _exit in child after fork, from Yayo Razo in GitHub issue 5376. --- cmd.c | 11 ++++++++--- job.c | 18 +++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/cmd.c b/cmd.c index b9b8f1a4a..d2e98d466 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd.c,v 1.185 2026/06/26 14:40:30 nicm Exp $ */ +/* $OpenBSD: cmd.c,v 1.186 2026/07/12 20:35:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -846,7 +846,7 @@ cmd_template_replace(const char *template, const char *s, int idx) char ch, *buf; const char *ptr, *cp, quote[] = "\"\\$;~"; int replaced, quoted; - size_t len; + size_t len, slen; if (strchr(template, '%') == NULL) return (xstrdup(template)); @@ -871,7 +871,10 @@ cmd_template_replace(const char *template, const char *s, int idx) if (quoted) ptr++; - buf = xrealloc(buf, len + (strlen(s) * 3) + 1); + slen = strlen(s); + if (slen > SIZE_MAX / 3 || len > SIZE_MAX - (slen * 3) - 1) + fatalx("argument too long"); + buf = xrealloc(buf, len + (slen * 3) + 1); for (cp = s; *cp != '\0'; cp++) { if (quoted && strchr(quote, *cp) != NULL) buf[len++] = '\\'; @@ -880,6 +883,8 @@ cmd_template_replace(const char *template, const char *s, int idx) buf[len] = '\0'; continue; } + if (len > SIZE_MAX - 2) + fatalx("argument too long"); buf = xrealloc(buf, len + 2); buf[len++] = ch; buf[len] = '\0'; diff --git a/job.c b/job.c index 8400ea584..82e87b825 100644 --- a/job.c +++ b/job.c @@ -1,4 +1,4 @@ -/* $OpenBSD: job.c,v 1.74 2025/09/08 11:21:56 nicm Exp $ */ +/* $OpenBSD: job.c,v 1.75 2026/07/12 20:35:52 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -149,7 +149,7 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, else if (chdir("/") == 0) environ_set(env, "PWD", 0, "/"); else - fatal("chdir failed"); + _exit(1); } environ_push(env); @@ -157,21 +157,21 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, if (~flags & JOB_PTY) { if (dup2(out[1], STDIN_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDIN_FILENO; if (dup2(out[1], STDOUT_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDOUT_FILENO; if (flags & JOB_SHOWSTDERR) { if (dup2(out[1], STDERR_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); do_close = do_close && out[1] != STDERR_FILENO; } else { nullfd = open(_PATH_DEVNULL, O_RDWR); if (nullfd == -1) - fatal("open failed"); + _exit(1); if (dup2(nullfd, STDERR_FILENO) == -1) - fatal("dup2 failed"); + _exit(1); if (nullfd != STDERR_FILENO) close(nullfd); } @@ -185,11 +185,11 @@ job_run(const char *cmd, int argc, char **argv, struct environ *e, if (flags & JOB_DEFAULTSHELL) setenv("SHELL", shell, 1); execl(shell, argv0, "-c", cmd, (char *)NULL); - fatal("execl failed"); + _exit(1); } else { argvp = cmd_copy_argv(argc, argv); execvp(argvp[0], argvp); - fatal("execvp failed"); + _exit(1); } } From f80d3f612392870cbd431a1d0fad3c315c035095 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:00:01 +0100 Subject: [PATCH 14/47] build: fetch cutover master before merge --- .github/workflows/update-openbsd-and-portable.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index f79d17545..003a0318b 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -143,9 +143,9 @@ jobs: git remote add tmux-openbsd "../$CUTOVER_DIR" fi - git fetch --no-tags tmux-openbsd + git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master git switch master - git merge --no-ff --log tmux-openbsd/master + git merge --no-ff --log refs/remotes/tmux-openbsd/master - name: Push portable repository if: steps.cutover.outputs.changed == 'true' From 0ce72059e6e7c724f00a7eb241a5c5e80e281e2e Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:05:10 +0100 Subject: [PATCH 15/47] build: capture openbsd-base before filtering --- .../workflows/update-openbsd-and-portable.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index 003a0318b..c5b592dc9 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -76,6 +76,18 @@ jobs: git fetch --filter=blob:none --no-tags origin master git switch -C master origin/master + - name: Capture published OpenBSD tmux base + id: openbsd-base + run: | + set -eu + + cd "$CUTOVER_DIR" + if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then + echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 + exit 1 + fi + echo "sha=$(git rev-parse origin/openbsd-git)" >> "$GITHUB_OUTPUT" + - name: Filter OpenBSD tmux history run: | set -eu @@ -99,12 +111,7 @@ jobs: cd "$CUTOVER_DIR" git switch master - if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then - echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 - exit 1 - fi - - old_base=$(git rev-parse origin/openbsd-git) + old_base="${{ steps.openbsd-base.outputs.sha }}" new_base=$(git rev-parse openbsd-git) if [ "$old_base" = "$new_base" ]; then From 8b2bc1a3841f34bb3ceade4e21c82a3a2bf5a2c2 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:10:24 +0100 Subject: [PATCH 16/47] build: always attempt portable merge --- .github/workflows/update-openbsd-and-portable.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index c5b592dc9..1894f52b9 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -137,7 +137,7 @@ jobs: git push origin master openbsd-git - name: Merge cutover into portable - if: steps.cutover.outputs.changed == 'true' + id: portable run: | set -eu @@ -153,9 +153,14 @@ jobs: git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master git switch master git merge --no-ff --log refs/remotes/tmux-openbsd/master + if git diff --quiet origin/master..master; then + echo "changed=false" >> "$GITHUB_OUTPUT" + else + echo "changed=true" >> "$GITHUB_OUTPUT" + fi - name: Push portable repository - if: steps.cutover.outputs.changed == 'true' + if: steps.portable.outputs.changed == 'true' run: | set -eu From e6eb26518bdb01c2d6fc6d046b35e57704dfcd9a Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:13:28 +0100 Subject: [PATCH 17/47] build: don't exit --- .../workflows/update-openbsd-and-portable.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index 1894f52b9..de4c6ab20 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -116,18 +116,17 @@ jobs: if [ "$old_base" = "$new_base" ]; then echo "changed=false" >> "$GITHUB_OUTPUT" - echo "No new OpenBSD tmux commits to import." - exit 0 - fi + echo "No new OpenBSD tmux commits to import; continuing to portable merge." + else + if ! git merge-base --is-ancestor "$old_base" "$new_base"; then + echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 + exit 1 + fi - if ! git merge-base --is-ancestor "$old_base" "$new_base"; then - echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 - exit 1 + git cherry-pick "$old_base..$new_base" + echo "changed=true" >> "$GITHUB_OUTPUT" fi - git cherry-pick "$old_base..$new_base" - echo "changed=true" >> "$GITHUB_OUTPUT" - - name: Push cutover repository if: steps.cutover.outputs.changed == 'true' run: | From 6126988781b50faca2f23d47bd89e8451342d8a7 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:22:53 +0100 Subject: [PATCH 18/47] portable: add Makefile.am --- Makefile.am | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index 84bdbdb2a..261ee0908 100644 --- a/Makefile.am +++ b/Makefile.am @@ -160,6 +160,8 @@ dist_tmux_SOURCES = \ compat.h \ control-notify.c \ control.c \ + events.c \ + events-payload.c \ environ.c \ file.c \ format.c \ @@ -168,6 +170,7 @@ dist_tmux_SOURCES = \ grid-reader.c \ grid-view.c \ grid.c \ + hooks.c \ hyperlinks.c \ input-keys.c \ input.c \ @@ -182,7 +185,6 @@ dist_tmux_SOURCES = \ mode-tree.c \ monitor.c \ names.c \ - notify.c \ options-table.c \ options.c \ paste.c \ From d03de4986a85c245f2fc11e921aa06ba8d183692 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 09:27:05 +0100 Subject: [PATCH 19/47] build: run every fifteen minutes --- .github/workflows/update-openbsd-and-portable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml index de4c6ab20..c914da332 100644 --- a/.github/workflows/update-openbsd-and-portable.yml +++ b/.github/workflows/update-openbsd-and-portable.yml @@ -3,7 +3,7 @@ name: Update OpenBSD tmux and portable on: workflow_dispatch: schedule: - - cron: "17 3 * * *" + - cron: "*/15 * * * *" permissions: contents: write From e0014bbc7cab17919485e0c5f62356955050e180 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 10:23:38 +0100 Subject: [PATCH 20/47] Add regress tests for events code. --- Makefile.am | 10 +- regress/control-notify-events.sh | 102 +++++++++++++++ regress/hooks-after.sh | 30 ++++- regress/hooks-lifecycle.sh | 14 +++ regress/hooks-notify.sh | 93 ++++++++++++++ regress/options-array.sh | 48 +++++++ regress/prompt-mechanics.sh | 22 ++++ regress/set-hook-B.sh | 10 ++ regress/set-hook-E.sh | 150 ++++++++++++++++++++++ regress/wait-for-E.sh | 206 +++++++++++++++++++++++++++++++ 10 files changed, 676 insertions(+), 9 deletions(-) create mode 100644 regress/control-notify-events.sh mode change 100755 => 100644 regress/set-hook-B.sh create mode 100755 regress/set-hook-E.sh create mode 100644 regress/wait-for-E.sh diff --git a/Makefile.am b/Makefile.am index 261ee0908..2edc7c36d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -160,12 +160,12 @@ dist_tmux_SOURCES = \ compat.h \ control-notify.c \ control.c \ - events.c \ - events-payload.c \ environ.c \ + events-payload.c \ + events.c \ file.c \ - format.c \ format-draw.c \ + format.c \ fuzzy.c \ grid-reader.c \ grid-view.c \ @@ -190,8 +190,8 @@ dist_tmux_SOURCES = \ paste.c \ popup.c \ proc.c \ - prompt.c \ prompt-history.c \ + prompt.c \ regsub.c \ resize.c \ screen-redraw.c \ @@ -206,9 +206,9 @@ dist_tmux_SOURCES = \ spawn.c \ status.c \ style.c \ + tmux-protocol.h \ tmux.c \ tmux.h \ - tmux-protocol.h \ tty-acs.c \ tty-draw.c \ tty-features.c \ diff --git a/regress/control-notify-events.sh b/regress/control-notify-events.sh new file mode 100644 index 000000000..ab696f5e8 --- /dev/null +++ b/regress/control-notify-events.sh @@ -0,0 +1,102 @@ +#!/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" +PID= + +cleanup() +{ + [ -n "$PID" ] && kill "$PID" 2>/dev/null + $TMUX kill-server 2>/dev/null + rm -rf "$TMPDIR" +} +trap cleanup EXIT + +wait_for() +{ + pattern=$1 + timeout=${2:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + if grep -F -- "$pattern" "$OUT" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + echo "missing: $pattern" + cat "$OUT" + return 1 +} + +wait_for_count() +{ + pattern=$1 + expected=$2 + timeout=${3:-6} + i=0 + + while [ "$i" -lt "$timeout" ]; do + count=$(grep -F -- "$pattern" "$OUT" 2>/dev/null | wc -l) + if [ "$count" -ge "$expected" ]; then + return 0 + fi + sleep 1 + i=$((i + 1)) + done + echo "missing count $expected for: $pattern" + cat "$OUT" + return 1 +} + +send() +{ + printf '%s\n' "$*" >&3 +} + +$TMUX kill-server 2>/dev/null +$TMUX new-session -d -s notify -x 80 -y 24 || exit 1 +wid=$($TMUX display-message -p -t notify:0 '#{window_id}') || exit 1 +pane=$($TMUX display-message -p -t notify:0.0 '#{pane_id}') || exit 1 + +mkfifo "$IN" +: >"$OUT" +$TMUX -C attach-session -t notify <"$IN" >"$OUT" 2>&1 & +PID=$! +exec 3>"$IN" + +send 'display-message -p ready' +wait_for 'ready' 3 || exit 1 + +send 'rename-window notify-renamed' +wait_for "%window-renamed $wid notify-renamed" || exit 1 + +send 'split-window -h' +wait_for "%layout-change $wid " || exit 1 + +send 'select-pane -t:.0' +wait_for "%window-pane-changed $wid $pane" || exit 1 + +send 'copy-mode -t:.0' +wait_for "%pane-mode-changed $pane" || exit 1 + +sessions_changed=$(grep -F -- '%sessions-changed' "$OUT" 2>/dev/null | \ + wc -l) +send 'new-session -d -s notify-extra' +wait_for_count '%sessions-changed' $((sessions_changed + 1)) || exit 1 + +send 'kill-session -t notify-extra' +wait_for_count '%sessions-changed' $((sessions_changed + 2)) || exit 1 + +exit 0 diff --git a/regress/hooks-after.sh b/regress/hooks-after.sh index 37934db8d..3c1ba7909 100644 --- a/regress/hooks-after.sh +++ b/regress/hooks-after.sh @@ -63,10 +63,10 @@ $TMUX new -d -s two || fail "new-session two failed" # An after hook fires with the command arguments as formats. $TMUX set -g @after 0 || fail "set @after failed" $TMUX set-hook -g after-rename-window \ - 'set -gF @after "#{hook}|#{hook_argument_0}|#{hook_flag_t}"' || + 'set -gF @after "#{hook}|#{hook_arguments}|#{hook_argument_0}|#{hook_flag_t}"' || fail "set-hook -g after-rename-window failed" $TMUX rename-window -t one:0 first || fail "rename-window first failed" -wait_for @after 'after-rename-window|first|one:0' +wait_for @after 'after-rename-window|-t one:0 first|first|one:0' # An appended hook command runs after the first. $TMUX set -g @after2 0 || fail "set @after2 failed" @@ -74,10 +74,32 @@ $TMUX set-hook -ga after-rename-window \ 'set -gF @after2 "#{@after}+2"' || fail "set-hook -ga after-rename-window failed" $TMUX rename-window -t one:0 second || fail "rename-window second failed" -wait_for @after 'after-rename-window|second|one:0' -wait_for @after2 'after-rename-window|second|one:0+2' +wait_for @after 'after-rename-window|-t one:0 second|second|one:0' +wait_for @after2 'after-rename-window|-t one:0 second|second|one:0+2' $TMUX set-hook -gu after-rename-window || fail "set-hook -gu failed" +# A hook command is inserted after the command that fired it, before the next +# command in the same command list. +$TMUX set -g @order '' || fail "set @order failed" +$TMUX set-hook -g after-rename-window \ + 'set -gF @order "#{@order}H"' || + fail "set-hook -g after-rename-window order failed" +$TMUX rename-window -t one:0 ordered \; set -gF @order '#{@order}N' || + fail "rename-window order failed" +wait_for @order 'HN' +$TMUX set-hook -gu after-rename-window || fail "set-hook -gu order failed" + +# Repeated flag values are available as numbered hook flag formats. +$TMUX set -g @flags 0 || fail "set @flags failed" +$TMUX set-hook -g after-split-window \ + 'set -gF @flags "#{hook_arguments}|#{hook_argument_0}|#{hook_flag_t}|#{hook_flag_e}|#{hook_flag_e_0}|#{hook_flag_e_1}"' || + fail "set-hook -g after-split-window failed" +$TMUX split-window -d -t one:0 -e A=1 -e B=2 'sleep 60' || + fail "split-window flags failed" +wait_for @flags '-d -e A=1 -e B=2 -t one:0 "sleep 60"|sleep 60|one:0|B=2|A=1|B=2' +$TMUX set-hook -gu after-split-window || + fail "set-hook -gu after-split-window failed" + # A session after hook only fires for commands targeting that session and # the hook commands run with the command target as current state. $TMUX set -g @safter 0 || fail "set @safter failed" diff --git a/regress/hooks-lifecycle.sh b/regress/hooks-lifecycle.sh index e90f7ba3a..3ec748ff4 100644 --- a/regress/hooks-lifecycle.sh +++ b/regress/hooks-lifecycle.sh @@ -96,6 +96,20 @@ fi $TMUX list-panes -sat main >/dev/null || fail "list-panes failed" $TMUX has -t main || fail "server died after pane exit chain" +# A pane-exited hook command can run after the pane has been removed. It +# should not retain only the event payload's temporary target references. +$TMUX set -g @queued-pane-exited 0 || fail "set @queued-pane-exited failed" +$TMUX set-hook -g pane-exited \ + 'display-message -p "queued #{hook_pane}" ; set -g @queued-pane-exited 1' || + fail "set-hook queued pane-exited failed" +pane=$($TMUX new -d -s queued -n qwin -P -F '#{pane_id}' 'true') || + fail "new-session queued failed" +wait_for @queued-pane-exited 1 +$TMUX has -t main || fail "server died after queued pane-exited hook" +$TMUX set-hook -g pane-exited \ + 'set -gF @log "#{@log}|pane-exited:#{hook_pane}"' || + fail "restore pane-exited hook failed" + # kill-window on the last window: window-unlinked then session-closed but # no pane-exited for the panes in the killed window. $TMUX set -g @log '' || fail "reset @log failed" diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 41916248c..37d1cd651 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -181,6 +181,99 @@ $TMUX set-hook -gu pane-title-changed || fail "unset pane-title-changed failed" $TMUX killp -t "$pane" || fail "kill-pane title failed" +# pane-created payload from a command pane, an empty pane, and a respawn. +$TMUX set -g @pc 0 || fail "set @pc failed" +$TMUX set-hook -g pane-created \ + 'set -gF @pc "#{hook}:#{hook_pane}:#{hook_pane_command}:#{hook_created_empty}:#{hook_created_respawn}"' || + fail "set-hook pane-created failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-created command failed" +wait_for @pc "pane-created:$pane:\"sleep 30\":0:0" +$TMUX killp -t "$pane" || fail "kill-pane pane-created command failed" + +$TMUX set -g @pc 0 || fail "reset @pc failed" +empty_command=$($TMUX show -gqv default-shell) || + fail "show default-shell failed" +pane=$($TMUX splitw -d -E -t main:0 -P -F '#{pane_id}') || + fail "split-window pane-created empty failed" +wait_for @pc "pane-created:$pane:$empty_command:1:0" + +$TMUX set -g @pc 0 || fail "reset @pc respawn failed" +$TMUX respawnp -k -t "$pane" 'sleep 30' || fail "respawn-pane failed" +wait_for @pc "pane-created:$pane:\"sleep 30\":0:1" +$TMUX killp -t "$pane" || fail "kill-pane pane-created respawn failed" +$TMUX set-hook -gu pane-created || fail "unset pane-created failed" + +# pane-resized carries old and new dimensions. +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-resized failed" +old_size=$($TMUX display -pt "$pane" '#{pane_width},#{pane_height}') || + fail "display old pane size failed" +$TMUX set -g @pr 0 || fail "set @pr failed" +$TMUX set-hook -g pane-resized \ + 'set -gF @pr "#{hook}:#{hook_pane}:#{hook_old_width},#{hook_old_height}->#{hook_width},#{hook_height}"' || + fail "set-hook pane-resized failed" +$TMUX resizep -t "$pane" -x 20 -y 10 || fail "resize-pane failed" +new_size=$($TMUX display -pt "$pane" '#{pane_width},#{pane_height}') || + fail "display new pane size failed" +wait_for @pr "pane-resized:$pane:$old_size->$new_size" +$TMUX set-hook -gu pane-resized || fail "unset pane-resized failed" +$TMUX killp -t "$pane" || fail "kill-pane pane-resized failed" + +# pane-mode-entered and pane-mode-exited expose split transition payloads. +$TMUX set -g @me 0 || fail "set @me failed" +$TMUX set -g @mx 0 || fail "set @mx failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-mode failed" +$TMUX set-hook -g pane-mode-entered \ + 'set -gF @me "#{hook}:#{hook_pane}:#{hook_mode_entered}:#{hook_current_mode}:#{hook_previous_mode}"' || + fail "set-hook pane-mode-entered failed" +$TMUX set-hook -g pane-mode-exited \ + 'set -gF @mx "#{hook}:#{hook_pane}:#{hook_mode_entered}:#{hook_current_mode}:#{hook_previous_mode}"' || + fail "set-hook pane-mode-exited failed" +$TMUX copy-mode -t "$pane" || fail "copy-mode split event failed" +wait_for @me "pane-mode-entered:$pane:1:copy-mode:" +$TMUX send-keys -t "$pane" -X cancel || fail "cancel split mode failed" +wait_for @mx "pane-mode-exited:$pane:0::copy-mode" +$TMUX set-hook -gu pane-mode-entered || fail "unset pane-mode-entered failed" +$TMUX set-hook -gu pane-mode-exited || fail "unset pane-mode-exited failed" +$TMUX killp -t "$pane" || fail "kill-pane pane-mode failed" + +# marked-pane-changed carries the new/old marked pane and marked flag. +$TMUX set -g @mk 0 || fail "set @mk failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window marked-pane failed" +$TMUX set-hook -g marked-pane-changed \ + 'set -gF @mk "#{hook}:#{hook_marked}:#{hook_pane}:#{hook_new_pane}:#{hook_old_pane}"' || + fail "set-hook marked-pane-changed failed" +$TMUX selectp -t "$pane" -m || fail "mark pane failed" +wait_for @mk "marked-pane-changed:1:$pane:$pane:" +$TMUX selectp -t "$pane" -m || fail "unmark pane failed" +wait_for @mk "marked-pane-changed:0:$pane::$pane" +$TMUX set-hook -gu marked-pane-changed || + fail "unset marked-pane-changed failed" +$TMUX killp -t "$pane" || fail "kill-pane marked-pane failed" + +# window-zoomed and window-unzoomed fire on resize-pane -Z. +$TMUX set -g @zoom 0 || fail "set @zoom failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window zoom failed" +window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display zoom window failed" +$TMUX set-hook -g window-zoomed \ + 'set -gF @zoom "#{hook}:#{hook_window}"' || + fail "set-hook window-zoomed failed" +$TMUX set-hook -g window-unzoomed \ + 'set -gF @zoom "#{hook}:#{hook_window}"' || + fail "set-hook window-unzoomed failed" +$TMUX resizep -Z -t "$pane" || fail "zoom pane failed" +wait_for @zoom "window-zoomed:$window" +$TMUX resizep -Z -t "$pane" || fail "unzoom pane failed" +wait_for @zoom "window-unzoomed:$window" +$TMUX set-hook -gu window-zoomed || fail "unset window-zoomed failed" +$TMUX set-hook -gu window-unzoomed || fail "unset window-unzoomed failed" +$TMUX killp -t "$pane" || fail "kill-pane zoom failed" + # client-attached and client-detached using a control client. $TMUX set -g @a 0 || fail "set @a failed" $TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' || diff --git a/regress/options-array.sh b/regress/options-array.sh index d9751c94c..fbaff6ae2 100644 --- a/regress/options-array.sh +++ b/regress/options-array.sh @@ -36,6 +36,21 @@ check_value() fi } +check_wait_value() +{ + i=0 + while [ "$i" -lt 30 ]; do + out=$($TMUX show $1 2>&1) + [ "$out" = "$2" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + echo "show $1 failed." + echo "Expected: '$2'" + echo "But got: '$out'" + exit 1 +} + # check_array $args $expected # # Compare the full (multi-line) show output for an array option with a @@ -162,6 +177,39 @@ check_ok set-hook -g window-renamed[notify] "display-message renamed" check_value "-gv window-renamed[notify]" "display-message renamed" check_ok set-hook -gu window-renamed[notify] check_value "-gv window-renamed[notify]" "" +check_ok set -g @hook_first 0 +check_ok set -g @hook_second 0 +check_ok set-hook -g window-renamed[first] "set -g @hook_first 1" +check_ok set-hook -g window-renamed[second] "set -g @hook_second 1" +shown=$($TMUX show-hooks -g window-renamed) || { + echo "show-hooks -g window-renamed failed" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[first\]' || { + echo "missing first hook key: $shown" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[second\]' || { + echo "missing second hook key: $shown" + exit 1 +} +check_ok rename-window -t main:0 array-hooks +check_wait_value "-gqv @hook_first" "1" +check_wait_value "-gqv @hook_second" "1" +check_ok set-hook -gu window-renamed[first] +shown=$($TMUX show-hooks -g window-renamed) || { + echo "show-hooks -g window-renamed after unset failed" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[first\]' && { + echo "first hook key was not unset: $shown" + exit 1 +} +echo "$shown" | grep -q '^window-renamed\[second\]' || { + echo "second hook key did not remain: $shown" + exit 1 +} +check_ok set-hook -gu window-renamed[second] # --- colour-type array ---------------------------------------------------- # diff --git a/regress/prompt-mechanics.sh b/regress/prompt-mechanics.sh index 55bd17a69..9e57d7a55 100644 --- a/regress/prompt-mechanics.sh +++ b/regress/prompt-mechanics.sh @@ -49,6 +49,19 @@ status_line() { got() { $IN show -gv @r } +wait_opt() { + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($IN show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} reset() { $IN set -g @r "SENTINEL" || exit 1 } @@ -85,6 +98,12 @@ $IN bind -n M-j command-prompt -P -I hello -p '(pre)' "set -g @r '%%'" || e $IN bind -n M-m command-prompt -p 'first,second' "set -g @r '%1/%2'" || exit 1 $IN bind -n M-c command-prompt -p '(cmd)' "set -g @r '%%'" || exit 1 $IN bind -n M-h command-prompt -T search -p '(srch)' "set -g @r '%%'" || exit 1 +$IN set-hook -g pane-prompt-opened \ + 'set -gF @prompt_open "#{hook}:#{hook_pane}:#{hook_prompt_type}"' || + exit 1 +$IN set-hook -g pane-prompt-closed \ + 'set -gF @prompt_close "#{hook}:#{hook_pane}:#{hook_prompt_type}"' || + exit 1 # --- Outer session: attach the inner one inside its pane. ------------------- $OUT new -d -x80 -y24 || exit 1 @@ -111,14 +130,17 @@ settle # --- 1b. window.c: drawn over the pane, not on the status line. --- reset +pane=$($IN display-message -p '#{pane_id}') || exit 1 $OUT send-keys M-p || exit 1 settle +wait_opt @prompt_open "pane-prompt-opened:$pane:command" capture | grep -qF '(pane)' || fail "pane prompt not drawn in the pane" status_line | grep -qF '(pane)' && \ fail "pane prompt drawn on the status line, not over the pane" $OUT send-keys -l "deep" || exit 1 $OUT send-keys Enter || exit 1 settle +wait_opt @prompt_close "pane-prompt-closed:$pane:command" [ "$(got)" = "deep" ] || fail "pane prompt accept recovered '$(got)', wanted 'deep'" # --- 1c. mode-tree.c: search prompt drawn in the pane. --- diff --git a/regress/set-hook-B.sh b/regress/set-hook-B.sh old mode 100755 new mode 100644 index 4947af70c..0144884dc --- a/regress/set-hook-B.sh +++ b/regress/set-hook-B.sh @@ -147,4 +147,14 @@ $TMUX set -pt "$target_pane" @target-value changed || fail "set pane @target-value failed" wait_for @target-pane "$target_pane" +$TMUX new -d -s zzz-survivor || fail "new survivor session failed" +$TMUX set -g @global-after-destroy 0 || + fail "set @global-after-destroy failed" +$TMUX set-hook -g -t three -B '@global-after-destroy-session::#{session_name}' \ + 'set -g @global-after-destroy "#{hook_last}->#{hook_value}"' || + fail "set-hook -B global after destroy failed" +assert_unchanged @global-after-destroy 0 +$TMUX kill-session -t three || fail "kill destroyed monitor session failed" +wait_for @global-after-destroy 'three->zzz-survivor' + exit 0 diff --git a/regress/set-hook-E.sh b/regress/set-hook-E.sh new file mode 100755 index 000000000..f8d30d05e --- /dev/null +++ b/regress/set-hook-E.sh @@ -0,0 +1,150 @@ +#!/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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_channel() +{ + channel=$1 + + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX wait-for "$channel" || + fail "wait-for $channel timed out" + return + fi + + $TMUX wait-for "$channel" & + pid=$! + i=0 + while kill -0 "$pid" 2>/dev/null; do + [ $i -lt 50 ] || { + kill "$pid" 2>/dev/null || true + fail "wait-for $channel timed out" + } + i=$((i + 1)) + sleep 0.2 + done + wait "$pid" || fail "wait-for $channel failed" +} + +wait_for() +{ + option=$1 + expected=$2 + i=0 + + while [ $i -lt 30 ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] && return 0 + i=$((i + 1)) + sleep 0.2 + done + fail "expected $option to be '$expected' but got '$value'" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + count=${3:-15} + i=0 + + while [ $i -lt "$count" ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +wait_list() +{ + name=$1 + i=0 + + while [ $i -lt 50 ]; do + value=$($TMUX wait-for -E -l "$name" 2>/dev/null || true) + if [ -n "$value" ]; then + printf '%s\n' "$value" | sed -n '1p' + return + fi + i=$((i + 1)) + sleep 0.2 + done + fail "wait-for -E -l $name found no waiters" +} + +$TMUX new -d -s one || fail "new-session one failed" +$TMUX new -d -s two || fail "new-session two failed" + +$TMUX set -g @event_seen 0 || fail "set @event_seen failed" +$TMUX wait-for -E @manual-event \; set -g @event_seen 1 \; \ + wait-for -S she-event & +event_pid=$! +wait_list @manual-event >/dev/null + +$TMUX set-hook -E @manual-event || fail "set-hook -E @manual-event failed" +wait_channel she-event +wait "$event_pid" || fail "wait-for -E @manual-event failed" +wait_for @event_seen 1 + +$TMUX set-hook -E @no-sink || fail "set-hook -E @no-sink failed" + +pane=$($TMUX display -pt two:0.0 '#{pane_id}') || + fail "display-message pane failed" +$TMUX set -g @hook_seen 0 || fail "set @hook_seen failed" +$TMUX set-hook -g @manual-hook \ + 'set -gF @hook_seen "#{hook}:#{session_name}:#{window_index}:#{pane_id}"' || + fail "set-hook @manual-hook failed" + +$TMUX set-hook -E -t two:0.0 @manual-hook || + fail "set-hook -E @manual-hook failed" +wait_for @hook_seen "@manual-hook:two:0:$pane" + +$TMUX set -g @r_hook 0 || fail "set @r_hook failed" +$TMUX set -g @r_event 0 || fail "set @r_event failed" +$TMUX set-hook -g @manual-r 'set -g @r_hook 1' || + fail "set-hook @manual-r failed" +$TMUX wait-for -E @manual-r \; set -g @r_event 1 \; wait-for -S she-r & +r_pid=$! +r_client=$(wait_list @manual-r) + +$TMUX set-hook -R @manual-r || fail "set-hook -R @manual-r failed" +wait_for @r_hook 1 +assert_unchanged @r_event 0 5 + +$TMUX wait-for -E -w "$r_client" @manual-r || + fail "wait-for -E -w @manual-r failed" +wait_channel she-r +wait "$r_pid" || fail "wait-for -E @manual-r failed" + +if $TMUX set-hook -E window-renamed 2>/dev/null; then + fail "set-hook -E window-renamed succeeded" +fi + +exit 0 diff --git a/regress/wait-for-E.sh b/regress/wait-for-E.sh new file mode 100644 index 000000000..f58437a34 --- /dev/null +++ b/regress/wait-for-E.sh @@ -0,0 +1,206 @@ +#!/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) +OUT=$(mktemp -d) +TMUX_TMPDIR="$OUT" +export TMUX_TMPDIR +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" + +fail() +{ + echo "$*" >&2 + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" + exit 1 +} + +cleanup() +{ + $TMUX kill-server 2>/dev/null || true + rm -rf "$OUT" +} +trap cleanup EXIT + +wait_channel() +{ + channel=$1 + + if command -v timeout >/dev/null 2>&1; then + timeout 10 $TMUX wait-for "$channel" || + fail "wait-for $channel timed out" + return + fi + + $TMUX wait-for "$channel" & + pid=$! + i=0 + while kill -0 "$pid" 2>/dev/null; do + [ $i -lt 50 ] || { + kill "$pid" 2>/dev/null || true + fail "wait-for $channel timed out" + } + i=$((i + 1)) + sleep 0.2 + done + wait "$pid" || fail "wait-for $channel failed" +} + +assert_unchanged() +{ + option=$1 + expected=$2 + count=${3:-15} + i=0 + + while [ $i -lt "$count" ]; do + value=$($TMUX show -gqv "$option" 2>/dev/null || true) + [ "$value" = "$expected" ] || + fail "expected $option to remain '$expected' but got '$value'" + i=$((i + 1)) + sleep 0.2 + done +} + +wait_list() +{ + event=$1 + name=$2 + i=0 + + while [ $i -lt 50 ]; do + if [ "$event" = 1 ]; then + value=$($TMUX wait-for -E -l "$name" 2>/dev/null || true) + else + value=$($TMUX wait-for -l "$name" 2>/dev/null || true) + fi + if [ -n "$value" ]; then + printf '%s\n' "$value" | sed -n '1p' + return + fi + i=$((i + 1)) + sleep 0.2 + done + fail "wait-for -l $name found no waiters" +} + +$TMUX new -d -s wf || fail "new-session failed" + +$TMUX wait-for wf-list & +list_pid=$! +client=$(wait_list 0 wf-list) +$TMUX wait-for -w "$client" wf-list || fail "wait-for -w wf-list failed" +wait "$list_pid" || fail "wait-for -w did not wake channel waiter" + +$TMUX set -g @forced 0 || fail "set @forced failed" +$TMUX wait-for -E @forced-event \; set -g @forced 1 & +forced_pid=$! +client=$(wait_list 1 @forced-event) +$TMUX wait-for -E -w "$client" @forced-event || + fail "wait-for -E -w @forced-event failed" +wait "$forced_pid" || fail "wait-for -E -w did not wake event waiter" +[ "$($TMUX show -gqv @forced)" = 1 ] || + fail "wait-for -E -w did not continue event waiter" + +if $TMUX wait-for -E foobar 2>/dev/null; then + fail "wait-for -E accepted invalid event" +fi + +$TMUX wait-for -E @not-yet-fired & +not_yet_pid=$! +client=$(wait_list 1 @not-yet-fired) +$TMUX wait-for -E -w "$client" @not-yet-fired || + fail "wait-for -E -w @not-yet-fired failed" +wait "$not_yet_pid" || fail "wait-for -E @not-yet-fired failed" + +$TMUX set -g @wf_value 0 || fail "set @wf_value failed" +$TMUX set-hook -g -B '@wf::#{@wf_value}' 'wait-for -S wf-hook' || + fail "set-hook -B failed" + +$TMUX wait-for -E @wf \; wait-for -S wf-event & +event_pid=$! + +# Let the monitor take its first sample so the next change is reported. +sleep 1.5 + +$TMUX set -g @wf_value 1 || fail "set @wf_value 1 failed" + +wait_channel wf-event +wait_channel wf-hook +wait "$event_pid" || fail "wait-for -E command failed" + +$TMUX set -g @late 0 || fail "set @late failed" +$TMUX wait-for -E @wf \; set -g @late 1 \; wait-for -S wf-late & +late_pid=$! +assert_unchanged @late 0 5 + +$TMUX set -g @wf_value 2 || fail "set @wf_value 2 failed" +wait_channel wf-late +wait "$late_pid" || fail "late wait-for -E command failed" + +$TMUX set -g @filtered 0 || fail "set @filtered failed" +$TMUX wait-for -E -F '#{==:#{value},3}' @wf \; set -g @filtered 1 \; \ + wait-for -S wf-filtered & +filtered_pid=$! +assert_unchanged @filtered 0 5 + +$TMUX set -g @wf_value unmatched || fail "set @wf_value unmatched failed" +assert_unchanged @filtered 0 5 + +$TMUX set -g @wf_value 3 || fail "set @wf_value 3 failed" +wait_channel wf-filtered +wait "$filtered_pid" || fail "filtered wait-for -E command failed" + +verbose_file="$OUT/verbose" +$TMUX wait-for -E -v @wf \; wait-for -S wf-verbose >"$verbose_file" & +verbose_pid=$! + +sleep 0.5 +$TMUX set -g @wf_value 4 || fail "set @wf_value 4 failed" +wait_channel wf-verbose +wait "$verbose_pid" || fail "verbose wait-for -E command failed" +grep '^event=@wf$' "$verbose_file" >/dev/null || + fail "verbose wait-for -E did not print event payload" +grep '^value=4$' "$verbose_file" >/dev/null || + fail "verbose wait-for -E did not print value payload" +grep '^_hook_monitor=' "$verbose_file" >/dev/null && + fail "verbose wait-for -E printed private payload" + +$TMUX new -d -s wf2 || fail "new-session wf2 failed" + +$TMUX wait-for -E window-renamed \; wait-for -S wf-renamed & +renamed_pid=$! + +sleep 0.5 +$TMUX rename-window -t wf2:0 renamed || fail "rename-window failed" +wait_channel wf-renamed +wait "$renamed_pid" || fail "wait-for -E window-renamed failed" + +$TMUX set-hook -g window-renamed 'wait-for -S wf-hook-renamed' || + fail "set-hook window-renamed failed" +$TMUX rename-window -t wf2:0 renamed-again || + fail "rename-window renamed-again failed" +wait_channel wf-hook-renamed + +$TMUX set -g @builtin_filtered 0 || fail "set @builtin_filtered failed" +target=$($TMUX splitw -d -t wf2:0 -P -F '#{pane_id}' 'sleep 30') || + fail "split-window target failed" +$TMUX wait-for -E -F "#{==:#{pane},$target}" pane-exited \; \ + set -g @builtin_filtered 1 \; wait-for -S wf-builtin-filtered & +builtin_filtered_pid=$! +assert_unchanged @builtin_filtered 0 5 + +$TMUX splitw -d -t wf2:0 'true' || fail "split-window nonmatching failed" +assert_unchanged @builtin_filtered 0 5 + +$TMUX send-keys -t "$target" C-c || fail "send C-c to target failed" +wait_channel wf-builtin-filtered +wait "$builtin_filtered_pid" || + fail "filtered wait-for -E pane-exited command failed" + +exit 0 From d9b953d5c6af08de2f4b1be97ef44a47a0dbafef Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 10:29:28 +0100 Subject: [PATCH 21/47] Create SECURITY.md --- SECURITY.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..56f11f8ae --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +Please report security issues to nicholas.marriott@gmail.com. From 309119f659472c9e18ae20026cf9c444db1e341f Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 10:54:27 +0100 Subject: [PATCH 22/47] Remove worflow from master --- .../workflows/update-openbsd-and-portable.yml | 174 ------------------ 1 file changed, 174 deletions(-) delete mode 100644 .github/workflows/update-openbsd-and-portable.yml diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml deleted file mode 100644 index c914da332..000000000 --- a/.github/workflows/update-openbsd-and-portable.yml +++ /dev/null @@ -1,174 +0,0 @@ -name: Update OpenBSD tmux and portable - -on: - workflow_dispatch: - schedule: - - cron: "*/15 * * * *" - -permissions: - contents: write - -concurrency: - group: update-openbsd-and-portable - cancel-in-progress: false - -env: - OPENBSD_SRC_URL: https://github.com/openbsd/src.git - OPENBSD_SRC_DIR: openbsd-src - CUTOVER_DIR: tmux-openbsd-cutover - PORTABLE_DIR: tmux-portable - PORTABLE_REPO: ${{ vars.TMUX_PORTABLE_REPO }} - -jobs: - update: - runs-on: ubuntu-latest - steps: - - name: Configure Git identity - run: | - git config --global user.name "tmux update bot" - git config --global user.email "actions@github.com" - - - name: Install git-filter-repo - run: python3 -m pip install --user git-filter-repo - - - name: Checkout cutover repository - uses: actions/checkout@v4 - with: - path: ${{ env.CUTOVER_DIR }} - fetch-depth: 0 - token: ${{ secrets.UPDATE_TOKEN || github.token }} - - - name: Check portable repo setting - run: | - if [ -z "$PORTABLE_REPO" ]; then - echo "Set repository variable TMUX_PORTABLE_REPO, for example owner/tmux-portable." >&2 - exit 1 - fi - - - name: Checkout portable repository - uses: actions/checkout@v4 - with: - repository: ${{ env.PORTABLE_REPO }} - path: ${{ env.PORTABLE_DIR }} - fetch-depth: 0 - token: ${{ secrets.UPDATE_TOKEN }} - - - name: Restore OpenBSD source clone cache - id: openbsd-cache - uses: actions/cache/restore@v4 - with: - path: ${{ env.OPENBSD_SRC_DIR }} - key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} - restore-keys: | - openbsd-src-blobless-${{ runner.os }}- - - - name: Refresh OpenBSD source clone - run: | - set -eu - - if [ ! -d "$OPENBSD_SRC_DIR/.git" ]; then - git clone --filter=blob:none --no-tags --single-branch --branch master \ - "$OPENBSD_SRC_URL" "$OPENBSD_SRC_DIR" - fi - - cd "$OPENBSD_SRC_DIR" - git remote set-url origin "$OPENBSD_SRC_URL" - git fetch --filter=blob:none --no-tags origin master - git switch -C master origin/master - - - name: Capture published OpenBSD tmux base - id: openbsd-base - run: | - set -eu - - cd "$CUTOVER_DIR" - if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then - echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 - exit 1 - fi - echo "sha=$(git rev-parse origin/openbsd-git)" >> "$GITHUB_OUTPUT" - - - name: Filter OpenBSD tmux history - run: | - set -eu - - cd "$OPENBSD_SRC_DIR" - git switch -C tmux-openbsd origin/master - git filter-repo \ - --force \ - --refs tmux-openbsd \ - --path usr.bin/tmux/ \ - --path-rename usr.bin/tmux/: \ - --replace-refs delete-no-add - - git push "../$CUTOVER_DIR" tmux-openbsd:refs/heads/openbsd-git - - - name: Update cutover master - id: cutover - run: | - set -eu - - cd "$CUTOVER_DIR" - git switch master - - old_base="${{ steps.openbsd-base.outputs.sha }}" - new_base=$(git rev-parse openbsd-git) - - if [ "$old_base" = "$new_base" ]; then - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "No new OpenBSD tmux commits to import; continuing to portable merge." - else - if ! git merge-base --is-ancestor "$old_base" "$new_base"; then - echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 - exit 1 - fi - - git cherry-pick "$old_base..$new_base" - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push cutover repository - if: steps.cutover.outputs.changed == 'true' - run: | - set -eu - - cd "$CUTOVER_DIR" - git push origin master openbsd-git - - - name: Merge cutover into portable - id: portable - run: | - set -eu - - cd "$PORTABLE_DIR" - git config remote.tmux-openbsd.tagOpt --no-tags - - if git remote get-url tmux-openbsd >/dev/null 2>&1; then - git remote set-url tmux-openbsd "../$CUTOVER_DIR" - else - git remote add tmux-openbsd "../$CUTOVER_DIR" - fi - - git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master - git switch master - git merge --no-ff --log refs/remotes/tmux-openbsd/master - if git diff --quiet origin/master..master; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push portable repository - if: steps.portable.outputs.changed == 'true' - run: | - set -eu - - cd "$PORTABLE_DIR" - git push origin master - - - name: Save OpenBSD source clone cache - if: always() - uses: actions/cache/save@v4 - with: - path: ${{ env.OPENBSD_SRC_DIR }} - key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} From 1f6385e7bb0fff61bc017f6840fd455d00aad4f6 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 11:03:46 +0100 Subject: [PATCH 23/47] build: remove openbsd sync job This only needs to run from the openbsd-cutover repository and will no longer be merged into thisd repo. --- .../workflows/update-openbsd-and-portable.yml | 174 ------------------ 1 file changed, 174 deletions(-) delete mode 100644 .github/workflows/update-openbsd-and-portable.yml diff --git a/.github/workflows/update-openbsd-and-portable.yml b/.github/workflows/update-openbsd-and-portable.yml deleted file mode 100644 index c914da332..000000000 --- a/.github/workflows/update-openbsd-and-portable.yml +++ /dev/null @@ -1,174 +0,0 @@ -name: Update OpenBSD tmux and portable - -on: - workflow_dispatch: - schedule: - - cron: "*/15 * * * *" - -permissions: - contents: write - -concurrency: - group: update-openbsd-and-portable - cancel-in-progress: false - -env: - OPENBSD_SRC_URL: https://github.com/openbsd/src.git - OPENBSD_SRC_DIR: openbsd-src - CUTOVER_DIR: tmux-openbsd-cutover - PORTABLE_DIR: tmux-portable - PORTABLE_REPO: ${{ vars.TMUX_PORTABLE_REPO }} - -jobs: - update: - runs-on: ubuntu-latest - steps: - - name: Configure Git identity - run: | - git config --global user.name "tmux update bot" - git config --global user.email "actions@github.com" - - - name: Install git-filter-repo - run: python3 -m pip install --user git-filter-repo - - - name: Checkout cutover repository - uses: actions/checkout@v4 - with: - path: ${{ env.CUTOVER_DIR }} - fetch-depth: 0 - token: ${{ secrets.UPDATE_TOKEN || github.token }} - - - name: Check portable repo setting - run: | - if [ -z "$PORTABLE_REPO" ]; then - echo "Set repository variable TMUX_PORTABLE_REPO, for example owner/tmux-portable." >&2 - exit 1 - fi - - - name: Checkout portable repository - uses: actions/checkout@v4 - with: - repository: ${{ env.PORTABLE_REPO }} - path: ${{ env.PORTABLE_DIR }} - fetch-depth: 0 - token: ${{ secrets.UPDATE_TOKEN }} - - - name: Restore OpenBSD source clone cache - id: openbsd-cache - uses: actions/cache/restore@v4 - with: - path: ${{ env.OPENBSD_SRC_DIR }} - key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} - restore-keys: | - openbsd-src-blobless-${{ runner.os }}- - - - name: Refresh OpenBSD source clone - run: | - set -eu - - if [ ! -d "$OPENBSD_SRC_DIR/.git" ]; then - git clone --filter=blob:none --no-tags --single-branch --branch master \ - "$OPENBSD_SRC_URL" "$OPENBSD_SRC_DIR" - fi - - cd "$OPENBSD_SRC_DIR" - git remote set-url origin "$OPENBSD_SRC_URL" - git fetch --filter=blob:none --no-tags origin master - git switch -C master origin/master - - - name: Capture published OpenBSD tmux base - id: openbsd-base - run: | - set -eu - - cd "$CUTOVER_DIR" - if ! git rev-parse --verify -q origin/openbsd-git >/dev/null; then - echo "origin/openbsd-git is missing; publish the openbsd-git branch before running updates." >&2 - exit 1 - fi - echo "sha=$(git rev-parse origin/openbsd-git)" >> "$GITHUB_OUTPUT" - - - name: Filter OpenBSD tmux history - run: | - set -eu - - cd "$OPENBSD_SRC_DIR" - git switch -C tmux-openbsd origin/master - git filter-repo \ - --force \ - --refs tmux-openbsd \ - --path usr.bin/tmux/ \ - --path-rename usr.bin/tmux/: \ - --replace-refs delete-no-add - - git push "../$CUTOVER_DIR" tmux-openbsd:refs/heads/openbsd-git - - - name: Update cutover master - id: cutover - run: | - set -eu - - cd "$CUTOVER_DIR" - git switch master - - old_base="${{ steps.openbsd-base.outputs.sha }}" - new_base=$(git rev-parse openbsd-git) - - if [ "$old_base" = "$new_base" ]; then - echo "changed=false" >> "$GITHUB_OUTPUT" - echo "No new OpenBSD tmux commits to import; continuing to portable merge." - else - if ! git merge-base --is-ancestor "$old_base" "$new_base"; then - echo "new openbsd-git is not a descendant of origin/openbsd-git; aborting." >&2 - exit 1 - fi - - git cherry-pick "$old_base..$new_base" - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push cutover repository - if: steps.cutover.outputs.changed == 'true' - run: | - set -eu - - cd "$CUTOVER_DIR" - git push origin master openbsd-git - - - name: Merge cutover into portable - id: portable - run: | - set -eu - - cd "$PORTABLE_DIR" - git config remote.tmux-openbsd.tagOpt --no-tags - - if git remote get-url tmux-openbsd >/dev/null 2>&1; then - git remote set-url tmux-openbsd "../$CUTOVER_DIR" - else - git remote add tmux-openbsd "../$CUTOVER_DIR" - fi - - git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master - git switch master - git merge --no-ff --log refs/remotes/tmux-openbsd/master - if git diff --quiet origin/master..master; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - - - name: Push portable repository - if: steps.portable.outputs.changed == 'true' - run: | - set -eu - - cd "$PORTABLE_DIR" - git push origin master - - - name: Save OpenBSD source clone cache - if: always() - uses: actions/cache/save@v4 - with: - path: ${{ env.OPENBSD_SRC_DIR }} - key: openbsd-src-blobless-${{ runner.os }}-${{ github.run_id }} From b2e2fe824dbea78585946a229d09b784fb567acf Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 11:32:28 +0100 Subject: [PATCH 24/47] Add scroll regress. --- regress/copy-mode-selection-scroll.sh | 113 ++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 regress/copy-mode-selection-scroll.sh diff --git a/regress/copy-mode-selection-scroll.sh b/regress/copy-mode-selection-scroll.sh new file mode 100644 index 000000000..7e7aee9ce --- /dev/null +++ b/regress/copy-mode-selection-scroll.sh @@ -0,0 +1,113 @@ +#!/bin/sh + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null + +cleanup() +{ + $TMUX kill-server 2>/dev/null + $TMUX2 kill-server 2>/dev/null +} +fail() +{ + echo "$1" + cleanup + exit 1 +} +expect_buffer() +{ + expected=$1 + actual=$($TMUX show-buffer) + [ "$actual" = "$expected" ] || + fail "unexpected buffer: expected [$expected], got [$actual]" +} +wheel() +{ + button=$1 + col=$2 + row=$3 + seq=$(printf '\033[<%s;%s;%sM' "$button" "$col" "$row") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} +trap cleanup 0 +trap 'exit 1' 1 2 3 15 + +$TMUX new -d -x40 -y10 \ + 'i=0; while [ $i -lt 80 ]; do printf "line %02d xxxxxxxxxx\n" $i; i=$((i + 1)); done; cat' || + exit 1 +$TMUX set -g window-size manual || exit 1 +$TMUX set -g mouse on || exit 1 + +$TMUX copy-mode || exit 1 +$TMUX send-keys -X history-top || exit 1 +$TMUX send-keys -N10 -X cursor-down || exit 1 +$TMUX send-keys -X start-of-line || exit 1 +$TMUX send-keys -X begin-selection || exit 1 +$TMUX send-keys -N2 -X cursor-down || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 + +initial=$(printf 'line 10 xxxxxxxxxx\nline 11 xxxxxxxxxx') +expect_buffer "$initial" + +$TMUX send-keys -X stop-selection || exit 1 +$TMUX send-keys -N3 -X scroll-down || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -N2 -X scroll-up || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X scroll-middle || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X scroll-bottom || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X scroll-top || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X recentre-top-bottom || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$initial" + +$TMUX send-keys -X other-end || exit 1 +$TMUX send-keys -X cursor-down || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 + +extended_end=$(printf 'line 10 xxxxxxxxxx\nline 11 xxxxxxxxxx\nline 12 xxxxxxxxxx') +expect_buffer "$extended_end" + +$TMUX send-keys -X stop-selection || exit 1 +$TMUX send-keys -X other-end || exit 1 +$TMUX send-keys -X other-end || exit 1 +$TMUX send-keys -X cursor-up || exit 1 +$TMUX send-keys -X copy-selection-no-clear || exit 1 + +extended_start=$(printf 'line 09 xxxxxxxxxx\nline 10 xxxxxxxxxx\nline 11 xxxxxxxxxx\nline 12 xxxxxxxxxx') +expect_buffer "$extended_start" + +$TMUX2 new-session -d -x40 -y10 "$TMUX attach" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "no outer pane" + +wheel 65 5 5 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$extended_start" + +wheel 64 5 5 +$TMUX send-keys -X copy-selection-no-clear || exit 1 +expect_buffer "$extended_start" + +exit 0 From 687c6b9cc0a32348ac42cc85d816579f19bf9e64 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 13:57:31 +0100 Subject: [PATCH 25/47] Regress for more hooks. --- regress/hooks-notify.sh | 257 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 249 insertions(+), 8 deletions(-) diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 37d1cd651..045b80af6 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -11,11 +11,13 @@ OUT=$(mktemp -d) TMUX_TMPDIR="$OUT" export TMUX_TMPDIR TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" fail() { echo "$*" >&2 $TMUX kill-server 2>/dev/null || true + $TMUX2 kill-server 2>/dev/null || true rm -rf "$OUT" exit 1 } @@ -23,6 +25,7 @@ fail() cleanup() { $TMUX kill-server 2>/dev/null || true + $TMUX2 kill-server 2>/dev/null || true rm -rf "$OUT" } trap cleanup EXIT @@ -92,6 +95,23 @@ wait_for @w 'window-unlinked:main:mywin' $TMUX set-hook -gu window-linked || fail "unset window-linked failed" $TMUX set-hook -gu window-unlinked || fail "unset window-unlinked failed" +# window-created and window-closed are actual window lifetime hooks. +$TMUX set -g @wc 0 || fail "set @wc failed" +$TMUX set-hook -g window-created \ + 'set -gF @wc "#{@wc}|#{hook}:#{hook_window}:#{hook_window_name}"' || + fail "set-hook window-created failed" +$TMUX set-hook -g window-closed \ + 'set -gF @wc "#{@wc}|#{hook}:#{hook_window}:#{hook_window_name}"' || + fail "set-hook window-closed failed" +$TMUX neww -d -t main: -n lifetime || fail "new-window lifetime failed" +window=$($TMUX display -pt main:lifetime '#{window_id}') || + fail "display lifetime window failed" +wait_for @wc "0|window-created:$window:lifetime" +$TMUX kill-window -t main:lifetime || fail "kill-window lifetime failed" +wait_for @wc "0|window-created:$window:lifetime|window-closed:$window:lifetime" +$TMUX set-hook -gu window-created || fail "unset window-created failed" +$TMUX set-hook -gu window-closed || fail "unset window-closed failed" + # window-renamed sees the window id. window=$($TMUX display -pt main:0 '#{window_id}') || fail "display window_id failed" @@ -114,26 +134,75 @@ $TMUX set-hook -gu window-layout-changed || fail "unset window-layout-changed failed" # window-pane-changed from select-pane. +old_pane=$($TMUX display -pt main:0.0 '#{pane_id}') || + fail "display old pane failed" +new_pane=$($TMUX display -pt main:0.1 '#{pane_id}') || + fail "display new pane failed" $TMUX set -g @p 0 || fail "set @p failed" $TMUX set-hook -g window-pane-changed \ - 'set -gF @p "#{hook}:#{hook_window}"' || + 'set -gF @p "#{hook}:#{hook_window}:#{hook_pane}:#{hook_old_pane}:#{hook_new_pane}"' || fail "set-hook window-pane-changed failed" $TMUX selectp -t main:0.1 || fail "select-pane failed" -wait_for @p "window-pane-changed:$window" +wait_for @p "window-pane-changed:$window:$new_pane:$old_pane:$new_pane" + +# window-pane-changed from removing the active pane. +$TMUX set -g @p 0 || fail "reset @p failed" +$TMUX killp -t "$new_pane" || fail "kill active pane failed" +wait_for @p "window-pane-changed:$window:$old_pane:$new_pane:$old_pane" $TMUX set-hook -gu window-pane-changed || fail "unset window-pane-changed failed" # session-window-changed from select-window. $TMUX neww -d -t main: -n w2 || fail "new-window w2 failed" +old_window=$($TMUX display -pt main:0 '#{window_id}') || + fail "display old window failed" +new_window=$($TMUX display -pt main:w2 '#{window_id}') || + fail "display new window failed" +old_index=$($TMUX display -pt main:0 '#{window_index}') || + fail "display old window index failed" +new_index=$($TMUX display -pt main:w2 '#{window_index}') || + fail "display new window index failed" $TMUX set -g @c 0 || fail "set @c failed" $TMUX set-hook -g session-window-changed \ - 'set -gF @c "#{hook}:#{hook_session_name}"' || + 'set -gF @c "#{hook}:#{hook_session_name}:#{hook_window}:#{hook_old_window}:#{hook_new_window}:#{hook_old_window_index}->#{hook_new_window_index}"' || fail "set-hook session-window-changed failed" $TMUX selectw -t main:w2 || fail "select-window failed" -wait_for @c 'session-window-changed:main' +wait_for @c "session-window-changed:main:$new_window:$old_window:$new_window:$old_index->$new_index" $TMUX set-hook -gu session-window-changed || fail "unset session-window-changed failed" +# session-added-to-group and session-removed-from-group carry group details. +$TMUX set -g @sg 0 || fail "set @sg failed" +$TMUX set-hook -g session-added-to-group \ + 'set -gF @sg "#{@sg}|#{hook}:#{hook_session_name}:#{hook_group}:#{hook_group_size}"' || + fail "set-hook session-added-to-group failed" +$TMUX set-hook -g session-removed-from-group \ + 'set -gF @sg "#{@sg}|#{hook}:#{hook_session_name}:#{hook_group}:#{hook_group_size}"' || + fail "set-hook session-removed-from-group failed" +$TMUX new -d -s group-a || fail "new-session group-a failed" +$TMUX new -d -s group-b -t group-a || fail "new-session group-b failed" +wait_for @sg "0|session-added-to-group:group-a:group-a:1|session-added-to-group:group-b:group-a:2" +$TMUX kill-session -t group-b || fail "kill-session group-b failed" +wait_for @sg "0|session-added-to-group:group-a:group-a:1|session-added-to-group:group-b:group-a:2|session-removed-from-group:group-b:group-a:2" +$TMUX set-hook -gu session-added-to-group || + fail "unset session-added-to-group failed" +$TMUX set-hook -gu session-removed-from-group || + fail "unset session-removed-from-group failed" +$TMUX kill-session -t group-a || fail "kill-session group-a failed" + +# window-resized carries old and new dimensions. +old_size=$($TMUX display -pt main:0 '#{window_width},#{window_height}') || + fail "display old window size failed" +$TMUX set -g @wr 0 || fail "set @wr failed" +$TMUX set-hook -g window-resized \ + 'set -gF @wr "#{hook}:#{hook_window}:#{hook_old_width},#{hook_old_height}->#{hook_width},#{hook_height}"' || + fail "set-hook window-resized failed" +$TMUX resizew -t main:0 -x 70 -y 20 || fail "resize-window failed" +new_size=$($TMUX display -pt main:0 '#{window_width},#{window_height}') || + fail "display new window size failed" +wait_for @wr "window-resized:$old_window:$old_size->$new_size" +$TMUX set-hook -gu window-resized || fail "unset window-resized failed" + # pane-mode-changed from entering and leaving copy mode. $TMUX set -g @m 0 || fail "set @m failed" pane=$($TMUX display -pt main:0.0 '#{pane_id}') || @@ -168,18 +237,44 @@ $TMUX set-hook -gu pane-died || fail "unset pane-died failed" $TMUX killp -t "$pane" || fail "kill-pane failed" $TMUX set -g remain-on-exit off || fail "reset remain-on-exit failed" -# pane-title-changed when a pane sets its title. +# pane-title-changed from split-window -T. $TMUX set -g @t 0 || fail "set @t failed" $TMUX set-hook -g pane-title-changed \ - 'set -gF @t "#{hook}:#{hook_pane}:#{pane_title}"' || + 'set -gF @t "#{hook}:#{hook_pane}:#{hook_new_title}:#{pane_title}"' || fail "set-hook pane-title-changed failed" +pane=$($TMUX splitw -d -t main:0 -T splittitle -P -F '#{pane_id}' \ + 'sleep 30') || + fail "split-window title option failed" +wait_for @t "pane-title-changed:$pane:splittitle:splittitle" +$TMUX killp -t "$pane" || fail "kill-pane title option failed" + +# pane-title-changed from select-pane -T. +$TMUX set -g @t 0 || fail "reset @t select title failed" +pane=$($TMUX display -pt main:0.0 '#{pane_id}') || + fail "display select title pane failed" +$TMUX selectp -t "$pane" -T selecttitle || fail "select-pane title failed" +wait_for @t "pane-title-changed:$pane:selecttitle:selecttitle" + +# pane-title-changed when a pane sets its title. +$TMUX set -g @t 0 || fail "reset @t OSC title failed" pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \ 'printf "\033]2;mytitle\007"; sleep 30') || fail "split-window title failed" -wait_for @t "pane-title-changed:$pane:mytitle" +wait_for @t "pane-title-changed:$pane:mytitle:mytitle" +$TMUX killp -t "$pane" || fail "kill-pane title failed" + +# pane-title-changed from popping the title stack. +$TMUX set -g @t 0 || fail "reset @t title pop failed" +$TMUX set-hook -g pane-title-changed \ + 'set -gF @t "#{@t}|#{hook_new_title}"' || + fail "set-hook pane-title-changed pop failed" +pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \ + 'printf "\033]2;stackbase\007\033[22;0t\033]2;stacktemp\007\033[23;0t"; sleep 30') || + fail "split-window title pop failed" +wait_for @t "0|stackbase|stacktemp|stackbase" $TMUX set-hook -gu pane-title-changed || fail "unset pane-title-changed failed" -$TMUX killp -t "$pane" || fail "kill-pane title failed" +$TMUX killp -t "$pane" || fail "kill-pane title pop failed" # pane-created payload from a command pane, an empty pane, and a respawn. $TMUX set -g @pc 0 || fail "set @pc failed" @@ -204,6 +299,82 @@ wait_for @pc "pane-created:$pane:\"sleep 30\":0:1" $TMUX killp -t "$pane" || fail "kill-pane pane-created respawn failed" $TMUX set-hook -gu pane-created || fail "unset pane-created failed" +# pane-moved from break-pane and join-pane. +$TMUX neww -d -t main: -n move-a 'sleep 30' || + fail "new-window move-a failed" +pane=$($TMUX splitw -d -t main:move-a -P -F '#{pane_id}' 'sleep 30') || + fail "split-window pane-moved failed" +target=$($TMUX display -pt main:move-a.0 '#{pane_id}') || + fail "display pane-moved target failed" +old_window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display pane-moved old window failed" +old_index=$($TMUX display -pt "$pane" '#{window_index}') || + fail "display pane-moved old index failed" +$TMUX set -g @pm 0 || fail "set @pm failed" +$TMUX set-hook -g pane-moved \ + 'set -gF @pm "#{hook}:#{hook_pane}:#{hook_old_window}:#{hook_old_window_index}->#{hook_new_window}:#{hook_new_window_index}"' || + fail "set-hook pane-moved failed" +$TMUX breakp -d -s "$pane" -n move-b -t main: || + fail "break-pane pane-moved failed" +new_window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display pane-moved break new window failed" +new_index=$($TMUX display -pt "$pane" '#{window_index}') || + fail "display pane-moved break new index failed" +wait_for @pm "pane-moved:$pane:$old_window:$old_index->$new_window:$new_index" + +$TMUX set -g @pm 0 || fail "reset @pm join failed" +old_window=$new_window +old_index=$new_index +$TMUX joinp -d -s "$pane" -t "$target" || + fail "join-pane pane-moved failed" +new_window=$($TMUX display -pt "$pane" '#{window_id}') || + fail "display pane-moved join new window failed" +new_index=$($TMUX display -pt "$pane" '#{window_index}') || + fail "display pane-moved join new index failed" +wait_for @pm "pane-moved:$pane:$old_window:$old_index->$new_window:$new_index" +$TMUX kill-window -t main:move-a || fail "kill-window move-a failed" + +# pane-moved from cross-window swap-pane fires once for each pane. +$TMUX neww -d -t main: -n swap-a 'sleep 30' || + fail "new-window swap-a failed" +$TMUX neww -d -t main: -n swap-b 'sleep 30' || + fail "new-window swap-b failed" +pane1=$($TMUX display -pt main:swap-a.0 '#{pane_id}') || + fail "display swap pane1 failed" +pane2=$($TMUX display -pt main:swap-b.0 '#{pane_id}') || + fail "display swap pane2 failed" +window1=$($TMUX display -pt "$pane1" '#{window_id}') || + fail "display swap window1 failed" +window2=$($TMUX display -pt "$pane2" '#{window_id}') || + fail "display swap window2 failed" +index1=$($TMUX display -pt "$pane1" '#{window_index}') || + fail "display swap index1 failed" +index2=$($TMUX display -pt "$pane2" '#{window_index}') || + fail "display swap index2 failed" +$TMUX set -g @pm 0 || fail "reset @pm swap failed" +$TMUX set-hook -g pane-moved \ + 'set -gF @pm "#{@pm}|#{hook_pane}:#{hook_old_window}:#{hook_old_window_index}->#{hook_new_window}:#{hook_new_window_index}"' || + fail "set-hook pane-moved swap failed" +$TMUX swapp -d -s "$pane1" -t "$pane2" || + fail "swap-pane pane-moved failed" +wait_for @pm "0|$pane1:$window1:$index1->$window2:$index2|$pane2:$window2:$index2->$window1:$index1" +$TMUX set-hook -gu pane-moved || fail "unset pane-moved failed" +$TMUX kill-window -t main:swap-a || fail "kill-window swap-a failed" +$TMUX kill-window -t main:swap-b || fail "kill-window swap-b failed" + +# pane-bell fires for the actual pane even when bell alerts are disabled. +pane=$($TMUX neww -d -t main: -n raw-bell -P -F '#{pane_id}' 'cat') || + fail "new-window pane-bell failed" +$TMUX setw -t main:raw-bell monitor-bell off || + fail "set monitor-bell off failed" +$TMUX set -g @pb 0 || fail "set @pb failed" +$TMUX set-hook -g pane-bell 'set -gF @pb "#{hook}:#{hook_pane}"' || + fail "set-hook pane-bell failed" +$TMUX send-keys -t "$pane" -H 07 0a || fail "send-keys pane-bell failed" +wait_for @pb "pane-bell:$pane" +$TMUX set-hook -gu pane-bell || fail "unset pane-bell failed" +$TMUX kill-window -t main:raw-bell || fail "kill-window pane-bell failed" + # pane-resized carries old and new dimensions. pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'sleep 30') || fail "split-window pane-resized failed" @@ -276,6 +447,13 @@ $TMUX killp -t "$pane" || fail "kill-pane zoom failed" # client-attached and client-detached using a control client. $TMUX set -g @a 0 || fail "set @a failed" +$TMUX set -g @cl 0 || fail "set @cl failed" +$TMUX set-hook -g client-created \ + 'set -gF @cl "#{@cl}|#{hook}:#{hook_client}"' || + fail "set-hook client-created failed" +$TMUX set-hook -g client-closed \ + 'set -gF @cl "#{@cl}|#{hook}:#{hook_client}"' || + fail "set-hook client-closed failed" $TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' || fail "set-hook client-attached failed" $TMUX set-hook -g client-detached 'set -gF @a "#{hook}"' || @@ -284,9 +462,72 @@ mkfifo "$OUT/fifo" || fail "mkfifo failed" $TMUX -C attach -t main <"$OUT/fifo" >"$OUT/control.out" 2>&1 & exec 3>"$OUT/fifo" wait_for @a 'client-attached' + +# client-session-changed carries old and new sessions. +$TMUX new -d -s client-target || fail "new-session client target failed" +client=$($TMUX list-clients -F '#{client_name} #{client_control_mode}' | + awk '$2 == 1 { print $1; exit }') || + fail "list control client failed" +[ -n "$client" ] || fail "no control client found" +wait_for @cl "0|client-created:$client" +old_session=$($TMUX display -pt main '#{session_id}') || + fail "display old client session failed" +new_session=$($TMUX display -pt client-target '#{session_id}') || + fail "display new client session failed" +$TMUX set -g @cs 0 || fail "set @cs failed" +$TMUX set-hook -g client-session-changed \ + 'set -gF @cs "#{hook}:#{hook_client}:#{hook_session}:#{hook_old_session}:#{hook_new_session}"' || + fail "set-hook client-session-changed failed" +$TMUX switch-client -c "$client" -t client-target || + fail "switch-client session failed" +wait_for @cs "client-session-changed:$client:$new_session:$old_session:$new_session" +$TMUX set-hook -gu client-session-changed || + fail "unset client-session-changed failed" + exec 3>&- wait_for @a 'client-detached' +wait_for @cl "0|client-created:$client|client-closed:$client" +$TMUX set-hook -gu client-created || fail "unset client-created failed" +$TMUX set-hook -gu client-closed || fail "unset client-closed failed" $TMUX set-hook -gu client-attached || fail "unset client-attached failed" $TMUX set-hook -gu client-detached || fail "unset client-detached failed" +# client-resized carries old and new dimensions. +$TMUX2 new -d -s resize -x 80 -y 24 'sleep 30' || + fail "new-session client-resized failed" +$TMUX2 set -g @cr 0 || fail "set @cr failed" +$TMUX2 set-hook -g client-resized \ + 'set -gF @cr "#{hook}:#{hook_client}:#{hook_old_width},#{hook_old_height}->#{hook_width},#{hook_height}"' || + fail "set-hook client-resized failed" +$TMUX neww -d -n resize-client "$TMUX2 attach -t resize" || + fail "new outer window for client-resized failed" +i=0 +client= +while [ $i -lt 30 ]; do + client=$($TMUX2 list-clients -F '#{client_name}' 2>/dev/null | + awk 'NR == 1 { print; exit }') + [ -n "$client" ] && break + i=$((i + 1)) + sleep 0.2 +done +[ -n "$client" ] || fail "inner client did not attach" +old_size=$($TMUX2 list-clients -F '#{client_width},#{client_height}') || + fail "display old client size failed" +$TMUX resizew -t resize-client -x 70 -y 20 || + fail "resize outer client window failed" +i=0 +while [ $i -lt 30 ]; do + value=$($TMUX2 show -gqv @cr 2>/dev/null || true) + new_size=$($TMUX2 list-clients -F '#{client_width},#{client_height}' \ + 2>/dev/null || true) + expected="client-resized:$client:$old_size->$new_size" + [ "$new_size" != "$old_size" ] && [ "$value" = "$expected" ] && + break + i=$((i + 1)) + sleep 0.2 +done +[ "$value" = "$expected" ] || + fail "expected @cr to be '$expected' but got '$value'" +$TMUX2 set-hook -gu client-resized || fail "unset client-resized failed" + exit 0 From 447d804a3a23f38b76c2014e270186cdb28cea33 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 09:37:39 +0000 Subject: [PATCH 26/47] Size comparison should be >= not >, from Yayo Razo. --- cmd.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd.c b/cmd.c index d2e98d466..01f11834b 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd.c,v 1.186 2026/07/12 20:35:52 nicm Exp $ */ +/* $OpenBSD: cmd.c,v 1.187 2026/07/13 09:37:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -872,7 +872,7 @@ cmd_template_replace(const char *template, const char *s, int idx) ptr++; slen = strlen(s); - if (slen > SIZE_MAX / 3 || len > SIZE_MAX - (slen * 3) - 1) + if (slen >= SIZE_MAX / 3 || len > SIZE_MAX - (slen * 3) - 1) fatalx("argument too long"); buf = xrealloc(buf, len + (slen * 3) + 1); for (cp = s; *cp != '\0'; cp++) { From 6d227a84791536449213f44cf0354b7bf9c86f25 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 09:42:12 +0000 Subject: [PATCH 27/47] Set other cell when creating layout set, from Dane Jensen. --- layout-set.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/layout-set.c b/layout-set.c index 7e944d32f..65cb38918 100644 --- a/layout-set.c +++ b/layout-set.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout-set.c,v 1.39 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: layout-set.c,v 1.40 2026/07/13 09:42:12 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -135,6 +135,21 @@ layout_set_first_tiled(struct window *w) return (NULL); } +static void +layout_set_link_floating(struct window *w, struct layout_cell *lcroot) +{ + struct window_pane *wp; + struct layout_cell *lc; + + TAILQ_FOREACH(wp, &w->panes, entry) { + lc = wp->layout_cell; + if (!layout_cell_is_tiled(lc)) { + TAILQ_INSERT_TAIL(&lcroot->cells, lc, entry); + lc->parent = lcroot; + } + } +} + static void layout_set_even(struct window *w, enum layout_type type) { @@ -267,6 +282,8 @@ layout_set_main_h(struct window *w) wp = TAILQ_NEXT(wp, entry); TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry); wp->layout_cell->parent = lcroot; + layout_set_size(wp->layout_cell, sx, otherh, 0, 0); + layout_set_link_floating(w, lcroot); } else { lcother = layout_create_cell(lcroot); layout_set_size(lcother, sx, otherh, 0, 0); @@ -364,6 +381,8 @@ layout_set_main_h_mirrored(struct window *w) wp = TAILQ_NEXT(wp, entry); TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry); wp->layout_cell->parent = lcroot; + layout_set_size(wp->layout_cell, sx, otherh, 0, 0); + layout_set_link_floating(w, lcroot); } else { lcother = layout_create_cell(lcroot); layout_set_size(lcother, sx, otherh, 0, 0); @@ -461,6 +480,8 @@ layout_set_main_v(struct window *w) wp = TAILQ_NEXT(wp, entry); TAILQ_INSERT_TAIL(&lcroot->cells, wp->layout_cell, entry); wp->layout_cell->parent = lcroot; + layout_set_size(wp->layout_cell, otherw, sy, 0, 0); + layout_set_link_floating(w, lcroot); } else { lcother = layout_create_cell(lcroot); layout_make_node(lcother, LAYOUT_TOPBOTTOM); @@ -559,6 +580,8 @@ layout_set_main_v_mirrored(struct window *w) wp = TAILQ_NEXT(wp, entry); TAILQ_INSERT_HEAD(&lcroot->cells, wp->layout_cell, entry); wp->layout_cell->parent = lcroot; + layout_set_size(wp->layout_cell, otherw, sy, 0, 0); + layout_set_link_floating(w, lcroot); } else { lcother = layout_create_cell(lcroot); layout_make_node(lcother, LAYOUT_TOPBOTTOM); @@ -700,6 +723,7 @@ layout_set_tiled(struct window *w) w->sy - used); } + layout_set_link_floating(w, lcroot); layout_fix_offsets(w); layout_fix_panes(w, NULL); From 2b091d91d38c88b9dc9bbfaea08b6f80b56f6573 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 10:03:27 +0000 Subject: [PATCH 28/47] Make split-window inside a floating pane work more nicely and not immediately create an overlapping pane, from Dane Jensen. --- cmd-join-pane.c | 9 ++- cmd-split-window.c | 35 +++++----- layout.c | 158 ++++++++++++++++++++++++++++++++++++++++----- tmux.1 | 19 +++++- tmux.h | 9 ++- 5 files changed, 194 insertions(+), 36 deletions(-) diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 1b290a01e..01fbfbe5b 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.69 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.70 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -459,6 +459,13 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) return (CMD_RETURN_ERROR); } + if (args_has(args, 'h')) + flags |= SPAWN_HORIZONTAL; + if (args_has(args, 'b')) + flags |= SPAWN_BEFORE; + if (args_has(args, 'f')) + flags |= SPAWN_FULLSIZE; + lc = layout_get_tiled_cell(item, args, dst_w, dst_wp, flags, &cause); if (cause != NULL) { cmdq_error(item, "size or position %s", cause); diff --git a/cmd-split-window.c b/cmd-split-window.c index 64fa6b877..22c346c8b 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.141 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.142 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -62,8 +62,8 @@ const struct cmd_entry cmd_split_window_entry = { .usage = "[-bdefhIklPvWZ] [-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " - "[-R inactive-border-style] [-T title] " CMD_TARGET_PANE_USAGE " " - "[shell-command [argument ...]]", + "[-R inactive-border-style] [-T title] " + CMD_TARGET_PANE_USAGE " [shell-command [argument ...]]", .target = { 't', CMD_FIND_PANE, 0 }, @@ -96,14 +96,23 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) if (cmd_get_entry(self) == &cmd_new_pane_entry) is_floating = !args_has(args, 'L'); - else + else { is_floating = window_pane_is_floating(wp); + flags |= SPAWN_SPLIT; + } - flags = is_floating ? SPAWN_FLOATING : 0; + if (is_floating) + flags |= SPAWN_FLOATING; + if (args_has(args, 'h')) + flags |= SPAWN_HORIZONTAL; if (args_has(args, 'b')) flags |= SPAWN_BEFORE; if (args_has(args, 'f')) flags |= SPAWN_FULLSIZE; + if (args_has(args, 'd')) + flags |= SPAWN_DETACHED; + if (args_has(args, 'Z')) + flags |= SPAWN_ZOOM; input = args_has(args, 'I'); if (input || (count == 1 && *args_string(args, 0) == '\0')) @@ -131,9 +140,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) } } - if (is_floating) - lc = layout_get_floating_cell(item, args, lines, w, wp, &cause); - else + if (flags & SPAWN_FLOATING) { + lc = layout_get_floating_cell(item, args, lines, w, wp, flags, + &cause); + } else lc = layout_get_tiled_cell(item, args, w, wp, flags, &cause); if (cause != NULL) { cmdq_error(item, "%s", cause); @@ -159,12 +169,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) sc.idx = -1; sc.cwd = args_get(args, 'c'); - sc.flags = flags; - if (args_has(args, 'd')) - sc.flags |= SPAWN_DETACHED; - if (args_has(args, 'Z')) - sc.flags |= SPAWN_ZOOM; if ((new_wp = spawn_pane(&sc, &cause)) == NULL) { cmdq_error(item, "create pane failed: %s", cause); @@ -240,10 +245,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) break; } } - if (!args_has(args, 'd')) + if (~flags & SPAWN_DETACHED) cmd_find_from_winlink_pane(current, wl, new_wp, 0); - if (!is_floating) { + if (~flags & SPAWN_FLOATING) { window_pop_zoom(wp->window); server_redraw_window(wp->window); } diff --git a/layout.c b/layout.c index 177d2f4cf..bddad3ab8 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD: layout.c,v 1.93 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: layout.c,v 1.94 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1598,7 +1598,7 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, struct window *w, struct window_pane *wp, int flags, char **cause) { struct layout_cell *lc; - enum layout_type type; + enum layout_type type = LAYOUT_TOPBOTTOM; u_int curval; int size = -1; char *error = NULL; @@ -1608,12 +1608,11 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, return (NULL); } - type = LAYOUT_TOPBOTTOM; - if (args_has(args, 'h')) + if (flags & SPAWN_HORIZONTAL) type = LAYOUT_LEFTRIGHT; if (args_has(args, 'l') || args_has(args, 'p')) { - if (args_has(args, 'f')) { + if (flags & SPAWN_FULLSIZE) { if (type == LAYOUT_TOPBOTTOM) curval = w->sy; else @@ -1641,12 +1640,7 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, return (NULL); } - if (args_has(args, 'b')) - flags |= SPAWN_BEFORE; - if (args_has(args, 'f')) - flags |= SPAWN_FULLSIZE; - - window_push_zoom(wp->window, 1, args_has(args, 'Z')); + window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM)); lc = layout_split_pane(wp, type, size, flags); if (lc == NULL) *cause = xstrdup("no space for a new pane"); @@ -1656,17 +1650,24 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args, struct layout_cell * layout_get_floating_cell(struct cmdq_item *item, struct args *args, - enum pane_lines lines, struct window *w, struct window_pane *wp, + enum pane_lines lines, struct window *w, struct window_pane *wp, int flags, char **cause) { - struct layout_cell *lcnew; + struct layout_cell *lcnew, *lc = wp->layout_cell; struct layout_geometry fg; layout_geometry_init(&fg); - if (layout_floating_args_parse(item, args, lines, w, &fg, cause) != 0) - return (NULL); + if (flags & SPAWN_SPLIT) { + if (layout_split_floating_cell(lc, w, &fg, lines, flags, cause) + != 0) + return (NULL); + } else { + if (layout_floating_args_parse(item, args, lines, w, &fg, cause) + != 0) + return (NULL); + } - window_push_zoom(wp->window, 1, args_has(args, 'Z')); + window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM)); lcnew = layout_floating_pane(w, wp, &fg); return (lcnew); } @@ -1766,6 +1767,131 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args, return (0); } +int +layout_split_floating_cell(struct layout_cell *lc, struct window *w, + struct layout_geometry *out, enum pane_lines lines, int flags, + char **cause) +{ + struct layout_geometry old, new; + int tborder = 1, bborder = w->sy - 1; + int lborder = 3, rborder = w->sx - 3; + int border = lines != PANE_LINES_NONE ? 1 : 0; + int size, space; + + /* First, move the target cell in-bounds. */ + memcpy(&old, &lc->g, sizeof old); + if (lborder > old.xoff - border) + old.xoff = lborder + border; + if (rborder < old.xoff + (int)old.sx + border) + old.xoff = rborder - (int)old.sx - border; + if (tborder > old.yoff - border) + old.yoff = tborder + border; + if (bborder < old.yoff + (int)old.sy + border) + old.yoff = bborder - (int)old.sy - border; + + /* Move the new cell to its ideal position. */ + memcpy(&new, &old, sizeof new); + if (flags & SPAWN_HORIZONTAL) { + if (flags & SPAWN_BEFORE) + new.xoff -= old.sx + 2 * border; + else + new.xoff += old.sx + 2 * border; + } else { + if (flags & SPAWN_BEFORE) + new.yoff -= old.sy + 2 * border; + else + new.yoff += old.sy + 2 * border; + } + + /* + * The position of the new cell is checked to see if it is in bounds. + * If it isn't, the availible space is split and equally given to both + * cells. Only one border is check because the target cell is in bounds + * already. + */ + if (lborder > new.xoff - border) { + /* + * The space for both panes is calculated. Since the offsets are + * associated to where pane contents start, we remove pane + * borders from the space. '1' is added in case the space is + * odd. + */ + space = old.xoff + old.sx - lborder - 3 * border + 1; + size = space / 2; + new.sx = size; + old.sx = size; + new.xoff = lborder + border; + old.xoff = new.xoff + new.sx + 2 * border; + /* + * If the original space was to be odd (now even), subtract 1 + * from the rightmost cell + */ + if (space % 2 == 0) + old.sx -= 1; + } else if (rborder < new.xoff + (int)new.sx + border) { + space = rborder - old.xoff - 3 * border + 1; + size = space / 2; + new.sx = size; + old.sx = size; + new.xoff = old.xoff + old.sx + 2 * border; + if (space % 2 == 0) + new.sx -= 1; + } else if (tborder > new.yoff - border) { + space = old.sy + old.yoff - tborder - 3 * border + 1; + size = space / 2; + new.sy = size; + old.sy = size; + new.yoff = tborder + border; + old.yoff = new.yoff + new.sy + 2 * border; + if (space % 2 == 0) + old.sy -= 1; + } else if (bborder < new.yoff + (int)new.sy + border) { + space = bborder - old.yoff - 3 * border + 1; + size = space / 2; + new.sy = size; + old.sy = size; + new.yoff = old.yoff + old.sy + 2 * border; + if (space % 2 == 0) + new.sy -= 1; + } + + /* + * Expand the cell to occupy the whole availible space where it was + * spawned. + */ + if (flags & SPAWN_FULLSIZE) { + if (flags & SPAWN_HORIZONTAL) { + new.yoff = tborder + border; + new.sy = bborder - tborder - 2 * border; + if (flags & SPAWN_BEFORE) { + new.xoff = lborder + border; + new.sx = old.xoff - new.xoff - 2 * border; + } else { + new.sx = rborder - new.xoff - border; + } + } else { + new.xoff = lborder + border; + new.sx = rborder - lborder - 2 * border; + if (flags & SPAWN_BEFORE) { + new.yoff = tborder + border; + new.sy = old.yoff - new.yoff - 2 * border; + } else { + new.sy = bborder - new.yoff - border; + } + } + } + + if (new.sx < PANE_MINIMUM || new.sy < PANE_MINIMUM || + old.sx < PANE_MINIMUM || old.sy < PANE_MINIMUM) { + *cause = xstrdup("no space for a new pane"); + return (-1); + } + + layout_set_size(lc, old.sx, old.sy, old.xoff, old.yoff); + memcpy(out, &new, sizeof *out); + return (0); +} + /* * Removes a cell from the tiled layout by giving the cell's space to the * nearest neighbour. diff --git a/tmux.1 b/tmux.1 index 262187889..01d654a25 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1132 2026/07/10 15:20:06 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1133 2026/07/13 10:03:27 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -14,7 +14,7 @@ .\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING .\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. .\" -.Dd $Mdocdate: July 10 2026 $ +.Dd $Mdocdate: July 13 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -4075,6 +4075,21 @@ or full window width (with .Fl v ) , instead of splitting the active pane. .Pp +If +.Ar target\-pane +is floating, a new floating pane is created adjacent to +.Ar target\-pane +with equal sizes. +The position is determined by the +.Fl h , +.Fl v , +and +.Fl b +flags. +If any of the new pane would not be visible on the screen, +.Ar target\-pane +and the new pane are resized to fit. +.Pp .Fl k keeps the pane open after the optional .Ar shell\-command diff --git a/tmux.h b/tmux.h index 5031a581f..387911558 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1396 2026/07/10 15:45:11 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1397 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2455,6 +2455,8 @@ struct spawn_context { #define SPAWN_EMPTY 0x40 #define SPAWN_ZOOM 0x80 #define SPAWN_FLOATING 0x100 +#define SPAWN_HORIZONTAL 0x200 +#define SPAWN_SPLIT 0x400 }; /* Paste buffer. */ @@ -3790,10 +3792,13 @@ struct layout_cell *layout_get_tiled_cell(struct cmdq_item *, struct args *, struct window *, struct window_pane *, int, char **); struct layout_cell *layout_get_floating_cell(struct cmdq_item *, struct args *, enum pane_lines, struct window *, struct window_pane *, - char **); + int, char **); int layout_floating_args_parse(struct cmdq_item *, struct args *, enum pane_lines, struct window *, struct layout_geometry *, char **); +int layout_split_floating_cell(struct layout_cell *, + struct window *, struct layout_geometry *, enum pane_lines, + int, char **); int layout_remove_tile(struct window *, struct layout_cell *); int layout_insert_tile(struct window *, struct layout_cell *); From fe1b877410c9f4b5ad351427b9de9097d606e927 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 10:22:52 +0000 Subject: [PATCH 29/47] Allow drag on a selection to modify it, from Michael Grant. --- window-copy.c | 159 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 151 insertions(+), 8 deletions(-) diff --git a/window-copy.c b/window-copy.c index 997cb8888..0e27af5c5 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.413 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.414 2026/07/13 10:22:52 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -103,8 +103,12 @@ static void window_copy_goto_line(struct window_mode_entry *, const char *); static void window_copy_update_cursor(struct window_mode_entry *, u_int, u_int); static void window_copy_start_selection(struct window_mode_entry *); +static int window_copy_mouse_in_selection(struct window_mode_entry *, + u_int, u_int, int *, int *); static int window_copy_adjust_selection(struct window_mode_entry *, u_int *, u_int *); +static int window_copy_update_selection_view(struct window_mode_entry *, + int, int); static int window_copy_set_selection(struct window_mode_entry *, int, int); static int window_copy_update_selection(struct window_mode_entry *, int, int); @@ -862,7 +866,7 @@ window_copy_scroll1(struct window_mode_entry *wme, struct window_pane *wp, if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); - window_copy_update_selection(wme, 1, 0); + window_copy_update_selection_view(wme, 1, 0); window_pane_scrollbar_show(wp, 1); window_copy_redraw_screen(wme, 1); } @@ -1648,7 +1652,7 @@ window_copy_cmd_scroll_to(struct window_copy_cmd_state *cs, u_int to) data->cy += delta; } - window_copy_update_selection(wme, 0, 0); + window_copy_update_selection_view(wme, 0, 0); return (WINDOW_COPY_CMD_REDRAW); } @@ -2361,6 +2365,7 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_CANCEL); return (WINDOW_COPY_CMD_NOTHING); } + for (; np != 0; np--) window_copy_cursor_down(wme, 1); if (data->scroll_exit && data->oy == 0 && data->screen.sel == NULL) @@ -2374,6 +2379,17 @@ window_copy_cmd_scroll_down_and_cancel(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; struct window_copy_mode_data *data = wme->data; u_int np = wme->prefix; + int dragging; + + /* With a selection but no active drag, only scroll the view. */ + dragging = (cs->c != NULL && cs->c->tty.mouse_drag_flag != 0); + if (data->screen.sel != NULL && !dragging) { + data->cursordrag = CURSORDRAG_NONE; + data->lineflag = LINE_SEL_NONE; + /* Move down in the history. */ + window_copy_scroll_up(wme, np); + return (WINDOW_COPY_CMD_NOTHING); + } for (; np != 0; np--) window_copy_cursor_down(wme, 1); @@ -2388,6 +2404,7 @@ window_copy_cmd_scroll_up(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; struct window_copy_mode_data *data = wme->data; u_int np = wme->prefix; + int dragging; /* * If at the top, nothing can change, so return WINDOW_COPY_CMD_NOTHING @@ -2395,6 +2412,17 @@ window_copy_cmd_scroll_up(struct window_copy_cmd_state *cs) */ if (data->oy == screen_hsize(data->backing)) return (WINDOW_COPY_CMD_NOTHING); + + /* With a selection but no active drag, only scroll the view. */ + dragging = (cs->c != NULL && cs->c->tty.mouse_drag_flag != 0); + if (data->screen.sel != NULL && !dragging) { + data->cursordrag = CURSORDRAG_NONE; + data->lineflag = LINE_SEL_NONE; + /* Move up in the history. */ + window_copy_scroll_down(wme, np); + return (WINDOW_COPY_CMD_NOTHING); + } + for (; np != 0; np--) window_copy_cursor_up(wme, 1); return (WINDOW_COPY_CMD_MOVE); @@ -3117,7 +3145,7 @@ window_copy_cmd_recentre_top_bottom(struct window_copy_cmd_state *cs) data->cy = cy + (data->oy - oy); break; } - window_copy_update_selection(wme, 0, 0); + window_copy_update_selection_view(wme, 0, 0); return (WINDOW_COPY_CMD_REDRAW); } @@ -5504,6 +5532,76 @@ window_copy_start_selection(struct window_mode_entry *wme) window_copy_set_selection(wme, 1, 0); } +/* + * Return whether x,y is inside the selection and optionally identify the + * endpoint. + */ +static int +window_copy_mouse_in_selection(struct window_mode_entry *wme, u_int x, u_int y, + int *on_start, int *on_end) +{ + struct window_copy_mode_data *data = wme->data; + u_int sx = screen_size_x(&data->screen); + u_int sy = screen_size_y(&data->screen); + u_int hsize, screeny; + u_int mx, my, selx, sely, endselx, endsely; + u_int cursorx, cursory; + long long mpos, spos, epos, dstart, dend; + + if (on_start != NULL) + *on_start = 0; + if (on_end != NULL) + *on_end = 0; + if (data->screen.sel == NULL) + return (0); + + hsize = screen_hsize(data->backing); + screeny = hsize - data->oy; + + selx = window_copy_cursor_offset(wme, data->selx, sx); + sely = data->sely - screeny; + if (data->sely >= screeny && sely < sy && x == selx && y == sely) { + if (on_start != NULL) + *on_start = 1; + return (1); + } + + endselx = window_copy_cursor_offset(wme, data->endselx, sx); + endsely = data->endsely - screeny; + if (data->endsely >= screeny && + endsely < sy && + x == endselx && y == endsely) { + if (on_end != NULL) + *on_end = 1; + return (1); + } + + cursorx = window_copy_cursor_offset(wme, data->cx, + sx); cursory = data->cy; + if (x != cursorx || y != cursory) { + if (!screen_check_selection(&data->screen, x, y)) + return (0); + } + + if (on_start != NULL || on_end != NULL) { + mx = window_copy_cursor_unoffset(wme, x, sx); + my = screeny + y; + mpos = (long long)my * (sx + 1) + mx; + spos = (long long)data->sely * (sx + 1) + data->selx; + epos = (long long)data->endsely * (sx + 1) + data->endselx; + dstart = llabs(mpos - spos); + dend = llabs(mpos - epos); + if (dstart <= dend) { + if (on_start != NULL) + *on_start = 1; + } else { + if (on_end != NULL) + *on_end = 1; + } + } + return (1); +} + static int window_copy_adjust_selection(struct window_mode_entry *wme, u_int *selx, u_int *sely) @@ -5549,6 +5647,35 @@ window_copy_update_selection(struct window_mode_entry *wme, int may_redraw, return (window_copy_set_selection(wme, may_redraw, no_reset)); } +/* + * Update the visible selection after the view has changed. If the mouse is + * not dragging, keep the selection endpoints anchored to the same text. + */ +static int +window_copy_update_selection_view(struct window_mode_entry *wme, int may_redraw, + int no_reset) +{ + struct window_copy_mode_data *data = wme->data; + u_int selx, sely, endselx, endsely; + int changed; + + if (data->cursordrag != CURSORDRAG_NONE) + return (window_copy_update_selection(wme, may_redraw, no_reset)); + + selx = data->selx; + sely = data->sely; + endselx = data->endselx; + endsely = data->endsely; + + changed = window_copy_update_selection(wme, may_redraw, 1); + + data->selx = selx; + data->sely = sely; + data->endselx = endselx; + data->endsely = endsely; + return (changed); +} + static int window_copy_set_selection(struct window_mode_entry *wme, int may_redraw, int no_reset) @@ -6578,7 +6705,7 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); - window_copy_update_selection(wme, 0, 0); + window_copy_update_selection_view(wme, 0, 0); if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { @@ -6649,7 +6776,7 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny) if (data->searchmark != NULL && !data->timeout) window_copy_search_marks(wme, NULL, data->searchregex, 1); - window_copy_update_selection(wme, 0, 0); + window_copy_update_selection_view(wme, 0, 0); if (window_copy_line_numbers_active(wme)) { if (window_copy_line_number_mode(wme) != WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) { @@ -6738,6 +6865,7 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) struct window_mode_entry *wme; struct window_copy_mode_data *data; u_int x, y, yg; + int inside_selection, on_start, on_end; if (c == NULL) return; @@ -6758,10 +6886,16 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) c->tty.mouse_drag_release = window_copy_drag_release; data = wme->data; + on_start = on_end = 0; + inside_selection = window_copy_mouse_in_selection(wme, x, y, + &on_start, &on_end); x = window_copy_cursor_unoffset(wme, x, screen_size_x(&data->screen)); yg = screen_hsize(data->backing) + y - data->oy; - if (x < data->selrx || x > data->endselrx || yg != data->selry) + if (on_start || on_end || !inside_selection || + x < data->selrx || x > data->endselrx || yg != data->selry) { + data->lineflag = LINE_SEL_NONE; data->selflag = SEL_CHAR; + } switch (data->selflag) { case SEL_WORD: if (data->separators != NULL) { @@ -6777,7 +6911,15 @@ window_copy_start_drag(struct client *c, struct mouse_event *m) break; case SEL_CHAR: window_copy_update_cursor(wme, x, y); - window_copy_start_selection(wme); + if (!inside_selection) + window_copy_start_selection(wme); + else { + if (on_start) + data->cursordrag = CURSORDRAG_SEL; + else if (on_end) + data->cursordrag = CURSORDRAG_ENDSEL; + window_copy_update_selection(wme, 1, 0); + } break; } @@ -6853,6 +6995,7 @@ window_copy_drag_release(struct client *c, struct mouse_event *m) data = wme->data; if (window_copy_line_numbers_active(wme)) window_copy_drag_update(c, m); + data->cursordrag = CURSORDRAG_NONE; evtimer_del(&data->dragtimer); } From 4a06a1f2960605536bf10ef834d8dbf80df4018e Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 10:29:17 +0000 Subject: [PATCH 30/47] Work out a single layout for the entire prompt so it can all be drawn together, fixes centered prompts. GitHub issue 5328. --- prompt.c | 301 +++++++++++++++++++++++++++++++++++++++---------------- tmux.h | 4 +- 2 files changed, 218 insertions(+), 87 deletions(-) diff --git a/prompt.c b/prompt.c index 6187a7a12..c12a249a9 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD: prompt.c,v 1.4 2026/06/26 14:40:30 nicm Exp $ */ +/* $OpenBSD: prompt.c,v 1.5 2026/07/13 10:29:17 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -42,6 +42,8 @@ struct prompt { char *word_separators; struct grid_cell style; struct grid_cell command_style; + char *style_str; + char *command_style_str; enum screen_cursor_style cstyle; enum screen_cursor_style command_cstyle; int ccolour; @@ -61,11 +63,30 @@ struct prompt { char *complete_display; }; +struct prompt_layout { + u_int area_x; + u_int area_width; + + u_int content_x; + u_int content_width; + + u_int label_width; + u_int input_x; + u_int cursor_x; + + u_int input_offset; + u_int input_width; +}; + static char *prompt_complete(struct prompt *, const char *, u_int); static void prompt_clear_complete(struct prompt *); -static char *prompt_expand(struct prompt *); +static struct format_tree *prompt_format_tree(struct prompt *); +static char *prompt_expand1(struct prompt *, struct format_tree *); +static void prompt_effective_style(struct prompt *, struct style *, + struct format_tree *); +static void prompt_layout(struct prompt *, u_int, u_int, + struct prompt_layout *, char **, struct style *); static int prompt_replace_complete(struct prompt *, const char *); -static u_int prompt_width(struct prompt *, u_int); /* Get prompt flags as a string. */ static const char * @@ -120,6 +141,8 @@ prompt_set_options(struct prompt_create_data *pd, struct session *s) style_apply(&pd->style, oo, "message-style", NULL); style_apply(&pd->command_style, oo, "message-command-style", NULL); + pd->style_str = options_get_string(oo, "message-style"); + pd->command_style_str = options_get_string(oo, "message-command-style"); n = options_get_number(oo, "prompt-cursor-style"); screen_set_cursor_style(n, &pd->cstyle, &pd->cmode); n = options_get_number(oo, "prompt-command-cursor-style"); @@ -179,6 +202,8 @@ prompt_create(const struct prompt_create_data *pd) memcpy(&pr->style, &pd->style, sizeof pr->style); memcpy(&pr->command_style, &pd->command_style, sizeof pr->command_style); + pr->style_str = xstrdup(pd->style_str); + pr->command_style_str = xstrdup(pd->command_style_str); pr->cstyle = pd->cstyle; pr->command_cstyle = pd->command_cstyle; pr->ccolour = pd->ccolour; @@ -201,6 +226,8 @@ prompt_free(struct prompt *pr) if (pr->freecb != NULL && pr->data != NULL) pr->freecb(pr->data); free(pr->message_format); + free(pr->style_str); + free(pr->command_style_str); free(pr->word_separators); free(pr->last); free(pr->string); @@ -321,16 +348,17 @@ prompt_redraw_character(struct screen_write_ctx *ctx, u_int offset, * 0 otherwise. */ static int -prompt_redraw_quote(const struct prompt *pr, u_int pcursor, - struct screen_write_ctx *ctx, u_int offset, u_int pwidth, u_int *width, +prompt_redraw_quote(const struct prompt *pr, u_int pcursor, u_int input_x, + struct screen_write_ctx *ctx, u_int offset, u_int pw, u_int *w, struct grid_cell *gc) { struct utf8_data ud; - if (pr->flags & PROMPT_QUOTENEXT && ctx->s->cx == pcursor + 1) { + if (pr->flags & PROMPT_QUOTENEXT && + pcursor >= offset && + ctx->s->cx == input_x + pcursor - offset) { utf8_set(&ud, '^'); - return (prompt_redraw_character(ctx, offset, pwidth, - width, gc, &ud)); + return (prompt_redraw_character(ctx, offset, pw, w, gc, &ud)); } return (1); } @@ -368,12 +396,12 @@ prompt_draw_complete(struct prompt *pr, struct screen_write_ctx *ctx, u_int ax, free(ud); } -/* Expand prompt string using the current input. */ -static char * -prompt_expand(struct prompt *pr) +/* Create the prompt format tree using the current input. */ +static struct format_tree * +prompt_format_tree(struct prompt *pr) { struct format_tree *ft; - char *expanded, *prompt, *tmp; + char *tmp; if (cmd_find_valid_state(&pr->state)) ft = format_create_from_state(NULL, NULL, &pr->state); @@ -385,31 +413,145 @@ prompt_expand(struct prompt *pr) format_add(ft, "prompt_flags", "%s", prompt_flags_to_string(pr->flags)); format_add(ft, "prompt_type", "%s", prompt_type_string(pr->type)); - prompt = format_expand_time(ft, pr->string); - format_add(ft, "message", "%s", prompt); if (pr->flags & PROMPT_COMMANDMODE) format_add(ft, "command_prompt", "1"); else format_add(ft, "command_prompt", "0"); + return (ft); +} + +/* Expand prompt string using the current input. */ +static char * +prompt_expand1(struct prompt *pr, struct format_tree *ft) +{ + char *expanded, *prompt; + + prompt = format_expand_time(ft, pr->string); + format_add(ft, "message", "%s", prompt); expanded = format_expand_time(ft, pr->message_format); free(prompt); - format_free(ft); return (expanded); } -/* Work out the width used by the prompt string. */ -static u_int -prompt_width(struct prompt *pr, u_int aw) +/* Get the effective message style for the current prompt. */ +static void +prompt_effective_style(struct prompt *pr, struct style *sy, + struct format_tree *ft) { - char *expanded; - u_int start; + const char *s; + struct grid_cell *gc; + char *expanded; - expanded = prompt_expand(pr); - start = format_width(expanded); - if (start > aw) - start = aw; - free(expanded); - return (start); + if (pr->flags & PROMPT_COMMANDMODE) { + s = pr->command_style_str; + gc = &pr->command_style; + } else { + s = pr->style_str; + gc = &pr->style; + } + + style_set(sy, gc); + if (s != NULL) { + expanded = format_expand_time(ft, s); + if (style_parse(sy, &grid_default_cell, expanded) != 0) + style_set(sy, gc); + free(expanded); + } +} + +/* Work out where the editable prompt content appears. */ +static void +prompt_layout(struct prompt *pr, u_int ax, u_int aw, struct prompt_layout *pl, + char **expanded, struct style *sy) +{ + char *local = NULL; + struct format_tree *ft; + u_int pcursor, pwidth, end, width, offset, avail; + + memset(pl, 0, sizeof *pl); + pl->area_x = ax; + pl->area_width = aw; + + ft = prompt_format_tree(pr); + if (sy != NULL) + prompt_effective_style(pr, sy, ft); + if (expanded != NULL) + *expanded = prompt_expand1(pr, ft); + else { + local = prompt_expand1(pr, ft); + expanded = &local; + } + format_free(ft); + + if (aw == 0) { + free(local); + return; + } + + pl->label_width = format_width(*expanded); + if (pl->label_width > aw) + pl->label_width = aw; + + pcursor = utf8_strwidth(pr->buffer, pr->index); + pwidth = utf8_strwidth(pr->buffer, -1); + if (pr->flags & PROMPT_QUOTENEXT) + pwidth++; + + avail = aw - pl->label_width; + if (avail == 0) { + pl->input_offset = 0; + pl->input_width = 0; + pl->cursor_x = pl->label_width; + } else { + if (pcursor >= avail) { + offset = (pcursor - avail) + 1; + width = avail; + } else { + offset = 0; + width = pwidth; + } + if (width > avail) + width = avail; + + pl->input_offset = offset; + pl->input_width = width; + pl->cursor_x = pl->label_width + pcursor - offset; + } + + pl->content_width = pl->label_width + pl->input_width; + if (pr->complete_display != NULL && + pr->index == utf8_strlen(pr->buffer) && + pl->cursor_x < aw) { + avail = aw - pl->cursor_x; + width = utf8_cstrwidth(pr->complete_display); + if (width > avail) + width = avail; + end = pl->cursor_x + width; + if (end > pl->content_width) + pl->content_width = end; + } + if (pl->content_width > aw) + pl->content_width = aw; + + if (sy != NULL) { + switch (sy->align) { + case STYLE_ALIGN_CENTRE: + case STYLE_ALIGN_ABSOLUTE_CENTRE: + pl->content_x = ax + (aw - pl->content_width) / 2; + break; + case STYLE_ALIGN_RIGHT: + pl->content_x = ax + aw - pl->content_width; + break; + default: + pl->content_x = ax; + break; + } + } else + pl->content_x = ax; + + pl->input_x = pl->content_x + pl->label_width; + pl->cursor_x += pl->content_x; + free(local); } /* Choose a completion from a mouse position. */ @@ -460,70 +602,59 @@ prompt_draw(struct prompt *pr, struct prompt_draw_data *pd) { struct screen_write_ctx *ctx = pd->ctx; struct screen *s = ctx->s; - u_int ax = pd->area_x, py = pd->prompt_line; - u_int aw = pd->area_width, *cx = pd->cursor_x; + u_int ax = pd->area_x, py = pd->prompt_line, *cx; + u_int aw = pd->area_width; struct grid_cell gc; - u_int i, offset, left, start, width; - u_int pcursor, pwidth; + struct prompt_layout pl; + struct style sy; + u_int i, width, pcursor; char *expanded; - /* Choose the cursor colour and style for this prompt. */ if (pr->flags & PROMPT_COMMANDMODE) { - memcpy(&gc, &pr->command_style, sizeof gc); s->default_cstyle = pr->command_cstyle; s->default_mode = pr->command_cmode; s->default_ccolour = pr->command_ccolour; } else { - memcpy(&gc, &pr->style, sizeof gc); s->default_cstyle = pr->cstyle; s->default_mode = pr->cmode; s->default_ccolour = pr->ccolour; } - expanded = prompt_expand(pr); - start = format_width(expanded); - if (start > aw) - start = aw; - *cx = ax + start; + prompt_layout(pr, ax, aw, &pl, &expanded, &sy); + memcpy(&gc, &sy.gc, sizeof gc); + cx = pd->cursor_x; + *cx = pl.cursor_x; screen_write_cursormove(ctx, ax, py, 0); - format_draw(ctx, &gc, aw, expanded, NULL, 0); - screen_write_cursormove(ctx, ax + start, py, 0); - free(expanded); - - left = aw - start; - if (left == 0) - return; + if (sy.fill != 8) + screen_write_clearcharacter(ctx, aw, sy.fill); pcursor = utf8_strwidth(pr->buffer, pr->index); - pwidth = utf8_strwidth(pr->buffer, -1); - if (pr->flags & PROMPT_QUOTENEXT) - pwidth++; - if (pcursor >= left) { - /* - * The cursor would be outside the screen so start drawing - * with it on the right. - */ - offset = (pcursor - left) + 1; - pwidth = left; - } else - offset = 0; - if (pwidth > left) - pwidth = left; - *cx = ax + start + pcursor - offset; - width = 0; - for (i = 0; pr->buffer[i].size != 0; i++) { - if (!prompt_redraw_quote(pr, pcursor, ctx, offset, pwidth, - &width, &gc)) - break; - if (!prompt_redraw_character(ctx, offset, pwidth, &width, &gc, - &pr->buffer[i])) - break; + if (pl.content_width != 0) { + screen_write_cursormove(ctx, pl.content_x, py, 0); + if (pl.label_width != 0) + format_draw(ctx, &gc, pl.label_width, expanded, NULL, + 0); + + screen_write_cursormove(ctx, pl.input_x, py, 0); + width = 0; + for (i = 0; pr->buffer[i].size != 0; i++) { + if (!prompt_redraw_quote(pr, pcursor, pl.input_x, + ctx, pl.input_offset, pl.input_width, &width, + &gc)) + break; + if (!prompt_redraw_character(ctx, pl.input_offset, + pl.input_width, &width, &gc, &pr->buffer[i])) + break; + } + prompt_redraw_quote(pr, pcursor, pl.input_x, ctx, + pl.input_offset, pl.input_width, &width, &gc); + + prompt_draw_complete(pr, ctx, pl.content_x, pl.content_width, + pl.cursor_x, py, &gc); } - prompt_redraw_quote(pr, pcursor, ctx, offset, pwidth, &width, &gc); - - prompt_draw_complete(pr, ctx, ax, aw, *cx, py, &gc); + free(expanded); } /* Move cursor in prompt from a mouse position. */ @@ -532,36 +663,34 @@ prompt_mouse(struct prompt *pr, u_int x, u_int ax, u_int aw, int *redraw) { struct utf8_data *ud; enum prompt_key_result result; - u_int cx, start, left, pcursor, pwidth, offset, width; - u_int target; + struct prompt_layout pl; + struct style sy; + char *expanded; + u_int pwidth, width, target; size_t idx; if (x < ax || x >= ax + aw) return (PROMPT_KEY_NOT_HANDLED); - start = prompt_width(pr, aw); - left = aw - start; - if (left == 0) + prompt_layout(pr, ax, aw, &pl, &expanded, &sy); + free(expanded); + + if (pl.input_width == 0) return (PROMPT_KEY_HANDLED); - pcursor = utf8_strwidth(pr->buffer, pr->index); pwidth = utf8_strwidth(pr->buffer, -1); if (pr->flags & PROMPT_QUOTENEXT) pwidth++; - if (pcursor >= left) - offset = (pcursor - left) + 1; - else - offset = 0; - cx = ax + start + pcursor - offset; - result = prompt_mouse_complete(pr, x, cx, ax, aw, redraw); + result = prompt_mouse_complete(pr, x, pl.cursor_x, pl.content_x, + pl.content_width, redraw); if (result != PROMPT_KEY_NOT_HANDLED) return (result); - if (x <= ax + start) - target = offset; + if (x <= pl.input_x) + target = pl.input_offset; else - target = offset + x - (ax + start); + target = pl.input_offset + x - pl.input_x; if (target > pwidth) target = pwidth; diff --git a/tmux.h b/tmux.h index 387911558..ea357d08c 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1397 2026/07/13 10:03:27 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1398 2026/07/13 10:29:17 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2086,6 +2086,8 @@ struct prompt_create_data { struct grid_cell style; struct grid_cell command_style; + const char *style_str; + const char *command_style_str; enum screen_cursor_style cstyle; enum screen_cursor_style command_cstyle; int ccolour; From 006cce5e4d7ab6f9c09b988b03b1e040231e3f5c Mon Sep 17 00:00:00 2001 From: jsg Date: Mon, 13 Jul 2026 12:53:56 +0000 Subject: [PATCH 31/47] fix window_copy_mouse_in_selection() indentation; ok nicm@ --- window-copy.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/window-copy.c b/window-copy.c index 0e27af5c5..6d0390fe9 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.414 2026/07/13 10:22:52 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.415 2026/07/13 12:53:56 jsg Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -5576,8 +5576,8 @@ window_copy_mouse_in_selection(struct window_mode_entry *wme, u_int x, u_int y, return (1); } - cursorx = window_copy_cursor_offset(wme, data->cx, - sx); cursory = data->cy; + cursorx = window_copy_cursor_offset(wme, data->cx, sx); + cursory = data->cy; if (x != cursorx || y != cursory) { if (!screen_check_selection(&data->screen, x, y)) return (0); From e00a1a3e2656bdb1aa2b4bb10edf30a3c864adf1 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 13:01:14 +0000 Subject: [PATCH 32/47] Add some additional simple events. --- cmd-break-pane.c | 9 ++++--- cmd-join-pane.c | 3 ++- cmd-swap-pane.c | 9 ++++++- input.c | 8 +++--- options-table.c | 10 ++++++- resize.c | 22 +++++++++++++-- server-client.c | 69 +++++++++++++++++++++++++++++++++++++++++++++--- session.c | 51 ++++++++++++++++++++++++++++++++--- spawn.c | 6 +++-- tmux.1 | 18 ++++++++++++- tmux.h | 4 ++- window.c | 30 +++++++++++++++++++-- 12 files changed, 216 insertions(+), 23 deletions(-) diff --git a/cmd-break-pane.c b/cmd-break-pane.c index 8d85c57b2..dcc5a6af4 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-break-pane.c,v 1.72 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-break-pane.c,v 1.73 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -98,9 +98,9 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) struct session *src_s = source->s; struct session *dst_s = target->s; struct window_pane *wp = source->wp; - struct window *w = wl->window; + struct window *w = wl->window, *old_w = w; char *cause, *cp; - int idx = target->idx, before; + int idx = target->idx, before, old_idx = wl->idx; const char *template, *name = args_get(args, 'n'); if (args_has(args, 'W')) @@ -137,6 +137,7 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) wl = winlink_find_by_window(&dst_s->windows, w); if (wl == NULL) return (CMD_RETURN_ERROR); + window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); goto out; } if (idx != -1 && winlink_find_by_index(&dst_s->windows, idx) != NULL) { @@ -175,6 +176,8 @@ cmd_break_pane_exec(struct cmd *self, struct cmdq_item *item) idx = -1 - options_get_number(dst_s->options, "base-index"); wl = session_attach(dst_s, w, idx, &cause); /* can't fail */ window_remove_ref(w, __func__); + events_fire_window("window-created", w); + window_fire_pane_moved(wp, old_w, old_idx, w, wl->idx); if (!args_has(args, 'd')) { session_select(dst_s, wl->idx); cmd_find_from_session(current, dst_s, 0); diff --git a/cmd-join-pane.c b/cmd-join-pane.c index 01fbfbe5b..927ccda43 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-join-pane.c,v 1.70 2026/07/13 10:03:27 nicm Exp $ */ +/* $OpenBSD: cmd-join-pane.c,v 1.71 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -506,6 +506,7 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item) } else server_status_session(dst_s); + window_fire_pane_moved(src_wp, src_w, src_wl->idx, dst_w, dst_idx); if (window_count_panes(src_w, 1) == 0) server_kill_window(src_w, 1); else diff --git a/cmd-swap-pane.c b/cmd-swap-pane.c index da7ea3706..b2e53f315 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-swap-pane.c,v 1.53 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: cmd-swap-pane.c,v 1.54 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -68,11 +68,14 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) struct window_pane *tmp_wp, *src_wp, *dst_wp; struct layout_cell *src_lc, *dst_lc; u_int sx, sy, xoff, yoff; + int src_idx, dst_idx; dst_w = target->wl->window; dst_wp = target->wp; + dst_idx = target->wl->idx; src_w = source->wl->window; src_wp = source->wp; + src_idx = source->wl->idx; if (window_push_zoom(dst_w, 0, args_has(args, 'Z'))) server_redraw_window(dst_w); @@ -180,6 +183,10 @@ cmd_swap_pane_exec(struct cmd *self, struct cmdq_item *item) redraw_invalidate_scene(dst_w); server_redraw_window(dst_w); + if (src_w != dst_w) { + window_fire_pane_moved(src_wp, src_w, src_idx, dst_w, dst_idx); + window_fire_pane_moved(dst_wp, dst_w, dst_idx, src_w, src_idx); + } events_fire_window("window-layout-changed", src_w); if (src_w != dst_w) events_fire_window("window-layout-changed", dst_w); diff --git a/input.c b/input.c index fe7297dae..46e7b91aa 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.266 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.267 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1316,8 +1316,10 @@ input_c0_dispatch(struct input_ctx *ictx) case '\000': /* NUL */ break; case '\007': /* BEL */ - if (wp != NULL) + if (wp != NULL) { + events_fire_pane("pane-bell", wp); alerts_queue(wp->window, WINDOW_BELL); + } break; case '\010': /* BS */ screen_write_backspace(sctx); @@ -2180,7 +2182,7 @@ input_csi_dispatch_winops(struct input_ctx *ictx) screen_pop_title(sctx->s); if (wp == NULL) break; - events_fire_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, sctx->s->title); server_redraw_window_borders(w); server_status_window(w); break; diff --git a/options-table.c b/options-table.c index 49cf9730f..3e8dd5b46 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.233 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.234 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1925,6 +1925,8 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("alert-silence", ""), OPTIONS_TABLE_HOOK("client-active", ""), OPTIONS_TABLE_HOOK("client-attached", ""), + OPTIONS_TABLE_HOOK("client-created", ""), + OPTIONS_TABLE_HOOK("client-closed", ""), OPTIONS_TABLE_HOOK("client-detached", ""), OPTIONS_TABLE_HOOK("client-focus-in", ""), OPTIONS_TABLE_HOOK("client-focus-out", ""), @@ -1934,6 +1936,7 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-bell", ""), OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), OPTIONS_TABLE_PANE_HOOK("pane-created", ""), @@ -1944,6 +1947,7 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", ""), OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", ""), OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", ""), + OPTIONS_TABLE_PANE_HOOK("pane-moved", ""), OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", ""), OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), @@ -1952,8 +1956,12 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), OPTIONS_TABLE_HOOK("session-closed", ""), OPTIONS_TABLE_HOOK("session-created", ""), + OPTIONS_TABLE_HOOK("session-added-to-group", ""), OPTIONS_TABLE_HOOK("session-renamed", ""), + OPTIONS_TABLE_HOOK("session-removed-from-group", ""), OPTIONS_TABLE_HOOK("session-window-changed", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-created", ""), + OPTIONS_TABLE_WINDOW_HOOK("window-closed", ""), OPTIONS_TABLE_WINDOW_HOOK("window-layout-changed", ""), OPTIONS_TABLE_HOOK("window-linked", ""), OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", ""), diff --git a/resize.c b/resize.c index c8257cec0..1d83e4b70 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: resize.c,v 1.56 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: resize.c,v 1.57 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -22,9 +22,27 @@ #include "tmux.h" +static void +resize_fire_window_resized(struct window *w, u_int old_sx, u_int old_sy) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_window(&fs, w, 0); + event_payload_set_target(ep, &fs); + event_payload_set_window(ep, "window", w); + event_payload_set_uint(ep, "width", w->sx); + event_payload_set_uint(ep, "height", w->sy); + event_payload_set_uint(ep, "old_width", old_sx); + event_payload_set_uint(ep, "old_height", old_sy); + events_fire("window-resized", ep); +} + void resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) { + u_int old_sx = w->sx, old_sy = w->sy; int zoomed; /* Check size limits. */ @@ -61,7 +79,7 @@ resize_window(struct window *w, u_int sx, u_int sy, int xpixel, int ypixel) tty_update_window_offset(w); server_redraw_window(w); events_fire_window("window-layout-changed", w); - events_fire_window("window-resized", w); + resize_fire_window_resized(w, old_sx, old_sy); w->flags &= ~WINDOW_RESIZE; } diff --git a/server-client.c b/server-client.c index 86360de20..971b8d1aa 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.486 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.487 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -407,6 +407,62 @@ server_client_attached_lost(struct client *c) } } +/* Fire client session changed. */ +static void +server_client_fire_session_changed(struct client *c, struct session *old) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_client(&fs, c, 0); + event_payload_set_target(ep, &fs); + event_payload_set_client(ep, "client", c); + if (fs.s != NULL) { + event_payload_set_session(ep, "session", fs.s); + event_payload_set_session(ep, "new_session", fs.s); + } + if (old != NULL) + event_payload_set_session(ep, "old_session", old); + if (fs.w != NULL) + event_payload_set_window(ep, "window", fs.w); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + else if (fs.idx != -1) + event_payload_set_int(ep, "window_index", fs.idx); + if (fs.wp != NULL) + event_payload_set_pane(ep, "pane", fs.wp); + events_fire("client-session-changed", ep); +} + +/* Fire client resized. */ +static void +server_client_fire_resized(struct client *c, u_int old_sx, u_int old_sy) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_client(&fs, c, 0); + event_payload_set_target(ep, &fs); + event_payload_set_client(ep, "client", c); + if (fs.s != NULL) + event_payload_set_session(ep, "session", fs.s); + if (fs.w != NULL) + event_payload_set_window(ep, "window", fs.w); + if (fs.wl != NULL) + event_payload_set_int(ep, "window_index", fs.wl->idx); + else if (fs.idx != -1) + event_payload_set_int(ep, "window_index", fs.idx); + if (fs.wp != NULL) + event_payload_set_pane(ep, "pane", fs.wp); + event_payload_set_uint(ep, "width", c->tty.sx); + event_payload_set_uint(ep, "height", c->tty.sy); + event_payload_set_uint(ep, "old_width", old_sx); + event_payload_set_uint(ep, "old_height", old_sy); + events_fire("client-resized", ep); +} + /* Set client session. */ void server_client_set_session(struct client *c, struct session *s) @@ -433,7 +489,7 @@ server_client_set_session(struct client *c, struct session *s) alerts_check_session(s); tty_update_client_offset(c); status_timer_start(c); - events_fire_client("client-session-changed", c); + server_client_fire_session_changed(c, old); server_redraw_client(c); } @@ -470,6 +526,8 @@ server_client_lost(struct client *c) server_client_attached_lost(c); events_fire_client("client-detached", c); } + if (c->name != NULL && (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL))) + events_fire_client("client-closed", c); if (c->flags & CLIENT_CONTROL) control_stop(c); @@ -2425,6 +2483,7 @@ server_client_dispatch(struct imsg *imsg, void *arg) struct client *c = arg; ssize_t datalen; struct session *s; + u_int old_sx, old_sy; if (c->flags & CLIENT_DEAD) return; @@ -2463,6 +2522,8 @@ server_client_dispatch(struct imsg *imsg, void *arg) if (c->flags & CLIENT_CONTROL) break; server_client_update_latest(c); + old_sx = c->tty.sx; + old_sy = c->tty.sy; tty_resize(&c->tty); tty_repeat_requests(&c->tty, 0); recalculate_sizes(); @@ -2472,7 +2533,7 @@ server_client_dispatch(struct imsg *imsg, void *arg) c->overlay_resize(c, c->overlay_data); server_redraw_client(c); if (c->session != NULL) - events_fire_client("client-resized", c); + server_client_fire_resized(c, old_sx, old_sy); break; case MSG_EXITING: if (datalen != 0) @@ -2774,6 +2835,8 @@ server_client_dispatch_identify(struct client *c, struct imsg *imsg) close(c->out_fd); c->out_fd = -1; } + if (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL)) + events_fire_client("client-created", c); /* If pasting has taken too long, turn it off. */ if (c->flags & (CLIENT_BRACKETPASTING|CLIENT_ASSUMEPASTING) && diff --git a/session.c b/session.c index 9c23c4b50..f51df3bbd 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD: session.c,v 1.105 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: session.c,v 1.106 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -473,6 +473,29 @@ session_last(struct session *s) return (session_set_current(s, wl)); } +/* Fire session window changed. */ +static void +session_fire_window_changed(struct session *s, struct winlink *wl, + struct winlink *old) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_winlink(&fs, wl, 0); + event_payload_set_target(ep, &fs); + event_payload_set_session(ep, "session", s); + event_payload_set_window(ep, "window", wl->window); + event_payload_set_window(ep, "new_window", wl->window); + event_payload_set_int(ep, "window_index", wl->idx); + event_payload_set_int(ep, "new_window_index", wl->idx); + if (old != NULL) { + event_payload_set_window(ep, "old_window", old->window); + event_payload_set_int(ep, "old_window_index", old->idx); + } + events_fire("session-window-changed", ep); +} + /* Set current winlink to wl .*/ int session_set_current(struct session *s, struct winlink *wl) @@ -495,7 +518,7 @@ session_set_current(struct session *s, struct winlink *wl) winlink_clear_flags(wl); window_update_activity(wl->window); tty_update_window_offset(wl->window); - events_fire_session("session-window-changed", s); + session_fire_window_changed(s, wl, old); return (0); } @@ -542,12 +565,33 @@ session_group_new(const char *name) return (sg); } +/* Fire session group changed. */ +static void +session_group_fire(const char *name, struct session_group *sg, + struct session *s) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + if (session_alive(s)) { + cmd_find_from_session(&fs, s, 0); + event_payload_set_target(ep, &fs); + } + event_payload_set_session(ep, "session", s); + event_payload_set_string(ep, "group", "%s", sg->name); + event_payload_set_uint(ep, "group_size", session_group_count(sg)); + events_fire(name, ep); +} + /* Add a session to a session group. */ void session_group_add(struct session_group *sg, struct session *s) { - if (session_group_contains(s) == NULL) + if (session_group_contains(s) == NULL) { TAILQ_INSERT_TAIL(&sg->sessions, s, gentry); + session_group_fire("session-added-to-group", sg, s); + } } /* Remove a session from its group and destroy the group if empty. */ @@ -558,6 +602,7 @@ session_group_remove(struct session *s) if ((sg = session_group_contains(s)) == NULL) return; + session_group_fire("session-removed-from-group", sg, s); TAILQ_REMOVE(&sg->sessions, s, gentry); if (TAILQ_EMPTY(&sg->sessions)) { RB_REMOVE(session_groups, &session_groups, sg); diff --git a/spawn.c b/spawn.c index 21fc25937..5d020b297 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD: spawn.c,v 1.47 2026/07/10 13:38:45 nicm Exp $ */ +/* $OpenBSD: spawn.c,v 1.48 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -231,8 +231,10 @@ spawn_window(struct spawn_context *sc, char **cause) session_select(s, sc->wl->idx); /* Fire notification if new window. */ - if (~sc->flags & SPAWN_RESPAWN) + if (~sc->flags & SPAWN_RESPAWN) { + events_fire_window("window-created", w); events_fire_winlink("window-linked", sc->wl); + } session_group_synchronize_from(s); return (sc->wl); diff --git a/tmux.1 b/tmux.1 index 01d654a25..3b4b228f6 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1133 2026/07/13 10:03:27 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1134 2026/07/13 13:01:14 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -6356,6 +6356,10 @@ See Run when a client becomes the latest active client of its session. .It client\-attached Run when a client is attached. +.It client\-closed +Run when a client is closed. +.It client\-created +Run when a client is created. .It client\-detached Run when a client is detached. .It client\-focus\-in @@ -6374,6 +6378,8 @@ Run when a client switches to a dark theme. Run when a command fails. .It marked\-pane\-changed Run when the marked pane is set or cleared. +.It pane\-bell +Run when a pane receives a bell. .It pane\-command\-finished Run when an OSC 133 command finishes in a pane. The event payload includes @@ -6407,6 +6413,8 @@ Run when a pane changes mode. Run when a pane enters a mode. .It pane\-mode\-exited Run when a pane exits a mode. +.It pane\-moved +Run when a pane is moved to another window. .It pane\-prompt\-closed Run when a prompt in a pane is closed. .It pane\-prompt\-opened @@ -6425,10 +6433,18 @@ Run when a pane title is changed. Run when a session is closed. .It session\-created Run when a new session is created. +.It session\-added\-to\-group +Run when a session is added to a session group. +.It session\-removed\-from\-group +Run when a session is removed from a session group. .It session\-renamed Run when a session is renamed. .It session\-window\-changed Run when a session changes its active window. +.It window\-closed +Run when a window is closed. +.It window\-created +Run when a window is created. .It window\-layout\-changed Run when a window layout is changed. .It window\-linked diff --git a/tmux.h b/tmux.h index ea357d08c..db6d4cf4e 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1398 2026/07/13 10:29:17 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1399 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -3624,6 +3624,8 @@ int window_has_floating_panes(struct window *); int window_has_pane(struct window *, struct window_pane *); int window_set_active_pane(struct window *, struct window_pane *, int); +void window_fire_pane_moved(struct window_pane *, struct window *, + int, struct window *, int); void window_update_focus(struct window *); void window_pane_update_focus(struct window_pane *); void window_redraw_active_switch(struct window *, diff --git a/window.c b/window.c index f6e1a1f23..ca2eca389 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window.c,v 1.360 2026/07/10 15:20:06 nicm Exp $ */ +/* $OpenBSD: window.c,v 1.361 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -130,6 +130,29 @@ window_fire_pane_changed(struct window *w, struct window_pane *wp, events_fire("window-pane-changed", ep); } +void +window_fire_pane_moved(struct window_pane *wp, struct window *old_w, + int old_idx, struct window *new_w, int new_idx) +{ + struct event_payload *ep; + struct cmd_find_state fs; + + ep = event_payload_create(); + cmd_find_from_pane(&fs, wp, 0); + event_payload_set_target(ep, &fs); + event_payload_set_pane(ep, "pane", wp); + event_payload_set_window(ep, "window", new_w); + event_payload_set_window(ep, "old_window", old_w); + event_payload_set_window(ep, "new_window", new_w); + if (old_idx != -1) + event_payload_set_int(ep, "old_window_index", old_idx); + if (new_idx != -1) { + event_payload_set_int(ep, "window_index", new_idx); + event_payload_set_int(ep, "new_window_index", new_idx); + } + events_fire("pane-moved", ep); +} + static void window_fire_pane_mode_changed(const char *name, struct window_pane *wp, const char *previous, const char *current, int entered) @@ -490,6 +513,9 @@ window_add_ref(struct window *w, const char *from) void window_remove_ref(struct window *w, const char *from) { + if (w->references == 1) + events_fire_window("window-closed", w); + w->references--; log_debug("%s: @%u %s, now %d", __func__, w->id, from, w->references); @@ -972,7 +998,7 @@ window_lost_pane(struct window *w, struct window_pane *wp) if (w->active != NULL) { window_pane_stack_remove(&w->last_panes, w->active); w->active->flags |= PANE_CHANGED; - events_fire_window("window-pane-changed", w); + window_fire_pane_changed(w, w->active, wp); window_update_focus(w); } } From 4cd39ee33e9d80f41d06ffb6801df893a2fddfb0 Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 17:10:04 +0100 Subject: [PATCH 33/47] new-pane mouse test. --- regress/new-pane-mouse.sh | 81 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 regress/new-pane-mouse.sh diff --git a/regress/new-pane-mouse.sh b/regress/new-pane-mouse.sh new file mode 100644 index 000000000..d2ae9ee97 --- /dev/null +++ b/regress/new-pane-mouse.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +# Test new-pane -M creates a floating pane from a mouse drag. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null" + +cleanup() +{ + $TMUX kill-server >/dev/null 2>&1 + $TMUX2 kill-server >/dev/null 2>&1 +} +fail() +{ + echo "$*" >&2 + cleanup + exit 1 +} +must_equal() +{ + got=$1 + want=$2 + [ "$got" = "$want" ] || fail "got '$got', expected '$want'" +} + +# drag STARTCOL STARTROW ENDCOL ENDROW +# +# Write an SGR Ctrl-mouse press, drag update and release at 1-based positions +# to the outer pane holding the inner client. +drag() +{ + scol="$1" + srow="$2" + ecol="$3" + erow="$4" + + seq=$(printf '\033[<16;%s;%sM' "$scol" "$srow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<48;%s;%sM' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 0.2 + seq=$(printf '\033[<16;%s;%sm' "$ecol" "$erow") + $TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null + sleep 1 +} + +cleanup + +$TMUX new-session -d -s inner -x 80 -y 24 || exit 1 +$TMUX set -g mouse on +$TMUX set -g default-command 'sleep 100' + +$TMUX2 new-session -d -x 80 -y 24 "$TMUX attach -t inner" || exit 1 +sleep 1 +OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1) +[ -n "$OUTER" ] || fail "No outer pane." +BASE=$($TMUX list-panes -F '#{pane_id}' | head -1) +[ -n "$BASE" ] || fail "No base pane." + +# Drag from SGR column 3,row 1 to column 10,row 5. These are window positions +# 2,0 to 9,4. With the default border, the floating pane content is inset by +# one cell and has size 6x3. +drag 3 1 10 5 + +id=$($TMUX list-panes -F '#{?pane_floating_flag,#{pane_id},}' | tail -n 1) +[ -n "$id" ] || fail "no floating pane created" + +must_equal "$($TMUX display-message -p -t "$id" '#{pane_left}')" 3 +must_equal "$($TMUX display-message -p -t "$id" '#{pane_top}')" 1 +must_equal "$($TMUX display-message -p -t "$id" '#{pane_width}')" 6 +must_equal "$($TMUX display-message -p -t "$id" '#{pane_height}')" 3 + +$TMUX new-pane -d -M -L -t "$BASE" 'sleep 100' || fail "new-pane -M -L failed" + +cleanup +exit 0 From b91086a5c80fd854f28f47d93aff5d85a886a7ff Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 15:03:03 +0000 Subject: [PATCH 34/47] Add pane-activity event. --- input.c | 6 +++++- options-table.c | 3 ++- server-client.c | 5 +++-- tmux.1 | 11 +++-------- tmux.h | 3 ++- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/input.c b/input.c index 46e7b91aa..66f65e01c 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD: input.c,v 1.267 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: input.c,v 1.268 2026/07/13 15:03:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1051,6 +1051,10 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len) return; window_update_activity(wp->window); + if (~wp->flags & PANE_ACTIVITY) { + wp->flags |= PANE_ACTIVITY; + events_fire_pane("pane-activity", wp); + } wp->flags |= PANE_CHANGED; /* Flag new input while in a mode. */ diff --git a/options-table.c b/options-table.c index 3e8dd5b46..a36901dcb 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.234 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.235 2026/07/13 15:03:03 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -1936,6 +1936,7 @@ const struct options_table_entry options_table[] = { OPTIONS_TABLE_HOOK("client-dark-theme", ""), OPTIONS_TABLE_HOOK("command-error", ""), OPTIONS_TABLE_HOOK("marked-pane-changed", ""), + OPTIONS_TABLE_PANE_HOOK("pane-activity", ""), OPTIONS_TABLE_PANE_HOOK("pane-bell", ""), OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), diff --git a/server-client.c b/server-client.c index 971b8d1aa..f28c80f85 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.487 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.488 2026/07/13 15:03:03 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1812,7 +1812,8 @@ server_client_loop(void) server_client_check_pane_resize(wp); server_client_check_pane_buffer(wp); } - wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR); + wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR| + PANE_ACTIVITY); } check_window_name(w); } diff --git a/tmux.1 b/tmux.1 index 3b4b228f6..cddeaac3b 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1134 2026/07/13 13:01:14 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1135 2026/07/13 15:03:03 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -6378,17 +6378,12 @@ Run when a client switches to a dark theme. Run when a command fails. .It marked\-pane\-changed Run when the marked pane is set or cleared. +.It pane\-activity +Run when there is new output in a pane. .It pane\-bell Run when a pane receives a bell. .It pane\-command\-finished Run when an OSC 133 command finishes in a pane. -The event payload includes -.Ql hook_command_start_time , -.Ql hook_command_end_time , -.Ql hook_command_duration -and -.Ql hook_command_status -if present. .It pane\-command\-started Run when an OSC 133 command starts in a pane. .It pane\-created diff --git a/tmux.h b/tmux.h index db6d4cf4e..d954d6388 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1399 2026/07/13 13:01:14 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1400 2026/07/13 15:03:03 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1268,6 +1268,7 @@ struct window_pane { #define PANE_REDRAWSCROLLBAR 0x8000 #define PANE_DESTROYED 0x10000 #define PANE_CMDRUNNING 0x20000 +#define PANE_ACTIVITY 0x40000 bitstr_t *sync_dirty; u_int sync_dirty_size; From 658e087f7aa426b122fa256b0199428ac499c80e Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 16:07:47 +0000 Subject: [PATCH 35/47] Add new-pane -M to create a new pane from a mouse binding and bind to C-MouseDrag1Pane, removing the old ctrl-click-to-swap-pane binding. --- cmd-split-window.c | 90 ++++++++++++++++++++++++++++++++++++++++++++-- key-bindings.c | 4 +-- server-client.c | 6 +++- tmux.1 | 9 +++-- tmux.h | 4 ++- 5 files changed, 104 insertions(+), 9 deletions(-) diff --git a/cmd-split-window.c b/cmd-split-window.c index 22c346c8b..a36f05603 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD: cmd-split-window.c,v 1.142 2026/07/13 10:03:27 nicm Exp $ */ +/* $OpenBSD: cmd-split-window.c,v 1.143 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -34,13 +34,15 @@ #define SPLIT_WINDOW_TEMPLATE "#{session_name}:#{window_index}.#{pane_index}" static enum cmd_retval cmd_split_window_exec(struct cmd *, struct cmdq_item *); +static void cmd_split_window_mouse_resize(struct client *, + struct mouse_event *); const struct cmd_entry cmd_new_pane_entry = { .name = "new-pane", .alias = "newp", - .args = { "bB:c:de:EfF:hIkl:Lm:p:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, - .usage = "[-bdefhIklPvWZ] [-B border-lines] " + .args = { "bB:c:de:EfF:hIkl:LMm:p:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, + .usage = "[-bdefhIklMPvWZ] [-B border-lines] " "[-c start-directory] [-e environment] " "[-F format] [-l size] [-m message] [-p percentage] " "[-s style] [-S active-border-style] " @@ -86,6 +88,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) struct layout_cell *lc = NULL; struct event_payload *ep; struct cmd_find_state fs; + struct key_event *event = cmdq_get_event(item); int input, empty, is_floating, flags = 0; const char *template, *style, *value; char *cause = NULL, *cp, *title; @@ -101,6 +104,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) flags |= SPAWN_SPLIT; } + if (args_has(args, 'M') && is_floating) { + if (event == NULL || !event->m.valid || tc == NULL) + return (CMD_RETURN_NORMAL); + } + if (is_floating) flags |= SPAWN_FLOATING; if (args_has(args, 'h')) @@ -254,6 +262,12 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item) } server_redraw_session(s); + if (args_has(args, 'M') && is_floating) { + tc->tty.mouse_last_pane = new_wp->id; + tc->tty.mouse_drag_update = cmd_split_window_mouse_resize; + cmd_split_window_mouse_resize(tc, &event->m); + } + if (args_has(args, 'P')) { if ((template = args_get(args, 'F')) == NULL) template = SPLIT_WINDOW_TEMPLATE; @@ -301,3 +315,73 @@ fail: return (CMD_RETURN_ERROR); } + +static void +cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m) +{ + struct window_pane *wp; + struct window *w; + struct layout_cell *lc; + enum pane_lines lines; + u_int sx, sy; + int x, y, xoff, yoff, border; + + if (c->tty.mouse_last_pane == -1) + return; + wp = window_pane_find_by_id(c->tty.mouse_last_pane); + if (wp == NULL || !window_pane_is_floating(wp)) { + c->tty.mouse_drag_update = NULL; + return; + } + w = wp->window; + lc = wp->layout_cell; + + x = m->x + m->ox; + y = m->y + m->oy; + if (m->statusat == 0 && y >= (int)m->statuslines) + y -= m->statuslines; + else if (m->statusat > 0 && y >= m->statusat) + y = m->statusat - 1; + + lines = window_pane_get_pane_lines(wp); + border = (lines != PANE_LINES_NONE); + + if (x >= (int)c->tty.mouse_drag_x) { + xoff = c->tty.mouse_drag_x + border; + sx = x - c->tty.mouse_drag_x + 1; + } else { + sx = c->tty.mouse_drag_x - x + 1; + xoff = c->tty.mouse_drag_x - sx + 1; + if (border) + xoff++; + } + if (y >= (int)c->tty.mouse_drag_y) { + yoff = c->tty.mouse_drag_y + border; + sy = y - c->tty.mouse_drag_y + 1; + } else { + sy = c->tty.mouse_drag_y - y + 1; + yoff = c->tty.mouse_drag_y - sy + 1; + if (border) + yoff++; + } + + if (border) { + if (sx <= 2) + sx = PANE_MINIMUM; + else + sx -= 2; + if (sy <= 2) + sy = PANE_MINIMUM; + else + sy -= 2; + } + if (sx < PANE_MINIMUM) + sx = PANE_MINIMUM; + if (sy < PANE_MINIMUM) + sy = PANE_MINIMUM; + + layout_set_size(lc, sx, sy, xoff, yoff); + layout_fix_panes(w, NULL); + server_redraw_window(w); + server_redraw_window_borders(w); +} diff --git a/key-bindings.c b/key-bindings.c index 92d417c5b..359a7b5cb 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.186 2026/07/07 12:30:36 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.187 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -496,10 +496,10 @@ key_bindings_init(void) /* Mouse button 1 down on pane. */ "bind -n MouseDown1Pane { select-pane -t=; send -M }", - "bind -n C-MouseDown1Pane { swap-pane -s@ }", /* Mouse button 1 drag on pane. */ "bind -n MouseDrag1Pane { if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -M } }", + "bind -n C-MouseDrag1Pane { new-pane -M }", "bind -n M-MouseDrag1Pane { move-pane -M }", /* Mouse wheel up on pane. */ diff --git a/server-client.c b/server-client.c index f28c80f85..1e85f0688 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD: server-client.c,v 1.488 2026/07/13 15:03:03 nicm Exp $ */ +/* $OpenBSD: server-client.c,v 1.489 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -1173,6 +1173,10 @@ have_event: * the scrollbar, store the relative position in the slider * where the user grabbed. */ + if (c->tty.mouse_drag_flag == 0) { + c->tty.mouse_drag_x = px; + c->tty.mouse_drag_y = py; + } c->tty.mouse_drag_flag = MOUSE_BUTTONS(b) + 1; /* Only change pane if not already dragging a pane border. */ diff --git a/tmux.1 b/tmux.1 index cddeaac3b..029f1ee26 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1135 2026/07/13 15:03:03 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1136 2026/07/13 16:07:47 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -3614,7 +3614,7 @@ but a different format may be specified with .Fl F . .Tg newp .It Xo Ic new\-pane -.Op Fl bdefhIkLPvWZ +.Op Fl bdefhIkLMPvWZ .Op Fl B Ar border\-lines .Op Fl c Ar start\-directory .Op Fl e Ar environment @@ -3660,6 +3660,11 @@ to specify a percentage of the window size. .Fl B sets the pane border lines for floating panes; see .Ic pane\-border\-lines . +.Fl M +may be used when bound to a mouse drag key; the new floating pane is resized to +follow the mouse drag. +It has no effect with +.Fl L . .Pp The .Fl L diff --git a/tmux.h b/tmux.h index d954d6388..8e548be3c 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD: tmux.h,v 1.1400 2026/07/13 15:03:03 nicm Exp $ */ +/* $OpenBSD: tmux.h,v 1.1401 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -1743,6 +1743,8 @@ struct tty { u_int mouse_last_y; u_int mouse_last_b; int mouse_drag_flag; + u_int mouse_drag_x; + u_int mouse_drag_y; int mouse_scrolling_flag; int mouse_slider_mpos; int mouse_last_pane; From 2dc05ae3286dd1bf018696c33d2e8df0d7f0264b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 19:52:27 +0100 Subject: [PATCH 36/47] Add to alerts tests. --- regress/alerts.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/regress/alerts.sh b/regress/alerts.sh index cfb84c711..2b9e4f495 100644 --- a/regress/alerts.sh +++ b/regress/alerts.sh @@ -254,6 +254,19 @@ assert_unchanged @log '' $TMUX set -wut mon:w0 monitor-activity || fail "unset monitor-activity w0 failed" +# Creating a non-current window counts as activity when monitor-activity is +# enabled in the window defaults. +$TMUX set -wg monitor-activity on || fail "set global monitor-activity failed" +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX neww -d -t mon: -n newact cat || fail "new-window newact failed" +wait_for @log '|alert-activity:mon:newact' +wait_for_fmt mon:newact '#{window_activity_flag}' 1 +flags_have mon:newact '#' +$TMUX set -wug monitor-activity || + fail "unset global monitor-activity failed" +$TMUX selectw -t mon:newact || fail "select-window newact failed" +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" + # monitor-silence: a quiet window fires alert-silence after the interval # and sets the silence flag, shown as ~ in window_flags. The hook fires # only once while the flag remains set even though the timer keeps @@ -273,6 +286,22 @@ wait_for_fmt mon:silw '#{window_silence_flag}' 0 flags_lack mon:silw '~' $TMUX selectw -t mon:w0 || fail "select-window w0 failed" +# Activity resets the silence timer: output before the original deadline +# prevents an alert until a full quiet interval has passed afterwards. +$TMUX neww -d -t mon: -n silreset cat || fail "new-window silreset failed" +$TMUX set -g @log '' || fail "reset @log failed" +$TMUX set -wt mon:silreset monitor-silence 2 || + fail "set monitor-silence silreset failed" +sleep 1 +activity mon:silreset +assert_unchanged @log '' +wait_for @log '|alert-silence:mon:silreset' +wait_for_fmt mon:silreset '#{window_silence_flag}' 1 +$TMUX set -wt mon:silreset monitor-silence 0 || + fail "reset monitor-silence silreset failed" +$TMUX selectw -t mon:silreset || fail "select-window silreset failed" +$TMUX selectw -t mon:w0 || fail "select-window w0 failed" + # A window linked into two sessions: the hook fires once per winlink and # the flag is set in both sessions. Selecting the window in one session # clears the flags on every winlink. From 8fbbde703595c55506bd7ce8f8eb6e667414c53b Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 16:30:28 +0000 Subject: [PATCH 37/47] Add keys and menu items for copy mode line numbers, and a menu item for auto refresh. --- key-bindings.c | 6 +++- tmux.1 | 17 ++++++++++- window-copy.c | 82 +++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 96 insertions(+), 9 deletions(-) diff --git a/key-bindings.c b/key-bindings.c index 359a7b5cb..f90307fc1 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD: key-bindings.c,v 1.187 2026/07/13 16:07:47 nicm Exp $ */ +/* $OpenBSD: key-bindings.c,v 1.188 2026/07/13 16:30:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,6 +49,9 @@ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Top,}' '<' {send -X history-top}" \ " '#{?#{m/r:(copy|view)-mode,#{pane_mode}},Go To Bottom,}' '>' {send -X history-bottom}" \ " ''" \ + " '#{?#{==:#{pane_mode},copy-mode},#{?copy_line_numbers,Hide Line Numbers,Show Line Numbers},}' 'L' {send -X line-numbers-toggle}" \ + " '#{?#{==:#{pane_mode},copy-mode},#{?refresh_active,Refresh Off,Refresh On},}' 'r' {send -X refresh-toggle}" \ + " ''" \ " '#{?#{&&:#{buffer_size},#{!:#{pane_in_mode}}},Paste #[underscore]#{=/9/...:buffer_sample},}' 'p' {paste-buffer}" \ " ''" \ " '#{?mouse_word,Search For #[underscore]#{=/9/...:mouse_word},}' 'C-r' {if -F '#{?#{m/r:(copy|view)-mode,#{pane_mode}},0,1}' 'copy-mode -t='; send -Xt= search-backward -- \"#{q:mouse_word}\"}" \ @@ -576,6 +579,7 @@ key_bindings_init(void) "bind -Tcopy-mode , { send -X jump-reverse }", "bind -Tcopy-mode \\; { send -X jump-again }", "bind -Tcopy-mode F { command-prompt -P -1p'(jump backward)' { send -X jump-backward -- '%%' } }", + "bind -Tcopy-mode L { send -X line-numbers-toggle }", "bind -Tcopy-mode N { send -X search-reverse }", "bind -Tcopy-mode P { send -X toggle-position }", "bind -Tcopy-mode R { send -X rectangle-toggle }", diff --git a/tmux.1 b/tmux.1 index 029f1ee26..52e83ed98 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD: tmux.1,v 1.1136 2026/07/13 16:07:47 nicm Exp $ +.\" $OpenBSD: tmux.1,v 1.1137 2026/07/13 16:30:28 nicm Exp $ .\" .\" Copyright (c) 2007 Nicholas Marriott .\" @@ -2158,6 +2158,19 @@ before the target. .Xc Jump to the last mark. .It Xo +.Ic line\-numbers\-on +.Xc +Show line numbers in copy mode. +.It Xo +.Ic line\-numbers\-off +.Xc +Hide line numbers in copy mode. +.It Xo +.Ic line\-numbers\-toggle +(emacs: L) +.Xc +Toggle line numbers in copy mode. +.It Xo .Ic middle\-line (vi: M) (emacs: M\-r) @@ -7175,6 +7188,7 @@ The following variables are available, where appropriate: .It Li "copy_cursor_word" Ta "" Ta "Word under cursor in copy mode" .It Li "copy_cursor_x" Ta "" Ta "Cursor X position in copy mode" .It Li "copy_cursor_y" Ta "" Ta "Cursor Y position in copy mode" +.It Li "copy_line_numbers" Ta "" Ta "1 if line numbers are shown in copy mode" .It Li "copy_position" Ta "" Ta "Position shown in the copy mode indicator" .It Li "copy_position_limit" Ta "" Ta "Limit shown in the copy mode indicator" .It Li "current_file" Ta "" Ta "Current configuration file" @@ -7294,6 +7308,7 @@ The following variables are available, where appropriate: .It Li "prev_window_active" Ta "" Ta "1 if previous window in W: loop is active" .It Li "prev_window_index" Ta "" Ta "Index of previous window in W: loop" .It Li "rectangle_toggle" Ta "" Ta "1 if rectangle selection is activated" +.It Li "refresh_active" Ta "" Ta "1 if copy mode refresh is active" .It Li "scroll_position" Ta "" Ta "Scroll position in copy mode" .It Li "scroll_region_lower" Ta "" Ta "Bottom of scroll region in pane" .It Li "scroll_region_upper" Ta "" Ta "Top of scroll region in pane" diff --git a/window-copy.c b/window-copy.c index 6d0390fe9..2213dcebb 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.415 2026/07/13 12:53:56 jsg Exp $ */ +/* $OpenBSD: window-copy.c,v 1.416 2026/07/13 16:30:28 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -57,6 +57,8 @@ static void window_copy_refresh_arm(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_style_changed(struct window_mode_entry *); +static void window_copy_set_line_numbers1(struct window_mode_entry *, int, + int); static int window_copy_line_number_mode(struct window_mode_entry *); static int window_copy_line_number_is_absolute(struct window_mode_entry *); static int window_copy_line_numbers_active(struct window_mode_entry *); @@ -296,7 +298,7 @@ struct window_copy_mode_data { int rectflag; /* in rectangle copy mode? */ int scroll_exit; /* exit on scroll to end? */ int hide_position; /* hide position marker */ - int line_numbers; + int line_numbers; /* 0 off, 1 from option, 2 default */ enum { SEL_CHAR, /* select one char at a time */ @@ -1117,6 +1119,9 @@ window_copy_formats(struct window_mode_entry *wme, struct format_tree *ft) } format_add(ft, "copy_position", "%u", position); format_add(ft, "copy_position_limit", "%u", limit); + format_add(ft, "copy_line_numbers", "%d", + window_copy_line_numbers_active(wme)); + format_add(ft, "refresh_active", "%d", data->refresh_active); format_add(ft, "rectangle_toggle", "%d", data->rectflag); format_add(ft, "copy_cursor_x", "%d", data->cx); @@ -3150,6 +3155,28 @@ window_copy_cmd_recentre_top_bottom(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_REDRAW); } +static enum window_copy_cmd_action +window_copy_cmd_line_numbers_on(struct window_copy_cmd_state *cs) +{ + window_copy_set_line_numbers1(cs->wme, 1, 1); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_line_numbers_off(struct window_copy_cmd_state *cs) +{ + window_copy_set_line_numbers1(cs->wme, 0, 0); + return (WINDOW_COPY_CMD_NOTHING); +} + +static enum window_copy_cmd_action +window_copy_cmd_line_numbers_toggle(struct window_copy_cmd_state *cs) +{ + window_copy_set_line_numbers1(cs->wme, + !window_copy_line_numbers_active(cs->wme), 1); + return (WINDOW_COPY_CMD_NOTHING); +} + static const struct { const char *command; u_int minargs; @@ -3414,6 +3441,24 @@ static const struct { .clear = WINDOW_COPY_CMD_CLEAR_ALWAYS, .f = window_copy_cmd_jump_to_mark }, + { .command = "line-numbers-on", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_line_numbers_on + }, + { .command = "line-numbers-off", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_line_numbers_off + }, + { .command = "line-numbers-toggle", + .args = { "", 0, 0, NULL }, + .flags = WINDOW_COPY_CMD_FLAG_READONLY, + .clear = WINDOW_COPY_CMD_CLEAR_NEVER, + .f = window_copy_cmd_line_numbers_toggle + }, { .command = "next-prompt", .args = { "o", 0, 0, NULL }, .flags = WINDOW_COPY_CMD_FLAG_READONLY, @@ -5066,10 +5111,14 @@ window_copy_line_number_mode(struct window_mode_entry *wme) struct window_pane *wp = wme->wp; struct window_copy_mode_data *data = wme->data; struct options *oo = wp->window->options; + int mode; if (!data->line_numbers) return (WINDOW_COPY_LINE_NUMBERS_OFF); - return (options_get_number(oo, "copy-mode-line-numbers")); + mode = options_get_number(oo, "copy-mode-line-numbers"); + if (data->line_numbers == 2 && mode == WINDOW_COPY_LINE_NUMBERS_OFF) + return (WINDOW_COPY_LINE_NUMBERS_DEFAULT); + return (mode); } static int @@ -5155,14 +5204,33 @@ void window_copy_set_line_numbers(struct window_pane *wp, int enabled) { struct window_mode_entry *wme = TAILQ_FIRST(&wp->modes); - struct window_copy_mode_data *data; if (wme == NULL || wme->mode != &window_copy_mode) return; - data = wme->data; - if (data == NULL || data->line_numbers == enabled) + window_copy_set_line_numbers1(wme, enabled, 0); +} + +static void +window_copy_set_line_numbers1(struct window_mode_entry *wme, int enabled, + int force) +{ + struct window_copy_mode_data *data = wme->data; + struct options *oo = wme->wp->window->options; + int active, line_numbers; + + if (data == NULL) return; - data->line_numbers = enabled; + active = window_copy_line_numbers_active(wme); + if (!enabled) + line_numbers = 0; + else if (force && options_get_number(oo, "copy-mode-line-numbers") == + WINDOW_COPY_LINE_NUMBERS_OFF) + line_numbers = 2; + else + line_numbers = 1; + if (data->line_numbers == line_numbers && active == enabled) + return; + data->line_numbers = line_numbers; window_copy_redraw_screen(wme, 1); } From c3e38b317d47c667e9132b5f2785cf71cb4b92c4 Mon Sep 17 00:00:00 2001 From: Yayo Razo Date: Mon, 13 Jul 2026 13:03:58 -0600 Subject: [PATCH 38/47] fix(compat): drop fgetln compat shim, migrate prompt-history.c to getline Following up on the comment on #5365 - this drops fgetln from prompt-history.c (its only remaining caller) in favor of getline, and removes the fgetln compat shim entirely since nothing references it anymore: the compat.h declaration, compat/fgetln.c itself, and its entry in configure.ac's AC_REPLACE_FUNCS list. fgetln doesn't null-terminate its returned buffer when the final line in the file lacks a trailing newline, so the old loop had a second branch to heap-copy and manually terminate that case. getline always null-terminates regardless, so that branch goes away and the loop becomes uniform. Verified with a real build (ubuntu-24.04 container, same --enable-utf8proc --enable-asan configure CI uses): crafted a history file with a normal entry, a blank line, and a final entry with no trailing newline at EOF (the exact case the removed branch existed for), loaded it through a real ASan-built tmux via show-prompt-history, all three entries came back correctly. Full regress suite is otherwise clean against this change. --- compat.h | 5 ---- compat/fgetln.c | 61 ------------------------------------------------ configure.ac | 1 - prompt-history.c | 27 +++++++-------------- 4 files changed, 9 insertions(+), 85 deletions(-) delete mode 100644 compat/fgetln.c diff --git a/compat.h b/compat.h index 8d71a9cf1..66a22640b 100644 --- a/compat.h +++ b/compat.h @@ -410,11 +410,6 @@ int asprintf(char **, const char *, ...); int vasprintf(char **, const char *, va_list); #endif -#ifndef HAVE_FGETLN -/* fgetln.c */ -char *fgetln(FILE *, size_t *); -#endif - #ifndef HAVE_GETLINE /* getline.c */ ssize_t getline(char **, size_t *, FILE *); diff --git a/compat/fgetln.c b/compat/fgetln.c deleted file mode 100644 index c71918cbb..000000000 --- a/compat/fgetln.c +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2015 Joerg Jung - * - * Permission to use, copy, modify, and distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -/* - * portable fgetln() version, NOT reentrant - */ - -#include -#include -#include - -#include "compat.h" - -char * -fgetln(FILE *fp, size_t *len) -{ - static char *buf = NULL; - static size_t bufsz = 0; - size_t r = 0; - char *p; - int c, e; - - if (!fp || !len) { - errno = EINVAL; - return NULL; - } - if (!buf) { - if (!(buf = calloc(1, BUFSIZ))) - return NULL; - bufsz = BUFSIZ; - } - while ((c = getc(fp)) != EOF) { - buf[r++] = c; - if (r == bufsz) { - if (!(p = reallocarray(buf, 2, bufsz))) { - e = errno; - free(buf); - errno = e; - buf = NULL, bufsz = 0; - return NULL; - } - buf = p, bufsz = 2 * bufsz; - } - if (c == '\n') - break; - } - return (*len = r) ? buf : NULL; -} diff --git a/configure.ac b/configure.ac index 6abb2d9d5..f65563c3e 100644 --- a/configure.ac +++ b/configure.ac @@ -177,7 +177,6 @@ AC_REPLACE_FUNCS([ \ clock_gettime \ closefrom \ explicit_bzero \ - fgetln \ freezero \ getdtablecount \ getdtablesize \ diff --git a/prompt-history.c b/prompt-history.c index ac99bdb95..4453a53a8 100644 --- a/prompt-history.c +++ b/prompt-history.c @@ -79,8 +79,9 @@ void prompt_load_history(void) { FILE *f; - char *history_file, *line, *tmp; - size_t length; + char *history_file, *line = NULL; + size_t length = 0; + ssize_t got; if ((history_file = prompt_find_history_file()) == NULL) return; @@ -94,23 +95,13 @@ prompt_load_history(void) } free(history_file); - for (;;) { - if ((line = fgetln(f, &length)) == NULL) - break; - - if (length > 0) { - if (line[length - 1] == '\n') { - line[length - 1] = '\0'; - prompt_add_typed_history(line); - } else { - tmp = xmalloc(length + 1); - memcpy(tmp, line, length); - tmp[length] = '\0'; - prompt_add_typed_history(tmp); - free(tmp); - } - } + while ((got = getline(&line, &length, f)) != -1) { + if (got > 0 && line[got - 1] == '\n') + line[got - 1] = '\0'; + if (got > 0) + prompt_add_typed_history(line); } + free(line); fclose(f); } From e4dbd11d44f13122719d598c5b091758dda42bc3 Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 20:27:02 +0100 Subject: [PATCH 39/47] doc: rewrite SYNCING and reformat to markdown Rewrite the SYNCING document to reflect that everything runs on Github. Add examples on how to deal with merge conflicts. Also reformat to markdown so any examples are more obvious in different renderers. --- README.ja | 2 +- SYNCING | 173 ------------------------------------------------------ 2 files changed, 1 insertion(+), 174 deletions(-) delete mode 100644 SYNCING diff --git a/README.ja b/README.ja index aeb6704ae..3d3d0b43c 100644 --- a/README.ja +++ b/README.ja @@ -56,7 +56,7 @@ gitコミットについての連絡先 tmux-users@googlegroups.com -本ファイル、CHANGES、 FAQ、SYNCINGそしてTODOはISC licenseで保護されています。 +本ファイル、CHANGES、 FAQ、SYNCING.mdそしてTODOはISC licenseで保護されています。 その他のファイルのライセンスや著作権については、ファイルの上部に明記されています。 -- Nicholas Marriott diff --git a/SYNCING b/SYNCING deleted file mode 100644 index 51b780100..000000000 --- a/SYNCING +++ /dev/null @@ -1,173 +0,0 @@ -Preamble -======== - -Tmux portable relies on repositories "tmux" and "tmux-obsd". -Here's a description of them: - -* "tmux" is the portable version, the one which contains code for other - operating systems, and autotools, etc., which isn't found or needed in the - OpenBSD base system. - -* "tmux-obsd" is the version of tmux in OpenBSD base system which provides - the basis of the portable tmux version. - -Note: The "tmux-obsd" repository is actually handled by "git cvsimport" -running at 15 minute intervals, so a commit made to OpenBSD's tmux CVS -repository will take at least that long to appear in this git repository. -(It might take longer, depending on the CVS mirror used to import the -OpenBSD code). - -If you've never used git before, git tracks meta-data about the committer -and the author, as part of a commit, hence: - -% git config [--global] user.name "Your name" -% git config [--global] user.email "you@yourdomain.com" - -Note that, if you already have this in the global ~/.gitconfig option, then -this will be used. Setting this per-repository would involve not using the -"--global" flag above. If you wish to use the same credentials always, -pass the "--global" option, as shown. - -This is a one-off operation once the repository has been cloned, assuming -this information has ever been set before. - -Cloning repositories -==================== - -This involves having both tmux and tmux-obsd cloned, as in: - -% cd /some/where/useful -% git clone https://github.com/tmux/tmux.git -% git clone https://github.com/ThomasAdam/tmux-obsd.git - -Note that you do not need additional checkouts to manage the sync -- an -existing clone of either repositories will suffice. So if you already have -these checkouts existing, skip that. - -Adding in git-remotes -===================== - -Because the portable "tmux" git repository and the "tmux-obsd" -repository do not inherently share any history between each other, the -history has been faked between them. This "faking of history" is something -which has to be told to git for the purposes of comparing the "tmux" and -"tmux-obsd" repositories for syncing. To do this, we must reference the -clone of the "tmux-obsd" repository from the "tmux" repository, as -shown by the following command: - -% cd /path/to/tmux -% git remote add obsd-tmux file:///path/to/tmux-obsd - -So that now, the remote "obsd-tmux" can be used to reference branches and -commits from the "tmux-obsd" repository, but from the context of the -portable "tmux" repository, which makes sense because it's the "tmux" -repository which will have the updates applied to them. - -Fetching updates -================ - -To ensure the latest commits from "tmux-obsd" can be found from within -"tmux", we have to ensure the "master" branch from "tmux-obsd" is -up-to-date first, and then reference that update in "tmux", as in: - -% cd /path/to/tmux-obsd -% git checkout master -% git pull - -Then back in "tmux": - -% cd /path/to/tmux -% git fetch obsd-tmux - -Creating the necessary branches -=============================== - -Now that "tmux" can see commits and branches from "tmux-obsd" by way -of the remote name "obsd-tmux", we can now create the master branch from -"tmux-obsd" in the "tmux" repository: - -% git checkout -b obsd-master obsd-tmux/master - -Adding in the fake history points -================================= - -To tie both the "master" branch from "tmux" and the "obsd-master" -branch from "tmux-obsd" together, the fake history points added to the -"tmux" repository need to be added. To do this, we must add an -additional refspec line, as in: - -% cd /path/to/tmux -% git config --add remote.origin.fetch '+refs/replace/*:refs/replace/*' -% git fetch origin - -Performing the Sync -=================== - -Make sure the "master" branch is checked out: - -% git checkout master - -The following will show commits on OpenBSD not yet synched with "tmux": - -% git log master..obsd-master - -From there, merge the result in, fixing up any conflicts which might arise. - -% git merge obsd-master - -Then ensure things look correct by BUILDING the result of that sync: - -% make clean && ./autogen.sh && ./configure && make - -Compare the git merge result with what's on origin/master -- that is, check -which commits you're about to push: - -% git log origin/master..master - -And if happy: - -% git push origin master - -Keeping an eye on libutil in OpenBSD -==================================== - -A lot of the compat/ code in tmux comes from libutil, especially imsg. -Sometimes the API can change, etc., which might cause interesting problems -trying to run the portable version of tmux. It's worth checking -periodically for any changes to libutil in OpenBSD and syncing those files -to compat/ as and when appropriate. - -Release tmux for next version -============================= - -1. Update and commit README and CHANGES. The former should be checked for - anything outdated and updated with a list of things that might break - upgrades and the latter should mention all the major changes since - the last version. - -2. Make sure configure.ac has the new version number. - -3. Tag with: - - % git tag -a 2.X - - Where "2.X" is the next version. - - Push the tag out with: - - % git push --tags - -4. Build the tarball with 'make dist'. - -5. Check the tarball. If it's good, go here to select the tag just pushed: - - https://github.com/tmux/tmux/tags - - Click the "Add release notes", upload the tarball and add a link in the - description field to the CHANGES file. - -6. Clone the tmux.github.io repository, and change the RELEASE version in the - Makefile. Commit it, and run 'make' to replace %%RELEASE%%. Push the - result out. - -7. Change version back to master in configure.ac. From 9c9477ae76ebb8f7ab4f1058e434d5e21fa3481c Mon Sep 17 00:00:00 2001 From: Thomas Adam Date: Mon, 13 Jul 2026 20:30:47 +0100 Subject: [PATCH 40/47] Tweak previous; include SYNCING.md --- SYNCING.md | 234 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 SYNCING.md diff --git a/SYNCING.md b/SYNCING.md new file mode 100644 index 000000000..9b710a463 --- /dev/null +++ b/SYNCING.md @@ -0,0 +1,234 @@ +# Preamble + +tmux portable is maintained from two repositories: + +* `tmux` is the portable repository. It contains the portability layer, + autotools build files, regression tests, documentation, and code needed for + platforms outside OpenBSD. +* `tmux-openbsd-cutover` is the OpenBSD tmux import repository. Its `master` + branch is the OpenBSD tmux history after the OpenBSD Git cutover. Portable + merges this branch when importing OpenBSD changes. + +To create the commits in the tmux-openbsd-cutover repo, the source repo is the +Github mirror of OpenBSD's src repo: + +```text +https://github.com/openbsd/src.git +``` + +The update automation filters the OpenBSD source tree to `usr.bin/tmux/`, +publishes that filtered history as `openbsd-git` in the cutover repository, +cherry-picks new filtered commits onto cutover `master`, then merges cutover +`master` into portable `master`. + +If you've never used git before, configure your identity before committing: + +```sh +git config [--global] user.name "Your name" +git config [--global] user.email "you@yourdomain.com" +``` + +# Repository layout + +The usual local layout is: + +```sh +cd /some/where/useful +git clone https://github.com/tmux/tmux.git tmux-portable +git clone https://github.com/ThomasAdam/tmux-obsd.git tmux-openbsd-cutover +``` + +The exact directory names do not matter, but the examples below use: + +```text +/path/to/tmux-portable +/path/to/tmux-openbsd-cutover +``` + +The cutover repository has three important branches: + +* `master` is the branch portable consumes. It should contain tmux source + only, not automation files. +* `openbsd-git` is the raw filtered OpenBSD tmux branch generated from + OpenBSD src `master`. +* `automation` is an orphan branch containing the GitHub Actions workflow. It + is separate so that workflow files are not merged into portable. + +# Adding the OpenBSD remote to portable + +In the portable repository, add the cutover repository as a remote: + +```sh +cd /path/to/tmux-portable +git remote add tmux-openbsd /path/to/tmux-openbsd-cutover +git config remote.tmux-openbsd.tagOpt --no-tags +``` + +If the remote already exists, update it instead: + +```sh +git remote set-url tmux-openbsd /path/to/tmux-openbsd-cutover +git config remote.tmux-openbsd.tagOpt --no-tags +``` + +Fetch the cutover master branch explicitly: + +```sh +git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master +``` + +# Automated syncing + +The normal sync is performed by the GitHub Actions workflow in the +`automation` branch of the cutover repository. That workflow: + +1. Fetches or clones OpenBSD src using a blobless clone. +2. Filters `usr.bin/tmux/` into a local `tmux-openbsd` branch. +3. Updates `tmux-openbsd-cutover/openbsd-git`. +4. Cherry-picks new `openbsd-git` commits onto cutover `master`. +5. Merges cutover `master` into portable `master`. +6. Pushes the changed repositories. + +The workflow deliberately runs from the orphan `automation` branch but checks +out cutover `master` as the branch to update. This keeps `.github/` out of +cutover `master`, so portable does not import workflow files when it merges +OpenBSD changes. + +# Manual portable merge + +If the workflow fails while merging into portable, do the merge locally and +push the result. + +Start from an up-to-date portable master: + +```sh +cd /path/to/tmux-portable +git fetch origin +git checkout master +git pull --ff-only origin master +``` + +Fetch the cutover branch: + +```sh +git remote add tmux-openbsd /path/to/tmux-openbsd-cutover 2>/dev/null || true +git config remote.tmux-openbsd.tagOpt --no-tags +git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master +``` + +Merge it: + +```sh +git merge --no-ff --log refs/remotes/tmux-openbsd/master +``` + +Resolve conflicts by deciding whether portable or OpenBSD owns the file. + +Useful commands: + +```sh +git checkout --ours path/to/file +git add path/to/file +``` + +This keeps the portable version of a conflicted file. + +```sh +git checkout --theirs path/to/file +git add path/to/file +``` + +This takes the OpenBSD/cutover version of a conflicted file. + +Before committing, inspect the result: + +```sh +git status +git diff --check +git diff --cached --stat +``` + +Then finish and publish: + +```sh +git commit +git push origin master +``` + +If the merge attempt is wrong, abort it: + +```sh +git merge --abort +``` + +After pushing a manually resolved merge, rerun the workflow. It should either +no-op or continue from the now-merged portable state. + +# Manual cutover update + +Normally this is handled by the workflow. If it must be done manually, update +`openbsd-git` in the cutover repository from a filtered OpenBSD src checkout, +then cherry-pick the new filtered commits onto cutover `master`. + +The important range is: + +```text +origin/openbsd-git..openbsd-git +``` + +where `origin/openbsd-git` is the previously published filtered OpenBSD tmux +branch and `openbsd-git` is the newly generated filtered branch. + +In the cutover repository: + +```sh +cd /path/to/tmux-openbsd-cutover +git checkout master +git cherry-pick origin/openbsd-git..openbsd-git +git push origin master openbsd-git +``` + +Do not put workflow files on cutover `master`; keep them on the orphan +`automation` branch. + +# Keeping an eye on libutil in OpenBSD + +A lot of the `compat/` code in tmux comes from OpenBSD libraries, especially +imsg. Sometimes APIs change in OpenBSD in ways that require corresponding +portable changes. It is worth checking periodically for relevant OpenBSD +libutil changes and syncing those files into `compat/` as appropriate. + +# Release tmux for next version + +1. Update and commit README and CHANGES. The former should be checked for + anything outdated and updated with a list of things that might break + upgrades and the latter should mention all the major changes since the last + version. +2. Make sure `configure.ac` has the new version number. +3. Tag with: + + ```sh + % git tag -a 2.X + ``` + + Where `2.X` is the next version. + + Push the tag out with: + + ```sh + % git push --tags + ``` + +4. Build the tarball with `make dist`. +5. Check the tarball. If it is good, go here to select the tag just pushed: + + ```text + https://github.com/tmux/tmux/tags + ``` + + Click "Add release notes", upload the tarball and add a link in the + description field to the CHANGES file. +6. Clone the tmux.github.io repository, and change the RELEASE version in the + Makefile. Commit it, and run `make` to replace `%%RELEASE%%`. Push the + result out. +7. Change version back to master in `configure.ac`. From fba5083b5896a8ec209e188017914a5850770113 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 19:32:28 +0000 Subject: [PATCH 41/47] Add hooks to customize mode. --- options-table.c | 255 ++++++++++++++++++++++++++++----------------- window-customize.c | 123 ++++++++++++++++++---- 2 files changed, 263 insertions(+), 115 deletions(-) diff --git a/options-table.c b/options-table.c index a36901dcb..ddbb9404d 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD: options-table.c,v 1.235 2026/07/13 15:03:03 nicm Exp $ */ +/* $OpenBSD: options-table.c,v 1.236 2026/07/13 19:32:28 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -240,33 +240,40 @@ static const char *options_table_status_format_default[] = { }; /* Helpers for hook options. */ -#define OPTIONS_TABLE_HOOK(hook_name, default_value) \ +#define OPTIONS_TABLE_HOOK(hook_name, default_value, hook_text) \ { .name = hook_name, \ .type = OPTIONS_TABLE_COMMAND, \ .scope = OPTIONS_TABLE_SESSION, \ .flags = OPTIONS_TABLE_IS_ARRAY|OPTIONS_TABLE_IS_HOOK, \ .default_str = default_value, \ - .separator = "" \ + .separator = "", \ + .text = hook_text \ } -#define OPTIONS_TABLE_PANE_HOOK(hook_name, default_value) \ +#define OPTIONS_TABLE_PANE_HOOK(hook_name, default_value, hook_text) \ { .name = hook_name, \ .type = OPTIONS_TABLE_COMMAND, \ .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, \ .flags = OPTIONS_TABLE_IS_ARRAY|OPTIONS_TABLE_IS_HOOK, \ .default_str = default_value, \ - .separator = "" \ + .separator = "", \ + .text = hook_text \ } -#define OPTIONS_TABLE_WINDOW_HOOK(hook_name, default_value) \ +#define OPTIONS_TABLE_WINDOW_HOOK(hook_name, default_value, hook_text) \ { .name = hook_name, \ .type = OPTIONS_TABLE_COMMAND, \ .scope = OPTIONS_TABLE_WINDOW, \ .flags = OPTIONS_TABLE_IS_ARRAY|OPTIONS_TABLE_IS_HOOK, \ .default_str = default_value, \ - .separator = "" \ + .separator = "", \ + .text = hook_text \ } +#define OPTIONS_TABLE_AFTER_HOOK(command_name) \ + OPTIONS_TABLE_HOOK("after-" command_name, "", \ + "Run after the " command_name " command completes.") + /* Map of name conversions. */ const struct options_name_map options_other_names[] = { { "display-panes-color", "display-panes-colour" }, @@ -1882,95 +1889,151 @@ const struct options_table_entry options_table[] = { }, /* Hook options. */ - OPTIONS_TABLE_HOOK("after-bind-key", ""), - OPTIONS_TABLE_HOOK("after-capture-pane", ""), - OPTIONS_TABLE_HOOK("after-copy-mode", ""), - OPTIONS_TABLE_HOOK("after-display-message", ""), - OPTIONS_TABLE_HOOK("after-display-panes", ""), - OPTIONS_TABLE_HOOK("after-kill-pane", ""), - OPTIONS_TABLE_HOOK("after-list-buffers", ""), - OPTIONS_TABLE_HOOK("after-list-clients", ""), - OPTIONS_TABLE_HOOK("after-list-keys", ""), - OPTIONS_TABLE_HOOK("after-list-panes", ""), - OPTIONS_TABLE_HOOK("after-list-sessions", ""), - OPTIONS_TABLE_HOOK("after-list-windows", ""), - OPTIONS_TABLE_HOOK("after-load-buffer", ""), - OPTIONS_TABLE_HOOK("after-lock-server", ""), - OPTIONS_TABLE_HOOK("after-new-session", ""), - OPTIONS_TABLE_HOOK("after-new-window", ""), - OPTIONS_TABLE_HOOK("after-paste-buffer", ""), - OPTIONS_TABLE_HOOK("after-pipe-pane", ""), - OPTIONS_TABLE_HOOK("after-queue", ""), - OPTIONS_TABLE_HOOK("after-refresh-client", ""), - OPTIONS_TABLE_HOOK("after-rename-session", ""), - OPTIONS_TABLE_HOOK("after-rename-window", ""), - OPTIONS_TABLE_HOOK("after-resize-pane", ""), - OPTIONS_TABLE_HOOK("after-resize-window", ""), - OPTIONS_TABLE_HOOK("after-save-buffer", ""), - OPTIONS_TABLE_HOOK("after-select-layout", ""), - OPTIONS_TABLE_HOOK("after-select-pane", ""), - OPTIONS_TABLE_HOOK("after-select-window", ""), - OPTIONS_TABLE_HOOK("after-send-keys", ""), - OPTIONS_TABLE_HOOK("after-set-buffer", ""), - OPTIONS_TABLE_HOOK("after-set-environment", ""), - OPTIONS_TABLE_HOOK("after-set-hook", ""), - OPTIONS_TABLE_HOOK("after-set-option", ""), - OPTIONS_TABLE_HOOK("after-show-environment", ""), - OPTIONS_TABLE_HOOK("after-show-messages", ""), - OPTIONS_TABLE_HOOK("after-show-options", ""), - OPTIONS_TABLE_HOOK("after-split-window", ""), - OPTIONS_TABLE_HOOK("after-unbind-key", ""), - OPTIONS_TABLE_HOOK("alert-activity", ""), - OPTIONS_TABLE_HOOK("alert-bell", ""), - OPTIONS_TABLE_HOOK("alert-silence", ""), - OPTIONS_TABLE_HOOK("client-active", ""), - OPTIONS_TABLE_HOOK("client-attached", ""), - OPTIONS_TABLE_HOOK("client-created", ""), - OPTIONS_TABLE_HOOK("client-closed", ""), - OPTIONS_TABLE_HOOK("client-detached", ""), - OPTIONS_TABLE_HOOK("client-focus-in", ""), - OPTIONS_TABLE_HOOK("client-focus-out", ""), - OPTIONS_TABLE_HOOK("client-resized", ""), - OPTIONS_TABLE_HOOK("client-session-changed", ""), - OPTIONS_TABLE_HOOK("client-light-theme", ""), - OPTIONS_TABLE_HOOK("client-dark-theme", ""), - OPTIONS_TABLE_HOOK("command-error", ""), - OPTIONS_TABLE_HOOK("marked-pane-changed", ""), - OPTIONS_TABLE_PANE_HOOK("pane-activity", ""), - OPTIONS_TABLE_PANE_HOOK("pane-bell", ""), - OPTIONS_TABLE_PANE_HOOK("pane-command-finished", ""), - OPTIONS_TABLE_PANE_HOOK("pane-command-started", ""), - OPTIONS_TABLE_PANE_HOOK("pane-created", ""), - OPTIONS_TABLE_PANE_HOOK("pane-died", ""), - OPTIONS_TABLE_PANE_HOOK("pane-exited", ""), - OPTIONS_TABLE_PANE_HOOK("pane-focus-in", ""), - OPTIONS_TABLE_PANE_HOOK("pane-focus-out", ""), - OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", ""), - OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", ""), - OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", ""), - OPTIONS_TABLE_PANE_HOOK("pane-moved", ""), - OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", ""), - OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", ""), - OPTIONS_TABLE_PANE_HOOK("pane-resized", ""), - OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", ""), - OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", ""), - OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), - OPTIONS_TABLE_HOOK("session-closed", ""), - OPTIONS_TABLE_HOOK("session-created", ""), - OPTIONS_TABLE_HOOK("session-added-to-group", ""), - OPTIONS_TABLE_HOOK("session-renamed", ""), - OPTIONS_TABLE_HOOK("session-removed-from-group", ""), - OPTIONS_TABLE_HOOK("session-window-changed", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-created", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-closed", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-layout-changed", ""), - OPTIONS_TABLE_HOOK("window-linked", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-renamed", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-resized", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-unzoomed", ""), - OPTIONS_TABLE_WINDOW_HOOK("window-zoomed", ""), - OPTIONS_TABLE_HOOK("window-unlinked", ""), + OPTIONS_TABLE_AFTER_HOOK("bind-key"), + OPTIONS_TABLE_AFTER_HOOK("capture-pane"), + OPTIONS_TABLE_AFTER_HOOK("copy-mode"), + OPTIONS_TABLE_AFTER_HOOK("display-message"), + OPTIONS_TABLE_AFTER_HOOK("display-panes"), + OPTIONS_TABLE_AFTER_HOOK("kill-pane"), + OPTIONS_TABLE_AFTER_HOOK("list-buffers"), + OPTIONS_TABLE_AFTER_HOOK("list-clients"), + OPTIONS_TABLE_AFTER_HOOK("list-keys"), + OPTIONS_TABLE_AFTER_HOOK("list-panes"), + OPTIONS_TABLE_AFTER_HOOK("list-sessions"), + OPTIONS_TABLE_AFTER_HOOK("list-windows"), + OPTIONS_TABLE_AFTER_HOOK("load-buffer"), + OPTIONS_TABLE_AFTER_HOOK("lock-server"), + OPTIONS_TABLE_AFTER_HOOK("new-session"), + OPTIONS_TABLE_AFTER_HOOK("new-window"), + OPTIONS_TABLE_AFTER_HOOK("paste-buffer"), + OPTIONS_TABLE_AFTER_HOOK("pipe-pane"), + OPTIONS_TABLE_AFTER_HOOK("refresh-client"), + OPTIONS_TABLE_AFTER_HOOK("rename-session"), + OPTIONS_TABLE_AFTER_HOOK("rename-window"), + OPTIONS_TABLE_AFTER_HOOK("resize-pane"), + OPTIONS_TABLE_AFTER_HOOK("resize-window"), + OPTIONS_TABLE_AFTER_HOOK("save-buffer"), + OPTIONS_TABLE_AFTER_HOOK("select-layout"), + OPTIONS_TABLE_AFTER_HOOK("select-pane"), + OPTIONS_TABLE_AFTER_HOOK("select-window"), + OPTIONS_TABLE_AFTER_HOOK("send-keys"), + OPTIONS_TABLE_AFTER_HOOK("set-buffer"), + OPTIONS_TABLE_AFTER_HOOK("set-environment"), + OPTIONS_TABLE_AFTER_HOOK("set-hook"), + OPTIONS_TABLE_AFTER_HOOK("set-option"), + OPTIONS_TABLE_AFTER_HOOK("show-environment"), + OPTIONS_TABLE_AFTER_HOOK("show-messages"), + OPTIONS_TABLE_AFTER_HOOK("show-options"), + OPTIONS_TABLE_AFTER_HOOK("split-window"), + OPTIONS_TABLE_AFTER_HOOK("unbind-key"), + OPTIONS_TABLE_HOOK("alert-activity", "", + "Run when a window has activity."), + OPTIONS_TABLE_HOOK("alert-bell", "", + "Run when a window has received a bell."), + OPTIONS_TABLE_HOOK("alert-silence", "", + "Run when a window has been silent."), + OPTIONS_TABLE_HOOK("client-active", "", + "Run when a client becomes the latest active client of its " + "session."), + OPTIONS_TABLE_HOOK("client-attached", "", + "Run when a client is attached."), + OPTIONS_TABLE_HOOK("client-created", "", + "Run when a client is created."), + OPTIONS_TABLE_HOOK("client-closed", "", + "Run when a client is closed."), + OPTIONS_TABLE_HOOK("client-detached", "", + "Run when a client is detached."), + OPTIONS_TABLE_HOOK("client-focus-in", "", + "Run when focus enters a client."), + OPTIONS_TABLE_HOOK("client-focus-out", "", + "Run when focus exits a client."), + OPTIONS_TABLE_HOOK("client-resized", "", + "Run when a client is resized."), + OPTIONS_TABLE_HOOK("client-session-changed", "", + "Run when a client's attached session is changed."), + OPTIONS_TABLE_HOOK("client-light-theme", "", + "Run when a client switches to a light theme."), + OPTIONS_TABLE_HOOK("client-dark-theme", "", + "Run when a client switches to a dark theme."), + OPTIONS_TABLE_HOOK("command-error", "", + "Run when a command fails."), + OPTIONS_TABLE_HOOK("marked-pane-changed", "", + "Run when the marked pane is set or cleared."), + OPTIONS_TABLE_PANE_HOOK("pane-activity", "", + "Run when there is new output in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-bell", "", + "Run when a pane receives a bell."), + OPTIONS_TABLE_PANE_HOOK("pane-command-finished", "", + "Run when an OSC 133 command finishes in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-command-started", "", + "Run when an OSC 133 command starts in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-created", "", + "Run when a pane is created or respawned."), + OPTIONS_TABLE_PANE_HOOK("pane-died", "", + "Run when the program running in a pane exits, but remain-on-exit " + "is on so the pane has not closed."), + OPTIONS_TABLE_PANE_HOOK("pane-exited", "", + "Run when the program running in a pane exits."), + OPTIONS_TABLE_PANE_HOOK("pane-focus-in", "", + "Run when the focus enters a pane, if the focus-events option is " + "on."), + OPTIONS_TABLE_PANE_HOOK("pane-focus-out", "", + "Run when the focus exits a pane, if the focus-events option is " + "on."), + OPTIONS_TABLE_PANE_HOOK("pane-mode-changed", "", + "Run when a pane changes mode."), + OPTIONS_TABLE_PANE_HOOK("pane-mode-entered", "", + "Run when a pane enters a mode."), + OPTIONS_TABLE_PANE_HOOK("pane-mode-exited", "", + "Run when a pane exits a mode."), + OPTIONS_TABLE_PANE_HOOK("pane-moved", "", + "Run when a pane is moved to another window."), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-closed", "", + "Run when a prompt in a pane is closed."), + OPTIONS_TABLE_PANE_HOOK("pane-prompt-opened", "", + "Run when a prompt is opened in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-resized", "", + "Run when a pane is resized."), + OPTIONS_TABLE_PANE_HOOK("pane-set-clipboard", "", + "Run when the terminal clipboard is set using the xterm escape " + "sequence."), + OPTIONS_TABLE_PANE_HOOK("pane-shell-prompt", "", + "Run when an OSC 133 shell prompt starts in a pane."), + OPTIONS_TABLE_PANE_HOOK("pane-title-changed", "", + "Run when a pane title is changed."), + OPTIONS_TABLE_HOOK("session-closed", "", + "Run when a session is closed."), + OPTIONS_TABLE_HOOK("session-created", "", + "Run when a new session is created."), + OPTIONS_TABLE_HOOK("session-added-to-group", "", + "Run when a session is added to a session group."), + OPTIONS_TABLE_HOOK("session-renamed", "", + "Run when a session is renamed."), + OPTIONS_TABLE_HOOK("session-removed-from-group", "", + "Run when a session is removed from a session group."), + OPTIONS_TABLE_HOOK("session-window-changed", "", + "Run when a session changes its active window."), + OPTIONS_TABLE_WINDOW_HOOK("window-created", "", + "Run when a window is created."), + OPTIONS_TABLE_WINDOW_HOOK("window-closed", "", + "Run when a window is closed."), + OPTIONS_TABLE_WINDOW_HOOK("window-layout-changed", "", + "Run when a window layout is changed."), + OPTIONS_TABLE_HOOK("window-linked", "", + "Run when a window is linked into a session."), + OPTIONS_TABLE_WINDOW_HOOK("window-pane-changed", "", + "Run when a window changes its active pane."), + OPTIONS_TABLE_WINDOW_HOOK("window-renamed", "", + "Run when a window is renamed."), + OPTIONS_TABLE_WINDOW_HOOK("window-resized", "", + "Run when a window is resized. This may be after the " + "client-resized hook is run."), + OPTIONS_TABLE_WINDOW_HOOK("window-unzoomed", "", + "Run when a window is unzoomed."), + OPTIONS_TABLE_WINDOW_HOOK("window-zoomed", "", + "Run when a window is zoomed."), + OPTIONS_TABLE_HOOK("window-unlinked", "", + "Run when a window is unlinked from a session."), { .name = NULL } }; diff --git a/window-customize.c b/window-customize.c index 9f84540ed..4a61eed72 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.30 2026/07/06 14:40:57 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.31 2026/07/13 19:32:28 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -81,6 +81,11 @@ enum window_customize_change { WINDOW_CUSTOMIZE_RESET, }; +enum window_customize_option_type { + WINDOW_CUSTOMIZE_OPTIONS, + WINDOW_CUSTOMIZE_HOOKS +}; + struct window_customize_itemdata { struct window_customize_modedata *data; enum window_customize_scope scope; @@ -274,7 +279,8 @@ static void window_customize_build_option(struct window_customize_modedata *data, struct mode_tree_item *top, enum window_customize_scope scope, struct options_entry *o, struct format_tree *ft, - const char *filter, struct cmd_find_state *fs) + const char *filter, struct cmd_find_state *fs, + enum window_customize_option_type type) { const struct options_table_entry *oe = options_table_entry(o); struct options *oo = options_owner(o); @@ -282,10 +288,23 @@ window_customize_build_option(struct window_customize_modedata *data, struct window_customize_itemdata *item; char *text, *expanded, *value; int global = 0, array = 0; + int is_hook = 0, is_monitor = 0; uint64_t tag; if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)) - return; + is_hook = 1; + if (options_get_monitor_data(o) != NULL) + is_monitor = 1; + switch (type) { + case WINDOW_CUSTOMIZE_OPTIONS: + if (is_hook || is_monitor) + return; + break; + case WINDOW_CUSTOMIZE_HOOKS: + if (!is_hook && !is_monitor) + return; + break; + } if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) array = 1; @@ -299,6 +318,8 @@ window_customize_build_option(struct window_customize_modedata *data, format_add(ft, "option_name", "%s", name); format_add(ft, "option_is_global", "%d", global); format_add(ft, "option_is_array", "%d", array); + format_add(ft, "option_is_hook", "%d", is_hook); + format_add(ft, "option_is_monitor", "%d", is_monitor); text = window_customize_scope_text(scope, fs); format_add(ft, "option_scope", "%s", text); @@ -309,6 +330,16 @@ window_customize_build_option(struct window_customize_modedata *data, else format_add(ft, "option_unit", "%s", ""); + if (is_monitor) { + value = hooks_monitor_to_string(o); + if (value != NULL) { + format_add(ft, "option_monitor", "%s", value); + free(value); + } else + format_add(ft, "option_monitor", "%s", ""); + } else + format_add(ft, "option_monitor", "%s", ""); + if (!array) { value = options_to_string(o, NULL, 0); format_add(ft, "option_value", "%s", value); @@ -376,7 +407,8 @@ window_customize_build_options(struct window_customize_modedata *data, enum window_customize_scope scope0, struct options *oo0, enum window_customize_scope scope1, struct options *oo1, enum window_customize_scope scope2, struct options *oo2, - struct format_tree *ft, const char *filter, struct cmd_find_state *fs) + struct format_tree *ft, const char *filter, struct cmd_find_state *fs, + enum window_customize_option_type type) { struct mode_tree_item *top; struct options_entry *o = NULL, *loop; @@ -400,6 +432,7 @@ window_customize_build_options(struct window_customize_modedata *data, window_customize_find_user_options(oo2, &list, &size); for (i = 0; i < size; i++) { + o = NULL; if (oo2 != NULL) o = options_get(oo2, list[i]); if (o == NULL && oo1 != NULL) @@ -413,7 +446,7 @@ window_customize_build_options(struct window_customize_modedata *data, else scope = scope0; window_customize_build_option(data, top, scope, o, ft, filter, - fs); + fs, type); } free(list); @@ -437,7 +470,7 @@ window_customize_build_options(struct window_customize_modedata *data, else scope = scope0; window_customize_build_option(data, top, scope, o, ft, filter, - fs); + fs, type); loop = options_next(loop); } } @@ -556,19 +589,31 @@ window_customize_build(void *modedata, WINDOW_CUSTOMIZE_SERVER, global_options, WINDOW_CUSTOMIZE_NONE, NULL, WINDOW_CUSTOMIZE_NONE, NULL, - ft, filter, &fs); + ft, filter, &fs, WINDOW_CUSTOMIZE_OPTIONS); window_customize_build_options(data, "Session Options", (3ULL << 62)|(OPTIONS_TABLE_SESSION << 1)|1, WINDOW_CUSTOMIZE_GLOBAL_SESSION, global_s_options, WINDOW_CUSTOMIZE_SESSION, fs.s->options, WINDOW_CUSTOMIZE_NONE, NULL, - ft, filter, &fs); + ft, filter, &fs, WINDOW_CUSTOMIZE_OPTIONS); window_customize_build_options(data, "Window & Pane Options", (3ULL << 62)|(OPTIONS_TABLE_WINDOW << 1)|1, WINDOW_CUSTOMIZE_GLOBAL_WINDOW, global_w_options, WINDOW_CUSTOMIZE_WINDOW, fs.w->options, WINDOW_CUSTOMIZE_PANE, fs.wp->options, - ft, filter, &fs); + ft, filter, &fs, WINDOW_CUSTOMIZE_OPTIONS); + window_customize_build_options(data, "Session Hooks", + (3ULL << 62)|(1ULL << 8)|(OPTIONS_TABLE_SESSION << 1)|1, + WINDOW_CUSTOMIZE_GLOBAL_SESSION, global_s_options, + WINDOW_CUSTOMIZE_SESSION, fs.s->options, + WINDOW_CUSTOMIZE_NONE, NULL, + ft, filter, &fs, WINDOW_CUSTOMIZE_HOOKS); + window_customize_build_options(data, "Window & Pane Hooks", + (3ULL << 62)|(1ULL << 8)|(OPTIONS_TABLE_WINDOW << 1)|1, + WINDOW_CUSTOMIZE_GLOBAL_WINDOW, global_w_options, + WINDOW_CUSTOMIZE_WINDOW, fs.w->options, + WINDOW_CUSTOMIZE_PANE, fs.wp->options, + ft, filter, &fs, WINDOW_CUSTOMIZE_HOOKS); format_free(ft); ft = format_create_from_state(NULL, NULL, &fs); @@ -662,10 +707,12 @@ window_customize_draw_option(struct window_customize_modedata *data, const char *array_key; const char *space = "", *unit = ""; char *value = NULL, *expanded; + char *monitor; char *default_value = NULL; char choices[256] = ""; struct cmd_find_state fs; struct format_tree *ft; + int is_hook, is_monitor; if (!window_customize_check_item(data, item, &fs)) return; @@ -676,6 +723,8 @@ window_customize_draw_option(struct window_customize_modedata *data, if (o == NULL) return; oe = options_table_entry(o); + is_hook = (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_HOOK)); + is_monitor = (options_get_monitor_data(o) != NULL); if (oe != NULL && oe->unit != NULL) { space = " "; @@ -683,9 +732,11 @@ window_customize_draw_option(struct window_customize_modedata *data, } ft = format_create_from_state(NULL, NULL, &fs); - if (oe == NULL || oe->text == NULL) - text = "This option doesn't have a description."; - else + if (oe == NULL || oe->text == NULL) { + text = is_monitor ? + "This hook runs when a monitor changes." : + "This option doesn't have a description."; + } else text = oe->text; if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, "%s", text)) @@ -694,6 +745,12 @@ window_customize_draw_option(struct window_customize_modedata *data, if (s->cy >= cy + sy - 1) goto out; + if (is_monitor) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This is a monitor hook.")) + goto out; + goto monitor; + } if (oe == NULL) text = "user"; else if ((oe->scope & (OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE)) == @@ -705,11 +762,31 @@ window_customize_draw_option(struct window_customize_modedata *data, text = "session"; else text = "server"; - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "This is a %s option.", text)) - goto out; + if (is_hook) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This is a %s hook.", text)) + goto out; + } else { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This is a %s option.", text)) + goto out; + } + +monitor: + monitor = hooks_monitor_to_string(o); + if (monitor != NULL) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Monitor: %s", monitor)) { + free(monitor); + goto out; + } + free(monitor); + } if (oe != NULL && (oe->flags & OPTIONS_TABLE_IS_ARRAY)) { - if (array_key != NULL) { + if (is_hook) { + if (array_key == NULL) + goto out; + } else if (array_key != NULL) { if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, &grid_default_cell, "This is an array option, key %s.", array_key)) @@ -734,9 +811,17 @@ window_customize_draw_option(struct window_customize_modedata *data, default_value = NULL; } } - if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, - &grid_default_cell, "Option value: %s%s%s", value, space, unit)) - goto out; + if (is_hook || is_monitor) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Hook command: %s%s%s", value, space, + unit)) + goto out; + } else { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Option value: %s%s%s", value, space, + unit)) + goto out; + } if (oe == NULL || oe->type == OPTIONS_TABLE_STRING) { expanded = format_expand(ft, value); if (strcmp(expanded, value) != 0) { From b1c347ee0ca31031efcea9c873017d72248b23b0 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 19:35:22 +0000 Subject: [PATCH 42/47] Replace last use of fgetln with getline, GitHub issue 5389 from Yayo Razo. --- prompt-history.c | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/prompt-history.c b/prompt-history.c index ad1ab4047..109043aa7 100644 --- a/prompt-history.c +++ b/prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD: prompt-history.c,v 1.1 2026/06/25 11:39:11 nicm Exp $ */ +/* $OpenBSD: prompt-history.c,v 1.2 2026/07/13 19:35:22 nicm Exp $ */ /* * Copyright (c) 2026 Nicholas Marriott @@ -79,8 +79,9 @@ void prompt_load_history(void) { FILE *f; - char *history_file, *line, *tmp; - size_t length; + char *history_file, *line = NULL; + size_t length = 0; + ssize_t got; if ((history_file = prompt_find_history_file()) == NULL) return; @@ -94,23 +95,13 @@ prompt_load_history(void) } free(history_file); - for (;;) { - if ((line = fgetln(f, &length)) == NULL) - break; - - if (length > 0) { - if (line[length - 1] == '\n') { - line[length - 1] = '\0'; - prompt_add_typed_history(line); - } else { - tmp = xmalloc(length + 1); - memcpy(tmp, line, length); - tmp[length] = '\0'; - prompt_add_typed_history(tmp); - free(tmp); - } - } + while ((got = getline(&line, &length, f)) != -1) { + if (got > 0 && line[got - 1] == '\n') + line[got - 1] = '\0'; + if (got > 0) + prompt_add_typed_history(line); } + free(line); fclose(f); } From 9c65d349e8ca2dca2010c87733fe7c0a317432ce Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 22:08:58 +0100 Subject: [PATCH 43/47] Tweak times in tests. --- regress/alerts.sh | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/regress/alerts.sh b/regress/alerts.sh index 2b9e4f495..60d9a05f7 100644 --- a/regress/alerts.sh +++ b/regress/alerts.sh @@ -46,7 +46,7 @@ wait_for() value=$($TMUX show -gqv "$option" 2>/dev/null || true) [ "$value" = "$expected" ] && return 0 i=$((i + 1)) - sleep 0.2 + sleep 0.5 done fail "expected $option to be '$expected' but got '$value'" } @@ -62,7 +62,7 @@ assert_unchanged() [ "$value" = "$expected" ] || \ fail "expected $option to remain '$expected' but got '$value'" i=$((i + 1)) - sleep 0.2 + sleep 0.5 done } @@ -77,7 +77,7 @@ wait_for_fmt() value=$($TMUX display -pt "$target" "$fmt" 2>/dev/null || true) [ "$value" = "$expected" ] && return 0 i=$((i + 1)) - sleep 0.2 + sleep 0.5 done fail "expected $fmt for $target to be '$expected' but got '$value'" } @@ -94,7 +94,7 @@ assert_fmt_unchanged() [ "$value" = "$expected" ] || \ fail "expected $fmt for $target to remain '$expected' but got '$value'" i=$((i + 1)) - sleep 0.2 + sleep 0.5 done } @@ -290,9 +290,9 @@ $TMUX selectw -t mon:w0 || fail "select-window w0 failed" # prevents an alert until a full quiet interval has passed afterwards. $TMUX neww -d -t mon: -n silreset cat || fail "new-window silreset failed" $TMUX set -g @log '' || fail "reset @log failed" -$TMUX set -wt mon:silreset monitor-silence 2 || +$TMUX set -wt mon:silreset monitor-silence 10 || fail "set monitor-silence silreset failed" -sleep 1 +sleep 3 activity mon:silreset assert_unchanged @log '' wait_for @log '|alert-silence:mon:silreset' From f0f59bc2fd47e0ef6f3f1635091e7ae81cf454ce Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 22:52:25 +0100 Subject: [PATCH 44/47] Fix a test. --- regress/hooks-notify.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/regress/hooks-notify.sh b/regress/hooks-notify.sh index 045b80af6..6eb6ae49e 100644 --- a/regress/hooks-notify.sh +++ b/regress/hooks-notify.sh @@ -449,10 +449,10 @@ $TMUX killp -t "$pane" || fail "kill-pane zoom failed" $TMUX set -g @a 0 || fail "set @a failed" $TMUX set -g @cl 0 || fail "set @cl failed" $TMUX set-hook -g client-created \ - 'set -gF @cl "#{@cl}|#{hook}:#{hook_client}"' || + 'set -gF @cl "#{?#{m:client-*,#{hook_client}},#{@cl}|#{hook}:#{hook_client},#{@cl}}"' || fail "set-hook client-created failed" $TMUX set-hook -g client-closed \ - 'set -gF @cl "#{@cl}|#{hook}:#{hook_client}"' || + 'set -gF @cl "#{?#{m:client-*,#{hook_client}},#{@cl}|#{hook}:#{hook_client},#{@cl}}"' || fail "set-hook client-closed failed" $TMUX set-hook -g client-attached 'set -gF @a "#{hook}"' || fail "set-hook client-attached failed" From 37da97fbe618efadc9c7a2f2d9a013c8e9155f87 Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 20:02:00 +0000 Subject: [PATCH 45/47] Add environment to customize mode also --- window-customize.c | 342 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 322 insertions(+), 20 deletions(-) diff --git a/window-customize.c b/window-customize.c index 4a61eed72..ba1c060cd 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-customize.c,v 1.31 2026/07/13 19:32:28 nicm Exp $ */ +/* $OpenBSD: window-customize.c,v 1.32 2026/07/13 20:02:00 nicm Exp $ */ /* * Copyright (c) 2020 Nicholas Marriott @@ -39,7 +39,11 @@ static void window_customize_key(struct window_mode_entry *, "#[fg=themelightgrey]#[ignore]#{option_value}" \ "#{?option_unit, #{option_unit},}" \ "," \ - "#{key}" \ + "#{?is_environment," \ + "#[fg=themelightgrey]#[ignore]#{environment_value}" \ + "," \ + "#{key}" \ + "}" \ "}" static const struct menu_item window_customize_menu_items[] = { @@ -73,7 +77,9 @@ enum window_customize_scope { WINDOW_CUSTOMIZE_SESSION, WINDOW_CUSTOMIZE_GLOBAL_WINDOW, WINDOW_CUSTOMIZE_WINDOW, - WINDOW_CUSTOMIZE_PANE + WINDOW_CUSTOMIZE_PANE, + WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT, + WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT }; enum window_customize_change { @@ -86,14 +92,23 @@ enum window_customize_option_type { WINDOW_CUSTOMIZE_HOOKS }; +enum window_customize_item_type { + WINDOW_CUSTOMIZE_ITEM_OPTION, + WINDOW_CUSTOMIZE_ITEM_KEY, + WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT +}; + struct window_customize_itemdata { struct window_customize_modedata *data; + enum window_customize_item_type type; enum window_customize_scope scope; char *table; key_code key; struct options *oo; + struct environ *environ; + int environ_flags; char *name; char *array_key; }; @@ -149,10 +164,27 @@ window_customize_get_tree(enum window_customize_scope scope, return (fs->w->options); case WINDOW_CUSTOMIZE_PANE: return (fs->wp->options); + case WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT: + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: + return (NULL); } return (NULL); } +static struct environ * +window_customize_get_environment(enum window_customize_scope scope, + struct cmd_find_state *fs) +{ + switch (scope) { + case WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT: + return (global_environ); + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: + return (fs->s->environ); + default: + return (NULL); + } +} + static int window_customize_check_item(struct window_customize_modedata *data, struct window_customize_itemdata *item, struct cmd_find_state *fsp) @@ -166,6 +198,10 @@ window_customize_check_item(struct window_customize_modedata *data, cmd_find_copy_state(fsp, &data->fs); else cmd_find_from_pane(fsp, data->wp, 0); + if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) { + return (item->environ == + window_customize_get_environment(item->scope, fsp)); + } return (item->oo == window_customize_get_tree(item->scope, fsp)); } @@ -203,6 +239,7 @@ window_customize_scope_text(enum window_customize_scope scope, xasprintf(&s, "pane %u", idx); break; case WINDOW_CUSTOMIZE_SESSION: + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: xasprintf(&s, "session %s", fs->s->name); break; case WINDOW_CUSTOMIZE_WINDOW: @@ -258,6 +295,7 @@ window_customize_build_array(struct window_customize_modedata *data, format_add(ft, "option_value", "%s", value); item = window_customize_add_item(data); + item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; item->scope = scope; item->oo = oo; item->name = xstrdup(options_name(o)); @@ -355,6 +393,7 @@ window_customize_build_option(struct window_customize_modedata *data, free(expanded); } item = window_customize_add_item(data); + item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; item->oo = oo; item->scope = scope; item->name = xstrdup(name); @@ -497,6 +536,7 @@ window_customize_build_keys(struct window_customize_modedata *data, ft = format_create_from_state(NULL, NULL, fs); format_add(ft, "is_option", "0"); format_add(ft, "is_key", "1"); + format_add(ft, "is_environment", "0"); bd = key_bindings_first(kt); while (bd != NULL) { @@ -514,6 +554,7 @@ window_customize_build_keys(struct window_customize_modedata *data, } item = window_customize_add_item(data); + item->type = WINDOW_CUSTOMIZE_ITEM_KEY; item->scope = WINDOW_CUSTOMIZE_KEY; item->table = xstrdup(kt->name); item->key = bd->key; @@ -558,6 +599,80 @@ window_customize_build_keys(struct window_customize_modedata *data, format_free(ft); } +static void +window_customize_build_environment(struct window_customize_modedata *data, + const char *title, uint64_t tag, enum window_customize_scope scope, + struct environ *env, struct format_tree *ft, const char *filter, + struct cmd_find_state *fs) +{ + struct mode_tree_item *top; + struct window_customize_itemdata *item; + struct environ_entry *envent; + char *name, *text, *expanded, *value; + uint64_t item_tag; + int global; + + top = mode_tree_add(data->data, NULL, NULL, tag, title, NULL, 0); + mode_tree_no_tag(top); + + global = (scope == WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT); + format_add(ft, "is_option", "0"); + format_add(ft, "is_key", "0"); + format_add(ft, "is_environment", "1"); + format_add(ft, "environment_is_global", "%d", global); + + text = window_customize_scope_text(scope, fs); + format_add(ft, "environment_scope", "%s", text); + free(text); + + envent = environ_first(env); + while (envent != NULL) { + format_add(ft, "environment_name", "%s", envent->name); + format_add(ft, "environment_hidden", "%d", + !!(envent->flags & ENVIRON_HIDDEN)); + format_add(ft, "environment_removed", "%d", + envent->value == NULL); + if (envent->value == NULL) + value = xstrdup(""); + else + value = xstrdup(envent->value); + format_add(ft, "environment_value", "%s", value); + + if (filter != NULL) { + expanded = format_expand(ft, filter); + if (!format_true(expanded)) { + free(expanded); + free(value); + envent = environ_next(envent); + continue; + } + free(expanded); + } + + item = window_customize_add_item(data); + item->type = WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT; + item->scope = scope; + item->environ = env; + item->environ_flags = envent->flags; + item->name = xstrdup(envent->name); + + if (envent->value == NULL) { + xasprintf(&name, "-%s", envent->name); + text = NULL; + } else { + name = xstrdup(envent->name); + text = format_expand(ft, data->format); + } + item_tag = (2ULL << 62)|((uint64_t)(uintptr_t)envent); + mode_tree_add(data->data, top, item, item_tag, name, text, 0); + free(name); + free(text); + free(value); + + envent = environ_next(envent); + } +} + static void window_customize_build(void *modedata, __unused struct sort_criteria *sort_crit, __unused uint64_t *tag, @@ -583,6 +698,7 @@ window_customize_build(void *modedata, ft = format_create_from_state(NULL, NULL, &fs); format_add(ft, "is_option", "1"); format_add(ft, "is_key", "0"); + format_add(ft, "is_environment", "0"); window_customize_build_options(data, "Server Options", (3ULL << 62)|(OPTIONS_TABLE_SERVER << 1)|1, @@ -614,9 +730,18 @@ window_customize_build(void *modedata, WINDOW_CUSTOMIZE_WINDOW, fs.w->options, WINDOW_CUSTOMIZE_PANE, fs.wp->options, ft, filter, &fs, WINDOW_CUSTOMIZE_HOOKS); + window_customize_build_environment(data, "Global Environment", + (3ULL << 62)|(2ULL << 8)|1, + WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT, global_environ, ft, filter, + &fs); + window_customize_build_environment(data, "Session Environment", + (3ULL << 62)|(2ULL << 8)|3, + WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT, fs.s->environ, ft, filter, + &fs); format_free(ft); ft = format_create_from_state(NULL, NULL, &fs); + format_add(ft, "is_environment", "0"); i = 0; kt = key_bindings_first_table(); @@ -931,6 +1056,67 @@ out: format_free(ft); } +static void +window_customize_draw_environment(struct window_customize_modedata *data, + struct window_customize_itemdata *item, struct screen_write_ctx *ctx, + u_int sx, u_int sy) +{ + struct screen *s = ctx->s; + u_int cx = s->cx, cy = s->cy; + struct environ_entry *envent, *parent; + struct cmd_find_state fs; + const char *text; + + if (!window_customize_check_item(data, item, &fs)) + return; + envent = environ_find(item->environ, item->name); + if (envent == NULL) + return; + + if (item->scope == WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT) + text = "global"; + else + text = "session"; + if (!screen_write_text(ctx, cx, sx, sy, 0, &grid_default_cell, + "This is a %s environment variable.", text)) + return; + if (envent->flags & ENVIRON_HIDDEN) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "This variable is hidden.")) + return; + } + + screen_write_cursormove(ctx, cx, s->cy + 1, 0); /* skip line */ + if (s->cy >= cy + sy - 1) + return; + + if (envent->value == NULL) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Variable is removed.")) + return; + } else { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Variable value: %s", envent->value)) + return; + } + + if (item->scope != WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT) + return; + + parent = environ_find(global_environ, item->name); + if (parent == NULL) + return; + if (parent->value == NULL) { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Global variable is removed.")) + return; + } else { + if (!screen_write_text(ctx, cx, sx, sy - (s->cy - cy), 0, + &grid_default_cell, "Global value: %s", parent->value)) + return; + } +} + static void window_customize_draw(void *modedata, void *itemdata, struct screen_write_ctx *ctx, u_int sx, u_int sy) @@ -941,8 +1127,10 @@ window_customize_draw(void *modedata, void *itemdata, if (item == NULL) return; - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_draw_key(data, item, ctx, sx, sy); + else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_draw_environment(data, item, ctx, sx, sy); else window_customize_draw_option(data, item, ctx, sx, sy); } @@ -994,7 +1182,7 @@ static const char** window_customize_help(u_int *width, const char **item) { *width = 52; - *item = "option"; + *item = "item"; return (window_customize_help_lines); } @@ -1139,6 +1327,85 @@ fail: return (PROMPT_CLOSE); } +static enum prompt_result +window_customize_set_environment_callback(__unused struct client *c, + void *itemdata, const char *s, __unused enum prompt_key_result key) +{ + struct window_customize_itemdata *item = itemdata; + struct window_customize_modedata *data = item->data; + struct environ_entry *envent; + int flags; + + if (s == NULL || data->dead) + return (PROMPT_CLOSE); + if (item == NULL || !window_customize_check_item(data, item, NULL)) + return (PROMPT_CLOSE); + + flags = item->environ_flags; + envent = environ_find(item->environ, item->name); + if (envent != NULL) + flags = envent->flags; + environ_set(item->environ, item->name, flags, "%s", s); + + mode_tree_build(data->data); + mode_tree_draw(data->data); + data->wp->flags |= PANE_REDRAW; + + return (PROMPT_CLOSE); +} + +static void +window_customize_set_environment(struct client *c, + struct window_customize_modedata *data, + struct window_customize_itemdata *item, int global) +{ + struct window_customize_itemdata *new_item; + struct environ_entry *envent; + struct environ *env; + enum window_customize_scope scope; + struct cmd_find_state fs; + const char *space = ""; + char *prompt, *text; + + if (item == NULL || !window_customize_check_item(data, item, &fs)) + return; + envent = environ_find(item->environ, item->name); + if (envent == NULL) + return; + + if (global) { + scope = WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT; + env = global_environ; + } else { + scope = item->scope; + env = item->environ; + } + + text = window_customize_scope_text(scope, &fs); + if (*text != '\0') + space = ", for "; + else if (scope == WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT) + space = ", global"; + xasprintf(&prompt, "(%s%s%s) ", item->name, space, text); + free(text); + + new_item = xcalloc(1, sizeof *new_item); + new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT; + new_item->scope = scope; + new_item->environ = env; + new_item->environ_flags = envent->flags; + new_item->name = xstrdup(item->name); + + data->references++; + mode_tree_set_prompt(data->data, c, prompt, + envent->value == NULL ? "" : envent->value, PROMPT_TYPE_COMMAND, + PROMPT_NOFORMAT, window_customize_set_environment_callback, + window_customize_free_item_callback, new_item); + + free(prompt); +} + static void window_customize_set_option(struct client *c, struct window_customize_modedata *data, @@ -1176,6 +1443,8 @@ window_customize_set_option(struct client *c, case WINDOW_CUSTOMIZE_SERVER: case WINDOW_CUSTOMIZE_GLOBAL_SESSION: case WINDOW_CUSTOMIZE_GLOBAL_WINDOW: + case WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT: + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: scope = item->scope; break; case WINDOW_CUSTOMIZE_SESSION: @@ -1210,6 +1479,10 @@ window_customize_set_option(struct client *c, else scope = WINDOW_CUSTOMIZE_WINDOW; break; + case WINDOW_CUSTOMIZE_GLOBAL_ENVIRONMENT: + case WINDOW_CUSTOMIZE_SESSION_ENVIRONMENT: + scope = item->scope; + break; } } if (scope == item->scope) @@ -1250,6 +1523,7 @@ window_customize_set_option(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; new_item->scope = scope; new_item->oo = oo; new_item->name = xstrdup(name); @@ -1331,6 +1605,7 @@ window_customize_set_array_key(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_OPTION; new_item->scope = item->scope; new_item->oo = item->oo; new_item->name = xstrdup(item->name); @@ -1345,6 +1620,19 @@ window_customize_set_array_key(struct client *c, free(prompt); } +static void +window_customize_unset_environment(struct window_customize_modedata *data, + struct window_customize_itemdata *item) +{ + if (item == NULL || !window_customize_check_item(data, item, NULL)) + return; + if (environ_find(item->environ, item->name) == NULL) + return; + if (item == mode_tree_get_current(data->data)) + mode_tree_up(data->data, 0); + environ_unset(item->environ, item->name); +} + static void window_customize_unset_option(struct window_customize_modedata *data, struct window_customize_itemdata *item) @@ -1469,6 +1757,7 @@ window_customize_set_key(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_KEY; new_item->scope = item->scope; new_item->table = xstrdup(item->table); new_item->key = key; @@ -1485,6 +1774,7 @@ window_customize_set_key(struct client *c, new_item = xcalloc(1, sizeof *new_item); new_item->data = data; + new_item->type = WINDOW_CUSTOMIZE_ITEM_KEY; new_item->scope = item->scope; new_item->table = xstrdup(item->table); new_item->key = key; @@ -1545,19 +1835,21 @@ window_customize_change_each(void *modedata, void *itemdata, switch (data->change) { case WINDOW_CUSTOMIZE_UNSET: - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_unset_key(data, item); + else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_unset_environment(data, item); else window_customize_unset_option(data, item); break; case WINDOW_CUSTOMIZE_RESET: - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_reset_key(data, item); - else + else if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) window_customize_reset_option(data, item); break; } - if (item->scope != WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) options_push_changes(item->name); } @@ -1576,19 +1868,21 @@ window_customize_change_current_callback(__unused struct client *c, item = mode_tree_get_current(data->data); switch (data->change) { case WINDOW_CUSTOMIZE_UNSET: - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_unset_key(data, item); + else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_unset_environment(data, item); else window_customize_unset_option(data, item); break; case WINDOW_CUSTOMIZE_RESET: - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_reset_key(data, item); - else + else if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) window_customize_reset_option(data, item); break; } - if (item->scope != WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_OPTION) options_push_changes(item->name); mode_tree_build(data->data); mode_tree_draw(data->data); @@ -1636,7 +1930,7 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, switch (key) { case 'a': - if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY) + if (item == NULL || item->type != WINDOW_CUSTOMIZE_ITEM_OPTION) break; window_customize_set_array_key(c, data, item); break; @@ -1644,8 +1938,10 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, case 's': if (item == NULL) break; - if (item->scope == WINDOW_CUSTOMIZE_KEY) + if (item->type == WINDOW_CUSTOMIZE_ITEM_KEY) window_customize_set_key(c, data, item); + else if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_set_environment(c, data, item, 0); else { window_customize_set_option(c, data, item, 0, 1); options_push_changes(item->name); @@ -1653,7 +1949,7 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, mode_tree_build(data->data); break; case 'w': - if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY) + if (item == NULL || item->type != WINDOW_CUSTOMIZE_ITEM_OPTION) break; window_customize_set_option(c, data, item, 0, 0); options_push_changes(item->name); @@ -1661,14 +1957,20 @@ window_customize_key(struct window_mode_entry *wme, struct client *c, break; case 'S': case 'W': - if (item == NULL || item->scope == WINDOW_CUSTOMIZE_KEY) + if (item == NULL || item->type == WINDOW_CUSTOMIZE_ITEM_KEY) break; - window_customize_set_option(c, data, item, 1, 0); - options_push_changes(item->name); + if (item->type == WINDOW_CUSTOMIZE_ITEM_ENVIRONMENT) + window_customize_set_environment(c, data, item, 1); + else { + window_customize_set_option(c, data, item, 1, 0); + options_push_changes(item->name); + } mode_tree_build(data->data); break; case 'd': - if (item == NULL || item->array_key != NULL) + if (item == NULL || + item->type != WINDOW_CUSTOMIZE_ITEM_OPTION || + item->array_key != NULL) break; xasprintf(&prompt, "Reset %s to default? ", item->name); data->references++; From df6ed20271a80d93783d2efc7f7bdd688c5cbb1b Mon Sep 17 00:00:00 2001 From: Nicholas Marriott Date: Mon, 13 Jul 2026 23:03:33 +0100 Subject: [PATCH 46/47] Add customize mode test. --- regress/customize-mode.sh | 746 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100644 regress/customize-mode.sh diff --git a/regress/customize-mode.sh b/regress/customize-mode.sh new file mode 100644 index 000000000..89ebc6a42 --- /dev/null +++ b/regress/customize-mode.sh @@ -0,0 +1,746 @@ +#!/bin/sh + +# Exercise customize-mode-specific mutations. mode-mutation.sh covers stale +# mode-tree data while customize-mode is open; this covers the actions unique to +# customize mode: adding, setting, editing, resetting, renaming array keys and +# unsetting options, hooks, environment variables and key bindings. + +PATH=/bin:/usr/bin +TERM=screen + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) +TMP=$(mktemp -d) || exit 1 +TMUX_TMPDIR="$TMP" +export TMUX_TMPDIR + +TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" +OUT="$TEST_TMUX -LtestB$$ -f/dev/null" +FORMAT='CM #{?is_option,O,#{?is_environment,E,K}} #{option_name}#{environment_name}#{key}=#{option_value}#{environment_value}' + +cleanup_servers() +{ + $TMUX kill-server 2>/dev/null + $OUT kill-server 2>/dev/null + sleep 0.5 +} + +cleanup() +{ + cleanup_servers + rm -rf "$TMP" +} +trap cleanup EXIT + +fail() +{ + echo "$1" >&2 + cleanup + exit 1 +} + +capture() +{ + $OUT capture-pane -p -t out:0 2>/dev/null +} + +settle() +{ + sleep 0.3 +} + +wait_clients() +{ + i=0 + while [ "$i" -lt 50 ]; do + c=$($TMUX list-clients -F x 2>/dev/null | grep -c x) + [ "$c" -eq "$1" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "expected $1 clients, have $c" +} + +wait_mode() +{ + want=$1 + + i=0 + while [ "$i" -lt 50 ]; do + got=$($TMUX display-message -p -t cm:0 '#{pane_in_mode}' \ + 2>/dev/null) + [ "$got" = "$want" ] && return 0 + sleep 0.2 + i=$((i + 1)) + done + fail "pane mode state is $got, expected $want" +} + +assert_alive() +{ + $TMUX display-message -p 'alive' >/dev/null 2>&1 || + fail "$1: server exited" +} + +start_client() +{ + cleanup_servers + $TMUX new-session -d -s cm -n main -x 80 -y 24 'cat' || + fail "new-session failed" + $OUT new-session -d -s out -x 80 -y 24 "$TMUX attach -t cm" || + fail "outer client failed" + wait_clients 1 +} + +open_customize() +{ + if [ "$#" -eq 0 ]; then + $TMUX customize-mode -t cm:0 -F "$FORMAT" || + fail "customize-mode failed" + else + $TMUX customize-mode -t cm:0 -F "$FORMAT" -f "$1" || + fail "customize-mode failed" + fi + wait_mode 1 + settle +} + +send() +{ + $TMUX send-keys -t cm:0 "$@" || fail "send-keys failed: $*" +} + +send_literal() +{ + $TMUX send-keys -t cm:0 -l "$1" || fail "send literal failed: $1" +} + +repeat_key() +{ + key=$1 + count=$2 + + i=0 + while [ "$i" -lt "$count" ]; do + send "$key" + i=$((i + 1)) + done +} + +replace_prompt() +{ + send C-u + send_literal "$1" + send Enter + settle +} + +assert_equals() +{ + got=$1 + want=$2 + what=$3 + + [ "$got" = "$want" ] || + fail "$what: got '$got', expected '$want'" +} + +assert_empty() +{ + got=$1 + what=$2 + + [ -z "$got" ] || fail "$what: got '$got', expected empty" +} + +assert_contains() +{ + text=$1 + needle=$2 + what=$3 + + echo "$text" | grep -F -q "$needle" || + fail "$what: missing '$needle' in '$text'" +} + +assert_not_contains() +{ + text=$1 + needle=$2 + what=$3 + + echo "$text" | grep -F -q "$needle" && + fail "$what: found '$needle' in '$text'" + return 0 +} + +test_user_option() +{ + start_client + + open_customize + send Enter + settle + send_literal "cm_add added" + send Enter + settle + assert_equals "$($TMUX show-options -sqv @cm_add)" "added" \ + "added server user option" + + send q + wait_mode 0 + open_customize '#{==:#{option_name},@cm_add}' + send Right j Enter + settle + replace_prompt "changed" + assert_equals "$($TMUX show-options -sqv @cm_add)" "changed" \ + "set user option" + + send u y + settle + assert_empty "$($TMUX show-options -sqv @cm_add)" "unset user option" + send q + wait_mode 0 +} + +test_reset_window_pane_option() +{ + start_client + + $TMUX set-option -g window-style fg=red || + fail "set global window-style failed" + $TMUX set-window-option -t cm:0 window-style fg=green || + fail "set window window-style failed" + $TMUX set-option -p -t cm:0.0 window-style fg=blue || + fail "set pane window-style failed" + + open_customize '#{==:#{option_name},window-style}' + send j j Right j + settle + send d y + settle + + assert_equals "$($TMUX show-option -gqv window-style)" "default" \ + "reset global window-style" + assert_empty "$($TMUX show-options -wqv -t cm:0 window-style)" \ + "reset window window-style" + assert_empty "$($TMUX show-option -p -t cm:0.0 -qv window-style)" \ + "reset pane window-style" + send q + wait_mode 0 +} + +test_environment() +{ + start_client + + open_customize + repeat_key j 6 + send Enter + settle + send_literal "CM_ENV=one" + send Enter + settle + assert_equals "$($TMUX show-environment -t cm CM_ENV)" \ + "CM_ENV=one" "add session environment" + + send q + wait_mode 0 + open_customize '#{==:#{environment_name},CM_ENV}' + repeat_key j 6 + send Right j Enter + settle + replace_prompt "two" + assert_equals "$($TMUX show-environment -t cm CM_ENV)" \ + "CM_ENV=two" "set session environment" + + send S + settle + replace_prompt "global" + assert_equals "$($TMUX show-environment -g CM_ENV)" \ + "CM_ENV=global" "set global environment" + + send u y + settle + $TMUX show-environment -t cm CM_ENV >/dev/null 2>&1 && + fail "unset session environment: variable still exists" + send q + wait_mode 0 +} + +test_user_hook() +{ + start_client + + open_customize + repeat_key j 3 + send Enter + settle + send_literal "cm_hook display-message hook" + send Enter + settle + assert_equals "$($TMUX show-option -qv @cm_hook)" \ + "display-message hook" "add user hook" + + send q + wait_mode 0 + open_customize '#{==:#{option_name},@cm_hook}' + repeat_key j 3 + send Right j + settle + assert_contains "$(capture)" "@cm_hook" "user hook shown as hook" + + send u y + settle + assert_empty "$($TMUX show-option -qv @cm_hook)" "unset user hook" + send q + wait_mode 0 +} + +test_hook_array() +{ + start_client + + $TMUX set-hook -g 'after-new-session[0]' 'display-message old' || + fail "set hook array failed" + + open_customize '#{==:#{option_name},after-new-session}' + repeat_key j 3 + send Right j Right j + settle + + send a + settle + replace_prompt "1" + hooks=$($TMUX show-hooks -g after-new-session) + assert_contains "$hooks" "after-new-session[1] display-message old" \ + "renamed hook array key" + assert_not_contains "$hooks" "after-new-session[0]" \ + "old hook array key removed" + + send Enter + settle + replace_prompt "display-message new" + hooks=$($TMUX show-hooks -g after-new-session) + assert_contains "$hooks" "after-new-session[1] display-message new" \ + "set hook array value" + + send u y + settle + hooks=$($TMUX show-hooks -g after-new-session) + assert_not_contains "$hooks" "after-new-session[1]" \ + "unset hook array item" + send q + wait_mode 0 +} + +test_changed_only() +{ + start_client + + $TMUX set-option -s @cm_changed yes || + fail "set changed server option failed" + $TMUX bind-key -T prefix User1 display-message changed-only || + fail "set changed key failed" + + open_customize + send C + settle + send M-+ + settle + screen=$(capture) + assert_contains "$screen" "@cm_changed" "changed-only user option" + assert_contains "$screen" "User1" "changed-only key" + assert_not_contains "$screen" "Global Environment" \ + "changed-only hides environment" + send q + wait_mode 0 +} + +test_reset_key() +{ + start_client + + orig=$($TMUX list-keys -T prefix -F '#{key_command}' c) || + fail "default c binding not found" + $TMUX bind-key -T prefix c display-message changed || + fail "change c binding failed" + + open_customize '#{==:#{key},c}' + repeat_key j 10 + send Right j d y + settle + after=$($TMUX list-keys -T prefix -F '#{key_command}' c) || + fail "reset c binding not found" + assert_equals "$after" "$orig" "reset key binding" + send q + wait_mode 0 +} + +test_tagged_unset() +{ + start_client + + $TMUX set-option -s @cm_tag_a one || fail "set tag option a failed" + $TMUX set-option -s @cm_tag_b two || fail "set tag option b failed" + + open_customize '#{m/r:^@cm_tag_,#{option_name}}' + send Right j t j t U y + settle + assert_empty "$($TMUX show-options -sqv @cm_tag_a)" \ + "tagged unset option a" + assert_empty "$($TMUX show-options -sqv @cm_tag_b)" \ + "tagged unset option b" + send q + wait_mode 0 +} + +test_key_binding() +{ + start_client + + open_customize + repeat_key j 10 + send Enter + settle + send_literal "User0 display-message before" + send Enter + settle + keys=$($TMUX list-keys -T prefix User0 2>/dev/null) || + fail "added key binding not found" + assert_contains "$keys" "display-message before" "add key binding" + + send q + wait_mode 0 + open_customize '#{==:#{key},User0}' + repeat_key j 10 + send Right j Right j + settle + + send s + settle + replace_prompt "display-message after" + keys=$($TMUX list-keys -T prefix User0 2>/dev/null) || + fail "edited key binding not found" + assert_contains "$keys" "display-message after" "set key command" + + send j s + settle + replace_prompt "custom note" + note=$($TMUX list-keys -T prefix -F '#{key_note}' User0) + assert_equals "$note" "custom note" "set key note" + + send j s + settle + keys=$($TMUX list-keys -T prefix User0 2>/dev/null) || + fail "repeat key binding not found" + assert_contains "$keys" "bind-key -r" "toggle key repeat" + + send u y + settle + $TMUX list-keys -T prefix User0 >/dev/null 2>&1 && + fail "unset key binding: binding still exists" + send q + wait_mode 0 +} + +test_editor() +{ + start_client + + editor="$TMP/editor.sh" + editor_value="$TMP/editor-value" + { + echo '#!/bin/sh' + printf 'cat "%s" > "$1"\n' "$editor_value" + } > "$editor" || fail "write editor helper failed" + chmod +x "$editor" || fail "chmod editor helper failed" + + $TMUX set-option -g editor "$editor" || fail "set editor failed" + $TMUX set-option -g @cm_edit old || fail "set edit option failed" + + printf 'edited' > "$editor_value" || fail "write editor value failed" + open_customize '#{==:#{option_name},@cm_edit}' + send j Right j e + + i=0 + while [ "$i" -lt 50 ]; do + value=$($TMUX show-option -gqv @cm_edit) + [ "$value" = "edited" ] && break + sleep 0.2 + i=$((i + 1)) + done + assert_equals "$value" "edited" "edit option in editor" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User2 display-message old || + fail "set editor key failed" + printf 'display-message edited' > "$editor_value" || + fail "write editor key command failed" + open_customize '#{==:#{key},User2}' + repeat_key j 10 + send Right j Right j e + i=0 + while [ "$i" -lt 50 ]; do + value=$($TMUX list-keys -T prefix -F '#{key_command}' User2) + [ "$value" = "display-message edited" ] && break + sleep 0.2 + i=$((i + 1)) + done + assert_equals "$value" "display-message edited" \ + "edit key command in editor" + + printf 'edited note' > "$editor_value" || + fail "write editor key note failed" + send j e + i=0 + while [ "$i" -lt 50 ]; do + value=$($TMUX list-keys -T prefix -F '#{key_note}' User2) + [ "$value" = "edited note" ] && break + sleep 0.2 + i=$((i + 1)) + done + assert_equals "$value" "edited note" "edit key note in editor" + send q + wait_mode 0 + + $TMUX set-environment -t cm CM_EDIT_ENV old || + fail "set editor environment failed" + printf 'env-edited' > "$editor_value" || + fail "write editor environment failed" + open_customize '#{==:#{environment_name},CM_EDIT_ENV}' + repeat_key j 6 + send Right j e + i=0 + while [ "$i" -lt 50 ]; do + value=$($TMUX show-environment -t cm CM_EDIT_ENV) + [ "$value" = "CM_EDIT_ENV=env-edited" ] && break + sleep 0.2 + i=$((i + 1)) + done + assert_equals "$value" "CM_EDIT_ENV=env-edited" \ + "edit environment in editor" + send q + wait_mode 0 +} + +test_stale_current_items() +{ + start_client + + $TMUX set-option -s @cm_stale_option old || + fail "set stale option failed" + open_customize '#{==:#{option_name},@cm_stale_option}' + send Right j + settle + $TMUX set-option -su @cm_stale_option || + fail "external unset stale option failed" + send Enter + send u y + send d y + settle + assert_alive "stale option current item" + send q + wait_mode 0 + + $TMUX set-environment -t cm CM_STALE_ENV old || + fail "set stale environment failed" + open_customize '#{==:#{environment_name},CM_STALE_ENV}' + repeat_key j 6 + send Right j + settle + $TMUX set-environment -t cm -u CM_STALE_ENV || + fail "external unset stale environment failed" + send Enter + send S + send u y + settle + assert_alive "stale environment current item" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User3 display-message stale || + fail "set stale key failed" + open_customize '#{==:#{key},User3}' + repeat_key j 10 + send Right j + settle + $TMUX unbind-key -T prefix User3 || + fail "external unbind stale key failed" + send s + send d y + send u y + settle + assert_alive "stale key current item" + send q + wait_mode 0 +} + +test_prompt_invalidation() +{ + start_client + + $TMUX set-option -s @cm_prompt_stale old || + fail "set prompt stale option failed" + open_customize '#{==:#{option_name},@cm_prompt_stale}' + send Right j Enter + settle + $TMUX set-option -su @cm_prompt_stale || + fail "external unset prompt option failed" + replace_prompt "new" + assert_empty "$($TMUX show-options -sqv @cm_prompt_stale)" \ + "prompt stale option should not be recreated" + assert_alive "stale option prompt" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User4 display-message old || + fail "set prompt stale key failed" + open_customize '#{==:#{key},User4}' + repeat_key j 10 + send Right j Right j s + settle + $TMUX unbind-key -T prefix User4 || + fail "external unbind prompt key failed" + replace_prompt "display-message new" + $TMUX list-keys -T prefix User4 >/dev/null 2>&1 && + fail "prompt stale key should stay unbound" + assert_alive "stale key prompt" + send q + wait_mode 0 + + $TMUX set-hook -g 'after-new-session[0]' 'display-message old' || + fail "set prompt stale hook array failed" + $TMUX set-hook -g 'after-new-session[1]' 'display-message taken' || + fail "set prompt stale hook array target failed" + open_customize '#{==:#{option_name},after-new-session}' + repeat_key j 3 + send Right j Right j a + settle + replace_prompt "1" + hooks=$($TMUX show-hooks -g after-new-session) + assert_contains "$hooks" "after-new-session[0] display-message old" \ + "array rename to existing key keeps old key" + assert_contains "$hooks" "after-new-session[1] display-message taken" \ + "array rename to existing key keeps target key" + assert_alive "existing array key prompt" + send q + wait_mode 0 +} + +test_editor_invalidation() +{ + start_client + + editor="$TMP/slow-editor.sh" + { + echo '#!/bin/sh' + echo 'sleep 1' + echo 'printf edited > "$1"' + } > "$editor" || fail "write slow editor helper failed" + chmod +x "$editor" || fail "chmod slow editor helper failed" + $TMUX set-option -g editor "$editor" || fail "set slow editor failed" + + $TMUX set-option -g @cm_editor_stale old || + fail "set editor stale option failed" + open_customize '#{==:#{option_name},@cm_editor_stale}' + send j Right j e + settle + $TMUX set-option -gu @cm_editor_stale || + fail "external unset editor option failed" + sleep 2 + assert_empty "$($TMUX show-option -gqv @cm_editor_stale)" \ + "editor stale option should not be recreated" + assert_alive "stale option editor" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User5 display-message old || + fail "set editor stale key failed" + open_customize '#{==:#{key},User5}' + repeat_key j 10 + send Right j Right j e + settle + $TMUX unbind-key -T prefix User5 || + fail "external unbind editor key failed" + sleep 2 + $TMUX list-keys -T prefix User5 >/dev/null 2>&1 && + fail "editor stale key should stay unbound" + assert_alive "stale key editor" + send q + wait_mode 0 + + $TMUX set-environment -t cm CM_EDITOR_STALE old || + fail "set editor stale environment failed" + open_customize '#{==:#{environment_name},CM_EDITOR_STALE}' + repeat_key j 6 + send Right j e + settle + $TMUX set-environment -t cm -u CM_EDITOR_STALE || + fail "external unset editor environment failed" + sleep 2 + assert_equals "$($TMUX show-environment -t cm CM_EDITOR_STALE)" \ + "CM_EDITOR_STALE=edited" "editor stale environment is reset" + assert_alive "stale environment editor" + send q + wait_mode 0 +} + +test_invalid_prompts() +{ + start_client + + open_customize + send Enter + settle + send_literal "bad value" + send Enter + settle + assert_empty "$($TMUX show-options -sqv bad)" "invalid user option name" + assert_alive "invalid user option prompt" + send q + wait_mode 0 + + open_customize + repeat_key j 10 + send Enter + settle + send_literal "NoSuchKey display-message invalid" + send Enter + settle + assert_alive "invalid key prompt" + send q + wait_mode 0 + + $TMUX bind-key -T prefix User6 display-message valid || + fail "set invalid command key failed" + open_customize '#{==:#{key},User6}' + repeat_key j 10 + send Right j Right j s + settle + replace_prompt "not-a-command -Z" + keys=$($TMUX list-keys -T prefix -F '#{key_command}' User6) + assert_equals "$keys" "display-message valid" \ + "invalid key command keeps old command" + assert_alive "invalid key command prompt" + send q + wait_mode 0 +} + +cleanup_servers +test_user_option +test_reset_window_pane_option +test_environment +test_user_hook +test_hook_array +test_changed_only +test_reset_key +test_tagged_unset +test_key_binding +test_editor +test_stale_current_items +test_prompt_invalidation +test_editor_invalidation +test_invalid_prompts +cleanup +exit 0 From 2a75cc22366286980a3d941276103bc349a337ea Mon Sep 17 00:00:00 2001 From: nicm Date: Mon, 13 Jul 2026 21:22:45 +0000 Subject: [PATCH 47/47] Apply dragging change to scroll-down as well as scroll-up. --- window-copy.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/window-copy.c b/window-copy.c index 2213dcebb..3d9194c69 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD: window-copy.c,v 1.416 2026/07/13 16:30:28 nicm Exp $ */ +/* $OpenBSD: window-copy.c,v 1.417 2026/07/13 21:22:45 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -2359,6 +2359,7 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) struct window_mode_entry *wme = cs->wme; struct window_copy_mode_data *data = wme->data; u_int np = wme->prefix; + int dragging; /* * If at the bottom and scroll_exit is active with no selection, @@ -2371,6 +2372,16 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_NOTHING); } + /* With a selection but no active drag, only scroll the view. */ + dragging = (cs->c != NULL && cs->c->tty.mouse_drag_flag != 0); + if (data->screen.sel != NULL && !dragging) { + data->cursordrag = CURSORDRAG_NONE; + data->lineflag = LINE_SEL_NONE; + /* Move down in the history. */ + window_copy_scroll_up(wme, np); + return (WINDOW_COPY_CMD_NOTHING); + } + for (; np != 0; np--) window_copy_cursor_down(wme, 1); if (data->scroll_exit && data->oy == 0 && data->screen.sel == NULL)