Compare commits

..
Author SHA1 Message Date
Nicholas Marriott e621d088b4 Merge branch 'master' into redraw-damage-rectangles-new 2026-09-25 10:11:51 +01:00
Nicholas Marriott ef881f318c Add --with-socket-path to configure. GitHub issue 5635 from Alexandre Fiori. 2026-09-25 10:07:39 +01:00
Nicholas Marriott 545ba99f8c Tests for 5636. 2026-09-25 09:46:21 +01:00
Nicholas Marriott 8cafc7633e Merge pull request #5633 from arthurscchan/new-fuzzer-2
OSS-Fuzz: Add new fuzzer targets json processing
2026-09-25 07:58:11 +01:00
tmux update bot 46fd3c16a3 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Keep the scroll position in tree mode when the list is rebuilt, GitHub issue 5631 from Kim A Brandt.
2026-09-25 06:40:35 +00:00
nicm 4ae837de68 Keep the scroll position in tree mode when the list is rebuilt, GitHub
issue 5631 from Kim A Brandt.
2026-09-25 06:40:33 +00:00
Arthur Chan 2888c6751c Prepare dict and options
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-09-25 06:20:49 +01:00
Arthur Chan 041f30acbe OSS-Fuzz: Add new fuzzer targets json processing
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-09-25 05:58:37 +01:00
Nicholas Marriott 4f523f41c9 Update CHANGES. 2026-09-24 20:49:41 +01:00
Nicholas Marriott 5a0440b4f8 Test for 5631. 2026-09-24 19:59:45 +01:00
tmux update bot 51593fc9bc Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  As well as tracking modified (dirty) lines during a synchronized update, also track scrolled lines. This means they can be scrolled as one instead of forcing each line to be redrawn - much less expensive for a very common operation. From Ben Maurer in GitHub issue 5611.
2026-09-24 12:27:53 +00:00
nicm 7fc7f1bf69 As well as tracking modified (dirty) lines during a synchronized update,
also track scrolled lines. This means they can be scrolled as one
instead of forcing each line to be redrawn - much less expensive for a
very common operation. From Ben Maurer in GitHub issue 5611.
2026-09-24 12:27:51 +00:00
Nicholas Marriott 29e6b80286 Tests for SU scrolling. 2026-09-24 12:20:02 +01:00
Nicholas Marriott 6973c6b221 Damage rectanges. 2026-09-24 11:49:24 +01:00
tmux update bot a4a2501b2f Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add margins for WezTerm and ghostty, from Michael Grant.
2026-09-24 09:44:56 +00:00
nicm aa4123ab58 Add margins for WezTerm and ghostty, from Michael Grant. 2026-09-24 09:44:54 +00:00
Nicholas Marriott 6bab150c58 Fix test. 2026-09-24 09:15:21 +01:00
tmux update bot 72e83b000b Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Note about default-shell, from Meriel Sartha Mittelbach.
2026-09-23 16:48:10 +00:00
nicm a4c051f27d Note about default-shell, from Meriel Sartha Mittelbach. 2026-09-23 16:48:08 +00:00
Nicholas Marriott 56373a08ee Merge pull request #5628 from arthurscchan/new-fuzzer
OSS-Fuzz: Add new fuzzer targets layout processing
2026-09-23 12:03:00 +01:00
tmux update bot 5e860c4e6c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Get x and y the right way round, fixes crash when opening a mode prompt when the client is offset into the window; may be the crash reported by sthen@.
2026-09-22 20:02:41 +00:00
nicm 183c3432b0 Get x and y the right way round, fixes crash when opening a mode prompt
when the client is offset into the window; may be the crash reported by
sthen@.
2026-09-22 20:02:40 +00:00
tmux update bot d1ee699dd1 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Some terminals have extended DECSCUSR to allow it to reset to the default style and put that as the Se capability in their terminfo entry. Change so that if Ss and Se exist, tmux does not overwrite them with its own. GitHub issue 5476.
2026-09-22 16:48:21 +00:00
nicm e7618f41c2 Some terminals have extended DECSCUSR to allow it to reset to the
default style and put that as the Se capability in their terminfo entry.
Change so that if Ss and Se exist, tmux does not overwrite them with its
own. GitHub issue 5476.
2026-09-22 16:48:18 +00:00
Arthur Chan be75a67b1f OSS-Fuzz: Add new fuzzer targets layout processing
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-09-22 16:45:24 +01:00
Nicholas Marriott 83c8962cf9 Some tests. 2026-09-22 14:25:51 +01:00
tmux update bot fca57a5fc9 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a feature for mintty's application escape to avoid dodgy terminals whinging about not supporting it, GitHub issue 5626 from Pete Dietl.
  Only rely on the previous line wrapping when the cursor is actually on it, GitHub issue 5625 from Ben Maurer.
  Correctly write Z indexes to v2 layouts if zoomed, GitHub issue 5624.
  Do not leak waiting clients (in wait-for) if they are killed, GitHub issue 5614.
2026-09-22 11:58:43 +00:00
nicm 83f62d0acc Add a feature for mintty's application escape to avoid dodgy terminals
whinging about not supporting it, GitHub issue 5626 from Pete Dietl.
2026-09-22 11:58:41 +00:00
nicm 19b7660778 Only rely on the previous line wrapping when the cursor is actually on
it, GitHub issue 5625 from Ben Maurer.
2026-09-22 11:58:41 +00:00
nicm 4624bc2129 Correctly write Z indexes to v2 layouts if zoomed, GitHub issue 5624. 2026-09-22 11:58:41 +00:00
nicm 0e1f6ed80d Do not leak waiting clients (in wait-for) if they are killed, GitHub
issue 5614.
2026-09-22 11:58:41 +00:00
Nicholas Marriott 21ea1c91c5 Merge pull request #5627 from pdietl/regress-input-keys-race
Wait for the pane to echo the key before capturing in input-keys.sh
2026-09-22 07:55:13 +01:00
Nicholas Marriott cb9f9b588f Fix test. 2026-09-22 07:52:51 +01:00
Pete Dietl d6db719b19 Wait for the pane to echo the key before capturing in input-keys.sh 2026-09-22 00:21:23 -05:00
tmux update bot 9860e60afa Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not cancel mark when click on border.
2026-09-21 22:31:27 +00:00
nicm ca5c28d224 Do not cancel mark when click on border. 2026-09-21 22:31:26 +00:00
Nicholas Marriott c3326194b6 Turn on Linux arm also. 2026-09-21 22:34:05 +01:00
Nicholas Marriott 6a8c21fb41 macOS parallel issues. 2026-09-21 22:27:43 +01:00
Nicholas Marriott 326b9a4f16 Another timing race. 2026-09-21 22:18:44 +01:00
Nicholas Marriott 6871fcb157 Add missing test cleanup. 2026-09-21 22:16:06 +01:00
Nicholas Marriott 1d9943c6e6 Keep window alive. 2026-09-21 22:12:33 +01:00
Nicholas Marriott f5c4998db3 Make regress parallel friendly. 2026-09-21 22:00:47 +01:00
Nicholas Marriott cc81429d8a Do not make test so timing sensitive. 2026-09-21 21:51:30 +01:00
tmux update bot 3a6c2e7877 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not do anything in session_detach and rely on the caller destroying the session, otherwise grouped sessions can reuse a session with no windows. From Jeong, Heon in GitHub issue 5620.
2026-09-21 18:56:57 +00:00
nicm 21b3da3bab Do not do anything in session_detach and rely on the caller destroying
the session, otherwise grouped sessions can reuse a session with no
windows. From Jeong, Heon in GitHub issue 5620.
2026-09-21 18:56:55 +00:00
tmux update bot a7bd2415ba Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add support for borderless menus, like panes. GitHub issue 5447 from harikp2002 at gmail dot com.
  Do not trim all lines to make a zero line grid which reflow does not like, from Kaixuan Li.
  Do not unzoom when resizing a floating pane that was created with -A. Similarly, skip hidden floating panes when changing Z order. Reported by Clark Wang.
2026-09-21 13:44:41 +00:00
nicm dda0e4d489 Add support for borderless menus, like panes. GitHub issue 5447 from harikp2002
at gmail dot com.
2026-09-21 13:44:39 +00:00
nicm e361b8f8ff Do not trim all lines to make a zero line grid which reflow does not like, from
Kaixuan Li.
2026-09-21 13:44:39 +00:00
nicm 9be3a35178 Do not unzoom when resizing a floating pane that was created with -A.
Similarly, skip hidden floating panes when changing Z order. Reported by Clark
Wang.
2026-09-21 13:44:39 +00:00
Nicholas Marriott 324c636c5c Test for 5447 and for copy-mode zero line crash. 2026-09-21 13:14:44 +01:00
Nicholas Marriott 166851bf87 Update tests and bits after popups removed. 2026-09-21 12:05:37 +01:00
Nicholas Marriott 5aa17a0cfa Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Remove popups and all the associated overlay machinery (they were the last user of it). display-popup stays but becomes an (undocumented) compatibility command to open a floating pane.
  Fix typo (too few 0s) in CLIENT_CONTROL_DISCARD, from someone in GitHub issue 5622.
2026-09-21 12:03:10 +01:00
nicm 34cd5da4e3 Remove popups and all the associated overlay machinery (they were the last user
of it). display-popup stays but becomes an (undocumented) compatibility command
to open a floating pane.
2026-09-21 11:01:12 +00:00
nicm 3d5f946f35 Fix typo (too few 0s) in CLIENT_CONTROL_DISCARD, from someone in GitHub issue
5622.
2026-09-21 11:01:12 +00:00
Nicholas Marriott 81794f3047 Test for newp -A and resize. 2026-09-21 11:39:09 +01:00
Thomas Adam c612f35fd8 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  build tmux with debug symbols, ok claudio nicm
  If no floating panes, reset default starting position, and reset if any part of the pane goes outside the window.
  Empty string for invalid old-style formats causes old iTerm2 versions to crash, so emit "0000," instead.
  Fix session_*_flag format variables which loop over the windows (they should only be false if all windows do not have the flag, not the first one). GitHub issue 5599.
  Do not loop forever if someone tries to give WCHAR_MAX a width, GitHub issue 5602.
  Expand -c for run-shell like the other -c flags, reported by Saúl Nogueras.
  Reset layout manually instead of calling window_unzoom which can go down the notification path and end up double freeing the pane (this was previously removed in 2015 but added back to fix a problem with late destroy - this is a better fix). GitHub issue 5591 from Romain Francoise.
2026-09-20 20:05:57 +01:00
sthen 56b36d671e build tmux with debug symbols, ok claudio nicm 2026-09-20 12:06:27 +00:00
nicm eee95e6479 If no floating panes, reset default starting position, and reset if any part of
the pane goes outside the window.
2026-09-20 12:06:27 +00:00
nicm bbd00148de Empty string for invalid old-style formats causes old iTerm2 versions to crash,
so emit "0000," instead.
2026-09-20 12:06:27 +00:00
nicm 2e9189c01d Fix session_*_flag format variables which loop over the windows (they should
only be false if all windows do not have the flag, not the first one). GitHub
issue 5599.
2026-09-20 12:06:27 +00:00
nicm d4dc032599 Do not loop forever if someone tries to give WCHAR_MAX a width, GitHub issue
5602.
2026-09-20 12:06:27 +00:00
nicm d1d07f9790 Expand -c for run-shell like the other -c flags, reported by Saúl Nogueras. 2026-09-20 12:06:27 +00:00
nicm b25af0a08c Reset layout manually instead of calling window_unzoom which can go down the
notification path and end up double freeing the pane (this was previously
removed in 2015 but added back to fix a problem with late destroy - this is a
better fix). GitHub issue 5591 from Romain Francoise.
2026-09-20 12:06:27 +00:00
Nicholas Marriott 1eed85c344 Change tests for 0000, for invalid layouts. 2026-09-20 09:37:59 +01:00
Nicholas Marriott 73042686d8 Test for 5599. 2026-09-20 09:19:35 +01:00
Nicholas Marriott 313ad6fdc5 Tests for 5602 and for run-shell -c. 2026-09-20 09:11:24 +01:00
Nicholas Marriott 594465fbe7 Regress test for 5591 and another one I forget. 2026-09-20 08:44:56 +01:00
tmux update bot e880cf63e0 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add rounded borders option for panes like popups.
2026-09-11 17:00:20 +00:00
nicm 325b40e8c4 Add rounded borders option for panes like popups. 2026-09-11 17:00:18 +00:00
tmux update bot f95363aa83 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Rather than allowing floating panes to remain outside the window and invisible after resize, move them and resize them so they are fully inside the window. GitHub issue 5582 from Noam Stolero.
2026-09-11 12:58:31 +00:00
nicm f8fc53b6f8 Rather than allowing floating panes to remain outside the window and
invisible after resize, move them and resize them so they are fully
inside the window. GitHub issue 5582 from Noam Stolero.
2026-09-11 12:58:29 +00:00
Nicholas Marriott d215280b00 Add rounded border tests. 2026-09-11 11:17:35 +01:00
Nicholas Marriott d859a0f410 Test for resizing floating panes, from Noam Stolero. 2026-09-11 09:16:55 +01:00
Nicholas Marriott 10d00cc7cb Modal pane test changes. 2026-09-11 09:10:14 +01:00
Nicholas Marriott 14a01dd337 Note changes from Heon Jeong. 2026-09-11 09:05:06 +01:00
tmux update bot a958883625 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add remain-on-exit failed-key and a -D flag to new-pane to have a modal pane wait for Escape/C-c. Both to allow better compatibility with popups.
2026-09-10 13:04:13 +00:00
nicm d202aa7ac1 Add remain-on-exit failed-key and a -D flag to new-pane to have a modal
pane wait for Escape/C-c. Both to allow better compatibility with
popups.
2026-09-10 13:04:11 +00:00
Nicholas Marriott d9a5e82678 Fix tests. 2026-09-10 12:00:01 +01:00
tmux update bot 13c10f672c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a nesting limit for v1 layouts, reported by M Khalilov, fix based on issue 5572 from Afonso Januário. Also tweak some language while here.
2026-09-09 19:23:09 +00:00
nicm b7c5030004 Add a nesting limit for v1 layouts, reported by M Khalilov, fix based on
issue 5572 from Afonso Januário. Also tweak some language while here.
2026-09-09 19:23:07 +00:00
Nicholas Marriott 4839123510 Tweak changes. 2026-09-09 13:50:42 +01:00
Nicholas Marriott 9b3268a2a0 Bump version. 2026-09-09 13:28:07 +01:00
Nicholas Marriott 5aeacf1cae Update CHANGES. 2026-09-09 13:06:56 +01:00
tmux update bot 7774731b95 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Draw bottom border with pane status line at the top.
  Bound the memory used by buffered control mode command replies (to 64 MB), GitHub issue 5565 from kagari dot shusei at proton dot me.
  Do not adjust prompt position on invalid Unicode, from Kaixuan Li.
2026-09-09 10:12:52 +00:00
nicm 0544f0b210 Draw bottom border with pane status line at the top. 2026-09-09 10:12:50 +00:00
nicm a3129249d1 Bound the memory used by buffered control mode command replies (to 64
MB), GitHub issue 5565 from kagari dot shusei at proton dot me.
2026-09-09 10:12:50 +00:00
nicm 930c81b819 Do not adjust prompt position on invalid Unicode, from Kaixuan Li. 2026-09-09 10:12:50 +00:00
Nicholas Marriott fc695b4e86 Test for v1 nesting limit, based on a changed from Afonso Januário. 2026-09-09 10:01:32 +01:00
Nicholas Marriott 36fb7ab084 Tests of border. 2026-09-09 09:32:14 +01:00
tmux update bot 90a7dd4654 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add new layout format which includes floating panes. The new format is now a JSON subset which is less fragile and easier to handle than the old custom format. The old (version 1) format is still supported for control mode clients for now - they must set the new-layouts flag to receive the new format. From Dane Jensen.
2026-09-09 08:27:51 +00:00
nicm bf43fdc0c7 Add new layout format which includes floating panes. The new format is
now a JSON subset which is less fragile and easier to handle than the
old custom format. The old (version 1) format is still supported for
control mode clients for now - they must set the new-layouts flag to
receive the new format. From Dane Jensen.
2026-09-09 08:27:49 +00:00
Nicholas Marriott 077d6e53cd Test for invalid prompt. 2026-09-09 08:53:51 +01:00
Nicholas Marriott 4e19f7a8dc New layout format regress test, from Dane Jensen. 2026-09-09 08:04:23 +01:00
tmux update bot 7941f7b863 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  The client for the layout format is the client we are sending it to, not the target client.
2026-09-08 21:40:59 +00:00
nicm c7102ebbe5 The client for the layout format is the client we are sending it to, not
the target client.
2026-09-08 21:40:57 +00:00
tmux update bot 1b87cb752e Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add new-layouts flag for control clients so they can default to old layouts for backwards compatibility.
2026-09-08 11:38:06 +00:00
nicm d9692f7ecf Add new-layouts flag for control clients so they can default to old
layouts for backwards compatibility.
2026-09-08 11:38:04 +00:00
Nicholas Marriott 98637576bd Test for new-layouts flag. 2026-09-08 11:27:25 +01:00
Nicholas Marriott d8bdb23108 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a function to find last pane index, from Dane Jensen.
  Add a parser for a subset of JSON, will be used for new layout strings (and maybe some other stuff), from Dane Jensen.
  Add capture-pane -I to show times.
2026-09-08 11:25:17 +01:00
nicm 2191c9b172 Add a function to find last pane index, from Dane Jensen. 2026-09-08 10:22:40 +00:00
nicm 01d1d6580a Add a parser for a subset of JSON, will be used for new layout strings
(and maybe some other stuff), from Dane Jensen.
2026-09-08 10:22:40 +00:00
nicm 96f4c7b9fa Add capture-pane -I to show times. 2026-09-08 10:22:40 +00:00
Nicholas Marriott 6a24d5437b JSON test. 2026-09-08 09:36:26 +01:00
Nicholas Marriott 8396a63128 capture-pane -I test. 2026-09-08 08:32:05 +01:00
tmux update bot d44bfda26d Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Error on invalid relative targets, such as +foo or -0. GitHub issue 5576 from imcusg at gmail dot com.
  Do not silently make a session monitor if the target is unknown, GitHub issue 5575 from zzchun12826 at gmail dot com.
2026-09-07 17:56:25 +00:00
nicm 00f3899aea Error on invalid relative targets, such as +foo or -0. GitHub issue 5576
from imcusg at gmail dot com.
2026-09-07 17:56:23 +00:00
nicm 557967c36c Do not silently make a session monitor if the target is unknown, GitHub
issue 5575 from zzchun12826 at gmail dot com.
2026-09-07 17:56:23 +00:00
Nicholas Marriott 73db0a54e5 Tests from GitHub issue 5576. 2026-09-07 13:05:38 +01:00
tmux update bot 578e07fcbc Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not attempt to write to control clients which have been destroyed, reported by Artur Penttinen in GitHub issue 5570.
2026-09-04 06:00:37 +00:00
nicm 93169e2e62 Do not attempt to write to control clients which have been destroyed,
reported by Artur Penttinen in GitHub issue 5570.
2026-09-04 06:00:36 +00:00
tmux update bot db24c029be Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Extend neww -S to work with -t as well as -t, GitHub issue 5571 from Mazunki Hoksaas.
2026-09-03 23:28:11 +00:00
nicm a6f9a206b7 Extend neww -S to work with -t as well as -t, GitHub issue 5571 from
Mazunki Hoksaas.
2026-09-03 23:28:09 +00:00
Nicholas Marriott ce253864f8 Test for late run-shell on a client that has exited. 2026-09-03 22:39:09 +01:00
tmux update bot 79f1f43830 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Wait for pane exit status in all cases not just remain-on-exit since events need it.
2026-09-03 21:24:20 +00:00
nicm 7b666e62be Wait for pane exit status in all cases not just remain-on-exit since
events need it.
2026-09-03 21:24:19 +00:00
Nicholas Marriott 5d5f66239a Test for -S from Mazunki Hoksaas. 2026-09-03 22:04:34 +01:00
Nicholas Marriott 4b1586fa81 Test for exit status. 2026-09-03 20:12:44 +01:00
Nicholas Marriott 9fa390aee6 Fix error in test. 2026-09-02 12:32:29 +01:00
Nicholas Marriott ad632726bd Some more regress tests. 2026-09-02 11:16:47 +01:00
Nicholas Marriott 1dd1717462 Fix tests for macOS. 2026-09-02 08:43:21 +01:00
tmux update bot 73db721dab Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Create screen before zoom may need to use it.
2026-09-02 06:15:13 +00:00
nicm 6bb6c286a9 Create screen before zoom may need to use it. 2026-09-02 06:15:11 +00:00
tmux update bot ca34d190b5 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not allow cursor on/off to escape synchronized updates.
2026-09-01 23:20:22 +00:00
nicm 57a13664cc Do not allow cursor on/off to escape synchronized updates. 2026-09-01 23:20:20 +00:00
Nicholas Marriott 59dc0e75c4 Add a couple more tests. 2026-09-01 22:07:08 +01:00
Nicholas Marriott 2d6aed1d92 Add some more tests for recent stuff. 2026-09-01 21:55:50 +01:00
Nicholas Marriott 3dd575ce55 Update CHANGES. 2026-09-01 21:48:30 +01:00
Nicholas Marriott 60e871658f Remove +x. 2026-09-01 20:52:44 +01:00
Nicholas Marriott c49dfdf496 Regress test for cursor updates. 2026-09-01 20:51:20 +01:00
196 changed files with 14545 additions and 6157 deletions
+5 -5
View File
@@ -27,10 +27,10 @@ jobs:
runner: ubuntu-24.04 runner: ubuntu-24.04
make: make make: make
configure: --enable-utf8proc --enable-asan configure: --enable-utf8proc --enable-asan
#- name: ubuntu-24.04-arm64 - name: ubuntu-24.04-arm64
# runner: ubuntu-24.04-arm runner: ubuntu-24.04-arm
# make: make make: make
# configure: --enable-utf8proc --enable-asan configure: --enable-utf8proc --enable-asan
- name: macos-26-arm64 - name: macos-26-arm64
runner: macos-26 runner: macos-26
make: gmake make: gmake
@@ -79,7 +79,7 @@ jobs:
- name: test - name: test
run: | run: |
cd regress cd regress
${{ matrix.make }} ${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)"
- name: logs - name: logs
if: failure() if: failure()
+85 -12
View File
@@ -9,7 +9,12 @@ CHANGES FROM 3.7c TO 3.8
- modal panes may be created with new-pane -O; a window can have one modal - modal panes may be created with new-pane -O; a window can have one modal
pane and it prevents interaction with the other panes while it is active. pane and it prevents interaction with the other panes while it is active.
A modal pane is now used for the editor in buffer mode; -C closes the pane when the mouse is clicked outside it and -K sends all
keys, including the prefix key, to the pane. -D closes the modal pane with
Escape or C-c. A modal pane is now used for the editor in buffer mode;
- new-pane -A creates a floating pane which remains above a zoomed pane;
modal panes do this by default;
- move-pane and resize-pane can now move and resize floating panes, including - move-pane and resize-pane can now move and resize floating panes, including
mouse dragging; mouse dragging;
@@ -24,10 +29,18 @@ CHANGES FROM 3.7c TO 3.8
-k for choose commands kills the pane when the mode exits, which is useful -k for choose commands kills the pane when the mode exits, which is useful
for modes in floating panes; for modes in floating panes;
- pane-border-status has top-floating and bottom-floating, and there are new - command-prompt -P opens a prompt inside a pane rather than on the status
default bindings under C-b g for common move and resize operations; line;
- floating panes are unzoomed before creation to avoid a crash. - pane-border-status has top-floating and bottom-floating, and there are new
default bindings under C-b g for common move and resize operations.
* Layout strings now use a JSON subset format which includes floating panes.
The old format is still accepted; control mode clients receive old layouts
unless they set the new-layouts flag.
* Add remain-on-exit failed-key to keep a pane open after a failed command
until a key is pressed.
* Add support for themes and improve default colours: * Add support for themes and improve default colours:
@@ -74,7 +87,7 @@ CHANGES FROM 3.7c TO 3.8
- new hooks to cover areas including client create and destroy, pane - new hooks to cover areas including client create and destroy, pane
activity, pane mode and prompt changes, pane movement and resizing, session activity, pane mode and prompt changes, pane movement and resizing, session
group changes and window creation, close and zoom; group changes and window creation, close, swap and zoom;
- OSC 133 escape sequences now trigger events: pane-command-started, - OSC 133 escape sequences now trigger events: pane-command-started,
pane-command-finished and pane-shell-prompt. pane-command-finished and pane-shell-prompt.
@@ -87,10 +100,29 @@ CHANGES FROM 3.7c TO 3.8
while scrolling or while hovered and disappears after pane-scrollbars-timeout while scrolling or while hovered and disappears after pane-scrollbars-timeout
(Michael Grant). (Michael Grant).
* Extend the fill-character option so both inside and outside the window can be * Add a clear-on-attach server option. When disabled, tmux does not enter the
changed. alternate screen on attach and instead scrolls the existing terminal content
into the scrollback buffer so it remains accessible (issue 5508).
* Change set-option and set-hooks to use formats and add a -F flag to each. * Copy mode can now automatically refresh as pane content changes with the
refresh-on, refresh-off and refresh-toggle commands. Automatic refresh is off
by default; refresh-now refreshes once and is bound to r. These replace
refresh-from-pane (issue 5165).
* Extend the fill-character option so both inside and outside the window can be
changed, with a new default for unused areas inside the window.
* Change show-options and show-hooks to use formats and add a -F flag to each.
* Allow individual terminal features to be disabled by adding @ to their names
in terminal-features, and add a utf8 feature for terminals which support
UTF-8 output.
* Add an appesc terminal feature for mintty's application escape key mode,
avoiding unsupported sequences on other terminals (Pete Dietl, issue 5626).
* Preserve existing Ss and Se terminfo capabilities for cursor styles and
restoring the default cursor (issue 5476).
* Add a #{A/count:frames} modifier to show a series of frames as an animation * Add a #{A/count:frames} modifier to show a series of frames as an animation
in the status line (issue 5412 from Fernando Daciuk). in the status line (issue 5412 from Fernando Daciuk).
@@ -105,6 +137,12 @@ CHANGES FROM 3.7c TO 3.8
* Add new-window -E, respawn-pane -E and respawn-window -E as more convenient * Add new-window -E, respawn-pane -E and respawn-window -E as more convenient
methods to create an empty pane (rather than using '' for the command). methods to create an empty pane (rather than using '' for the command).
* Add capture-pane -I to include the time each line entered history.
* Various memory leak fixes from Heon Jeong.
* Add a default C-b T binding to change the current pane title.
* Menus now belong to the window, so appear on all clients. * Menus now belong to the window, so appear on all clients.
* Dragging over an existing copy mode selection now adjusts it (Michael Grant). * Dragging over an existing copy mode selection now adjusts it (Michael Grant).
@@ -118,6 +156,13 @@ CHANGES FROM 3.7c TO 3.8
* Copy mode no longer exits at the bottom while a selection is in progress * Copy mode no longer exits at the bottom while a selection is in progress
(issue 5349). (issue 5349).
* Add copy-mode-current-line-style to set the style of the line containing the
cursor in copy mode (issue 5391).
* Copy mode word commands now recognise all Unicode whitespace characters; a
space in word-separators matches any Unicode whitespace character (issue
5562).
* Add style attributes for dimming colours (dim=) and for hyperlinks (link= and * Add style attributes for dimming colours (dim=) and for hyperlinks (link= and
nolink) (issues 4842 and 4280 from Moritz Angermann). nolink) (issues 4842 and 4280 from Moritz Angermann).
@@ -128,7 +173,9 @@ CHANGES FROM 3.7c TO 3.8
* Add or improve format variables including client_colours, * Add or improve format variables including client_colours,
pane_start_command_list, window_manual_width, window_manual_height, pane_start_command_list, window_manual_width, window_manual_height,
pane_last_output_time, pane_modal_flag and window_modal_pane. pane_last_output_time, pane_modal_flag, window_modal_pane, pane_private_modes,
pane_output_generation, history_added, history_collected, history_generation,
hook_fire_count and hook_fire_time.
* Add additional pane sort orders, and a z sort order for choose-tree so * Add additional pane sort orders, and a z sort order for choose-tree so
floating panes can be sorted by z-index. floating panes can be sorted by z-index.
@@ -140,15 +187,34 @@ CHANGES FROM 3.7c TO 3.8
* The mouse option now defaults to on. * The mouse option now defaults to on.
* The active-pane client flag has been removed.
* Fix send-keys -K so keys are inserted in the correct place in the input * Fix send-keys -K so keys are inserted in the correct place in the input
queue, like keys from key bindings (issue 3476). queue, like keys from key bindings (issue 3476).
* Fix control mode clients hanging on exit if pty data was still queued (Ben * Fix control mode clients hanging on exit if pty data was still queued (Ben
Maurer, issue 5356), and avoid sending notifications to clients which are Maurer, issue 5356), and avoid sending notifications to clients which are
already exiting (Ben Maurer, issue 5357). already exiting (Ben Maurer, issue 5357). Queue notifications so they are not
sent inside %begin/%end (issue 5458), do not let a stuck client prevent the
server from exiting (issue 5444), and reset control mode offsets when a pane
is respawned (issue 5498). Bound buffered command replies so a control mode
client that stops reading cannot consume unlimited memory (issue 5565), and
fix a collision between the output discard and bracketed paste flags (issue
5622).
* Fix grouped sessions sometimes being left as unusable command targets while * Fix grouped sessions sometimes being left as unusable command targets while
they are being killed (Bryce Miller, issue 5180). they are being killed (Bryce Miller, issue 5180), and prevent reuse of a
session with no windows while detaching (Heon Jeong, issue 5620).
* Free wait-for clients when they disconnect and do not run their remaining
queued commands (issue 5614).
* Expand formats in run-shell -c like other start-directory flags (reported by
Saul Nogueras).
* Fix session alert flag formats to check all windows (issue 5599).
* Do not clear the marked pane when clicking on a pane border.
* Fix choose mode filtering when more than one pane exists (issue 5326), and * Fix choose mode filtering when more than one pane exists (issue 5326), and
fix an infinite loop in customize mode when a filter does not match. fix an infinite loop in customize mode when a filter does not match.
@@ -159,11 +225,18 @@ CHANGES FROM 3.7c TO 3.8
status lines, menus, popups, wide characters and floating panes. This status lines, menus, popups, wide characters and floating panes. This
includes fixes for flicker with alternate screen applications and scrollbars includes fixes for flicker with alternate screen applications and scrollbars
(Michael Grant, issue 5351; Noam Stolero, issue 5350; Aung Myo Kyaw, issue (Michael Grant, issue 5351; Noam Stolero, issue 5350; Aung Myo Kyaw, issue
5098). 5098), and misplaced wrapped lines during partial redraws (Ben Maurer, issue
5625).
* Do not crash looking for the next or previous session (issue 5344), or when * Do not crash looking for the next or previous session (issue 5344), or when
no client is available. no client is available.
* Fix crashes when destroying a zoomed pane (Romain Francoise, issue 5591),
copying from a blank source pane (Kaixuan Li), or opening a mode prompt when
the client is offset into the window.
* Avoid an infinite loop when assigning a width to WCHAR_MAX (issue 5602).
CHANGES FROM 3.7b TO 3.7c CHANGES FROM 3.7b TO 3.7c
* Build with jemalloc on macOS to avoid what appears to be a bug in calloc * Build with jemalloc on macOS to avoid what appears to be a bug in calloc
+11 -3
View File
@@ -15,6 +15,9 @@ AM_CPPFLAGS += @XOPEN_DEFINES@ \
-DTMUX_LOCK_CMD='"@DEFAULT_LOCK_CMD@"' \ -DTMUX_LOCK_CMD='"@DEFAULT_LOCK_CMD@"' \
-DTMUX_TERM='"@DEFAULT_TERM@"' \ -DTMUX_TERM='"@DEFAULT_TERM@"' \
-DTMUX_MOUSE=1 -DTMUX_MOUSE=1
if HAVE_TMUX_SOCK
AM_CPPFLAGS += -DTMUX_SOCK='"@TMUX_SOCK@"'
endif
# Additional object files. # Additional object files.
LDADD = $(LIBOBJS) LDADD = $(LIBOBJS)
@@ -104,7 +107,6 @@ dist_tmux_SOURCES = \
cmd-find-window.c \ cmd-find-window.c \
cmd-find.c \ cmd-find.c \
cmd-if-shell.c \ cmd-if-shell.c \
cmd-invoke.c \
cmd-join-pane.c \ cmd-join-pane.c \
cmd-kill-pane.c \ cmd-kill-pane.c \
cmd-kill-server.c \ cmd-kill-server.c \
@@ -175,6 +177,7 @@ dist_tmux_SOURCES = \
input-keys.c \ input-keys.c \
input.c \ input.c \
job.c \ job.c \
json.c \
key-bindings.c \ key-bindings.c \
key-string.c \ key-string.c \
layout-custom.c \ layout-custom.c \
@@ -188,7 +191,6 @@ dist_tmux_SOURCES = \
options-table.c \ options-table.c \
options.c \ options.c \
paste.c \ paste.c \
popup.c \
proc.c \ proc.c \
prompt-history.c \ prompt-history.c \
prompt.c \ prompt.c \
@@ -258,7 +260,9 @@ check_PROGRAMS = \
fuzz/input-fuzzer \ fuzz/input-fuzzer \
fuzz/cmd-parse-fuzzer \ fuzz/cmd-parse-fuzzer \
fuzz/format-fuzzer \ fuzz/format-fuzzer \
fuzz/style-fuzzer fuzz/style-fuzzer \
fuzz/layout-fuzzer \
fuzz/json-fuzzer
fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS) fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_input_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS) fuzz_input_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_cmd_parse_fuzzer_LDFLAGS = $(FUZZING_LIBS) fuzz_cmd_parse_fuzzer_LDFLAGS = $(FUZZING_LIBS)
@@ -267,6 +271,10 @@ fuzz_format_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_format_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS) fuzz_format_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_style_fuzzer_LDFLAGS = $(FUZZING_LIBS) fuzz_style_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_style_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS) fuzz_style_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_layout_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_layout_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_json_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_json_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
endif endif
# Install tmux.1 in the right format. # Install tmux.1 in the right format.
+71 -8
View File
@@ -35,7 +35,7 @@ The usual local layout is:
```sh ```sh
cd /some/where/useful cd /some/where/useful
git clone https://github.com/tmux/tmux.git tmux-portable git clone https://github.com/tmux/tmux.git tmux-portable
git clone https://github.com/ThomasAdam/tmux-obsd.git tmux-openbsd-cutover git clone https://github.com/tmux/tmux-openbsd-cutover.git tmux-openbsd-cutover
``` ```
The exact directory names do not matter, but the examples below use: The exact directory names do not matter, but the examples below use:
@@ -56,18 +56,19 @@ The cutover repository has three important branches:
# Adding the OpenBSD remote to portable # Adding the OpenBSD remote to portable
In the portable repository, add the cutover repository as a remote: In the portable repository, add the published cutover repository as a remote.
This works regardless of which branch is checked out in a local cutover clone:
```sh ```sh
cd /path/to/tmux-portable cd /path/to/tmux-portable
git remote add tmux-openbsd /path/to/tmux-openbsd-cutover git remote add tmux-openbsd https://github.com/tmux/tmux-openbsd-cutover.git
git config remote.tmux-openbsd.tagOpt --no-tags git config remote.tmux-openbsd.tagOpt --no-tags
``` ```
If the remote already exists, update it instead: If the remote already exists, update it instead:
```sh ```sh
git remote set-url tmux-openbsd /path/to/tmux-openbsd-cutover git remote set-url tmux-openbsd https://github.com/tmux/tmux-openbsd-cutover.git
git config remote.tmux-openbsd.tagOpt --no-tags git config remote.tmux-openbsd.tagOpt --no-tags
``` ```
@@ -77,6 +78,32 @@ Fetch the cutover master branch explicitly:
git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master
``` ```
To merge unpublished changes from a local cutover clone instead, first ensure
it has an up-to-date local `master` branch. A normal clone may check out
`automation` and have only `origin/master`; fetching `master` from that clone
will then fail with `couldn't find remote ref master`.
With a clean cutover working tree:
```sh
cd /path/to/tmux-openbsd-cutover
git fetch --no-tags origin
git switch master
git merge --ff-only origin/master
```
`git switch master` creates a tracking branch from `origin/master` if there is
no local `master` yet. If the fast-forward fails, reconcile the local cutover
changes before continuing; do not reset them away.
Then, in portable, point the remote at that clone and fetch its local `master`:
```sh
cd /path/to/tmux-portable
git remote set-url tmux-openbsd /path/to/tmux-openbsd-cutover
git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master
```
# Automated syncing # Automated syncing
The normal sync is performed by the GitHub Actions workflow in the The normal sync is performed by the GitHub Actions workflow in the
@@ -99,7 +126,9 @@ OpenBSD changes.
If the workflow fails while merging into portable, do the merge locally and If the workflow fails while merging into portable, do the merge locally and
push the result. push the result.
Start from an up-to-date portable master: Start with a clean working tree and an up-to-date portable master. If a merge
is already in progress, skip to resolving conflicts, or abort it before
starting again:
```sh ```sh
cd /path/to/tmux-portable cd /path/to/tmux-portable
@@ -108,10 +137,15 @@ git checkout master
git pull --ff-only origin master git pull --ff-only origin master
``` ```
Fetch the cutover branch: Fetch the published cutover branch. Update an existing remote as well, since
it may point at a local clone without a `master` branch or with a stale one:
```sh ```sh
git remote add tmux-openbsd /path/to/tmux-openbsd-cutover 2>/dev/null || true if git remote get-url tmux-openbsd >/dev/null 2>&1; then
git remote set-url tmux-openbsd https://github.com/tmux/tmux-openbsd-cutover.git
else
git remote add tmux-openbsd https://github.com/tmux/tmux-openbsd-cutover.git
fi
git config remote.tmux-openbsd.tagOpt --no-tags git config remote.tmux-openbsd.tagOpt --no-tags
git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master git fetch --no-tags tmux-openbsd master:refs/remotes/tmux-openbsd/master
``` ```
@@ -122,7 +156,22 @@ Merge it:
git merge --no-ff --log refs/remotes/tmux-openbsd/master git merge --no-ff --log refs/remotes/tmux-openbsd/master
``` ```
Resolve conflicts by deciding whether portable or OpenBSD owns the file. If merging a local cutover branch instead, use the local-clone preparation
and fetch commands above in place of this fetch block.
When the merge reports conflicts, it leaves the merge in progress. List the
unresolved files, edit the conflict markers to combine the required portable
and OpenBSD changes, then stage each resolved file:
```sh
git diff --name-only --diff-filter=U
git diff -- path/to/file
git add path/to/file
```
For files that should come entirely from one side, decide whether portable or
OpenBSD owns the file before using the commands below. They replace the whole
file, including changes outside the conflicting hunks.
Useful commands: Useful commands:
@@ -140,11 +189,25 @@ git add path/to/file
This takes the OpenBSD/cutover version of a conflicted file. This takes the OpenBSD/cutover version of a conflicted file.
For a modify/delete conflict, the side that deleted the file has no version
to check out. For example, portable generates `Makefile` using autotools and
does not track OpenBSD's `Makefile`. If Git reports that `Makefile` was deleted
in HEAD and modified in cutover, keep the portable deletion with:
```sh
git rm -- Makefile
```
This removes the OpenBSD file left by the merge; regenerate the portable
`Makefile` with your usual configure command before building. Use this only
for the unmerged OpenBSD file, not an existing generated build file.
Before committing, inspect the result: Before committing, inspect the result:
```sh ```sh
git status git status
git diff --check git diff --check
git diff --cached --check
git diff --cached --stat git diff --cached --stat
``` ```
+139 -40
View File
@@ -51,6 +51,13 @@ struct args {
struct args_value *values; struct args_value *values;
}; };
/* Prepared command state. */
struct args_command_state {
struct cmd_list *cmdlist;
char *cmd;
struct cmd_parse_input pi;
};
static struct args_entry *args_find(struct args *, u_char); static struct args_entry *args_find(struct args *, u_char);
static int args_cmp(struct args_entry *, struct args_entry *); static int args_cmp(struct args_entry *, struct args_entry *);
@@ -82,7 +89,8 @@ args_copy_value(struct args_value *to, struct args_value *from)
case ARGS_NONE: case ARGS_NONE:
break; break;
case ARGS_COMMANDS: case ARGS_COMMANDS:
to->cmd = cmd_parse_add_ref(from->cmd); to->cmdlist = from->cmdlist;
to->cmdlist->references++;
break; break;
case ARGS_STRING: case ARGS_STRING:
to->string = xstrdup(from->string); to->string = xstrdup(from->string);
@@ -115,7 +123,7 @@ args_value_as_string(struct args_value *value)
return (""); return ("");
case ARGS_COMMANDS: case ARGS_COMMANDS:
if (value->cached == NULL) if (value->cached == NULL)
value->cached = cmd_parse_print(value->cmd, 0); value->cached = cmd_list_print(value->cmdlist, 0);
return (value->cached); return (value->cached);
case ARGS_STRING: case ARGS_STRING:
return (value->string); return (value->string);
@@ -359,7 +367,7 @@ args_copy_copy_value(struct args_value *to, struct args_value *from, int argc,
to->string = expanded; to->string = expanded;
break; break;
case ARGS_COMMANDS: case ARGS_COMMANDS:
to->cmd = cmd_parse_add_ref(from->cmd); to->cmdlist = cmd_list_copy(from->cmdlist, argc, argv);
break; break;
} }
} }
@@ -410,7 +418,7 @@ args_free_value(struct args_value *value)
free(value->string); free(value->string);
break; break;
case ARGS_COMMANDS: case ARGS_COMMANDS:
cmd_parse_free(value->cmd); cmd_list_free(value->cmdlist);
break; break;
} }
free(value->cached); free(value->cached);
@@ -469,7 +477,7 @@ args_to_vector(struct args *args, int *argc, char ***argv)
cmd_append_argv(argc, argv, args->values[i].string); cmd_append_argv(argc, argv, args->values[i].string);
break; break;
case ARGS_COMMANDS: case ARGS_COMMANDS:
s = cmd_parse_print(args->values[i].cmd, 0); s = cmd_list_print(args->values[i].cmdlist, 0);
cmd_append_argv(argc, argv, s); cmd_append_argv(argc, argv, s);
free(s); free(s);
break; break;
@@ -524,7 +532,7 @@ args_print_add_value(char **buf, size_t *len, struct args_value *value)
case ARGS_NONE: case ARGS_NONE:
break; break;
case ARGS_COMMANDS: case ARGS_COMMANDS:
expanded = cmd_parse_print(value->cmd, 0); expanded = cmd_list_print(value->cmdlist, 0);
args_print_add(buf, len, "{ %s }", expanded); args_print_add(buf, len, "{ %s }", expanded);
break; break;
case ARGS_STRING: case ARGS_STRING:
@@ -739,53 +747,144 @@ args_string(struct args *args, u_int idx)
return (args_value_as_string(&args->values[idx])); return (args_value_as_string(&args->values[idx]));
} }
/* /* Make a command now. */
* Convert one command argument into a parse tree. A string value is parsed; struct cmd_list *
* existing commands are referenced. args_make_commands_now(struct cmd *self, struct cmdq_item *item, u_int idx,
*/ int expand)
struct cmd_parse_tree * {
args_to_commands(struct cmd *self, struct cmdq_item *item, u_int idx, struct args_command_state *state;
const char *default_command, int expand, char **cause) char *error;
struct cmd_list *cmdlist;
state = args_make_commands_prepare(self, item, idx, NULL, 0, expand);
cmdlist = args_make_commands(state, 0, NULL, &error);
if (cmdlist == NULL) {
cmdq_error(item, "%s", error);
free(error);
}
args_make_commands_free(state);
return (cmdlist);
}
/* Save bits to make a command later. */
struct args_command_state *
args_make_commands_prepare(struct cmd *self, struct cmdq_item *item, u_int idx,
const char *default_command, int wait, int expand)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item);
struct client *tc = cmdq_get_target_client(item);
struct args_value *value; struct args_value *value;
struct cmd_parse_input pi = { 0 }; struct args_command_state *state;
const char *cmd = NULL, *file; const char *cmd;
char *expanded; const char *file;
struct cmd_parse_tree *tree;
*cause = NULL; state = xcalloc(1, sizeof *state);
if (idx >= args->count) if (idx < args->count) {
cmd = default_command;
else {
value = &args->values[idx]; value = &args->values[idx];
switch (value->type) { if (value->type == ARGS_COMMANDS) {
case ARGS_COMMANDS: state->cmdlist = value->cmdlist;
return (cmd_parse_add_ref(value->cmd)); state->cmdlist->references++;
case ARGS_STRING: return (state);
}
cmd = value->string; cmd = value->string;
break; } else {
case ARGS_NONE: if (default_command == NULL)
break;
}
}
if (cmd == NULL)
fatalx("argument out of range"); fatalx("argument out of range");
cmd = default_command;
}
if (expand) if (expand)
expanded = format_single_from_target(item, cmd); state->cmd = format_single_from_target(item, cmd);
else else
expanded = xstrdup(cmd); state->cmd = xstrdup(cmd);
log_debug("%s: %s", __func__, expanded); log_debug("%s: %s", __func__, state->cmd);
cmd_get_source(self, &file, &pi.line); if (wait)
state->pi.item = item;
cmd_get_source(self, &file, &state->pi.line);
if (file != NULL) if (file != NULL)
pi.file = file; state->pi.file = xstrdup(file);
pi.flags = cmd_get_parse_flags(self); state->pi.c = tc;
tree = cmd_parse_from_string(expanded, &pi, cause); if (state->pi.c != NULL)
free(expanded); state->pi.c->references++;
return (tree); cmd_find_copy_state(&state->pi.fs, target);
return (state);
}
/* Return argument as command. */
struct cmd_list *
args_make_commands(struct args_command_state *state, int argc, char **argv,
char **error)
{
struct cmd_parse_result *pr;
char *cmd, *new_cmd;
int i;
if (state->cmdlist != NULL) {
if (argc == 0) {
state->cmdlist->references++;
return (state->cmdlist);
}
return (cmd_list_copy(state->cmdlist, argc, argv));
}
cmd = xstrdup(state->cmd);
log_debug("%s: %s", __func__, cmd);
cmd_log_argv(argc, argv, __func__);
for (i = 0; i < argc; i++) {
new_cmd = cmd_template_replace(cmd, argv[i], i + 1);
log_debug("%s: %%%u %s: %s", __func__, i + 1, argv[i], new_cmd);
free(cmd);
cmd = new_cmd;
}
log_debug("%s: %s", __func__, cmd);
pr = cmd_parse_from_string(cmd, &state->pi);
free(cmd);
switch (pr->status) {
case CMD_PARSE_ERROR:
*error = pr->error;
return (NULL);
case CMD_PARSE_SUCCESS:
return (pr->cmdlist);
}
fatalx("invalid parse return state");
}
/* Free commands state. */
void
args_make_commands_free(struct args_command_state *state)
{
if (state->cmdlist != NULL)
cmd_list_free(state->cmdlist);
if (state->pi.c != NULL)
server_client_unref(state->pi.c);
free((void *)state->pi.file);
free(state->cmd);
free(state);
}
/* Get prepared command. */
char *
args_make_commands_get_command(struct args_command_state *state)
{
struct cmd *first;
int n;
char *s;
if (state->cmdlist != NULL) {
first = cmd_list_first(state->cmdlist);
if (first == NULL)
return (xstrdup(""));
return (xstrdup(cmd_get_entry(first)->name));
}
n = strcspn(state->cmd, " ,");
xasprintf(&s, "%.*s", n, state->cmd);
return (s);
} }
/* Get first value in argument. */ /* Get first value in argument. */
+26 -24
View File
@@ -87,21 +87,20 @@ start_cfg(void)
if (cfg_quiet) if (cfg_quiet)
flags = CMD_PARSE_QUIET; flags = CMD_PARSE_QUIET;
for (i = 0; i < cfg_nfiles; i++) for (i = 0; i < cfg_nfiles; i++)
load_cfg(cfg_files[i], NULL, NULL, flags, NULL); load_cfg(cfg_files[i], c, NULL, NULL, flags, NULL);
cmdq_append(NULL, cmdq_get_callback(cfg_done, NULL)); cmdq_append(NULL, cmdq_get_callback(cfg_done, NULL));
} }
int int
load_cfg(const char *path, struct cmdq_item *item, load_cfg(const char *path, struct client *c, struct cmdq_item *item,
struct cmd_find_state *current, int flags, struct cmdq_item **new_item) struct cmd_find_state *current, int flags, struct cmdq_item **new_item)
{ {
FILE *f; FILE *f;
struct cmd_parse_input pi; struct cmd_parse_input pi;
struct cmd_parse_tree *tree; struct cmd_parse_result *pr;
struct cmdq_item *new_item0; struct cmdq_item *new_item0;
struct cmdq_state *state; struct cmdq_state *state;
char *cause = NULL;
if (new_item != NULL) if (new_item != NULL)
*new_item = NULL; *new_item = NULL;
@@ -118,16 +117,18 @@ load_cfg(const char *path, struct cmdq_item *item,
pi.flags = flags; pi.flags = flags;
pi.file = path; pi.file = path;
pi.line = 1; pi.line = 1;
tree = cmd_parse_from_file(f, &pi, &cause); pi.item = item;
pi.c = c;
pr = cmd_parse_from_file(f, &pi);
fclose(f); fclose(f);
if (tree == NULL) { if (pr->status == CMD_PARSE_ERROR) {
cfg_add_cause("%s", cause); cfg_add_cause("%s", pr->error);
free(cause); free(pr->error);
return (-1); return (-1);
} }
cmd_parse_log(__func__, tree);
if (flags & CMD_PARSE_PARSEONLY) { if (flags & CMD_PARSE_PARSEONLY) {
cmd_parse_free(tree); cmd_list_free(pr->cmdlist);
return (0); return (0);
} }
@@ -137,12 +138,12 @@ load_cfg(const char *path, struct cmdq_item *item,
state = cmdq_new_state(NULL, NULL, 0); state = cmdq_new_state(NULL, NULL, 0);
cmdq_add_format(state, "current_file", "%s", pi.file); cmdq_add_format(state, "current_file", "%s", pi.file);
new_item0 = cmd_invoke_get(tree, state, NULL); new_item0 = cmdq_get_command(pr->cmdlist, state);
if (item != NULL) if (item != NULL)
new_item0 = cmdq_insert_after(item, new_item0); new_item0 = cmdq_insert_after(item, new_item0);
else else
new_item0 = cmdq_append(NULL, new_item0); new_item0 = cmdq_append(NULL, new_item0);
cmd_parse_free(tree); cmd_list_free(pr->cmdlist);
cmdq_free_state(state); cmdq_free_state(state);
if (new_item != NULL) if (new_item != NULL)
@@ -152,14 +153,13 @@ load_cfg(const char *path, struct cmdq_item *item,
int int
load_cfg_from_buffer(const void *buf, size_t len, const char *path, load_cfg_from_buffer(const void *buf, size_t len, const char *path,
struct cmdq_item *item, struct cmd_find_state *current, int flags, struct client *c, struct cmdq_item *item, struct cmd_find_state *current,
struct cmdq_item **new_item) int flags, struct cmdq_item **new_item)
{ {
struct cmd_parse_input pi; struct cmd_parse_input pi;
struct cmd_parse_tree *tree; struct cmd_parse_result *pr;
struct cmdq_item *new_item0; struct cmdq_item *new_item0;
struct cmdq_state *state; struct cmdq_state *state;
char *cause = NULL;
if (new_item != NULL) if (new_item != NULL)
*new_item = NULL; *new_item = NULL;
@@ -170,15 +170,17 @@ load_cfg_from_buffer(const void *buf, size_t len, const char *path,
pi.flags = flags; pi.flags = flags;
pi.file = path; pi.file = path;
pi.line = 1; pi.line = 1;
tree = cmd_parse_from_buffer(buf, len, &pi, &cause); pi.item = item;
if (tree == NULL) { pi.c = c;
cfg_add_cause("%s", cause);
free(cause); pr = cmd_parse_from_buffer(buf, len, &pi);
if (pr->status == CMD_PARSE_ERROR) {
cfg_add_cause("%s", pr->error);
free(pr->error);
return (-1); return (-1);
} }
cmd_parse_log(__func__, tree);
if (flags & CMD_PARSE_PARSEONLY) { if (flags & CMD_PARSE_PARSEONLY) {
cmd_parse_free(tree); cmd_list_free(pr->cmdlist);
return (0); return (0);
} }
@@ -188,12 +190,12 @@ load_cfg_from_buffer(const void *buf, size_t len, const char *path,
state = cmdq_new_state(NULL, NULL, 0); state = cmdq_new_state(NULL, NULL, 0);
cmdq_add_format(state, "current_file", "%s", pi.file); cmdq_add_format(state, "current_file", "%s", pi.file);
new_item0 = cmd_invoke_get(tree, state, NULL); new_item0 = cmdq_get_command(pr->cmdlist, state);
if (item != NULL) if (item != NULL)
new_item0 = cmdq_insert_after(item, new_item0); new_item0 = cmdq_insert_after(item, new_item0);
else else
new_item0 = cmdq_append(NULL, new_item0); new_item0 = cmdq_append(NULL, new_item0);
cmd_parse_free(tree); cmd_list_free(pr->cmdlist);
cmdq_free_state(state); cmdq_free_state(state);
if (new_item != NULL) if (new_item != NULL)
+17 -20
View File
@@ -69,25 +69,6 @@ static void client_dispatch_attached(struct imsg *);
static void client_dispatch_wait(struct imsg *); static void client_dispatch_wait(struct imsg *);
static const char *client_exit_message(void); static const char *client_exit_message(void);
/* Does any command have a flag? */
static int
client_command_has_flag(int argc, char **argv, int flag)
{
struct args_value *values;
struct cmd_parse_tree *tree;
int found;
values = args_from_vector(argc, argv);
tree = cmd_parse_from_arguments(values, argc, NULL);
found = cmd_parse_any_have(tree, flag);
cmd_parse_free(tree);
args_free_values(values, argc);
free(values);
return (found);
}
/* /*
* Get server create lock. If already held then server start is happening in * Get server create lock. If already held then server start is happening in
* another client, so block until the lock is released and return -2 to * another client, so block until the lock is released and return -2 to
@@ -251,6 +232,7 @@ int
client_main(struct event_base *base, int argc, char **argv, uint64_t flags, client_main(struct event_base *base, int argc, char **argv, uint64_t flags,
int feat) int feat)
{ {
struct cmd_parse_result *pr;
struct msg_command *data; struct msg_command *data;
int fd, i; int fd, i;
const char *ttynam, *termname, *cwd; const char *ttynam, *termname, *cwd;
@@ -260,6 +242,7 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags,
size_t size; size_t size;
char **caps = NULL, *cause; char **caps = NULL, *cause;
u_int ncaps = 0; u_int ncaps = 0;
struct args_value *values;
/* Set up the initial command. */ /* Set up the initial command. */
if (shell_command != NULL) { if (shell_command != NULL) {
@@ -270,8 +253,22 @@ client_main(struct event_base *base, int argc, char **argv, uint64_t flags,
flags |= CLIENT_STARTSERVER; flags |= CLIENT_STARTSERVER;
} else { } else {
msg = MSG_COMMAND; msg = MSG_COMMAND;
if (client_command_has_flag(argc, argv, CMD_STARTSERVER))
/*
* It's annoying parsing the command string twice (in client
* and later in server) but it is necessary to get the start
* server flag.
*/
values = args_from_vector(argc, argv);
pr = cmd_parse_from_arguments(values, argc, NULL);
if (pr->status == CMD_PARSE_SUCCESS) {
if (cmd_list_any_have(pr->cmdlist, CMD_STARTSERVER))
flags |= CLIENT_STARTSERVER; flags |= CLIENT_STARTSERVER;
cmd_list_free(pr->cmdlist);
} else
free(pr->error);
args_free_values(values, argc);
free(values);
} }
/* Create client process structure (starts logging). */ /* Create client process structure (starts logging). */
+3 -1
View File
@@ -91,8 +91,10 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag,
wp = target.wp; wp = target.wp;
if (wl != NULL) { if (wl != NULL) {
if (wp != NULL) if (wp != NULL) {
window_redraw_active_switch(wp->window, wp);
window_set_active_pane(wp->window, wp, 1); window_set_active_pane(wp->window, wp, 1);
}
session_set_current(s, wl); session_set_current(s, wl);
if (wp != NULL) if (wp != NULL)
cmd_find_from_winlink_pane(current, wl, wp, 0); cmd_find_from_winlink_pane(current, wl, wp, 0);
+19 -13
View File
@@ -56,12 +56,11 @@ cmd_bind_key_exec(struct cmd *self, struct cmdq_item *item)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
key_code key; key_code key;
const char *tablename, *note = args_get(args, 'N'), *file; const char *tablename, *note = args_get(args, 'N');
struct cmd_parse_result *pr;
int repeat; int repeat;
struct args_value *value; struct args_value *value;
u_int count = args_count(args); u_int count = args_count(args);
struct cmd_parse_tree *cmd;
struct cmd_parse_input pi;
key = key_string_lookup_string(args_string(args, 0)); key = key_string_lookup_string(args_string(args, 0));
if (key == KEYC_NONE || key == KEYC_UNKNOWN) { if (key == KEYC_NONE || key == KEYC_UNKNOWN) {
@@ -84,18 +83,25 @@ cmd_bind_key_exec(struct cmd *self, struct cmdq_item *item)
value = args_value(args, 1); value = args_value(args, 1);
if (count == 2 && value->type == ARGS_COMMANDS) { if (count == 2 && value->type == ARGS_COMMANDS) {
cmd = cmd_parse_add_ref(value->cmd); key_bindings_add(tablename, key, note, repeat, value->cmdlist);
key_bindings_add(tablename, key, note, repeat, cmd); value->cmdlist->references++;
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
memset(&pi, 0, sizeof pi); if (count == 2)
cmd_get_source(self, &file, &pi.line); pr = cmd_parse_from_string(args_string(args, 1), NULL);
if (file != NULL) else {
pi.file = file; pr = cmd_parse_from_arguments(args_values(args) + 1, count - 1,
pi.flags = cmd_get_parse_flags(self); NULL);
}
cmd = cmd_parse_from_arguments(args_values(args) + 1, count - 1, &pi); switch (pr->status) {
key_bindings_add(tablename, key, note, repeat, cmd); case CMD_PARSE_ERROR:
cmdq_error(item, "%s", pr->error);
free(pr->error);
return (CMD_RETURN_ERROR);
case CMD_PARSE_SUCCESS:
break;
}
key_bindings_add(tablename, key, note, repeat, pr->cmdlist);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
+13 -7
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-capture-pane.c,v 1.68 2026/07/20 11:16:33 nicm Exp $ */ /* $OpenBSD: cmd-capture-pane.c,v 1.69 2026/09/08 07:31:59 nicm Exp $ */
/* /*
* Copyright (c) 2009 Jonathan Alvarado <radobobo@users.sourceforge.net> * Copyright (c) 2009 Jonathan Alvarado <radobobo@users.sourceforge.net>
@@ -42,8 +42,8 @@ const struct cmd_entry cmd_capture_pane_entry = {
.name = "capture-pane", .name = "capture-pane",
.alias = "capturep", .alias = "capturep",
.args = { "ab:CeE:FHJLMNpPqRS:Tt:", 0, 0, NULL }, .args = { "ab:CeE:FHIJLMNpPqRS:Tt:", 0, 0, NULL },
.usage = "[-aCeFHJLMNpPqRT] " CMD_BUFFER_USAGE " [-E end-line] " .usage = "[-aCeFHIJLMNpPqRT] " CMD_BUFFER_USAGE " [-E end-line] "
"[-S start-line] " CMD_TARGET_PANE_USAGE, "[-S start-line] " CMD_TARGET_PANE_USAGE,
.target = { 't', CMD_FIND_PANE, 0 }, .target = { 't', CMD_FIND_PANE, 0 },
@@ -200,7 +200,7 @@ cmd_capture_pane_pending(struct args *args, struct window_pane *wp,
tmp[0] = line[i]; tmp[0] = line[i];
tmp[1] = '\0'; tmp[1] = '\0';
} else } else
xsnprintf(tmp, sizeof tmp, "\\%03hho", line[i]); snprintf(tmp, sizeof tmp, "\\%03hho", line[i]);
buf = cmd_capture_pane_append(buf, len, tmp, buf = cmd_capture_pane_append(buf, len, tmp,
strlen(tmp)); strlen(tmp));
} }
@@ -259,7 +259,7 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
struct grid_cell *gc = NULL; struct grid_cell *gc = NULL;
struct window_mode_entry *wme; struct window_mode_entry *wme;
int n, join_lines, number_lines, flags = 0; int n, join_lines, number_lines, flags = 0;
int show_flags, hyperlinks; int show_flags, show_time, hyperlinks;
u_int *links = NULL, nlinks = 0; u_int *links = NULL, nlinks = 0;
u_int i, sx, top, bottom, tmp; u_int i, sx, top, bottom, tmp;
char *cause, *buf = NULL, *line, b[64], *cp; char *cause, *buf = NULL, *line, b[64], *cp;
@@ -342,6 +342,7 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
flags |= GRID_STRING_TRIM_SPACES; flags |= GRID_STRING_TRIM_SPACES;
number_lines = args_has(args, 'L'); number_lines = args_has(args, 'L');
show_flags = args_has(args, 'F'); show_flags = args_has(args, 'F');
show_time = args_has(args, 'I');
hyperlinks = args_has(args, 'H'); hyperlinks = args_has(args, 'H');
if (hyperlinks) if (hyperlinks)
links = xreallocarray(NULL, gd->sx, sizeof *links); links = xreallocarray(NULL, gd->sx, sizeof *links);
@@ -358,6 +359,7 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
free(line); free(line);
continue; continue;
} }
gl = grid_peek_line(gd, i);
if (number_lines) { if (number_lines) {
if (i >= gd->hsize) if (i >= gd->hsize)
@@ -368,11 +370,16 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
if (n >= 0) if (n >= 0)
buf = cmd_capture_pane_append(buf, len, b, n); buf = cmd_capture_pane_append(buf, len, b, n);
} }
if (show_time) {
n = snprintf(b, sizeof b, "%llu ",
(unsigned long long)grid_line_time(gl));
if (n >= 0)
buf = cmd_capture_pane_append(buf, len, b, n);
}
if (show_flags) { if (show_flags) {
cp = b; cp = b;
*cp = '\0'; *cp = '\0';
gl = grid_peek_line(gd, i);
if (gl->flags & GRID_LINE_DEAD) if (gl->flags & GRID_LINE_DEAD)
*cp++ = 'D'; *cp++ = 'D';
if (gl->flags & GRID_LINE_HYPERLINK) if (gl->flags & GRID_LINE_HYPERLINK)
@@ -393,7 +400,6 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
} }
buf = cmd_capture_pane_append(buf, len, line, linelen); buf = cmd_capture_pane_append(buf, len, line, linelen);
gl = grid_peek_line(gd, i);
if (!join_lines || !(gl->flags & GRID_LINE_WRAPPED)) if (!join_lines || !(gl->flags & GRID_LINE_WRAPPED))
buf[(*len)++] = '\n'; buf[(*len)++] = '\n';
+19 -47
View File
@@ -57,7 +57,7 @@ struct cmd_command_prompt_prompt {
struct cmd_command_prompt_cdata { struct cmd_command_prompt_cdata {
struct cmdq_item *item; struct cmdq_item *item;
struct cmd_parse_tree *tree; struct args_command_state *state;
int flags; int flags;
enum prompt_type prompt_type; enum prompt_type prompt_type;
@@ -85,13 +85,13 @@ cmd_command_prompt_exec(struct cmd *self, struct cmdq_item *item)
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct client *tc = cmdq_get_target_client(item); struct client *tc = cmdq_get_target_client(item);
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
const char *type, *s, *input, *cmd; const char *type, *s, *input;
struct cmd_command_prompt_cdata *cdata; struct cmd_command_prompt_cdata *cdata;
char *tmp, *prompts, *prompt, *next_prompt; char *tmp, *prompts, *prompt, *next_prompt;
char *inputs = NULL, *next_input, *cause = NULL; char *inputs = NULL, *next_input;
struct window_pane *wp = target->wp; struct window_pane *wp = target->wp;
u_int count = args_count(args); u_int count = args_count(args);
int wait = !args_has(args, 'b'), space = 1, n; int wait = !args_has(args, 'b'), space = 1;
int pane = args_has(args, 'P'); int pane = args_has(args, 'P');
if (pane) { if (pane) {
@@ -107,22 +107,14 @@ cmd_command_prompt_exec(struct cmd *self, struct cmdq_item *item)
cdata->item = item; cdata->item = item;
if (pane) if (pane)
cdata->wp = wp; cdata->wp = wp;
if (count != 0) { cdata->state = args_make_commands_prepare(self, item, 0, "%1", wait,
cdata->tree = args_to_commands(self, item, 0, NULL, args_has(args, 'F'));
args_has(args, 'F'), &cause);
if (cdata->tree == NULL) {
cmdq_error(item, "%s", cause);
free(cause);
free(cdata);
return (CMD_RETURN_ERROR);
}
}
if ((s = args_get(args, 'p')) == NULL) { if ((s = args_get(args, 'p')) == NULL) {
if (count != 0) { if (count != 0) {
cmd = args_string(args, 0); tmp = args_make_commands_get_command(cdata->state);
n = strcspn(cmd, " ,"); xasprintf(&prompts, "(%s)", tmp);
xasprintf(&prompts, "(%.*s)", n, cmd); free(tmp);
} else { } else {
prompts = xstrdup(":"); prompts = xstrdup(":");
space = 0; space = 0;
@@ -209,11 +201,8 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s,
struct cmd_command_prompt_cdata *cdata = data; struct cmd_command_prompt_cdata *cdata = data;
char *error; char *error;
struct cmdq_item *item = cdata->item, *new_item; struct cmdq_item *item = cdata->item, *new_item;
struct cmdq_state *cs = NULL; struct cmd_list *cmdlist;
struct cmd_command_prompt_prompt *prompt; struct cmd_command_prompt_prompt *prompt;
struct cmd_invoke_input ci = { 0 };
struct cmd_parse_tree *tree;
struct cmd_parse_input pi = { 0 };
int argc = 0; int argc = 0;
char **argv = NULL; char **argv = NULL;
@@ -246,35 +235,18 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s,
cdata->argv = cmd_copy_argv(argc, argv); cdata->argv = cmd_copy_argv(argc, argv);
} }
if (item != NULL) cmdlist = args_make_commands(cdata->state, argc, argv, &error);
cs = cmdq_get_state(item); if (cmdlist == NULL) {
if (cdata->tree != NULL) {
/* Explicit body: prompt inputs become the template argv. */
ci.argc = argc;
ci.argv = argv;
new_item = cmd_invoke_get(cdata->tree, cs, &ci);
if (item == NULL)
cmdq_append(c, new_item);
else
cmdq_insert_after(item, new_item);
} else if (argc > 0 && argv[0] != NULL) {
/* No body: parse the submitted text as command language. */
tree = cmd_parse_from_string(argv[0], &pi, &error);
if (tree == NULL) {
if (item == NULL)
cmdq_append(c, cmdq_get_error(error)); cmdq_append(c, cmdq_get_error(error));
else
cmdq_error(item, "%s", error);
free(error); free(error);
} else { } else if (item == NULL) {
new_item = cmd_invoke_get(tree, cs, NULL); new_item = cmdq_get_command(cmdlist, NULL);
cmd_parse_free(tree);
if (item == NULL)
cmdq_append(c, new_item); cmdq_append(c, new_item);
else cmd_list_free(cmdlist);
} else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
} cmd_list_free(cmdlist);
} }
cmd_free_argv(argc, argv); cmd_free_argv(argc, argv);
@@ -311,6 +283,6 @@ cmd_command_prompt_free(void *data)
} }
free(cdata->prompts); free(cdata->prompts);
cmd_free_argv(cdata->argc, cdata->argv); cmd_free_argv(cdata->argc, cdata->argv);
cmd_parse_free(cdata->tree); args_make_commands_free(cdata->state);
free(cdata); free(cdata);
} }
+13 -16
View File
@@ -51,7 +51,7 @@ const struct cmd_entry cmd_confirm_before_entry = {
struct cmd_confirm_before_data { struct cmd_confirm_before_data {
struct cmdq_item *item; struct cmdq_item *item;
struct cmd_parse_tree *tree; struct cmd_list *cmdlist;
u_char confirm_key; u_char confirm_key;
int default_yes; int default_yes;
}; };
@@ -70,15 +70,13 @@ cmd_confirm_before_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_confirm_before_data *cdata; struct cmd_confirm_before_data *cdata;
struct client *tc = cmdq_get_target_client(item); struct client *tc = cmdq_get_target_client(item);
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
char *new_prompt, *cause = NULL; char *new_prompt;
const char *confirm_key, *prompt, *cmd; const char *confirm_key, *prompt, *cmd;
int wait = !args_has(args, 'b'), n; int wait = !args_has(args, 'b');
cdata = xcalloc(1, sizeof *cdata); cdata = xcalloc(1, sizeof *cdata);
cdata->tree = args_to_commands(self, item, 0, NULL, 1, &cause); cdata->cmdlist = args_make_commands_now(self, item, 0, 1);
if (cdata->tree == NULL) { if (cdata->cmdlist == NULL) {
cmdq_error(item, "%s", cause);
free(cause);
free(cdata); free(cdata);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
@@ -94,7 +92,8 @@ cmd_confirm_before_exec(struct cmd *self, struct cmdq_item *item)
cdata->confirm_key = confirm_key[0]; cdata->confirm_key = confirm_key[0];
else { else {
cmdq_error(item, "invalid confirm key"); cmdq_error(item, "invalid confirm key");
cmd_confirm_before_free(cdata); cmd_list_free(cdata->cmdlist);
free(cdata);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
} }
@@ -104,9 +103,8 @@ cmd_confirm_before_exec(struct cmd *self, struct cmdq_item *item)
if ((prompt = args_get(args, 'p')) != NULL) if ((prompt = args_get(args, 'p')) != NULL)
xasprintf(&new_prompt, "%s ", prompt); xasprintf(&new_prompt, "%s ", prompt);
else { else {
cmd = args_string(args, 0); cmd = cmd_get_entry(cmd_list_first(cdata->cmdlist))->name;
n = strcspn(cmd, " ,"); xasprintf(&new_prompt, "Confirm '%s'? (%c/n) ", cmd,
xasprintf(&new_prompt, "Confirm '%.*s'? (%c/n) ", n, cmd,
cdata->confirm_key); cdata->confirm_key);
} }
@@ -126,7 +124,6 @@ cmd_confirm_before_callback(struct client *c, void *data, const char *s,
{ {
struct cmd_confirm_before_data *cdata = data; struct cmd_confirm_before_data *cdata = data;
struct cmdq_item *item = cdata->item, *new_item; struct cmdq_item *item = cdata->item, *new_item;
struct cmdq_state *cs;
int retcode = 1; int retcode = 1;
if (c->flags & CLIENT_DEAD) if (c->flags & CLIENT_DEAD)
@@ -139,11 +136,11 @@ cmd_confirm_before_callback(struct client *c, void *data, const char *s,
retcode = 0; retcode = 0;
if (item == NULL) { if (item == NULL) {
new_item = cmd_invoke_get(cdata->tree, NULL, NULL); new_item = cmdq_get_command(cdata->cmdlist, NULL);
cmdq_append(c, new_item); cmdq_append(c, new_item);
} else { } else {
cs = cmdq_get_state(item); new_item = cmdq_get_command(cdata->cmdlist,
new_item = cmd_invoke_get(cdata->tree, cs, NULL); cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
} }
@@ -162,6 +159,6 @@ cmd_confirm_before_free(void *data)
{ {
struct cmd_confirm_before_data *cdata = data; struct cmd_confirm_before_data *cdata = data;
cmd_parse_free(cdata->tree); cmd_list_free(cdata->cmdlist);
free(cdata); free(cdata);
} }
+171 -300
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */ /* $OpenBSD: cmd-display-menu.c,v 1.55 2026/09/21 12:14:32 nicm Exp $ */
/* /*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -54,8 +54,8 @@ const struct cmd_entry cmd_display_popup_entry = {
.name = "display-popup", .name = "display-popup",
.alias = "popup", .alias = "popup",
.args = { "Bb:Cc:d:e:Eh:kNs:S:t:T:w:x:y:", 0, -1, NULL }, .args = { "Bb:Cc:d:e:Eh:ks:S:t:T:w:x:y:", 0, -1, NULL },
.usage = "[-BCEkN] [-b border-lines] [-c target-client] " .usage = "[-BCEk] [-b border-lines] [-c target-client] "
"[-d start-directory] [-e environment] [-h height] " "[-d start-directory] [-e environment] [-h height] "
"[-s style] [-S border-style] " CMD_TARGET_PANE_USAGE "[-s style] [-S border-style] " CMD_TARGET_PANE_USAGE
" [-T title] [-w width] [-x position] [-y position] " " [-T title] [-w width] [-x position] [-y position] "
@@ -91,203 +91,6 @@ cmd_display_menu_args_parse(struct args *args, u_int idx, __unused char **cause)
return (type); return (type);
} }
static int
cmd_display_menu_get_popup_pos(struct client *tc, struct cmdq_item *item,
struct args *args, u_int *px, u_int *py, u_int w, u_int h)
{
struct tty *tty = &tc->tty;
struct cmd_find_state *target = cmdq_get_target(item);
struct key_event *event = cmdq_get_event(item);
struct session *s = tc->session;
struct winlink *wl = target->wl;
struct window_pane *wp = target->wp;
struct style_ranges *ranges = NULL;
struct style_range *sr = NULL;
const char *xp, *yp;
char *p;
int top;
u_int line, ox, oy, sx, sy, lines, position;
long n;
struct format_tree *ft;
/*
* Work out the position from the -x and -y arguments. This is the
* bottom-left position.
*/
/* If the popup is too big, stop now. */
if (w > tty->sx || h > tty->sy)
return (0);
/* Create format with mouse position if any. */
ft = format_create_from_target(item);
if (event->m.valid) {
format_add(ft, "popup_mouse_x", "%u", event->m.x);
format_add(ft, "popup_mouse_y", "%u", event->m.y);
}
/* Position of the previous menu, for -x/-y L. */
format_add(ft, "popup_last_x", "%u", target->w->menu_last_px);
format_add(ft, "popup_last_y", "%u", target->w->menu_last_py + h);
/*
* If there are any status lines, add this window position and the
* status line position.
*/
top = status_at_line(tc);
if (top != -1) {
lines = status_line_size(tc);
if (top == 0)
top = lines;
else
top = 0;
position = options_get_number(s->options, "status-position");
for (line = 0; line < lines; line++) {
ranges = &tc->status.entries[line].ranges;
TAILQ_FOREACH(sr, ranges, entry) {
if (sr->type != STYLE_RANGE_WINDOW)
continue;
if (sr->argument == (u_int)wl->idx)
break;
}
if (sr != NULL)
break;
}
if (sr != NULL) {
format_add(ft, "popup_window_status_line_x", "%u",
sr->start);
if (position == 0) {
format_add(ft, "popup_window_status_line_y",
"%u", line + 1 + h);
} else {
format_add(ft, "popup_window_status_line_y",
"%u", tty->sy - lines + line);
}
}
if (position == 0)
format_add(ft, "popup_status_line_y", "%u", lines + h);
else {
format_add(ft, "popup_status_line_y", "%u",
tty->sy - lines);
}
} else
top = 0;
/* Popup width and height. */
format_add(ft, "popup_width", "%u", w);
format_add(ft, "popup_height", "%u", h);
/* Position so popup is in the centre. */
n = (long)(tty->sx - 1) / 2 - w / 2;
if (n < 0)
format_add(ft, "popup_centre_x", "%u", 0);
else
format_add(ft, "popup_centre_x", "%ld", n);
n = (tty->sy - 1) / 2 + h / 2;
if (n >= tty->sy)
format_add(ft, "popup_centre_y", "%u", tty->sy - h);
else
format_add(ft, "popup_centre_y", "%ld", n);
/* Position of popup relative to mouse. */
if (event->m.valid) {
n = (long)event->m.x - w / 2;
if (n < 0)
format_add(ft, "popup_mouse_centre_x", "%u", 0);
else
format_add(ft, "popup_mouse_centre_x", "%ld", n);
n = event->m.y - h / 2;
if (n + h >= tty->sy) {
format_add(ft, "popup_mouse_centre_y", "%u",
tty->sy - h);
} else
format_add(ft, "popup_mouse_centre_y", "%ld", n);
n = (long)event->m.y + h;
if (n >= tty->sy)
format_add(ft, "popup_mouse_top", "%u", tty->sy - 1);
else
format_add(ft, "popup_mouse_top", "%ld", n);
n = event->m.y - h;
if (n < 0)
format_add(ft, "popup_mouse_bottom", "%u", 0);
else
format_add(ft, "popup_mouse_bottom", "%ld", n);
}
/* Position in pane. */
tty_window_offset(&tc->tty, &ox, &oy, &sx, &sy);
n = top + wp->yoff - oy + h;
if (n >= tty->sy)
format_add(ft, "popup_pane_top", "%u", tty->sy - h);
else
format_add(ft, "popup_pane_top", "%ld", n);
format_add(ft, "popup_pane_bottom", "%u", top + wp->yoff + wp->sy - oy);
format_add(ft, "popup_pane_left", "%u", wp->xoff - ox);
n = (long)wp->xoff + wp->sx - ox - w;
if (n < 0)
format_add(ft, "popup_pane_right", "%u", 0);
else
format_add(ft, "popup_pane_right", "%ld", n);
/* Expand horizontal position. */
xp = args_get(args, 'x');
if (xp == NULL || strcmp(xp, "C") == 0)
xp = "#{popup_centre_x}";
else if (strcmp(xp, "R") == 0)
xp = "#{popup_pane_right}";
else if (strcmp(xp, "P") == 0)
xp = "#{popup_pane_left}";
else if (strcmp(xp, "M") == 0)
xp = "#{popup_mouse_centre_x}";
else if (strcmp(xp, "L") == 0)
xp = "#{popup_last_x}";
else if (strcmp(xp, "W") == 0)
xp = "#{popup_window_status_line_x}";
p = format_expand(ft, xp);
n = strtol(p, NULL, 10);
if (n + w >= tty->sx)
n = tty->sx - w;
else if (n < 0)
n = 0;
*px = n;
log_debug("%s: -x: %s = %s = %u (-w %u)", __func__, xp, p, *px, w);
free(p);
/* Expand vertical position */
yp = args_get(args, 'y');
if (yp == NULL || strcmp(yp, "C") == 0)
yp = "#{popup_centre_y}";
else if (strcmp(yp, "P") == 0)
yp = "#{popup_pane_bottom}";
else if (strcmp(yp, "M") == 0)
yp = "#{popup_mouse_top}";
else if (strcmp(yp, "L") == 0)
yp = "#{popup_last_y}";
else if (strcmp(yp, "S") == 0)
yp = "#{popup_status_line_y}";
else if (strcmp(yp, "W") == 0)
yp = "#{popup_window_status_line_y}";
p = format_expand(ft, yp);
n = strtol(p, NULL, 10);
if (n < h)
n = 0;
else
n -= h;
if (n + h >= tty->sy)
n = tty->sy - h;
else if (n < 0)
n = 0;
*py = n;
log_debug("%s: -y: %s = %s = %u (-h %u)", __func__, yp, p, *py, h);
free(p);
format_free(ft);
return (1);
}
static int static int
cmd_display_menu_get_menu_pos(struct client *tc, struct cmdq_item *item, cmd_display_menu_get_menu_pos(struct client *tc, struct cmdq_item *item,
struct args *args, u_int *px, u_int *py, u_int w, u_int h) struct args *args, u_int *px, u_int *py, u_int w, u_int h)
@@ -482,8 +285,8 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
enum box_lines lines = BOX_LINES_DEFAULT; enum box_lines lines = BOX_LINES_DEFAULT;
char *title, *cause = NULL; char *title, *cause = NULL;
int flags = 0, starting_choice = 0; int flags = 0, starting_choice = 0;
u_int px, py, i, count = args_count(args); u_int px, py, sx, sy, i, count = args_count(args);
struct options *o = target->s->curw->window->options; struct options *o = target->w->options;
struct options_entry *oe; struct options_entry *oe;
if (args_has(args, 'C')) { if (args_has(args, 'C')) {
@@ -531,9 +334,6 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
} }
if (menu->count == 0) if (menu->count == 0)
goto out; goto out;
if (!cmd_display_menu_get_menu_pos(tc, item, args, &px, &py,
menu->width + 4, menu->count + 2))
goto out;
value = args_get(args, 'b'); value = args_get(args, 'b');
if (value != NULL) { if (value != NULL) {
@@ -544,7 +344,11 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "menu-border-lines %s", cause); cmdq_error(item, "menu-border-lines %s", cause);
goto fail; goto fail;
} }
} } else
lines = options_get_number(o, "menu-border-lines");
menu_get_size(menu, lines, &sx, &sy);
if (!cmd_display_menu_get_menu_pos(tc, item, args, &px, &py, sx, sy))
goto out;
if (args_has(args, 'O')) if (args_has(args, 'O'))
flags |= MENU_STAYOPEN; flags |= MENU_STAYOPEN;
@@ -565,6 +369,22 @@ fail:
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
static enum pane_lines
cmd_display_popup_get_lines(const char *value, char **cause)
{
const struct options_table_entry *oe;
if (value == NULL)
value = "single";
else if (strcmp(value, "rounded") == 0)
value = "single";
else if (strcmp(value, "padded") == 0)
value = "spaces";
oe = options_search("pane-border-lines");
return (options_find_choice(oe, value, cause));
}
static enum cmd_retval static enum cmd_retval
cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item) cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
{ {
@@ -572,148 +392,199 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
struct session *s = target->s; struct session *s = target->s;
struct client *tc = cmdq_get_target_client(item); struct client *tc = cmdq_get_target_client(item);
struct tty *tty = &tc->tty; struct winlink *wl = target->wl;
const char *value, *shell, *shellcmd = NULL; struct window *w = wl->window;
const char *style = args_get(args, 's'); struct window_pane *wp = target->wp, *new_wp = NULL;
struct spawn_context sc = { 0 };
struct layout_cell *lc = NULL;
struct layout_geometry lg;
struct event_payload *ep;
struct cmd_find_state fs;
const char *value, *style = args_get(args, 's');
const char *border_style = args_get(args, 'S'); const char *border_style = args_get(args, 'S');
char *cwd = NULL, *cause = NULL, **argv = NULL; char *cause = NULL, *title = NULL;
char *title = NULL; enum pane_lines lines = PANE_LINES_SINGLE;
int modify = popup_present(tc); u_int px, py, sx, sy, count = args_count(args);
int flags = -1, argc = 0;
enum box_lines lines = BOX_LINES_DEFAULT;
u_int px, py, w, h, count = args_count(args);
struct args_value *av; struct args_value *av;
struct environ *env = NULL; long long ll;
struct options *o = s->curw->window->options;
struct options_entry *oe;
if (args_has(args, 'C')) { if (args_has(args, 'C')) {
server_client_clear_overlay(tc); if (w->modal != NULL)
server_kill_pane(w->modal);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
if (tc->flags & CLIENT_CONTROL) if (tc->flags & CLIENT_CONTROL)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
if (!modify && tc->overlay_draw != NULL) if (w->modal != NULL)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
if (!modify) { value = args_get(args, 'b');
h = tty->sy / 2; if (args_has(args, 'B'))
lines = PANE_LINES_NONE;
else if (value != NULL) {
lines = cmd_display_popup_get_lines(value, &cause);
if (cause != NULL) {
cmdq_error(item, "pane-border-lines %s", cause);
goto fail;
}
}
sy = w->sy / 2;
if (args_has(args, 'h')) { if (args_has(args, 'h')) {
h = args_percentage(args, 'h', 1, tty->sy, tty->sy, ll = args_percentage(args, 'h', 1, w->sy, w->sy, &cause);
&cause);
if (cause != NULL) { if (cause != NULL) {
cmdq_error(item, "height %s", cause); cmdq_error(item, "height %s", cause);
goto fail; goto fail;
} }
sy = ll;
} }
w = tty->sx / 2; sx = w->sx / 2;
if (args_has(args, 'w')) { if (args_has(args, 'w')) {
w = args_percentage(args, 'w', 1, tty->sx, tty->sx, ll = args_percentage(args, 'w', 1, w->sx, w->sx, &cause);
&cause);
if (cause != NULL) { if (cause != NULL) {
cmdq_error(item, "width %s", cause); cmdq_error(item, "width %s", cause);
goto fail; goto fail;
} }
sx = ll;
} }
if (w > tty->sx) if (sx > w->sx)
w = tty->sx; sx = w->sx;
if (h > tty->sy) if (sy > w->sy)
h = tty->sy; sy = w->sy;
if (!cmd_display_menu_get_popup_pos(tc, item, args, &px, &py, if ((lines == PANE_LINES_NONE && (sx < 1 || sy < 1)) ||
w, h)) (lines != PANE_LINES_NONE && (sx < 3 || sy < 3)))
goto out;
if (!cmd_display_menu_get_menu_pos(tc, item, args, &px, &py, sx, sy))
goto out; goto out;
value = args_get(args, 'd'); lg.sx = sx;
if (value != NULL) lg.sy = sy;
cwd = format_single_from_target(item, value); lg.xoff = px;
else lg.yoff = py;
cwd = xstrdup(server_client_get_cwd(tc, s)); if (lines != PANE_LINES_NONE) {
if (count == 0) { lg.sx -= 2;
shellcmd = options_get_string(s->options, lg.sy -= 2;
"default-command"); lg.xoff++;
} else if (count == 1) lg.yoff++;
shellcmd = args_string(args, 0); }
if (count <= 1 && (shellcmd == NULL || *shellcmd == '\0')) {
shellcmd = NULL;
shell = options_get_string(s->options, "default-shell");
if (!checkshell(shell))
shell = _PATH_BSHELL;
cmd_append_argv(&argc, &argv, shell);
} else
args_to_vector(args, &argc, &argv);
if (args_has(args, 'e') >= 1) { window_push_zoom(w, 0, 1);
env = environ_create(); lc = layout_floating_pane(w, wp, &lg);
if (lc == NULL) {
window_pop_zoom(w);
goto out;
}
sc.item = item;
sc.s = s;
sc.wl = wl;
sc.tc = tc;
sc.wp0 = wp;
sc.lc = lc;
sc.idx = -1;
sc.cwd = args_get(args, 'd');
sc.flags = (SPAWN_FLOATING|SPAWN_MODAL|SPAWN_FLOATOVERZOOM);
if (count != 1 || *args_string(args, 0) != '\0')
args_to_vector(args, &sc.argc, &sc.argv);
sc.environ = environ_create();
av = args_first_value(args, 'e'); av = args_first_value(args, 'e');
while (av != NULL) { while (av != NULL) {
environ_put(env, av->string, 0); environ_put(sc.environ, av->string, 0);
av = args_next_value(av); av = args_next_value(av);
} }
}
}
value = args_get(args, 'b'); new_wp = spawn_pane(&sc, &cause);
if (args_has(args, 'B')) if (new_wp == NULL) {
lines = BOX_LINES_NONE; cmdq_error(item, "create pane failed: %s", cause);
else if (value != NULL) { free(cause);
oe = options_get(o, "popup-border-lines"); cause = NULL;
lines = options_find_choice(options_table_entry(oe), value, window_pop_zoom(w);
&cause);
if (cause != NULL) {
cmdq_error(item, "popup-border-lines %s", cause);
goto fail; goto fail;
} }
} window_pop_zoom(w);
new_wp->flags |= PANE_CAPTUREALLKEYS;
if (!args_has(args, 'E'))
new_wp->flags |= PANE_CLOSEONCANCEL;
options_set_number(new_wp->options, "pane-border-lines", lines);
if (args_has(args, 'E') > 1) {
if (args_has(args, 'k'))
options_set_number(new_wp->options, "remain-on-exit", 4);
else
options_set_number(new_wp->options, "remain-on-exit", 2);
} else if (args_has(args, 'E'))
options_set_number(new_wp->options, "remain-on-exit", 0);
else if (args_has(args, 'k'))
options_set_number(new_wp->options, "remain-on-exit", 3);
else
options_set_number(new_wp->options, "remain-on-exit", 1);
options_set_string(new_wp->options, "remain-on-exit-format", 0, "%s", "");
if (style != NULL) {
if (options_set_string(new_wp->options, "window-style", 0,
"%s", style) == NULL) {
cmdq_error(item, "bad style: %s", style);
goto fail;
}
options_set_string(new_wp->options, "window-active-style", 0,
"%s", style);
new_wp->flags |= (PANE_REDRAW|PANE_STYLECHANGED|
PANE_THEMECHANGED);
}
if (border_style != NULL) {
if (options_set_string(new_wp->options, "pane-border-style", 0,
"%s", border_style) == NULL) {
cmdq_error(item, "bad border style: %s", border_style);
goto fail;
}
options_set_string(new_wp->options, "pane-active-border-style",
0, "%s", border_style);
}
if (args_has(args, 'T')) if (args_has(args, 'T'))
title = format_single_from_target(item, args_get(args, 'T')); title = format_single_from_target(item, args_get(args, 'T'));
else if (title != NULL) {
title = xstrdup(""); options_set_number(new_wp->options, "pane-border-status",
PANE_STATUS_TOP);
if (args_has(args, 'N') || !modify) options_set_string(new_wp->options, "pane-border-format", 0,
flags = 0; "%s", "#{pane_title}");
if (args_has(args, 'E') > 1) { screen_set_title(&new_wp->base, title, 0);
if (flags == -1) ep = event_payload_create();
flags = 0; cmd_find_from_pane(&fs, new_wp, 0);
flags |= POPUP_CLOSEEXITZERO; event_payload_set_target(ep, &fs);
} else if (args_has(args, 'E')) { event_payload_set_pane(ep, "pane", new_wp);
if (flags == -1) event_payload_set_window(ep, "window", new_wp->window);
flags = 0; event_payload_set_string(ep, "new_title", "%s", title);
flags |= POPUP_CLOSEEXIT; events_fire("pane-title-changed", ep);
}
if (args_has(args, 'k')) {
if (flags == -1)
flags = 0;
flags |= POPUP_CLOSEANYKEY;
} }
if (modify) { new_wp->wait_item = item;
popup_modify(tc, title, style, border_style, lines, flags); server_redraw_session(s);
goto out; if (sc.argv != NULL)
} cmd_free_argv(sc.argc, sc.argv);
if (popup_display(flags, lines, item, px, py, w, h, env, shellcmd, argc, environ_free(sc.environ);
argv, cwd, title, tc, s, style, border_style, NULL, NULL) != 0)
goto out;
environ_free(env);
free(cwd);
free(title); free(title);
cmd_free_argv(argc, argv);
return (CMD_RETURN_WAIT); return (CMD_RETURN_WAIT);
out: out:
cmd_free_argv(argc, argv); if (sc.argv != NULL)
environ_free(env); cmd_free_argv(sc.argc, sc.argv);
free(cwd); environ_free(sc.environ);
free(title); free(title);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
fail: fail:
free(cause); free(cause);
cmd_free_argv(argc, argv); if (new_wp != NULL) {
environ_free(env); server_client_remove_pane(new_wp);
free(cwd); layout_close_pane(new_wp);
window_remove_pane(new_wp->window, new_wp);
}
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);
free(title); free(title);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
+23 -7
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-display-message.c,v 1.65 2026/02/23 08:46:57 nicm Exp $ */ /* $OpenBSD: cmd-display-message.c,v 1.66 2026/09/08 08:33:10 nicm Exp $ */
/* /*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org> * Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -39,8 +39,8 @@ const struct cmd_entry cmd_display_message_entry = {
.name = "display-message", .name = "display-message",
.alias = "display", .alias = "display",
.args = { "aCc:d:lINpt:F:v", 0, 1, NULL }, .args = { "aCc:d:jlINpt:F:v", 0, 1, NULL },
.usage = "[-aCIlNpv] [-c target-client] [-d delay] [-F format] " .usage = "[-aCIjlNpv] [-c target-client] [-d delay] [-F format] "
CMD_TARGET_PANE_USAGE " [message]", CMD_TARGET_PANE_USAGE " [message]",
.target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL }, .target = { 't', CMD_FIND_PANE, CMD_FIND_CANFAIL },
@@ -67,14 +67,15 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
struct winlink *wl = target->wl; struct winlink *wl = target->wl;
struct window_pane *wp = target->wp; struct window_pane *wp = target->wp;
const char *template; const char *template;
char *msg, *cause; char *msg, *cause = NULL;
int delay = -1, flags, Nflag = args_has(args, 'N'); int delay = -1, flags, Nflag = args_has(args, 'N');
int Cflag = args_has(args, 'C'); int Cflag = args_has(args, 'C');
struct format_tree *ft; struct format_tree *ft;
u_int count = args_count(args); u_int count = args_count(args);
struct evbuffer *evb; struct evbuffer *evb;
struct json_node *jn;
if (args_has(args, 'I')) { if (args_has(args, 'I') && !args_has(args, 'j')) {
if (wp == NULL) if (wp == NULL)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
switch (window_pane_start_input(wp, item, &cause)) { switch (window_pane_start_input(wp, item, &cause)) {
@@ -107,7 +108,9 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
template = args_string(args, 0); template = args_string(args, 0);
else else
template = args_get(args, 'F'); template = args_get(args, 'F');
if (template == NULL) if (args_has(args, 'j') && template == NULL)
template = "";
else if (template == NULL)
template = DISPLAY_MESSAGE_TEMPLATE; template = DISPLAY_MESSAGE_TEMPLATE;
/* /*
@@ -129,7 +132,7 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, flags); ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, flags);
format_defaults(ft, c, s, wl, wp); format_defaults(ft, c, s, wl, wp);
if (args_has(args, 'a')) { if (args_has(args, 'a') && !args_has(args, 'j')) {
format_each(ft, cmd_display_message_each, item); format_each(ft, cmd_display_message_each, item);
format_free(ft); format_free(ft);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
@@ -139,6 +142,19 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
msg = xstrdup(template); msg = xstrdup(template);
else else
msg = format_expand_time(ft, template); msg = format_expand_time(ft, template);
if (args_has(args, 'j')) {
jn = json_parse(msg, &cause);
if (jn == NULL) {
cmdq_error(item, "%s", cause);
free(cause);
free(msg);
format_free(ft);
return (CMD_RETURN_ERROR);
}
free(msg);
msg = json_to_string(jn);
json_destroy_node(jn);
}
if (cmdq_get_client(item) == NULL) if (cmdq_get_client(item) == NULL)
cmdq_error(item, "%s", msg); cmdq_error(item, "%s", msg);
+11 -7
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-find.c,v 1.87 2026/07/17 12:42:51 nicm Exp $ */ /* $OpenBSD: cmd-find.c,v 1.88 2026/09/07 12:05:12 nicm Exp $ */
/* /*
* Copyright (c) 2015 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2015 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -388,9 +388,11 @@ cmd_find_get_window_with_session(struct cmd_find_state *fs, const char *window)
/* Try as an offset. */ /* Try as an offset. */
if (!exact && (window[0] == '+' || window[0] == '-')) { if (!exact && (window[0] == '+' || window[0] == '-')) {
if (window[1] != '\0') if (window[1] != '\0') {
n = strtonum(window + 1, 1, INT_MAX, NULL); n = strtonum(window + 1, 1, INT_MAX, &errstr);
else if (errstr != NULL)
return (-1);
} else
n = 1; n = 1;
s = fs->s; s = fs->s;
if (fs->flags & CMD_FIND_WINDOW_INDEX) { if (fs->flags & CMD_FIND_WINDOW_INDEX) {
@@ -626,9 +628,11 @@ cmd_find_get_pane_with_window(struct cmd_find_state *fs, const char *pane)
/* Try as an offset. */ /* Try as an offset. */
if (pane[0] == '+' || pane[0] == '-') { if (pane[0] == '+' || pane[0] == '-') {
if (pane[1] != '\0') if (pane[1] != '\0') {
n = strtonum(pane + 1, 1, INT_MAX, NULL); n = strtonum(pane + 1, 1, INT_MAX, &errstr);
else if (errstr != NULL)
return (-1);
} else
n = 1; n = 1;
wp = fs->w->active; wp = fs->w->active;
if (pane[0] == '+') if (pane[0] == '+')
+43 -48
View File
@@ -53,9 +53,8 @@ const struct cmd_entry cmd_if_shell_entry = {
}; };
struct cmd_if_shell_data { struct cmd_if_shell_data {
struct cmd_parse_tree *cmd_if; struct args_command_state *cmd_if;
struct cmd_parse_tree *cmd_else; struct args_command_state *cmd_else;
struct client *client; struct client *client;
struct cmdq_item *item; struct cmdq_item *item;
@@ -77,58 +76,46 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
struct cmd_if_shell_data *cdata; struct cmd_if_shell_data *cdata;
struct cmdq_item *new_item; struct cmdq_item *new_item;
struct cmd_parse_tree *tree; char *shellcmd;
char *shellcmd, *cause = NULL;
struct client *tc = cmdq_get_target_client(item); struct client *tc = cmdq_get_target_client(item);
struct session *s = target->s; struct session *s = target->s;
u_int count = args_count(args), idx; struct cmd_list *cmdlist;
u_int count = args_count(args);
int wait = !args_has(args, 'b'); int wait = !args_has(args, 'b');
shellcmd = format_single_from_target(item, args_string(args, 0)); shellcmd = format_single_from_target(item, args_string(args, 0));
if (args_has(args, 'F')) { if (args_has(args, 'F')) {
if (*shellcmd != '0' && *shellcmd != '\0') if (*shellcmd != '0' && *shellcmd != '\0')
idx = 1; cmdlist = args_make_commands_now(self, item, 1, 0);
else if (count == 3) else if (count == 3)
idx = 2; cmdlist = args_make_commands_now(self, item, 2, 0);
else { else {
free(shellcmd); free(shellcmd);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
free(shellcmd); free(shellcmd);
if (cmdlist == NULL)
tree = args_to_commands(self, item, idx, NULL, 0, &cause);
if (tree == NULL) {
cmdq_error(item, "%s", cause);
free(cause);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
new_item = cmd_invoke_get(tree, cmdq_get_state(item), NULL);
cmd_parse_free(tree);
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
cdata = xcalloc(1, sizeof *cdata); cdata = xcalloc(1, sizeof *cdata);
cdata->cmd_if = args_to_commands(self, item, 1, NULL, 0, &cause);
if (cdata->cmd_if == NULL) { cdata->cmd_if = args_make_commands_prepare(self, item, 1, NULL, wait,
cmdq_error(item, "%s", cause); 0);
goto fail;
}
if (count == 3) { if (count == 3) {
cdata->cmd_else = args_to_commands(self, item, 2, NULL, 0, cdata->cmd_else = args_make_commands_prepare(self, item, 2,
&cause); NULL, wait, 0);
if (cdata->cmd_else == NULL) {
cmdq_error(item, "%s", cause);
goto fail;
}
} }
if (!wait) if (wait) {
cdata->client = tc;
else {
cdata->client = cmdq_get_client(item); cdata->client = cmdq_get_client(item);
cdata->item = item; cdata->item = item;
} } else
cdata->client = tc;
if (cdata->client != NULL) if (cdata->client != NULL)
cdata->client->references++; cdata->client->references++;
@@ -137,19 +124,15 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item)
cmd_if_shell_callback, cmd_if_shell_free, cdata, 0, -1, cmd_if_shell_callback, cmd_if_shell_free, cdata, 0, -1,
-1) == NULL) { -1) == NULL) {
cmdq_error(item, "failed to run command: %s", shellcmd); cmdq_error(item, "failed to run command: %s", shellcmd);
goto fail; free(shellcmd);
cmd_if_shell_free(cdata);
return (CMD_RETURN_ERROR);
} }
free(shellcmd); free(shellcmd);
if (!wait) if (!wait)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
return (CMD_RETURN_WAIT); return (CMD_RETURN_WAIT);
fail:
free(cause);
free(shellcmd);
cmd_if_shell_free(cdata);
return (CMD_RETURN_ERROR);
} }
static void static void
@@ -158,23 +141,35 @@ cmd_if_shell_callback(struct job *job)
struct cmd_if_shell_data *cdata = job_get_data(job); struct cmd_if_shell_data *cdata = job_get_data(job);
struct client *c = cdata->client; struct client *c = cdata->client;
struct cmdq_item *item = cdata->item, *new_item; struct cmdq_item *item = cdata->item, *new_item;
struct cmd_parse_tree *tree; struct args_command_state *state;
struct cmd_list *cmdlist;
char *error;
int status; int status;
status = job_get_status(job); status = job_get_status(job);
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
tree = cdata->cmd_else; state = cdata->cmd_else;
else else
tree = cdata->cmd_if; state = cdata->cmd_if;
if (tree == NULL) if (state == NULL)
goto out; goto out;
if (item == NULL) { cmdlist = args_make_commands(state, 0, NULL, &error);
new_item = cmd_invoke_get(tree, NULL, NULL); if (cmdlist == NULL) {
if (cdata->item == NULL) {
*error = toupper((u_char)*error);
status_message_set(c, -1, 1, 0, 0, "%s", error);
} else
cmdq_error(cdata->item, "%s", error);
free(error);
} else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item); cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else { } else {
new_item = cmd_invoke_get(tree, cmdq_get_state(item), NULL); new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
} }
out: out:
@@ -191,8 +186,8 @@ cmd_if_shell_free(void *data)
server_client_unref(cdata->client); server_client_unref(cdata->client);
if (cdata->cmd_else != NULL) if (cdata->cmd_else != NULL)
cmd_parse_free(cdata->cmd_else); args_make_commands_free(cdata->cmd_else);
if (cdata->cmd_if != NULL) args_make_commands_free(cdata->cmd_if);
cmd_parse_free(cdata->cmd_if);
free(cdata); free(cdata);
} }
-594
View File
@@ -1,594 +0,0 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicm@users.sourceforge.net>
*
* 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 <sys/types.h>
#include <pwd.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
#include "tmux-parser.h"
/* One frame in the parse tree stack. */
struct cmd_invoke_frame {
struct cmd_parse_tree *tree;
struct cmd_parse_node *node;
struct cmd_parse_node *next;
struct cmd_parse_node *end;
};
/* Shared state for a parsed command tree invocation. */
struct cmd_invoke_state {
u_int references;
struct cmd_parse_tree *tree;
struct cmd_parse_tree *current_tree;
struct cmd_invoke_frame *stack;
u_int nstack;
enum cmd_retval last;
int have_last;
int argc;
char **argv;
};
static void cmd_invoke_push(struct cmd_invoke_state *,
struct cmd_parse_tree *, struct cmd_parse_node *,
struct cmd_parse_node *, struct cmd_parse_node *);
static struct cmd_parse_tree *cmd_invoke_tree(struct cmd_invoke_state *);
static struct cmd_parse_node *cmd_invoke_next(struct cmd_invoke_state *);
static void cmd_invoke_skip_sequence(struct cmd_invoke_state *);
static int cmd_invoke_expand_string(struct cmdq_item *,
struct cmd_invoke_state *, struct cmd_parse_node *,
char **);
static int cmd_invoke_assignment(struct cmdq_item *,
struct cmd_invoke_state *, struct cmd_parse_node *);
static int cmd_invoke_if(struct cmdq_item *, struct cmd_invoke_state *,
struct cmd_parse_node *);
static struct cmd *cmd_invoke_build_command(struct cmdq_item *,
struct cmd_invoke_state *, struct cmd_parse_node *);
static int cmd_invoke_read_only(struct cmdq_item *, struct cmd *);
/* Push a node's child range onto the traversal stack. */
static void
cmd_invoke_push(struct cmd_invoke_state *is, struct cmd_parse_tree *tree,
struct cmd_parse_node *node, struct cmd_parse_node *first,
struct cmd_parse_node *end)
{
u_int n = is->nstack + 1;
is->stack = xreallocarray(is->stack, n, sizeof *is->stack);
is->stack[is->nstack].tree = cmd_parse_add_ref(tree);
is->stack[is->nstack].node = node;
is->stack[is->nstack].next = first;
is->stack[is->nstack].end = end;
is->nstack = n;
}
static struct cmd_parse_tree *
cmd_invoke_tree(struct cmd_invoke_state *is)
{
if (is->current_tree != NULL)
return (is->current_tree);
return (is->tree);
}
/* Return the next node to execute from the traversal stack. */
static struct cmd_parse_node *
cmd_invoke_next(struct cmd_invoke_state *is)
{
struct cmd_invoke_frame *frame;
struct cmd_parse_node *node;
for (;;) {
if (is->nstack == 0)
return (NULL);
frame = &is->stack[is->nstack - 1];
if (frame->next != NULL && frame->next != frame->end)
break;
cmd_parse_free(frame->tree);
is->nstack--;
}
node = frame->next;
frame->next = cmd_parse_node_next(node);
is->current_tree = frame->tree;
return (node);
}
/*
* Skip the rest of the active sequence after a command failure. Failure scope
* is the active CMD_PARSE_SEQUENCE. Discard nested frames and advance only as
* far as the end of that sequence.
*/
static void
cmd_invoke_skip_sequence(struct cmd_invoke_state *is)
{
enum cmd_parse_node_type type;
u_int i;
for (i = is->nstack; i > 0; i--) {
type = cmd_parse_node_type(is->stack[i - 1].node);
if (type == CMD_PARSE_SEQUENCE) {
is->stack[i - 1].next = is->stack[i - 1].end;
while (is->nstack > i)
cmd_parse_free(is->stack[--is->nstack].tree);
is->nstack = i;
break;
}
}
is->have_last = 0;
}
/* Append a string to a dynamically allocated buffer. */
static void
cmd_invoke_append(char **buf, size_t *len, const char *s)
{
size_t slen;
if (s != NULL) {
slen = strlen(s);
*buf = xrealloc(*buf, *len + slen + 1);
memcpy(*buf + *len, s, slen + 1);
*len += slen;
}
}
/* Look up an environment variable. */
static const char *
cmd_invoke_getenv(struct cmdq_item *item, const char *name)
{
struct client *c = cmdq_get_client(item);
struct environ_entry *envent;
if (c != NULL && c->environ != NULL) {
envent = environ_find(c->environ, name);
if (envent != NULL && envent->value != NULL)
return (envent->value);
}
envent = environ_find(global_environ, name);
if (envent != NULL && envent->value != NULL)
return (envent->value);
return ("");
}
/* Resolve a tilde expansion to a home directory. */
static const char *
cmd_invoke_tilde(const char *name)
{
struct passwd *pw;
if (name == NULL || *name == '\0')
return (find_home());
if ((pw = getpwnam(name)) == NULL)
return ("");
return (pw->pw_dir);
}
/* Expand a parsed string node into an argv string. */
static int
cmd_invoke_expand_string(struct cmdq_item *item, struct cmd_invoke_state *is,
struct cmd_parse_node *node, char **out)
{
struct cmd_parse_node *child;
const char *s, *value;
char *buf = NULL, *new;
size_t len = 0;
int i;
child = cmd_parse_node_first_child(node);
while (child != NULL) {
value = cmd_parse_node_value(child);
switch (cmd_parse_node_type(child)) {
case CMD_PARSE_TEXT:
s = value;
break;
case CMD_PARSE_ENVIRONMENT:
s = cmd_invoke_getenv(item, value);
break;
case CMD_PARSE_TILDE:
s = cmd_invoke_tilde(value);
break;
default:
fatalx("unexpected node type in string");
}
cmd_invoke_append(&buf, &len, s);
child = cmd_parse_node_next(child);
}
if (buf == NULL)
buf = xstrdup("");
for (i = 0; i < is->argc; i++) {
new = cmd_template_replace(buf, is->argv[i], i + 1);
free(buf);
buf = new;
}
*out = buf;
return (0);
}
/* Execute an assignment node by updating the environment. */
static int
cmd_invoke_assignment(struct cmdq_item *item, struct cmd_invoke_state *is,
struct cmd_parse_node *node)
{
struct cmd_parse_node *value;
const char *name;
char *expanded;
int flags = 0;
value = cmd_parse_node_first_child(node);
if (value == NULL)
return (-1);
if (cmd_invoke_expand_string(item, is, value, &expanded) != 0)
return (-1);
name = cmd_parse_node_value(node);
if (cmd_parse_node_type(node) == CMD_PARSE_HIDDEN_ASSIGN)
flags |= ENVIRON_HIDDEN;
environ_set(global_environ, name, flags, "%s", expanded);
free(expanded);
return (0);
}
/* Expand and evaluate a conditional expression. */
static int
cmd_invoke_is_true(struct cmdq_item *item,
struct cmd_invoke_state *is, struct cmd_parse_node *node, int *result)
{
struct format_tree *ft;
char *s, *expanded;
if (cmd_invoke_expand_string(item, is, node, &s) != 0)
return (-1);
ft = format_create_from_target(item);
expanded = format_expand(ft, s);
*result = format_true(expanded);
free(expanded);
format_free(ft);
free(s);
return (0);
}
/* Skip else branches. */
static struct cmd_parse_node *
cmd_invoke_if_branch_end(struct cmd_parse_node *first)
{
struct cmd_parse_node *node;
for (node = first; node != NULL; node = cmd_parse_node_next(node)) {
switch (cmd_parse_node_type(node)) {
case CMD_PARSE_ELIF:
case CMD_PARSE_ELSE:
return (node);
default:
break;
}
}
return (NULL);
}
/* Select and queue the branch for a conditional node. */
static int
cmd_invoke_if(struct cmdq_item *item, struct cmd_invoke_state *is,
struct cmd_parse_node *node)
{
struct cmd_parse_tree *tree = cmd_invoke_tree(is);
struct cmd_parse_node *child, *first, *next;
int r;
next = cmd_parse_node_first_child(node);
if (next == NULL)
return (0);
first = cmd_parse_node_next(next);
if (cmd_invoke_is_true(item, is, next, &r) != 0)
return (-1);
if (r) {
next = cmd_invoke_if_branch_end(first);
cmd_invoke_push(is, tree, node, first, next);
return (0);
}
for (child = first; child != NULL; child = cmd_parse_node_next(child)) {
switch (cmd_parse_node_type(child)) {
case CMD_PARSE_ELIF:
next = cmd_parse_node_first_child(child);
if (next == NULL)
break;
if (cmd_invoke_is_true(item, is, next, &r) != 0)
return (-1);
if (r) {
next = cmd_parse_node_next(next);
cmd_invoke_push(is, tree, child, next, NULL);
return (0);
}
break;
case CMD_PARSE_ELSE:
next = cmd_parse_node_first_child(child);
cmd_invoke_push(is, tree, child, next, NULL);
return (0);
default:
break;
}
}
return (0);
}
/* Report an error from an invoke item. */
static void
cmd_invoke_error(struct cmdq_item *item, struct cmd_invoke_state *is,
struct cmd_parse_node *node, const char *cause)
{
struct client *c = cmdq_get_client(item);
struct cmd_parse_tree *tree = cmd_invoke_tree(is);
const char *file = cmd_parse_file(tree);
u_int line = cmd_parse_node_line(node);
if (c != NULL && (~c->flags & CLIENT_CONTROL || file == NULL)) {
cmdq_error(item, "%s", cause);
return;
}
if (file != NULL)
cfg_add_cause("%s:%u: %s", file, line, cause);
else
cfg_add_cause("%s", cause);
}
/* Build one command from a parsed command node. */
static struct cmd *
cmd_invoke_build_command(struct cmdq_item *item, struct cmd_invoke_state *is,
struct cmd_parse_node *node)
{
struct cmd_parse_tree *tree = cmd_invoke_tree(is);
struct cmd_parse_node *child;
struct args_value *values = NULL;
struct cmd *cmd;
const char *file = cmd_parse_file(tree);
u_int line = cmd_parse_node_line(node);
int flags = cmd_parse_flags(tree);
char *cause = NULL;
u_int count = 0;
child = cmd_parse_node_first_child(node);
while (child != NULL) {
values = xreallocarray(values, count + 1, sizeof *values);
memset(&values[count], 0, sizeof values[count]);
switch (cmd_parse_node_type(child)) {
case CMD_PARSE_STRING:
values[count].type = ARGS_STRING;
if (cmd_invoke_expand_string(item, is, child,
&values[count].string) != 0)
goto fail;
break;
case CMD_PARSE_COMMANDS:
values[count].type = ARGS_COMMANDS;
values[count].cmd = cmd_parse_from_node(tree, child);
break;
default:
fatalx("unexpected node type in command");
}
count++;
child = cmd_parse_node_next(child);
}
cmd = cmd_parse(values, count, file, line, flags, &cause);
if (cmd == NULL) {
cmd_invoke_error(item, is, node, cause);
free(cause);
goto fail;
}
args_free_values(values, count);
free(values);
return (cmd);
fail:
args_free_values(values, count);
free(values);
return (NULL);
}
static int
cmd_invoke_read_only(struct cmdq_item *item, struct cmd *cmd)
{
struct client *c = cmdq_get_client(item);
const struct cmd_entry *entry = cmd_get_entry(cmd);
if (c == NULL || (~c->flags & CLIENT_READONLY))
return (0);
if (entry->flags & CMD_READONLY)
return (0);
cmdq_error(item, "client is read-only");
return (-1);
}
/* Create the first invoke queue item for a parsed tree. */
struct cmdq_item *
cmd_invoke_get(struct cmd_parse_tree *tree, struct cmdq_state *state,
const struct cmd_invoke_input *input)
{
struct cmd_invoke_state *is;
struct cmd_parse_node *root = cmd_parse_root(tree), *first;
struct cmdq_item *item;
struct cmd_invoke_input new_input = { 0 };
int i;
if (input == NULL)
input = &new_input;
is = xcalloc(1, sizeof *is);
is->references = 1;
is->tree = cmd_parse_add_ref(tree);
is->argc = input->argc;
if (input->argc != 0) {
is->argv = xreallocarray(NULL, input->argc, sizeof *is->argv);
for (i = 0; i < input->argc; i++)
is->argv[i] = xstrdup(input->argv[i]);
}
first = cmd_parse_node_first_child(root);
cmd_invoke_push(is, tree, root, first, NULL);
item = cmdq_get_invoke(is, state);
cmd_invoke_state_free(is);
return (item);
}
/* Add a reference to shared invoke state. */
struct cmd_invoke_state *
cmd_invoke_state_add_ref(struct cmd_invoke_state *is)
{
is->references++;
return (is);
}
/* Release a reference to shared invoke state. */
void
cmd_invoke_state_free(struct cmd_invoke_state *is)
{
int i;
u_int j;
if (is == NULL)
return;
if (--is->references != 0)
return;
for (i = 0; i < is->argc; i++)
free(is->argv[i]);
free(is->argv);
for (j = 0; j < is->nstack; j++)
cmd_parse_free(is->stack[j].tree);
cmd_parse_free(is->tree);
free(is->stack);
free(is);
}
/* Record the result from the last command item. */
void
cmd_invoke_result(struct cmd_invoke_state *is, enum cmd_retval retval)
{
is->last = retval;
is->have_last = 1;
}
/* Fire an invoke item and queue the next command plus continuation. */
enum cmd_retval
cmd_invoke_fire(struct cmdq_item *item, struct cmd_invoke_state *is)
{
struct cmd_parse_node *node, *first;
struct cmd_parse_tree *tree, *alias;
struct cmdq_item *new_item, *next;
struct cmdq_state *state;
struct client *c;
struct cmd *cmd;
const char *file;
char *cause;
int r;
if (is->have_last && is->last == CMD_RETURN_ERROR)
cmd_invoke_skip_sequence(is);
else
is->have_last = 0;
for (;;) {
node = cmd_invoke_next(is);
if (node == NULL)
return (CMD_RETURN_NORMAL);
cmd_parse_log_node(__func__, node);
tree = cmd_invoke_tree(is);
switch (cmd_parse_node_type(node)) {
case CMD_PARSE_ROOT:
case CMD_PARSE_SEQUENCE:
first = cmd_parse_node_first_child(node);
cmd_invoke_push(is, tree, node, first, NULL);
break;
case CMD_PARSE_ASSIGN:
case CMD_PARSE_HIDDEN_ASSIGN:
if (cmd_invoke_assignment(item, is, node) == 0)
break;
cmd_invoke_error(item, is, node, "bad assignment");
cmd_invoke_skip_sequence(is);
break;
case CMD_PARSE_IF:
if (cmd_invoke_if(item, is, node) == 0)
break;
cmd_invoke_error(item, is, node, "bad condition");
cmd_invoke_skip_sequence(is);
break;
case CMD_PARSE_ELIF:
case CMD_PARSE_ELSE:
break;
case CMD_PARSE_COMMAND:
r = cmd_parse_expand_alias(tree, node, &alias, &cause);
if (r == 1) {
node = cmd_parse_root(alias);
first = cmd_parse_node_first_child(node);
cmd_invoke_push(is, alias, node, first, NULL);
cmd_parse_free(alias);
break;
}
if (r == -1) {
c = cmdq_get_client(item);
file = cmd_parse_file(tree);
if (c != NULL &&
(~c->flags & CLIENT_CONTROL || file == NULL))
cmdq_error(item, "%s", cause);
else
cfg_add_cause("%s", cause);
free(cause);
cmd_invoke_skip_sequence(is);
break;
}
cmd = cmd_invoke_build_command(item, is, node);
if (cmd == NULL) {
cmd_invoke_skip_sequence(is);
break;
}
if (cmd_invoke_read_only(item, cmd) != 0) {
cmd_free(cmd);
cmd_invoke_skip_sequence(is);
break;
}
/*
* Queue one command followed by this walker. WAIT and
* command-inserted items therefore run before resume.
*/
state = cmdq_get_state(item);
new_item = cmdq_get_one_command(cmd, state, is);
next = cmdq_get_invoke(is, state);
cmdq_insert_after(item, next);
cmdq_insert_after(item, new_item);
return (CMD_RETURN_NORMAL);
case CMD_PARSE_STRING:
case CMD_PARSE_COMMANDS:
case CMD_PARSE_TEXT:
case CMD_PARSE_ENVIRONMENT:
case CMD_PARSE_TILDE:
fatalx("unexpected node type");
}
}
}
+22 -7
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-join-pane.c,v 1.74 2026/08/03 20:29:52 nicm Exp $ */ /* $OpenBSD: cmd-join-pane.c,v 1.75 2026/09/21 10:33:16 nicm Exp $ */
/* /*
* Copyright (c) 2011 George Nachman <tmux@georgester.com> * Copyright (c) 2011 George Nachman <tmux@georgester.com>
@@ -133,7 +133,7 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
} else if (strcmp(position, "back") == 0) { } else if (strcmp(position, "back") == 0) {
TAILQ_REMOVE(&w->z_index, wp, zentry); TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_FOREACH(owp, &w->z_index, zentry) { TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp)) if (!window_pane_is_floating_with_hidden(owp))
break; break;
} }
if (owp != NULL) if (owp != NULL)
@@ -142,24 +142,30 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
TAILQ_INSERT_TAIL(&w->z_index, wp, zentry); TAILQ_INSERT_TAIL(&w->z_index, wp, zentry);
} else if (strcmp(position, "forward") == 0) { } else if (strcmp(position, "forward") == 0) {
owp = TAILQ_PREV(wp, window_panes_zindex, zentry); owp = TAILQ_PREV(wp, window_panes_zindex, zentry);
while (owp != NULL && owp->layout_cell == NULL)
owp = TAILQ_PREV(owp, window_panes_zindex, zentry);
if (owp != NULL) { if (owp != NULL) {
TAILQ_REMOVE(&w->z_index, wp, zentry); TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_BEFORE(owp, wp, zentry); TAILQ_INSERT_BEFORE(owp, wp, zentry);
} }
} else if (strcmp(position, "backward") == 0) { } else if (strcmp(position, "backward") == 0) {
owp = TAILQ_NEXT(wp, zentry); owp = TAILQ_NEXT(wp, zentry);
while (owp != NULL && owp->layout_cell == NULL)
owp = TAILQ_NEXT(owp, zentry);
if (owp != NULL && window_pane_is_floating(owp)) { if (owp != NULL && window_pane_is_floating(owp)) {
TAILQ_REMOVE(&w->z_index, wp, zentry); TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_AFTER(&w->z_index, owp, wp, zentry); TAILQ_INSERT_AFTER(&w->z_index, owp, wp, zentry);
} }
} else if (strcmp(position, "forward-loop") == 0) { } else if (strcmp(position, "forward-loop") == 0) {
owp = TAILQ_PREV(wp, window_panes_zindex, zentry); owp = TAILQ_PREV(wp, window_panes_zindex, zentry);
while (owp != NULL && owp->layout_cell == NULL)
owp = TAILQ_PREV(owp, window_panes_zindex, zentry);
TAILQ_REMOVE(&w->z_index, wp, zentry); TAILQ_REMOVE(&w->z_index, wp, zentry);
if (owp != NULL) if (owp != NULL)
TAILQ_INSERT_BEFORE(owp, wp, zentry); TAILQ_INSERT_BEFORE(owp, wp, zentry);
else { else {
TAILQ_FOREACH(owp, &w->z_index, zentry) { TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp)) if (!window_pane_is_floating_with_hidden(owp))
break; break;
} }
if (owp != NULL) if (owp != NULL)
@@ -169,6 +175,8 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
} }
} else if (strcmp(position, "backward-loop") == 0) { } else if (strcmp(position, "backward-loop") == 0) {
owp = TAILQ_NEXT(wp, zentry); owp = TAILQ_NEXT(wp, zentry);
while (owp != NULL && owp->layout_cell == NULL)
owp = TAILQ_NEXT(owp, zentry);
if (owp != NULL && window_pane_is_floating(owp)) { if (owp != NULL && window_pane_is_floating(owp)) {
TAILQ_REMOVE(&w->z_index, wp, zentry); TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_AFTER(&w->z_index, owp, wp, zentry); TAILQ_INSERT_AFTER(&w->z_index, owp, wp, zentry);
@@ -299,7 +307,7 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
struct window *w; struct window *w;
struct window_pane *wp; struct window_pane *wp;
struct layout_cell *lc; struct layout_cell *lc;
int y, ly, x, lx; int y, ly, x, lx, oxoff, oyoff, osx, osy;
wp = cmd_mouse_pane(m, NULL, &wl); wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) { if (wp == NULL) {
@@ -321,10 +329,16 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
ly = m->statusat - 1; ly = m->statusat - 1;
if (x != lx || y != ly) { if (x != lx || y != ly) {
oxoff = wp->xoff;
oyoff = wp->yoff;
osx = wp->sx;
osy = wp->sy;
lc->g.xoff += x - lx; lc->g.xoff += x - lx;
lc->g.yoff += y - ly; lc->g.yoff += y - ly;
layout_fix_panes(w, NULL); layout_fix_panes(w, NULL);
server_redraw_window(w);
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
server_redraw_window_borders(w); server_redraw_window_borders(w);
} }
} }
@@ -347,8 +361,10 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl,
n = 0; n = 0;
TAILQ_FOREACH(owp, &w->z_index, zentry) { TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp)) if (!window_pane_is_floating_with_hidden(owp))
break; break;
if (owp->layout_cell == NULL)
continue;
if (n >= z) if (n >= z)
break; break;
n++; n++;
@@ -443,7 +459,6 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "pane is not floating"); cmdq_error(item, "pane is not floating");
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
server_unzoom_window(dst_w);
if ((s = args_get(args, 'P')) != NULL) if ((s = args_get(args, 'P')) != NULL)
return (cmd_join_pane_place(item, dst_wl, dst_wp, s)); return (cmd_join_pane_place(item, dst_wl, dst_wp, s));
if ((s = args_get(args, 'z')) != NULL) if ((s = args_get(args, 'z')) != NULL)
+13 -27
View File
@@ -37,16 +37,6 @@
"-T #{p|#{key_table_width}:key_table} " \ "-T #{p|#{key_table_width}:key_table} " \
"#{p|#{key_string_width}:#{q|a:key_string}} " \ "#{p|#{key_string_width}:#{q|a:key_string}} " \
"#{key_command}}" "#{key_command}}"
#define LIST_KEYS_TEMPLATE_PRETTY \
"#{?notes_only," \
"#{key_prefix} " \
"#{key_string} " \
"#{?key_note,#{key_note},#{key_command}}" \
"," \
"bind-key #{?key_repeat,-r ,}" \
"-T #{key_table} " \
"#{q|a:key_string} " \
"#{key_command}}"
static enum cmd_retval cmd_list_keys_exec(struct cmd *, struct cmdq_item *); static enum cmd_retval cmd_list_keys_exec(struct cmd *, struct cmdq_item *);
@@ -54,8 +44,8 @@ const struct cmd_entry cmd_list_keys_entry = {
.name = "list-keys", .name = "list-keys",
.alias = "lsk", .alias = "lsk",
.args = { "1aF:NO:P:prT:", 0, 1, NULL }, .args = { "1aF:NO:P:rT:", 0, 1, NULL },
.usage = "[-1aNpr] [-F format] [-O order] [-P prefix-string]" .usage = "[-1aNr] [-F format] [-O order] [-P prefix-string]"
"[-T key-table] [key]", "[-T key-table] [key]",
.flags = CMD_STARTSERVER|CMD_AFTERHOOK, .flags = CMD_STARTSERVER|CMD_AFTERHOOK,
@@ -129,7 +119,7 @@ cmd_list_keys_get_root_and_prefix(u_int *n, struct sort_criteria *sort_crit)
} }
static void static void
cmd_list_keys_do_filter(int filter_notes, int filter_key, key_code only, cmd_list_keys_filter_key_list(int filter_notes, int filter_key, key_code only,
struct key_binding **l, u_int *n) struct key_binding **l, u_int *n)
{ {
key_code key; key_code key;
@@ -147,8 +137,8 @@ cmd_list_keys_do_filter(int filter_notes, int filter_key, key_code only,
} }
static void static void
cmd_list_keys_format_key_binding(struct format_tree *ft, cmd_list_keys_format_add_key_binding(struct format_tree *ft,
const struct key_binding *bd, const char *prefix, int flags) const struct key_binding *bd, const char *prefix)
{ {
char *s; char *s;
@@ -167,7 +157,8 @@ cmd_list_keys_format_key_binding(struct format_tree *ft,
format_add(ft, "key_string", "%s", key_string_lookup_key(bd->key, 0)); format_add(ft, "key_string", "%s", key_string_lookup_key(bd->key, 0));
s = cmd_parse_print(bd->cmd, flags); s = cmd_list_print(bd->cmdlist, CMD_LIST_PRINT_ESCAPED|
CMD_LIST_PRINT_NO_GROUPS);
format_add(ft, "key_command", "%s", s); format_add(ft, "key_command", "%s", s);
free(s); free(s);
} }
@@ -186,7 +177,6 @@ cmd_list_keys_exec(struct cmd *self, struct cmdq_item *item)
char *prefix = NULL; char *prefix = NULL;
u_int i, n; u_int i, n;
int single, notes_only, filter_notes, filter_key; int single, notes_only, filter_notes, filter_key;
int print_flags = 0;
struct sort_criteria sort_crit; struct sort_criteria sort_crit;
if ((keystr = args_string(args, 0)) != NULL) { if ((keystr = args_string(args, 0)) != NULL) {
@@ -216,15 +206,9 @@ cmd_list_keys_exec(struct cmd *self, struct cmdq_item *item)
prefix = cmd_list_keys_get_prefix(args); prefix = cmd_list_keys_get_prefix(args);
single = args_has(args, '1'); single = args_has(args, '1');
notes_only = args_has(args, 'N'); notes_only = args_has(args, 'N');
if (args_has(args, 'p'))
print_flags |= CMD_PARSE_PRINT_MULTILINE;
if ((template = args_get(args, 'F')) == NULL) { if ((template = args_get(args, 'F')) == NULL)
if (print_flags & CMD_PARSE_PRINT_MULTILINE)
template = LIST_KEYS_TEMPLATE_PRETTY;
else
template = LIST_KEYS_TEMPLATE; template = LIST_KEYS_TEMPLATE;
}
if (table) if (table)
l = sort_get_key_bindings_table(table, &n, &sort_crit); l = sort_get_key_bindings_table(table, &n, &sort_crit);
@@ -235,8 +219,10 @@ cmd_list_keys_exec(struct cmd *self, struct cmdq_item *item)
filter_notes = notes_only && !args_has(args, 'a'); filter_notes = notes_only && !args_has(args, 'a');
filter_key = only != KEYC_UNKNOWN; filter_key = only != KEYC_UNKNOWN;
if (filter_notes || filter_key) if (filter_notes || filter_key) {
cmd_list_keys_do_filter(filter_notes, filter_key, only, l, &n); cmd_list_keys_filter_key_list(filter_notes, filter_key, only, l,
&n);
}
if (filter_key && n == 0) { if (filter_key && n == 0) {
cmdq_error(item, "unknown key: %s", keystr); cmdq_error(item, "unknown key: %s", keystr);
free(prefix); free(prefix);
@@ -253,7 +239,7 @@ cmd_list_keys_exec(struct cmd *self, struct cmdq_item *item)
format_add(ft, "key_table_width", "%u", format_add(ft, "key_table_width", "%u",
cmd_list_keys_get_table_width(l, n)); cmd_list_keys_get_table_width(l, n));
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
cmd_list_keys_format_key_binding(ft, l[i], prefix, print_flags); cmd_list_keys_format_add_key_binding(ft, l[i], prefix);
line = format_expand(ft, template); line = format_expand(ft, template);
if (single && tc != NULL && (~tc->flags & CLIENT_CONTROL)) if (single && tc != NULL && (~tc->flags & CLIENT_CONTROL))
+3 -2
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-list-panes.c,v 1.40 2026/06/01 14:01:09 nicm Exp $ */ /* $OpenBSD: cmd-list-panes.c,v 1.41 2026/09/08 10:20:08 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -97,6 +97,7 @@ cmd_list_panes_window(struct cmd *self, struct session *s, struct winlink *wl,
struct cmdq_item *item, int type) struct cmdq_item *item, int type)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_client(item);
struct window_pane *wp, **l; struct window_pane *wp, **l;
u_int i, n; u_int i, n;
struct format_tree *ft; struct format_tree *ft;
@@ -147,7 +148,7 @@ cmd_list_panes_window(struct cmd *self, struct session *s, struct winlink *wl,
wp = l[i]; wp = l[i];
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_add(ft, "line", "%u", n); format_add(ft, "line", "%u", n);
format_defaults(ft, NULL, s, wl, wp); format_defaults(ft, c, s, wl, wp);
if (filter != NULL) { if (filter != NULL) {
expanded = format_expand(ft, filter); expanded = format_expand(ft, filter);
+3 -2
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-list-sessions.c,v 1.36 2026/02/27 08:25:12 nicm Exp $ */ /* $OpenBSD: cmd-list-sessions.c,v 1.37 2026/09/08 10:20:08 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -53,6 +53,7 @@ static enum cmd_retval
cmd_list_sessions_exec(struct cmd *self, struct cmdq_item *item) cmd_list_sessions_exec(struct cmd *self, struct cmdq_item *item)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_client(item);
struct session **l; struct session **l;
u_int n, i; u_int n, i;
struct format_tree *ft; struct format_tree *ft;
@@ -76,7 +77,7 @@ cmd_list_sessions_exec(struct cmd *self, struct cmdq_item *item)
for (i = 0; i < n; i++) { for (i = 0; i < n; i++) {
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_add(ft, "line", "%u", i); format_add(ft, "line", "%u", i);
format_defaults(ft, NULL, l[i], NULL, NULL); format_defaults(ft, c, l[i], NULL, NULL);
if (filter != NULL) { if (filter != NULL) {
expanded = format_expand(ft, filter); expanded = format_expand(ft, filter);
+3 -2
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-list-windows.c,v 1.50 2026/02/27 08:25:12 nicm Exp $ */ /* $OpenBSD: cmd-list-windows.c,v 1.51 2026/09/08 10:20:08 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -60,6 +60,7 @@ cmd_list_windows_exec(struct cmd *self, struct cmdq_item *item)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
struct client *c = cmdq_get_client(item);
struct winlink *wl, **l; struct winlink *wl, **l;
struct session *s; struct session *s;
u_int i, n; u_int i, n;
@@ -94,7 +95,7 @@ cmd_list_windows_exec(struct cmd *self, struct cmdq_item *item)
s = wl->session; s = wl->session;
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0); ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_add(ft, "line", "%u", n); format_add(ft, "line", "%u", n);
format_defaults(ft, NULL, s, wl, NULL); format_defaults(ft, c, s, wl, NULL);
if (filter != NULL) { if (filter != NULL) {
expanded = format_expand(ft, filter); expanded = format_expand(ft, filter);
+17 -7
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-new-window.c,v 1.103 2026/07/08 08:07:42 nicm Exp $ */ /* $OpenBSD: cmd-new-window.c,v 1.104 2026/09/03 21:04:11 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -60,7 +60,8 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
struct client *tc = cmdq_get_target_client(item); struct client *tc = cmdq_get_target_client(item);
struct session *s = target->s; struct session *s = target->s;
struct winlink *wl = target->wl, *new_wl = NULL; struct winlink *wl = target->wl, *new_wl = NULL;
int idx = target->idx, before, count = args_count(args); int idx = target->idx, before;
int count = args_count(args);
char *cause = NULL, *cp, *expanded, *wname = NULL; char *cause = NULL, *cp, *expanded, *wname = NULL;
const char *template, *name; const char *template, *name;
struct cmd_find_state fs; struct cmd_find_state fs;
@@ -74,8 +75,10 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
} }
/* /*
* If -S and -n are given and -t is not and a single window with this * If -S is given, select an existing window instead of creating a
* name already exists, select it. * new one: preferring -t if it already points at a window, or
* otherwise -n if one exists with that name. If neither matches,
* fall through and create a window as normal.
*/ */
name = args_get(args, 'n'); name = args_get(args, 'n');
if (name != NULL) { if (name != NULL) {
@@ -88,7 +91,10 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
wname = clean_name(expanded, 0); wname = clean_name(expanded, 0);
free(expanded); free(expanded);
} }
if (args_has(args, 'S') && wname != NULL && target->idx == -1) { if (args_has(args, 'S')) {
if (idx != -1)
new_wl = winlink_find_by_index(&s->windows, idx);
else if (wname != NULL) {
expanded = format_single(item, wname, c, s, NULL, NULL); expanded = format_single(item, wname, c, s, NULL, NULL);
RB_FOREACH(wl, winlinks, &s->windows) { RB_FOREACH(wl, winlinks, &s->windows) {
if (strcmp(wl->window->name, expanded) != 0) if (strcmp(wl->window->name, expanded) != 0)
@@ -97,12 +103,17 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
new_wl = wl; new_wl = wl;
continue; continue;
} }
cmdq_error(item, "multiple windows named %s", wname); cmdq_error(item, "multiple windows named %s",
wname);
free(wname); free(wname);
free(expanded); free(expanded);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
free(expanded); free(expanded);
}
}
/* Found an existing window to select instead of creating a new one. */
if (new_wl != NULL) { if (new_wl != NULL) {
free(wname); free(wname);
if (args_has(args, 'd')) if (args_has(args, 'd'))
@@ -114,7 +125,6 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
recalculate_sizes(); recalculate_sizes();
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
}
before = args_has(args, 'b'); before = args_has(args, 'b');
if (args_has(args, 'a') || before) { if (args_has(args, 'a') || before) {
+1162 -1575
View File
File diff suppressed because it is too large Load Diff
+75 -43
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */ /* $OpenBSD: cmd-queue.c,v 1.124 2026/09/22 06:46:50 nicm Exp $ */
/* /*
* Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -35,7 +35,6 @@
enum cmdq_type { enum cmdq_type {
CMDQ_COMMAND, CMDQ_COMMAND,
CMDQ_CALLBACK, CMDQ_CALLBACK,
CMDQ_INVOKE,
}; };
/* Command queue item. */ /* Command queue item. */
@@ -48,6 +47,7 @@ struct cmdq_item {
struct client *target_client; struct client *target_client;
enum cmdq_type type; enum cmdq_type type;
u_int group;
u_int number; u_int number;
time_t time; time_t time;
@@ -58,8 +58,8 @@ struct cmdq_item {
struct cmd_find_state source; struct cmd_find_state source;
struct cmd_find_state target; struct cmd_find_state target;
struct cmd_list *cmdlist;
struct cmd *cmd; struct cmd *cmd;
struct cmd_invoke_state *invoke_state;
cmdq_cb cb; cmdq_cb cb;
void *data; void *data;
@@ -428,10 +428,8 @@ cmdq_remove(struct cmdq_item *item)
{ {
if (item->client != NULL) if (item->client != NULL)
server_client_unref(item->client); server_client_unref(item->client);
if (item->cmd != NULL) if (item->cmdlist != NULL)
cmd_free(item->cmd); cmd_list_free(item->cmdlist);
if (item->invoke_state != NULL)
cmd_invoke_state_free(item->invoke_state);
cmdq_free_state(item->state); cmdq_free_state(item->state);
TAILQ_REMOVE(&item->queue->list, item, entry); TAILQ_REMOVE(&item->queue->list, item, entry);
@@ -440,46 +438,75 @@ cmdq_remove(struct cmdq_item *item)
free(item); free(item);
} }
/* Get a single command for command queue with invoke state. */ /* Remove all subsequent items that match this item's group. */
struct cmdq_item * static void
cmdq_get_one_command(struct cmd *cmd, struct cmdq_state *state, cmdq_remove_group(struct cmdq_item *item)
struct cmd_invoke_state *invoke_state)
{ {
struct cmdq_item *item; struct cmdq_item *this, *next;
const struct cmd_entry *entry = cmd_get_entry(cmd);
if (item->group == 0)
return;
this = TAILQ_NEXT(item, entry);
while (this != NULL) {
next = TAILQ_NEXT(this, entry);
if (this->group == item->group)
cmdq_remove(this);
this = next;
}
}
/* Empty command callback. */
static enum cmd_retval
cmdq_empty_command(__unused struct cmdq_item *item, __unused void *data)
{
return (CMD_RETURN_NORMAL);
}
/* Get a command for the command queue. */
struct cmdq_item *
cmdq_get_command(struct cmd_list *cmdlist, struct cmdq_state *state)
{
struct cmdq_item *item, *first = NULL, *last = NULL;
struct cmd *cmd;
const struct cmd_entry *entry;
int created = 0;
if ((cmd = cmd_list_first(cmdlist)) == NULL)
return (cmdq_get_callback(cmdq_empty_command, NULL));
if (state == NULL) {
state = cmdq_new_state(NULL, NULL, 0);
created = 1;
}
while (cmd != NULL) {
entry = cmd_get_entry(cmd);
item = xcalloc(1, sizeof *item); item = xcalloc(1, sizeof *item);
xasprintf(&item->name, "[%s/%p]", entry->name, item); xasprintf(&item->name, "[%s/%p]", entry->name, item);
item->type = CMDQ_COMMAND; item->type = CMDQ_COMMAND;
item->group = cmd_get_group(cmd);
item->state = cmdq_link_state(state);
item->cmdlist = cmdlist;
item->cmd = cmd; item->cmd = cmd;
if (state == NULL) cmdlist->references++;
item->state = cmdq_new_state(NULL, NULL, 0); log_debug("%s: %s group %u", __func__, item->name, item->group);
else
item->state = cmdq_link_state(state);
if (invoke_state != NULL) if (first == NULL)
item->invoke_state = cmd_invoke_state_add_ref(invoke_state); first = item;
if (last != NULL)
last->next = item;
last = item;
log_debug("%s: %s", __func__, item->name); cmd = cmd_list_next(cmd);
return (item); }
}
/* Get an invoke continuation for command queue. */ if (created)
struct cmdq_item * cmdq_free_state(state);
cmdq_get_invoke(struct cmd_invoke_state *invoke_state, struct cmdq_state *state) return (first);
{
struct cmdq_item *item;
item = xcalloc(1, sizeof *item);
xasprintf(&item->name, "[invoke/%p]", item);
item->type = CMDQ_INVOKE;
if (state == NULL)
item->state = cmdq_new_state(NULL, NULL, 0);
else
item->state = cmdq_link_state(state);
item->invoke_state = cmd_invoke_state_add_ref(invoke_state);
return (item);
} }
/* Fill in flag for a command. */ /* Fill in flag for a command. */
@@ -553,11 +580,14 @@ cmdq_fire_command(struct cmdq_item *item)
int flags, quiet = 0; int flags, quiet = 0;
char *tmp; char *tmp;
if (item->client != NULL && (item->client->flags & CLIENT_DEAD))
return (CMD_RETURN_ERROR);
if (cfg_finished) if (cfg_finished)
cmdq_add_message(item); cmdq_add_message(item);
if (log_get_level() > 1) { if (log_get_level() > 1) {
tmp = cmd_print(cmd); tmp = cmd_print(cmd);
log_debug("%s %s: %s", __func__, name, tmp); log_debug("%s %s: (%u) %s", __func__, name, item->group, tmp);
free(tmp); free(tmp);
} }
@@ -636,6 +666,7 @@ cmdq_get_callback1(const char *name, cmdq_cb cb, void *data)
xasprintf(&item->name, "[%s/%p]", name, item); xasprintf(&item->name, "[%s/%p]", name, item);
item->type = CMDQ_CALLBACK; item->type = CMDQ_CALLBACK;
item->group = 0;
item->state = cmdq_new_state(NULL, NULL, 0); item->state = cmdq_new_state(NULL, NULL, 0);
item->cb = cb; item->cb = cb;
@@ -717,16 +748,17 @@ cmdq_next(struct client *c)
switch (item->type) { switch (item->type) {
case CMDQ_COMMAND: case CMDQ_COMMAND:
retval = cmdq_fire_command(item); retval = cmdq_fire_command(item);
if (item->invoke_state != NULL)
cmd_invoke_result(item->invoke_state, retval);
/*
* If a command returns an error, remove any
* subsequent commands in the same group.
*/
if (retval == CMD_RETURN_ERROR)
cmdq_remove_group(item);
break; break;
case CMDQ_CALLBACK: case CMDQ_CALLBACK:
retval = cmdq_fire_callback(item); retval = cmdq_fire_callback(item);
break; break;
case CMDQ_INVOKE:
retval = cmd_invoke_fire(item, item->invoke_state);
break;
default: default:
retval = CMD_RETURN_ERROR; retval = CMD_RETURN_ERROR;
break; break;
+7 -4
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-resize-pane.c,v 1.68 2026/08/31 07:44:39 nicm Exp $ */ /* $OpenBSD: cmd-resize-pane.c,v 1.69 2026/09/21 10:33:16 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -91,6 +91,7 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item)
server_redraw_window(w); server_redraw_window(w);
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
if (!window_pane_is_floating(wp))
server_unzoom_window(w); server_unzoom_window(w);
lc = wp->layout_cell; /* may have been replaced by unzoom */ lc = wp->layout_cell; /* may have been replaced by unzoom */
@@ -236,8 +237,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
struct window_pane *wp; struct window_pane *wp;
struct layout_cell *lc; struct layout_cell *lc;
int y, ly, x, lx, sx, sy, new_sx, new_sy; int y, ly, x, lx, sx, sy, new_sx, new_sy;
int left, right; int left, right, resizes = 0;
int new_xoff, new_yoff, resizes = 0; int new_xoff, new_yoff, old_xoff, old_yoff;
wp = cmd_mouse_pane(m, NULL, &wl); wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) { if (wp == NULL) {
@@ -248,6 +249,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
lc = wp->layout_cell; lc = wp->layout_cell;
sx = wp->sx; sx = wp->sx;
sy = wp->sy; sy = wp->sy;
old_xoff = wp->xoff;
old_yoff = wp->yoff;
left = wp->xoff - 1; left = wp->xoff - 1;
right = wp->xoff + sx; right = wp->xoff + sx;
if (window_pane_scrollbar_reserve(wp) && if (window_pane_scrollbar_reserve(wp) &&
@@ -347,7 +350,7 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
} }
if (resizes != 0) { if (resizes != 0) {
layout_fix_panes(w, NULL); layout_fix_panes(w, NULL);
server_redraw_window(w); window_redraw_floating_pane(wp, old_xoff, old_yoff, sx, sy);
server_redraw_window_borders(w); server_redraw_window_borders(w);
} }
} }
+31 -29
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-run-shell.c,v 1.94 2026/08/25 06:04:33 nicm Exp $ */ /* $OpenBSD: cmd-run-shell.c,v 1.95 2026/09/20 07:59:55 nicm Exp $ */
/* /*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org> * Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -57,7 +57,7 @@ const struct cmd_entry cmd_run_shell_entry = {
struct cmd_run_shell_data { struct cmd_run_shell_data {
struct client *client; struct client *client;
char *cmd; char *cmd;
struct cmd_parse_tree *tree; struct args_command_state *state;
char *cwd; char *cwd;
struct cmdq_item *item; struct cmdq_item *item;
struct session *s; struct session *s;
@@ -119,7 +119,7 @@ cmd_run_shell_exec(struct cmd *self, struct cmdq_item *item)
struct format_tree *ft; struct format_tree *ft;
double d; double d;
struct timeval tv; struct timeval tv;
char *end, key[16], *cause = NULL; char *end, key[16];
u_int i; u_int i;
int wait = !args_has(args, 'b'); int wait = !args_has(args, 'b');
@@ -129,10 +129,8 @@ cmd_run_shell_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "invalid delay time: %s", delay); cmdq_error(item, "invalid delay time: %s", delay);
return (CMD_RETURN_ERROR); return (CMD_RETURN_ERROR);
} }
} else { } else if (args_count(args) == 0)
if (args_count(args) == 0)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
}
cdata = xcalloc(1, sizeof *cdata); cdata = xcalloc(1, sizeof *cdata);
if (!args_has(args, 'C')) { if (!args_has(args, 'C')) {
@@ -147,13 +145,8 @@ cmd_run_shell_exec(struct cmd *self, struct cmdq_item *item)
format_free(ft); format_free(ft);
} }
} else { } else {
cdata->tree = args_to_commands(self, item, 0, NULL, 1, &cause); cdata->state = args_make_commands_prepare(self, item, 0, NULL,
if (cdata->tree == NULL) { wait, 1);
cmdq_error(item, "%s", cause);
free(cause);
cmd_run_shell_free(cdata);
return (CMD_RETURN_ERROR);
}
} }
if (args_has(args, 't') && wp != NULL) if (args_has(args, 't') && wp != NULL)
@@ -170,29 +163,26 @@ cmd_run_shell_exec(struct cmd *self, struct cmdq_item *item)
} }
if (cdata->client != NULL) if (cdata->client != NULL)
cdata->client->references++; cdata->client->references++;
if (args_has(args, 'c')) if (args_has(args, 'c'))
cdata->cwd = xstrdup(args_get(args, 'c')); cdata->cwd = format_single_from_target(item, args_get(args, 'c'));
else else
cdata->cwd = xstrdup(server_client_get_cwd(c, s)); cdata->cwd = xstrdup(server_client_get_cwd(c, s));
if (args_has(args, 'E')) if (args_has(args, 'E'))
cdata->flags |= JOB_SHOWSTDERR; cdata->flags |= JOB_SHOWSTDERR;
if (s != NULL) {
cdata->s = s; cdata->s = s;
if (s != NULL)
session_add_ref(s, __func__); session_add_ref(s, __func__);
}
evtimer_set(&cdata->timer, cmd_run_shell_timer, cdata); evtimer_set(&cdata->timer, cmd_run_shell_timer, cdata);
if (delay == NULL) if (delay != NULL) {
event_active(&cdata->timer, EV_TIMEOUT, 1);
else {
timerclear(&tv); timerclear(&tv);
tv.tv_sec = (time_t)d; tv.tv_sec = (time_t)d;
tv.tv_usec = (d - (double)tv.tv_sec) * 1000000U; tv.tv_usec = (d - (double)tv.tv_sec) * 1000000U;
evtimer_add(&cdata->timer, &tv); evtimer_add(&cdata->timer, &tv);
} } else
event_active(&cdata->timer, EV_TIMEOUT, 1);
if (!wait) if (!wait)
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
@@ -206,9 +196,10 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg)
struct client *c = cdata->client; struct client *c = cdata->client;
const char *cmd = cdata->cmd; const char *cmd = cdata->cmd;
struct cmdq_item *item = cdata->item, *new_item; struct cmdq_item *item = cdata->item, *new_item;
struct cmdq_state *cs = NULL; struct cmd_list *cmdlist;
char *error;
if (cdata->tree == NULL) { if (cdata->state == NULL) {
if (cmd == NULL) { if (cmd == NULL) {
if (cdata->item != NULL) if (cdata->item != NULL)
cmdq_continue(cdata->item); cmdq_continue(cdata->item);
@@ -231,13 +222,23 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg)
return; return;
} }
if (item != NULL) cmdlist = args_make_commands(cdata->state, 0, NULL, &error);
cs = cmdq_get_state(item); if (cmdlist == NULL) {
new_item = cmd_invoke_get(cdata->tree, cs, NULL); if (cdata->item == NULL) {
if (item == NULL) *error = toupper((u_char)*error);
status_message_set(c, -1, 1, 0, 0, "%s", error);
} else
cmdq_error(cdata->item, "%s", error);
free(error);
} else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item); cmdq_append(c, new_item);
else cmd_list_free(cmdlist);
} else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item); cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
}
if (cdata->item != NULL) if (cdata->item != NULL)
cmdq_continue(cdata->item); cmdq_continue(cdata->item);
@@ -305,7 +306,8 @@ cmd_run_shell_free(void *data)
session_remove_ref(cdata->s, __func__); session_remove_ref(cdata->s, __func__);
if (cdata->client != NULL) if (cdata->client != NULL)
server_client_unref(cdata->client); server_client_unref(cdata->client);
cmd_parse_free(cdata->tree); if (cdata->state != NULL)
args_make_commands_free(cdata->state);
free(cdata->cwd); free(cdata->cwd);
free(cdata->cmd); free(cdata->cmd);
free(cdata); free(cdata);
+9 -4
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-select-layout.c,v 1.43 2026/07/10 13:38:45 nicm Exp $ */ /* $OpenBSD: cmd-select-layout.c,v 1.45 2026/09/09 07:03:39 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -73,12 +73,13 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item)
{ {
struct args *args = cmd_get_args(self); struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item); struct cmd_find_state *target = cmdq_get_target(item);
struct client *c = cmdq_get_target_client(item);
struct winlink *wl = target->wl; struct winlink *wl = target->wl;
struct window *w = wl->window; struct window *w = wl->window;
struct window_pane *wp = target->wp; struct window_pane *wp = target->wp;
const char *layoutname; const char *layoutname;
char *oldlayout, *cause; char *oldlayout, *cause = NULL;
int next, previous, layout; int next, previous, layout, flags = 0;
server_unzoom_window(w); server_unzoom_window(w);
@@ -89,8 +90,12 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'p')) if (args_has(args, 'p'))
previous = 1; previous = 1;
if (c != NULL &&
(c->flags & CLIENT_CONTROL) &&
(~c->flags & CLIENT_CONTROL_NEWLAYOUTS))
flags |= LAYOUT_CUSTOM_OLD_FORMAT;
oldlayout = w->old_layout; oldlayout = w->old_layout;
w->old_layout = layout_dump(w, w->layout_root); w->old_layout = layout_dump(w, w->layout_root, flags);
if (next || previous) { if (next || previous) {
if (next) if (next)
+1 -1
View File
@@ -131,7 +131,7 @@ cmd_source_file_done(__unused struct client *oc, const char *path,
if (error != 0) if (error != 0)
cmdq_error(item, "%s: %s", strerror(error), path); cmdq_error(item, "%s: %s", strerror(error), path);
else if (bsize != 0) { else if (bsize != 0) {
if (load_cfg_from_buffer(bdata, bsize, path, cdata->after, if (load_cfg_from_buffer(bdata, bsize, path, c, cdata->after,
target, cdata->flags, &new_item) < 0) target, cdata->flags, &new_item) < 0)
cdata->retval = CMD_RETURN_ERROR; cdata->retval = CMD_RETURN_ERROR;
else if (new_item != NULL) else if (new_item != NULL)
+13 -4
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-split-window.c,v 1.150 2026/08/20 09:19:24 nicm Exp $ */ /* $OpenBSD: cmd-split-window.c,v 1.151 2026/09/10 11:02:18 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -40,8 +40,8 @@ const struct cmd_entry cmd_new_pane_entry = {
.name = "new-pane", .name = "new-pane",
.alias = "newp", .alias = "newp",
.args = { "AbB:Cc:de:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL }, .args = { "AbB:Cc:Dde:EfF:hIkl:KLMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-AbCdefhIkKLMOPvWZ] [-B border-lines] " .usage = "[-AbCDefhIkKLMOPvWZ] [-B border-lines] "
"[-c start-directory] [-e environment] " "[-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] " "[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] " "[-s style] [-S active-border-style] "
@@ -219,6 +219,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
new_wp->flags |= PANE_CAPTUREALLKEYS; new_wp->flags |= PANE_CAPTUREALLKEYS;
if (args_has(args, 'C') && args_has(args, 'O')) if (args_has(args, 'C') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCLICK; new_wp->flags |= PANE_CLOSEONCLICK;
if (args_has(args, 'D') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCANCEL;
style = args_get(args, 's'); style = args_get(args, 's');
if (style != NULL) { if (style != NULL) {
@@ -360,6 +362,7 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
enum pane_lines lines; enum pane_lines lines;
u_int sx, sy; u_int sx, sy;
int x, y, xoff, yoff, border; int x, y, xoff, yoff, border;
int oxoff, oyoff, osx, osy;
if (c->tty.mouse_last_pane == -1) if (c->tty.mouse_last_pane == -1)
return; return;
@@ -415,8 +418,14 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
if (sy < PANE_MINIMUM) if (sy < PANE_MINIMUM)
sy = PANE_MINIMUM; sy = PANE_MINIMUM;
oxoff = wp->xoff;
oyoff = wp->yoff;
osx = wp->sx;
osy = wp->sy;
layout_set_size(lc, sx, sy, xoff, yoff); layout_set_size(lc, sx, sy, xoff, yoff);
layout_fix_panes(w, NULL); layout_fix_panes(w, NULL);
server_redraw_window(w);
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
server_redraw_window_borders(w); server_redraw_window_borders(w);
} }
+35 -1
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-wait-for.c,v 1.23 2026/07/10 13:38:45 nicm Exp $ */ /* $OpenBSD: cmd-wait-for.c,v 1.24 2026/09/22 06:46:50 nicm Exp $ */
/* /*
* Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -480,6 +480,40 @@ cmd_wait_for_unlock(struct cmdq_item *item, const char *name,
return (CMD_RETURN_NORMAL); return (CMD_RETURN_NORMAL);
} }
void
cmd_wait_for_client_lost(struct client *c)
{
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) {
if (cmdq_get_client(wei->item) == c) {
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) {
if (cmdq_get_client(wi->item) == c) {
cmdq_continue(wi->item);
TAILQ_REMOVE(&wc->waiters, wi, entry);
free(wi);
}
}
TAILQ_FOREACH_SAFE(wi, &wc->lockers, entry, wi1) {
if (cmdq_get_client(wi->item) == c) {
cmdq_continue(wi->item);
TAILQ_REMOVE(&wc->lockers, wi, entry);
free(wi);
}
}
cmd_wait_for_remove_empty(wc);
}
}
void void
cmd_wait_for_flush(void) cmd_wait_for_flush(void)
{ {
+199
View File
@@ -220,11 +220,18 @@ const struct cmd_entry *cmd_table[] = {
struct cmd { struct cmd {
const struct cmd_entry *entry; const struct cmd_entry *entry;
struct args *args; struct args *args;
u_int group;
char *file; char *file;
u_int line; u_int line;
int parse_flags; int parse_flags;
TAILQ_ENTRY(cmd) qentry;
}; };
TAILQ_HEAD(cmds, cmd);
/* Next group number for new command list. */
static u_int cmd_list_next_group = 1;
/* Log an argument vector. */ /* Log an argument vector. */
void printflike(3, 4) void printflike(3, 4)
@@ -395,6 +402,13 @@ cmd_get_args(struct cmd *cmd)
return (cmd->args); return (cmd->args);
} }
/* Get group for command. */
u_int
cmd_get_group(struct cmd *cmd)
{
return (cmd->group);
}
/* Get file and line for command. */ /* Get file and line for command. */
void void
cmd_get_source(struct cmd *cmd, const char **file, u_int *line) cmd_get_source(struct cmd *cmd, const char **file, u_int *line)
@@ -544,6 +558,23 @@ cmd_free(struct cmd *cmd)
free(cmd); free(cmd);
} }
/* Copy a command. */
struct cmd *
cmd_copy(struct cmd *cmd, int argc, char **argv)
{
struct cmd *new_cmd;
new_cmd = xcalloc(1, sizeof *new_cmd);
new_cmd->entry = cmd->entry;
new_cmd->args = args_copy(cmd->args, argc, argv);
if (cmd->file != NULL)
new_cmd->file = xstrdup(cmd->file);
new_cmd->line = cmd->line;
return (new_cmd);
}
/* Get a command as a string. */ /* Get a command as a string. */
char * char *
cmd_print(struct cmd *cmd) cmd_print(struct cmd *cmd)
@@ -560,6 +591,174 @@ cmd_print(struct cmd *cmd)
return (out); return (out);
} }
/* Create a new command list. */
struct cmd_list *
cmd_list_new(void)
{
struct cmd_list *cmdlist;
cmdlist = xcalloc(1, sizeof *cmdlist);
cmdlist->references = 1;
cmdlist->group = cmd_list_next_group++;
cmdlist->list = xcalloc(1, sizeof *cmdlist->list);
TAILQ_INIT(cmdlist->list);
return (cmdlist);
}
/* Append a command to a command list. */
void
cmd_list_append(struct cmd_list *cmdlist, struct cmd *cmd)
{
cmd->group = cmdlist->group;
TAILQ_INSERT_TAIL(cmdlist->list, cmd, qentry);
}
/* Append all commands from one list to another. */
void
cmd_list_append_all(struct cmd_list *cmdlist, struct cmd_list *from)
{
struct cmd *cmd;
TAILQ_FOREACH(cmd, from->list, qentry)
cmd->group = cmdlist->group;
TAILQ_CONCAT(cmdlist->list, from->list, qentry);
}
/* Move all commands from one command list to another. */
void
cmd_list_move(struct cmd_list *cmdlist, struct cmd_list *from)
{
TAILQ_CONCAT(cmdlist->list, from->list, qentry);
cmdlist->group = cmd_list_next_group++;
}
/* Free a command list. */
void
cmd_list_free(struct cmd_list *cmdlist)
{
struct cmd *cmd, *cmd1;
if (--cmdlist->references != 0)
return;
TAILQ_FOREACH_SAFE(cmd, cmdlist->list, qentry, cmd1) {
TAILQ_REMOVE(cmdlist->list, cmd, qentry);
cmd_free(cmd);
}
free(cmdlist->list);
free(cmdlist);
}
/* Copy a command list, expanding %s in arguments. */
struct cmd_list *
cmd_list_copy(const struct cmd_list *cmdlist, int argc, char **argv)
{
struct cmd *cmd;
struct cmd_list *new_cmdlist;
struct cmd *new_cmd;
u_int group = cmdlist->group;
char *s;
s = cmd_list_print(cmdlist, 0);
log_debug("%s: %s", __func__, s);
free(s);
new_cmdlist = cmd_list_new();
TAILQ_FOREACH(cmd, cmdlist->list, qentry) {
if (cmd->group != group) {
new_cmdlist->group = cmd_list_next_group++;
group = cmd->group;
}
new_cmd = cmd_copy(cmd, argc, argv);
cmd_list_append(new_cmdlist, new_cmd);
}
s = cmd_list_print(new_cmdlist, 0);
log_debug("%s: %s", __func__, s);
free(s);
return (new_cmdlist);
}
/* Get a command list as a string. */
char *
cmd_list_print(const struct cmd_list *cmdlist, int flags)
{
struct cmd *cmd, *next;
char *buf, *this;
size_t len;
const char *separator;
int escaped = flags & CMD_LIST_PRINT_ESCAPED;
int no_groups = flags & CMD_LIST_PRINT_NO_GROUPS;
const char *single_separator = escaped ? " \\; " : " ; ";
const char *double_separator = escaped ? " \\;\\; " : " ;; ";
len = 1;
buf = xcalloc(1, len);
TAILQ_FOREACH(cmd, cmdlist->list, qentry) {
this = cmd_print(cmd);
len += strlen(this) + 6;
buf = xrealloc(buf, len);
strlcat(buf, this, len);
next = TAILQ_NEXT(cmd, qentry);
if (next != NULL) {
if (!no_groups && cmd->group != next->group)
separator = double_separator;
else
separator = single_separator;
strlcat(buf, separator, len);
}
free(this);
}
return (buf);
}
/* Get first command in list. */
struct cmd *
cmd_list_first(struct cmd_list *cmdlist)
{
return (TAILQ_FIRST(cmdlist->list));
}
/* Get next command in list. */
struct cmd *
cmd_list_next(struct cmd *cmd)
{
return (TAILQ_NEXT(cmd, qentry));
}
/* Do all of the commands in this command list have this flag? */
int
cmd_list_all_have(struct cmd_list *cmdlist, int flag)
{
struct cmd *cmd;
TAILQ_FOREACH(cmd, cmdlist->list, qentry) {
if (~cmd->entry->flags & flag)
return (0);
}
return (1);
}
/* Do any of the commands in this command list have this flag? */
int
cmd_list_any_have(struct cmd_list *cmdlist, int flag)
{
struct cmd *cmd;
TAILQ_FOREACH(cmd, cmdlist->list, qentry) {
if (cmd->entry->flags & flag)
return (1);
}
return (0);
}
/* Adjust current mouse position for a pane. */ /* Adjust current mouse position for a pane. */
int int
cmd_mouse_at(struct window_pane *wp, struct mouse_event *m, u_int *xp, cmd_mouse_at(struct window_pane *wp, struct mouse_event *m, u_int *xp,
+12 -1
View File
@@ -1,6 +1,6 @@
# configure.ac # configure.ac
AC_INIT([tmux], next-3.8) AC_INIT([tmux], next-3.9)
AC_PREREQ([2.60]) AC_PREREQ([2.60])
AC_CONFIG_AUX_DIR(etc) AC_CONFIG_AUX_DIR(etc)
@@ -111,6 +111,17 @@ case "x$DEFAULT_TERM" in
;; ;;
esac esac
# Allow the socket directory list to be set. It is a colon-separated list of
# directories, tried in order; a leading $ is an environment variable.
AC_ARG_WITH(
socket-path,
AS_HELP_STRING(--with-socket-path=PATHS, set socket directory list),
[TMUX_SOCK=`echo "$withval" | sed 's/[[$]]/$$/g'`],
[TMUX_SOCK=]
)
AM_CONDITIONAL(HAVE_TMUX_SOCK, [test "x$TMUX_SOCK" != x])
AC_SUBST(TMUX_SOCK)
# Do we need fuzzers? # Do we need fuzzers?
AM_CONDITIONAL(NEED_FUZZING, test "x$enable_fuzzing" = xyes) AM_CONDITIONAL(NEED_FUZZING, test "x$enable_fuzzing" = xyes)
+13 -6
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: control-notify.c,v 1.38 2026/08/03 13:38:42 nicm Exp $ */ /* $OpenBSD: control-notify.c,v 1.39 2026/09/08 10:20:08 nicm Exp $ */
/* /*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -70,6 +70,7 @@ control_window_layout_changed_cb(__unused const char *name,
struct session *s; struct session *s;
struct winlink *wl; struct winlink *wl;
struct window *w = event_payload_get_window(ep, "window"); struct window *w = event_payload_get_window(ep, "window");
struct format_tree *ft;
const char *template; const char *template;
char *cp; char *cp;
@@ -84,19 +85,25 @@ control_window_layout_changed_cb(__unused const char *name,
* and we don't need to inform the client about the layout change * and we don't need to inform the client about the layout change
* because the whole window will go away soon. * because the whole window will go away soon.
*/ */
wl = TAILQ_FIRST(&w->winlinks); if (TAILQ_FIRST(&w->winlinks) == NULL || w->layout_root == NULL)
if (wl == NULL || w->layout_root == NULL)
return; return;
cp = format_single(NULL, template, NULL, NULL, wl, NULL);
TAILQ_FOREACH(c, &clients, entry) { TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL) if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL)
continue; continue;
s = c->session; s = c->session;
if (winlink_find_by_window_id(&s->windows, w->id) != NULL) wl = winlink_find_by_window_id(&s->windows, w->id);
if (wl == NULL)
continue;
ft = format_create(c, NULL, FORMAT_NONE, 0);
format_defaults(ft, c, s, wl, NULL);
cp = format_expand(ft, template);
format_free(ft);
control_notify_write(c, "%s", cp); control_notify_write(c, "%s", cp);
}
free(cp); free(cp);
}
} }
/* Notify control clients that window pane changed. */ /* Notify control clients that window pane changed. */
+67 -12
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.66 2026/08/18 07:43:44 nicm Exp $ */ /* $OpenBSD: control.c,v 1.68 2026/09/09 08:30:05 nicm Exp $ */
/* /*
* Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2012 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -108,6 +108,7 @@ struct control_state {
u_int pending_count; u_int pending_count;
TAILQ_HEAD(, control_block) all_blocks; TAILQ_HEAD(, control_block) all_blocks;
size_t queued_reply_bytes;
struct bufferevent *read_event; struct bufferevent *read_event;
struct bufferevent *write_event; struct bufferevent *write_event;
@@ -132,6 +133,9 @@ struct control_state {
/* Maximum age for clients that are not using pause mode. */ /* Maximum age for clients that are not using pause mode. */
#define CONTROL_MAXIMUM_AGE 300000 #define CONTROL_MAXIMUM_AGE 300000
/* Maximum buffered command replies for a client that is not reading. */
#define CONTROL_MAXIMUM_REPLY_BUFFER (64 * 1024 * 1024)
/* Flags to ignore client. */ /* Flags to ignore client. */
#define CONTROL_IGNORE_FLAGS \ #define CONTROL_IGNORE_FLAGS \
(CLIENT_CONTROL_NOOUTPUT| \ (CLIENT_CONTROL_NOOUTPUT| \
@@ -165,6 +169,15 @@ RB_GENERATE_STATIC(control_windows, control_window, entry, control_window_cmp);
static void static void
control_free_block(struct control_state *cs, struct control_block *cb) control_free_block(struct control_state *cs, struct control_block *cb)
{ {
size_t size;
if (cb->size == 0 && cb->line != NULL) {
size = strlen(cb->line) + 1;
if (cs->queued_reply_bytes > size)
cs->queued_reply_bytes -= size;
else
cs->queued_reply_bytes = 0;
}
free(cb->line); free(cb->line);
TAILQ_REMOVE(&cs->all_blocks, cb, all_entry); TAILQ_REMOVE(&cs->all_blocks, cb, all_entry);
free(cb); free(cb);
@@ -407,16 +420,52 @@ control_reset_pane(struct client *c, struct window_pane *wp)
memcpy(&cp->queued, &wp->offset, sizeof cp->queued); memcpy(&cp->queued, &wp->offset, sizeof cp->queued);
} }
/*
* Check if the replies buffered for a client, including one about to be
* added, have grown too large and kill it if so. Returns 1 if further output
* for the client should be dropped.
*/
static int
control_check_reply_buffer(struct client *c, size_t added)
{
struct control_state *cs = c->control_state;
size_t size;
if (c->flags & CLIENT_CONTROL_DISCARD)
return (1);
size = EVBUFFER_LENGTH(cs->write_event->output);
size += cs->queued_reply_bytes;
size += added;
if (size < CONTROL_MAXIMUM_REPLY_BUFFER)
return (0);
log_debug("%s: %s: %zu bytes of replies buffered", __func__, c->name,
size);
if (~c->flags & CLIENT_EXIT) {
c->exit_message = xstrdup("too far behind");
c->flags |= CLIENT_EXIT;
control_discard(c);
}
c->flags |= CLIENT_CONTROL_DISCARD;
return (1);
}
/* Write an already-formatted line, queueing it behind %output if needed. */ /* Write an already-formatted line, queueing it behind %output if needed. */
static void static void
control_write_line(struct client *c, char *line) control_write_line(struct client *c, char *line)
{ {
struct control_state *cs = c->control_state; struct control_state *cs = c->control_state;
struct control_block *cb; struct control_block *cb;
size_t size = strlen(line) + 1;
if (control_check_reply_buffer(c, size)) {
free(line);
return;
}
if (TAILQ_EMPTY(&cs->all_blocks)) { if (TAILQ_EMPTY(&cs->all_blocks)) {
log_debug("%s: %s: writing line: %s", __func__, c->name, line); log_debug("%s: %s: writing line: %s", __func__, c->name, line);
bufferevent_write(cs->write_event, line, strlen(line)); bufferevent_write(cs->write_event, line, size - 1);
bufferevent_write(cs->write_event, "\n", 1); bufferevent_write(cs->write_event, "\n", 1);
bufferevent_enable(cs->write_event, EV_WRITE); bufferevent_enable(cs->write_event, EV_WRITE);
free(line); free(line);
@@ -426,6 +475,7 @@ control_write_line(struct client *c, char *line)
cb = xcalloc(1, sizeof *cb); cb = xcalloc(1, sizeof *cb);
cb->line = line; cb->line = line;
TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry); TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
cs->queued_reply_bytes += size;
cb->t = get_timer(); cb->t = get_timer();
log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line); log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line);
@@ -455,8 +505,12 @@ control_flush_deferred(struct client *c)
void void
control_write(struct client *c, const char *fmt, ...) control_write(struct client *c, const char *fmt, ...)
{ {
va_list ap; struct control_state *cs = c->control_state;
char *line; char *line;
va_list ap;
if (cs == NULL)
return;
va_start(ap, fmt); va_start(ap, fmt);
xvasprintf(&line, fmt, ap); xvasprintf(&line, fmt, ap);
@@ -479,6 +533,9 @@ control_write_guard(struct client *c, const char *guard, long t, u_int number,
struct control_state *cs = c->control_state; struct control_state *cs = c->control_state;
char *line; char *line;
if (cs == NULL)
return;
if (strcmp(guard, "begin") == 0) if (strcmp(guard, "begin") == 0)
cs->guard_depth++; cs->guard_depth++;
@@ -503,6 +560,9 @@ control_notify_write(struct client *c, const char *fmt, ...)
va_list ap; va_list ap;
char *line; char *line;
if (cs == NULL)
return;
va_start(ap, fmt); va_start(ap, fmt);
xvasprintf(&line, fmt, ap); xvasprintf(&line, fmt, ap);
va_end(ap); va_end(ap);
@@ -641,9 +701,8 @@ control_read_callback(__unused struct bufferevent *bufev, void *data)
struct control_state *cs = c->control_state; struct control_state *cs = c->control_state;
struct evbuffer *buffer = cs->read_event->input; struct evbuffer *buffer = cs->read_event->input;
char *line, *error; char *line, *error;
struct cmd_parse_tree *tree;
struct cmdq_item *new_item;
struct cmdq_state *state; struct cmdq_state *state;
enum cmd_parse_status status;
for (;;) { for (;;) {
line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_LF); line = evbuffer_readln(buffer, NULL, EVBUFFER_EOL_LF);
@@ -657,14 +716,9 @@ control_read_callback(__unused struct bufferevent *bufev, void *data)
} }
state = cmdq_new_state(NULL, NULL, CMDQ_STATE_CONTROL); state = cmdq_new_state(NULL, NULL, CMDQ_STATE_CONTROL);
tree = cmd_parse_from_string(line, NULL, &error); status = cmd_parse_and_append(line, NULL, c, state, &error);
if (tree == NULL) if (status == CMD_PARSE_ERROR)
cmdq_append(c, cmdq_get_callback(control_error, error)); cmdq_append(c, cmdq_get_callback(control_error, error));
else {
new_item = cmd_invoke_get(tree, state, NULL);
cmdq_append(c, new_item);
cmd_parse_free(tree);
}
cmdq_free_state(state); cmdq_free_state(state);
free(line); free(line);
@@ -1003,6 +1057,7 @@ control_discard_all(struct client *c)
control_discard(c); control_discard(c);
TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1) TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1)
control_free_block(cs, cb); control_free_block(cs, cb);
cs->queued_reply_bytes = 0;
bufferevent_disable(cs->write_event, EV_WRITE); bufferevent_disable(cs->write_event, EV_WRITE);
} }
+26 -9
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.415 2026/08/31 19:34:09 nicm Exp $ */ /* $OpenBSD: format.c,v 1.418 2026/09/20 08:19:31 nicm Exp $ */
/* /*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -855,26 +855,42 @@ format_cb_window_active_clients_list(struct format_tree *ft)
static void * static void *
format_cb_window_layout(struct format_tree *ft) format_cb_window_layout(struct format_tree *ft)
{ {
struct client *c = ft->client;
struct window *w = ft->w; struct window *w = ft->w;
struct layout_cell *lcroot;
int flags = 0;
if (w == NULL) if (w == NULL)
return (NULL); return (NULL);
if (w->saved_layout_root != NULL) if (w->saved_layout_root != NULL)
return (layout_dump(w, w->saved_layout_root)); lcroot = w->saved_layout_root;
return (layout_dump(w, w->layout_root)); else
lcroot = w->layout_root;
if (c != NULL &&
(c->flags & CLIENT_CONTROL) &&
(~c->flags & CLIENT_CONTROL_NEWLAYOUTS))
flags |= LAYOUT_CUSTOM_OLD_FORMAT;
return (layout_dump(w, lcroot, flags));
} }
/* Callback for window_visible_layout. */ /* Callback for window_visible_layout. */
static void * static void *
format_cb_window_visible_layout(struct format_tree *ft) format_cb_window_visible_layout(struct format_tree *ft)
{ {
struct client *c = ft->client;
struct window *w = ft->w; struct window *w = ft->w;
int flags = 0;
if (w == NULL) if (w == NULL)
return (NULL); return (NULL);
return (layout_dump(w, w->layout_root)); if (c != NULL &&
(c->flags & CLIENT_CONTROL) &&
(~c->flags & CLIENT_CONTROL_NEWLAYOUTS))
flags |= LAYOUT_CUSTOM_OLD_FORMAT;
return (layout_dump(w, w->layout_root, flags));
} }
/* Callback for pane_start_command. */ /* Callback for pane_start_command. */
@@ -2860,10 +2876,10 @@ format_cb_session_activity_flag(struct format_tree *ft)
if (ft->s != NULL) { if (ft->s != NULL) {
RB_FOREACH(wl, winlinks, &ft->s->windows) { RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (ft->wl->flags & WINLINK_ACTIVITY) if (wl->flags & WINLINK_ACTIVITY)
return (xstrdup("1")); return (xstrdup("1"));
return (xstrdup("0"));
} }
return (xstrdup("0"));
} }
return (NULL); return (NULL);
} }
@@ -2878,8 +2894,8 @@ format_cb_session_bell_flag(struct format_tree *ft)
RB_FOREACH(wl, winlinks, &ft->s->windows) { RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (wl->flags & WINLINK_BELL) if (wl->flags & WINLINK_BELL)
return (xstrdup("1")); return (xstrdup("1"));
return (xstrdup("0"));
} }
return (xstrdup("0"));
} }
return (NULL); return (NULL);
} }
@@ -2892,10 +2908,10 @@ format_cb_session_silence_flag(struct format_tree *ft)
if (ft->s != NULL) { if (ft->s != NULL) {
RB_FOREACH(wl, winlinks, &ft->s->windows) { RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (ft->wl->flags & WINLINK_SILENCE) if (wl->flags & WINLINK_SILENCE)
return (xstrdup("1")); return (xstrdup("1"));
return (xstrdup("0"));
} }
return (xstrdup("0"));
} }
return (NULL); return (NULL);
} }
@@ -5234,6 +5250,7 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt)
format_log(es, "session loop: $%u", s->id); format_log(es, "session loop: $%u", s->id);
if (active != NULL && if (active != NULL &&
ft->c != NULL && ft->c != NULL &&
ft->c->session != NULL &&
s->id == ft->c->session->id) s->id == ft->c->session->id)
use = active; use = active;
else else
+12 -6
View File
@@ -36,8 +36,7 @@ int
LLVMFuzzerTestOneInput(const u_char *data, size_t size) LLVMFuzzerTestOneInput(const u_char *data, size_t size)
{ {
struct cmd_parse_input pi; struct cmd_parse_input pi;
struct cmd_parse_tree *tree; struct cmd_parse_result *pr;
char *cause = NULL;
if (size > 2048 || size == 0) if (size > 2048 || size == 0)
return 0; return 0;
@@ -45,10 +44,17 @@ LLVMFuzzerTestOneInput(const u_char *data, size_t size)
memset(&pi, 0, sizeof pi); memset(&pi, 0, sizeof pi);
pi.flags = CMD_PARSE_QUIET; pi.flags = CMD_PARSE_QUIET;
tree = cmd_parse_from_buffer(data, size, &pi, &cause); pr = cmd_parse_from_buffer(data, size, &pi);
if (tree != NULL) switch (pr->status) {
cmd_parse_free(tree); case CMD_PARSE_SUCCESS:
free(cause); cmd_list_free(pr->cmdlist);
break;
case CMD_PARSE_ERROR:
free(pr->error);
break;
default:
break;
}
return 0; return 0;
} }
+1 -1
View File
@@ -44,7 +44,7 @@ LLVMFuzzerTestOneInput(const u_char *data, size_t size)
w = window_create(PANE_WIDTH, PANE_HEIGHT, 0, 0); w = window_create(PANE_WIDTH, PANE_HEIGHT, 0, 0);
wp = window_add_pane(w, NULL, 0, 0); wp = window_add_pane(w, NULL, 0, 0);
bufferevent_pair_new(libevent, BEV_OPT_CLOSE_ON_FREE, vpty); bufferevent_pair_new(libevent, BEV_OPT_CLOSE_ON_FREE, vpty);
wp->ictx = input_init(wp, vpty[0], NULL, NULL); wp->ictx = input_init(wp, vpty[0], NULL);
window_add_ref(w, __func__); window_add_ref(w, __func__);
wp->fd = open("/dev/null", O_WRONLY); wp->fd = open("/dev/null", O_WRONLY);
+88
View File
@@ -0,0 +1,88 @@
/*
* Fuzz the tmux JSON parser and serializer.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
struct event_base *libevent;
static void
walk_json(struct json_node *node)
{
struct json_node *child, *object;
const char *string;
int64_t number;
int boolean;
if (node == NULL)
return;
(void)json_get_string(node, &string);
(void)json_get_number(node, &number);
(void)json_get_boolean(node, &boolean);
if (json_get_object(node, &object) == 0) {
(void)json_find(object, "V");
(void)json_find(object, "L");
}
for (child = json_array_first(node); child != NULL;
child = json_array_next(child))
walk_json(child);
}
int
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
struct json_node *node;
char *input, *serialized, *cause = NULL;
if (size == 0 || size > 1U << 20)
return 0;
input = malloc(size + 1);
if (input == NULL)
return 0;
memcpy(input, data, size);
input[size] = '\0';
node = json_parse(input, &cause);
free(cause);
if (node != NULL) {
walk_json(node);
serialized = json_to_string(node);
free(serialized);
json_destroy_node(node);
}
free(input);
return 0;
}
int
LLVMFuzzerInitialize(__unused int *argc, __unused char ***argv)
{
const struct options_table_entry *oe;
global_environ = environ_create();
global_options = options_create(NULL);
global_s_options = options_create(NULL);
global_w_options = options_create(NULL);
for (oe = options_table; oe->name != NULL; oe++) {
if (oe->scope & OPTIONS_TABLE_SERVER)
options_default(global_options, oe);
if (oe->scope & OPTIONS_TABLE_SESSION)
options_default(global_s_options, oe);
if (oe->scope & OPTIONS_TABLE_WINDOW)
options_default(global_w_options, oe);
}
libevent = osdep_event_init();
socket_path = xstrdup("dummy");
return 0;
}
+37
View File
@@ -0,0 +1,37 @@
# JSON structural tokens
"{"
"}"
"["
"]"
":"
","
"\""
# JSON values
"true"
"false"
"null"
"0"
"-1"
"123456789"
"\\\""
"\\\\"
"\\n"
"\\u0000"
# tmux layout/control-mode keys
"V"
"L"
"W"
"H"
"X"
"Y"
"T"
"S"
"id"
"result"
"error"
"routes"
"uri"
"name"
"value"
+2
View File
@@ -0,0 +1,2 @@
[libfuzzer]
max_len = 4096
+111
View File
@@ -0,0 +1,111 @@
/*
* Copyright (c) 2026 Arthur Chan <arthur.chan@adalogics.com>
*
* 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.
*/
/*
* Fuzz the custom layout parser.
*
* A layout string such as "bb62,80x24,0,0{40x24,0,0,1,39x24,41,0,2}" is
* accepted by select-layout and is what tmux stores and restores for a
* window, so it is parsed from configuration and from commands. It drives
* layout-custom.c (the string parser and the checksum), then layout.c, which
* resizes and assigns the cells.
*
* layout_parse() refuses a window with no panes, and refuses a layout whose
* cell count is smaller than the pane count, so the window is given a fixed
* number of panes. The count is fixed rather than derived from the input, so
* a mutation always means a different layout string.
*/
#include <sys/types.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
#define FUZZER_MAXLEN 1024
#define FUZZER_PANES 4
struct event_base *libevent;
int
LLVMFuzzerTestOneInput(const u_char *data, size_t size)
{
struct window *w;
struct window_pane *wp;
char *buf, *cause = NULL, *dump;
u_int i;
if (size == 0 || size > FUZZER_MAXLEN)
return 0;
/* layout_parse() takes a C string. */
buf = malloc(size + 1);
if (buf == NULL)
return 0;
memcpy(buf, data, size);
buf[size] = '\0';
w = window_create(80, 24, 0, 0);
if (w == NULL) {
free(buf);
return 0;
}
window_add_ref(w, __func__);
for (i = 0; i < FUZZER_PANES; i++) {
wp = window_add_pane(w, NULL, 0, 0);
if (w->active == NULL)
w->active = wp;
}
if (layout_parse(w, buf, &cause) == 0) {
dump = layout_dump(w, w->layout_root, 1);
free(dump);
}
free(cause);
window_remove_ref(w, __func__);
free(buf);
return 0;
}
int
LLVMFuzzerInitialize(__unused int *argc, __unused char ***argv)
{
const struct options_table_entry *oe;
global_environ = environ_create();
global_options = options_create(NULL);
global_s_options = options_create(NULL);
global_w_options = options_create(NULL);
for (oe = options_table; oe->name != NULL; oe++) {
if (oe->scope & OPTIONS_TABLE_SERVER)
options_default(global_options, oe);
if (oe->scope & OPTIONS_TABLE_SESSION)
options_default(global_s_options, oe);
if (oe->scope & OPTIONS_TABLE_WINDOW)
options_default(global_w_options, oe);
}
libevent = osdep_event_init();
socket_path = xstrdup("dummy");
return 0;
}
+17
View File
@@ -0,0 +1,17 @@
# tmux custom layout strings: "<checksum>,<sx>x<sy>,<x>,<y>{...}" / "[...]"
","
"x"
"{"
"}"
"["
"]"
"0"
"1"
"80x24,0,0"
"40x24,0,0"
"bb62,"
"cafe,"
",0,0,0"
",0,0{"
",0,0["
"@"
+2
View File
@@ -0,0 +1,2 @@
[libfuzzer]
max_len = 1024
+35 -32
View File
@@ -59,35 +59,35 @@ static struct hooks_events hooks_events = TAILQ_HEAD_INITIALIZER(hooks_events);
/* Insert one hook command list. */ /* Insert one hook command list. */
static struct cmdq_item * static struct cmdq_item *
hooks_insert_one(struct cmdq_item *item, struct hooks_data *hd, hooks_insert_one(struct cmdq_item *item, struct hooks_data *hd,
struct cmd_parse_tree *tree, struct cmdq_state *state) struct cmd_list *cmdlist, struct cmdq_state *state)
{ {
struct cmdq_item *new_item; struct cmdq_item *new_item;
char *s; char *s;
if (tree == NULL) if (cmdlist == NULL)
return (item); return (item);
if (log_get_level() != 0) { if (log_get_level() != 0) {
s = cmd_parse_print(tree, 0); s = cmd_list_print(cmdlist, 0);
log_debug("%s: hook %s is: %s", __func__, hd->name, s); log_debug("%s: hook %s is: %s", __func__, hd->name, s);
free(s); free(s);
} }
new_item = cmd_invoke_get(tree, state, NULL); new_item = cmdq_get_command(cmdlist, state);
if (item != NULL) if (item != NULL)
return (cmdq_insert_after(item, new_item)); return (cmdq_insert_after(item, new_item));
return (cmdq_append(NULL, new_item)); return (cmdq_append(NULL, new_item));
} }
/* Parse a hook command. */ /* Parse a hook command. */
static struct cmd_parse_tree * static struct cmd_parse_result *
hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs, hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs,
const char *value, char **cause) const char *value)
{ {
struct cmd_parse_tree *tree; struct cmd_parse_result *pr;
struct format_tree *ft; struct format_tree *ft;
char *expanded; char *expanded;
if (!hd->expand) if (!hd->expand)
return (cmd_parse_from_string(value, NULL, cause)); return (cmd_parse_from_string(value, NULL));
ft = format_create_defaults(NULL, hd->client, fs->s, fs->wl, fs->wp); ft = format_create_defaults(NULL, hd->client, fs->s, fs->wl, fs->wp);
if (hd->formats != NULL) if (hd->formats != NULL)
@@ -95,9 +95,9 @@ hooks_parse(struct hooks_data *hd, struct cmd_find_state *fs,
expanded = format_expand(ft, value); expanded = format_expand(ft, value);
format_free(ft); format_free(ft);
tree = cmd_parse_from_string(expanded, NULL, cause); pr = cmd_parse_from_string(expanded, NULL);
free(expanded); free(expanded);
return (tree); return (pr);
} }
/* Insert commands for a hook. */ /* Insert commands for a hook. */
@@ -109,9 +109,9 @@ hooks_insert(struct cmdq_item *item, struct hooks_data *hd)
struct cmdq_state *state; struct cmdq_state *state;
struct options_entry *o; struct options_entry *o;
struct options_array_item *a; struct options_array_item *a;
struct cmd_parse_tree *tree; struct cmd_list *cmdlist;
const char *value; const char *value;
char *cause = NULL; struct cmd_parse_result *pr;
log_debug("%s: inserting hook %s", __func__, hd->name); log_debug("%s: inserting hook %s", __func__, hd->name);
@@ -155,35 +155,38 @@ hooks_insert(struct cmdq_item *item, struct hooks_data *hd)
if (*hd->name == '@') { if (*hd->name == '@') {
value = options_get_string(oo, hd->name); value = options_get_string(oo, hd->name);
cause = NULL; pr = hooks_parse(hd, &fs, value);
tree = hooks_parse(hd, &fs, value, &cause); switch (pr->status) {
if (tree == NULL) { case CMD_PARSE_ERROR:
log_debug("%s: can't parse hook %s: %s", __func__, log_debug("%s: can't parse hook %s: %s", __func__,
hd->name, cause); hd->name, pr->error);
free(cause); free(pr->error);
} else { break;
hooks_insert_one(item, hd, tree, state); case CMD_PARSE_SUCCESS:
cmd_parse_free(tree); hooks_insert_one(item, hd, pr->cmdlist, state);
break;
} }
} else { } else {
a = options_array_first(o); a = options_array_first(o);
while (a != NULL) { while (a != NULL) {
if (hd->expand) { if (hd->expand) {
value = options_array_item_value(a)->string; value = options_array_item_value(a)->string;
cause = NULL; pr = hooks_parse(hd, &fs, value);
tree = hooks_parse(hd, &fs, value, &cause); switch (pr->status) {
if (tree == NULL) { case CMD_PARSE_ERROR:
if (cause != NULL) if (pr->error != NULL) {
cmdq_error(item, "%s", cause); cmdq_error(item, "%s",
free(cause); pr->error);
} else { }
item = hooks_insert_one(item, hd, tree, break;
state); case CMD_PARSE_SUCCESS:
cmd_parse_free(tree); item = hooks_insert_one(item, hd,
pr->cmdlist, state);
break;
} }
} else { } else {
tree = options_array_item_value(a)->cmd; cmdlist = options_array_item_value(a)->cmdlist;
item = hooks_insert_one(item, hd, tree, item = hooks_insert_one(item, hd, cmdlist,
state); state);
} }
a = options_array_next(a); a = options_array_next(a);
+2 -10
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.271 2026/08/31 19:34:09 nicm Exp $ */ /* $OpenBSD: input.c,v 1.272 2026/09/21 10:22:31 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -101,7 +101,6 @@ struct input_ctx {
struct bufferevent *event; struct bufferevent *event;
struct screen_write_ctx ctx; struct screen_write_ctx ctx;
struct colour_palette *palette; struct colour_palette *palette;
struct client *c;
struct input_cell cell; struct input_cell cell;
struct input_cell old_cell; struct input_cell old_cell;
@@ -874,7 +873,7 @@ input_restore_state(struct input_ctx *ictx)
/* Initialise input parser. */ /* Initialise input parser. */
struct input_ctx * struct input_ctx *
input_init(struct window_pane *wp, struct bufferevent *bev, input_init(struct window_pane *wp, struct bufferevent *bev,
struct colour_palette *palette, struct client *c) struct colour_palette *palette)
{ {
struct input_ctx *ictx; struct input_ctx *ictx;
@@ -882,7 +881,6 @@ input_init(struct window_pane *wp, struct bufferevent *bev,
ictx->wp = wp; ictx->wp = wp;
ictx->event = bev; ictx->event = bev;
ictx->palette = palette; ictx->palette = palette;
ictx->c = c;
ictx->input_space = INPUT_BUF_START; ictx->input_space = INPUT_BUF_START;
ictx->input_buf = xmalloc(INPUT_BUF_START); ictx->input_buf = xmalloc(INPUT_BUF_START);
@@ -3425,15 +3423,9 @@ input_osc_52(struct input_ctx *ictx, const char *p)
return; return;
if (wp == NULL) { if (wp == NULL) {
/* Popup window. */
if (ictx->c == NULL) {
free(out); free(out);
return; return;
}
tty_set_selection(&ictx->c->tty, clip, out, outlen);
paste_add(NULL, out, outlen);
} else { } else {
/* Normal window. */
screen_write_start_pane(&ctx, wp, NULL); screen_write_start_pane(&ctx, wp, NULL);
screen_write_setselection(&ctx, clip, out, outlen); screen_write_setselection(&ctx, clip, out, outlen);
screen_write_stop(&ctx); screen_write_stop(&ctx);
+1001
View File
File diff suppressed because it is too large Load Diff
+44 -31
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: key-bindings.c,v 1.192 2026/08/06 09:05:04 nicm Exp $ */ /* $OpenBSD: key-bindings.c,v 1.193 2026/09/21 20:45:48 nicm Exp $ */
/* /*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -130,7 +130,7 @@ key_bindings_cmp(struct key_binding *bd1, struct key_binding *bd2)
static void static void
key_bindings_free(struct key_binding *bd) key_bindings_free(struct key_binding *bd)
{ {
cmd_parse_free(bd->cmd); cmd_list_free(bd->cmdlist);
free((void *)bd->note); free((void *)bd->note);
free(bd); free(bd);
} }
@@ -222,15 +222,16 @@ key_bindings_next(__unused struct key_table *table, struct key_binding *bd)
void void
key_bindings_add(const char *name, key_code key, const char *note, int repeat, key_bindings_add(const char *name, key_code key, const char *note, int repeat,
struct cmd_parse_tree *cmd) struct cmd_list *cmdlist)
{ {
struct key_table *table = key_bindings_get_table(name, 1); struct key_table *table;
struct key_binding *bd; struct key_binding *bd;
const char *k;
char *s; char *s;
table = key_bindings_get_table(name, 1);
bd = key_bindings_get(table, key & ~KEYC_MASK_FLAGS); bd = key_bindings_get(table, key & ~KEYC_MASK_FLAGS);
if (cmd == NULL) { if (cmdlist == NULL) {
if (bd != NULL) { if (bd != NULL) {
if (note != NULL) { if (note != NULL) {
free((void *)bd->note); free((void *)bd->note);
@@ -255,11 +256,11 @@ key_bindings_add(const char *name, key_code key, const char *note, int repeat,
if (repeat) if (repeat)
bd->flags |= KEY_BINDING_REPEAT; bd->flags |= KEY_BINDING_REPEAT;
bd->cmd = cmd; bd->cmdlist = cmdlist;
k = key_string_lookup_key(bd->key, 1); s = cmd_list_print(bd->cmdlist, 0);
s = cmd_parse_print(bd->cmd, 0); log_debug("%s: %#llx %s = %s", __func__, bd->key,
log_debug("%s: %#llx %s = %s", __func__, bd->key, k, s); key_string_lookup_key(bd->key, 1), s);
free(s); free(s);
} }
@@ -268,7 +269,6 @@ key_bindings_remove(const char *name, key_code key)
{ {
struct key_table *table; struct key_table *table;
struct key_binding *bd; struct key_binding *bd;
const char *k;
table = key_bindings_get_table(name, 0); table = key_bindings_get_table(name, 0);
if (table == NULL) if (table == NULL)
@@ -278,8 +278,8 @@ key_bindings_remove(const char *name, key_code key)
if (bd == NULL) if (bd == NULL)
return; return;
k = key_string_lookup_key(bd->key, 1); log_debug("%s: %#llx %s", __func__, bd->key,
log_debug("%s: %#llx %s", __func__, bd->key, k); key_string_lookup_key(bd->key, 1));
RB_REMOVE(key_bindings, &table->key_bindings, bd); RB_REMOVE(key_bindings, &table->key_bindings, bd);
key_bindings_free(bd); key_bindings_free(bd);
@@ -311,8 +311,9 @@ key_bindings_reset(const char *name, key_code key)
return; return;
} }
cmd_parse_free(bd->cmd); cmd_list_free(bd->cmdlist);
bd->cmd = cmd_parse_add_ref(dd->cmd); bd->cmdlist = dd->cmdlist;
bd->cmdlist->references++;
free((void *)bd->note); free((void *)bd->note);
if (dd->note != NULL) if (dd->note != NULL)
@@ -369,7 +370,8 @@ key_bindings_init_done(__unused struct cmdq_item *item, __unused void *data)
if (bd->note != NULL) if (bd->note != NULL)
new_bd->note = xstrdup(bd->note); new_bd->note = xstrdup(bd->note);
new_bd->flags = bd->flags; new_bd->flags = bd->flags;
new_bd->cmd = cmd_parse_add_ref(bd->cmd); new_bd->cmdlist = bd->cmdlist;
new_bd->cmdlist->references++;
RB_INSERT(key_bindings, &table->default_key_bindings, RB_INSERT(key_bindings, &table->default_key_bindings,
new_bd); new_bd);
} }
@@ -521,7 +523,7 @@ key_bindings_init(void)
"bind -n TripleClick1Pane { select-pane -t=; if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -H; send -X select-line; run -d0.3; send -X copy-pipe-and-cancel } }", "bind -n TripleClick1Pane { select-pane -t=; if -F '#{||:#{pane_in_mode},#{mouse_any_flag}}' { send -M } { copy-mode -H; send -X select-line; run -d0.3; send -X copy-pipe-and-cancel } }",
/* Mouse button 1 on border. */ /* Mouse button 1 on border. */
"bind -n MouseDown1Border { select-pane -M }", "bind -n MouseDown1Border { select-pane -t= }",
/* Mouse button 1 drag on border. */ /* Mouse button 1 drag on border. */
"bind -n MouseDrag1Border { resize-pane -M }", "bind -n MouseDrag1Border { resize-pane -M }",
@@ -731,39 +733,50 @@ key_bindings_init(void)
"bind -Tcopy-mode-vi C-Up { send -X scroll-up }", "bind -Tcopy-mode-vi C-Up { send -X scroll-up }",
"bind -Tcopy-mode-vi C-Down { send -X scroll-down }", "bind -Tcopy-mode-vi C-Down { send -X scroll-down }",
}; };
u_int i; u_int i;
struct cmd_parse_tree *tree; struct cmd_parse_result *pr;
struct cmdq_item *new_item;
char *cause = NULL;
for (i = 0; i < nitems(defaults); i++) { for (i = 0; i < nitems(defaults); i++) {
tree = cmd_parse_from_string(defaults[i], NULL, &cause); pr = cmd_parse_from_string(defaults[i], NULL);
if (tree == NULL) if (pr->status != CMD_PARSE_SUCCESS) {
log_debug("%s", pr->error);
fatalx("bad default key: %s", defaults[i]); fatalx("bad default key: %s", defaults[i]);
}
new_item = cmd_invoke_get(tree, NULL, NULL); cmdq_append(NULL, cmdq_get_command(pr->cmdlist, NULL));
cmdq_append(NULL, new_item); cmd_list_free(pr->cmdlist);
cmd_parse_free(tree);
} }
cmdq_append(NULL, cmdq_get_callback(key_bindings_init_done, NULL)); cmdq_append(NULL, cmdq_get_callback(key_bindings_init_done, NULL));
} }
static enum cmd_retval
key_bindings_read_only(struct cmdq_item *item, __unused void *data)
{
cmdq_error(item, "client is read-only");
return (CMD_RETURN_ERROR);
}
struct cmdq_item * struct cmdq_item *
key_bindings_dispatch(struct key_binding *bd, struct cmdq_item *item, key_bindings_dispatch(struct key_binding *bd, struct cmdq_item *item,
struct client *c, struct key_event *event, struct cmd_find_state *fs) struct client *c, struct key_event *event, struct cmd_find_state *fs)
{ {
struct cmdq_item *new_item; struct cmdq_item *new_item;
struct cmdq_state *new_state; struct cmdq_state *new_state;
int flags = 0; int readonly, flags = 0;
if (c == NULL || (~c->flags & CLIENT_READONLY))
readonly = 1;
else
readonly = cmd_list_all_have(bd->cmdlist, CMD_READONLY);
if (!readonly)
new_item = cmdq_get_callback(key_bindings_read_only, NULL);
else {
if (bd->flags & KEY_BINDING_REPEAT) if (bd->flags & KEY_BINDING_REPEAT)
flags |= CMDQ_STATE_REPEAT; flags |= CMDQ_STATE_REPEAT;
new_state = cmdq_new_state(fs, event, flags); new_state = cmdq_new_state(fs, event, flags);
new_item = cmdq_get_command(bd->cmdlist, new_state);
new_item = cmd_invoke_get(bd->cmd, new_state, NULL);
cmdq_free_state(new_state); cmdq_free_state(new_state);
}
if (item != NULL) if (item != NULL)
new_item = cmdq_insert_after(item, new_item); new_item = cmdq_insert_after(item, new_item);
else else
+962 -138
View File
File diff suppressed because it is too large Load Diff
+75 -35
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: layout.c,v 1.98 2026/08/25 18:38:05 nicm Exp $ */ /* $OpenBSD: layout.c,v 1.101 2026/09/20 08:42:46 nicm Exp $ */
/* /*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -106,7 +106,7 @@ layout_free_cell(struct layout_cell *lc, int only_nodes)
} }
break; break;
case LAYOUT_WINDOWPANE: case LAYOUT_WINDOWPANE:
if (lc->wp != NULL) { if (lc->wp != NULL && lc->wp->layout_cell != NULL) {
lc->wp->layout_cell->parent = NULL; lc->wp->layout_cell->parent = NULL;
lc->wp->layout_cell = NULL; lc->wp->layout_cell = NULL;
} }
@@ -233,29 +233,6 @@ layout_make_node(struct layout_cell *lc, enum layout_type type)
lc->wp = NULL; lc->wp = NULL;
} }
/* Fix z-indexes. */
void
layout_fix_zindexes(struct window *w, struct layout_cell *lc)
{
struct layout_cell *lcchild;
if (lc == NULL)
return;
switch (lc->type) {
case LAYOUT_WINDOWPANE:
TAILQ_INSERT_TAIL(&w->z_index, lc->wp, zentry);
break;
case LAYOUT_LEFTRIGHT:
case LAYOUT_TOPBOTTOM:
TAILQ_FOREACH(lcchild, &lc->cells, entry)
layout_fix_zindexes(w, lcchild);
return;
default:
fatalx("bad layout type");
}
}
int int
layout_cell_is_tiled(struct layout_cell *lc) layout_cell_is_tiled(struct layout_cell *lc)
{ {
@@ -265,7 +242,7 @@ layout_cell_is_tiled(struct layout_cell *lc)
return is_leaf && !is_floating; return is_leaf && !is_floating;
} }
static int int
layout_cell_has_tiled_child(struct layout_cell *lc) layout_cell_has_tiled_child(struct layout_cell *lc)
{ {
struct layout_cell *lcchild; struct layout_cell *lcchild;
@@ -492,7 +469,6 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
sx = PANE_MINIMUM; sx = PANE_MINIMUM;
else else
sx = sx - sb_w - sb_pad; sx = sx - sb_w - sb_pad;
wp->flags |= PANE_REDRAWSCROLLBAR;
} }
window_pane_resize(wp, sx, sy); window_pane_resize(wp, sx, sy);
@@ -500,27 +476,32 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
if (wp->xoff != old_xoff || if (wp->xoff != old_xoff ||
wp->yoff != old_yoff || wp->yoff != old_yoff ||
wp->sx != old_sx || wp->sx != old_sx ||
wp->sy != old_sy) wp->sy != old_sy) {
if (window_pane_scrollbar_reserve(wp))
wp->flags |= PANE_REDRAWSCROLLBAR;
changed = 1; changed = 1;
} }
}
if (changed) if (changed)
redraw_invalidate_scene(w); redraw_invalidate_scene(w);
} }
/* Count the number of available cells in a layout. */ /* Count the number of available cells in a layout. */
u_int u_int
layout_count_cells(struct layout_cell *lc) layout_count_cells(struct layout_cell *lc, int with_floating)
{ {
struct layout_cell *lcchild; struct layout_cell *lcchild;
u_int count = 0; u_int count = 0;
switch (lc->type) { switch (lc->type) {
case LAYOUT_WINDOWPANE: case LAYOUT_WINDOWPANE:
if (lc->flags & LAYOUT_CELL_FLOATING && !with_floating)
return 0;
return (1); return (1);
case LAYOUT_LEFTRIGHT: case LAYOUT_LEFTRIGHT:
case LAYOUT_TOPBOTTOM: case LAYOUT_TOPBOTTOM:
TAILQ_FOREACH(lcchild, &lc->cells, entry) TAILQ_FOREACH(lcchild, &lc->cells, entry)
count += layout_count_cells(lcchild); count += layout_count_cells(lcchild, with_floating);
return (count); return (count);
default: default:
fatalx("bad layout type"); fatalx("bad layout type");
@@ -721,7 +702,7 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc,
/* If no parent, this is the last pane in a window. */ /* If no parent, this is the last pane in a window. */
lcparent = lc->parent; lcparent = lc->parent;
if (lcparent == NULL) { if (lcparent == NULL) {
if (lc->wp != NULL) if (*lcroot == lc)
*lcroot = NULL; *lcroot = NULL;
layout_free_cell(lc, 0); layout_free_cell(lc, 0);
return; return;
@@ -789,6 +770,49 @@ layout_free(struct window *w, int only_nodes)
layout_free_cell(w->layout_root, only_nodes); layout_free_cell(w->layout_root, only_nodes);
} }
/* Move and resize floating panes so they stay inside the window. */
static void
layout_clamp_floating_panes(struct window *w, u_int sx, u_int sy)
{
struct window_pane *wp;
struct layout_cell *lc;
u_int pad, avail, csx, csy;
TAILQ_FOREACH(wp, &w->z_index, zentry) {
lc = wp->layout_cell;
if (lc == NULL || (~lc->flags & LAYOUT_CELL_FLOATING))
continue;
if (window_pane_get_pane_lines(wp) == PANE_LINES_NONE)
pad = 0;
else
pad = 1;
csx = lc->g.sx;
avail = (sx > 2 * pad) ? sx - 2 * pad : 0;
if (csx > avail)
csx = (avail > PANE_MINIMUM) ? avail : PANE_MINIMUM;
csy = lc->g.sy;
avail = (sy > 2 * pad) ? sy - 2 * pad : 0;
if (csy > avail)
csy = (avail > PANE_MINIMUM) ? avail : PANE_MINIMUM;
if (csx != lc->g.sx || csy != lc->g.sy)
layout_set_size(lc, csx, csy, lc->g.xoff, lc->g.yoff);
if (lc->g.xoff + lc->g.sx + pad > sx) {
if (lc->g.sx + 2 * pad >= sx)
lc->g.xoff = pad;
else
lc->g.xoff = sx - lc->g.sx - pad;
}
if (lc->g.yoff + lc->g.sy + pad > sy) {
if (lc->g.sy + 2 * pad >= sy)
lc->g.yoff = pad;
else
lc->g.yoff = sy - lc->g.sy - pad;
}
}
}
/* Resize the entire layout after window resize. */ /* Resize the entire layout after window resize. */
void void
layout_resize(struct window *w, u_int sx, u_int sy) layout_resize(struct window *w, u_int sx, u_int sy)
@@ -809,8 +833,11 @@ layout_resize(struct window *w, u_int sx, u_int sy)
* out proportionately - this should leave the layout fitting the new * out proportionately - this should leave the layout fitting the new
* window size. * window size.
*/ */
if (lc->type == LAYOUT_WINDOWPANE && (lc->flags & LAYOUT_CELL_FLOATING)) if (lc->type == LAYOUT_WINDOWPANE && (lc->flags & LAYOUT_CELL_FLOATING)) {
layout_clamp_floating_panes(w, sx, sy);
layout_fix_panes(w, NULL);
return; return;
}
xchange = sx - lc->g.sx; xchange = sx - lc->g.sx;
xlimit = layout_resize_check(w, lc, LAYOUT_LEFTRIGHT); xlimit = layout_resize_check(w, lc, LAYOUT_LEFTRIGHT);
if (xchange < 0 && xchange < -xlimit) if (xchange < 0 && xchange < -xlimit)
@@ -840,6 +867,7 @@ layout_resize(struct window *w, u_int sx, u_int sy)
/* Fix cell offsets. */ /* Fix cell offsets. */
layout_fix_offsets(w); layout_fix_offsets(w);
layout_clamp_floating_panes(w, sx, sy);
layout_fix_panes(w, NULL); layout_fix_panes(w, NULL);
} }
@@ -1702,7 +1730,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
enum pane_lines lines, struct window *w, struct layout_geometry *lg, enum pane_lines lines, struct window *w, struct layout_geometry *lg,
char **cause) char **cause)
{ {
int sx, sy, ox, oy; int sx, sy, ox, oy, pad;
char *error = NULL; char *error = NULL;
sx = lg->sx == UINT_MAX ? w->sx / 2 : lg->sx; sx = lg->sx == UINT_MAX ? w->sx / 2 : lg->sx;
@@ -1751,12 +1779,20 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
} }
} }
if (!window_has_floating_panes(w)) {
w->last_new_pane_x = 0;
w->last_new_pane_y = 0;
}
if (ox == INT_MAX) { if (ox == INT_MAX) {
if (w->last_new_pane_x == 0) if (w->last_new_pane_x == 0)
ox = 4; ox = 4;
else { else {
if (lines != PANE_LINES_NONE)
pad = 1;
else
pad = 0;
ox = w->last_new_pane_x + 4; ox = w->last_new_pane_x + 4;
if (w->last_new_pane_x > w->sx) if (ox + sx + pad > (int)w->sx)
ox = 4; ox = 4;
} }
w->last_new_pane_x = ox; w->last_new_pane_x = ox;
@@ -1767,8 +1803,12 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
if (w->last_new_pane_y == 0) if (w->last_new_pane_y == 0)
oy = 2; oy = 2;
else { else {
if (lines != PANE_LINES_NONE)
pad = 1;
else
pad = 0;
oy = w->last_new_pane_y + 2; oy = w->last_new_pane_y + 2;
if (w->last_new_pane_y > w->sy) if (oy + sy + pad > (int)w->sy)
oy = 2; oy = 2;
} }
w->last_new_pane_y = oy; w->last_new_pane_y = oy;
+42 -29
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: menu.c,v 1.70 2026/08/17 07:33:55 nicm Exp $ */ /* $OpenBSD: menu.c,v 1.71 2026/09/21 12:14:32 nicm Exp $ */
/* /*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -51,6 +51,18 @@ struct menu_data {
void *data; void *data;
}; };
void
menu_get_size(struct menu *menu, enum box_lines lines, u_int *sx, u_int *sy)
{
if (lines == BOX_LINES_NONE) {
*sx = menu->item_width + 2;
*sy = menu->count;
} else {
*sx = menu->width + 4;
*sy = menu->count + 2;
}
}
void void
menu_add_items(struct menu *menu, const struct menu_item *items, menu_add_items(struct menu *menu, const struct menu_item *items,
struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs) struct cmdq_item *qitem, struct client *c, struct cmd_find_state *fs)
@@ -142,6 +154,8 @@ menu_add_item(struct menu *menu, const struct menu_item *item,
width = format_width(new_item->name); width = format_width(new_item->name);
if (*new_item->name == '-') if (*new_item->name == '-')
width--; width--;
if (width > menu->item_width)
menu->item_width = width;
if (width > menu->width) if (width > menu->width)
menu->width = width; menu->width = width;
} }
@@ -238,7 +252,7 @@ menu_update(struct menu_data *md)
screen_write_clearscreen(&ctx, 8); screen_write_clearscreen(&ctx, 8);
if (md->border_lines != BOX_LINES_NONE) { if (md->border_lines != BOX_LINES_NONE) {
screen_write_box(&ctx, menu->width + 4, menu->count + 2, screen_write_box(&ctx, menu_width(md), menu_height(md),
md->border_lines, &md->border_style_gc, menu->title); md->border_lines, &md->border_style_gc, menu->title);
} }
@@ -286,11 +300,13 @@ menu_destroy(struct window *w)
void void
menu_get_cursor(struct menu_data *md, u_int *cx, u_int *cy) menu_get_cursor(struct menu_data *md, u_int *cx, u_int *cy)
{ {
*cx = md->px + 2; u_int border = (md->border_lines != BOX_LINES_NONE);
*cx = md->px + 1 + border;
if (md->choice == -1) if (md->choice == -1)
*cy = md->py; *cy = md->py;
else else
*cy = md->py + 1 + md->choice; *cy = md->py + border + md->choice;
} }
struct screen * struct screen *
@@ -302,13 +318,19 @@ menu_screen(struct menu_data *md)
u_int u_int
menu_width(struct menu_data *md) menu_width(struct menu_data *md)
{ {
return (md->menu->width + 4); u_int sx, sy;
menu_get_size(md->menu, md->border_lines, &sx, &sy);
return (sx);
} }
u_int u_int
menu_height(struct menu_data *md) menu_height(struct menu_data *md)
{ {
return (md->menu->count + 2); u_int sx, sy;
menu_get_size(md->menu, md->border_lines, &sx, &sy);
return (sy);
} }
u_int u_int
@@ -333,12 +355,12 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
int move; int move;
const char *name = NULL; const char *name = NULL;
const struct menu_item *item; const struct menu_item *item;
struct cmd_parse_tree *tree;
struct cmdq_item *new_item;
struct key_event saved_event; struct key_event saved_event;
struct cmdq_state *state; struct cmdq_state *state;
enum cmd_parse_status status;
char *error; char *error;
key_code key; key_code key;
u_int border;
if (KEYC_IS_MOUSE(event->key)) { if (KEYC_IS_MOUSE(event->key)) {
/* /*
@@ -354,10 +376,10 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
return (1); return (1);
return (0); return (0);
} }
if (m->x < md->px || border = (md->border_lines != BOX_LINES_NONE);
m->x > md->px + 4 + menu->width || if (m->x < md->px || m->x >= md->px + menu_width(md) ||
m->y < md->py + 1 || m->y < md->py + border ||
m->y > md->py + 1 + n - 1) { m->y >= md->py + border + n) {
if (~md->flags & MENU_STAYOPEN) { if (~md->flags & MENU_STAYOPEN) {
if (!move && MOUSE_RELEASE(m->b)) if (!move && MOUSE_RELEASE(m->b))
return (1); return (1);
@@ -380,7 +402,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b)) if (!MOUSE_WHEEL(m->b) && !MOUSE_DRAG(m->b))
goto chosen; goto chosen;
} }
md->choice = m->y - (md->py + 1); md->choice = m->y - (md->py + border);
if (md->choice != old) if (md->choice != old)
server_redraw_window_menu(md->w); server_redraw_window_menu(md->w);
return (0); return (0);
@@ -529,14 +551,11 @@ chosen:
} else } else
event = NULL; event = NULL;
state = cmdq_new_state(&md->fs, event, 0); state = cmdq_new_state(&md->fs, event, 0);
tree = cmd_parse_from_string(item->command, NULL, &error);
if (tree == NULL) { status = cmd_parse_and_append(item->command, NULL, c, state, &error);
if (status == CMD_PARSE_ERROR) {
cmdq_append(c, cmdq_get_error(error)); cmdq_append(c, cmdq_get_error(error));
free(error); free(error);
} else {
new_item = cmd_invoke_get(tree, state, NULL);
cmdq_append(c, new_item);
cmd_parse_free(tree);
} }
cmdq_free_state(state); cmdq_free_state(state);
@@ -550,20 +569,16 @@ menu_resize(struct menu_data *md, struct window *w)
if (md == NULL) if (md == NULL)
return; return;
nx = md->px; nx = md->px;
ny = md->py; ny = md->py;
sx = md->menu->width + 4; menu_get_size(md->menu, md->border_lines, &sx, &sy);
sy = md->menu->count + 2;
if (nx + sx > w->sx) { if (nx + sx > w->sx) {
if (w->sx <= sx) if (w->sx <= sx)
nx = 0; nx = 0;
else else
nx = w->sx - sx; nx = w->sx - sx;
} }
if (ny + sy > w->sy) { if (ny + sy > w->sy) {
if (w->sy <= sy) if (w->sy <= sy)
ny = 0; ny = 0;
@@ -595,8 +610,9 @@ menu_display(struct menu *menu, int flags, int starting_choice,
w = fs->w; w = fs->w;
o = w->options; o = w->options;
sx = menu->width + 4; if (lines == BOX_LINES_DEFAULT)
sy = menu->count + 2; lines = options_get_number(o, "menu-border-lines");
menu_get_size(menu, lines, &sx, &sy);
if (sx >= w->sx) if (sx >= w->sx)
px = 0; px = 0;
else if (px + sx > w->sx) else if (px + sx > w->sx)
@@ -608,9 +624,6 @@ menu_display(struct menu *menu, int flags, int starting_choice,
w->menu_last_px = px; w->menu_last_px = px;
w->menu_last_py = py; w->menu_last_py = py;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "menu-border-lines");
md = xcalloc(1, sizeof *md); md = xcalloc(1, sizeof *md);
md->w = w; md->w = w;
md->flags = flags; md->flags = flags;
+31 -26
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: mode-tree.c,v 1.101 2026/08/05 07:50:21 nicm Exp $ */ /* $OpenBSD: mode-tree.c,v 1.103 2026/09/24 18:59:00 nicm Exp $ */
/* /*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -277,11 +277,22 @@ mode_tree_free_items(struct mode_tree_list *mtl)
static void static void
mode_tree_check_selected(struct mode_tree_data *mtd) mode_tree_check_selected(struct mode_tree_data *mtd)
{ {
if (mtd->height == 0)
return;
/* If the list has shrunk, do not leave empty lines at the bottom. */
if (mtd->line_size <= mtd->height)
mtd->offset = 0;
else if (mtd->offset > mtd->line_size - mtd->height)
mtd->offset = mtd->line_size - mtd->height;
/* /*
* If the current line would now be off screen reset the offset to the * If the current line would now be off screen move the offset so it is
* last visible line. * visible.
*/ */
if (mtd->current > mtd->height - 1) if (mtd->current < mtd->offset)
mtd->offset = mtd->current;
else if (mtd->current > mtd->offset + mtd->height - 1)
mtd->offset = mtd->current - mtd->height + 1; mtd->offset = mtd->current - mtd->height + 1;
} }
@@ -501,20 +512,14 @@ mode_tree_set_current(struct mode_tree_data *mtd, uint64_t tag)
if (mode_tree_get_tag(mtd, tag, &found)) { if (mode_tree_get_tag(mtd, tag, &found)) {
mtd->current = found; mtd->current = found;
if (mtd->current > mtd->height - 1) mode_tree_check_selected(mtd);
mtd->offset = mtd->current - mtd->height + 1;
else
mtd->offset = 0;
return (1); return (1);
} }
if (mtd->current >= mtd->line_size) { if (mtd->current >= mtd->line_size) {
if (mtd->line_size == 0) if (mtd->line_size == 0)
return (0); return (0);
mtd->current = mtd->line_size - 1; mtd->current = mtd->line_size - 1;
if (mtd->current > mtd->height - 1) mode_tree_check_selected(mtd);
mtd->offset = mtd->current - mtd->height + 1;
else
mtd->offset = 0;
} }
return (0); return (0);
} }
@@ -683,13 +688,13 @@ mode_tree_build(struct mode_tree_data *mtd)
if (mtd->line_list != NULL && tag == UINT64_MAX) if (mtd->line_list != NULL && tag == UINT64_MAX)
tag = mtd->line_list[mtd->current].item->tag; tag = mtd->line_list[mtd->current].item->tag;
mode_tree_set_current(mtd, tag);
mtd->width = screen_size_x(s); mtd->width = screen_size_x(s);
if (mtd->preview != MODE_TREE_PREVIEW_OFF) if (mtd->preview != MODE_TREE_PREVIEW_OFF)
mode_tree_set_height(mtd); mode_tree_set_height(mtd);
else else
mtd->height = screen_size_y(s); mtd->height = screen_size_y(s);
mode_tree_set_current(mtd, tag);
mode_tree_check_selected(mtd); mode_tree_check_selected(mtd);
} }
@@ -1375,12 +1380,14 @@ static void
mode_tree_display_menu(struct mode_tree_data *mtd, struct client *c, u_int x, mode_tree_display_menu(struct mode_tree_data *mtd, struct client *c, u_int x,
u_int y, int outside) u_int y, int outside)
{ {
struct window *w = mtd->wp->window;
struct mode_tree_item *mti; struct mode_tree_item *mti;
struct menu *menu; struct menu *menu;
const struct menu_item *items; const struct menu_item *items;
struct mode_tree_menu *mtm; struct mode_tree_menu *mtm;
char *title; char *title;
u_int line; enum box_lines lines;
u_int line, sx, sy;
if (mtd->offset + y > mtd->line_size - 1) if (mtd->offset + y > mtd->line_size - 1)
line = mtd->current; line = mtd->current;
@@ -1405,14 +1412,17 @@ mode_tree_display_menu(struct mode_tree_data *mtd, struct client *c, u_int x,
mtm->line = line; mtm->line = line;
mtd->references++; mtd->references++;
if (x >= (menu->width + 4) / 2) lines = options_get_number(w->options, "menu-border-lines");
x -= (menu->width + 4) / 2; menu_get_size(menu, lines, &sx, &sy);
if (x >= sx / 2)
x -= sx / 2;
else else
x = 0; x = 0;
x += mtd->wp->xoff; x += mtd->wp->xoff;
y += mtd->wp->yoff; y += mtd->wp->yoff;
if (menu_display(menu, 0, 0, NULL, x, y, c, BOX_LINES_DEFAULT, NULL,
NULL, NULL, NULL, mode_tree_menu_callback, mtm) != 0) { if (menu_display(menu, 0, 0, NULL, x, y, c, lines, NULL, NULL, NULL,
NULL, mode_tree_menu_callback, mtm) != 0) {
mode_tree_remove_ref(mtd); mode_tree_remove_ref(mtd);
free(mtm); free(mtm);
menu_free(menu); menu_free(menu);
@@ -1815,25 +1825,20 @@ void
mode_tree_run_command(struct client *c, struct cmd_find_state *fs, mode_tree_run_command(struct client *c, struct cmd_find_state *fs,
const char *template, const char *name) const char *template, const char *name)
{ {
struct cmd_parse_tree *tree;
struct cmdq_item *new_item;
struct cmdq_state *state; struct cmdq_state *state;
char *command, *error; char *command, *error;
enum cmd_parse_status status;
command = cmd_template_replace(template, name, 1); command = cmd_template_replace(template, name, 1);
if (command != NULL && *command != '\0') { if (command != NULL && *command != '\0') {
state = cmdq_new_state(fs, NULL, 0); state = cmdq_new_state(fs, NULL, 0);
tree = cmd_parse_from_string(command, NULL, &error); status = cmd_parse_and_append(command, NULL, c, state, &error);
if (tree == NULL) { if (status == CMD_PARSE_ERROR) {
if (c != NULL) { if (c != NULL) {
*error = toupper((u_char)*error); *error = toupper((u_char)*error);
status_message_set(c, -1, 1, 0, 0, "%s", error); status_message_set(c, -1, 1, 0, 0, "%s", error);
} }
free(error); free(error);
} else {
new_item = cmd_invoke_get(tree, state, NULL);
cmdq_append(c, new_item);
cmd_parse_free(tree);
} }
cmdq_free_state(state); cmdq_free_state(state);
} }
+4 -2
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: monitor.c,v 1.7 2026/07/27 19:15:58 nicm Exp $ */ /* $OpenBSD: monitor.c,v 1.8 2026/09/07 10:15:25 nicm Exp $ */
/* /*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -614,8 +614,10 @@ monitor_parse(const char *value, char **name, enum monitor_type *type, int *id,
*type = MONITOR_ALL_WINDOWS; *type = MONITOR_ALL_WINDOWS;
else if (sscanf(what, "@%d", id) == 1 && *id >= 0) else if (sscanf(what, "@%d", id) == 1 && *id >= 0)
*type = MONITOR_WINDOW; *type = MONITOR_WINDOW;
else else if (*what == '\0')
*type = MONITOR_SESSION; *type = MONITOR_SESSION;
else
goto fail;
*name = xstrdup(copy); *name = xstrdup(copy);
*format = xstrdup(split); *format = xstrdup(split);
+8 -33
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: options-table.c,v 1.244 2026/09/01 12:49:49 nicm Exp $ */ /* $OpenBSD: options-table.c,v 1.247 2026/09/21 10:22:31 nicm Exp $ */
/* /*
* Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2011 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -76,9 +76,10 @@ static const char *options_table_pane_border_indicators_list[] = {
"off", "colour", "arrows", "both", NULL "off", "colour", "arrows", "both", NULL
}; };
static const char *options_table_pane_border_lines_list[] = { static const char *options_table_pane_border_lines_list[] = {
"single", "double", "heavy", "simple", "number", "spaces", "none", NULL "single", "double", "heavy", "simple", "number", "spaces", "none",
"rounded", NULL
}; };
static const char *options_table_popup_border_lines_list[] = { static const char *options_table_menu_border_lines_list[] = {
"single", "double", "heavy", "simple", "rounded", "padded", "none", NULL "single", "double", "heavy", "simple", "rounded", "padded", "none", NULL
}; };
static const char *options_table_set_clipboard_list[] = { static const char *options_table_set_clipboard_list[] = {
@@ -91,7 +92,7 @@ static const char *options_table_window_size_list[] = {
"largest", "smallest", "manual", "latest", NULL "largest", "smallest", "manual", "latest", NULL
}; };
static const char *options_table_remain_on_exit_list[] = { static const char *options_table_remain_on_exit_list[] = {
"off", "on", "failed", "key", NULL "off", "on", "failed", "key", "failed-key", NULL
}; };
static const char *options_table_destroy_unattached_list[] = { static const char *options_table_destroy_unattached_list[] = {
"off", "on", "keep-last", "keep-group", NULL "off", "on", "keep-last", "keep-group", NULL
@@ -492,7 +493,7 @@ const struct options_table_entry options_table[] = {
{ .name = "menu-border-lines", { .name = "menu-border-lines",
.type = OPTIONS_TABLE_CHOICE, .type = OPTIONS_TABLE_CHOICE,
.scope = OPTIONS_TABLE_WINDOW, .scope = OPTIONS_TABLE_WINDOW,
.choices = options_table_popup_border_lines_list, .choices = options_table_menu_border_lines_list,
.default_num = BOX_LINES_SINGLE, .default_num = BOX_LINES_SINGLE,
.text = "Type of characters used to draw menu border lines. Some of " .text = "Type of characters used to draw menu border lines. Some of "
"these are only supported on terminals with UTF-8 support." "these are only supported on terminals with UTF-8 support."
@@ -1665,40 +1666,14 @@ const struct options_table_entry options_table[] = {
.text = "Pane scrollbar position." .text = "Pane scrollbar position."
}, },
{ .name = "popup-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
.default_str = "bg=themedarkgrey,fg=themewhite",
.flags = OPTIONS_TABLE_IS_STYLE,
.separator = ",",
.text = "Default style of popups."
},
{ .name = "popup-border-style",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_WINDOW,
.default_str = "bg=themedarkgrey,fg=themelightgrey",
.flags = OPTIONS_TABLE_IS_STYLE,
.separator = ",",
.text = "Default style of popup borders."
},
{ .name = "popup-border-lines",
.type = OPTIONS_TABLE_CHOICE,
.scope = OPTIONS_TABLE_WINDOW,
.choices = options_table_popup_border_lines_list,
.default_num = BOX_LINES_SINGLE,
.text = "Type of characters used to draw popup border lines. Some of "
"these are only supported on terminals with UTF-8 support."
},
{ .name = "remain-on-exit", { .name = "remain-on-exit",
.type = OPTIONS_TABLE_CHOICE, .type = OPTIONS_TABLE_CHOICE,
.scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE, .scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE,
.choices = options_table_remain_on_exit_list, .choices = options_table_remain_on_exit_list,
.default_num = 0, .default_num = 0,
.text = "Whether panes should remain ('on'), remain until a key is " .text = "Whether panes should remain ('on'), remain until a key is "
"pressed ('key') or be automatically killed ('off' or " "pressed after any exit ('key') or after a failure "
"('failed-key'), or be automatically killed ('off' or "
"'failed') when the program inside exits." "'failed') when the program inside exits."
}, },
+37 -26
View File
@@ -180,8 +180,8 @@ options_value_free(struct options_entry *o, union options_value *ov)
{ {
if (OPTIONS_IS_STRING(o)) if (OPTIONS_IS_STRING(o))
free(ov->string); free(ov->string);
if (OPTIONS_IS_COMMAND(o)) if (OPTIONS_IS_COMMAND(o) && ov->cmdlist != NULL)
cmd_parse_free(ov->cmd); cmd_list_free(ov->cmdlist);
} }
static char * static char *
@@ -191,7 +191,7 @@ options_value_to_string(struct options_entry *o, union options_value *ov,
char *s; char *s;
if (OPTIONS_IS_COMMAND(o)) if (OPTIONS_IS_COMMAND(o))
return (cmd_parse_print(ov->cmd, 0)); return (cmd_list_print(ov->cmdlist, 0));
if (OPTIONS_IS_NUMBER(o)) { if (OPTIONS_IS_NUMBER(o)) {
switch (o->tableentry->type) { switch (o->tableentry->type) {
case OPTIONS_TABLE_NUMBER: case OPTIONS_TABLE_NUMBER:
@@ -315,8 +315,8 @@ options_default(struct options *oo, const struct options_table_entry *oe)
struct options_entry *o; struct options_entry *o;
union options_value *ov; union options_value *ov;
char key[32]; char key[32];
char *error = NULL;
u_int i; u_int i;
struct cmd_parse_result *pr;
o = options_empty(oo, oe); o = options_empty(oo, oe);
ov = &o->value; ov = &o->value;
@@ -338,9 +338,15 @@ options_default(struct options *oo, const struct options_table_entry *oe)
ov->string = xstrdup(oe->default_str); ov->string = xstrdup(oe->default_str);
break; break;
case OPTIONS_TABLE_COMMAND: case OPTIONS_TABLE_COMMAND:
ov->cmd = cmd_parse_from_string(oe->default_str, NULL, &error); pr = cmd_parse_from_string(oe->default_str, NULL);
if (ov->cmd == NULL) switch (pr->status) {
fatalx("bad default option %s", oe->name); case CMD_PARSE_ERROR:
free(pr->error);
break;
case CMD_PARSE_SUCCESS:
ov->cmdlist = pr->cmdlist;
break;
}
break; break;
default: default:
ov->number = oe->default_num; ov->number = oe->default_num;
@@ -543,9 +549,8 @@ options_array_set(struct options_entry *o, const char *key, const char *value,
{ {
struct options_array_item *a; struct options_array_item *a;
char *new, *new_key; char *new, *new_key;
struct cmd_parse_result *pr;
long long number; long long number;
struct cmd_parse_tree *tree;
char *error = NULL;
if (!OPTIONS_IS_ARRAY(o)) { if (!OPTIONS_IS_ARRAY(o)) {
if (cause != NULL) if (cause != NULL)
@@ -569,14 +574,17 @@ options_array_set(struct options_entry *o, const char *key, const char *value,
} }
if (OPTIONS_IS_COMMAND(o)) { if (OPTIONS_IS_COMMAND(o)) {
tree = cmd_parse_from_string(value, NULL, &error); pr = cmd_parse_from_string(value, NULL);
if (tree == NULL) { switch (pr->status) {
case CMD_PARSE_ERROR:
if (cause != NULL) if (cause != NULL)
*cause = error; *cause = pr->error;
else else
free(error); free(pr->error);
free(new_key); free(new_key);
return (-1); return (-1);
case CMD_PARSE_SUCCESS:
break;
} }
a = options_array_item(o, new_key); a = options_array_item(o, new_key);
@@ -584,7 +592,7 @@ options_array_set(struct options_entry *o, const char *key, const char *value,
a = options_array_new(o, new_key); a = options_array_new(o, new_key);
else else
options_value_free(o, &a->value); options_value_free(o, &a->value);
a->value.cmd = tree; a->value.cmdlist = pr->cmdlist;
free(new_key); free(new_key);
return (0); return (0);
} }
@@ -904,7 +912,7 @@ options_get_number(struct options *oo, const char *name)
return (o->value.number); return (o->value.number);
} }
struct cmd_parse_tree * struct cmd_list *
options_get_command(struct options *oo, const char *name) options_get_command(struct options *oo, const char *name)
{ {
struct options_entry *o; struct options_entry *o;
@@ -914,7 +922,7 @@ options_get_command(struct options *oo, const char *name)
fatalx("missing option %s", name); fatalx("missing option %s", name);
if (!OPTIONS_IS_COMMAND(o)) if (!OPTIONS_IS_COMMAND(o))
fatalx("option %s is not a command", name); fatalx("option %s is not a command", name);
return (o->value.cmd); return (o->value.cmdlist);
} }
struct options_entry * struct options_entry *
@@ -980,7 +988,7 @@ options_set_number(struct options *oo, const char *name, long long value)
struct options_entry * struct options_entry *
options_set_command(struct options *oo, const char *name, options_set_command(struct options *oo, const char *name,
struct cmd_parse_tree *value) struct cmd_list *value)
{ {
struct options_entry *o; struct options_entry *o;
@@ -996,8 +1004,9 @@ options_set_command(struct options *oo, const char *name,
if (!OPTIONS_IS_COMMAND(o)) if (!OPTIONS_IS_COMMAND(o))
fatalx("option %s is not a command", name); fatalx("option %s is not a command", name);
cmd_parse_free(o->value.cmd); if (o->value.cmdlist != NULL)
o->value.cmd = cmd_parse_add_ref(value); cmd_list_free(o->value.cmdlist);
o->value.cmdlist = value;
return (o); return (o);
} }
@@ -1273,8 +1282,7 @@ options_from_string(struct options *oo, const struct options_table_entry *oe,
const char *errstr, *new; const char *errstr, *new;
char *old; char *old;
key_code key; key_code key;
struct cmd_parse_tree *tree; struct cmd_parse_result *pr;
struct cmd_parse_input pi = { 0 };
if (oe != NULL) { if (oe != NULL) {
if (value == NULL && if (value == NULL &&
@@ -1332,14 +1340,17 @@ options_from_string(struct options *oo, const struct options_table_entry *oe,
return (options_from_string_flag(oo, name, value, cause)); return (options_from_string_flag(oo, name, value, cause));
case OPTIONS_TABLE_CHOICE: case OPTIONS_TABLE_CHOICE:
return (options_from_string_choice(oe, oo, name, value, cause)); return (options_from_string_choice(oe, oo, name, value, cause));
case OPTIONS_TABLE_COMMAND: { case OPTIONS_TABLE_COMMAND:
tree = cmd_parse_from_string(value, &pi, cause); pr = cmd_parse_from_string(value, NULL);
if (tree == NULL) switch (pr->status) {
case CMD_PARSE_ERROR:
*cause = pr->error;
return (-1); return (-1);
options_set_command(oo, name, tree); case CMD_PARSE_SUCCESS:
cmd_parse_free(tree); options_set_command(oo, name, pr->cmdlist);
return (0); return (0);
} }
break;
} }
return (-1); return (-1);
} }
-661
View File
@@ -1,661 +0,0 @@
/* $OpenBSD: popup.c,v 1.77 2026/08/28 08:02:16 nicm Exp $ */
/*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
*
* 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 <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "tmux.h"
struct popup_data {
struct client *c;
struct cmdq_item *item;
int flags;
char *title;
char *style;
char *border_style;
struct grid_cell border_cell;
enum box_lines border_lines;
struct screen s;
struct grid_cell defaults;
struct colour_palette palette;
struct visible_ranges r;
struct job *job;
struct input_ctx *ictx;
int status;
popup_close_cb cb;
void *arg;
int close;
/* Current position and size. */
u_int px;
u_int py;
u_int sx;
u_int sy;
/* Preferred position and size. */
u_int ppx;
u_int ppy;
u_int psx;
u_int psy;
enum { OFF, MOVE, SIZE } dragging;
u_int dx;
u_int dy;
u_int lx;
u_int ly;
u_int lb;
};
static void
popup_free(struct popup_data *pd)
{
server_client_unref(pd->c);
if (pd->job != NULL)
job_free(pd->job);
if (pd->ictx != NULL)
input_free(pd->ictx);
free(pd->r.ranges);
screen_free(&pd->s);
colour_palette_free(&pd->palette);
free(pd->title);
free(pd->style);
free(pd->border_style);
free(pd);
}
static void
popup_reapply_styles(struct popup_data *pd)
{
struct client *c = pd->c;
struct session *s = c->session;
struct options *o;
struct format_tree *ft;
struct style sytmp;
if (s == NULL)
return;
o = s->curw->window->options;
ft = format_create_defaults(NULL, c, s, s->curw, NULL);
/* Reapply popup style from options. */
memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
style_apply(&pd->defaults, o, "popup-style", ft);
if (pd->style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->defaults, pd->style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
pd->defaults.attr = 0;
/* Reapply border style from options. */
memcpy(&pd->border_cell, &grid_default_cell, sizeof pd->border_cell);
style_apply(&pd->border_cell, o, "popup-border-style", ft);
if (pd->border_style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->border_cell,
pd->border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
pd->border_cell.attr = 0;
format_free(ft);
}
static void
popup_redraw_cb(const struct tty_ctx *ttyctx)
{
struct popup_data *pd = ttyctx->arg;
pd->c->flags |= CLIENT_REDRAWOVERLAY;
}
static int
popup_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
struct popup_data *pd = ttyctx->arg;
if (c != pd->c)
return (0);
if (pd->c->flags & CLIENT_REDRAWOVERLAY)
return (0);
ttyctx->wox = 0;
ttyctx->woy = 0;
ttyctx->wsx = c->tty.sx;
ttyctx->wsy = c->tty.sy;
if (pd->border_lines == BOX_LINES_NONE) {
ttyctx->xoff = ttyctx->rxoff = pd->px;
ttyctx->yoff = ttyctx->ryoff = pd->py;
} else {
ttyctx->xoff = ttyctx->rxoff = pd->px + 1;
ttyctx->yoff = ttyctx->ryoff = pd->py + 1;
}
return (1);
}
static void
popup_init_ctx_cb(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
{
struct popup_data *pd = ctx->arg;
memcpy(&ttyctx->defaults, &pd->defaults, sizeof ttyctx->defaults);
ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER;
ttyctx->style_ctx.defaults = &ttyctx->defaults;
ttyctx->style_ctx.palette = &pd->palette;
ttyctx->redraw_cb = popup_redraw_cb;
ttyctx->set_client_cb = popup_set_client_cb;
ttyctx->arg = pd;
}
static struct screen *
popup_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy)
{
struct popup_data *pd = data;
if (pd->border_lines == BOX_LINES_NONE) {
*cx = pd->px + pd->s.cx;
*cy = pd->py + pd->s.cy;
} else {
*cx = pd->px + 1 + pd->s.cx;
*cy = pd->py + 1 + pd->s.cy;
}
return (&pd->s);
}
/* Return parts of the input range which are not obstructed by the popup. */
static struct visible_ranges *
popup_check_cb(__unused struct client* c, void *data, u_int px, u_int py,
u_int nx)
{
struct popup_data *pd = data;
struct visible_ranges *r = &pd->r;
server_client_overlay_range(pd->px, pd->py, pd->sx, pd->sy, px, py, nx,
r);
return (r);
}
static void
popup_draw_cb(struct client *c, void *data)
{
struct popup_data *pd = data;
struct tty *tty = &c->tty;
struct screen s;
struct screen_write_ctx ctx;
u_int i, px = pd->px, py = pd->py;
struct grid_cell defaults;
struct tty_style_ctx style_ctx;
popup_reapply_styles(pd);
screen_init(&s, pd->sx, pd->sy, 0);
if (pd->s.hyperlinks != NULL) {
hyperlinks_free(s.hyperlinks);
s.hyperlinks = hyperlinks_copy(pd->s.hyperlinks);
}
screen_write_start(&ctx, &s);
screen_write_clearscreen(&ctx, 8);
if (pd->border_lines == BOX_LINES_NONE) {
screen_write_cursormove(&ctx, 0, 0, 0);
screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx, pd->sy);
} else if (pd->sx > 2 && pd->sy > 2) {
screen_write_box(&ctx, pd->sx, pd->sy, pd->border_lines,
&pd->border_cell, pd->title);
screen_write_cursormove(&ctx, 1, 1, 0);
screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx - 2,
pd->sy - 2);
}
screen_write_stop(&ctx);
memcpy(&defaults, &pd->defaults, sizeof defaults);
if (defaults.fg == 8)
defaults.fg = pd->palette.fg;
if (defaults.bg == 8)
defaults.bg = pd->palette.bg;
style_ctx.defaults = &defaults;
style_ctx.palette = &pd->palette;
style_ctx.dim = 0;
style_ctx.hyperlinks = s.hyperlinks;
c->overlay_check = NULL;
c->overlay_data = NULL;
for (i = 0; i < pd->sy; i++)
tty_draw_line(tty, &s, 0, i, pd->sx, px, py + i, &style_ctx);
screen_free(&s);
c->overlay_check = popup_check_cb;
c->overlay_data = pd;
}
static void
popup_free_cb(__unused struct client *c, void *data)
{
struct popup_data *pd = data;
struct cmdq_item *item = pd->item;
if (pd->cb != NULL)
pd->cb(pd->status, pd->arg);
if (item != NULL) {
if (cmdq_get_client(item) != NULL &&
cmdq_get_client(item)->session == NULL)
cmdq_get_client(item)->retval = pd->status;
cmdq_continue(item);
}
popup_free(pd);
}
static void
popup_resize_cb(__unused struct client *c, void *data)
{
struct popup_data *pd = data;
struct tty *tty = &c->tty;
if (pd == NULL)
return;
/* Adjust position and size. */
if (pd->psy > tty->sy)
pd->sy = tty->sy;
else
pd->sy = pd->psy;
if (pd->psx > tty->sx)
pd->sx = tty->sx;
else
pd->sx = pd->psx;
if (pd->ppy + pd->sy > tty->sy)
pd->py = tty->sy - pd->sy;
else
pd->py = pd->ppy;
if (pd->ppx + pd->sx > tty->sx)
pd->px = tty->sx - pd->sx;
else
pd->px = pd->ppx;
/* Avoid zero size screens. */
if (pd->border_lines == BOX_LINES_NONE) {
screen_resize(&pd->s, pd->sx, pd->sy, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx, pd->sy );
} else if (pd->sx > 2 && pd->sy > 2) {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
}
static void
popup_handle_drag(struct client *c, struct popup_data *pd,
struct mouse_event *m)
{
u_int px, py;
if (!MOUSE_DRAG(m->b))
pd->dragging = OFF;
else if (pd->dragging == MOVE) {
if (m->x < pd->dx)
px = 0;
else if (m->x - pd->dx + pd->sx > c->tty.sx)
px = c->tty.sx - pd->sx;
else
px = m->x - pd->dx;
if (m->y < pd->dy)
py = 0;
else if (m->y - pd->dy + pd->sy > c->tty.sy)
py = c->tty.sy - pd->sy;
else
py = m->y - pd->dy;
pd->px = px;
pd->py = py;
pd->dx = m->x - pd->px;
pd->dy = m->y - pd->py;
pd->ppx = px;
pd->ppy = py;
server_redraw_client(c);
} else if (pd->dragging == SIZE) {
if (pd->border_lines == BOX_LINES_NONE) {
if (m->x < pd->px + 1)
return;
if (m->y < pd->py + 1)
return;
} else {
if (m->x < pd->px + 3)
return;
if (m->y < pd->py + 3)
return;
}
pd->sx = m->x - pd->px;
pd->sy = m->y - pd->py;
pd->psx = pd->sx;
pd->psy = pd->sy;
if (pd->border_lines == BOX_LINES_NONE) {
screen_resize(&pd->s, pd->sx, pd->sy, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx, pd->sy);
} else {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
server_redraw_client(c);
}
}
static int
popup_key_cb(struct client *c, void *data, struct key_event *event)
{
struct popup_data *pd = data;
struct mouse_event *m = &event->m;
const char *buf;
size_t len;
u_int px, py;
enum { NONE, LEFT, RIGHT, TOP, BOTTOM } border = NONE;
if (KEYC_IS_MOUSE(event->key)) {
if (pd->dragging != OFF) {
popup_handle_drag(c, pd, m);
goto out;
}
if (m->x < pd->px ||
m->x > pd->px + pd->sx - 1 ||
m->y < pd->py ||
m->y > pd->py + pd->sy - 1) {
return (0);
}
if (pd->border_lines != BOX_LINES_NONE) {
if (m->x == pd->px)
border = LEFT;
else if (m->x == pd->px + pd->sx - 1)
border = RIGHT;
else if (m->y == pd->py)
border = TOP;
else if (m->y == pd->py + pd->sy - 1)
border = BOTTOM;
}
if ((m->b & MOUSE_MASK_MODIFIERS) == 0 &&
MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_3 &&
(border == LEFT || border == TOP))
goto out;
if (((m->b & MOUSE_MASK_MODIFIERS) == MOUSE_MASK_META) ||
(border != NONE && !MOUSE_DRAG(m->lb))) {
if (!MOUSE_DRAG(m->b))
goto out;
if (MOUSE_BUTTONS(m->lb) == MOUSE_BUTTON_1)
pd->dragging = MOVE;
else if (MOUSE_BUTTONS(m->lb) == MOUSE_BUTTON_3)
pd->dragging = SIZE;
pd->dx = m->lx - pd->px;
pd->dy = m->ly - pd->py;
goto out;
}
}
if ((((pd->flags & (POPUP_CLOSEEXIT|POPUP_CLOSEEXITZERO)) == 0) ||
pd->job == NULL) &&
(event->key == '\033' || event->key == ('c'|KEYC_CTRL)))
return (1);
if (pd->job == NULL && (pd->flags & POPUP_CLOSEANYKEY) &&
!KEYC_IS_MOUSE(event->key) && !KEYC_IS_PASTE(event->key))
return (1);
if (pd->job != NULL) {
if (KEYC_IS_MOUSE(event->key)) {
/* Must be inside, checked already. */
if (pd->border_lines == BOX_LINES_NONE) {
px = m->x - pd->px;
py = m->y - pd->py;
} else {
px = m->x - pd->px - 1;
py = m->y - pd->py - 1;
}
if (!input_key_get_mouse(&pd->s, m, px, py, &buf, &len))
return (0);
bufferevent_write(job_get_event(pd->job), buf, len);
return (0);
}
input_key(&pd->s, job_get_event(pd->job), event->key);
}
return (0);
out:
pd->lx = m->x;
pd->ly = m->y;
pd->lb = m->b;
return (0);
}
static void
popup_job_update_cb(struct job *job)
{
struct popup_data *pd = job_get_data(job);
struct evbuffer *evb = job_get_event(job)->input;
struct client *c = pd->c;
struct screen *s = &pd->s;
void *data = EVBUFFER_DATA(evb);
size_t size = EVBUFFER_LENGTH(evb);
if (size == 0)
return;
c->overlay_check = NULL;
c->overlay_data = NULL;
input_parse_screen(pd->ictx, s, popup_init_ctx_cb, pd, data, size);
c->overlay_check = popup_check_cb;
c->overlay_data = pd;
evbuffer_drain(evb, size);
}
static void
popup_job_complete_cb(struct job *job)
{
struct popup_data *pd = job_get_data(job);
int status;
status = job_get_status(pd->job);
if (WIFEXITED(status))
pd->status = WEXITSTATUS(status);
else if (WIFSIGNALED(status))
pd->status = WTERMSIG(status);
else
pd->status = 0;
pd->job = NULL;
if ((pd->flags & POPUP_CLOSEEXIT) ||
((pd->flags & POPUP_CLOSEEXITZERO) && pd->status == 0))
server_client_clear_overlay(pd->c);
}
int
popup_present(struct client *c)
{
return (c->overlay_draw == popup_draw_cb);
}
int
popup_modify(struct client *c, const char *title, const char *style,
const char *border_style, enum box_lines lines, int flags)
{
struct popup_data *pd = c->overlay_data;
struct style sytmp;
if (title != NULL) {
if (pd->title != NULL)
free(pd->title);
pd->title = xstrdup(title);
}
if (border_style != NULL) {
free(pd->border_style);
pd->border_style = xstrdup(border_style);
style_set(&sytmp, &pd->border_cell);
if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
if (style != NULL) {
free(pd->style);
pd->style = xstrdup(style);
style_set(&sytmp, &pd->defaults);
if (style_parse(&sytmp, &pd->defaults, style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
if (lines != BOX_LINES_DEFAULT) {
if (lines == BOX_LINES_NONE && pd->border_lines != lines) {
screen_resize(&pd->s, pd->sx, pd->sy, 1);
job_resize(pd->job, pd->sx, pd->sy);
} else if (pd->border_lines == BOX_LINES_NONE &&
pd->border_lines != lines) {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 1);
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
pd->border_lines = lines;
tty_resize(&c->tty);
}
if (flags != -1)
pd->flags = flags;
server_redraw_client(c);
return (0);
}
int
popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px,
u_int py, u_int sx, u_int sy, struct environ *env, const char *shellcmd,
int argc, char **argv, const char *cwd, const char *title, struct client *c,
struct session *s, const char *style, const char *border_style,
popup_close_cb cb, void *arg)
{
struct popup_data *pd;
u_int jx, jy;
struct options *o;
struct style sytmp;
if (s != NULL)
o = s->curw->window->options;
else
o = c->session->curw->window->options;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "popup-border-lines");
if (lines == BOX_LINES_NONE) {
if (sx < 1 || sy < 1)
return (-1);
jx = sx;
jy = sy;
} else {
if (sx < 3 || sy < 3)
return (-1);
jx = sx - 2;
jy = sy - 2;
}
if (c->tty.sx < sx || c->tty.sy < sy)
return (-1);
pd = xcalloc(1, sizeof *pd);
pd->item = item;
pd->flags = flags;
if (title != NULL)
pd->title = xstrdup(title);
if (style != NULL)
pd->style = xstrdup(style);
if (border_style != NULL)
pd->border_style = xstrdup(border_style);
pd->c = c;
pd->c->references++;
pd->cb = cb;
pd->arg = arg;
pd->status = 128 + SIGHUP;
pd->border_lines = lines;
memcpy(&pd->border_cell, &grid_default_cell, sizeof pd->border_cell);
style_apply(&pd->border_cell, o, "popup-border-style", NULL);
if (border_style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
pd->border_cell.attr = 0;
screen_init(&pd->s, jx, jy, 0);
screen_set_default_cursor(&pd->s, global_w_options);
colour_palette_init(&pd->palette);
colour_palette_from_option(&pd->palette, global_w_options);
memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
style_apply(&pd->defaults, o, "popup-style", NULL);
if (style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->defaults, style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
pd->defaults.attr = 0;
pd->px = px;
pd->py = py;
pd->sx = sx;
pd->sy = sy;
pd->ppx = px;
pd->ppy = py;
pd->psx = sx;
pd->psy = sy;
pd->job = job_run(shellcmd, argc, argv, env, s, cwd,
popup_job_update_cb, popup_job_complete_cb, NULL, pd,
JOB_NOWAIT|JOB_PTY|JOB_KEEPWRITE|JOB_DEFAULTSHELL, jx, jy);
if (pd->job == NULL) {
popup_free(pd);
return (-1);
}
pd->ictx = input_init(NULL, job_get_event(pd->job), &pd->palette, c);
server_client_set_overlay(c, 0, popup_check_cb, popup_mode_cb,
popup_draw_cb, popup_key_cb, popup_free_cb, popup_resize_cb, pd);
return (0);
}
+5 -3
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: prompt.c,v 1.6 2026/08/17 06:45:16 nicm Exp $ */ /* $OpenBSD: prompt.c,v 1.7 2026/09/09 07:53:03 nicm Exp $ */
/* /*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com> * Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1493,9 +1493,11 @@ append_key:
utf8_set(&tmp, key); utf8_set(&tmp, key);
if (key <= 0x1f || key == 0x7f) if (key <= 0x1f || key == 0x7f)
tmp.width = 2; tmp.width = 2;
} else if (KEYC_IS_UNICODE(key)) } else if (KEYC_IS_UNICODE(key)) {
utf8_to_data(key, &tmp); utf8_to_data(key, &tmp);
else if (tmp.size == 0)
return (PROMPT_KEY_HANDLED);
} else
return (PROMPT_KEY_HANDLED); return (PROMPT_KEY_HANDLED);
pr->buffer = xreallocarray(pr->buffer, size + 2, pr->buffer = xreallocarray(pr->buffer, size + 2,
+23 -20
View File
@@ -1,33 +1,17 @@
TESTS!= echo *.sh TESTS!= echo *.sh
LOGDIR=logs LOGDIR=logs
.PHONY: all .PHONY: all prepare $(TESTS)
.NOTPARALLEL: all
all: all: prepare $(TESTS)
@mkdir -p "$(LOGDIR)"; \ @failed=0; failures=; \
rm -f "$(LOGDIR)"/*.log; \
failed=0; failures=; \
for test in $(TESTS); do \ for test in $(TESTS); do \
base=$${test##*/}; \ base=$${test##*/}; \
log="$(LOGDIR)/$${base%.sh}.log"; \ log="$(LOGDIR)/$${base%.sh}.log"; \
rm -f "$$log"; \ if [ -f "$$log" ]; then \
printf '%-40s ' "$$test"; \
start=$$(date +%s); \
env -i LC_CTYPE=C.UTF-8 MallocNanoZone=0 \
sh -x "$$test" >"$$log" 2>&1; \
if [ $$? -eq 0 ]; then \
end=$$(date +%s); \
rm -f "$$log"; \
echo "PASS ($$((end - start))s)"; \
else \
end=$$(date +%s); \
echo "FAIL ($$((end - start))s)"; \
echo " log: $$log"; \
failed=1; \ failed=1; \
failures="$$failures $$test"; \ failures="$$failures $$test"; \
fi; \ fi; \
sleep 1; \
done; \ done; \
if [ "$$failed" -ne 0 ]; then \ if [ "$$failed" -ne 0 ]; then \
echo; \ echo; \
@@ -39,3 +23,22 @@ all:
rmdir "$(LOGDIR)" 2>/dev/null || true; \ rmdir "$(LOGDIR)" 2>/dev/null || true; \
fi; \ fi; \
exit $$failed exit $$failed
prepare:
@mkdir -p "$(LOGDIR)"
@rm -f "$(LOGDIR)"/*.log
$(TESTS): prepare
@base="$@"; base=$${base##*/}; \
log="$(LOGDIR)/$${base%.sh}.log"; \
start=$$(date +%s); \
if env -i LC_CTYPE=C.UTF-8 MallocNanoZone=0 \
sh -x "$@" >"$$log" 2>&1; then \
end=$$(date +%s); \
rm -f "$$log"; \
printf '%-40s PASS (%ss)\n' "$@" "$$((end - start))"; \
else \
end=$$(date +%s); \
printf '%-40s FAIL (%ss)\n log: %s\n' \
"$@" "$$((end - start))" "$$log"; \
fi
+32 -2
View File
@@ -2,8 +2,9 @@
# monitor-activity, monitor-bell and monitor-silence: both the # monitor-activity, monitor-bell and monitor-silence: both the
# alert-activity, alert-bell and alert-silence hooks and the winlink alert # alert-activity, alert-bell and alert-silence hooks and the winlink alert
# flags (window_activity_flag, window_bell_flag, window_silence_flag and # flags (window_activity_flag, window_bell_flag, window_silence_flag,
# the #, !, ~ characters in window_flags). The sessions are detached so # their session equivalents and the #, !, ~ characters in window_flags).
# The sessions are detached so
# alert flags are set even on the current window; the *-action options # alert flags are set even on the current window; the *-action options
# still decide whether the hooks fire. Panes run cat: activity is # still decide whether the hooks fire. Panes run cat: activity is
# generated by the tty echo of send-keys and a bell by sending a BEL and # generated by the tty echo of send-keys and a bell by sending a BEL and
@@ -111,6 +112,24 @@ flags_have()
esac esac
} }
assert_session_flag()
{
flag="#{session_${1}_flag}"
expected=$2
# Only mon exists here. Check both window contexts and list-sessions.
for target in mon:w0 "mon:$3"; do
value=$($TMUX display -pt "$target" "$flag") ||
fail "display $flag failed"
[ "$value" = "$expected" ] ||
fail "expected $flag for $target to be '$expected' but got '$value'"
done
value=$($TMUX list-sessions -F "$flag") ||
fail "list-sessions $flag failed"
[ "$value" = "$expected" ] ||
fail "expected list-sessions $flag to be '$expected' but got '$value'"
}
flags_lack() flags_lack()
{ {
target=$1 target=$1
@@ -150,10 +169,12 @@ $TMUX set-hook -g alert-silence \
# alert-bell and sets the bell flag, shown as ! in window_flags. # alert-bell and sets the bell flag, shown as ! in window_flags.
$TMUX neww -d -t mon: -n bellw cat || fail "new-window bellw failed" $TMUX neww -d -t mon: -n bellw cat || fail "new-window bellw failed"
assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0 assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0
assert_session_flag bell 0 bellw
flags_lack mon:bellw '!' flags_lack mon:bellw '!'
bell mon:bellw bell mon:bellw
wait_for @log '|alert-bell:mon:bellw' wait_for @log '|alert-bell:mon:bellw'
wait_for_fmt mon:bellw '#{window_bell_flag}' 1 wait_for_fmt mon:bellw '#{window_bell_flag}' 1
assert_session_flag bell 1 bellw
flags_have mon:bellw '!' flags_have mon:bellw '!'
# Bells are not deduplicated: a second bell fires the hook again even # Bells are not deduplicated: a second bell fires the hook again even
@@ -164,6 +185,7 @@ wait_for @log '|alert-bell:mon:bellw|alert-bell:mon:bellw'
# Selecting the window clears the alert flags. # Selecting the window clears the alert flags.
$TMUX selectw -t mon:bellw || fail "select-window bellw failed" $TMUX selectw -t mon:bellw || fail "select-window bellw failed"
wait_for_fmt mon:bellw '#{window_bell_flag}' 0 wait_for_fmt mon:bellw '#{window_bell_flag}' 0
assert_session_flag bell 0 bellw
flags_lack mon:bellw '!' flags_lack mon:bellw '!'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed" $TMUX selectw -t mon:w0 || fail "select-window w0 failed"
@@ -173,6 +195,7 @@ $TMUX set -g @log '' || fail "reset @log failed"
bell mon:w0 bell mon:w0
wait_for @log '|alert-bell:mon:w0' wait_for @log '|alert-bell:mon:w0'
wait_for_fmt mon:w0 '#{window_bell_flag}' 1 wait_for_fmt mon:w0 '#{window_bell_flag}' 1
assert_session_flag bell 1 bellw
$TMUX selectw -t mon:bellw || fail "select-window bellw failed" $TMUX selectw -t mon:bellw || fail "select-window bellw failed"
$TMUX selectw -t mon:w0 || fail "select-window w0 failed" $TMUX selectw -t mon:w0 || fail "select-window w0 failed"
wait_for_fmt mon:w0 '#{window_bell_flag}' 0 wait_for_fmt mon:w0 '#{window_bell_flag}' 0
@@ -199,6 +222,7 @@ $TMUX neww -d -t mon: -n actw cat || fail "new-window actw failed"
$TMUX set -g @log '' || fail "reset @log failed" $TMUX set -g @log '' || fail "reset @log failed"
activity mon:actw activity mon:actw
assert_fmt_unchanged mon:actw '#{window_activity_flag}' 0 assert_fmt_unchanged mon:actw '#{window_activity_flag}' 0
assert_session_flag activity 0 actw
assert_unchanged @log '' assert_unchanged @log ''
# With monitor-activity on, output fires alert-activity and sets the # With monitor-activity on, output fires alert-activity and sets the
@@ -208,6 +232,7 @@ $TMUX set -wt mon:actw monitor-activity on ||
activity mon:actw activity mon:actw
wait_for @log '|alert-activity:mon:actw' wait_for @log '|alert-activity:mon:actw'
wait_for_fmt mon:actw '#{window_activity_flag}' 1 wait_for_fmt mon:actw '#{window_activity_flag}' 1
assert_session_flag activity 1 actw
flags_have mon:actw '#' flags_have mon:actw '#'
# While the flag is set further activity does not fire the hook again. # While the flag is set further activity does not fire the hook again.
@@ -234,6 +259,7 @@ exec 3>"$OUT/fifo"
wait_for_fmt mon: '#{session_attached}' 1 wait_for_fmt mon: '#{session_attached}' 1
$TMUX selectw -t mon:actw || fail "select-window actw failed" $TMUX selectw -t mon:actw || fail "select-window actw failed"
wait_for_fmt mon:actw '#{window_activity_flag}' 0 wait_for_fmt mon:actw '#{window_activity_flag}' 0
assert_session_flag activity 0 actw
flags_lack mon:actw '#' flags_lack mon:actw '#'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed" $TMUX selectw -t mon:w0 || fail "select-window w0 failed"
exec 3>&- exec 3>&-
@@ -250,6 +276,7 @@ $TMUX set -wt mon:w0 monitor-activity on ||
$TMUX set -g @log '' || fail "reset @log failed" $TMUX set -g @log '' || fail "reset @log failed"
activity mon:w0 activity mon:w0
wait_for_fmt mon:w0 '#{window_activity_flag}' 1 wait_for_fmt mon:w0 '#{window_activity_flag}' 1
assert_session_flag activity 1 actw
assert_unchanged @log '' assert_unchanged @log ''
$TMUX set -wut mon:w0 monitor-activity || $TMUX set -wut mon:w0 monitor-activity ||
fail "unset monitor-activity w0 failed" fail "unset monitor-activity w0 failed"
@@ -273,16 +300,19 @@ $TMUX selectw -t mon:w0 || fail "select-window w0 failed"
# running. # running.
$TMUX neww -d -t mon: -n silw cat || fail "new-window silw failed" $TMUX neww -d -t mon: -n silw cat || fail "new-window silw failed"
assert_fmt_unchanged mon:silw '#{window_silence_flag}' 0 assert_fmt_unchanged mon:silw '#{window_silence_flag}' 0
assert_session_flag silence 0 silw
$TMUX set -g @log '' || fail "reset @log failed" $TMUX set -g @log '' || fail "reset @log failed"
$TMUX set -wt mon:silw monitor-silence 1 || fail "set monitor-silence failed" $TMUX set -wt mon:silw monitor-silence 1 || fail "set monitor-silence failed"
wait_for @log '|alert-silence:mon:silw' wait_for @log '|alert-silence:mon:silw'
wait_for_fmt mon:silw '#{window_silence_flag}' 1 wait_for_fmt mon:silw '#{window_silence_flag}' 1
assert_session_flag silence 1 silw
flags_have mon:silw '~' flags_have mon:silw '~'
assert_unchanged @log '|alert-silence:mon:silw' assert_unchanged @log '|alert-silence:mon:silw'
$TMUX set -wt mon:silw monitor-silence 0 || $TMUX set -wt mon:silw monitor-silence 0 ||
fail "reset monitor-silence failed" fail "reset monitor-silence failed"
$TMUX selectw -t mon:silw || fail "select-window silw failed" $TMUX selectw -t mon:silw || fail "select-window silw failed"
wait_for_fmt mon:silw '#{window_silence_flag}' 0 wait_for_fmt mon:silw '#{window_silence_flag}' 0
assert_session_flag silence 0 silw
flags_lack mon:silw '~' flags_lack mon:silw '~'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed" $TMUX selectw -t mon:w0 || fail "select-window w0 failed"
+79
View File
@@ -0,0 +1,79 @@
#!/bin/sh
# Selecting a pane with attach-session from an already attached client must
# redraw pane contents when window-style and window-active-style differ.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lattach-redraw-outer-$$ -f/dev/null"
BEFORE=$DIR/before
AFTER=$DIR/after
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
LEFT=$($INNER new-session -dPF '#{pane_id}' -s inner -x 40 -y 8 \
"printf 'LEFT'; exec sleep 100") || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g default-terminal screen || exit 1
$INNER split-window -h -t "$LEFT" "printf 'RIGHT'; exec sleep 100" || exit 1
$INNER set-option -w -t "$LEFT" window-style bg=red || exit 1
$INNER set-option -w -t "$LEFT" window-active-style bg=blue || exit 1
$INNER bind-key -n x attach-session -t "$LEFT" || exit 1
$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
sleep 1
$OUTER send-keys -t outer:0.0 x || exit 1
sleep 1
[ "$($INNER display-message -p -t inner '#{pane_id}')" = "$LEFT" ] ||
fail "attach-session did not select the target pane"
$OUTER capture-pane -pe -t outer:0.0 >"$BEFORE" || exit 1
# A forced redraw produces the correct scene. It must be identical to the
# scene drawn immediately by attach-session.
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 1
$OUTER capture-pane -pe -t outer:0.0 >"$AFTER" || exit 1
cmp -s "$BEFORE" "$AFTER" ||
fail "attach-session left stale active/inactive pane styles"
exit 0
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# capture-pane -I line timestamps
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMP=$(mktemp)
trap '$TMUX kill-server 2>/dev/null; rm -f "$TMP"' 0 1 15
$TMUX kill-server 2>/dev/null
before=$(date +%s)
$TMUX new-session -d -x 40 -y 5 'seq 1 12; sleep 10' || exit 1
sleep 1
after=$(date +%s)
$TMUX capture-pane -pILF -S - -E - >"$TMP" || exit 1
awk -v before="$before" -v after="$after" '
$1 !~ /^-?[0-9]+$/ || $2 !~ /^[0-9]+$/ { exit 1 }
$4 == "1" {
if ($2 < before || $2 > after)
exit 1
history = 1
}
$4 == "9" {
if ($2 != 0)
exit 1
visible = 1
}
END { if (!history || !visible) exit 1 }
' "$TMP" || {
cat "$TMP"
exit 1
}
exit 0
Executable → Regular
View File
+10 -1
View File
@@ -5,6 +5,15 @@
PATH=/bin:/usr/bin PATH=/bin:/usr/bin
TERM=screen TERM=screen
# The pane's shell must not be the user's own interactive shell: a custom
# PS1/PROMPT_COMMAND that sets the terminal title (as many do, for tmux/xterm
# TERM types) would redraw over the titles this test sets and checks on
# every prompt, regardless of how long it waits first.
shell=
if command -v bash >/dev/null 2>&1; then
shell='bash --noprofile --norc +o history'
fi
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null" TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null
@@ -30,7 +39,7 @@ must_equal()
[ "$got" = "$want" ] || fail "got '$got', expected '$want'" [ "$got" = "$want" ] || fail "got '$got', expected '$want'"
} }
$TMUX new-session -d -x 80 -y 24 || exit 1 $TMUX new-session -d -x 80 -y 24 -- $shell || exit 1
$TMUX set-option -qg allow-set-title on || exit 1 $TMUX set-option -qg allow-set-title on || exit 1
$TMUX set-option -qg allow-rename on || exit 1 $TMUX set-option -qg allow-rename on || exit 1
$TMUX set-option -qg automatic-rename off || exit 1 $TMUX set-option -qg automatic-rename off || exit 1
+88
View File
@@ -290,4 +290,92 @@ wait_count ': G4' 7
fail "window 1 of zzz not killed" fail "window 1 of zzz not killed"
exit_mode q exit_mode q
# --- help and information preview --------------------------------------------
# The complete tree help is taller than the 24-line test terminal, where the
# help renderer deliberately leaves the tree unchanged.
$TMUX2 resize-window -t out:0 -x 80 -y 50 || exit 1
i=0
while [ "$i" -lt 50 ]; do
# The inner session has a one-line status, leaving a 49-line window.
[ "$($TMUX display-message -p -t aaa:0 '#{window_height}')" -ge 49 ] &&
break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "attached client did not resize for tree help"
$TMUX choose-tree -t aaa:0 -F 'G5' -O index || exit 1
wait_count ': G5' 7
$TMUX2 send-keys -t out:0 F1 || fail "send-keys F1 failed"
wait_for 'Swap current and previous window'
# The first key closes help; the second exits the tree.
$TMUX send-keys -t aaa:0 q q || fail "send-keys help exit failed"
wait_mode aaa:0 0
$TMUX choose-tree -t aaa:0 -F 'G6' -O index || exit 1
wait_count ': G6' 7
$TMUX send-keys -t aaa:0 i || fail "send-keys info failed"
wait_for 'Session'
$TMUX send-keys -t aaa:0 i || fail "send-keys preview failed"
exit_mode q
# --- window swapping and command prompt for current/tagged items -------------
# Restrict a window-only tree to two disposable windows in aaa. J (the
# Shift-Down equivalent) swaps their window objects while leaving their indexes
# in place.
$TMUX new-window -d -t aaa: -n swap-a 'cat' || exit 1
$TMUX new-window -d -t aaa: -n swap-b 'cat' || exit 1
aidx=$($TMUX list-windows -t aaa -F '#{window_index}:#{window_name}' | \
awk -F: '$2 == "swap-a" {print $1}')
bidx=$($TMUX list-windows -t aaa -F '#{window_index}:#{window_name}' | \
awk -F: '$2 == "swap-b" {print $1}')
$TMUX choose-tree -w -t aaa:0 -O index -F 'WT #{window_name}' \
-f '#{m:swap-*,#{window_name}}' || exit 1
wait_count 'WT swap-' 2
$TMUX send-keys -t aaa:0 g j J || fail "window tree swap failed"
i=0
while [ "$i" -lt 50 ]; do
aname=$($TMUX display-message -p -t "aaa:$aidx" '#{window_name}')
bname=$($TMUX display-message -p -t "aaa:$bidx" '#{window_name}')
[ "$aname:$bname" = 'swap-b:swap-a' ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "window tree did not swap the two windows"
# ':' runs an entered command for the current item and rebuilds the tree when
# its queued command completes.
$TMUX set-option -g @tree-command '' || exit 1
$TMUX send-keys -t aaa:0 : || fail "window tree command prompt failed"
wait_for '(current)'
$TMUX send-keys -t aaa:0 -l "set-option -g @tree-command '%%'" || exit 1
$TMUX send-keys -t aaa:0 Enter || exit 1
i=0
while [ "$i" -lt 50 ]; do
value=$($TMUX show-option -gqv @tree-command)
[ -n "$value" ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "window tree entered command did not run"
case "$value" in
=aaa:*.) ;;
*) fail "window tree command target was '$value'" ;;
esac
# Tag both disposable windows and kill them through the tagged confirmation
# callback. The mode-hosting window is outside the filter and survives.
$TMUX send-keys -t aaa:0 g j t j t X || fail "tagged kill keys failed"
wait_for 'Kill 2 tagged?'
$TMUX send-keys -t aaa:0 y || fail "tagged kill confirmation failed"
i=0
while [ "$i" -lt 50 ]; do
left=$($TMUX list-windows -t aaa -F '#{window_name}' | \
grep -c '^swap-' || true)
[ "$left" -eq 0 ] && break
sleep 0.2
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "tagged windows were not killed"
exit_mode q
exit 0 exit 0
+96
View File
@@ -0,0 +1,96 @@
#!/bin/sh
# With clear-on-attach disabled tmux must preserve the terminal contents by
# scrolling them away, rather than entering and clearing the alternate screen.
# Capture the bytes written by a real attached client for both settings.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER_ON="$TEST_TMUX -LtestIon$$ -f/dev/null"
OUTER_ON="$TEST_TMUX -LtestOon$$ -f/dev/null"
INNER_OFF="$TEST_TMUX -LtestIoff$$ -f/dev/null"
OUTER_OFF="$TEST_TMUX -LtestOoff$$ -f/dev/null"
DIR=$(mktemp -d) || exit 1
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER_ON kill-server 2>/dev/null
$INNER_ON kill-server 2>/dev/null
$OUTER_OFF kill-server 2>/dev/null
$INNER_OFF kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
$current_inner list-clients 2>/dev/null | grep -q . && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
capture_attach()
{
setting=$1
output=$2
go=$DIR/go-$setting
if [ "$setting" = on ]; then
current_inner=$INNER_ON
current_outer=$OUTER_ON
else
current_inner=$INNER_OFF
current_outer=$OUTER_OFF
fi
$current_inner new-session -d -s inner -x40 -y10 \
'exec sleep 100' || exit 1
$current_inner set-option -g status off || exit 1
$current_inner set-option -g clear-on-attach "$setting" || exit 1
$current_outer new-session -d -s outer -x40 -y10 \
"while [ ! -e '$go' ]; do sleep 0.01; done; exec $current_inner attach-session -t inner" || exit 1
$current_outer set-option -g status off || exit 1
$current_outer pipe-pane -O "cat >'$output'" || exit 1
touch "$go"
wait_for_client
sleep 0.5
$current_outer pipe-pane -O || exit 1
}
on=$DIR/on
off=$DIR/off
capture_attach on "$on"
capture_attach off "$off"
smcup=$(printf '\033[?1049h')
grep -Fq "$smcup" "$on" || fail "clear-on-attach on did not use smcup"
if grep -Fq "$smcup" "$off"; then
fail "clear-on-attach off used smcup"
fi
# Some screen terminfo entries have indn (CSI Ps S), while older ones only
# have ind (newline). The preserving path uses indn once or repeats ind once
# for every line in the terminal plus one.
escape=$(printf '\033')
if ! LC_ALL=C grep -Eq "${escape}\\[[0-9]+S" "$off"; then
hex=$(od -An -tx1 -v "$off" | tr -d ' \n')
case "$hex" in
*0a0a0a0a0a0a0a0a0a0a0a*) ;;
*) fail "clear-on-attach off did not scroll the old contents away" ;;
esac
fi
exit 0
+82
View File
@@ -0,0 +1,82 @@
#!/bin/sh
# send-keys -K injects a key into a client. Commands produced by that key must
# be inserted immediately after send-keys, before later commands in the same
# command list. Focus events arriving between a prefix and its following key
# must be forwarded without cancelling the prefix table.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
client=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$client" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_for_option()
{
option=$1
expected=$2
i=0
while [ "$i" -lt 50 ]; do
actual=$($INNER show-option -gv "$option" 2>/dev/null)
[ "$actual" = "$expected" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "$option is '$actual', expected '$expected'"
}
cleanup
$INNER new-session -d -s inner -x40 -y10 'exec sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g focus-events on || exit 1
$OUTER new-session -d -s outer -x40 -y10 "$INNER attach -t inner" || exit 1
$OUTER set-option -g status off || exit 1
wait_for_client
# The binding generated by send-keys -K must run before the command which
# follows send-keys in this command list. Before the queue-ordering fix this
# produces AB rather than BA.
$INNER set-option -g @order '' || exit 1
$INNER bind-key -n a set-option -gF @order '#{@order}B' || exit 1
$INNER send-keys -K -c "$client" a \; \
set-option -gF @order '#{@order}A' || exit 1
wait_for_option @order BA
# Put the real attached client in the prefix table, deliver FocusIn, then the
# bound key. FocusIn is not bindable and must not reset the prefix table.
$INNER bind-key x set-option -g @focus-prefix yes || exit 1
$OUTER send-keys C-b || exit 1
focus_in=$(printf '\033[I')
$OUTER send-keys -l "$focus_in" || exit 1
$OUTER send-keys x || exit 1
wait_for_option @focus-prefix yes
exit 0
+101
View File
@@ -0,0 +1,101 @@
#!/bin/sh
# Exercise clock-mode initialization, all four styles, its one-second timer,
# resize into the compact renderer, arbitrary-key exit, and cleanup.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
capture()
{
$OUTER capture-pane -p -t outer:0.0 2>/dev/null
}
wait_mode()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER display-message -p -t clock:0 '#{pane_mode}' 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "pane mode is '$got', expected '$want'"
}
wait_hashes()
{
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -q '#' && return 0
sleep 0.1
i=$((i + 1))
done
fail "large clock did not render"
}
$INNER new-session -d -s clock -x80 -y24 'exec sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -w clock-mode-colour red || exit 1
$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t clock" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
sleep 1
for style in 12 24 12-with-seconds 24-with-seconds; do
$INNER set-option -w -t clock:0 clock-mode-style "$style" || exit 1
$INNER clock-mode -t clock:0 || exit 1
wait_mode clock-mode
wait_hashes
# Let at least one timer callback observe a new second.
[ "$style" != 12-with-seconds ] || sleep 1.2
$INNER send-keys -t clock:0 x || exit 1
wait_mode ''
done
# Resizing an active mode below the large-glyph threshold selects the compact
# renderer. A visible time contains a colon and no block-clock hash.
$INNER clock-mode -t clock:0 || exit 1
wait_mode clock-mode
$INNER resize-window -t clock:0 -x20 -y5 || exit 1
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -Eq '[0-9][0-9]?:[0-9][0-9]' && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "compact clock did not render after resize"
printf '%s\n' "$captured" | grep -q '#' &&
fail "compact clock still used large glyphs"
$INNER send-keys -t clock:0 Enter || exit 1
wait_mode ''
$INNER has-session -t clock || fail "server died during clock-mode tests"
exit 0
-50
View File
@@ -1,50 +0,0 @@
#!/bin/sh
# Client argv parsing and the start-server boundary.
#
# The command line given to the client is parsed and invoked as one sequence.
# This checks: a multi-command argv runs every command and starts the server
# (new-session carries CMD_STARTSERVER); a command without start-server fails
# cleanly against no server; and a parse error leaves no stray server behind.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
# Multi-command argv: starts the server and runs all commands in order.
$TMUX new-session -d -swork -nbase \; set -g @a one \; new-window -dn W2 \; \
set -g @b two || exit 1
[ "$($TMUX show -gv @a)" = one ] || { echo "argv cmd 2 did not run" >&2; exit 1; }
[ "$($TMUX show -gv @b)" = two ] || { echo "argv cmd 4 did not run" >&2; exit 1; }
got=$($TMUX list-windows -t work -F '#{window_name}' | tr '\n' ',')
[ "$got" = "base,W2," ] || { echo "argv windows: got [$got]" >&2; exit 1; }
# kill-server is asynchronous; wait for the server to actually exit before the
# checks below that require no server to be running.
$TMUX kill-server 2>/dev/null
i=0
while $TMUX has-session 2>/dev/null; do
sleep 0.05
i=$((i + 1))
[ $i -gt 100 ] && break
done
# A command without start-server fails cleanly when no server is running and
# does not fork one.
$TMUX new-window -dn ZZ 2>/dev/null && { echo "new-window unexpectedly succeeded" >&2; exit 1; }
if $TMUX has-session 2>/dev/null; then
echo "non-start-server command left a stray server" >&2
exit 1
fi
# A parse error with no server running starts no server.
$TMUX 'if-shell true {' 2>/dev/null && { echo "parse error did not fail" >&2; exit 1; }
if $TMUX has-session 2>/dev/null; then
echo "parse error left a stray server" >&2
exit 1
fi
$TMUX kill-server 2>/dev/null
exit 0
-87
View File
@@ -1,87 +0,0 @@
#!/bin/sh
# command-alias expansion, against a running server and at server start.
#
# An alias replaces a command name with parsed command text; arguments after the
# alias name are appended to the last command of the expansion. This covers a
# single-command alias, a multi-command alias, argument appending, a built-in
# default alias, and aliases that are defined and used as the server starts
# (both from the startup config and from the client command line that starts
# the server).
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
CONF=$(mktemp)
trap "rm -f $CONF" 0 1 15
wait_gone() {
i=0
while $TMUX has-session 2>/dev/null; do
sleep 0.05
i=$((i + 1))
[ $i -gt 100 ] && break
done
}
# --- Against a running server. ---------------------------------------------
$TMUX -f/dev/null start \; new-session -d -swork -nbase 2>/dev/null || exit 1
# Single-command alias.
$TMUX split-window -dt work || exit 1
$TMUX set -s command-alias[200] 'zoomit=resize-pane -Z' || exit 1
$TMUX zoomit -t work:.0 || exit 1
[ "$($TMUX display-message -p -t work '#{window_zoomed_flag}')" = 1 ] || {
echo "single-command alias did not zoom" >&2; exit 1; }
# Multi-command alias: both commands run.
$TMUX set -s command-alias[201] 'twowin=new-window -dn AA ; new-window -dn BB' || exit 1
$TMUX run-shell -C 'twowin' || exit 1
got=$($TMUX list-windows -t work -F '#{window_name}' | tr '\n' ',')
[ "$got" = "base,AA,BB," ] || { echo "multi-command alias: got [$got]" >&2; exit 1; }
# Arguments after the alias name are appended to the expansion.
$TMUX set -s command-alias[202] 'namewin=new-window -d -n' || exit 1
$TMUX run-shell -C 'namewin CC' || exit 1
$TMUX list-windows -t work -F '#{window_name}' | grep -qx CC || {
echo "alias argument append did not create window CC" >&2; exit 1; }
# A built-in default alias (splitp -> split-window).
before=$($TMUX list-panes -t work | wc -l)
$TMUX run-shell -C 'splitp -d -t work' || exit 1
after=$($TMUX list-panes -t work | wc -l)
[ "$after" -gt "$before" ] || { echo "built-in alias splitp did not split" >&2; exit 1; }
$TMUX kill-server 2>/dev/null
wait_gone
# --- At server start: alias defined and used in the startup config. --------
cat <<'EOF' >$CONF
set -s command-alias[100] greet='set -g @g started'
set -s command-alias[101] mkwins='new-window -dn SW1 ; new-window -dn SW2'
new-session -d -swork -nbase
greet
mkwins
EOF
$TMUX -f$CONF start 2>/dev/null || exit 1
[ "$($TMUX show -gv @g)" = started ] || { echo "startup-config alias did not run" >&2; exit 1; }
got=$($TMUX list-windows -t work -F '#{window_name}' | tr '\n' ',')
[ "$got" = "base,SW1,SW2," ] || { echo "startup-config multi alias: got [$got]" >&2; exit 1; }
$TMUX kill-server 2>/dev/null
wait_gone
# --- At server start: alias from config, used on the command line that starts
# the server. ---------------------------------------------------------
cat <<'EOF' >$CONF
set -s command-alias[100] greet='set -g @g argv'
EOF
$TMUX -f$CONF new-session -d -swork \; greet 2>/dev/null || exit 1
[ "$($TMUX show -gv @g)" = argv ] || { echo "argv-start alias did not run" >&2; exit 1; }
$TMUX kill-server 2>/dev/null
exit 0
-102
View File
@@ -1,102 +0,0 @@
#!/bin/sh
# Deferred command callbacks.
#
# Several commands store a parsed tree and invoke it later through
# cmd_invoke_get: if-shell (then/else, sync and background), run-shell -C, and -
# once a key is pressed by an attached client - a key binding, confirm-before and
# command-prompt. The headless cases run directly on the inner server; the
# client cases use the nested-tmux pattern from prompt-mechanics.sh: an outer
# server hosts the inner client in a pane and keys are injected with send-keys.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT="$TEST_TMUX -Ltest -f/dev/null"
IN="$TEST_TMUX -Ltest2 -f/dev/null"
$OUT kill-server 2>/dev/null
$IN kill-server 2>/dev/null
trap "$OUT kill-server 2>/dev/null; $IN kill-server 2>/dev/null" 0 1 15
fail() { echo "[FAIL] $1" >&2; exit 1; }
settle() { sleep 0.5; }
CONF=$(mktemp)
trap "rm -f $CONF; $OUT kill-server 2>/dev/null; $IN kill-server 2>/dev/null" 0 1 15
# --- Headless deferred callbacks: no client needed. ------------------------
$IN new -d -x80 -y23 -nbase "sh -c 'exec sleep 1000'" || exit 1
$IN set -g status on || exit 1
$IN set -g status-keys emacs || exit 1
$IN set -g window-size manual || exit 1
$IN if-shell true 'set -g @t then' 'set -g @t else' || exit 1
[ "$($IN show -gv @t)" = then ] || fail "if-shell true ran wrong branch"
$IN if-shell false 'set -g @f then' 'set -g @f else' || exit 1
[ "$($IN show -gv @f)" = else ] || fail "if-shell false ran wrong branch"
# Background (-b) if-shell evaluates its condition in a shell asynchronously.
$IN if-shell -b 'true' 'set -g @b yes' 'set -g @b no' || exit 1
settle
[ "$($IN show -gv @b)" = yes ] || fail "background if-shell ran wrong branch"
# A multi-command then-body runs both commands.
$IN if-shell true 'new-window -dn IF1 ; new-window -dn IF2' '' || exit 1
$IN list-windows -F '#{window_name}' | grep -qx IF1 || fail "if-shell body cmd1 missing"
$IN list-windows -F '#{window_name}' | grep -qx IF2 || fail "if-shell body cmd2 missing"
# run-shell -C invokes its argument as tmux commands.
$IN run-shell -C 'set -g @rc ran' || exit 1
[ "$($IN show -gv @rc)" = ran ] || fail "run-shell -C did not run command"
# --- Client deferred callbacks: key binding, confirm-before, command-prompt. -
$IN bind -n M-c confirm-before -p '(ok) ' 'set -g @cb confirmed' || exit 1
$IN bind -n M-d command-prompt -I pre -p '(cmd) ' 'set -g @cp %%' || exit 1
# A brace body must come through the lexer, so bind it from a config.
cat <<'EOF' >$CONF
bind -n M-k { new-window -dn K1 ; new-window -dn K2 }
EOF
$IN source-file $CONF || exit 1
$OUT new -d -x80 -y24 || exit 1
$OUT set -g status off || exit 1
$OUT set -g window-size manual || exit 1
$OUT send-keys -l "$IN attach" || exit 1
$OUT send-keys Enter || exit 1
sleep 1
# Key binding with a stored multi-command body fires on keypress.
$OUT send-keys M-k || exit 1
settle
$IN list-windows -F '#{window_name}' | grep -qx K1 || fail "key binding body cmd1 missing"
$IN list-windows -F '#{window_name}' | grep -qx K2 || fail "key binding body cmd2 missing"
# confirm-before runs its command only when confirmed.
$IN set -g @cb none || exit 1
$OUT send-keys M-c || exit 1
settle
$OUT capture-pane -p | tail -1 | grep -qF '(ok)' || fail "confirm-before prompt not shown"
$OUT send-keys n || exit 1
settle
[ "$($IN show -gv @cb)" = none ] || fail "confirm-before ran command after 'n'"
$OUT send-keys M-c || exit 1
settle
$OUT send-keys y || exit 1
settle
[ "$($IN show -gv @cb)" = confirmed ] || fail "confirm-before did not run command after 'y'"
# command-prompt feeds the typed line (with -I prefill) into its template.
$IN set -g @cp none || exit 1
$OUT send-keys M-d || exit 1
settle
$OUT capture-pane -p | tail -1 | grep -qF '(cmd)' || fail "command-prompt not shown"
$OUT send-keys -l X || exit 1
$OUT send-keys Enter || exit 1
settle
[ "$($IN show -gv @cp)" = preX ] || fail "command-prompt recovered '$($IN show -gv @cp)'"
$OUT kill-server 2>/dev/null
$IN kill-server 2>/dev/null
exit 0
-75
View File
@@ -1,75 +0,0 @@
#!/bin/sh
# Invoke-time expansion and conditionals.
#
# The parser builds syntax only; environment/tilde expansion, assignments and
# %if/%elif/%else are all evaluated when the tree is invoked. This drives every
# one of those through a single config and checks the resulting option values:
# - FOO=bar assignments set the environment, read back with ${FOO} and $FOO
# - %hidden assignments behave the same
# - ~ expands to the home directory
# - %if / %elif / %else selects the correct branch
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
CONF=$(mktemp)
trap "rm -f $CONF" 0 1 15
cat <<'EOF' >$CONF
FOO=hello
set -g @assign "${FOO}"
set -g @dollar "$FOO"
%hidden BAR=secret
set -g @hidden "${BAR}"
set -g @undef "x${NOSUCHVAR_ZZZ}y"
set -g @tilde ~
set -g @baduser ~nosuchuser_zzz9
%if 1
set -g @iftrue then
%else
set -g @iftrue else
%endif
%if 0
set -g @iffalse then
%else
set -g @iffalse else
%endif
%if 0
set -g @elif a
%elif 1
set -g @elif b
%else
set -g @elif c
%endif
EOF
$TMUX -f/dev/null start \; new-session -d 2>/dev/null || exit 1
$TMUX source-file $CONF || exit 1
check() {
got=$($TMUX show -gv "$1")
[ "$got" = "$2" ] || { echo "$1: got [$got] expected [$2]" >&2; exit 1; }
}
check @assign hello
check @dollar hello
check @hidden secret
check @undef xy # undefined environment variable expands to nothing
check @baduser "" # unknown user in ~user expands to nothing
check @iftrue then
check @iffalse else
check @elif b
# ~ expands to an absolute home directory (value is machine dependent).
tilde=$($TMUX show -gv @tilde)
case "$tilde" in
/*) ;;
*) echo "@tilde did not expand to an absolute path: [$tilde]" >&2; exit 1;;
esac
$TMUX kill-server 2>/dev/null
exit 0
-72
View File
@@ -1,72 +0,0 @@
#!/bin/sh
# Command failure scope.
#
# Failure scope is the active sequence (CMD_PARSE_SEQUENCE): commands joined by
# ';' share one scope, so a failure skips the rest of that sequence; commands on
# separate lines are independent sequences and are not skipped. After a failed
# inner brace the enclosing sequence still resumes.
#
# Each case below creates windows around a deliberately invalid command and the
# resulting window list shows exactly which commands ran.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)
CONF=$(mktemp)
trap "rm -f $TMP $CONF" 0 1 15
# kill-server is asynchronous, so wait for the old server to actually exit
# before starting a fresh one (otherwise the next start races a dying server).
fresh_server() {
$TMUX kill-server 2>/dev/null
i=0
while $TMUX list-sessions >/dev/null 2>&1; do
sleep 0.05
i=$((i + 1))
[ $i -gt 100 ] && break
done
$TMUX -f/dev/null start \; new-session -d -swork -nbase || exit 1
}
# $1 label, $2 config body, $3 expected comma-separated window list.
run() {
fresh_server
printf '%s\n' "$2" >$CONF
$TMUX source-file $CONF >/dev/null 2>&1
got=$($TMUX list-windows -t work -F '#{window_name}' | tr '\n' ',')
if [ "$got" != "$3" ]; then
echo "$1: got [$got] expected [$3]" >&2
exit 1
fi
}
# ';' sequence in a brace body: A runs, the bad command skips B (same sequence),
# the outer sequence resumes so C runs.
run "semicolon body" 'if-shell true { new-window -dn A ; nonexistent_cmd ; new-window -dn B }
new-window -dn C' 'base,A,C,'
# Newlines are independent sequences: the bad command skips only itself, so both
# B and C still run.
run "newline body" 'if-shell true {
new-window -dn A
nonexistent_cmd
new-window -dn B
}
new-window -dn C' 'base,A,B,C,'
# Top-level ';' sequence: failure skips the rest of the line.
run "top-level semicolon" 'new-window -dn A ; nonexistent_cmd ; new-window -dn B' 'base,A,'
# Top-level newlines: independent, both run.
run "top-level newline" 'new-window -dn A
nonexistent_cmd
new-window -dn B' 'base,A,B,'
$TMUX kill-server 2>/dev/null
exit 0
-46
View File
@@ -1,46 +0,0 @@
#!/bin/sh
# Hooks run stored command trees.
#
# A hook stores a parsed command tree that is invoked through cmd_invoke_get when
# the hook fires. Covers a single-command hook, a multi-entry hook array (every
# entry fires), and that show-hooks prints the stored commands back.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX -f/dev/null start \; new-session -d -sfirst -nbase 2>/dev/null || exit 1
# Single-command hook fires on session creation.
$TMUX set-hook -g session-created 'set -g @hook done' || exit 1
$TMUX new-session -d -ssecond || exit 1
[ "$($TMUX show -gv @hook)" = done ] || {
echo "single-command hook did not fire" >&2
exit 1
}
# A hook array fires every entry.
$TMUX set-hook -g session-created[10] 'new-window -dn H1' || exit 1
$TMUX set-hook -g session-created[11] 'set -g @hook2 two' || exit 1
$TMUX new-session -d -sthird || exit 1
[ "$($TMUX show -gv @hook2)" = two ] || {
echo "hook array entry 11 did not fire" >&2
exit 1
}
echo "$($TMUX list-windows -t third -F '#{window_name}')" | grep -qx H1 || {
echo "hook array entry 10 did not create window" >&2
exit 1
}
# show-hooks prints the stored hook commands.
$TMUX show-hooks -g | grep -q 'session-created.* set -g @hook done' || {
echo "show-hooks did not print stored hook command" >&2
exit 1
}
$TMUX kill-server 2>/dev/null
exit 0
-56
View File
@@ -1,56 +0,0 @@
#!/bin/sh
# Read-only client enforcement.
#
# When the invoking client is read-only, cmd_invoke builds each command and
# rejects it (without running it) unless the command entry is marked read-only.
# A read-only client is created by attaching with -r; the nested-tmux pattern
# from cmd-invoke-deferred.sh is used so a real read-only client presses a key.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT="$TEST_TMUX -Ltest -f/dev/null"
IN="$TEST_TMUX -Ltest2 -f/dev/null"
$OUT kill-server 2>/dev/null
$IN kill-server 2>/dev/null
trap "$OUT kill-server 2>/dev/null; $IN kill-server 2>/dev/null" 0 1 15
fail() { echo "[FAIL] $1" >&2; exit 1; }
settle() { sleep 0.5; }
# Inner session: a key bound to a non-read-only command.
$IN new -d -x80 -y23 -nbase "sh -c 'exec sleep 1000'" || exit 1
$IN set -g status on || exit 1
$IN set -g status-keys emacs || exit 1
$IN set -g window-size manual || exit 1
$IN bind -n M-w new-window -dn ROTRY || exit 1
# Outer session hosts the inner client, attached read-only with -r.
$OUT new -d -x80 -y24 || exit 1
$OUT set -g status off || exit 1
$OUT set -g window-size manual || exit 1
$OUT send-keys -l "$IN attach -r" || exit 1
$OUT send-keys Enter || exit 1
sleep 1
[ "$($IN list-clients -F '#{client_readonly}')" = 1 ] || fail "client is not read-only"
# Pressing the key runs new-window through the read-only client: it must be
# rejected, leave no window, and report the error.
$OUT send-keys M-w || exit 1
settle
$IN list-windows -F '#{window_name}' | grep -qx ROTRY && \
fail "read-only client was allowed to create a window"
$OUT capture-pane -p | grep -qi 'read-only' || fail "no read-only error was shown"
# Positive control: the same command with no client is not read-only and runs,
# proving the command itself is valid and only the read-only client blocked it.
$IN new-window -dn OKWIN || exit 1
$IN list-windows -F '#{window_name}' | grep -qx OKWIN || fail "control new-window did not run"
$OUT kill-server 2>/dev/null
$IN kill-server 2>/dev/null
exit 0
-51
View File
@@ -1,51 +0,0 @@
#!/bin/sh
# Malformed input rejection and crash/leak canary.
#
# Each input below is a syntax error. For every one we require that:
# - parsing reports failure (non-zero exit), and
# - the server is still alive afterwards (a follow-up command succeeds).
# The second check is the important one: a parser that crashes or corrupts state
# on bad input would take the server down here.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX -f/dev/null start \; new-session -d 2>/dev/null || exit 1
n=0
check() {
n=$((n + 1))
# Expect a parse failure.
if $TMUX run-shell -C "$1" >/dev/null 2>&1; then
echo "case $n: expected failure but succeeded: $1" >&2
exit 1
fi
# Expect the server to still be responsive.
if ! $TMUX list-sessions >/dev/null 2>&1; then
echo "case $n: server died after: $1" >&2
exit 1
fi
}
check 'if-shell true {' # unterminated brace body
check 'display-message a }' # stray close brace
check 'if-shell true { display-message a' # unclosed nested brace
check 'if-shell true { ; }' # empty sequence in body
check '%elif 1' # %elif with no %if
check '%endif' # %endif with no %if
check '%zzz' # unknown % directive
check '; display-message a' # leading separator
input='%if 1
display-message a'
check "$input" # unterminated %if
input='%if 1
%endif'
check "$input" # empty %if body
$TMUX kill-server 2>/dev/null
exit 0
-120
View File
@@ -1,120 +0,0 @@
#!/bin/sh
# Parse/print golden suite for the command parser.
#
# Binds a spread of command syntax forms into a dedicated key table, then prints
# them back with list-keys (which calls cmd_parse_print on the stored tree). A
# command-valued option is printed too. Three things are checked:
# - the default (single line) form is byte-for-byte as expected: quoting,
# escaped \; separators, inline braces, and preservation of unexpanded
# ${env}, ~ and #{format} inside stored bodies;
# - the -p (multiline) form is as expected;
# - the default form round-trips: sourcing it again reproduces the same keys.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
SOCKET=$(mktemp -u testXXXXXX)
TMUX="$TEST_TMUX -L$SOCKET -f/dev/null"
TMUX2=
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)
CONF=$(mktemp)
EXP=$(mktemp)
RT1=$(mktemp)
RT2=$(mktemp)
trap "[ -n \"\$TMUX\" ] && \$TMUX kill-server 2>/dev/null; [ -n \"\$TMUX2\" ] && \$TMUX2 kill-server 2>/dev/null; rm -f $TMP $CONF $EXP $RT1 $RT2" 0 1 15
cat <<'EOF' >$CONF
bind -T parsetest a display-message hello
bind -T parsetest b display-message "hello world"
bind -T parsetest c display-message 'literal $HOME #{p} ~'
bind -T parsetest d display-message ""
bind -T parsetest e display-message "#{pane_id}"
bind -T parsetest f display-message a \; display-message b
bind -T parsetest g {
display-message one
display-message two
}
bind -T parsetest h if-shell true { display-message yes } { display-message no }
bind -T parsetest m if-shell true { display-message "${HOME}" "~" "~root" "#{pane_id}" }
bind -T parsetest n if-shell true { display-message "a\nb" "x;y" '#literal' }
EOF
# Expected output. The default form is single line (round-trippable); the -p
# form is multiline, with lines inside braced bodies indented by a single tab.
cat <<'EOF' >$EXP
bind-key -T parsetest a display-message hello
bind-key -T parsetest b display-message 'hello world'
bind-key -T parsetest c display-message 'literal $HOME #{p} ~'
bind-key -T parsetest d display-message ''
bind-key -T parsetest e display-message '#{pane_id}'
bind-key -T parsetest f display-message a \; display-message b
bind-key -T parsetest g display-message one \; display-message two
bind-key -T parsetest h if-shell true { display-message yes } { display-message no }
bind-key -T parsetest m if-shell true { display-message ${HOME} ~ ~root '#{pane_id}' }
bind-key -T parsetest n if-shell true { display-message a\nb 'x;y' '#literal' }
--- options ---
display-message 'hi there'
--- multiline ---
bind-key -T parsetest a display-message hello
bind-key -T parsetest b display-message 'hello world'
bind-key -T parsetest c display-message 'literal $HOME #{p} ~'
bind-key -T parsetest d display-message ''
bind-key -T parsetest e display-message '#{pane_id}'
bind-key -T parsetest f display-message a \; display-message b
bind-key -T parsetest g display-message one
display-message two
bind-key -T parsetest h if-shell true {
display-message yes
} {
display-message no
}
bind-key -T parsetest m if-shell true {
display-message ${HOME} ~ ~root '#{pane_id}'
}
bind-key -T parsetest n if-shell true {
display-message a\nb 'x;y' '#literal'
}
EOF
$TMUX -f/dev/null start \; new-session -d 2>/dev/null || exit 1
$TMUX source-file $CONF || exit 1
$TMUX set -g default-client-command 'display-message "hi there"' || exit 1
set +x
{
$TMUX list-keys -T parsetest
echo "--- options ---"
$TMUX show -gv default-client-command
echo "--- multiline ---"
$TMUX list-keys -p -T parsetest
} >$TMP 2>&1 || exit 1
set -x
cmp -s $TMP $EXP || {
echo "cmd-parse-print: output differs from expected" >&2
diff -u $EXP $TMP >&2
exit 1
}
# Round-trip: the default form re-sourced into a fresh server must reproduce the
# identical key list.
$TMUX list-keys -T parsetest >$RT1 || exit 1
$TMUX kill-server 2>/dev/null
SOCKET2=$(mktemp -u testXXXXXX)
TMUX2="$TEST_TMUX -L$SOCKET2 -f/dev/null"
$TMUX2 -f/dev/null start \; new-session -d 2>/dev/null || exit 1
$TMUX2 source-file $RT1 || exit 1
$TMUX2 list-keys -T parsetest >$RT2 || exit 1
$TMUX2 kill-server 2>/dev/null
cmp -s $RT1 $RT2 || {
echo "cmd-parse-print: default form does not round-trip" >&2
diff -u $RT1 $RT2 >&2
exit 1
}
exit 0
+53
View File
@@ -0,0 +1,53 @@
#!/bin/sh
# Width cache ranges must include their endpoint without overflowing wchar_t.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
SERVER=
WATCHDOG=
cleanup()
{
if [ -n "$WATCHDOG" ]; then
kill "$WATCHDOG" 2>/dev/null
wait "$WATCHDOG" 2>/dev/null
fi
[ -n "$SERVER" ] && kill -9 "$SERVER" 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 15
$TMUX new-session -d 'exec sleep 60' || exit 1
SERVER=$($TMUX display-message -p '#{pid}') || exit 1
# kill-server cannot stop a server stuck rebuilding the width cache.
(
sleep 5
kill -9 "$SERVER" 2>/dev/null
) &
WATCHDOG=$!
$TMUX set -g codepoint-widths 'U+1F600=2' || exit 1
# Cover both signed and unsigned 32-bit wchar_t. Values above WCHAR_MAX
# are ignored by the parser on platforms where they cannot be represented.
for value in U+7FFFFFFF=1 U+7FFFFFFE-U+7FFFFFFF=2 \
U+FFFFFFFF=1 U+FFFFFFFE-U+FFFFFFFF=2; do
$TMUX set -g codepoint-widths "$value" || exit 1
[ "$($TMUX display-message -p alive)" = alive ] || exit 1
done
# Check that an ordinary range still includes both endpoints and stops there.
$TMUX set -g codepoint-widths 'U+03B1-U+03B3=2' || exit 1
$TMUX set -g @text 'αβγδ' || exit 1
[ "$($TMUX display-message -p '#{w:@text}')" = 7 ] || exit 1
$TMUX set -gu codepoint-widths || exit 1
[ "$($TMUX display-message -p '#{w:@text}')" = 4 ] || exit 1
$TMUX kill-server || exit 1
SERVER=
exit 0
+150
View File
@@ -0,0 +1,150 @@
#!/bin/sh
# Exercise confirm-before's waiting and background callbacks, custom keys and
# prompts, default-yes handling and errors. Also cover all three lock command
# entry points with a harmless lock-command which leaves a marker.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
capture()
{
$OUTER capture-pane -p -t outer:0.0 2>/dev/null
}
wait_capture()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -Fq "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "timed out waiting for '$marker'"
}
wait_option()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER show-option -gqv @confirmed 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "@confirmed is '$got', expected '$want'"
}
wait_file()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
[ -f "$marker" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "lock command did not create $marker"
}
$INNER new-session -d -s test -x80 -y24 'exec sleep 100' || exit 1
$INNER set-option -g status on || exit 1
$INNER set-option -g status-position bottom || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t test" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
sleep 1
client=$($INNER list-clients -F '#{client_name}')
[ -n "$client" ] || fail "inner client did not attach"
$INNER bind-key -n M-a confirm-before \
'set-option -g @confirmed accepted' || exit 1
$INNER bind-key -n M-b confirm-before -b -p 'Background?' \
'set-option -g @confirmed background' || exit 1
$INNER bind-key -n M-c confirm-before -c x -p 'Custom?' \
'set-option -g @confirmed custom' || exit 1
$INNER bind-key -n M-y confirm-before -y -p 'Default?' \
'set-option -g @confirmed default' || exit 1
# A non-confirming response closes the waiting prompt without running its
# command; reopening and accepting inserts the command after the waiting item.
$INNER set-option -g @confirmed sentinel || exit 1
$OUTER send-keys M-a || exit 1
wait_capture "Confirm 'set-option'? (y/n)"
$OUTER send-keys n || exit 1
sleep 0.2
wait_option sentinel
$OUTER send-keys M-a || exit 1
wait_capture "Confirm 'set-option'? (y/n)"
$OUTER send-keys y || exit 1
wait_option accepted
# -b has no waiting queue item and appends its command to the client queue.
$OUTER send-keys M-b || exit 1
wait_capture 'Background?'
$OUTER send-keys y || exit 1
wait_option background
# A custom printable key is accepted, and -y makes Enter affirmative.
$OUTER send-keys M-c || exit 1
wait_capture 'Custom?'
$OUTER send-keys x || exit 1
wait_option custom
$OUTER send-keys M-y || exit 1
wait_capture 'Default?'
$OUTER send-keys Enter || exit 1
wait_option default
# Reject invalid multi-character and control confirmation keys.
if $INNER confirm-before -b -t "$client" -c xx 'display-message x' \
>/dev/null 2>&1; then
fail "multi-character confirm key was accepted"
fi
if $INNER confirm-before -b -t "$client" -c "$(printf '\001')" \
'display-message x' >/dev/null 2>&1; then
fail "control confirm key was accepted"
fi
# lock-client, lock-session and lock-server all send lock-command to the real
# attached client. The command returns immediately and unlocks the client.
for kind in client session server; do
marker="$DIR/locked-$kind"
$INNER set-option -t test lock-command "printf locked >$marker" || exit 1
case "$kind" in
client) $INNER lock-client -t "$client" || exit 1 ;;
session) $INNER lock-session -t test || exit 1 ;;
server) $INNER lock-server || exit 1 ;;
esac
wait_file "$marker"
done
$INNER has-session -t test || fail "server died during prompt or lock tests"
exit 0
+1 -1
View File
@@ -17,7 +17,7 @@ PATH=/bin:/usr/bin
TERM=screen TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest" TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null
DIR=$(mktemp -d) DIR=$(mktemp -d)
+1 -2
View File
@@ -19,8 +19,7 @@ PATH=/bin:/usr/bin
TERM=screen TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
SOCKET=$(mktemp -u testXXXXXX) TMUX="$TEST_TMUX -Ltest$$"
TMUX="$TEST_TMUX -L$SOCKET"
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null
DIR=$(mktemp -d) DIR=$(mktemp -d)
+123
View File
@@ -0,0 +1,123 @@
#!/bin/sh
# Check that control client flags are available in every format expansion path
# used to produce layouts. The layout strings themselves are tested separately.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
LANG=C.UTF-8
export PATH TERM LC_ALL LANG
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
DIR=$(mktemp -d) || exit 1
FIFO=$DIR/input
OUT=$DIR/output
CFG=$DIR/flags.conf
PID=
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
exec 3>&-
[ -n "$PID" ] && kill "$PID" 2>/dev/null
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup EXIT
wait_for()
{
pattern=$1
i=0
while [ "$i" -lt 50 ]; do
grep -F -- "$pattern" "$OUT" >/dev/null 2>&1 && return 0
if [ -n "$PID" ] && ! kill -0 "$PID" 2>/dev/null; then
fail "control client exited waiting for: $pattern"
fi
sleep 0.1
i=$((i + 1))
done
fail "missing: $pattern"
}
send()
{
printf '%s\n' "$*" >&3
}
check_commands()
{
name=$1
expected=$2
match='#{m:*new-layouts*,#{client_flags}}'
send "display-message -p 'DISPLAY-$name $match'"
wait_for "DISPLAY-$name $expected"
send "list-panes -F 'PANES-$name $match'"
wait_for "PANES-$name $expected"
send "list-windows -F 'WINDOWS-$name $match'"
wait_for "WINDOWS-$name $expected"
send "list-sessions -F 'SESSIONS-$name $match'"
wait_for "SESSIONS-$name $expected"
send "list-clients -F 'CLIENTS-$name $match'"
wait_for "CLIENTS-$name $expected"
}
check_config()
{
name=$1
expected=$2
send "source-file '$CFG'"
send "display-message -p 'CONFIG-$name #{@config-new-layouts}'"
wait_for "CONFIG-$name $expected"
}
cat >"$CFG" <<'EOF'
%if #{m:*new-layouts*,#{client_flags}}
set-option -g @config-new-layouts 1
%else
set-option -g @config-new-layouts 0
%endif
EOF
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -s layouts -x 80 -y 24 || exit 1
$TMUX split-window -h -t layouts: || exit 1
mkfifo "$FIFO" || exit 1
: >"$OUT"
$TMUX -C attach-session -t layouts <"$FIFO" >"$OUT" 2>&1 &
PID=$!
exec 3>"$FIFO"
send 'display-message -p READY'
wait_for READY
check_commands OFF 0
check_config OFF 0
send 'refresh-client -f new-layouts'
check_commands ON 1
check_config ON 1
send 'refresh-client -f !new-layouts'
check_commands OFF-AGAIN 0
check_config OFF-AGAIN 0
# The list commands also run for unattached command clients. Supplying that
# client to formats must not make the session loop dereference a NULL session.
$TMUX list-sessions -F '#{S:all,active}' >/dev/null ||
fail "session loop failed for unattached command client"
$TMUX has-session -t layouts || fail "server exited"
exit 0
+64
View File
@@ -0,0 +1,64 @@
#!/bin/sh
# Popups require an attached terminal and cannot be displayed by a control
# client.
# A popup command from control mode must be ignored cleanly, leaving the
# client command queue and server usable.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
DIR=$(mktemp -d) || exit 1
FIFO=$DIR/input
OUT=$DIR/output
RAN=$DIR/popup-ran
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
exec 3>&-
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
mkfifo "$FIFO" || exit 1
$TMUX new-session -d -s control -x40 -y10 'exec sleep 100' || exit 1
$TMUX -C attach-session -t control <"$FIFO" >"$OUT" 2>&1 &
control_pid=$!
exec 3>"$FIFO"
i=0
while [ "$i" -lt 50 ]; do
$TMUX list-clients -F '#{client_flags}' 2>/dev/null |
grep -q 'control-mode' && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "control client did not attach"
printf '%s\n' "display-popup -E 'touch $RAN'" >&3
printf '%s\n' "display-message -p CONTROL-POPUP-ALIVE" >&3
i=0
while [ "$i" -lt 50 ]; do
grep -q 'CONTROL-POPUP-ALIVE' "$OUT" 2>/dev/null && break
if ! kill -0 "$control_pid" 2>/dev/null; then
fail "control client exited after display-popup"
fi
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "command after control-client popup did not run"
$TMUX has-session -t control || fail "server exited after control-client popup"
[ ! -e "$RAN" ] || fail "control client started a popup command"
exit 0
+38
View File
@@ -0,0 +1,38 @@
#!/bin/sh
# A waited run-shell command can complete after a control client has closed its
# input and exited. The command queue still references the client until the job
# callback runs, but control_stop has already destroyed c->control_state. Late
# command output must be discarded instead of dereferencing the stopped control
# state and killing the server.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
OUT=$(mktemp)
trap "$TMUX kill-server 2>/dev/null; rm -f $OUT" 0 1 15
$TMUX -f/dev/null new-session -d -s main || exit 1
printf '%s\n' 'run-shell "sleep 0.2; echo TEST"' |
$TMUX -f/dev/null -C >"$OUT" 2>&1
sleep 1
$TMUX has-session -t main 2>/dev/null || {
echo "server exited after late run-shell output"
cat "$OUT"
exit 1
}
grep -q '^%exit' "$OUT" || {
echo "control client did not exit"
cat "$OUT"
exit 1
}
exit 0
+5 -2
View File
@@ -30,8 +30,11 @@ killw
EOF EOF
sleep 1 sleep 1
$TMUX has || exit 1 $TMUX has || exit 1
$TMUX lsp -aF '#{pane_id} #{window_layout}' >$TMP || exit 1 # Use a control client to request legacy layouts, keeping only pane lines
cat <<EOF|cmp -s $TMP - || exit 1 # from the control protocol output.
$TMUX -C lsp -aF '#{pane_id} #{window_layout}' |
grep '^%[0-9]' >$TMP || exit 1
cat <<EOF|cmp $TMP - || exit 1
%0 f5ab,200x200,0,0[200x50,0,0,0,200x149,0,51,3] %0 f5ab,200x200,0,0[200x50,0,0,0,200x149,0,51,3]
%3 f5ab,200x200,0,0[200x50,0,0,0,200x149,0,51,3] %3 f5ab,200x200,0,0[200x50,0,0,0,200x149,0,51,3]
%2 dcbd,200x200,0,0[200x100,0,0,2,200x99,0,101,4] %2 dcbd,200x200,0,0[200x100,0,0,2,200x99,0,101,4]
+1 -1
View File
@@ -10,7 +10,7 @@ PATH=/bin:/usr/bin
TERM=screen TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) [ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest" TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null
FIFO=$(mktemp -u) FIFO=$(mktemp -u)
Executable → Regular
View File
+253
View File
@@ -0,0 +1,253 @@
#!/bin/sh
# Exercise the broad non-mouse copy-mode command surface: navigation, search,
# jumps, state toggles, line/end-of-line copies, append, and pipe variants.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
TMUX="$TEST_TMUX -Ltest$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
x()
{
$TMUX send-keys -t copy:0 -X "$@" || fail "copy command failed: $*"
}
wait_mode()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($TMUX display-message -p -t copy:0 '#{pane_in_mode}' 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "pane_in_mode is '$got', expected '$want'"
}
fresh()
{
$TMUX copy-mode -t copy:0 || exit 1
wait_mode 1
x history-top
x start-of-line
}
select_text()
{
fresh
x begin-selection
x cursor-right
x cursor-right
}
$TMUX new-session -d -s copy -x40 -y8 \
"sh -c 'i=0; while [ \$i -lt 40 ]; do printf \" line-%02d needle (a[b]c) end\\n\" \$i; i=\$((i + 1)); done; printf \"\\nparagraph two needle\\n\\nparagraph three\\n\"; printf \"\\033]133;A\\007prompt-one\\n\\033]133;C\\007output-one\\n\\033]133;A\\007prompt-two\\n\\033]133;C\\007output-two\\n\"; exec sleep 100'" ||
exit 1
$TMUX set-option -g status off || exit 1
$TMUX set-option -g history-limit 200 || exit 1
i=0
while [ "$i" -lt 50 ]; do
history=$($TMUX display-message -p -t copy:0 '#{history_size}')
[ "$history" -ge 30 ] && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "history did not fill"
fresh
# Copy-mode-specific format callbacks expose the word, line, hyperlink and
# active search match at the cursor.
formats=$($TMUX display-message -p -t copy:0 \
'#{copy_cursor_word}|#{copy_cursor_line}|#{copy_cursor_hyperlink}|#{search_match}') ||
exit 1
[ -n "$formats" ] || fail "copy-mode cursor formats were empty"
# Cursor, viewport and paragraph commands. Each is asserted by successful
# dispatch and the final mode-liveness check; detailed word/selection geometry
# is covered by the existing copy-mode tests.
for command in back-to-indentation bottom-line top-line middle-line \
cursor-centre-vertical cursor-centre-horizontal end-of-line history-bottom \
history-top halfpage-down halfpage-up page-down page-up next-paragraph \
previous-paragraph next-matching-bracket previous-matching-bracket; do
x "$command"
done
x goto-line 15
x set-mark
x cursor-down
x jump-to-mark
# OSC 133 prompt and output markers support shell-integration navigation.
x history-top
x next-prompt
case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in
*prompt-one*) ;;
*) fail "next-prompt did not find prompt-one" ;;
esac
x next-prompt
x previous-prompt
case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in
*prompt-one*) ;;
*) fail "previous-prompt did not return to prompt-one" ;;
esac
x next-prompt -o
case "$($TMUX display-message -p -t copy:0 '#{copy_cursor_line}')" in
*output-one*) ;;
*) fail "next-prompt -o did not find output-one" ;;
esac
# Character jumps remember their direction and character for repeat/reverse.
x jump-forward e
x jump-again
x jump-reverse
x jump-backward e
x jump-to-forward e
x jump-to-backward e
# Direct, text and incremental searches cover both directions and the saved
# search used by again/reverse.
x search-forward needle
x search-again
x search-reverse
x search-backward needle
x search-forward-text needle
x search-backward-text needle
x search-forward-incremental '=needle'
x search-forward-incremental '+needle'
x search-backward-incremental '=needle'
x search-backward-incremental '-needle'
# Stateful display and behaviour toggles.
for command in rectangle-on rectangle-off rectangle-toggle \
line-numbers-on line-numbers-off line-numbers-toggle \
refresh-on refresh-now refresh-off refresh-toggle \
scroll-exit-on scroll-exit-off scroll-exit-toggle toggle-position; do
x "$command"
done
wait_mode 1
x cancel
wait_mode 0
# Line and end-of-line copy commands work without an explicit selection.
fresh
x select-line
x copy-selection-and-cancel
case "$($TMUX show-buffer)" in
*line-00*) ;;
*) fail "select-line did not select the current line" ;;
esac
fresh
x copy-line
[ -n "$($TMUX show-buffer)" ] || fail "copy-line produced an empty buffer"
x cancel
fresh
x copy-end-of-line
[ -n "$($TMUX show-buffer)" ] || fail "copy-end-of-line produced an empty buffer"
x cancel
# Append variants retain or close the mode as advertised.
$TMUX set-buffer -b append-buffer prefix || exit 1
select_text
x append-selection
appended=$($TMUX show-buffer -b append-buffer)
case "$appended" in
prefix*) ;;
*) fail "append-selection produced '$appended'" ;;
esac
x cancel
$TMUX set-buffer -b append-cancel-buffer prefix || exit 1
select_text
x append-selection-and-cancel
wait_mode 0
appended=$($TMUX show-buffer -b append-cancel-buffer)
case "$appended" in
prefix*) ;;
*) fail "append-selection-and-cancel produced '$appended'" ;;
esac
# The explicit cancel variants close copy mode.
fresh
x copy-line-and-cancel
wait_mode 0
fresh
x copy-end-of-line-and-cancel
wait_mode 0
select_text
x copy-selection-and-cancel
wait_mode 0
# Pipe wrappers have distinct command handlers. /dev/null keeps the test
# deterministic while exercising asynchronous pipe job creation and cleanup.
fresh
x copy-pipe-line 'cat >/dev/null'
x cancel
fresh
x copy-pipe-line-and-cancel 'cat >/dev/null'
wait_mode 0
fresh
x copy-pipe-end-of-line 'cat >/dev/null'
x cancel
fresh
x copy-pipe-end-of-line-and-cancel 'cat >/dev/null'
wait_mode 0
select_text
x copy-pipe-no-clear 'cat >/dev/null'
x pipe-no-clear 'cat >/dev/null'
x copy-pipe 'cat >/dev/null'
x pipe 'cat >/dev/null'
x cancel
select_text
x copy-pipe-and-cancel 'cat >/dev/null'
wait_mode 0
select_text
x pipe-and-cancel 'cat >/dev/null'
wait_mode 0
# Commands which cancel on downward movement have separate handlers.
fresh
x history-bottom
x bottom-line
x cursor-down-and-cancel
wait_mode 0
fresh
x history-bottom
x halfpage-down-and-cancel
wait_mode 0
fresh
x history-bottom
x page-down-and-cancel
wait_mode 0
fresh
x history-bottom
x bottom-line
x scroll-down-and-cancel
wait_mode 0
$TMUX has-session -t copy || fail "server died during copy-mode commands"
exit 0
+81
View File
@@ -0,0 +1,81 @@
#!/bin/sh
# Changing an existing character selection to line mode must reset both ends
# to complete lines, including when the cursor subsequently crosses the fixed
# end. An invalid regular-expression search must also discard marks left by a
# previous successful search.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
}
trap cleanup 0 1 15
$TMUX new-session -d -x30 -y6 \
"printf 'alpha\nbeta\ngamma\ndelta\n'; exec sleep 100" || exit 1
$TMUX set-option -g window-size manual || exit 1
sleep 1
# A valid regular-expression search creates marks. Replacing it with an
# invalid expression, growing the mode, and scrolling must not leave an old
# mark array indexed with the new geometry (the original bug was an ASan heap
# overflow in this sequence).
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -X search-forward beta || exit 1
[ "$($TMUX display-message -p '#{search_present}')" = 1 ] ||
fail "successful search did not create marks"
$TMUX send-keys -X search-forward '[' || exit 1
$TMUX resize-window -x40 -y8 || exit 1
$TMUX send-keys -X scroll-down || exit 1
[ "$($TMUX display-message -p '#{search_present}')" = 0 ] ||
fail "invalid regular expression left stale marks after resize"
$TMUX send-keys -X cancel || exit 1
# Select part of alpha through part of beta, then change the existing
# selection to line mode. Both lines, and no partial columns, must be copied.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X begin-selection || exit 1
$TMUX send-keys -X cursor-down || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X selection-mode line || exit 1
[ "$($TMUX display-message -p '#{selection_mode}')" = line ] ||
fail "selection did not change to line mode"
$TMUX send-keys -X copy-selection || exit 1
expected=$(printf 'alpha\nbeta')
[ "$($TMUX show-buffer)" = "$expected" ] ||
fail "line-mode selection did not expand to complete lines"
# Start with the cursor above the fixed end so selection-mode itself takes the
# reverse-direction path, then extend it through another line.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX send-keys -N2 -X cursor-down || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X begin-selection || exit 1
$TMUX send-keys -X cursor-up || exit 1
$TMUX send-keys -N2 -X cursor-right || exit 1
$TMUX send-keys -X selection-mode line || exit 1
$TMUX send-keys -X cursor-up || exit 1
$TMUX send-keys -X copy-selection || exit 1
expected=$(printf 'alpha\nbeta\ngamma')
[ "$($TMUX show-buffer)" = "$expected" ] ||
fail "reversed line-mode selection did not keep complete lines"
exit 0
+63
View File
@@ -0,0 +1,63 @@
#!/bin/sh
# Trimming a blank source must leave a visible line for cursor reflow.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
}
trap cleanup 0
trap 'exit 1' 1 2 3 15
$TMUX new-session -d -x80 -y24 -s test 'sleep 100' || exit 1
$TMUX set-option -g window-size manual || exit 1
$TMUX new-window -t test:1 'sleep 100' || exit 1
$TMUX resize-window -t test:0 -x20 -y24 || exit 1
# Exercise widening, narrowing, and equal widths with no source history.
for pair in '0 1' '1 0' '1 2'; do
set -- $pair
if [ "$2" = 2 ]; then
$TMUX new-window -t test:2 'sleep 100' || exit 1
$TMUX resize-window -t test:2 -x80 -y24 || exit 1
fi
source=test:$1.0
target=test:$2.0
[ "$($TMUX display-message -p -t "$source" '#{history_size}')" = 0 ] ||
fail "source has unexpected history"
$TMUX copy-mode -s "$source" -t "$target" || exit 1
$TMUX has-session -t test || fail "server died copying blank source"
[ "$($TMUX display-message -p -t "$target" \
'#{pane_in_mode} #{copy_cursor_x} #{copy_cursor_y}')" = '1 0 0' ] ||
fail "blank source did not enter copy mode at the first cell"
$TMUX send-keys -t "$target" -X cancel || exit 1
done
# Real content still trims trailing empty lines, clamping the cursor to beta.
$TMUX respawn-pane -k -t test:0.0 \
"printf 'alpha\r\nbeta\r\n\r\n\r\n'; exec sleep 100" || exit 1
i=0
while [ "$($TMUX display-message -p -t test:0.0 '#{cursor_y}')" != 4 ]; do
i=$((i + 1))
[ "$i" -lt 10 ] || fail "source output did not arrive"
sleep 1
done
$TMUX copy-mode -s test:0.0 -t test:1.0 || exit 1
[ "$($TMUX display-message -p -t test:1.0 \
'#{pane_in_mode} #{copy_cursor_y} #{copy_cursor_line}')" = '1 1 beta' ] ||
fail "nonblank source content or trailing-line trimming changed"
exit 0
View File
+5 -3
View File
@@ -153,11 +153,13 @@ wait_format "$p0" '#{pane_in_mode}' '0'
wait_option @picked "$p1" wait_option @picked "$p1"
# Commands after display-panes run immediately while the mode remains. # Commands after display-panes run immediately while the mode remains.
# Record the mode in the same command queue so client startup cannot race
# the 500ms timeout.
$TMUX set -g @after none || fail "set @after failed" $TMUX set -g @after none || fail "set @after failed"
$TMUX display-panes -Nd 500 -t "$p0" \; set -g @after fast || $TMUX display-panes -Nd 500 -t "$p0" \; \
set -gF -t "$p0" @after '#{pane_mode}' ||
fail "display-panes immediate command failed" fail "display-panes immediate command failed"
wait_option @after fast wait_option @after 'panes-mode'
wait_format "$p0" '#{pane_mode}' 'panes-mode'
wait_format "$p0" '#{pane_in_mode}' '0' wait_format "$p0" '#{pane_in_mode}' '0'
# Existing zoom is restored on exit. # Existing zoom is restored on exit.
+118
View File
@@ -0,0 +1,118 @@
#!/bin/sh
# Exercise find-window's content, name and title filters in every combination,
# glob/regex and case-insensitive suffixes, plus zoom restoration.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -LtestI$$ -f/dev/null"
OUTER="$TEST_TMUX -LtestO$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
capture()
{
$OUTER capture-pane -p -t outer:0.0 2>/dev/null
}
wait_mode()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
got=$($INNER display-message -p -t find:base.0 '#{pane_mode}' 2>/dev/null)
[ "$got" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "pane mode is '$got', expected '$want'"
}
run_find()
{
label=$1
expected=$2
shift 2
$INNER find-window -t find:base.0 "$@" || fail "$label failed"
wait_mode tree-mode
i=0
while [ "$i" -lt 50 ]; do
captured=$(capture)
printf '%s\n' "$captured" | grep -Fq "$expected" && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "$label did not show '$expected'"
$INNER send-keys -t find:base.0 q || exit 1
wait_mode ''
}
$INNER new-session -d -s find -n base -x80 -y24 \
"sh -c 'printf \"BaseBody\\n\"; exec sleep 100'" || exit 1
$INNER split-window -d -h -t find:base \
"sh -c 'printf \"OtherBody\\n\"; exec sleep 100'" || exit 1
$INNER new-window -d -t find: -n NameNeedle 'exec sleep 100' || exit 1
$INNER new-window -d -t find: -n body \
"sh -c 'printf \"BodyNeedle\\n\"; exec sleep 100'" || exit 1
$INNER new-window -d -t find: -n titled 'exec sleep 100' || exit 1
$INNER select-pane -t find:titled.0 -T TitleNeedle || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x80 -y24 "$INNER attach -t find:base" || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
sleep 1
# Wait until the body text is in the pane history before searching it.
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t find:body.0 | grep -Fq BodyNeedle && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "pane content was not ready"
# No flags means C+N+T. Then cover all three pairs and all three singletons.
run_find default NameNeedle NameNeedle
run_find content-name NameNeedle -C -N NameNeedle
run_find content-title titled -C -T TitleNeedle
run_find name-title titled -N -T TitleNeedle
run_find content-only body -C BodyNeedle
run_find name-only NameNeedle -N NameNeedle
run_find title-only titled -T TitleNeedle
# Glob case folding and all regular-expression suffix variants.
run_find insensitive NameNeedle -N -i nameneedle
run_find regex NameNeedle -N -r '^NameNeedle$'
run_find regex-insensitive NameNeedle -N -r -i '^nameneedle$'
# -Z temporarily zooms the target window and restores its prior state on exit.
$INNER find-window -Z -t find:base.0 -N NameNeedle || exit 1
wait_mode tree-mode
[ "$($INNER display-message -p -t find:base '#{window_zoomed_flag}')" = 1 ] ||
fail "find-window -Z did not zoom"
$INNER send-keys -t find:base.0 q || exit 1
wait_mode ''
[ "$($INNER display-message -p -t find:base '#{window_zoomed_flag}')" = 0 ] ||
fail "find-window -Z did not restore zoom"
exit 0
+135
View File
@@ -0,0 +1,135 @@
#!/bin/sh
# Damage-only redraws must evaluate both active and inactive border styles
# for each client, even though the cached border cells belong to the pane.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lborder-style-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lborder-style-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_for_marker()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client in $target did not receive $marker"
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $phase (1 .. 2) {
while (!-e "$ENV{TRIGGER}-$phase") {
select undef, undef, undef, 0.01;
}
# Change an unused palette entry and acknowledge it in the pane body.
print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase";
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 20 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g pane-border-lines simple || exit 1
$INNER set -g pane-border-status top || exit 1
$INNER set -g pane-border-format 'CLIENT=#{client_name}' || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \
'sleep 100') || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1
RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1
for target in "$LEFT" "$RIGHT"; do
$OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" ||
exit 1
done
wait_for_clients
NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1
NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1
STYLE="fg=#{?#{==:#{client_name},$NAME1},red,blue}"
$INNER set -g pane-border-style "$STYLE" || exit 1
$INNER set -g pane-active-border-style "$STYLE" || exit 1
# Capture just the floating pane's rows, excluding the acknowledgement in
# the tiled pane. Exercise each cache by changing the floating pane's focus.
phase=1
while [ "$phase" -le 2 ]; do
if [ "$phase" -eq 2 ]; then
$INNER select-pane -t "$FLOAT" || exit 1
fi
$INNER refresh-client -t "$NAME1" || exit 1
$INNER refresh-client -t "$NAME2" || exit 1
sleep 0.5
for target in "$LEFT" "$RIGHT"; do
$OUTER capture-pane -pe -S 5 -E 10 -t "$target" \
>"$DIR/before-$target" || exit 1
done
RED=$(printf '\033[31m')
BLUE=$(printf '\033[34m')
grep -Fq "$RED" "$DIR/before-$LEFT" || fail "missing red border"
grep -Fq "$BLUE" "$DIR/before-$RIGHT" || fail "missing blue border"
: >"$DIR/trigger-$phase"
wait_for_marker "$LEFT" "DAMAGE$phase"
wait_for_marker "$RIGHT" "DAMAGE$phase"
sleep 0.2
for target in "$LEFT" "$RIGHT"; do
$OUTER capture-pane -pe -S 5 -E 10 -t "$target" \
>"$CAPTURE" || exit 1
diff -u "$DIR/before-$target" "$CAPTURE" ||
fail "phase $phase changed $target's border style"
done
phase=$((phase + 1))
done
exit 0
+108
View File
@@ -0,0 +1,108 @@
#!/bin/sh
# Regression test for a floating-pane drag bug: cmd_resize_pane_redraw_floating()
# (cmd-resize-pane.c) reported damage for just a dragged floating pane's
# content rectangle, not the one-cell border frame drawn around it (see the
# "floating" case in screen-redraw.c, which draws that frame at
# xoff-1/yoff-1 through xoff+sx/yoff+sy - one cell outside the pane's own
# content area). Damage scoped to only the content area left the frame's
# previous position undrawn as the pane moved, so dragging it left a trail
# of un-erased border frames behind - visible as several "corners" stacked
# up rather than just the pane's current one.
#
# This bug has nothing to do with images - it reproduces with a plain
# floating pane and no image support required.
PATH=/bin:/usr/bin
TERM=screen
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"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
#
# Write a plain (unmodified) SGR button-1 press, drag update and release at
# 1-based positions to the outer pane holding the inner client - this
# matches the default MouseDown1Border/MouseDrag1Border bindings used to
# move or resize a floating pane by its border.
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on
$TMUX set -g default-command 'sh -c "sleep 100"'
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Sanity check: exactly one floating pane, so exactly one top-left corner,
# before dragging anything.
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "sanity: expected 1 corner before drag, found $n"
# Drag the floating pane by its top border (row FTOP-1, some column within
# its width) down several rows in a few separate steps, then release. A
# single drag() call already does press/motion/release, so call it several
# times in a row to simulate a multi-step real drag.
GRABCOL=$((FLEFT + FWIDTH / 2))
STARTROW=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((STARTROW + i + 1))
drag $((GRABCOL + 1)) $((STARTROW + i)) $((GRABCOL + 1)) $newrow
i=$((i + 1))
done
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
# Exactly one top-left corner should remain - the pane's current position.
# This is expected to fail before the fix: multiple corners (a trail of
# un-erased frames) would remain from the intermediate drag positions.
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "expected exactly 1 corner after drag, found $n (ghost frames left behind)"
exit 0
+88
View File
@@ -0,0 +1,88 @@
#!/bin/sh
# Each drag motion must draw the floating pane content once.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status-interval 0 || exit 1
$INNER set-option -g automatic-rename off || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
"printf 'DRAGMARK'; exec sleep 100") || exit 1
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# Begin capture after focus changes from the initial mouse press have settled.
mouse 0 "$GRABCOL" "$BORDERROW" M
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1
$INNER refresh-client || exit 1
sleep 0.5
grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content"
grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row"
offset=$(wc -c <"$DIR/output")
i=0
steps=6
while [ "$i" -lt "$steps" ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns"
tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output"
n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output")
[ "$n" -eq "$steps" ] ||
fail "$steps drag motions wrote the floating pane content $n times"
exit 0
+159
View File
@@ -0,0 +1,159 @@
#!/bin/sh
# Regression test: dragging a floating pane across another pane's ordinary
# content must not redraw that other pane's scrollbar, unless the drag
# actually crosses the scrollbar's own strip.
#
# cmd_resize_pane_redraw_floating() (cmd-resize-pane.c) used to flag
# PANE_REDRAWSCROLLBAR on any pane whose whole *body* intersected the
# floating pane's old or new rectangle, rather than just its narrow
# scrollbar strip - so dragging a floating pane back and forth over an
# ordinary tiled pane's content (never touching its scrollbar) still
# needlessly redrew that pane's scrollbar on every motion step. See
# tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by giving the non-dragged pane a distinctive scrollbar
# colour and counting how many times its SGR code appears in the client's
# raw output while the floating pane is dragged vertically over that pane's
# body, well clear of its scrollbar column: with the fix, it should never
# reappear after the initial draw.
PATH=/bin:/usr/bin
TERM=screen
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"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.5
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on || fail "set mouse failed"
$TMUX set -g default-command 'sh -c "sleep 100"' || fail "set default-command failed"
$TMUX set -g pane-scrollbars on || fail "set pane-scrollbars failed"
$TMUX split-window -h -t inner 'sh -c "sleep 100"' || fail "split-window failed"
PANES=$($TMUX list-panes -t inner -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
[ -n "$LEFT" ] || fail "could not identify left pane"
# A distinctive scrollbar colour for the non-dragged (left) pane only.
$TMUX set -p -t "$LEFT" pane-scrollbars-style 'fg=colour201,bg=colour17' ||
fail "set pane-scrollbars-style failed"
ALEFT=$($TMUX display-message -p -t "$LEFT" '#{pane_left}')
ATOP=$($TMUX display-message -p -t "$LEFT" '#{pane_top}')
AWIDTH=$($TMUX display-message -p -t "$LEFT" '#{pane_width}')
AHEIGHT=$($TMUX display-message -p -t "$LEFT" '#{pane_height}')
[ "$AWIDTH" -gt 15 ] || fail "left pane too narrow for this test ($AWIDTH)"
# A small floating pane placed well inside the left pane's content area,
# clear of its (right-hand) scrollbar column by several columns.
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 8 -y 5 \
-X $((ALEFT + 2)) -Y $((ATOP + 2))) || fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
[ $((FLEFT + FWIDTH + 3)) -lt $((ALEFT + AWIDTH)) ] ||
fail "sanity: floating pane too close to the scrollbar column"
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which draws the scrollbars) has already happened, missing it.
$TMUX2 new-session -d -x 60 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
# Sanity check: the distinctive scrollbar colour reaches the client at all.
grep -qa '48;5;201' $TMP || fail "sanity: scrollbar colour never reached the client"
: >$TMP
# Drag the floating pane straight up and down by its top border, staying at
# a fixed column the whole time - this never crosses the left pane's
# scrollbar strip, only its ordinary content.
GRABCOL=$((FLEFT + FWIDTH / 2))
row=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((row + 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
i=0
while [ $i -lt 6 ]; do
newrow=$((row - 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
# The scrollbar colour should never reappear - its geometry never changed,
# and the drag never crossed its column. This is expected to fail before
# the fix - see the header comment.
n=$(grep -ac '48;5;201' $TMP)
[ "$n" -eq 0 ] ||
fail "left pane's scrollbar was redrawn $n times while dragging over its body only"
# Cross the scrollbar and return, comparing each damaged redraw with a full
# refresh to check that covering and uncovering its cells leaves no artifacts.
offset=$((ALEFT + AWIDTH - FLEFT - FWIDTH / 2))
for delta in "$offset" "$((-offset))"; do
oldleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
newcol=$((GRABCOL + delta))
drag "$GRABCOL" "$row" "$newcol" "$row"
GRABCOL=$newcol
newleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
[ "$newleft" -eq "$((oldleft + delta))" ] ||
fail "floating pane did not move across the scrollbar as expected"
before=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))")
$TMUX refresh-client || fail "refresh-client failed"
sleep 0.5
after=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))")
[ "$before" = "$after" ] ||
fail "scrollbar crossing differs from a full redraw"
done
exit 0
+93
View File
@@ -0,0 +1,93 @@
#!/bin/sh
# Synchronized drag output must not cause a full redraw of untouched rows.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status-interval 0 || exit 1
$INNER set-option -g automatic-rename off || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
"printf 'DRAGMARK'; exec sleep 100") || exit 1
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$INNER set-option -as terminal-features ',screen-256color:sync' || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# Begin capture after focus changes from the initial mouse press have settled.
mouse 0 "$GRABCOL" "$BORDERROW" M
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1
$INNER refresh-client || exit 1
sleep 0.5
grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content"
grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row"
offset=$(wc -c <"$DIR/output")
i=0
steps=6
while [ "$i" -lt "$steps" ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns"
tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output"
n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output")
[ "$n" -ge "$steps" ] || fail "drag output did not reach the client"
perl -0777 -ne 'exit(/\e\[\?2026h/ ? 0 : 1)' "$DIR/drag-output" ||
fail "drag did not use synchronized output"
if grep -aq OUTSIDE "$DIR/drag-output"; then
fail "synchronized drag redrew an untouched row"
fi
exit 0
+206
View File
@@ -0,0 +1,206 @@
#!/bin/sh
# Mirror of floating-pane-drag-wide-character.sh for the *right* edge of a
# damage rectangle. redraw_damage_grow_span_clip() (screen-redraw.c) widens
# a damage rectangle's right edge by one cell whenever it isn't already at
# the span's own edge, to pull in a wide character's base half when the
# edge lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell (a character fully
# outside the range), growing right pulls in just that base cell -
# tty_draw_line() sees it can't fit that character's full width in the
# remaining range (tty_draw_line_get_empty()'s gc->data.width > nx check)
# and clears it, exactly as the left-edge bug cleared a neighbouring
# character's padding half.
#
# As with the left-edge test, the exact column parity needs to be
# deterministic to actually catch it. This constructs it by creating the
# floating pane, checking its real position, and recreating it one column
# over if necessary until the vacated rectangle's right edge lands on a
# base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidecharr-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
PANEWIDTH=12
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated right edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's right edge
# (xoff + PANEWIDTH + 1, the border-grown exclusive end - see
# window_pane_damage_floating(), window.c) lands on an even (base-cell)
# column.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x "$PANEWIDTH" -y 3 \
-X "$startx" -Y 5 \
'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldright=$((XOFF + PANEWIDTH + 1))
if [ $((oldright % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF + PANEWIDTH + 1) % 2)) -eq 0 ] ||
fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF + PANEWIDTH - 2))
COL2=$((XOFF + PANEWIDTH + 2))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle - far enough right
# that its new position starts past the checked columns above (which sit
# just past the pane's *original* right edge).
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0
+200
View File
@@ -0,0 +1,200 @@
#!/bin/sh
# Damage at a floating pane's vacated edge must always redraw a complete
# grid character. redraw_damage_grow_span_clip() (screen-redraw.c) widens a
# damage rectangle's left edge by one cell whenever it isn't already at the
# span's own edge, to pull in a wide character's base half when the edge
# lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell, growing left
# walks into the *previous*, unrelated character's padding cell and blanks
# it (tty_draw_line() treats any leading padding cell as proof its own
# range starts mid-character).
#
# This is a general damage-composition bug, not specific to any one kind of
# pane, but the exact column parity needs to be deterministic to actually
# catch it (a lucky parity draws fine). This constructs it by creating the
# floating pane, checking its real position (the border-framing offset
# added to -X is not something to hand-compute), and recreating it one
# column over if necessary until the vacated rectangle's left edge lands on
# a base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidechar-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's left edge
# (xoff - 1) lands on an even (base-cell) column - the odd case is the one
# every earlier manual test happened to land on by chance.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 12 -y 3 -X "$startx" \
-Y 5 'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldleft=$((XOFF - 1))
if [ $((oldleft % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF - 1) % 2)) -eq 0 ] || fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF - 4))
COL2=$((XOFF + 4))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle.
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0
+144
View File
@@ -268,5 +268,149 @@ $TMUX kill-pane -t "$floating" || exit 1
$TMUX kill-pane -t "$right" || exit 1 $TMUX kill-pane -t "$right" || exit 1
$TMUX kill-pane -t "$lower" || exit 1 $TMUX kill-pane -t "$lower" || exit 1
# --- Floating panes clamped when the window shrinks (issue #5581, PR #5582) ---
#
# layout_resize clamps floating panes back inside the window when it shrinks:
# move them and, only if they cannot fit, shrink them (never below
# PANE_MINIMUM). A floating cell is the pane's content, so with the default
# single-line border the clamp counts 1 cell of border per side, exactly as
# layout_floating_args_parse does on creation; with no border it counts 0.
# Each case below gets its own window, since resize-window fixes a window at
# a manual size for the rest of its life.
# Case 1: a lone floating pane -- break-pane -W on a window's only pane --
# takes the early-return path in layout_resize, added during PR #5582's
# review round, since there is no tiled tree to walk.
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for lone float failed"
# -x 20 -y 6 -> pane 18x4; -X 60 -Y 18 -> pane_left 61, pane_top 19: with the
# border, the footprint is columns 60-79, rows 18-23, flush with the 80x24
# window's right and bottom edges, so the float fits before it is shrunk.
$TMUX break-pane -W -s "$win" -x 20 -y 6 -X 60 -Y 18 ||
fail "break-pane -W for lone float failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 61
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 19
# Shrink to 40x16. The float still fits at its own size (18 <= 40-2, 4 <=
# 16-2) so only its position moves, flush to the new right/bottom edges:
# xoff = 40 - 18 - 1 = 21; yoff = 16 - 4 - 1 = 11.
$TMUX resize-window -t "$win" -x 40 -y 16 ||
fail "resize-window (lone float) failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 21
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 11
# Case 5: grow the window back. The clamp must not chase the window back
# outward -- it only ever pulls a float in, never restores where it was.
$TMUX resize-window -t "$win" -x 80 -y 24 ||
fail "resize-window grow (lone float) failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 21
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 11
$TMUX kill-window -t "$win" || exit 1
# Case 2: the same float, but with a tiled sibling surviving alongside it, so
# the window's root cell stays tiled and layout_resize takes the normal path
# (the clamp call after layout_fix_offsets) instead of the early return
# above. Same geometry as case 1, so the same numbers should come out.
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for tiled sibling failed"
$TMUX split-window -t "$win" 'sleep 100' ||
fail "split-window for tiled sibling failed"
$TMUX break-pane -W -s "$win" -x 20 -y 6 -X 60 -Y 18 ||
fail "break-pane -W for tiled sibling failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 61
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 19
$TMUX resize-window -t "$win" -x 40 -y 16 ||
fail "resize-window (tiled sibling) failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 21
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 11
$TMUX kill-window -t "$win" || exit 1
# Case 3: new-pane float, the original repro from issue #5581 and the PR
# body -- also the normal path, via the tiled base pane new-window creates.
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for new-pane repro failed"
$TMUX resize-window -t "$win" -x 120 -y 40 ||
fail "resize-window to 120x40 failed"
# -x 30 -y 10 -> pane 28x8; -X 85 -Y 25 -> pane_left 86, pane_top 26.
floating=$($TMUX new-pane -t "$win" -dPF '#{pane_id}' \
-x 30 -y 10 -X 85 -Y 25 'sleep 100') ||
fail "new-pane for new-pane repro failed"
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_width}')" 28
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_left}')" 86
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 26
# Shrink to 60x20: xoff = 60 - 28 - 1 = 31; yoff = 20 - 8 - 1 = 11.
$TMUX resize-window -t "$win" -x 60 -y 20 ||
fail "resize-window (new-pane repro) failed"
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_width}')" 28
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_left}')" 31
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 11
$TMUX kill-window -t "$win" || exit 1
# Case 4: a float that already fits inside the shrunk window is left alone --
# assert position and size are both unchanged, not just one of them.
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for untouched float failed"
# -x 20 -y 6 -> pane 18x4; -X 8 -Y 3 -> pane_left 9, pane_top 4 (as at the top
# of this file). Footprint columns 8-27, rows 3-8: well inside 60x20 too.
floating=$($TMUX new-pane -t "$win" -dPF '#{pane_id}' \
-x 20 -y 6 -X 8 -Y 3 'sleep 100') ||
fail "new-pane for untouched float failed"
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_left}')" 9
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 4
$TMUX resize-window -t "$win" -x 60 -y 20 ||
fail "resize-window (untouched float) failed"
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_width}')" 18
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 4
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_left}')" 9
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 4
$TMUX kill-window -t "$win" || exit 1
# Case 6: pad = 0, via the pane-border-lines window option set to none. The
# border arithmetic differs here (no -2/+1 adjustment either on creation or
# in the clamp).
win=$($TMUX new-window -dPF '#{window_id}') ||
fail "new-window for pad=0 float failed"
$TMUX set-option -w -t "$win" pane-border-lines none ||
fail "set pane-border-lines none failed"
# No border: -x 20 -y 6 -> pane 20x6 directly; -X 60 -Y 18 -> pane_left 60,
# pane_top 18 directly. Footprint (== content, no border) is columns 60-79,
# rows 18-23: flush right/bottom of the 80x24 window, same as case 1.
$TMUX break-pane -W -s "$win" -x 20 -y 6 -X 60 -Y 18 ||
fail "break-pane -W for pad=0 float failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 20
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 6
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 60
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 18
# Shrink to 40x16 with pad=0: xoff = 40 - 20 - 0 = 20; yoff = 16 - 6 - 0 = 10.
$TMUX resize-window -t "$win" -x 40 -y 16 ||
fail "resize-window (pad=0 float) failed"
must_equal "$($TMUX display-message -p -t "$win" '#{pane_width}')" 20
must_equal "$($TMUX display-message -p -t "$win" '#{pane_height}')" 6
must_equal "$($TMUX display-message -p -t "$win" '#{pane_left}')" 20
must_equal "$($TMUX display-message -p -t "$win" '#{pane_top}')" 10
$TMUX kill-window -t "$win" || exit 1
$TMUX kill-server 2>/dev/null $TMUX kill-server 2>/dev/null
exit 0 exit 0

Some files were not shown because too many files have changed in this diff Show More