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 diff --git a/Makefile.am b/Makefile.am index 84bdbdb2a..2edc7c36d 100644 --- a/Makefile.am +++ b/Makefile.am @@ -161,13 +161,16 @@ dist_tmux_SOURCES = \ control-notify.c \ control.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 \ grid.c \ + hooks.c \ hyperlinks.c \ input-keys.c \ input.c \ @@ -182,14 +185,13 @@ dist_tmux_SOURCES = \ mode-tree.c \ monitor.c \ names.c \ - notify.c \ options-table.c \ options.c \ paste.c \ popup.c \ proc.c \ - prompt.c \ prompt-history.c \ + prompt.c \ regsub.c \ resize.c \ screen-redraw.c \ @@ -204,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/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/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. 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. 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`. diff --git a/alerts.c b/alerts.c index 81c41fa6a..ef4ae4061 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 @@ -205,7 +205,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; @@ -241,7 +241,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; @@ -277,7 +277,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 ee311a013..a4b6a0f35 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 bb19f54ce..39345d059 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 8f7bbb198..c7f574159 100644 --- a/client.c +++ b/client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: client.c,v 1.166 2026/07/10 15:45:11 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..dcc5a6af4 100644 --- a/cmd-break-pane.c +++ b/cmd-break-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-break-pane.c,v 1.73 2026/07/13 13:01:14 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); @@ -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; - char *newname, *cause, *cp; - int idx = target->idx, before; + struct window *w = wl->window, *old_w = w; + char *cause, *cp; + 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) { @@ -151,6 +152,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 +160,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 +175,9 @@ 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__); + 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-capture-pane.c b/cmd-capture-pane.c index c3729cc45..a17ae6243 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 22d4bd503..ba1bcf4ae 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 555e5f1f0..f4c8245c3 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-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 314fc396f..659f3bee0 100644 --- a/cmd-join-pane.c +++ b/cmd-join-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-join-pane.c,v 1.71 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2011 George Nachman @@ -186,7 +186,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); @@ -256,7 +256,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); } @@ -358,7 +358,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); @@ -397,7 +397,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); @@ -458,6 +458,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); @@ -498,11 +505,12 @@ 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 - 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-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 7233b7d27..feed94403 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 844b8b853..d96d6c7d2 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 2a221516d..d608593be 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 @@ -350,74 +350,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); } @@ -425,20 +404,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 a6d57979b..6dd844cdd 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 09e4674f5..08ca351a0 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 @@ -145,7 +145,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-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..335a54760 100644 --- a/cmd-set-option.c +++ b/cmd-set-option.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-set-option.c,v 1.146 2026/07/10 15:20:06 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 = { "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 }, @@ -85,17 +87,63 @@ 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; enum monitor_type type; - int id, scope; + int id, scope, flags = 0; if (args_count(args) > 1) { cmdq_error(item, "too many arguments"); @@ -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,13 @@ 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; + if (args_has(args, 'T')) + flags |= MONITOR_NOTIFY_TRUE; + hooks_monitor_add(item, oo, name, type, id, format, flags, &fs, s); out: free(newvalue); @@ -185,6 +239,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 +253,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 +345,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-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 aa02c203d..bdb433714 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 cd15c80d8..94017a6be 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 @@ -215,7 +215,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 cf5d5ff3b..7c83c467d 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 ee072c055..f56aaac96 100644 --- a/cmd-split-window.c +++ b/cmd-split-window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd-split-window.c,v 1.143 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -33,13 +33,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] " @@ -61,8 +63,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 }, @@ -83,7 +85,9 @@ 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; + 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; @@ -94,14 +98,28 @@ 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 (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')) + 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')) @@ -129,9 +147,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); @@ -157,12 +176,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); @@ -217,7 +231,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); } @@ -232,15 +252,21 @@ 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); } 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; @@ -288,3 +314,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/cmd-swap-pane.c b/cmd-swap-pane.c index 7f5160e65..b2e53f315 100644 --- a/cmd-swap-pane.c +++ b/cmd-swap-pane.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $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,9 +183,13 @@ 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); + 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) - 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 fe7fcb196..30558b31f 100644 --- a/cmd.c +++ b/cmd.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: cmd.c,v 1.187 2026/07/13 09:37:39 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -845,7 +845,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)); @@ -870,7 +870,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++] = '\\'; @@ -879,6 +882,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/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/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/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; diff --git a/configure.ac b/configure.ac index c118800b8..51700a2ad 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/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 a177c8b88..8b4ad3884 100644 --- a/control.c +++ b/control.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: control.c,v 1.61 2026/07/10 15:45:11 nicm Exp $ */ /* * Copyright (c) 2012 Nicholas Marriott diff --git a/environ.c b/environ.c index 19f9c327e..f2a60e258 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 0e10b37a4..2eed63403 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 5f1a13452..56745f638 100644 --- a/format.c +++ b/format.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: format.c,v 1.403 2026/07/10 15:20:06 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -2201,6 +2201,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) @@ -3517,6 +3617,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 }, @@ -3568,6 +3683,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/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 99d5371a9..d753e03f1 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..fd55c7273 --- /dev/null +++ b/hooks.c @@ -0,0 +1,461 @@ +/* $OpenBSD: hooks.c,v 1.13 2026/07/10 15:20:06 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, + int flags, 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, flags); +} + +/* 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 1ea55f3b1..27af5c4f0 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 39692232c..4cade4e53 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 a85b555f8..0aac9bfbb 100644 --- a/input.c +++ b/input.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: input.c,v 1.268 2026/07/13 15:03:03 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; @@ -1017,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); } @@ -1032,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. */ @@ -1297,8 +1320,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 +2205,7 @@ input_csi_dispatch_winops(struct input_ctx *ictx) screen_pop_title(sctx->s); if (wp == NULL) break; - notify_pane("pane-title-changed", wp); + input_fire_pane_title_changed(wp, sctx->s->title); server_redraw_window_borders(w); server_status_window(w); break; @@ -2700,7 +2725,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); } @@ -2778,7 +2803,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); } @@ -3146,7 +3171,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; } @@ -3166,24 +3192,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; } } @@ -3289,7 +3383,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 72dbec95c..d665ede9f 100644 --- a/job.c +++ b/job.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: job.c,v 1.75 2026/07/12 20:35:52 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -147,7 +147,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); @@ -155,21 +155,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); } @@ -183,11 +183,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); } } diff --git a/key-bindings.c b/key-bindings.c index c73fd7c33..f90307fc1 100644 --- a/key-bindings.c +++ b/key-bindings.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $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}\"}" \ @@ -496,10 +499,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. */ @@ -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/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 e8a1be7fd..5dba09f11 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 @@ -368,7 +368,7 @@ layout_parse(struct window *w, const char *layout, int flags, char **cause) recalculate_sizes(); layout_print_cell(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..65cb38918 100644 --- a/layout-set.c +++ b/layout-set.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $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) { @@ -183,7 +198,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); } @@ -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); @@ -292,7 +309,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); } @@ -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); @@ -389,7 +408,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); } @@ -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); @@ -486,7 +507,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); } @@ -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); @@ -584,7 +607,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); } @@ -700,12 +723,13 @@ layout_set_tiled(struct window *w) w->sy - used); } + layout_set_link_floating(w, lcroot); layout_fix_offsets(w); layout_fix_panes(w, NULL); 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 060f6aea4..8efdce5ba 100644 --- a/layout.c +++ b/layout.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: layout.c,v 1.94 2026/07/13 10:03:27 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -956,7 +956,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. */ @@ -1501,7 +1501,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. */ @@ -1597,7 +1597,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; @@ -1607,12 +1607,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 @@ -1640,12 +1639,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"); @@ -1655,17 +1649,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); } @@ -1765,6 +1766,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/log.c b/log.c index 0e0d1d1a1..966675679 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-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..d6338e878 100644 --- a/monitor.c +++ b/monitor.c @@ -1,7 +1,7 @@ -/* $OpenBSD$ */ +/* $OpenBSD: monitor.c,v 1.6 2026/07/10 15:20:06 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 @@ -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/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 07656ccff..000000000 --- a/notify.c +++ /dev/null @@ -1,515 +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 "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 afcfea18a..7fbeac5c8 100644 --- a/options-table.c +++ b/options-table.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: options-table.c,v 1.236 2026/07/13 19:32:28 nicm Exp $ */ /* * Copyright (c) 2011 Nicholas Marriott @@ -239,33 +239,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" }, @@ -1881,74 +1888,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-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_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-set-clipboard", ""), - OPTIONS_TABLE_PANE_HOOK("pane-title-changed", ""), - OPTIONS_TABLE_HOOK("session-closed", ""), - OPTIONS_TABLE_HOOK("session-created", ""), - OPTIONS_TABLE_HOOK("session-renamed", ""), - OPTIONS_TABLE_HOOK("session-window-changed", ""), - 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_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/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/osdep-openbsd.c b/osdep-openbsd.c index b4c8fc56b..fbcd329b9 100644 --- a/osdep-openbsd.c +++ b/osdep-openbsd.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/paste.c b/paste.c index 5ee80afd1..15e8cccc1 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 @@ -43,6 +43,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) { @@ -137,7 +147,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); @@ -195,7 +205,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. */ @@ -253,8 +263,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); } @@ -311,7 +321,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); } @@ -324,7 +334,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 6cb45951f..fad0eb498 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 b3a0afb25..7da1b430c 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/prompt-history.c b/prompt-history.c index ac99bdb95..109043aa7 100644 --- a/prompt-history.c +++ b/prompt-history.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $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); } diff --git a/prompt.c b/prompt.c index 1ab447ebe..c12a249a9 100644 --- a/prompt.c +++ b/prompt.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $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/regress/alerts.sh b/regress/alerts.sh index cfb84c711..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 } @@ -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 10 || + fail "set monitor-silence silreset failed" +sleep 3 +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. 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/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/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 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 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..6eb6ae49e 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,21 +237,223 @@ $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" +$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-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" +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 -g @cl 0 || fail "set @cl failed" +$TMUX set-hook -g client-created \ + '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 "#{?#{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" $TMUX set-hook -g client-detached 'set -gF @a "#{hook}"' || @@ -191,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 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 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/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/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 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 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. 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 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..1d83e4b70 100644 --- a/resize.c +++ b/resize.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $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. */ @@ -60,8 +78,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); + resize_fire_window_resized(w, old_sx, old_sy); w->flags &= ~WINDOW_RESIZE; } diff --git a/screen-redraw.c b/screen-redraw.c index b944bfe72..fd67041f7 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 c198ea226..a311296e7 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 62a079270..6b539b382 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 @@ -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 } 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 0473e2b59..d08bfd17d 100644 --- a/server-client.c +++ b/server-client.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: server-client.c,v 1.489 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2009 Nicholas Marriott @@ -403,6 +403,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) @@ -429,7 +485,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); + server_client_fire_session_changed(c, old); server_redraw_client(c); } @@ -464,8 +520,10 @@ 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->name != NULL && (c->flags & (CLIENT_CONTROL|CLIENT_TERMINAL))) + events_fire_client("client-closed", c); if (c->flags & CLIENT_CONTROL) control_stop(c); @@ -1111,6 +1169,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. */ @@ -1284,7 +1346,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. */ @@ -1750,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); } @@ -2421,6 +2484,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; @@ -2459,6 +2523,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(); @@ -2468,7 +2534,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); + server_client_fire_resized(c, old_sx, old_sy); break; case MSG_EXITING: if (datalen != 0) @@ -2775,6 +2841,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) && @@ -3090,10 +3158,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 8c7d0e947..9bf48b162 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 @@ -29,6 +29,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) @@ -202,6 +227,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; @@ -219,6 +245,7 @@ server_kill_window(struct window *w, int renumber) server_renumber_session(s); } recalculate_sizes(); + window_remove_ref(w, __func__); } void @@ -272,8 +299,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); @@ -351,7 +377,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') { @@ -374,7 +400,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 7780ff519..4142272ae 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 @@ -214,6 +214,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 445897e2a..d52b70b88 100644 --- a/session.c +++ b/session.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: session.c,v 1.106 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -206,7 +206,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); @@ -219,7 +219,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); } @@ -330,7 +330,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); @@ -346,7 +346,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); @@ -471,6 +471,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) @@ -493,7 +516,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); + session_fire_window_changed(s, wl, old); return (0); } @@ -540,12 +563,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. */ @@ -556,6 +600,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); @@ -657,7 +702,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; } @@ -683,7 +728,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 af6e03d5c..f11e2e2a0 100644 --- a/spawn.c +++ b/spawn.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: spawn.c,v 1.48 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2019 Nicholas Marriott @@ -71,6 +71,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) { @@ -131,7 +170,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); @@ -190,8 +229,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) - notify_session_window("window-linked", s, w); + 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); @@ -519,8 +560,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); @@ -531,7 +572,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 dfca7fe34..421f14a4b 100644 --- a/tmux.1 +++ b/tmux.1 @@ -1,4 +1,4 @@ -.\" $OpenBSD$ +.\" $OpenBSD: tmux.1,v 1.1137 2026/07/13 16:30:28 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 13 2026 $ .Dt TMUX 1 .Os .Sh NAME @@ -2160,6 +2160,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) @@ -3616,7 +3629,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 @@ -3662,6 +3675,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 @@ -4077,6 +4095,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 @@ -6325,11 +6358,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. @@ -6347,12 +6376,16 @@ 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 +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 @@ -6363,6 +6396,18 @@ 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\-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. +.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 Run when the program running in a pane exits, but .Ic remain\-on\-exit @@ -6377,20 +6422,50 @@ 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\-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 +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\-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 -Run when a session closed. +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 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 @@ -6398,6 +6473,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 @@ -6405,13 +6484,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 agpERTuw .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 @@ -6434,6 +6515,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 @@ -6452,6 +6538,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 @@ -7096,6 +7190,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" @@ -7161,6 +7256,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" @@ -7178,6 +7278,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" @@ -7208,6 +7310,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" @@ -8562,8 +8665,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 @@ -8576,6 +8681,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 3739b6008..2942d3788 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 78ec6de10..58f61be7e 100644 --- a/tmux.h +++ b/tmux.h @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: tmux.h,v 1.1401 2026/07/13 16:07:47 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -49,6 +49,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; @@ -1298,6 +1301,8 @@ struct window_pane { #define PANE_UNSEENCHANGES 0x4000 #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; @@ -1320,6 +1325,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; @@ -1769,6 +1780,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; @@ -2117,6 +2130,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; @@ -2331,6 +2346,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; @@ -2343,6 +2359,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; @@ -2468,6 +2501,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. */ @@ -2652,29 +2687,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); + 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 *); /* options.c */ struct options *options_create(struct options *); @@ -3593,6 +3677,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 *, @@ -3763,10 +3849,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 *); @@ -3883,7 +3972,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 */ @@ -3907,20 +3996,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 b1f8032d6..656100ba9 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 c248aa840..164b9f977 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 03dabb335..a0d212732 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 e57100fd2..887d2b0f9 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 639cb5ede..3e0d3df62 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 ace160a8b..9853b2abb 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..3d9194c69 100644 --- a/window-copy.c +++ b/window-copy.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window-copy.c,v 1.417 2026/07/13 21:22:45 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 *); @@ -103,8 +105,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); @@ -292,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 */ @@ -862,7 +868,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); } @@ -1113,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); @@ -1648,7 +1657,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); } @@ -2350,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, @@ -2361,6 +2371,17 @@ window_copy_cmd_scroll_down(struct window_copy_cmd_state *cs) return (WINDOW_COPY_CMD_CANCEL); 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) @@ -2374,6 +2395,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 +2420,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 +2428,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,11 +3161,33 @@ 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); } +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; @@ -3386,6 +3452,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, @@ -5038,10 +5122,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 @@ -5127,14 +5215,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); } @@ -5504,6 +5611,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 +5726,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) @@ -5764,7 +5970,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 +6055,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); @@ -6578,7 +6784,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 +6855,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 +6944,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 +6965,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 +6990,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 +7074,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); } diff --git a/window-customize.c b/window-customize.c index 09da37f2e..ba1c060cd 100644 --- a/window-customize.c +++ b/window-customize.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $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 { @@ -81,14 +87,28 @@ enum window_customize_change { WINDOW_CUSTOMIZE_RESET, }; +enum window_customize_option_type { + WINDOW_CUSTOMIZE_OPTIONS, + 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; }; @@ -144,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) @@ -161,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)); } @@ -198,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: @@ -253,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)); @@ -274,7 +317,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 +326,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 +356,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 +368,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); @@ -324,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); @@ -376,7 +446,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 +471,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 +485,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 +509,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); } } @@ -464,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) { @@ -481,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; @@ -525,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, @@ -550,28 +698,50 @@ 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, 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); + 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(); @@ -662,10 +832,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 +848,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 +857,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 +870,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 +887,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 +936,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) { @@ -846,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) @@ -856,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); } @@ -909,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); } @@ -1054,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, @@ -1091,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: @@ -1125,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) @@ -1165,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); @@ -1246,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); @@ -1260,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) @@ -1384,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; @@ -1400,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; @@ -1460,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); } @@ -1491,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); @@ -1551,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; @@ -1559,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); @@ -1568,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); @@ -1576,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++; 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 98898e01a..35b7e68e0 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 298491604..70cd9a559 100644 --- a/window.c +++ b/window.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $OpenBSD: window.c,v 1.361 2026/07/13 13:01:14 nicm Exp $ */ /* * Copyright (c) 2007 Nicholas Marriott @@ -73,8 +73,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); @@ -86,6 +86,7 @@ struct window_pane_prompt { status_prompt_input_cb inputcb; prompt_free_cb freecb; void *data; + enum prompt_type type; }; int @@ -94,6 +95,101 @@ 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); +} + +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) +{ + 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) { @@ -415,6 +511,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); @@ -444,13 +543,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); } } @@ -560,13 +661,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); @@ -602,7 +703,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); } @@ -792,7 +893,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); @@ -818,8 +920,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); @@ -901,7 +1005,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); + window_fire_pane_changed(w, w->active, wp); window_update_focus(w); } } @@ -1106,6 +1210,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); @@ -1230,9 +1335,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); @@ -1363,6 +1468,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; @@ -1385,6 +1492,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 @@ -1394,9 +1512,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) @@ -1422,7 +1544,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); } @@ -1433,11 +1557,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); @@ -1454,13 +1580,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); @@ -1523,6 +1652,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); @@ -1540,19 +1670,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..c6a2f6f13 100644 --- a/xmalloc.c +++ b/xmalloc.c @@ -1,4 +1,4 @@ -/* $OpenBSD$ */ +/* $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; } diff --git a/xmalloc.h b/xmalloc.h index a87f053bf..f64d3f734 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