Compare commits

..

188 Commits

Author SHA1 Message Date
Michael Grant
6933bc417c tty: drop WT_SESSION-based Windows Terminal detection
nicm is opposed to identifying a terminal via an environment variable, so
remove the WindowsTerminal tty_default_features() entry and the
WT_SESSION check in tty_term_create() entirely, pending some other
accepted identification mechanism for Windows Terminal (it cannot be
identified via the existing XTVERSION path at all - see the previous
commit's now-removed comment for why).

This leaves WezTerm and ghostty's margins grants in place - both are
identified via the existing, already-accepted XTVERSION mechanism, with
DECSLRM support confirmed directly in their own source, independent of
this change.

regress/tty-margins-wt-session.sh tested only the removed mechanism and
is replaced by regress/tty-margins-scrollbar.sh, which checks the same
underlying scrollbar/margins scroll-decision mechanism (still real, and
still what WezTerm's and ghostty's table entries rely on) via the
generic terminal-features option instead of simulating any one
terminal's identification handshake.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 09:02:13 +01:00
Michael Grant
20f76fb069 tty: broaden DECSLRM (margins) terminal-feature coverage
Any pane that doesn't span the terminal's full width - because
pane-scrollbars is on (the scrollbar occupies a column) or the pane is one
of a side-by-side split - needs DECSLRM (left/right margin) support to use
the fast native-scroll path (tty_cmd_linefeed()/scrollup()/scrolldown()/
reverseindex(), tty.c: "(!tty_full_width(tty, ctx) && !tty_use_margin(tty))").
Without it, every single scroll falls back to tty_redraw_region()'s full
manual repaint of the whole region - confirmed via -vv log
("tty_redraw_region: ... large region redraw") - which is a real source of
flicker, and separately implicated in reports of image content not
surviving a scroll in this area of the tree.

The DECSLRM machinery itself (tty_margin_pane()/tty_margin()) is already
built and already called from all four native-scroll dispatch functions.
What was missing was terminal-capability coverage:

  - WezTerm and ghostty are already correctly identified via the existing
    XTVERSION mechanism (tty_keys_extended_device_attributes(),
    tty-keys.c), and both genuinely implement DECSLRM - confirmed directly
    in their own source (WezTerm: decslrm() in csi.rs, documented in their
    escape-sequences reference, a changelog entry fixing a DECSLRM bug
    confirming active use; ghostty: handled in dcs.zig/stream.zig,
    explicitly emitted alongside DECSTBM in their own formatter.zig, with
    its own terminfo entry for it). Neither's entry in
    tty_default_features()'s table (tty-features.c) granted "margins".
    This was a gap, not a detection problem, backed by source-level
    evidence rather than guesswork - kitty and Rio show zero DECSLRM
    references in their own source, so their omission is left as-is.

  - Windows Terminal's DECSLRM support was independently confirmed by
    direct, non-tmux escape-sequence testing, but it can never be
    identified via XTVERSION - the tracking issue for that
    (github.com/microsoft/terminal#18382) was explicitly closed
    not_planned by the maintainer, calling XTVERSION "not extensible or
    helpful for feature detection". Detected instead via the WT_SESSION
    environment variable Windows Terminal sets for every child process -
    a long-standing, stable signal already used for this exact purpose by
    many other tools. Checked in tty_term_create() (tty-term.c) alongside
    the existing COLORTERM-based RGB/256 detection, which already reads a
    named variable out of the attaching client's own environment
    (c->environ, populated via the MSG_IDENTIFY_ENVIRON handshake) the
    same way. Deliberately grants only "margins" for now, not the full
    modern-xterm feature bundle other entries get, since no other
    capability has been verified for it.

New regress/tty-margins-wt-session.sh reproduces the scrollbar-pane
scenario with WT_SESSION injected into the attaching client's environment
and checks, via the -vv log, that no tty_redraw_region() fallback occurs -
plus a sanity phase confirming the same scenario does fall back without
WT_SESSION set, proving the test isn't accidentally trivial. Verified
failing 3/3 against the pre-fix code and passing 5/5 standalone; full
regress suite passes clean twice, with only pre-existing, unrelated
failures (check-names.sh - confirmed identical on unmodified master;
prompt-words-history.sh - already-known flake; four untracked
image-*-noflash scratch tests left over from unrelated work on another
branch, not part of this tree's tracked suite).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:37:06 +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
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
tmux update bot
9b0ad73a4b Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  When changing selection-mode to line, set up the selection start and end correctly, GitHub issue 5545.
  Extend word commands to use any Unicode whitespace character, GitHub issue 5562 from Hongyi Zhao.
2026-09-01 14:55:38 +00:00
nicm
bd8218e85d When changing selection-mode to line, set up the selection start and end
correctly, GitHub issue 5545.
2026-09-01 14:55:35 +00:00
nicm
8f3101d820 Extend word commands to use any Unicode whitespace character, GitHub
issue 5562 from Hongyi Zhao.
2026-09-01 14:55:35 +00:00
Nicholas Marriott
77309c07b0 Whitespace regress test from Hongyi Zhao. 2026-09-01 13:50:56 +01:00
tmux update bot
c8e889694d Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a generation counter to pane output, from Michael K Darling.
  Add format variables for history change counters, from Michael K Darling.
2026-09-01 00:07:34 +00:00
nicm
b402687fcd Add a generation counter to pane output, from Michael K Darling. 2026-09-01 00:07:32 +00:00
nicm
0530a7d89a Add format variables for history change counters, from Michael K Darling. 2026-09-01 00:07:32 +00:00
tmux update bot
6bdb9ba54c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  tmux: add utf8 terminal-features
2026-08-31 20:43:13 +00:00
kirill
f2bfd611e2 tmux: add utf8 terminal-features
OK: nicm@
2026-08-31 20:43:11 +00:00
Nicholas Marriott
9c1d5a080b Use hash for actions, from Andreas Fehlner. 2026-08-31 20:52:54 +01:00
tmux update bot
b28244e2fe Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not leak temporary commands when expanding aliases, from Jeong, Heon.
  Do not allow popups for control clients, from Noam Stolero.
  Layout cell can be replaced by unzoom, fixes use after free. From Uzair Aftab.
  Do not try to append zero size cells, from Uzair Aftab.
  Clear stale marks when a search regex fails to compile, GitHub issue 5547 from Uzair Aftab.
2026-08-31 14:56:14 +00:00
nicm
8b3834c34f Do not leak temporary commands when expanding aliases, from Jeong, Heon. 2026-08-31 14:56:12 +00:00
nicm
af3e4d2e5b Do not allow popups for control clients, from Noam Stolero. 2026-08-31 14:56:12 +00:00
nicm
c552d1bf16 Layout cell can be replaced by unzoom, fixes use after free. From Uzair
Aftab.
2026-08-31 14:56:12 +00:00
nicm
6a1ecdcce3 Do not try to append zero size cells, from Uzair Aftab. 2026-08-31 14:56:12 +00:00
nicm
39357c829c Clear stale marks when a search regex fails to compile, GitHub issue
5547 from Uzair Aftab.
2026-08-31 14:56:12 +00:00
tmux update bot
40381bdcd9 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not walk off end of grid lines if last line is wrapped, reported by Moe Khalilov.
  Do no free input context if it is NULL when popup create fails.
2026-08-28 10:32:01 +00:00
nicm
7bad4f7722 Do not walk off end of grid lines if last line is wrapped, reported by
Moe Khalilov.
2026-08-28 10:32:00 +00:00
nicm
65e811570b Do no free input context if it is NULL when popup create fails. 2026-08-28 10:32:00 +00:00
tmux update bot
dd31900f80 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Ignore focus events earlier to avoid them interfering with prefix, GitHub issue 5539.
2026-08-28 09:02:23 +00:00
nicm
013ada58c8 Ignore focus events earlier to avoid them interfering with prefix,
GitHub issue 5539.
2026-08-28 09:02:22 +00:00
Nicholas Marriott
78dad45f3b Add timeout for lock and don't keep credentials for regress, from Andreas
Fehlner.
2026-08-28 09:07:16 +01:00
Nicholas Marriott
52b452948f Check for buffer write error. 2026-08-28 08:56:12 +01:00
tmux update bot
267746603a Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Fix tiled pane resizing with floating layout cells, reported by Kevin Hovsäter.
2026-08-25 21:31:27 +00:00
nicm
b26b240613 Fix tiled pane resizing with floating layout cells, reported by Kevin
Hovsäter.
2026-08-25 21:31:25 +00:00
Nicholas Marriott
8e48c889dd Add a resize test. 2026-08-25 19:36:06 +01:00
tmux update bot
f4445a310a Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a clear-on-attach option which when turned off scrolls away the existing terminal state instead of entering the alternate screen. This is useful in environments where tmux is started as a login shell but it is important that any pre-tmux messages are still visible. GitHub issue 5508 from Josef Řídký.
2026-08-25 11:30:47 +00:00
nicm
4157b64754 Add a clear-on-attach option which when turned off scrolls away the
existing terminal state instead of entering the alternate screen. This
is useful in environments where tmux is started as a login shell but it
is important that any pre-tmux messages are still visible. GitHub issue
5508 from Josef Řídký.
2026-08-25 11:30:45 +00:00
tmux update bot
0d2aafe7b7 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not pass NULL to qsort, and add checks for invalid session or window after swap.
2026-08-25 09:38:42 +00:00
nicm
6f4c9b32af Do not pass NULL to qsort, and add checks for invalid session or window
after swap.
2026-08-25 09:38:40 +00:00
tmux update bot
eb77a75ab7 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not leak exit message on client free, from Jeong, Heon.
  Tidy up args_make_commands reference counting to fix a memory leak, from Jeong, Heon.
2026-08-25 07:59:42 +00:00
nicm
0851650c6b Do not leak exit message on client free, from Jeong, Heon. 2026-08-25 07:59:40 +00:00
nicm
1459c90a7f Tidy up args_make_commands reference counting to fix a memory leak, from
Jeong, Heon.
2026-08-25 07:59:40 +00:00
tmux update bot
af29c080ba Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Correct a comparison ('\0' should be 0).
  If a pane has no scrollbar, don't bother to redraw it, and if redraw is deferred change scrollbar redraws into redraw-all-scrollbars not redraw entire window. GitHub issue 5529.
2026-08-25 01:46:56 +00:00
nicm
50ea32162f Correct a comparison ('\0' should be 0). 2026-08-25 01:46:55 +00:00
nicm
2d5328a860 If a pane has no scrollbar, don't bother to redraw it, and if redraw is
deferred change scrollbar redraws into redraw-all-scrollbars not redraw
entire window. GitHub issue 5529.
2026-08-25 01:46:55 +00:00
tmux update bot
e5db7598b2 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not add messages to cfg_causes after config has finished, GitHub issue 5528.
2026-08-24 22:03:29 +00:00
nicm
dc78d8f9ea Do not add messages to cfg_causes after config has finished, GitHub
issue 5528.
2026-08-24 22:03:27 +00:00
tmux update bot
022b8451e3 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Always use synchronized updates in copy mode (like when not in the active pane), since copy mode will almost always moved the cursor. From Artem Goldenberg in GitHub issue 5525.
2026-08-24 16:44:37 +00:00
nicm
5b54c7d41a Always use synchronized updates in copy mode (like when not in the
active pane), since copy mode will almost always moved the cursor. From
Artem Goldenberg in GitHub issue 5525.
2026-08-24 16:44:34 +00:00
tmux update bot
f94036b2b0 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Free any remaining items when destroying screen, from Jeong, Heon.
2026-08-24 15:57:10 +00:00
nicm
7d358208b9 Free any remaining items when destroying screen, from Jeong, Heon. 2026-08-24 15:57:09 +00:00
tmux update bot
c8ff8ea96f Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add {} to shell escape characters (special in bash/ksh/csh), GitHub issue 5514.
2026-08-24 15:02:17 +00:00
nicm
402d366bba Add {} to shell escape characters (special in bash/ksh/csh), GitHub
issue 5514.
2026-08-24 15:02:15 +00:00
Nicholas Marriott
41e701ad84 Return early if image is zero height when generating fallback, otherwise we can
allocated too little space. Reported by Vivek Parikh.
2026-08-24 15:28:17 +01:00
tmux update bot
f763f94556 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not leak cached status line, from Jeong, Heon.
  Free pane fallback range when freeing pane, from Jeong, Heon.
  Do not leak path when destroying client, from Jeong, Heon.
  Do not leak format when drawing preview, from Jeong, Heon.
  Do not leak buffer name on failure, from Jeong, Heon.
2026-08-24 08:28:15 +00:00
nicm
c9f5ecb4b5 Do not leak cached status line, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
nicm
7792a74c76 Free pane fallback range when freeing pane, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
nicm
4cabc2ae85 Do not leak path when destroying client, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
nicm
995b395795 Do not leak format when drawing preview, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
nicm
3d44f75d09 Do not leak buffer name on failure, from Jeong, Heon. 2026-08-24 08:28:13 +00:00
tmux update bot
c1f947a3c5 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Use correct X position when combining characters, GitHub issue 5511 from Francesc Rosas.
2026-08-21 11:56:01 +00:00
nicm
696a16cca2 Use correct X position when combining characters, GitHub issue 5511 from
Francesc Rosas.
2026-08-21 11:55:59 +00:00
Nicholas Marriott
936bb6e7cd Regress for GitHub issue 5511 from Francesc Rosas. 2026-08-21 10:54:29 +01:00
tmux update bot
8dfa903346 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a way for floating panes to stay above zoom (-A) flag. Use by default for modal panes to match popups.
2026-08-20 12:57:29 +00:00
nicm
95a7170709 Add a way for floating panes to stay above zoom (-A) flag. Use by
default for modal panes to match popups.
2026-08-20 12:57:26 +00:00
Nicholas Marriott
554c4bbde4 Tests for modal panes on top. 2026-08-20 10:19:19 +01:00
Nicholas Marriott
ac23f21a2c More regress tests for zoom. 2026-08-20 09:18:58 +01:00
tmux update bot
6696151008 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Stop at the right end line when walking wrapped lines, GitHub issue 5479.
2026-08-19 21:05:08 +00:00
nicm
50d0348b64 Stop at the right end line when walking wrapped lines, GitHub issue 5479. 2026-08-19 21:05:06 +00:00
tmux update bot
bf7bbba7ea Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a -K flag to new-pane to make a modal pane capture all keys like popups used to.
2026-08-19 13:10:09 +00:00
nicm
b28b1b7b9a Add a -K flag to new-pane to make a modal pane capture all keys like
popups used to.
2026-08-19 13:10:08 +00:00
Nicholas Marriott
8499d8007e new-pane -K test bits. 2026-08-19 11:56:27 +01:00
tmux update bot
e5a2058c7c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Use DECRQM to detect mode 2026, from Michael Grant.
2026-08-18 12:03:17 +00:00
nicm
e94fcd96f9 Use DECRQM to detect mode 2026, from Michael Grant. 2026-08-18 12:03:15 +00:00
Nicholas Marriott
c5017e6da8 Bump time limit. 2026-08-18 11:49:34 +01:00
tmux update bot
0169f9e18c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Change correct depth counter for source-file when used from hooks, GitHub issue 5437.
2026-08-18 09:49:28 +00:00
nicm
b170c19274 Change correct depth counter for source-file when used from hooks,
GitHub issue 5437.
2026-08-18 09:49:26 +00:00
tmux update bot
22b3679752 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  When a pane is respawned, reset the control mode offsets. GitHub issue 5498 from Alex Rattray.
  Correctly clear clients referring to a session with destroy-unattached, GitHub issue 5497 from Jeong, Heon.
  Restore umask if bind fails, from nachalsa at naver dot com.
2026-08-18 09:04:02 +00:00
nicm
4714f478fe When a pane is respawned, reset the control mode offsets. GitHub issue
5498 from Alex Rattray.
2026-08-18 09:04:00 +00:00
nicm
72c5037f06 Correctly clear clients referring to a session with destroy-unattached,
GitHub issue 5497 from Jeong, Heon.
2026-08-18 09:04:00 +00:00
nicm
6238793ad7 Restore umask if bind fails, from nachalsa at naver dot com. 2026-08-18 09:04:00 +00:00
Nicholas Marriott
da641e4e8d Add test for GitHub issue 5498 from Alex Rattray. 2026-08-18 08:45:11 +01:00
Nicholas Marriott
19a085a672 Fix socket names for macOS. 2026-08-18 08:23:16 +01:00
tmux update bot
f672a0f677 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Log unknown message types.
2026-08-17 23:38:12 +00:00
nicm
db194ced5b Log unknown message types. 2026-08-17 23:38:11 +00:00
tmux update bot
11ed661e2c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Flush output before ending sync. GitHub issue 5495 from xiangzhedev at gmail dot com.
2026-08-17 21:11:33 +00:00
nicm
b2de803871 Flush output before ending sync. GitHub issue 5495 from xiangzhedev at
gmail dot com.
2026-08-17 21:11:31 +00:00
167 changed files with 10235 additions and 8526 deletions

View File

@@ -17,8 +17,9 @@ jobs:
action:
runs-on: ubuntu-latest
if: github.repository == 'tmux/tmux'
timeout-minutes: 10
steps:
- uses: dessant/lock-threads@v6
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6
with:
github-token: ${{ github.token }}
issue-inactive-days: '30'

View File

@@ -17,7 +17,7 @@ jobs:
name: ${{ matrix.name }}
runs-on: ${{ matrix.runner }}
if: github.repository == 'tmux/tmux'
timeout-minutes: 45
timeout-minutes: 300
strategy:
fail-fast: false
@@ -27,10 +27,10 @@ jobs:
runner: ubuntu-24.04
make: make
configure: --enable-utf8proc --enable-asan
#- name: ubuntu-24.04-arm64
# runner: ubuntu-24.04-arm
# make: make
# configure: --enable-utf8proc --enable-asan
- name: ubuntu-24.04-arm64
runner: ubuntu-24.04-arm
make: make
configure: --enable-utf8proc --enable-asan
- name: macos-26-arm64
runner: macos-26
make: gmake
@@ -38,7 +38,9 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- name: dependencies
if: runner.os == 'Linux'
@@ -70,18 +72,18 @@ jobs:
- name: build
run: |
sh autogen.sh
sh autogen.sh
./configure ${{ matrix.configure }}
${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)"
- name: test
run: |
cd regress
${{ matrix.make }}
${{ matrix.make }} -j"$(getconf _NPROCESSORS_ONLN)"
- name: logs
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: regress-logs-${{ matrix.name }}
path: regress/logs/*.log

64
CHANGES
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
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. 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
mouse dragging;
@@ -24,10 +29,15 @@ CHANGES FROM 3.7c TO 3.8
-k for choose commands kills the pane when the mode exits, which is useful
for modes in floating panes;
- 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;
- command-prompt -P opens a prompt inside a pane rather than on the status
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 support for themes and improve default colours:
@@ -74,7 +84,7 @@ CHANGES FROM 3.7c TO 3.8
- new hooks to cover areas including client create and destroy, pane
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,
pane-command-finished and pane-shell-prompt.
@@ -87,10 +97,23 @@ CHANGES FROM 3.7c TO 3.8
while scrolling or while hovered and disappears after pane-scrollbars-timeout
(Michael Grant).
* Extend the fill-character option so both inside and outside the window can be
changed.
* Add a clear-on-attach server option. When disabled, tmux does not enter the
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 a #{A/count:frames} modifier to show a series of frames as an animation
in the status line (issue 5412 from Fernando Daciuk).
@@ -105,6 +128,12 @@ CHANGES FROM 3.7c TO 3.8
* 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).
* 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.
* Dragging over an existing copy mode selection now adjusts it (Michael Grant).
@@ -118,6 +147,13 @@ CHANGES FROM 3.7c TO 3.8
* Copy mode no longer exits at the bottom while a selection is in progress
(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
nolink) (issues 4842 and 4280 from Moritz Angermann).
@@ -128,7 +164,9 @@ CHANGES FROM 3.7c TO 3.8
* Add or improve format variables including client_colours,
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
floating panes can be sorted by z-index.
@@ -140,12 +178,18 @@ CHANGES FROM 3.7c TO 3.8
* 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
queue, like keys from key bindings (issue 3476).
* 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
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).
* Fix grouped sessions sometimes being left as unusable command targets while
they are being killed (Bryce Miller, issue 5180).

View File

@@ -1,715 +0,0 @@
# Image Redraw and Damage Plan
## Purpose
Make image redraws correct and efficient for tiled panes, floating panes,
scrolling, copy-mode selection, pane borders, pane status lines, scrollbars,
and clients using different output protocols.
The intended model is retained: the visible screen is a scene of horizontal
cell spans. A change damages cell-aligned regions, and the compositor redraws
only scene spans intersecting those regions.
Synchronized output may hide unavoidable intermediate terminal updates, but
it must not be required for correctness.
## Current state and recovery point
The last clean, pushed commit on `4902-image-support` is:
```
ec9859b1 sixel: preserve image aspect across client geometries
```
That commit already contains the important persistent model:
- sparse image placements represented by spans attached to grid lines;
- input-protocol semantics and placement ordering;
- source coordinates for cropping placement spans;
- movement, copying, duplication, and removal of image spans with grid data;
- per-client output backends for Kitty, SIXEL, and fallback rendering; and
- cached image/rendering data.
The current uncommitted changes touch eight files and mix several attempted
optimizations. They are known to damage SIXEL in Mintty and cause unnecessary
redraws with floating panes. Do not commit them as the solution.
Before discarding them, save one patch outside the branch for reference:
```
git diff -- image.c regress/image-support.sh screen-redraw.c screen-write.c \
server-client.c tmux.h tty.c window-copy.c \
> /tmp/4902-image-redraw-experiment.patch
```
Then restore those tracked files to `ec9859b1` before beginning the new work.
Do this only after the user explicitly approves the rollback.
### Ideas worth retaining, but reimplementing separately
- Redraw damage should be restricted to scene-visible spans.
- Image-related selection tests are useful.
- Floating-pane and pane-status regression scenarios are useful.
- Window chrome must be emitted after pane image/text composition whenever
the same damaged region contains both.
- A placement's complete canvas may need to be represented even where its
pixels are transparent; visibility and alpha must then be tested while
compositing rather than by losing the placement geometry.
- Native scrolling should be selected according to the affected region and
backend behaviour.
### Changes not worth retaining in their present form
- Promoting an obscured-pane update to `PANE_REDRAW` and then sometimes to
`CLIENT_REDRAWWINDOW`.
- Repairing images directly from individual `tty_cmd_*` text functions.
- Separate ad-hoc redraw paths for images, selection, scrollbars, borders,
and floating panes.
- The current `TTY_CTX_IMAGE_SCROLL`, `TTY_CTX_PANE_FULL_WIDTH`, and
`CLIENT_REDRAWSCROLLBARS` plumbing as a combined patch.
- Assuming an unchanged pane-status string does not need physical redraw
after some other drawing operation has covered its cells.
## Required invariants
These rules should be written down in code comments and enforced throughout
the implementation.
1. **Input determines semantics.** Whether text removes an image layer, lies
above it, or lies below it is determined by the protocol received from the
application. It must not depend on whether the outer client uses Kitty,
SIXEL, or fallback output.
2. **Output backends encode a scene.** An output backend may choose different
escape sequences, caches, or cropping, but it must render the same logical
result.
3. **One compositor owns repair.** No output path may independently erase and
reconstruct image cells without going through the same scene ordering.
4. **Damage is regional.** Ordinary content changes do not become pane-wide
or window-wide redraws. Full redraw remains appropriate for attachment,
resize, geometry changes, terminal reset, or invalid scene geometry.
5. **Damage uses cell coordinates.** Pixel offsets are handled when an image
backend maps the damaged cells to a source-pixel crop.
6. **Logical state changes before physical output.** Grid text and image spans
are moved, removed, or inserted first. Rendering then reads the completed
logical state.
7. **Chrome is part of composition.** Pane borders, pane status lines,
scrollbars, menus, and floating panes must not be repaired independently in
an order that allows image output to cover them afterward.
8. **No-image behaviour remains unchanged.** When a damaged region does not
intersect image data or graphical overlays, existing fast text output and
native scrolling should remain available.
## Established image semantics
The image semantics present at the `ec9859b1` recovery point are a
compatibility requirement. They resulted from the review discussion in PR
#5445, particularly the temporal-versus-positional model described in
[Theory and Practice of Sprixels](https://nick-black.com/dankwiki/index.php?title=Theory_and_Practice_of_Sprixels#Transparency_and_z-ordering),
and were accepted after commit `a4abd616`, which added the corresponding
manual visual checks.
- SIXEL input is temporal. Transparent pixels preserve what existed when the
SIXEL was emitted. A later text write annihilates the affected SIXEL cells;
the SIXEL does not remain logically behind that text.
- Kitty input is positional. Negative-z placements remain below the text plane;
nonnegative placements remain above it. Text writes do not convert or remove
Kitty placements merely because the output client uses SIXEL.
- Overlapping placements remain separate ordered layers. Transparent pixels in
an upper placement reveal retained lower placements or text.
- Input protocol, z-index, and placement order determine the logical scene. The
attached client's output protocol only determines how that scene is encoded.
- Redraw, damage, scrolling, copy mode, and backend conversion must preserve
these rules exactly.
The manual checks introduced by `a4abd616` and the corresponding regression
cases are semantic compatibility tests, not merely visual diagnostics.
## Coordinate spaces
Keep conversions explicit. Do not pass ambiguous `x` and `y` values between
layers.
- **Grid coordinates:** include history and belong to a pane's `struct grid`.
- **Pane coordinates:** visible cells inside a pane.
- **Window coordinates:** include pane offsets and floating-pane positions.
- **Client scene coordinates:** window coordinates clipped by the client's
viewport (`ox`, `oy`, `sx`, and `sy`).
- **Terminal coordinates:** include the client status-line offset and any
terminal viewport offset.
- **Image source coordinates:** identify cell rows/columns within a placement;
the backend maps these to source pixels using terminal cell geometry and
placement pixel offsets.
Store damage in window coordinates. Translate and clip it separately for each
client when drawing, because clients may have different viewports, status-line
positions, cell geometries, and output backends.
## Compartmentalization
Keep image implementation details in the existing image files. The rest of
tmux should know only that some scene cells require image-aware composition.
### `image.c`
Own all protocol-independent image state and composition policy:
- placements, spans, input protocol, z-index, serial ordering, and alpha;
- applying input semantics when text overwrites image cells;
- testing whether a grid rectangle intersects image placements;
- mapping a damaged cell rectangle to ordered image fragments;
- deciding which logical image layers are below or above the text plane;
- generic backend dispatch; and
- public image helpers used by the redraw subsystem.
### `image-sixel.c`
Own only SIXEL-specific output details:
- cached quantized/indexed representations;
- mapping cell crops to pixel and six-pixel-band crops;
- emitting SIXEL fragments;
- alpha and palette encoding; and
- backend-specific reset, erase, and scrolling capabilities.
### `image-kitty.c`
Own only Kitty-specific output details:
- client-side transmission and placement caches;
- placement creation, clipping, movement, and deletion;
- Kitty IDs and geometry; and
- backend-specific reset and scrolling behaviour.
### Other tmux files
- `screen-redraw.c` owns scene construction, damage clipping, and the ordering
of pane content versus window chrome. It calls generic `image.c` helpers and
contains no Kitty or SIXEL protocol logic.
- `screen-write.c` updates logical grid state and reports pane/window damage.
It must not render image fragments or select an output backend.
- `tty.c` owns terminal text and scrolling primitives. It receives an already
selected native-scroll operation; it must not reconstruct image layers or
call image repair after individual characters.
- `server-client.c` schedules and drains per-client damage. It must not inspect
image placements or output protocols.
- `window-copy.c` reports copy-mode damage and copies logical image state using
generic image helpers; it must not render images itself.
- `tmux.h` exposes only the minimum generic structures and function prototypes
required across those boundaries.
Prefer one generic call from `screen-redraw.c` into `image.c` over `ENABLE_IMAGES`
branches scattered through tty commands. A build without image support should
retain the existing redraw path with minimal conditional code.
## Damage representation
Add a small regional damage abstraction owned by the redraw subsystem. Do not
add per-cell queues or enlarge `struct grid_cell`.
A damage entry needs:
```
x, y, sx, sy window-cell rectangle
reason content, scroll exposure, pane move, chrome, and so on
```
The exact names should follow tmux style. A short list or dynamic array per
client is sufficient. When a new rectangle is added:
- clip it to the window;
- discard empty rectangles;
- merge overlapping or directly adjacent rectangles where that does not make
the result substantially larger;
- cap the list at a modest number; above the cap, replace it with their union;
- promote to a full redraw only if geometry is invalid or the union covers
most of the client scene.
A shared window damage list must not be cleared after the first client draws.
Either append translated window rectangles to every affected client, or give
damage events monotonically increasing sequence numbers so each client can
consume them independently. The simpler initial implementation is to append
window-coordinate rectangles to each client currently displaying the window.
Suggested public operations are conceptually:
```
redraw_damage_window(window, x, y, sx, sy, reason)
redraw_damage_pane(window_pane, x, y, sx, sy, reason)
redraw_damage_client(client, x, y, sx, sy, reason)
```
Do not expose image-backend details through these calls.
## Regional scene composition
Extend `screen-redraw.c` so an existing `redraw_scene` can be drawn through a
set of clipping rectangles.
For every damaged line:
1. Intersect the damage rectangle with that line's scene spans.
2. Split only at damage boundaries; do not rebuild the complete scene.
3. Draw intersecting pane content in logical layer order.
4. Draw window chrome intersecting the same rectangle last.
For a pane span, the scene has already selected the topmost visible pane at
that position. Composition inside that pane should then be explicit:
1. image placements below the text plane, such as negative Kitty z-indexes;
2. the text plane, including the requested copy-mode style;
3. surviving temporal SIXEL placements and images above the text plane;
4. pane borders, pane status, scrollbars, menus, and other chrome.
The exact placement order is already represented by `struct image_placement`
and its input protocol, z-index, and serial. Do not derive it from the client's
output backend.
Only the topmost pane content is normally needed because panes are opaque.
Underlying pane content is retained in its own grid and becomes visible when a
floating pane moves, at which time the old floating-pane rectangle is damaged
and recomposed.
### Image fragments
An `image_span` is placement metadata, not a separately stored bitmap. For an
intersection, compute the corresponding source-cell rectangle and ask the
backend to emit that crop from its cached rendering.
- Do not rerun quantization or dithering for every small damage rectangle.
- SIXEL output operates in six-pixel vertical bands. Expand or clip the
backend operation as required, then ensure any additionally touched cells
are included in the same composition pass.
- A transparent top image requires lower image layers to be drawn first.
- Kitty placements may be deleted, clipped, or replaced only within the
damaged placement region.
## Fast text writes
Do not route every ordinary character through a deferred full compositor.
For an application text write:
1. Apply input-protocol damage rules to the logical image spans. For example,
text written after a SIXEL image removes the affected SIXEL coverage.
2. Test whether the affected cells still intersect any image layer or scene
overlay requiring composition.
3. If not, retain the existing direct `tty_cmd_cell`/`tty_cmd_cells` path.
4. If they do, suppress conflicting direct terminal repair and enqueue only
those cells for regional composition.
This decision belongs above the backend-specific tty output functions. Avoid
callbacks such as `image_draw_after_text()` from several `tty_cmd_*` functions;
they reproduce ordering logic in multiple places.
## Selection and copy mode
Copy mode must not mutate the application's logical image placements.
- Treat selection as a temporary tmux UI style on the text plane.
- Follow the chosen policy that selection highlighting does not overwrite
image-covered cells.
- Mouse motion should damage only cells whose selection state actually
changed.
- If changed selection cells do not intersect visible images, draw them using
the normal text fast path.
- If they intersect an image canvas, compose those cells once; do not erase a
whole image or pane.
- Entering or leaving copy mode should damage the visible pane rectangle once,
not repeatedly for every unchanged mouse event.
Copy-mode image spans should be copied before its text view is composed, but
this should be implemented through the same logical state and damage API, not
through special tty repair calls.
## Floating panes
### Focus changes
Changing the active pane should damage only cells whose appearance changes:
- affected pane borders;
- pane status cells whose style or contents change; and
- any active-pane indicators.
It must not redraw pane interiors or retransmit unrelated SIXEL images.
### Moving or resizing a floating pane
For an old rectangle `A` and new rectangle `B`:
- invalidate the scene geometry;
- damage `A union B` (represented as a few rectangles rather than necessarily
one large bounding box);
- rebuild the scene because pane ownership changed;
- recompose only spans intersecting those rectangles.
The old rectangle restores tiled panes and any image fragments that were
previously hidden. The new rectangle draws the floating pane and its chrome.
Multiple overlapping floating panes follow their window z-order.
## Scrolling
Implement scrolling in three stages. Do not begin with the most complicated
native-scroll optimization.
### Stage 1: correct regional redraw
1. Move grid text and image spans using the existing grid operations.
2. Record the pane scroll rectangle and newly exposed rows.
3. Recompose that rectangle from the completed logical scene.
This may still retransmit more image data than desired, but it establishes a
correct regional baseline without pane- or window-wide redraws.
### Stage 2: native scrolling without obstructions
Use the terminal's native scroll operation only when:
- the backend/terminal is known to scroll its image pixels consistently;
- the source and destination rectangle belong to the same visible pane;
- no floating pane, menu, overlay, pane border/status, or reserved scrollbar
intersects it;
- required vertical and horizontal margins are available and reliable; and
- existing BCE and terminal-size checks permit the operation.
After native scrolling, compose only newly exposed rows and any protocol-sized
boundary expansion.
### Stage 3: mixed native scrolling around obstructions
Derive safe rectangles from visible scene spans:
- a destination cell is native-scrollable only when its source cell and
destination cell are both visible parts of the same pane;
- coalesce equal horizontal ranges across adjacent rows into rectangles;
- native-scroll those maximal rectangles;
- recompose cells crossing an obstruction boundary and newly exposed cells.
With one floating pane, left and right strips can often scroll over the full
height. Above and below it, cells whose source crosses behind the floating pane
must be recomposed rather than recovered from terminal pixels.
As a simpler optional strategy, native-scroll the full tiled-pane rectangle
and recompose the floating pane's old/current rectangles afterward. This is
correct if all disturbed cells are marked, but may visibly flash without
synchronized output. Prefer safe-rectangle scrolling once correctness is
established.
If rectangle decomposition becomes too fragmented, fall back to regional
composition. It is an optimization, never a separate correctness path.
## Redraw scheduling
Do not turn queued regional damage into `CLIENT_REDRAWWINDOW` merely because
the tty output buffer is nonempty. Preserve the damage list and process it
after output drains.
Use full redraw only for:
- initial attachment;
- client or terminal geometry changes;
- a terminal reset or invalid backend cache;
- layout/viewport changes where rebuilding and clipping regions is not yet
safe; or
- a damage union large enough that full redraw is cheaper.
Focus changes, selection changes, ordinary scrolling, and moving one floating
pane should remain regional.
## Debugging instrumentation
Add temporary debug logging before optimizing:
- damage reason and window rectangle;
- client clipping and coordinate translation;
- scene generation used;
- native-scroll rectangles selected;
- spans recomposed in each layer phase; and
- image fragments emitted by each backend.
The log should make it possible to answer, for every SIXEL retransmission,
which damage rectangle required it. Remove excessively noisy instrumentation
before the final commit or keep it behind normal verbose logging.
## Tests
Build the tests before each optimization and keep each test focused on one
invariant.
### Automated regressions
1. **No-image baseline:** ordinary output, scrolling, copy mode, floating pane
focus, borders, status lines, and scrollbars behave unchanged.
2. **Focus only:** changing focus with a SIXEL elsewhere emits no SIXEL DCS and
updates only the affected border/status cells.
3. **Selection outside image:** emits no image output.
4. **Selection crossing image:** the image remains intact during and after the
drag; only changed non-image cells are highlighted.
5. **Unobstructed scroll:** logical image spans move with text; where native
scrolling is supported, no complete SIXEL retransmission occurs.
6. **Scroll with floating pane:** both pane-status lines, borders, scrollbar,
floating-pane contents, and visible image fragments remain correct.
7. **Move floating pane:** old and new rectangles are restored; unrelated image
regions are not emitted.
8. **Multiple floating panes:** overlapping z-order remains correct while the
top pane moves.
9. **Transparent placement:** lower images/text show through correctly, and
moving/selection does not treat transparency as absent placement geometry.
10. **Cross-backend clients:** the same input scene produces equivalent results
on Kitty, SIXEL, and fallback clients attached simultaneously.
11. **Pane status top/bottom:** unchanged status contents are physically
restored if their cells were damaged.
12. **Scrollbar reserved/overlay:** scroll thumb and track remain present and
images do not overwrite them.
Where possible, capture raw terminal output and assert both presence and
absence of DCS/APC sequences. `capture-pane` alone cannot prove physical output
ordering because it observes logical grid contents rather than terminal image
pixels.
### Manual matrix
Test at least:
- Microsoft Terminal with SIXEL;
- Mintty with SIXEL;
- WezTerm with SIXEL;
- Kitty with Kitty output;
- a Kitty client attached to a session created from a SIXEL client and the
reverse; and
- a terminal without graphical image support.
For each, test scrolling by application output and mouse wheel, selecting text,
entering/leaving copy mode, changing focus, opening/closing/moving a floating
pane, pane status at top and bottom, and scrollbars.
## Implementation steps and commit boundaries
This must not be implemented as one monolithic change. Each numbered step is a
separate commit, review, and testing gate. Do not begin the next step until the
current step passes its focused tests. If a step exposes a pre-existing bug,
record it before expanding the patch.
### Step 0: archive the experiment and recover the baseline
**Goal:** Begin development from known source rather than repairing the mixed
experimental diff.
**Work:**
- Preserve the current diff and this plan on a clearly named WIP branch.
- Return `4902-image-support` to clean commit `ec9859b1`.
- Build with and without image support.
- Run the existing image regression and ordinary tmux regression suite.
**Gate:** The branch is clean and existing behaviour is reproduced. No product
code is changed in this step.
### Step 1: add focused regressions and tracing
**Goal:** Establish observable failures before changing redraw behaviour.
**Primary files:** `regress/image-support.sh`, temporary debug logging in
`screen-redraw.c` if necessary.
**Work:** Add independent tests for focus-only changes, selection crossing an
image, unobstructed scrolling, scrolling beneath a floating pane, moving a
floating pane, both pane-status lines, and scrollbars. Raw terminal output must
be used where a test needs to prove that SIXEL or Kitty data was or was not
emitted.
**Gate:** Each test either passes on the baseline or has a documented expected
failure. Tests must not depend on timing races or visual screenshots. The
existing edge-case checks introduced by `a4abd616` must pass unchanged on the
recovery baseline and establish the semantic compatibility result expected by
later gates.
**Not included:** No redraw implementation changes.
### Step 2: introduce regional damage scheduling
**Goal:** Represent and preserve small window-coordinate damage rectangles
without changing the rendered result.
**Primary files:** `screen-redraw.c`, `server-client.c`, minimal declarations in
`tmux.h`.
**Work:** Add per-client damage storage, clipping, merging, a complexity cap,
and verbose logging. Existing callers may initially convert the damage to the
same redraw they used before. Pending tty output must retain the region rather
than promote it automatically to a full-window redraw.
**Gate:** No-image and image output remain visually and byte-for-byte equivalent
where practical. Unit-style tests verify rectangle clipping and merging.
**Not included:** No image-layer traversal, selection changes, floating-pane
optimization, or native scrolling.
### Step 3: add a generic image-region composition API
**Goal:** Give the scene compositor one protocol-independent way to render the
image layers intersecting a cell rectangle.
**Primary files:** `image.c`, `image-sixel.c`, `image-kitty.c`, with only generic
prototypes in `tmux.h`.
**Work:**
- In `image.c`, intersect damage with placement spans and preserve logical
input/z/serial order.
- In `image-sixel.c`, emit a crop from cached quantized data and handle SIXEL
band alignment.
- In `image-kitty.c`, update/delete/place only the intersecting cached
placements.
- Define clearly separate below-text and above-text passes.
**Gate:** Rendering a whole image as adjacent damaged rectangles produces the
same final scene as rendering it in one operation. Test transparent and
overlapping placements on both output backends. The `a4abd616` semantic
compatibility checks must continue to pass unchanged.
**Not included:** No changes to individual tty text commands and no native
scroll optimization.
### Step 4: clip scene composition to damage rectangles
**Goal:** Make `screen-redraw.c` repaint only scene spans intersecting queued
damage.
**Primary files:** `screen-redraw.c`; generic calls into `image.c`.
**Work:** For each damaged line, intersect existing redraw spans, compose the
selected pane's below-text images, text, and above-text images, then draw
intersecting chrome last. Full redraw continues to use the same compositor with
a full-client damage rectangle.
**Gate:** Full and tiled panes redraw correctly; pane borders, pane status,
scrollbars, menus, and images have deterministic output order. No floating-pane
movement or scroll optimization is added yet.
### Step 5: route text and selection damage through the compositor
**Goal:** Repair only cells where text or copy-mode selection interacts with
image layers.
**Primary files:** `screen-write.c`, `window-copy.c`, `screen-redraw.c`, and
logical helpers in `image.c`.
**Work:** Apply input semantics to logical spans first. Preserve direct tty text
output when no image placement intersects the cells. Otherwise enqueue those
cells for composition. Selection changes damage only cells whose selection
state changed and follow the policy of not overwriting image-covered cells.
**Gate:** Dragging selection across an image never damages it, entering/leaving
copy mode restores the same scene, and selection outside images emits no image
data. The `a4abd616` semantic compatibility checks must continue to pass
unchanged.
**Not included:** Do not add image callbacks to `tty_cmd_cell`,
`tty_cmd_cells`, or `tty_cmd_clearcharacter`.
### Step 6: make floating-pane changes regional
**Goal:** Focus, movement, resize, open, and close operations damage only the
cells whose scene ownership or chrome changed.
**Primary files:** floating-pane/window code that reports geometry changes,
plus `screen-redraw.c`. Image implementation remains in the image files.
**Work:** Focus changes enqueue border/status cells only. Movement and resize
enqueue the old and new pane rectangles and invalidate scene geometry. Regional
composition restores underlying panes/images in the old rectangle and draws
the floating pane in the new rectangle.
**Gate:** Changing focus emits no unrelated SIXEL/APC. Moving a floating pane
does not damage images, either pane-status line, borders, or scrollbars. Test
one and multiple overlapping floating panes.
### Step 7: implement correct regional scrolling without native optimization
**Goal:** Establish correct scrolling using logical state plus regional
composition.
**Primary files:** grid/image state helpers in `image.c`, damage reporting in
`screen-write.c`, composition in `screen-redraw.c`.
**Work:** Move text and image spans first, then damage the scroll rectangle and
compose it. Do not use special image repair from `tty.c`.
**Gate:** Application scrolling, mouse-wheel copy-mode scrolling, partial
scroll regions, scrollbars, and scrolling beneath floating panes are correct on
Kitty, Microsoft Terminal, Mintty, and WezTerm. This step may redraw more than
the final optimized version, but must not redraw outside the scroll rectangle.
The `a4abd616` semantic compatibility checks must continue to pass unchanged.
### Step 8: enable native scrolling for unobstructed rectangles
**Goal:** Avoid retransmitting image pixels when the terminal can safely move
them.
**Primary files:** scroll planning in `screen-redraw.c`, generic backend
capability in `image.c`, backend facts in `image-sixel.c` and `image-kitty.c`,
terminal primitive execution in `tty.c`.
**Work:** Select native scrolling only when source/destination cells belong to
the same unobstructed pane and terminal margins/backend behaviour permit it.
Compose newly exposed rows afterward. Retain Step 7 as the fallback.
**Gate:** Raw output proves the native scroll is used and a complete image is
not retransmitted. Final output must match Step 7 on every tested backend.
### Step 9: decompose scrolling around floating panes
**Goal:** Combine native scrolling of safe rectangles with composition of
obstructed and boundary rectangles.
**Primary files:** scroll planning and scene-span traversal in
`screen-redraw.c`; no new protocol logic outside the image files.
**Work:** Derive maximal rectangles where both source and destination are
visible cells of the same pane, coalesce adjacent equal ranges, natively scroll
them, and compose everything else. Enforce a fragmentation threshold that
falls back to Step 7.
**Gate:** Scrolling beneath one or several floating panes produces the same
final scene as Step 7, reduces emitted image data, and never loses pane status,
borders, scrollbars, or images.
### Step 10: cleanup, performance checks, and full regression
**Goal:** Remove superseded paths only after all replacements are proven.
**Work:** Remove obsolete redraw helpers and flags introduced or made unused by
the new path. Reduce temporary logging. Check memory growth, rectangle-list
bounds, output byte counts, and behaviour without image support. Run the entire
regression suite and the manual terminal matrix.
**Gate:** All completion criteria below are satisfied. Synchronized output may
improve appearance, but disabling it does not alter final contents. The
`a4abd616` semantic compatibility checks must continue to pass unchanged.
Do not squash these implementation steps during development. Keeping their
boundaries visible makes it possible to bisect correctness and performance
regressions. They can be reorganized later only after review.
## Completion criteria
The work is complete when:
- Mintty, Microsoft Terminal, WezTerm, and Kitty show no persistent damage;
- focus and selection do not retransmit unrelated images;
- floating-pane movement redraws only old/new affected regions;
- scrolling preserves images, pane borders, both tiled and floating pane
status lines, and scrollbars;
- input semantics produce the same composition on every output backend;
- no-image performance and behaviour remain unchanged;
- full regression tests pass; and
- synchronized output improves appearance but disabling it never changes the
final screen contents.

View File

@@ -5,8 +5,7 @@ CLEANFILES = tmux.1.mdoc tmux.1.man cmd-parse.c
# Distribution tarball options.
EXTRA_DIST = \
CHANGES README COPYING example_tmux.conf \
osdep-*.c mdoc2man.awk tmux.1 \
tools/generate-image-diacritics.sh
osdep-*.c mdoc2man.awk tmux.1
dist_EXTRA_tmux_SOURCES = compat/*.[ch]
# Preprocessor flags.
@@ -175,6 +174,7 @@ dist_tmux_SOURCES = \
input-keys.c \
input.c \
job.c \
json.c \
key-bindings.c \
key-string.c \
layout-custom.c \
@@ -188,7 +188,6 @@ dist_tmux_SOURCES = \
options-table.c \
options.c \
paste.c \
popup.c \
proc.c \
prompt-history.c \
prompt.c \
@@ -247,9 +246,9 @@ if HAVE_UTF8PROC
nodist_tmux_SOURCES += compat/utf8proc.c
endif
# Enable image support.
if ENABLE_IMAGES
dist_tmux_SOURCES += image.c image-sixel.c image-kitty.c
# Enable sixel support.
if ENABLE_SIXEL
dist_tmux_SOURCES += image.c image-sixel.c
endif
# Add bits for fuzzing if enabled.

View File

@@ -35,7 +35,7 @@ The usual local layout is:
```sh
cd /some/where/useful
git clone https://github.com/tmux/tmux.git tmux-portable
git clone https://github.com/ThomasAdam/tmux-obsd.git tmux-openbsd-cutover
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:
@@ -56,18 +56,19 @@ The cutover repository has three important branches:
# 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
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
```
If the remote already exists, update it instead:
```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
```
@@ -77,6 +78,32 @@ Fetch the cutover master branch explicitly:
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
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
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
cd /path/to/tmux-portable
@@ -108,10 +137,15 @@ git checkout 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
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 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
```
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:
@@ -140,11 +189,25 @@ git add path/to/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:
```sh
git status
git diff --check
git diff --cached --check
git diff --cached --stat
```

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: arguments.c,v 1.66 2026/06/26 09:54:56 nicm Exp $ */
/* $OpenBSD: arguments.c,v 1.67 2026/08/25 06:04:33 nicm Exp $ */
/*
* Copyright (c) 2010 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -762,8 +762,6 @@ args_make_commands_now(struct cmd *self, struct cmdq_item *item, u_int idx,
cmdq_error(item, "%s", error);
free(error);
}
else
cmdlist->references++;
args_make_commands_free(state);
return (cmdlist);
}
@@ -827,8 +825,10 @@ args_make_commands(struct args_command_state *state, int argc, char **argv,
int i;
if (state->cmdlist != NULL) {
if (argc == 0)
if (argc == 0) {
state->cmdlist->references++;
return (state->cmdlist);
}
return (cmd_list_copy(state->cmdlist, argc, argv));
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: client.c,v 1.167 2026/08/17 07:56:56 nicm Exp $ */
/* $OpenBSD: client.c,v 1.168 2026/08/17 20:14:31 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -712,6 +712,9 @@ client_dispatch_wait(struct imsg *imsg)
fprintf(stderr, "server version is too old for client\n");
proc_exit(client_proc);
break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
}
}
@@ -798,5 +801,8 @@ client_dispatch_attached(struct imsg *imsg)
system(data);
proc_send(client_peer, MSG_UNLOCK, -1, NULL, 0);
break;
default:
log_debug("unknown message type %u", imsg->hdr.type);
break;
}
}

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>
@@ -42,8 +42,8 @@ const struct cmd_entry cmd_capture_pane_entry = {
.name = "capture-pane",
.alias = "capturep",
.args = { "ab:CeE:FHJLMNpPqRS:Tt:", 0, 0, NULL },
.usage = "[-aCeFHJLMNpPqRT] " CMD_BUFFER_USAGE " [-E end-line] "
.args = { "ab:CeE:FHIJLMNpPqRS:Tt:", 0, 0, NULL },
.usage = "[-aCeFHIJLMNpPqRT] " CMD_BUFFER_USAGE " [-E end-line] "
"[-S start-line] " CMD_TARGET_PANE_USAGE,
.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[1] = '\0';
} else
xsnprintf(tmp, sizeof tmp, "\\%03hho", line[i]);
snprintf(tmp, sizeof tmp, "\\%03hho", line[i]);
buf = cmd_capture_pane_append(buf, len, tmp,
strlen(tmp));
}
@@ -259,7 +259,7 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
struct grid_cell *gc = NULL;
struct window_mode_entry *wme;
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 i, sx, top, bottom, tmp;
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;
number_lines = args_has(args, 'L');
show_flags = args_has(args, 'F');
show_time = args_has(args, 'I');
hyperlinks = args_has(args, 'H');
if (hyperlinks)
links = xreallocarray(NULL, gd->sx, sizeof *links);
@@ -358,6 +359,7 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
free(line);
continue;
}
gl = grid_peek_line(gd, i);
if (number_lines) {
if (i >= gd->hsize)
@@ -368,11 +370,16 @@ cmd_capture_pane_history(struct args *args, struct cmdq_item *item,
if (n >= 0)
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) {
cp = b;
*cp = '\0';
gl = grid_peek_line(gd, i);
if (gl->flags & GRID_LINE_DEAD)
*cp++ = 'D';
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);
gl = grid_peek_line(gd, i);
if (!join_lines || !(gl->flags & GRID_LINE_WRAPPED))
buf[(*len)++] = '\n';

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-command-prompt.c,v 1.75 2026/06/25 11:39:11 nicm Exp $ */
/* $OpenBSD: cmd-command-prompt.c,v 1.76 2026/08/25 06:04:33 nicm Exp $ */
/*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -242,9 +242,11 @@ cmd_command_prompt_callback(struct client *c, void *data, const char *s,
} else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
}
cmd_free_argv(argc, argv);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-display-menu.c,v 1.52 2026/07/14 19:07:03 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>
@@ -54,8 +54,8 @@ const struct cmd_entry cmd_display_popup_entry = {
.name = "display-popup",
.alias = "popup",
.args = { "Bb:Cc:d:e:Eh:kNs:S:t:T:w:x:y:", 0, -1, NULL },
.usage = "[-BCEkN] [-b border-lines] [-c target-client] "
.args = { "Bb:Cc:d:e:Eh:ks:S:t:T:w:x:y:", 0, -1, NULL },
.usage = "[-BCEk] [-b border-lines] [-c target-client] "
"[-d start-directory] [-e environment] [-h height] "
"[-s style] [-S border-style] " CMD_TARGET_PANE_USAGE
" [-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);
}
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
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)
@@ -482,8 +285,8 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
enum box_lines lines = BOX_LINES_DEFAULT;
char *title, *cause = NULL;
int flags = 0, starting_choice = 0;
u_int px, py, i, count = args_count(args);
struct options *o = target->s->curw->window->options;
u_int px, py, sx, sy, i, count = args_count(args);
struct options *o = target->w->options;
struct options_entry *oe;
if (args_has(args, 'C')) {
@@ -531,9 +334,6 @@ cmd_display_menu_exec(struct cmd *self, struct cmdq_item *item)
}
if (menu->count == 0)
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');
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);
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'))
flags |= MENU_STAYOPEN;
@@ -565,6 +369,22 @@ fail:
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
cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
{
@@ -572,146 +392,199 @@ cmd_display_popup_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state *target = cmdq_get_target(item);
struct session *s = target->s;
struct client *tc = cmdq_get_target_client(item);
struct tty *tty = &tc->tty;
const char *value, *shell, *shellcmd = NULL;
const char *style = args_get(args, 's');
struct winlink *wl = target->wl;
struct window *w = wl->window;
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');
char *cwd = NULL, *cause = NULL, **argv = NULL;
char *title = NULL;
int modify = popup_present(tc);
int flags = -1, argc = 0;
enum box_lines lines = BOX_LINES_DEFAULT;
u_int px, py, w, h, count = args_count(args);
char *cause = NULL, *title = NULL;
enum pane_lines lines = PANE_LINES_SINGLE;
u_int px, py, sx, sy, count = args_count(args);
struct args_value *av;
struct environ *env = NULL;
struct options *o = s->curw->window->options;
struct options_entry *oe;
long long ll;
if (args_has(args, 'C')) {
server_client_clear_overlay(tc);
if (w->modal != NULL)
server_kill_pane(w->modal);
return (CMD_RETURN_NORMAL);
}
if (!modify && tc->overlay_draw != NULL)
if (tc->flags & CLIENT_CONTROL)
return (CMD_RETURN_NORMAL);
if (w->modal != NULL)
return (CMD_RETURN_NORMAL);
if (!modify) {
h = tty->sy / 2;
if (args_has(args, 'h')) {
h = args_percentage(args, 'h', 1, tty->sy, tty->sy,
&cause);
if (cause != NULL) {
cmdq_error(item, "height %s", cause);
goto fail;
}
}
w = tty->sx / 2;
if (args_has(args, 'w')) {
w = args_percentage(args, 'w', 1, tty->sx, tty->sx,
&cause);
if (cause != NULL) {
cmdq_error(item, "width %s", cause);
goto fail;
}
}
if (w > tty->sx)
w = tty->sx;
if (h > tty->sy)
h = tty->sy;
if (!cmd_display_menu_get_popup_pos(tc, item, args, &px, &py,
w, h))
goto out;
value = args_get(args, 'd');
if (value != NULL)
cwd = format_single_from_target(item, value);
else
cwd = xstrdup(server_client_get_cwd(tc, s));
if (count == 0) {
shellcmd = options_get_string(s->options,
"default-command");
} else if (count == 1)
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) {
env = environ_create();
av = args_first_value(args, 'e');
while (av != NULL) {
environ_put(env, av->string, 0);
av = args_next_value(av);
}
}
}
value = args_get(args, 'b');
if (args_has(args, 'B'))
lines = BOX_LINES_NONE;
lines = PANE_LINES_NONE;
else if (value != NULL) {
oe = options_get(o, "popup-border-lines");
lines = options_find_choice(options_table_entry(oe), value,
&cause);
lines = cmd_display_popup_get_lines(value, &cause);
if (cause != NULL) {
cmdq_error(item, "popup-border-lines %s", cause);
cmdq_error(item, "pane-border-lines %s", cause);
goto fail;
}
}
sy = w->sy / 2;
if (args_has(args, 'h')) {
ll = args_percentage(args, 'h', 1, w->sy, w->sy, &cause);
if (cause != NULL) {
cmdq_error(item, "height %s", cause);
goto fail;
}
sy = ll;
}
sx = w->sx / 2;
if (args_has(args, 'w')) {
ll = args_percentage(args, 'w', 1, w->sx, w->sx, &cause);
if (cause != NULL) {
cmdq_error(item, "width %s", cause);
goto fail;
}
sx = ll;
}
if (sx > w->sx)
sx = w->sx;
if (sy > w->sy)
sy = w->sy;
if ((lines == PANE_LINES_NONE && (sx < 1 || sy < 1)) ||
(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;
lg.sx = sx;
lg.sy = sy;
lg.xoff = px;
lg.yoff = py;
if (lines != PANE_LINES_NONE) {
lg.sx -= 2;
lg.sy -= 2;
lg.xoff++;
lg.yoff++;
}
window_push_zoom(w, 0, 1);
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');
while (av != NULL) {
environ_put(sc.environ, av->string, 0);
av = args_next_value(av);
}
new_wp = spawn_pane(&sc, &cause);
if (new_wp == NULL) {
cmdq_error(item, "create pane failed: %s", cause);
free(cause);
cause = NULL;
window_pop_zoom(w);
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'))
title = format_single_from_target(item, args_get(args, 'T'));
else
title = xstrdup("");
if (args_has(args, 'N') || !modify)
flags = 0;
if (args_has(args, 'E') > 1) {
if (flags == -1)
flags = 0;
flags |= POPUP_CLOSEEXITZERO;
} else if (args_has(args, 'E')) {
if (flags == -1)
flags = 0;
flags |= POPUP_CLOSEEXIT;
}
if (args_has(args, 'k')) {
if (flags == -1)
flags = 0;
flags |= POPUP_CLOSEANYKEY;
if (title != NULL) {
options_set_number(new_wp->options, "pane-border-status",
PANE_STATUS_TOP);
options_set_string(new_wp->options, "pane-border-format", 0,
"%s", "#{pane_title}");
screen_set_title(&new_wp->base, title, 0);
ep = event_payload_create();
cmd_find_from_pane(&fs, new_wp, 0);
event_payload_set_target(ep, &fs);
event_payload_set_pane(ep, "pane", new_wp);
event_payload_set_window(ep, "window", new_wp->window);
event_payload_set_string(ep, "new_title", "%s", title);
events_fire("pane-title-changed", ep);
}
if (modify) {
popup_modify(tc, title, style, border_style, lines, flags);
goto out;
}
if (popup_display(flags, lines, item, px, py, w, h, env, shellcmd, argc,
argv, cwd, title, tc, s, style, border_style, NULL, NULL) != 0)
goto out;
environ_free(env);
free(cwd);
new_wp->wait_item = item;
server_redraw_session(s);
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);
free(title);
cmd_free_argv(argc, argv);
return (CMD_RETURN_WAIT);
out:
cmd_free_argv(argc, argv);
environ_free(env);
free(cwd);
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);
free(title);
return (CMD_RETURN_NORMAL);
fail:
free(cause);
cmd_free_argv(argc, argv);
environ_free(env);
free(cwd);
if (new_wp != NULL) {
server_client_remove_pane(new_wp);
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);
return (CMD_RETURN_ERROR);
}

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>
@@ -39,8 +39,8 @@ const struct cmd_entry cmd_display_message_entry = {
.name = "display-message",
.alias = "display",
.args = { "aCc:d:lINpt:F:v", 0, 1, NULL },
.usage = "[-aCIlNpv] [-c target-client] [-d delay] [-F format] "
.args = { "aCc:d:jlINpt:F:v", 0, 1, NULL },
.usage = "[-aCIjlNpv] [-c target-client] [-d delay] [-F format] "
CMD_TARGET_PANE_USAGE " [message]",
.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 window_pane *wp = target->wp;
const char *template;
char *msg, *cause;
char *msg, *cause = NULL;
int delay = -1, flags, Nflag = args_has(args, 'N');
int Cflag = args_has(args, 'C');
struct format_tree *ft;
u_int count = args_count(args);
struct evbuffer *evb;
struct json_node *jn;
if (args_has(args, 'I')) {
if (args_has(args, 'I') && !args_has(args, 'j')) {
if (wp == NULL)
return (CMD_RETURN_NORMAL);
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);
else
template = args_get(args, 'F');
if (template == NULL)
if (args_has(args, 'j') && template == NULL)
template = "";
else if (template == NULL)
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);
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_free(ft);
return (CMD_RETURN_NORMAL);
@@ -139,6 +142,19 @@ cmd_display_message_exec(struct cmd *self, struct cmdq_item *item)
msg = xstrdup(template);
else
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)
cmdq_error(item, "%s", msg);

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>
@@ -388,9 +388,11 @@ cmd_find_get_window_with_session(struct cmd_find_state *fs, const char *window)
/* Try as an offset. */
if (!exact && (window[0] == '+' || window[0] == '-')) {
if (window[1] != '\0')
n = strtonum(window + 1, 1, INT_MAX, NULL);
else
if (window[1] != '\0') {
n = strtonum(window + 1, 1, INT_MAX, &errstr);
if (errstr != NULL)
return (-1);
} else
n = 1;
s = fs->s;
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. */
if (pane[0] == '+' || pane[0] == '-') {
if (pane[1] != '\0')
n = strtonum(pane + 1, 1, INT_MAX, NULL);
else
if (pane[1] != '\0') {
n = strtonum(pane + 1, 1, INT_MAX, &errstr);
if (errstr != NULL)
return (-1);
} else
n = 1;
wp = fs->w->active;
if (pane[0] == '+')

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-if-shell.c,v 1.86 2025/08/01 09:05:51 nicm Exp $ */
/* $OpenBSD: cmd-if-shell.c,v 1.87 2026/08/25 06:04:33 nicm Exp $ */
/*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -98,6 +98,7 @@ cmd_if_shell_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_ERROR);
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
return (CMD_RETURN_NORMAL);
}
@@ -164,9 +165,11 @@ cmd_if_shell_callback(struct job *job)
} else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
}
out:

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>
@@ -133,7 +133,7 @@ cmd_join_pane_place(struct cmdq_item *item, struct winlink *wl,
} else if (strcmp(position, "back") == 0) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp))
if (!window_pane_is_floating_with_hidden(owp))
break;
}
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);
} else if (strcmp(position, "forward") == 0) {
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) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_BEFORE(owp, wp, zentry);
}
} else if (strcmp(position, "backward") == 0) {
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)) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_AFTER(&w->z_index, owp, wp, zentry);
}
} else if (strcmp(position, "forward-loop") == 0) {
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);
if (owp != NULL)
TAILQ_INSERT_BEFORE(owp, wp, zentry);
else {
TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp))
if (!window_pane_is_floating_with_hidden(owp))
break;
}
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) {
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)) {
TAILQ_REMOVE(&w->z_index, wp, zentry);
TAILQ_INSERT_AFTER(&w->z_index, owp, wp, zentry);
@@ -347,8 +355,10 @@ cmd_join_pane_zindex(struct cmdq_item *item, struct winlink *wl,
n = 0;
TAILQ_FOREACH(owp, &w->z_index, zentry) {
if (!window_pane_is_floating(owp))
if (!window_pane_is_floating_with_hidden(owp))
break;
if (owp->layout_cell == NULL)
continue;
if (n >= z)
break;
n++;
@@ -443,7 +453,6 @@ cmd_join_pane_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "pane is not floating");
return (CMD_RETURN_ERROR);
}
server_unzoom_window(dst_w);
if ((s = args_get(args, 'P')) != NULL)
return (cmd_join_pane_place(item, dst_wl, dst_wp, s));
if ((s = args_get(args, 'z')) != NULL)

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>
@@ -97,6 +97,7 @@ cmd_list_panes_window(struct cmd *self, struct session *s, struct winlink *wl,
struct cmdq_item *item, int type)
{
struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_client(item);
struct window_pane *wp, **l;
u_int i, n;
struct format_tree *ft;
@@ -147,7 +148,7 @@ cmd_list_panes_window(struct cmd *self, struct session *s, struct winlink *wl,
wp = l[i];
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_add(ft, "line", "%u", n);
format_defaults(ft, NULL, s, wl, wp);
format_defaults(ft, c, s, wl, wp);
if (filter != NULL) {
expanded = format_expand(ft, filter);

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>
@@ -53,6 +53,7 @@ static enum cmd_retval
cmd_list_sessions_exec(struct cmd *self, struct cmdq_item *item)
{
struct args *args = cmd_get_args(self);
struct client *c = cmdq_get_client(item);
struct session **l;
u_int n, i;
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++) {
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
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) {
expanded = format_expand(ft, filter);

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>
@@ -60,6 +60,7 @@ cmd_list_windows_exec(struct cmd *self, struct cmdq_item *item)
{
struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item);
struct client *c = cmdq_get_client(item);
struct winlink *wl, **l;
struct session *s;
u_int i, n;
@@ -94,7 +95,7 @@ cmd_list_windows_exec(struct cmd *self, struct cmdq_item *item)
s = wl->session;
ft = format_create(cmdq_get_client(item), item, FORMAT_NONE, 0);
format_add(ft, "line", "%u", n);
format_defaults(ft, NULL, s, wl, NULL);
format_defaults(ft, c, s, wl, NULL);
if (filter != NULL) {
expanded = format_expand(ft, filter);

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>
@@ -60,7 +60,8 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
struct client *tc = cmdq_get_target_client(item);
struct session *s = target->s;
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;
const char *template, *name;
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
* name already exists, select it.
* If -S is given, select an existing window instead of creating a
* 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');
if (name != NULL) {
@@ -88,32 +91,39 @@ cmd_new_window_exec(struct cmd *self, struct cmdq_item *item)
wname = clean_name(expanded, 0);
free(expanded);
}
if (args_has(args, 'S') && wname != NULL && target->idx == -1) {
expanded = format_single(item, wname, c, s, NULL, NULL);
RB_FOREACH(wl, winlinks, &s->windows) {
if (strcmp(wl->window->name, expanded) != 0)
continue;
if (new_wl == NULL) {
new_wl = wl;
continue;
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);
RB_FOREACH(wl, winlinks, &s->windows) {
if (strcmp(wl->window->name, expanded) != 0)
continue;
if (new_wl == NULL) {
new_wl = wl;
continue;
}
cmdq_error(item, "multiple windows named %s",
wname);
free(wname);
free(expanded);
return (CMD_RETURN_ERROR);
}
cmdq_error(item, "multiple windows named %s", wname);
free(wname);
free(expanded);
return (CMD_RETURN_ERROR);
}
free(expanded);
if (new_wl != NULL) {
free(wname);
if (args_has(args, 'd'))
return (CMD_RETURN_NORMAL);
if (session_set_current(s, new_wl) == 0)
server_redraw_session(s);
if (c != NULL && c->session != NULL)
s->curw->window->latest = c;
recalculate_sizes();
}
/* Found an existing window to select instead of creating a new one. */
if (new_wl != NULL) {
free(wname);
if (args_has(args, 'd'))
return (CMD_RETURN_NORMAL);
}
if (session_set_current(s, new_wl) == 0)
server_redraw_session(s);
if (c != NULL && c->session != NULL)
s->curw->window->latest = c;
recalculate_sizes();
return (CMD_RETURN_NORMAL);
}
before = args_has(args, 'b');

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-parse.y,v 1.58 2026/04/27 12:31:11 nicm Exp $ */
/* $OpenBSD: cmd-parse.y,v 1.60 2026/09/08 10:20:08 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -210,7 +210,7 @@ expanded : format
cmd_find_from_client(&fs, c, 0);
fsp = &fs;
}
ft = format_create(NULL, pi->item, FORMAT_NONE, flags);
ft = format_create(c, pi->item, FORMAT_NONE, flags);
format_defaults(ft, c, fsp->s, fsp->wl, fsp->wp);
$$ = format_expand(ft, $1);
@@ -802,6 +802,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd,
if (last == NULL) {
pr->status = CMD_PARSE_SUCCESS;
pr->cmdlist = cmd_list_new();
cmd_parse_free_commands(cmds);
return (1);
}
@@ -814,6 +815,7 @@ cmd_parse_expand_alias(struct cmd_parse_command *cmd,
pi->flags |= CMD_PARSE_NOALIAS;
cmd_parse_build_commands(cmds, pi, pr);
pi->flags &= ~CMD_PARSE_NOALIAS;
cmd_parse_free_commands(cmds);
return (1);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-queue.c,v 1.122 2026/08/03 13:38:42 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>
@@ -580,6 +580,9 @@ cmdq_fire_command(struct cmdq_item *item)
int flags, quiet = 0;
char *tmp;
if (item->client != NULL && (item->client->flags & CLIENT_DEAD))
return (CMD_RETURN_ERROR);
if (cfg_finished)
cmdq_add_message(item);
if (log_get_level() > 1) {
@@ -850,7 +853,18 @@ cmdq_error(struct cmdq_item *item, const char *fmt, ...)
if (c == NULL) {
cmd_get_source(cmd, &file, &line);
cfg_add_cause("%s:%u: %s", file, line, msg);
if (!cfg_finished) {
if (file != NULL)
cfg_add_cause("%s:%u: %s", file, line, msg);
else
cfg_add_cause("%s", msg);
} else {
if (file != NULL) {
server_add_message("message: %s:%u: %s", file,
line, msg);
} else
server_add_message("message: %s", msg);
}
} else if (c->session == NULL || (c->flags & CLIENT_CONTROL)) {
server_add_message("%s message: %s", c->name, msg);
if (~c->flags & CLIENT_UTF8) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-resize-pane.c,v 1.67 2026/07/10 13:38:45 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>
@@ -91,7 +91,9 @@ cmd_resize_pane_exec(struct cmd *self, struct cmdq_item *item)
server_redraw_window(w);
return (CMD_RETURN_NORMAL);
}
server_unzoom_window(w);
if (!window_pane_is_floating(wp))
server_unzoom_window(w);
lc = wp->layout_cell; /* may have been replaced by unzoom */
if (args_has(args, 'x')) {
x = args_percentage(args, 'x', 0, PANE_MAXIMUM, w->sx, &cause);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-run-shell.c,v 1.93 2026/07/17 12:42:51 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>
@@ -164,7 +164,7 @@ cmd_run_shell_exec(struct cmd *self, struct cmdq_item *item)
if (cdata->client != NULL)
cdata->client->references++;
if (args_has(args, 'c'))
cdata->cwd = xstrdup(args_get(args, 'c'));
cdata->cwd = format_single_from_target(item, args_get(args, 'c'));
else
cdata->cwd = xstrdup(server_client_get_cwd(c, s));
@@ -233,9 +233,11 @@ cmd_run_shell_timer(__unused int fd, __unused short events, void* arg)
} else if (item == NULL) {
new_item = cmdq_get_command(cmdlist, NULL);
cmdq_append(c, new_item);
cmd_list_free(cmdlist);
} else {
new_item = cmdq_get_command(cmdlist, cmdq_get_state(item));
cmdq_insert_after(item, new_item);
cmd_list_free(cmdlist);
}
if (cdata->item != NULL)

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>
@@ -73,12 +73,13 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item)
{
struct args *args = cmd_get_args(self);
struct cmd_find_state *target = cmdq_get_target(item);
struct client *c = cmdq_get_target_client(item);
struct winlink *wl = target->wl;
struct window *w = wl->window;
struct window_pane *wp = target->wp;
const char *layoutname;
char *oldlayout, *cause;
int next, previous, layout;
char *oldlayout, *cause = NULL;
int next, previous, layout, flags = 0;
server_unzoom_window(w);
@@ -89,8 +90,12 @@ cmd_select_layout_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'p'))
previous = 1;
if (c != NULL &&
(c->flags & CLIENT_CONTROL) &&
(~c->flags & CLIENT_CONTROL_NEWLAYOUTS))
flags |= LAYOUT_CUSTOM_OLD_FORMAT;
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)

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-select-pane.c,v 1.77 2026/07/17 12:42:51 nicm Exp $ */
/* $OpenBSD: cmd-select-pane.c,v 1.78 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -160,6 +160,7 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
char *title;
const char *style;
struct options_entry *o;
int visible, Zflag = args_has(args, 'Z');
if (entry == &cmd_last_pane_entry || args_has(args, 'l')) {
/*
@@ -185,14 +186,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
server_redraw_window_borders(lastwp->window);
server_status_window(lastwp->window);
} else {
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && lastwp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(lastwp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, lastwp);
if (window_set_active_pane(w, lastwp, 1)) {
cmd_find_from_winlink(current, wl, 0);
cmd_select_pane_redraw(w);
}
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
}
return (CMD_RETURN_NORMAL);
@@ -268,14 +273,18 @@ cmd_select_pane_exec(struct cmd *self, struct cmdq_item *item)
if (wp == w->active)
return (CMD_RETURN_NORMAL);
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && wp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(wp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, wp);
if (window_set_active_pane(w, wp, 1))
cmd_find_from_winlink_pane(current, wl, wp, 0);
cmdq_insert_hook(s, item, current, "after-select-pane");
cmd_select_pane_redraw(w);
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
return (CMD_RETURN_NORMAL);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-set-buffer.c,v 1.37 2026/02/15 17:43:26 nicm Exp $ */
/* $OpenBSD: cmd-set-buffer.c,v 1.38 2026/08/24 07:05:23 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -100,6 +100,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "%s", cause);
goto fail;
}
free(bufname);
return (CMD_RETURN_NORMAL);
}
@@ -107,8 +108,10 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
cmdq_error(item, "no data specified");
goto fail;
}
if ((newsize = strlen(args_string(args, 0))) == 0)
if ((newsize = strlen(args_string(args, 0))) == 0) {
free(bufname);
return (CMD_RETURN_NORMAL);
}
if (args_has(args, 'a') && pb != NULL) {
olddata = paste_buffer_data(pb, &bufsize);
@@ -127,6 +130,7 @@ cmd_set_buffer_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'w') && tc != NULL)
tty_set_selection(&tc->tty, "", bufdata, bufsize);
free(bufname);
return (CMD_RETURN_NORMAL);
fail:

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-source-file.c,v 1.62 2025/11/18 08:42:09 nicm Exp $ */
/* $OpenBSD: cmd-source-file.c,v 1.63 2026/08/18 08:05:05 nicm Exp $ */
/*
* Copyright (c) 2008 Tiago Cunha <me@tiagocunha.org>
@@ -50,6 +50,7 @@ const struct cmd_entry cmd_source_file_entry = {
struct cmd_source_file_data {
struct cmdq_item *item;
struct client *client;
int flags;
struct cmdq_item *after;
@@ -60,10 +61,24 @@ struct cmd_source_file_data {
u_int nfiles;
};
static enum cmd_retval
cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data)
static void
cmd_source_file_free_data(struct cmd_source_file_data *cdata)
{
struct client *c = cmdq_get_client(item);
u_int i;
for (i = 0; i < cdata->nfiles; i++)
free(cdata->files[i]);
free(cdata->files);
if (cdata->client != NULL)
server_client_unref(cdata->client);
free(cdata);
}
static enum cmd_retval
cmd_source_file_complete_cb(struct cmdq_item *item, void *data)
{
struct cmd_source_file_data *cdata = data;
struct client *c = cdata->client;
if (c == NULL) {
cmd_source_file_depth--;
@@ -74,36 +89,36 @@ cmd_source_file_complete_cb(struct cmdq_item *item, __unused void *data)
}
cfg_print_causes(item);
cmd_source_file_free_data(cdata);
return (CMD_RETURN_NORMAL);
}
static void
cmd_source_file_complete(struct client *c, struct cmd_source_file_data *cdata)
cmd_source_file_complete(struct cmd_source_file_data *cdata)
{
struct client *c = cdata->client;
struct cmdq_item *new_item;
u_int i;
if (cfg_finished) {
if (cdata->retval == CMD_RETURN_ERROR &&
c != NULL &&
c->session == NULL)
c->retval = 1;
new_item = cmdq_get_callback(cmd_source_file_complete_cb, NULL);
cmdq_insert_after(cdata->after, new_item);
if (!cfg_finished) {
cmd_source_file_free_data(cdata);
return;
}
for (i = 0; i < cdata->nfiles; i++)
free(cdata->files[i]);
free(cdata->files);
free(cdata);
if (cdata->retval == CMD_RETURN_ERROR &&
c != NULL &&
c->session == NULL)
c->retval = 1;
new_item = cmdq_get_callback(cmd_source_file_complete_cb, cdata);
cmdq_insert_after(cdata->after, new_item);
}
static void
cmd_source_file_done(struct client *c, const char *path, int error,
int closed, struct evbuffer *buffer, void *data)
cmd_source_file_done(__unused struct client *oc, const char *path,
int error, int closed, struct evbuffer *buffer, void *data)
{
struct cmd_source_file_data *cdata = data;
struct cmdq_item *item = cdata->item;
struct client *c = cdata->client;
void *bdata = EVBUFFER_DATA(buffer);
size_t bsize = EVBUFFER_LENGTH(buffer);
u_int n;
@@ -127,7 +142,7 @@ cmd_source_file_done(struct client *c, const char *path, int error,
if (n < cdata->nfiles)
file_read(c, cdata->files[n], cmd_source_file_done, cdata);
else {
cmd_source_file_complete(c, cdata);
cmd_source_file_complete(cdata);
cmdq_continue(item);
}
}
@@ -187,6 +202,9 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item)
cdata = xcalloc(1, sizeof *cdata);
cdata->item = item;
cdata->client = c;
if (c != NULL)
c->references++;
if (args_has(args, 'q'))
cdata->flags |= CMD_PARSE_QUIET;
@@ -249,7 +267,7 @@ cmd_source_file_exec(struct cmd *self, struct cmdq_item *item)
file_read(c, cdata->files[0], cmd_source_file_done, cdata);
retval = CMD_RETURN_WAIT;
} else
cmd_source_file_complete(c, cdata);
cmd_source_file_complete(cdata);
free(cwd);
return (retval);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-split-window.c,v 1.148 2026/08/07 08:10:53 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>
@@ -40,8 +40,8 @@ const struct cmd_entry cmd_new_pane_entry = {
.name = "new-pane",
.alias = "newp",
.args = { "bB:Cc:de:EfF:hIkl:LMm:Op:PR:s:S:t:T:vWx:X:y:Y:Z", 0, -1, NULL },
.usage = "[-bCdefhIklMOPvWZ] [-B border-lines] "
.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] "
"[-c start-directory] [-e environment] "
"[-F format] [-l size] [-m message] [-p percentage] "
"[-s style] [-S active-border-style] "
@@ -89,6 +89,7 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state fs;
struct key_event *event = cmdq_get_event(item);
int input, empty, is_floating, flags = 0;
int restore_zoom = 0;
const char *template, *style, *value;
char *cause = NULL, *cp, *title;
const struct options_table_entry *oe;
@@ -96,10 +97,16 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
enum pane_lines lines;
u_int count = args_count(args);
if (window_active_pane_is_over_zoom(w))
restore_zoom = 1;
if (cmd_get_entry(self) == &cmd_new_pane_entry)
is_floating = !args_has(args, 'L');
else {
window_unzoom(w, 1);
if (!window_pane_is_visible(wp))
restore_zoom = 0;
if (!restore_zoom)
window_unzoom(w, 1);
is_floating = window_pane_is_floating(wp);
flags |= SPAWN_SPLIT;
}
@@ -133,7 +140,11 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (args_has(args, 'Z'))
flags |= SPAWN_ZOOM;
if (args_has(args, 'O'))
flags |= SPAWN_MODAL;
flags |= SPAWN_MODAL|SPAWN_FLOATOVERZOOM;
if (is_floating && args_has(args, 'A'))
flags |= SPAWN_FLOATOVERZOOM;
if ((w->flags & WINDOW_ZOOMED) && (flags & SPAWN_FLOATOVERZOOM))
restore_zoom = 1;
input = args_has(args, 'I');
if (input || (count == 1 && *args_string(args, 0) == '\0'))
@@ -169,6 +180,8 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (cause != NULL) {
cmdq_error(item, "%s", cause);
free(cause);
if (restore_zoom)
window_pop_zoom(w);
return (CMD_RETURN_ERROR);
}
@@ -202,8 +215,12 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
*/
goto fail;
}
if (args_has(args, 'K') && args_has(args, 'O'))
new_wp->flags |= PANE_CAPTUREALLKEYS;
if (args_has(args, 'C') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCLICK;
if (args_has(args, 'D') && args_has(args, 'O'))
new_wp->flags |= PANE_CLOSEONCANCEL;
style = args_get(args, 's');
if (style != NULL) {
@@ -271,7 +288,10 @@ cmd_split_window_exec(struct cmd *self, struct cmdq_item *item)
if (~flags & SPAWN_DETACHED)
cmd_find_from_winlink_pane(current, wl, new_wp, 0);
if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) {
if (restore_zoom) {
window_pop_zoom(wp->window);
server_redraw_window(wp->window);
} else if ((~flags & SPAWN_FLOATING) && !args_has(args, 'O')) {
window_pop_zoom(wp->window);
server_redraw_window(wp->window);
}
@@ -322,8 +342,9 @@ fail:
if (!is_floating)
layout_close_pane(new_wp);
window_remove_pane(wp->window, new_wp);
} else if (args_has(args, 'O'))
window_pop_modal_zoom(wp->window);
}
if (restore_zoom || (~flags & SPAWN_FLOATING))
window_pop_zoom(wp->window);
if (sc.argv != NULL)
cmd_free_argv(sc.argc, sc.argv);
environ_free(sc.environ);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-switch-client.c,v 1.74 2026/05/22 15:22:43 nicm Exp $ */
/* $OpenBSD: cmd-switch-client.c,v 1.75 2026/08/20 09:19:24 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -53,7 +53,7 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
struct cmd_find_state target;
const char *tflag = args_get(args, 't');
enum cmd_find_type type;
int flags;
int flags, visible, Zflag = args_has(args, 'Z');
struct client *c = cmdq_get_client(item);
struct client *tc = cmdq_get_target_client(item);
struct session *s;
@@ -139,11 +139,15 @@ cmd_switch_client_exec(struct cmd *self, struct cmdq_item *item)
return (CMD_RETURN_NORMAL);
if (wl != NULL && wp != NULL && wp != wl->window->active) {
w = wl->window;
if (window_push_zoom(w, 0, args_has(args, 'Z')))
if (w->modal != NULL && wp != w->modal)
visible = 1;
else
visible = window_pane_is_visible(wp);
if (!visible && window_push_zoom(w, 0, Zflag))
server_redraw_window(w);
window_redraw_active_switch(w, wp);
window_set_active_pane(w, wp, 1);
if (window_pop_zoom(w))
if (!visible && window_pop_zoom(w))
server_redraw_window(w);
}
if (wl != NULL) {

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>
@@ -480,6 +480,40 @@ cmd_wait_for_unlock(struct cmdq_item *item, const char *name,
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
cmd_wait_for_flush(void)
{

View File

@@ -1,6 +1,6 @@
# configure.ac
AC_INIT([tmux], next-3.8)
AC_INIT([tmux], next-3.9)
AC_PREREQ([2.60])
AC_CONFIG_AUX_DIR(etc)
@@ -530,34 +530,15 @@ if test "x$enable_cgroups" = xyes; then
fi
fi
# Enable image support.
AC_ARG_ENABLE(
images,
AS_HELP_STRING(--enable-images, enable Kitty/SIXEL/ASCII images)
)
# Keep the old option as an alias for one compatibility cycle.
# Enable sixel support.
AC_ARG_ENABLE(
sixel,
AS_HELP_STRING(--enable-sixel, deprecated alias for --enable-images)
AS_HELP_STRING(--enable-sixel, enable sixel images)
)
if test "x$enable_sixel" = xyes; then
enable_images=yes
AC_DEFINE(ENABLE_SIXEL)
fi
if test "x$enable_images" = xyes; then
PKG_CHECK_MODULES(
IMAGE,
[libpng >= 1.6 zlib],
[
AM_CPPFLAGS="$IMAGE_CFLAGS $AM_CPPFLAGS"
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
LIBS="$IMAGE_LIBS $LIBS"
],
AC_MSG_ERROR("image support requires libpng 1.6 and zlib")
)
AC_DEFINE(ENABLE_IMAGES)
fi
AM_CONDITIONAL(ENABLE_IMAGES, [test "x$enable_images" = xyes])
AM_CONDITIONAL(ENABLE_SIXEL, [test "x$enable_sixel" = xyes])
# Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well.
AC_MSG_CHECKING(for b64_ntop)
@@ -1156,10 +1137,10 @@ if test "x$enable_asan" = xyes; then
else
AC_MSG_NOTICE([ASAN: off])
fi
if test "x$enable_images" = xyes; then
AC_MSG_NOTICE([images: on])
if test "x$enable_sixel" = xyes; then
AC_MSG_NOTICE([SIXEL: on])
else
AC_MSG_NOTICE([images: off])
AC_MSG_NOTICE([SIXEL: off])
fi
if test "x$enable_debug" = xyes; then
AC_MSG_NOTICE([debug: on])

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>
@@ -70,6 +70,7 @@ control_window_layout_changed_cb(__unused const char *name,
struct session *s;
struct winlink *wl;
struct window *w = event_payload_get_window(ep, "window");
struct format_tree *ft;
const char *template;
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
* because the whole window will go away soon.
*/
wl = TAILQ_FIRST(&w->winlinks);
if (wl == NULL || w->layout_root == NULL)
if (TAILQ_FIRST(&w->winlinks) == NULL || w->layout_root == NULL)
return;
cp = format_single(NULL, template, NULL, NULL, wl, NULL);
TAILQ_FOREACH(c, &clients, entry) {
if (!CONTROL_SHOULD_NOTIFY_CLIENT(c) || c->session == NULL)
continue;
s = c->session;
if (winlink_find_by_window_id(&s->windows, w->id) != NULL)
control_notify_write(c, "%s", cp);
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);
free(cp);
}
free(cp);
}
/* Notify control clients that window pane changed. */

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: control.c,v 1.65 2026/08/04 11:18:22 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>
@@ -108,6 +108,7 @@ struct control_state {
u_int pending_count;
TAILQ_HEAD(, control_block) all_blocks;
size_t queued_reply_bytes;
struct bufferevent *read_event;
struct bufferevent *write_event;
@@ -132,6 +133,9 @@ struct control_state {
/* Maximum age for clients that are not using pause mode. */
#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. */
#define CONTROL_IGNORE_FLAGS \
(CLIENT_CONTROL_NOOUTPUT| \
@@ -165,6 +169,15 @@ RB_GENERATE_STATIC(control_windows, control_window, entry, control_window_cmp);
static void
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);
TAILQ_REMOVE(&cs->all_blocks, cb, all_entry);
free(cb);
@@ -388,16 +401,71 @@ control_pause_pane(struct client *c, struct window_pane *wp)
}
}
/*
* Reset a pane after its buffer has been replaced: drop any output still
* queued from the old buffer and start again from the pane's own offset.
*/
void
control_reset_pane(struct client *c, struct window_pane *wp)
{
struct control_pane *cp;
if (c->control_state == NULL)
return;
cp = control_get_pane(c, wp);
if (cp == NULL)
return;
control_discard_pane(c, cp);
memcpy(&cp->offset, &wp->offset, sizeof cp->offset);
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. */
static void
control_write_line(struct client *c, char *line)
{
struct control_state *cs = c->control_state;
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)) {
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_enable(cs->write_event, EV_WRITE);
free(line);
@@ -407,6 +475,7 @@ control_write_line(struct client *c, char *line)
cb = xcalloc(1, sizeof *cb);
cb->line = line;
TAILQ_INSERT_TAIL(&cs->all_blocks, cb, all_entry);
cs->queued_reply_bytes += size;
cb->t = get_timer();
log_debug("%s: %s: storing line: %s", __func__, c->name, cb->line);
@@ -436,8 +505,12 @@ control_flush_deferred(struct client *c)
void
control_write(struct client *c, const char *fmt, ...)
{
va_list ap;
char *line;
struct control_state *cs = c->control_state;
char *line;
va_list ap;
if (cs == NULL)
return;
va_start(ap, fmt);
xvasprintf(&line, fmt, ap);
@@ -460,6 +533,9 @@ control_write_guard(struct client *c, const char *guard, long t, u_int number,
struct control_state *cs = c->control_state;
char *line;
if (cs == NULL)
return;
if (strcmp(guard, "begin") == 0)
cs->guard_depth++;
@@ -484,6 +560,9 @@ control_notify_write(struct client *c, const char *fmt, ...)
va_list ap;
char *line;
if (cs == NULL)
return;
va_start(ap, fmt);
xvasprintf(&line, fmt, ap);
va_end(ap);
@@ -978,6 +1057,7 @@ control_discard_all(struct client *c)
control_discard(c);
TAILQ_FOREACH_SAFE(cb, &cs->all_blocks, all_entry, cb1)
control_free_block(cs, cb);
cs->queued_reply_bytes = 0;
bufferevent_disable(cs->write_event, EV_WRITE);
}

109
format.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: format.c,v 1.412 2026/08/05 07:31:08 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>
@@ -855,26 +855,42 @@ format_cb_window_active_clients_list(struct format_tree *ft)
static void *
format_cb_window_layout(struct format_tree *ft)
{
struct window *w = ft->w;
struct client *c = ft->client;
struct window *w = ft->w;
struct layout_cell *lcroot;
int flags = 0;
if (w == NULL)
return (NULL);
if (w->saved_layout_root != NULL)
return (layout_dump(w, w->saved_layout_root));
return (layout_dump(w, w->layout_root));
lcroot = w->saved_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. */
static void *
format_cb_window_visible_layout(struct format_tree *ft)
{
struct client *c = ft->client;
struct window *w = ft->w;
int flags = 0;
if (w == 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. */
@@ -1887,6 +1903,37 @@ format_cb_cursor_blinking(struct format_tree *ft)
return (NULL);
}
/* Callback for history_added. */
static void *
format_cb_history_added(struct format_tree *ft)
{
if (ft->wp != NULL)
return (format_printf("%u", ft->wp->base.grid->scroll_added));
return (NULL);
}
/* Callback for history_collected. */
static void *
format_cb_history_collected(struct format_tree *ft)
{
struct window_pane *wp = ft->wp;
if (wp != NULL)
return (format_printf("%u", wp->base.grid->scroll_collected));
return (NULL);
}
/* Callback for history_generation. */
static void *
format_cb_history_generation(struct format_tree *ft)
{
struct window_pane *wp = ft->wp;
if (wp != NULL)
return (format_printf("%u", wp->base.grid->scroll_generation));
return (NULL);
}
/* Callback for history_limit. */
static void *
format_cb_history_limit(struct format_tree *ft)
@@ -2276,6 +2323,19 @@ format_cb_pane_last_output_time(struct format_tree *ft)
return (NULL);
}
/* Callback for pane_output_generation. */
static void *
format_cb_pane_output_generation(struct format_tree *ft)
{
unsigned long long value;
if (ft->wp != NULL) {
value = ft->wp->output_generation;
return (format_printf("%llu", value));
}
return (NULL);
}
/* Callback for pane_last_prompt_time. */
static void *
format_cb_pane_last_prompt_time(struct format_tree *ft)
@@ -2816,10 +2876,10 @@ format_cb_session_activity_flag(struct format_tree *ft)
if (ft->s != NULL) {
RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (ft->wl->flags & WINLINK_ACTIVITY)
if (wl->flags & WINLINK_ACTIVITY)
return (xstrdup("1"));
return (xstrdup("0"));
}
return (xstrdup("0"));
}
return (NULL);
}
@@ -2834,8 +2894,8 @@ format_cb_session_bell_flag(struct format_tree *ft)
RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (wl->flags & WINLINK_BELL)
return (xstrdup("1"));
return (xstrdup("0"));
}
return (xstrdup("0"));
}
return (NULL);
}
@@ -2848,10 +2908,10 @@ format_cb_session_silence_flag(struct format_tree *ft)
if (ft->s != NULL) {
RB_FOREACH(wl, winlinks, &ft->s->windows) {
if (ft->wl->flags & WINLINK_SILENCE)
if (wl->flags & WINLINK_SILENCE)
return (xstrdup("1"));
return (xstrdup("0"));
}
return (xstrdup("0"));
}
return (NULL);
}
@@ -3018,17 +3078,6 @@ format_cb_sixel_support(__unused struct format_tree *ft)
#endif
}
/* Callback for image_support. */
static void *
format_cb_image_support(__unused struct format_tree *ft)
{
#ifdef ENABLE_IMAGES
return (xstrdup("1"));
#else
return (xstrdup("0"));
#endif
}
/* Callback for active_window_index. */
static void *
format_cb_active_window_index(struct format_tree *ft)
@@ -3674,12 +3723,21 @@ static const struct format_table_entry format_table[] = {
{ "cursor_y", FORMAT_TABLE_STRING,
format_cb_cursor_y
},
{ "history_added", FORMAT_TABLE_STRING,
format_cb_history_added
},
{ "history_all_bytes", FORMAT_TABLE_STRING,
format_cb_history_all_bytes
},
{ "history_bytes", FORMAT_TABLE_STRING,
format_cb_history_bytes
},
{ "history_collected", FORMAT_TABLE_STRING,
format_cb_history_collected
},
{ "history_generation", FORMAT_TABLE_STRING,
format_cb_history_generation
},
{ "history_limit", FORMAT_TABLE_STRING,
format_cb_history_limit
},
@@ -3692,9 +3750,6 @@ static const struct format_table_entry format_table[] = {
{ "host_short", FORMAT_TABLE_STRING,
format_cb_host_short
},
{ "image_support", FORMAT_TABLE_STRING,
format_cb_image_support
},
{ "insert_flag", FORMAT_TABLE_STRING,
format_cb_insert_flag
},
@@ -3863,6 +3918,9 @@ static const struct format_table_entry format_table[] = {
{ "pane_mode", FORMAT_TABLE_STRING,
format_cb_pane_mode
},
{ "pane_output_generation", FORMAT_TABLE_STRING,
format_cb_pane_output_generation
},
{ "pane_path", FORMAT_TABLE_STRING,
format_cb_pane_path
},
@@ -4401,7 +4459,7 @@ format_quote_shell(const char *s)
at = out = xmalloc(strlen(s) * 2 + 1);
for (cp = s; *cp != '\0'; cp++) {
if (strchr("|&;<>()$`\\\"'*?[# =%\n\t", *cp) != NULL)
if (strchr("|&;<>(){}$`\\\"'*?[# =%\n\t", *cp) != NULL)
*at++ = '\\';
*at++ = *cp;
}
@@ -5192,6 +5250,7 @@ format_loop_sessions(struct format_expand_state *es, const char *fmt)
format_log(es, "session loop: $%u", s->id);
if (active != NULL &&
ft->c != NULL &&
ft->c->session != NULL &&
s->id == ft->c->session->id)
use = active;
else

View File

@@ -44,7 +44,7 @@ LLVMFuzzerTestOneInput(const u_char *data, size_t size)
w = window_create(PANE_WIDTH, PANE_HEIGHT, 0, 0);
wp = window_add_pane(w, NULL, 0, 0);
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__);
wp->fd = open("/dev/null", O_WRONLY);

120
grid.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: grid.c,v 1.156 2026/08/03 12:58:53 nicm Exp $ */
/* $OpenBSD: grid.c,v 1.158 2026/09/01 12:49:49 nicm Exp $ */
/*
* Copyright (c) 2008 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -86,9 +86,6 @@ grid_check_is_clear(struct grid *gd)
assert(gl->extdsize == 0);
assert(gl->flags == 0);
assert(gl->time == 0);
#ifdef ENABLE_IMAGES
assert(gl->images == NULL);
#endif
}
}
#else
@@ -356,9 +353,6 @@ grid_free_line(struct grid *gd, u_int py)
assert(gl->cellsize == 0 || gl->celldata != NULL);
#endif
#ifdef ENABLE_IMAGES
image_grid_free_line(gd, gl);
#endif
free(gl->celldata);
free(gl->extddata);
memset(gl, 0, sizeof *gl);
@@ -403,9 +397,6 @@ void
grid_destroy(struct grid *gd)
{
grid_free_lines(gd, 0, gd->hsize + gd->sy);
#ifdef ENABLE_IMAGES
image_grid_free(gd);
#endif
free(gd->linedata);
free(gd);
}
@@ -738,9 +729,6 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
if (nx == 0 || ny == 0)
return;
#ifdef ENABLE_IMAGES
image_grid_damage(gd, px, py, nx, ny);
#endif
if (px == 0 && nx == gd->sx) {
grid_clear_lines(gd, py, ny, bg);
@@ -776,10 +764,6 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
void
grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
{
struct grid_line *gl;
#ifdef ENABLE_IMAGES
struct image_line *images;
#endif
u_int yy;
if (ny == 0)
@@ -791,18 +775,8 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
return;
for (yy = py; yy < py + ny; yy++) {
gl = &gd->linedata[yy];
#ifdef ENABLE_IMAGES
image_grid_damage(gd, 0, yy, gd->sx, 1);
images = gl->images;
#endif
free(gl->celldata);
free(gl->extddata);
memset(gl, 0, sizeof *gl);
grid_free_line(gd, yy);
grid_empty_line(gd, yy, bg);
#ifdef ENABLE_IMAGES
gl->images = images;
#endif
}
if (py != 0)
gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED;
@@ -867,9 +841,6 @@ grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx,
grid_expand_line(gd, py, px + nx, 8);
grid_expand_line(gd, py, dx + nx, 8);
#ifdef ENABLE_IMAGES
image_grid_move_cells(gd, dx, px, py, nx);
#endif
memmove(&gl->celldata[dx], &gl->celldata[px],
nx * sizeof *gl->celldata);
if (dx + nx > gl->cellused)
@@ -1242,6 +1213,7 @@ grid_string_cells(struct grid *gd, u_int px, u_int py, u_int nx,
grid_get_cell(gd, xx, py, &gc);
if (gc.flags & GRID_FLAG_PADDING)
continue;
if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) {
grid_string_cells_code(*lastgc, &gc, code, sizeof code,
flags, s, &has_link);
@@ -1308,18 +1280,11 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
{
struct grid_line *dstl, *srcl;
u_int yy;
#ifdef ENABLE_IMAGES
u_int original_dy, original_sy;
#endif
if (dy + ny > dst->hsize + dst->sy)
ny = dst->hsize + dst->sy - dy;
if (sy + ny > src->hsize + src->sy)
ny = src->hsize + src->sy - sy;
#ifdef ENABLE_IMAGES
original_dy = dy;
original_sy = sy;
#endif
grid_free_lines(dst, dy, ny);
for (yy = 0; yy < ny; yy++) {
@@ -1327,9 +1292,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
dstl = &dst->linedata[dy];
memcpy(dstl, srcl, sizeof *dstl);
#ifdef ENABLE_IMAGES
dstl->images = NULL;
#endif
if (srcl->cellsize != 0) {
dstl->celldata = xreallocarray(NULL,
srcl->cellsize, sizeof *dstl->celldata);
@@ -1349,9 +1311,6 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
sy++;
dy++;
}
#ifdef ENABLE_IMAGES
image_grid_duplicate_lines(dst, original_dy, src, original_sy, ny);
#endif
}
/* Mark line as dead. */
@@ -1362,30 +1321,6 @@ grid_reflow_dead(struct grid_line *gl)
gl->flags = GRID_LINE_DEAD;
}
/* Image rows are cell-aligned and must not be reflowed like text. */
static int
grid_reflow_has_image(struct grid_line *gl)
{
struct grid_cell gc;
u_int i;
#ifdef ENABLE_IMAGES
if (image_grid_line_has_images(gl))
return (1);
#endif
if (~gl->flags & GRID_LINE_EXTENDED)
return (0);
for (i = 0; i < gl->cellused; i++) {
grid_get_cell1(gl, i, &gc);
/* Kitty Unicode placeholder base character (U+10EEEE). */
if (gc.data.size >= 4 && gc.data.data[0] == 0xf4 &&
gc.data.data[1] == 0x8e && gc.data.data[2] == 0xbb &&
gc.data.data[3] == 0xae)
return (1);
}
return (0);
}
/* Add lines, return the first new one. */
static struct grid_line *
grid_reflow_add(struct grid *gd, u_int n)
@@ -1448,10 +1383,6 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
break;
line = yy + 1 + lines;
/* Do not join wrapped text to a cell-aligned image row. */
if (grid_reflow_has_image(&gd->linedata[line]))
break;
/* If the next line is empty, skip it. */
if (~gd->linedata[line].flags & GRID_LINE_WRAPPED)
wrapped = 0;
@@ -1512,7 +1443,8 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
/* Remove the lines that were completely consumed. */
for (i = yy + 1; i < yy + 1 + lines; i++) {
grid_free_line(gd, i);
free(gd->linedata[i].celldata);
free(gd->linedata[i].extddata);
grid_reflow_dead(&gd->linedata[i]);
}
@@ -1614,12 +1546,6 @@ grid_reflow(struct grid *gd, u_int sx)
if (gl->flags & GRID_LINE_DEAD)
continue;
/* Keep image layers at their original cell coordinates. */
if (grid_reflow_has_image(gl)) {
grid_reflow_move(target, gl);
continue;
}
/*
* Work out the width of this line. at is the point at which
* the available width is hit, and width is the full line
@@ -1709,7 +1635,7 @@ grid_wrap_position(struct grid *gd, u_int px, u_int py, u_int *wx, u_int *wy)
void
grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy)
{
u_int yy, ay = 0;
u_int yy, ay = 0, ey = gd->hsize + gd->sy - 1;
for (yy = 0; yy < gd->hsize + gd->sy - 1; yy++) {
if (ay == wy)
@@ -1723,7 +1649,7 @@ grid_unwrap_position(struct grid *gd, u_int *px, u_int *py, u_int wx, u_int wy)
* until we find the end or the line now containing wx.
*/
if (wx == UINT_MAX) {
while (gd->linedata[yy].flags & GRID_LINE_WRAPPED)
while (yy < ey && gd->linedata[yy].flags & GRID_LINE_WRAPPED)
yy++;
wx = gd->linedata[yy].cellused;
} else {
@@ -1785,21 +1711,29 @@ grid_in_set(struct grid *gd, u_int px, u_int py, const char *set)
{
struct grid_cell gc, tmp_gc;
u_int pxx;
int has_tab, has_space;
has_tab = (strchr(set, '\t') != NULL);
has_space = (strchr(set, ' ') != NULL);
grid_get_cell(gd, px, py, &gc);
if (strchr(set, '\t')) {
if (gc.flags & GRID_FLAG_PADDING) {
pxx = px;
do
grid_get_cell(gd, --pxx, py, &tmp_gc);
while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING);
if (tmp_gc.flags & GRID_FLAG_TAB)
return (tmp_gc.data.width - (px - pxx));
} else if (gc.flags & GRID_FLAG_TAB)
return (gc.data.width);
}
if (gc.flags & GRID_FLAG_PADDING)
if (gc.flags & GRID_FLAG_PADDING) {
if (!has_tab && !has_space)
return (0);
pxx = px;
do
grid_get_cell(gd, --pxx, py, &tmp_gc);
while (pxx > 0 && tmp_gc.flags & GRID_FLAG_PADDING);
if (((has_tab || has_space) &&
(tmp_gc.flags & GRID_FLAG_TAB)) ||
(has_space && utf8_has_whitespace(&tmp_gc.data)))
return (tmp_gc.data.width - (px - pxx));
return (0);
}
if ((has_tab || has_space) && (gc.flags & GRID_FLAG_TAB))
return (gc.data.width);
if (has_space && utf8_has_whitespace(&gc.data))
return (gc.data.width == 0 ? 1 : gc.data.width);
return (utf8_cstrhas(set, &gc.data));
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1600
image.c

File diff suppressed because it is too large Load Diff

124
input.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.269 2026/07/20 11:16:33 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>
@@ -101,7 +101,6 @@ struct input_ctx {
struct bufferevent *event;
struct screen_write_ctx ctx;
struct colour_palette *palette;
struct client *c;
struct input_cell cell;
struct input_cell old_cell;
@@ -145,9 +144,6 @@ struct input_ctx {
*/
struct evbuffer *since_ground;
struct event ground_timer;
#ifdef ENABLE_IMAGES
void *kitty_state;
#endif
};
/* Helper functions. */
@@ -186,9 +182,6 @@ static void input_enter_osc(struct input_ctx *);
static void input_exit_osc(struct input_ctx *);
static void input_enter_apc(struct input_ctx *);
static void input_exit_apc(struct input_ctx *);
#ifdef ENABLE_IMAGES
static int input_handle_kitty(struct input_ctx *, const u_char *, size_t);
#endif
static void input_enter_rename(struct input_ctx *);
static void input_exit_rename(struct input_ctx *);
@@ -880,7 +873,7 @@ input_restore_state(struct input_ctx *ictx)
/* Initialise input parser. */
struct input_ctx *
input_init(struct window_pane *wp, struct bufferevent *bev,
struct colour_palette *palette, struct client *c)
struct colour_palette *palette)
{
struct input_ctx *ictx;
@@ -888,7 +881,6 @@ input_init(struct window_pane *wp, struct bufferevent *bev,
ictx->wp = wp;
ictx->event = bev;
ictx->palette = palette;
ictx->c = c;
ictx->input_space = INPUT_BUF_START;
ictx->input_buf = xmalloc(INPUT_BUF_START);
@@ -922,9 +914,6 @@ input_free(struct input_ctx *ictx)
event_del(&ictx->request_timer);
free(ictx->input_buf);
#ifdef ENABLE_IMAGES
kitty_free_state(ictx->kitty_state);
#endif
evbuffer_free(ictx->since_ground);
event_del(&ictx->ground_timer);
@@ -947,7 +936,6 @@ input_reset(struct input_ctx *ictx, int clear)
screen_write_start_pane(sctx, wp, &wp->base);
else
screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
screen_write_reset(sctx);
screen_write_stop(sctx);
}
@@ -1059,6 +1047,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
if (len == 0)
return;
wp->output_generation++;
window_update_activity(wp->window);
if (~wp->flags & PANE_ACTIVITY) {
wp->flags |= PANE_ACTIVITY;
@@ -1075,7 +1064,6 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
screen_write_start_pane(sctx, wp, &wp->base);
else
screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id,
ictx->state->name, len, (int)len, buf);
@@ -1095,7 +1083,6 @@ input_parse_screen(struct input_ctx *ictx, struct screen *s,
return;
screen_write_start_callback(sctx, s, cb, arg);
sctx->flags |= SCREEN_WRITE_INPUT;
input_parse(ictx, buf, len);
screen_write_stop(sctx);
}
@@ -1595,7 +1582,7 @@ input_csi_dispatch(struct input_ctx *ictx)
case -1:
break;
case 0:
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
input_reply(ictx, 1, "\033[?1;2;4c");
#else
input_reply(ictx, 1, "\033[?1;2c");
@@ -1990,7 +1977,7 @@ input_csi_dispatch_rm_private(struct input_ctx *ictx)
screen_write_mode_clear(sctx, MODE_BRACKETPASTE);
break;
case 2026:
screen_write_stop_sync(ictx->wp);
screen_write_end_sync(sctx);
break;
case 2031:
screen_write_mode_clear(sctx, MODE_THEME_UPDATES);
@@ -2113,7 +2100,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx)
static void
input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx)
{
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
int n, m, o;
if (ictx->param_list_len > 3)
@@ -2638,10 +2625,10 @@ input_dcs_dispatch(struct input_ctx *ictx)
const char prefix[] = "tmux;";
const u_int prefixlen = (sizeof prefix) - 1;
long long allow_passthrough = 0;
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
struct window *w;
struct sixel_image *si;
int p1, p2;
int p2;
#endif
if (wp == NULL)
@@ -2654,18 +2641,15 @@ input_dcs_dispatch(struct input_ctx *ictx)
return (0);
}
#ifdef ENABLE_IMAGES
#ifdef ENABLE_SIXEL
if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) {
w = wp->window;
if (input_split(ictx) != 0)
return (0);
p1 = input_get(ictx, 0, 0, 0);
if (p1 == -1)
p1 = 0;
p2 = input_get(ictx, 1, 0, 0);
if (p2 == -1)
p2 = 0;
si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel);
si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel);
if (si != NULL)
screen_write_sixelimage(sctx, si, ictx->cell.cell.bg);
}
@@ -2804,57 +2788,6 @@ input_enter_apc(struct input_ctx *ictx)
}
/* APC terminator (ST) received. */
#ifdef ENABLE_IMAGES
static int
input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
{
struct screen_write_ctx *sctx = &ictx->ctx;
struct window_pane *wp = ictx->wp;
struct image *im;
u_int image_id = 0, replace_id = 0, quiet = 0;
u_int placement_id = 0;
int32_t z = 0;
char action = '\0', delete = '\0';
int status;
if (wp == NULL)
return (0);
im = kitty_parse_image(&ictx->kitty_state, buf, len,
wp->window->xpixel,
wp->window->ypixel, &image_id, &replace_id, &quiet, &action,
&delete, &placement_id, &z, &status);
if (status == KITTY_PARSE_MORE)
return (1);
if (status != KITTY_PARSE_OK) {
if (quiet < 2 && action != '\0') {
if (status == KITTY_PARSE_MISSING)
input_reply(ictx, 0, "\033_Gi=%u;ENOENT\033\\",
image_id);
else
input_reply(ictx, 0, "\033_Gi=%u;EINVAL\033\\",
image_id);
}
return (1);
}
if (replace_id != 0)
image_clear(sctx, replace_id);
if (im != NULL) {
if (action == 'd')
image_clear_kitty(sctx, delete, image_id, placement_id, z);
else
image_write_kitty(sctx, im, ictx->cell.cell.bg, image_id,
placement_id, z);
image_free(image_get_id(im));
if (quiet == 0 && image_id != 0)
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
} else if (action == 'd')
image_clear_kitty(sctx, delete, image_id, placement_id, z);
else if ((action == 't' || action == 'q' || action == 'u') && quiet == 0)
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
return (1);
}
#endif
static void
input_exit_apc(struct input_ctx *ictx)
{
@@ -2865,13 +2798,6 @@ input_exit_apc(struct input_ctx *ictx)
return;
log_debug("%s: \"%s\"", __func__, ictx->input_buf);
#ifdef ENABLE_IMAGES
if (ictx->input_len > 1 && ictx->input_buf[0] == 'G' &&
input_handle_kitty(ictx, ictx->input_buf + 1,
ictx->input_len - 1))
return;
#endif
if (wp != NULL &&
options_get_number(wp->options, "allow-set-title") &&
screen_set_title(sctx->s, ictx->input_buf, 1)) {
@@ -2932,12 +2858,6 @@ input_top_bit_set(struct input_ctx *ictx)
{
struct screen_write_ctx *sctx = &ictx->ctx;
struct utf8_data *ud = &ictx->utf8data;
#ifdef ENABLE_IMAGES
struct grid_cell gc;
struct image *im;
u_int x, source_x, source_y, image_id, placement_id;
int32_t z;
#endif
ictx->flags &= ~INPUT_LAST;
@@ -2965,20 +2885,6 @@ input_top_bit_set(struct input_ctx *ictx)
utf8_copy(&ictx->cell.cell.data, ud);
screen_write_collect_add(sctx, &ictx->cell.cell);
#ifdef ENABLE_IMAGES
if (sctx->s->cx != 0) {
x = sctx->s->cx - 1; /* cx-1 is the cell just written. */
grid_view_get_cell(sctx->s->grid, x, sctx->s->cy, &gc);
if (kitty_placeholder_to_image(ictx->kitty_state,
sctx->s->grid, &gc, x, sctx->s->cy, &im, &source_x,
&source_y, &image_id, &placement_id, &z)) {
grid_view_set_cell(sctx->s->grid, x, sctx->s->cy, &gc);
image_place_cell_kitty(sctx, im, x, sctx->s->cy, source_x,
source_y, image_id, placement_id, z);
}
}
#endif
utf8_copy(&ictx->last, &ictx->cell.cell.data);
ictx->flags |= INPUT_LAST;
@@ -3517,15 +3423,9 @@ input_osc_52(struct input_ctx *ictx, const char *p)
return;
if (wp == NULL) {
/* Popup window. */
if (ictx->c == NULL) {
free(out);
return;
}
tty_set_selection(&ictx->c->tty, clip, out, outlen);
paste_add(NULL, out, outlen);
free(out);
return;
} else {
/* Normal window. */
screen_write_start_pane(&ctx, wp, NULL);
screen_write_setselection(&ctx, clip, out, outlen);
screen_write_stop(&ctx);

1001
json.c Normal file

File diff suppressed because it is too large Load Diff

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>
@@ -523,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 } }",
/* Mouse button 1 on border. */
"bind -n MouseDown1Border { select-pane -M }",
"bind -n MouseDown1Border { select-pane -t= }",
/* Mouse button 1 drag on border. */
"bind -n MouseDrag1Border { resize-pane -M }",

File diff suppressed because it is too large Load Diff

152
layout.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: layout.c,v 1.96 2026/07/15 13:02:33 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>
@@ -106,7 +106,7 @@ layout_free_cell(struct layout_cell *lc, int only_nodes)
}
break;
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 = NULL;
}
@@ -233,29 +233,6 @@ layout_make_node(struct layout_cell *lc, enum layout_type type)
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
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;
}
static int
int
layout_cell_has_tiled_child(struct layout_cell *lc)
{
struct layout_cell *lcchild;
@@ -509,18 +486,20 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
/* Count the number of available cells in a layout. */
u_int
layout_count_cells(struct layout_cell *lc)
layout_count_cells(struct layout_cell *lc, int with_floating)
{
struct layout_cell *lcchild;
u_int count = 0;
switch (lc->type) {
case LAYOUT_WINDOWPANE:
if (lc->flags & LAYOUT_CELL_FLOATING && !with_floating)
return 0;
return (1);
case LAYOUT_LEFTRIGHT:
case LAYOUT_TOPBOTTOM:
TAILQ_FOREACH(lcchild, &lc->cells, entry)
count += layout_count_cells(lcchild);
count += layout_count_cells(lcchild, with_floating);
return (count);
default:
fatalx("bad layout type");
@@ -539,6 +518,10 @@ layout_resize_check(struct window *w, struct layout_cell *lc,
status = window_get_pane_status(w);
/* Floating cells do not take space from the tiled layout. */
if (!layout_cell_is_tiled(lc) && !layout_cell_has_tiled_child(lc))
return (0);
if (lc->type == LAYOUT_WINDOWPANE) {
/* Space available in this cell only. */
if (type == LAYOUT_LEFTRIGHT) {
@@ -568,6 +551,9 @@ layout_resize_check(struct window *w, struct layout_cell *lc,
/* Different type: minimum of available space in child cells. */
minimum = UINT_MAX;
TAILQ_FOREACH(lcchild, &lc->cells, entry) {
if (!layout_cell_is_tiled(lcchild) &&
!layout_cell_has_tiled_child(lcchild))
continue;
available = layout_resize_check(w, lcchild, type);
if (available < minimum)
minimum = available;
@@ -661,7 +647,7 @@ layout_resize_set_size(struct window *w, struct layout_cell *lc,
/* Find and return the nearest neighbour to a cell in a specific direction. */
static struct layout_cell *
layout_cell_get_neighbour_direction(struct layout_cell *lc, int direction)
layout_cell_get_neighbour_dir(struct layout_cell *lc, int direction)
{
struct layout_cell *lcn = lc;
@@ -695,9 +681,9 @@ layout_cell_get_neighbour(struct layout_cell *lc)
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells))
direction = !direction;
lcother = layout_cell_get_neighbour_direction(lc, direction);
lcother = layout_cell_get_neighbour_dir(lc, direction);
if (lcother == NULL)
lcother = layout_cell_get_neighbour_direction(lc, !direction);
lcother = layout_cell_get_neighbour_dir(lc, !direction);
return (lcother);
}
@@ -714,7 +700,7 @@ layout_destroy_cell(struct window *w, struct layout_cell *lc,
/* If no parent, this is the last pane in a window. */
lcparent = lc->parent;
if (lcparent == NULL) {
if (lc->wp != NULL)
if (*lcroot == lc)
*lcroot = NULL;
layout_free_cell(lc, 0);
return;
@@ -782,6 +768,49 @@ layout_free(struct window *w, int 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. */
void
layout_resize(struct window *w, u_int sx, u_int sy)
@@ -802,8 +831,11 @@ layout_resize(struct window *w, u_int sx, u_int sy)
* out proportionately - this should leave the layout fitting the new
* 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;
}
xchange = sx - lc->g.sx;
xlimit = layout_resize_check(w, lc, LAYOUT_LEFTRIGHT);
if (xchange < 0 && xchange < -xlimit)
@@ -833,6 +865,7 @@ layout_resize(struct window *w, u_int sx, u_int sy)
/* Fix cell offsets. */
layout_fix_offsets(w);
layout_clamp_floating_panes(w, sx, sy);
layout_fix_panes(w, NULL);
}
@@ -860,7 +893,7 @@ layout_resize_pane_to(struct window_pane *wp, enum layout_type type,
size = lc->g.sx;
else
size = lc->g.sy;
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells))
if (layout_cell_is_last_tiled(lc))
change = size - new_size;
else
change = new_size - size;
@@ -985,11 +1018,11 @@ layout_resize_pane(struct window_pane *wp, enum layout_type type, int change,
if (lcparent == NULL)
return;
/* If this is the last cell, move back one. */
if (lc == TAILQ_LAST(&lcparent->cells, layout_cells)) {
do
lc = TAILQ_PREV(lc, layout_cells, entry);
while (lc->flags & LAYOUT_CELL_FLOATING);
/* If this is the last tiled cell, move back one. */
if (layout_cell_is_last_tiled(lc)) {
lc = layout_cell_get_neighbour_dir(lc, 0);
if (lc == NULL)
return;
}
layout_resize_layout(wp->window, lc, type, change, opposite);
@@ -1007,22 +1040,22 @@ layout_resize_pane_grow(struct window *w, struct layout_cell *lc,
lcadd = lc;
/* Look towards the tail for a suitable cell for reduction. */
lcremove = TAILQ_NEXT(lc, entry);
lcremove = layout_cell_get_neighbour_dir(lc, 1);
while (lcremove != NULL) {
size = layout_resize_check(w, lcremove, type);
if (size > 0)
break;
lcremove = TAILQ_NEXT(lcremove, entry);
lcremove = layout_cell_get_neighbour_dir(lcremove, 1);
}
/* If none found, look towards the head. */
if (opposite && lcremove == NULL) {
lcremove = TAILQ_PREV(lc, layout_cells, entry);
lcremove = layout_cell_get_neighbour_dir(lc, 0);
while (lcremove != NULL) {
size = layout_resize_check(w, lcremove, type);
if (size > 0)
break;
lcremove = TAILQ_PREV(lcremove, layout_cells, entry);
lcremove = layout_cell_get_neighbour_dir(lcremove, 0);
}
}
if (lcremove == NULL)
@@ -1050,13 +1083,13 @@ layout_resize_pane_shrink(struct window *w, struct layout_cell *lc,
size = layout_resize_check(w, lcremove, type);
if (size != 0)
break;
lcremove = TAILQ_PREV(lcremove, layout_cells, entry);
lcremove = layout_cell_get_neighbour_dir(lcremove, 0);
} while (lcremove != NULL);
if (lcremove == NULL)
return (0);
/* And add onto the next cell (from the original cell). */
lcadd = TAILQ_NEXT(lc, entry);
lcadd = layout_cell_get_neighbour_dir(lc, 1);
if (lcadd == NULL)
return (0);
@@ -1650,7 +1683,10 @@ layout_get_tiled_cell(struct cmdq_item *item, struct args *args,
return (NULL);
}
window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM));
if (window_active_pane_is_over_zoom(w))
window_push_zoom(w, 0, 1);
else
window_push_zoom(w, 1, (flags & SPAWN_ZOOM));
lc = layout_split_pane(wp, type, size, flags);
if (lc == NULL)
*cause = xstrdup("no space for a new pane");
@@ -1677,8 +1713,10 @@ layout_get_floating_cell(struct cmdq_item *item, struct args *args,
return (NULL);
}
if (flags & SPAWN_MODAL)
window_push_modal_zoom(w);
if (flags & SPAWN_FLOATOVERZOOM)
window_push_zoom(wp->window, 0, 1);
else if (window_active_pane_is_over_zoom(w))
window_push_zoom(wp->window, 0, 1);
else
window_push_zoom(wp->window, 1, (flags & SPAWN_ZOOM));
lcnew = layout_floating_pane(w, wp, &fg);
@@ -1690,7 +1728,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
enum pane_lines lines, struct window *w, struct layout_geometry *lg,
char **cause)
{
int sx, sy, ox, oy;
int sx, sy, ox, oy, pad;
char *error = NULL;
sx = lg->sx == UINT_MAX ? w->sx / 2 : lg->sx;
@@ -1739,12 +1777,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 (w->last_new_pane_x == 0)
ox = 4;
else {
if (lines != PANE_LINES_NONE)
pad = 1;
else
pad = 0;
ox = w->last_new_pane_x + 4;
if (w->last_new_pane_x > w->sx)
if (ox + sx + pad > (int)w->sx)
ox = 4;
}
w->last_new_pane_x = ox;
@@ -1755,8 +1801,12 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
if (w->last_new_pane_y == 0)
oy = 2;
else {
if (lines != PANE_LINES_NONE)
pad = 1;
else
pad = 0;
oy = w->last_new_pane_y + 2;
if (w->last_new_pane_y > w->sy)
if (oy + sy + pad > (int)w->sy)
oy = 2;
}
w->last_new_pane_y = oy;

59
menu.c
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>
@@ -51,6 +51,18 @@ struct menu_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
menu_add_items(struct menu *menu, const struct menu_item *items,
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);
if (*new_item->name == '-')
width--;
if (width > menu->item_width)
menu->item_width = width;
if (width > menu->width)
menu->width = width;
}
@@ -238,7 +252,7 @@ menu_update(struct menu_data *md)
screen_write_clearscreen(&ctx, 8);
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);
}
@@ -286,11 +300,13 @@ menu_destroy(struct window *w)
void
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)
*cy = md->py;
else
*cy = md->py + 1 + md->choice;
*cy = md->py + border + md->choice;
}
struct screen *
@@ -302,13 +318,19 @@ menu_screen(struct menu_data *md)
u_int
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
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
@@ -338,6 +360,7 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
enum cmd_parse_status status;
char *error;
key_code key;
u_int border;
if (KEYC_IS_MOUSE(event->key)) {
/*
@@ -353,10 +376,10 @@ menu_key(struct client *c, struct menu_data *md, struct key_event *event)
return (1);
return (0);
}
if (m->x < md->px ||
m->x > md->px + 4 + menu->width ||
m->y < md->py + 1 ||
m->y > md->py + 1 + n - 1) {
border = (md->border_lines != BOX_LINES_NONE);
if (m->x < md->px || m->x >= md->px + menu_width(md) ||
m->y < md->py + border ||
m->y >= md->py + border + n) {
if (~md->flags & MENU_STAYOPEN) {
if (!move && MOUSE_RELEASE(m->b))
return (1);
@@ -379,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))
goto chosen;
}
md->choice = m->y - (md->py + 1);
md->choice = m->y - (md->py + border);
if (md->choice != old)
server_redraw_window_menu(md->w);
return (0);
@@ -546,20 +569,16 @@ menu_resize(struct menu_data *md, struct window *w)
if (md == NULL)
return;
nx = md->px;
ny = md->py;
sx = md->menu->width + 4;
sy = md->menu->count + 2;
menu_get_size(md->menu, md->border_lines, &sx, &sy);
if (nx + sx > w->sx) {
if (w->sx <= sx)
nx = 0;
else
nx = w->sx - sx;
}
if (ny + sy > w->sy) {
if (w->sy <= sy)
ny = 0;
@@ -591,8 +610,9 @@ menu_display(struct menu *menu, int flags, int starting_choice,
w = fs->w;
o = w->options;
sx = menu->width + 4;
sy = menu->count + 2;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "menu-border-lines");
menu_get_size(menu, lines, &sx, &sy);
if (sx >= w->sx)
px = 0;
else if (px + sx > w->sx)
@@ -604,9 +624,6 @@ menu_display(struct menu *menu, int flags, int starting_choice,
w->menu_last_px = px;
w->menu_last_py = py;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "menu-border-lines");
md = xcalloc(1, sizeof *md);
md->w = w;
md->flags = flags;

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.102 2026/09/21 12:14:32 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1375,12 +1375,14 @@ static void
mode_tree_display_menu(struct mode_tree_data *mtd, struct client *c, u_int x,
u_int y, int outside)
{
struct window *w = mtd->wp->window;
struct mode_tree_item *mti;
struct menu *menu;
const struct menu_item *items;
struct mode_tree_menu *mtm;
char *title;
u_int line;
enum box_lines lines;
u_int line, sx, sy;
if (mtd->offset + y > mtd->line_size - 1)
line = mtd->current;
@@ -1405,14 +1407,17 @@ mode_tree_display_menu(struct mode_tree_data *mtd, struct client *c, u_int x,
mtm->line = line;
mtd->references++;
if (x >= (menu->width + 4) / 2)
x -= (menu->width + 4) / 2;
lines = options_get_number(w->options, "menu-border-lines");
menu_get_size(menu, lines, &sx, &sy);
if (x >= sx / 2)
x -= sx / 2;
else
x = 0;
x += mtd->wp->xoff;
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);
free(mtm);
menu_free(menu);

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>
@@ -614,8 +614,10 @@ monitor_parse(const char *value, char **name, enum monitor_type *type, int *id,
*type = MONITOR_ALL_WINDOWS;
else if (sscanf(what, "@%d", id) == 1 && *id >= 0)
*type = MONITOR_WINDOW;
else
else if (*what == '\0')
*type = MONITOR_SESSION;
else
goto fail;
*name = xstrdup(copy);
*format = xstrdup(split);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: options-table.c,v 1.242 2026/07/27 08:03:01 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>
@@ -76,9 +76,10 @@ static const char *options_table_pane_border_indicators_list[] = {
"off", "colour", "arrows", "both", NULL
};
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
};
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
};
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[] = {
"off", "on", "keep-last", "keep-group", NULL
@@ -305,6 +306,16 @@ const struct options_table_entry options_table[] = {
"When this is reached, the oldest buffer is deleted."
},
{ .name = "clear-on-attach",
.type = OPTIONS_TABLE_FLAG,
.scope = OPTIONS_TABLE_SERVER,
.default_num = 1,
.text = "Whether to use the alternate screen and clear it when "
"a client is attached. When disabled, tmux does not "
"enter the alternate screen on attach so terminal "
"content before tmux remains in scrollback."
},
{ .name = "command-alias",
.type = OPTIONS_TABLE_STRING,
.scope = OPTIONS_TABLE_SERVER,
@@ -482,7 +493,7 @@ const struct options_table_entry options_table[] = {
{ .name = "menu-border-lines",
.type = OPTIONS_TABLE_CHOICE,
.scope = OPTIONS_TABLE_WINDOW,
.choices = options_table_popup_border_lines_list,
.choices = options_table_menu_border_lines_list,
.default_num = BOX_LINES_SINGLE,
.text = "Type of characters used to draw menu border lines. Some of "
"these are only supported on terminals with UTF-8 support."
@@ -541,7 +552,6 @@ const struct options_table_entry options_table[] = {
.scope = OPTIONS_TABLE_SERVER,
.flags = OPTIONS_TABLE_IS_ARRAY,
.default_str = "xterm*:clipboard:ccolour:cstyle:focus:title,"
"xterm-kitty*:kitty,"
"screen*:title,"
"rxvt*:ignorefkeys",
.separator = ",",
@@ -1241,7 +1251,8 @@ const struct options_table_entry options_table[] = {
* underscore.
*/
.default_str = "!\"#$%&'()*+,-./:;<=>?@[\\]^`{|}~",
.text = "Characters considered to separate words."
.text = "Characters considered to separate words; a space matches "
"any character with the Unicode White_Space property."
},
/* Window options. */
@@ -1655,40 +1666,14 @@ const struct options_table_entry options_table[] = {
.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",
.type = OPTIONS_TABLE_CHOICE,
.scope = OPTIONS_TABLE_WINDOW|OPTIONS_TABLE_PANE,
.choices = options_table_remain_on_exit_list,
.default_num = 0,
.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."
},

660
popup.c
View File

@@ -1,660 +0,0 @@
/* $OpenBSD: popup.c,v 1.76 2026/07/14 19:07:03 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);
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);
}

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>
@@ -1493,9 +1493,11 @@ append_key:
utf8_set(&tmp, key);
if (key <= 0x1f || key == 0x7f)
tmp.width = 2;
} else if (KEYC_IS_UNICODE(key))
} else if (KEYC_IS_UNICODE(key)) {
utf8_to_data(key, &tmp);
else
if (tmp.size == 0)
return (PROMPT_KEY_HANDLED);
} else
return (PROMPT_KEY_HANDLED);
pr->buffer = xreallocarray(pr->buffer, size + 2,

View File

@@ -1,33 +1,17 @@
TESTS!= echo *.sh
LOGDIR=logs
.PHONY: all
.NOTPARALLEL: all
.PHONY: all prepare $(TESTS)
all:
@mkdir -p "$(LOGDIR)"; \
rm -f "$(LOGDIR)"/*.log; \
failed=0; failures=; \
all: prepare $(TESTS)
@failed=0; failures=; \
for test in $(TESTS); do \
base=$${test##*/}; \
log="$(LOGDIR)/$${base%.sh}.log"; \
rm -f "$$log"; \
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"; \
if [ -f "$$log" ]; then \
failed=1; \
failures="$$failures $$test"; \
fi; \
sleep 1; \
done; \
if [ "$$failed" -ne 0 ]; then \
echo; \
@@ -39,3 +23,22 @@ all:
rmdir "$(LOGDIR)" 2>/dev/null || true; \
fi; \
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

View File

@@ -2,8 +2,9 @@
# monitor-activity, monitor-bell and monitor-silence: both the
# alert-activity, alert-bell and alert-silence hooks and the winlink alert
# flags (window_activity_flag, window_bell_flag, window_silence_flag and
# the #, !, ~ characters in window_flags). The sessions are detached so
# flags (window_activity_flag, window_bell_flag, window_silence_flag,
# 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
# 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
@@ -111,6 +112,24 @@ flags_have()
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()
{
target=$1
@@ -150,10 +169,12 @@ $TMUX set-hook -g alert-silence \
# 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"
assert_fmt_unchanged mon:bellw '#{window_bell_flag}' 0
assert_session_flag bell 0 bellw
flags_lack mon:bellw '!'
bell mon:bellw
wait_for @log '|alert-bell:mon:bellw'
wait_for_fmt mon:bellw '#{window_bell_flag}' 1
assert_session_flag bell 1 bellw
flags_have mon:bellw '!'
# 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.
$TMUX selectw -t mon:bellw || fail "select-window bellw failed"
wait_for_fmt mon:bellw '#{window_bell_flag}' 0
assert_session_flag bell 0 bellw
flags_lack mon:bellw '!'
$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
wait_for @log '|alert-bell:mon:w0'
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:w0 || fail "select-window w0 failed"
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"
activity mon:actw
assert_fmt_unchanged mon:actw '#{window_activity_flag}' 0
assert_session_flag activity 0 actw
assert_unchanged @log ''
# 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
wait_for @log '|alert-activity:mon:actw'
wait_for_fmt mon:actw '#{window_activity_flag}' 1
assert_session_flag activity 1 actw
flags_have mon:actw '#'
# 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
$TMUX selectw -t mon:actw || fail "select-window actw failed"
wait_for_fmt mon:actw '#{window_activity_flag}' 0
assert_session_flag activity 0 actw
flags_lack mon:actw '#'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"
exec 3>&-
@@ -250,6 +276,7 @@ $TMUX set -wt mon:w0 monitor-activity on ||
$TMUX set -g @log '' || fail "reset @log failed"
activity mon:w0
wait_for_fmt mon:w0 '#{window_activity_flag}' 1
assert_session_flag activity 1 actw
assert_unchanged @log ''
$TMUX set -wut mon:w0 monitor-activity ||
fail "unset monitor-activity w0 failed"
@@ -273,16 +300,19 @@ $TMUX selectw -t mon:w0 || fail "select-window w0 failed"
# running.
$TMUX neww -d -t mon: -n silw cat || fail "new-window silw failed"
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 -wt mon:silw monitor-silence 1 || fail "set monitor-silence failed"
wait_for @log '|alert-silence:mon:silw'
wait_for_fmt mon:silw '#{window_silence_flag}' 1
assert_session_flag silence 1 silw
flags_have mon:silw '~'
assert_unchanged @log '|alert-silence:mon:silw'
$TMUX set -wt mon:silw monitor-silence 0 ||
fail "reset monitor-silence failed"
$TMUX selectw -t mon:silw || fail "select-window silw failed"
wait_for_fmt mon:silw '#{window_silence_flag}' 0
assert_session_flag silence 0 silw
flags_lack mon:silw '~'
$TMUX selectw -t mon:w0 || fail "select-window w0 failed"

View File

@@ -28,7 +28,7 @@ $TMUX kill-server 2>/dev/null
TMP=$(mktemp)
TMP2=$(mktemp)
trap 'rm -f "$TMP" "$TMP2"; $TMUX kill-server 2>/dev/null' 0 1 15
trap 'rm -f "$TMP" "$TMP2" "$TMP".*; $TMUX kill-server 2>/dev/null' 0 1 15
# check_ok $cmd...
#
@@ -270,6 +270,27 @@ if [ $? -eq 0 ]; then
exit 1
fi
# A write error after a successful open must be reported to the server.
dd if=/dev/zero of="$TMP.big" bs=100000 count=1 2>/dev/null
check_ok load-buffer -b write-error "$TMP.big"
mkfifo "$TMP.fifo" || exit 1
dd if="$TMP.fifo" of="$TMP.got" bs=1 count=8 2>/dev/null &
reader=$!
exec 3>"$TMP.fifo"
out=$($TMUX save-buffer -b write-error "$TMP.fifo" 3>&- 2>&1)
status=$?
exec 3>&-
wait "$reader"
if [ $status -eq 0 ]; then
echo "save-buffer after write error succeeded."
exit 1
fi
if [ "$(wc -c <"$TMP.got")" -ne 8 ]; then
echo "save-buffer write error test wrote wrong amount."
exit 1
fi
check_ok delete-buffer -b write-error
# save-buffer - writes to stdout and load-buffer - reads from stdin.
out=$($TMUX save-buffer -b sb -)
if [ "$out" != "data" ]; then

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

0
regress/cfg-client-lost.sh Executable file → Normal file
View File

View File

@@ -290,4 +290,92 @@ wait_count ': G4' 7
fail "window 1 of zzz not killed"
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

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

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
regress/clock-mode.sh Normal file
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

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

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

2
regress/control-client-exit-stalled.sh Executable file → Normal file
View File

@@ -17,7 +17,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)

View File

@@ -19,7 +19,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
DIR=$(mktemp -d)

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

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

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

View File

@@ -30,8 +30,11 @@ killw
EOF
sleep 1
$TMUX has || exit 1
$TMUX lsp -aF '#{pane_id} #{window_layout}' >$TMP || exit 1
cat <<EOF|cmp -s $TMP - || exit 1
# Use a control client to request legacy layouts, keeping only pane lines
# 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]
%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]

View File

@@ -10,7 +10,7 @@ PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Ltest"
TMUX="$TEST_TMUX -Ltest$$"
$TMUX kill-server 2>/dev/null
FIFO=$(mktemp -u)

0
regress/control-notify-guard.sh Executable file → Normal file
View File

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

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

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

@@ -0,0 +1,163 @@
#!/bin/sh
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
fail()
{
echo "$1"
[ -z "$TMUX" ] || $TMUX kill-server 2>/dev/null
exit 1
}
goto_cell()
{
row=$1
column=$2
$TMUX send-keys -X history-top || fail "$mode: history-top failed"
$TMUX send-keys -X start-of-line || fail "$mode: start-of-line failed"
[ "$row" -eq 0 ] ||
$TMUX send-keys -X -N "$row" cursor-down ||
fail "$mode: cursor-down failed"
[ "$column" -eq 0 ] ||
$TMUX send-keys -X -N "$column" cursor-right ||
fail "$mode: cursor-right failed"
}
check_cursor()
{
expected=$1
actual=$($TMUX display-message -p '#{copy_cursor_x},#{copy_cursor_y}')
[ "$actual" = "$expected" ] ||
fail "$mode: expected cursor $expected, got $actual"
}
check_word()
{
row=$1
column=$2
expected=$3
goto_cell "$row" "$column"
$TMUX set-buffer sentinel || fail "$mode: set-buffer failed"
$TMUX send-keys -X select-word || fail "$mode: select-word failed"
$TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed"
actual=$($TMUX show-buffer 2>/dev/null)
[ "$actual" = "$expected" ] ||
fail "$mode: expected word '$expected', got '$actual'"
}
check_next_word()
{
row=$1
expected=$2
goto_cell "$row" 0
$TMUX send-keys -X next-word || fail "$mode: next-word failed"
$TMUX set-buffer sentinel || fail "$mode: set-buffer failed"
$TMUX send-keys -X select-word || fail "$mode: select-word failed"
$TMUX send-keys -X copy-selection || fail "$mode: copy-selection failed"
actual=$($TMUX show-buffer 2>/dev/null)
[ "$actual" = "$expected" ] ||
fail "$mode: expected next word '$expected', got '$actual'"
}
for mode in emacs vi; do
TMUX="$TEST_TMUX -Lunicode-whitespace-$mode-$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX new-session -d -x16 -y20 \
"printf 'aa bb\naa\tbb\naa\302\240bb\naa\343\200\200bb\naa:bb\naaaaaaaaaaaaaaa\302\240b\nabcdefghijklmnopq\nhardone\nhardtwo\naa\341\232\200bb\naa\342\200\200bb\naa\342\200\250bb\naa\342\200\251bb\naa\342\200\257bb\naa\342\201\237bb\nxa\302\205b c\n'; exec cat" ||
fail "$mode: new-session failed"
$TMUX set-option -g window-size manual || fail "$mode: set size failed"
$TMUX set-window-option -g mode-keys "$mode" ||
fail "$mode: set mode-keys failed"
$TMUX set-window-option -g word-separators "" ||
fail "$mode: clear word-separators failed"
$TMUX copy-mode || fail "$mode: copy-mode failed"
# ASCII space and TAB retain their existing behaviour.
goto_cell 0 0
$TMUX send-keys -X next-space || fail "$mode: ASCII next-space failed"
check_cursor 3,0
goto_cell 1 0
$TMUX send-keys -X next-space || fail "$mode: TAB next-space failed"
check_cursor 8,1
$TMUX send-keys -X previous-space ||
fail "$mode: TAB previous-space failed"
check_cursor 0,1
# NBSP is whitespace for selection and word movement.
goto_cell 2 0
$TMUX send-keys -X next-word || fail "$mode: NBSP next-word failed"
check_cursor 3,2
$TMUX send-keys -X previous-word ||
fail "$mode: NBSP previous-word failed"
check_cursor 0,2
check_word 2 0 aa
goto_cell 2 0
$TMUX send-keys -X next-word-end ||
fail "$mode: NBSP next-word-end failed"
if [ "$mode" = emacs ]; then
check_cursor 2,2
else
check_cursor 1,2
fi
# U+3000 is two columns wide; both its leading and padding cells must be
# skipped as one whitespace character in either direction.
goto_cell 3 0
$TMUX send-keys -X next-word || fail "$mode: U+3000 next-word failed"
check_cursor 4,3
$TMUX send-keys -X previous-word ||
fail "$mode: U+3000 previous-word failed"
check_cursor 0,3
goto_cell 3 0
$TMUX send-keys -X next-space-end ||
fail "$mode: U+3000 next-space-end failed"
if [ "$mode" = emacs ]; then
check_cursor 2,3
else
check_cursor 1,3
fi
# Other characters in word-separators remain literal when space enables
# Unicode whitespace matching.
$TMUX set-window-option -g word-separators ': ' ||
fail "$mode: set custom word-separators failed"
goto_cell 4 0
$TMUX send-keys -X next-word || fail "$mode: literal next-word failed"
check_cursor 2,4
$TMUX set-window-option -g word-separators "" ||
fail "$mode: restore word-separators failed"
# Whitespace in the last column remains a boundary across a soft wrap.
goto_cell 5 0
$TMUX send-keys -X next-space ||
fail "$mode: wrapped NBSP next-space failed"
check_cursor 0,6
# A word split only by a soft wrap remains one word, while a hard line
# boundary still terminates selection.
check_word 7 0 abcdefghijklmnopq
check_word 9 0 hardone
# Check the remaining representative White_Space ranges and U+0085
# combined into a cell with another character.
check_next_word 11 bb
check_next_word 12 bb
check_next_word 13 bb
check_next_word 14 bb
check_next_word 15 bb
check_next_word 16 bb
goto_cell 17 0
$TMUX send-keys -X next-word ||
fail "$mode: combined U+0085 next-word failed"
check_cursor 2,17
$TMUX kill-server 2>/dev/null
done
exit 0

118
regress/find-window.sh Normal file
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

View File

@@ -199,5 +199,218 @@ must_fail $TMUX resize-pane -t "$id" -x 0
must_fail $TMUX resize-pane -t "$id" -y 0
$TMUX kill-pane -t "$id" || exit 1
# --- Tiled pane resize with floating cells in the layout ---
$TMUX set-option -w -u pane-border-lines || exit 1
base=$($TMUX display-message -p '#{pane_id}')
# A floating cell after the last tiled cell must not become the recipient of a
# relative or absolute resize. This is the sequence from GitHub issue 5135.
floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 50% -y 50% -X 50% -Y 50% 'sleep 100') ||
fail "new-pane for tiled resize test failed"
lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') ||
fail "split-window for tiled resize test failed"
$TMUX resize-pane -t "$lower" -U 5 || fail "relative tiled resize failed"
must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_top}')" 13
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10
$TMUX resize-pane -t "$lower" -y 11 || fail "absolute tiled reset failed"
$TMUX resize-pane -t "$lower" -y 16 || fail "absolute tiled resize failed"
must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 7
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 8
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 16
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 10
$TMUX kill-pane -t "$floating" || exit 1
$TMUX kill-pane -t "$lower" || exit 1
# A floating cell between tiled siblings must be skipped when finding the cell
# which donates space to a resize.
lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') ||
fail "split-window for middle floating cell test failed"
floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 20 -y 8 -X 30 -Y 8 'sleep 100') ||
fail "new-pane for middle floating cell test failed"
$TMUX resize-pane -t "$base" -D 3 || fail "resize past floating cell failed"
must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 6
$TMUX kill-pane -t "$floating" || exit 1
$TMUX kill-pane -t "$lower" || exit 1
# A small floating child must not limit the available tiled space in a nested
# layout with a different split direction.
lower=$($TMUX split-window -dPF '#{pane_id}' -t "$base" 'sleep 100') ||
fail "split-window for nested floating cell test failed"
right=$($TMUX split-window -dhPF '#{pane_id}' -t "$lower" 'sleep 100') ||
fail "horizontal split for nested floating cell test failed"
floating=$($TMUX new-pane -dPF '#{pane_id}' -t "$lower" \
-x 20 -y 3 -X 30 -Y 10 'sleep 100') ||
fail "new-pane for nested floating cell test failed"
$TMUX resize-pane -t "$base" -D 3 || fail "nested tiled resize failed"
must_equal "$($TMUX display-message -p -t "$base" '#{pane_height}')" 15
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_top}')" 16
must_equal "$($TMUX display-message -p -t "$lower" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$right" '#{pane_top}')" 16
must_equal "$($TMUX display-message -p -t "$right" '#{pane_height}')" 8
must_equal "$($TMUX display-message -p -t "$floating" '#{pane_height}')" 1
$TMUX kill-pane -t "$floating" || exit 1
$TMUX kill-pane -t "$right" || 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
exit 0

View File

@@ -0,0 +1,158 @@
#!/bin/sh
# Moving, resizing and reordering an over-zoom float must leave the tiled
# pane zoomed, and the changes must survive an explicit unzoom.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null' 0
trap 'exit 1' 1 2 15
fail()
{
echo "$*" >&2
exit 1
}
run()
{
$TMUX "$@" || fail "failed: $*"
}
check()
{
got=$(run display-message -p -t "$1" "$2") || exit 1
[ "$got" = "$3" ] || fail "$1 $2: got '$got', expected '$3'"
}
check_zoom()
{
check "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1'
check "$float" '#{pane_floating_flag}:#{pane_active}' "1:$active"
check "$base" '#{@unzoomed}' ''
}
run new-session -d -x 80 -y 24
base=$(run display-message -p '#{pane_id}') || exit 1
other=$(run split-window -dPF '#{pane_id}') || exit 1
layout=$(run display-message -p '#{window_layout}') || exit 1
run set-hook -g window-unzoomed 'set -g @unzoomed 1'
# Cover creation before and after zoom, with both the float and the tiled
# pane active. Modal panes use the same over-zoom flag.
for mode in before after detached modal; do
if [ "$mode" != before ]; then
run resize-pane -Z -t "$base"
fi
case "$mode" in
detached) flags=-Ad; active=0 ;;
modal) flags=-O; active=1 ;;
*) flags=-A; active=1 ;;
esac
float=$(run new-pane "$flags" -PF '#{pane_id}' -t "$base" \
-x 20 -y 8 -X 8 -Y 3 '') || exit 1
if [ "$mode" = before ]; then
run resize-pane -Z -t "$base"
fi
run set -gu @unzoomed
check_zoom
run move-pane -t "$float" -D
check "$float" '#{pane_top}' 5
check_zoom
run move-pane -t "$float" -U 2 -R 3 -L 1
check "$float" '#{pane_left}:#{pane_top}' '11:3'
check_zoom
run move-pane -t "$float" -X 25% -Y 25%
check "$float" '#{pane_left}:#{pane_top}' '21:7'
check_zoom
run move-pane -t "$float" -P bottom-right
check "$float" '#{pane_left}:#{pane_top}' '61:17'
check_zoom
run resize-pane -t "$float" -x 30 -y 10
check "$float" '#{pane_width}:#{pane_height}' '28:8'
check_zoom
run resize-pane -t "$float" -U 1 -L 2
check "$float" '#{pane_left}:#{pane_top}:#{pane_width}:#{pane_height}' \
'59:16:30:9'
check_zoom
run move-pane -t "$float" -P centre
check "$float" '#{pane_left}:#{pane_top}' '25:7'
check_zoom
# Failed commands must not change zoom either.
for command in 'move-pane -D invalid' 'move-pane -P invalid' \
'move-pane -z invalid' 'resize-pane -x invalid' \
'resize-pane -D invalid'; do
$TMUX $command -t "$float" 2>/dev/null &&
fail "unexpected success: $command"
check_zoom
done
run resize-pane -Z -t "$base"
check "$float" '#{pane_left}:#{pane_top}:#{pane_width}:#{pane_height}' \
'25:7:30:9'
run kill-pane -t "$float"
check "$base" '#{window_layout}' "$layout"
done
# Put an ordinary (hidden while zoomed) float between two over-zoom floats
# in the stacking order. Reordering must skip it when counting visible
# positions, but keep all floats ahead of the tiled panes after unzoom.
back=$(run new-pane -AdPF '#{pane_id}' -t "$base" '') || exit 1
hidden=$(run new-pane -dPF '#{pane_id}' -t "$base" '') || exit 1
float=$(run new-pane -AdPF '#{pane_id}' -t "$base" '') || exit 1
active=0
run resize-pane -Z -t "$base"
run set -gu @unzoomed
for position in backward back forward-loop; do
run move-pane -t "$float" -P front
run move-pane -t "$float" -P "$position"
check "$float" '#{pane_z}' 1
check "$back" '#{pane_z}' 0
check_zoom
done
for position in forward front backward-loop; do
run move-pane -t "$float" -P back
run move-pane -t "$float" -P "$position"
check "$float" '#{pane_z}' 0
check "$back" '#{pane_z}' 1
check_zoom
done
for z in 1 0 99; do
run move-pane -t "$float" -z "$z"
want=$z
[ "$z" = 99 ] && want=1
check "$float" '#{pane_z}' "$want"
check_zoom
done
run resize-pane -Z -t "$base"
check "$float" '#{pane_z}' 2
check "$base" '#{pane_z}' 4
run kill-pane -t "$hidden"
run kill-pane -t "$back"
# Explicit zoom toggling and operations on tiled panes retain their
# existing behaviour. The over-zoom flag alone does not make a pane a float:
# zooming the float itself gives it a tiled cell until unzoom.
run resize-pane -Z -t "$float"
check "$float" '#{pane_zoomed_flag}:#{pane_floating_flag}' '1:0'
run resize-pane -t "$float" -x 25
check "$float" '#{window_zoomed_flag}:#{pane_floating_flag}:#{pane_width}' \
'0:1:23'
run resize-pane -Z -t "$base"
run resize-pane -t "$other" -D 1
check "$base" '#{window_zoomed_flag}' 0
# A hidden ordinary float must still be unzoomed before resizing it.
hidden=$(run new-pane -dPF '#{pane_id}' -t "$base" '') || exit 1
run resize-pane -Z -t "$base"
run resize-pane -t "$hidden" -x 25
check "$hidden" '#{window_zoomed_flag}:#{pane_floating_flag}:#{pane_width}' \
'0:1:23'
exit 0

View File

@@ -7,6 +7,8 @@ TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null' 0
trap 'exit 1' 1 2 15
# test_format $format $expected_result
test_format()

View File

@@ -18,6 +18,24 @@ export TZ LANG LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
FIFO1="${TMPDIR:-/tmp}/fmt-l-$$-1"
FIFO2="${TMPDIR:-/tmp}/fmt-l-$$-2"
HOLD1=
HOLD2=
CC1=
CC2=
cleanup()
{
for pid in $HOLD1 $HOLD2 $CC1 $CC2; do
kill "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
done
$TMUX kill-server 2>/dev/null
rm -f "$FIFO1" "$FIFO2"
}
trap cleanup 0
trap 'exit 1' 1 2 15
ESC=$(printf '\033')
@@ -579,8 +597,6 @@ assert_alive "verbose loop expansion"
# L loops over attached clients. Attach two control-mode clients, each held
# open by a background process keeping a FIFO's write end open.
FIFO1="${TMPDIR:-/tmp}/fmt-l-$$-1"
FIFO2="${TMPDIR:-/tmp}/fmt-l-$$-2"
rm -f "$FIFO1" "$FIFO2"
mkfifo "$FIFO1" "$FIFO2" || exit 1
# Hold the write ends open so the control clients stay attached.
@@ -605,9 +621,9 @@ test_format "#{L/nr:x}" "xx"
test_format "#{L/r:x}" "xx"
# Now detach one and confirm the count drops to one.
kill $HOLD2 2>/dev/null
wait $HOLD2 2>/dev/null
HOLD2=
sleep 1
test_format "#{L:x}" "x"
kill $HOLD1 $CC1 $CC2 2>/dev/null
rm -f "$FIFO1" "$FIFO2"
exit 0

View File

@@ -7,6 +7,8 @@ TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
trap '$TMUX kill-server 2>/dev/null' 0
trap 'exit 1' 1 2 15
# test_format $format $expected_result
test_format()

View File

@@ -71,7 +71,10 @@ cursor_very_visible
cursor_x
cursor_y
history_all_bytes
history_added
history_bytes
history_collected
history_generation
history_limit
history_size
host
@@ -103,6 +106,11 @@ pane_at_right
pane_at_top
pane_bg
pane_bottom
pane_command_duration
pane_command_end_time
pane_command_running
pane_command_start_time
pane_command_status
pane_current_command
pane_current_path
pane_dead
@@ -120,16 +128,21 @@ pane_index
pane_input_off
pane_key_mode
pane_last
pane_last_output_time
pane_last_prompt_time
pane_left
pane_marked
pane_marked_set
pane_modal_flag
pane_mode
pane_output_generation
pane_path
pane_pb_progress
pane_pb_state
pane_pid
pane_pipe
pane_pipe_pid
pane_private_modes
pane_right
pane_search_string
pane_start_command
@@ -141,6 +154,8 @@ pane_title
pane_top
pane_tty
pane_unseen_changes
pane_unzoomed_height
pane_unzoomed_width
pane_width
pane_x
pane_y
@@ -206,7 +221,10 @@ window_layout
window_linked
window_linked_sessions
window_linked_sessions_list
window_manual_height
window_manual_width
window_marked_flag
window_modal_pane
window_name
window_offset_x
window_offset_y

View File

@@ -76,14 +76,14 @@ $TMUX set-hook -g session-closed \
fail "set-hook session-closed failed"
# The only pane of the only window of a session exits: pane-exited, then
# window-unlinked, then session-closed, each seeing the dead object in the
# session-closed, then window-unlinked, each seeing the dead object in the
# hook formats.
pane=$($TMUX new -d -s doomed -n dwin -P -F '#{pane_id}' 'true') ||
fail "new-session doomed failed"
wait_for @log \
"|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed"
"|pane-exited:$pane|session-closed:doomed|window-unlinked:doomed:dwin"
assert_unchanged @log \
"|pane-exited:$pane|window-unlinked:doomed:dwin|session-closed:doomed"
"|pane-exited:$pane|session-closed:doomed|window-unlinked:doomed:dwin"
# The dead pane, window and session cannot be used as targets but the
# server survives.
@@ -110,14 +110,14 @@ $TMUX set-hook -g pane-exited \
'set -gF @log "#{@log}|pane-exited:#{hook_pane}"' ||
fail "restore pane-exited hook failed"
# kill-window on the last window: window-unlinked then session-closed but
# kill-window on the last window: session-closed then window-unlinked but
# no pane-exited for the panes in the killed window.
$TMUX set -g @log '' || fail "reset @log failed"
$TMUX new -d -s doomed2 -n dwin2 || fail "new-session doomed2 failed"
$TMUX splitw -d -t doomed2:0 || fail "split-window doomed2 failed"
$TMUX kill-window -t doomed2:0 || fail "kill-window failed"
wait_for @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2'
assert_unchanged @log '|window-unlinked:doomed2:dwin2|session-closed:doomed2'
wait_for @log '|session-closed:doomed2|window-unlinked:doomed2:dwin2'
assert_unchanged @log '|session-closed:doomed2|window-unlinked:doomed2:dwin2'
$TMUX has -t main || fail "server died after kill-window chain"
# kill-session: session-closed fires first, then window-unlinked for its

View File

@@ -218,11 +218,13 @@ $TMUX set-hook -gu pane-mode-changed || fail "unset pane-mode-changed failed"
# pane-exited when a pane's command exits with remain-on-exit off.
$TMUX set -g @x 0 || fail "set @x failed"
$TMUX set-hook -g pane-exited 'set -gF @x "#{hook}:#{hook_pane}"' ||
$TMUX set-hook -g pane-exited \
'set -gF @x "#{hook}:#{hook_pane}:#{hook_exit_status}:#{hook_exit_success}"' ||
fail "set-hook pane-exited failed"
pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' 'true') ||
fail "split-window true failed"
wait_for @x "pane-exited:$pane"
pane=$($TMUX splitw -d -t main:0 -P -F '#{pane_id}' \
'trap "" HUP; exec </dev/null >/dev/null 2>&1; sleep 1; exit 42') ||
fail "split-window exit 42 failed"
wait_for @x "pane-exited:$pane:42:0"
$TMUX set-hook -gu pane-exited || fail "unset pane-exited failed"
# pane-died when a pane's command exits with remain-on-exit on.

View File

@@ -12,7 +12,8 @@ $TMUX kill-server 2>/dev/null
TMP=$(mktemp)
OUT=$(mktemp)
trap "rm -f $TMP $OUT" 0 1 15
trap 'rm -f "$TMP" "$OUT"; $TMUX kill-server 2>/dev/null' 0
trap 'exit 1' 1 2 15
cat <<EOF >$TMP
if 'true' 'wibble wobble'

View File

@@ -1,437 +0,0 @@
#!/bin/sh
# Grid-resident image layers: Kitty input, history, copy and overwrite.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Limage$$ -f/dev/null"
TMUX2="$TEST_TMUX -Limage-client$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
FIXTURE=$(pwd)/monkey-2.sixel.txt
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15
$TMUX new-session -d -x 20 -y 8 "
printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,m=1;/wAA/wD/\033\\'
printf '\033_Gm=0;AP8AAP///////w==\033\\'
i=0
while [ \$i -lt 10 ]; do printf '\nline-%d' \$i; i=\$((i + 1)); done
sleep 10"
sleep 1
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
# Images scroll as grid cells, while capture output contains ordinary spaces.
[ "$($TMUX display-message -p '#{history_size}')" -gt 0 ] || exit 1
$TMUX capture-pane -pS- >$TMP || exit 1
grep '_G' $TMP >/dev/null && exit 1
# Copy mode duplicates the backing grid, including image layers.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX capture-pane -p >$TMP || exit 1
$TMUX send-keys -X cancel || exit 1
# Ordinary text updates the image underlay through the normal grid write path.
# Deleting the image reveals the newly written text.
$TMUX new-window -d "
printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,i=8;/wAA/wD/AP8AAP///////w==\033\\'
printf '\033[HXY'
printf '\033_Ga=d,d=i,q=2,i=8\033\\'
sleep 10"
sleep 1
[ "$($TMUX capture-pane -pt:1 -S0 -E0)" = "XY" ] || exit 1
# The protocol default is transmit-only; a later placement uses the retained
# immutable image and advances by the same canonical one-cell footprint.
$TMUX new-window -d "
printf '\033_Gq=2,f=32,s=1,v=1,c=1,r=1,i=9;/wAA/w==\033\\'
printf '\033_Ga=p,q=2,i=9\033\\'
printf '\033_Ga=d,d=I,q=2,i=9\033\\'
sleep 10"
sleep 1
[ "$($TMUX display-message -pt:2 '#{cursor_y}')" = 1 ] || exit 1
# PNG Kitty input uses the shared image decoder and canonical cell sizing.
$TMUX new-window -d "
printf '\033_Ga=T,q=2,f=100;iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURf8AAP///0EdNBEAAAABYktHRAH/Ai3eAAAAB3RJTUUH6ggCDAECH324BwAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=\033\\'
sleep 10"
sleep 1
[ "$($TMUX display-message -pt:3 '#{cursor_y}')" = 1 ] || exit 1
# SIXEL input reaches the same grid layer and copy-mode paths.
$TMUX new-window -d "cat '$FIXTURE'; sleep 10"
sleep 1
[ "$($TMUX display-message -pt:4 '#{cursor_y}')" -gt 0 ] || exit 1
$TMUX copy-mode -t:4 || exit 1
$TMUX send-keys -t:4 -X history-top || exit 1
$TMUX capture-pane -pt:4 >$TMP || exit 1
# A 26-pixel raster occupies two default 16-pixel cells, but the final cell
# remains only partially filled instead of stretching the raster to 32 pixels.
# A client without an image feature exposes this in the sampled text backend.
$TMUX kill-server 2>/dev/null
$TMUX2 new-session -d -x 10 -y 4 "
printf '\033Pq\"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 10 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# Re-emitting the image preserves the 26-pixel raster rather than expanding
# it to the two complete 16-pixel grid cells occupied by the image.
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 refresh-client -R || exit 1
sleep 1
grep -a '"1;1;26;26' $TMP >/dev/null || exit 1
# A selection redraw uses the single-cell path. It must leave ASCII image
# cells visible (graphical clients skip these cells to avoid erasing pixels).
$TMUX2 copy-mode || exit 1
$TMUX2 send-keys -X history-top || exit 1
$TMUX2 send-keys -X start-of-line || exit 1
$TMUX2 send-keys -N 4 -X cursor-right || exit 1
$TMUX2 send-keys -X begin-selection || exit 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 send-keys -X cursor-right || exit 1
sleep 1
$TMUX pipe-pane || exit 1
grep -a '"1;1;' $TMP >/dev/null && exit 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# Selection must also leave transparent cells inside the image canvas alone.
# This 26-pixel canvas paints only its first pixel column, while the text below
# it keeps the copy-mode cursor movable across both occupied terminal cells.
$TMUX2 send-keys -X cancel || exit 1
$TMUX2 new-window -d "
printf 'ab\r'
printf '\033Pq\"1;1;26;26#0;2;100;100;100#0~-#0~-#0~-#0~-#0B\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:1 || exit 1
sleep 1
$TMUX2 copy-mode || exit 1
$TMUX2 send-keys -X top-line || exit 1
$TMUX2 send-keys -X start-of-line || exit 1
$TMUX2 send-keys -X cursor-right || exit 1
$TMUX2 send-keys -X begin-selection || exit 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 send-keys -X cursor-right || exit 1
sleep 1
$TMUX pipe-pane || exit 1
grep -a 'b' $TMP >/dev/null && exit 1
# Scrolling moves the terminal's image rows with its text rows. It must not
# force tmux to erase and retransmit the complete SIXEL image.
$TMUX2 send-keys -X cancel || exit 1
SCROLL_WINDOW=$($TMUX2 new-window -P -F '#{window_id}' \
"cat '$FIXTURE'; exec sh") || exit 1
sleep 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 send-keys -t"$SCROLL_WINDOW" -l \
"i=0; while [ \$i -lt 12 ]; do printf 'scroll-marker-%d\\n' \$i; i=\$((i + 1)); done" || exit 1
$TMUX2 send-keys -t"$SCROLL_WINDOW" Enter || exit 1
sleep 1
grep -a 'scroll-marker-11' $TMP >/dev/null || exit 1
grep -a '"1;1;' $TMP >/dev/null && exit 1
# A retained Kitty image may be placed repeatedly using source rectangles.
# Crop the green and white right column from a red/green/blue/white image.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 10 -y 4 "
printf '\033_Ga=t,q=2,f=32,s=2,v=2,i=10;/wAA/wD/AP8AAP///////w==\033\\'
printf '\033_Ga=p,q=2,i=10,x=1,y=0,w=1,h=2,c=1,r=2\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX new-session -d -x 10 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "*" ] || exit 1
[ "$(sed -n 2p $TMP)" = "@" ] || exit 1
# C=1 places the image without moving the cursor or scrolling. The four-row
# image is clipped at the bottom of this four-row pane, leaving its white last
# row off screen.
$TMUX2 new-window -d "
printf '\033[2;3H'
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:1 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{cursor_x},#{cursor_y}')" = "2,1" ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 3p $TMP)" = " -" ] || exit 1
[ "$(sed -n 4p $TMP)" = " *" ] || exit 1
# With normal cursor movement, scrolling is calculated from the full image
# height. Rows which scrolled above the pane are then cropped from the top, so
# the bottom three source rows remain visible and the cursor is on the last row.
$TMUX2 new-window -d "
printf '\033[3;1H'
printf '\033_Ga=T,q=2,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:2 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{cursor_y}')" = 3 ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "-" ] || exit 1
[ "$(sed -n 2p $TMP)" = "*" ] || exit 1
[ "$(sed -n 3p $TMP)" = "@" ] || exit 1
# Image rows remain cell-aligned when a narrower terminal causes text
# reflow. The ten-column rows are clipped to five columns, not split into four
# wrapped rows.
$TMUX2 new-window -d "
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=2,c=10,r=2;/wAA//////8=\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:3 || exit 1
sleep 1
$TMUX resize-window -x 5 -y 4 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{window_width}x#{window_height}')" = "5x4" ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "....." ] || exit 1
[ "$(sed -n 2p $TMP)" = "@@@@@" ] || exit 1
[ -z "$(sed -n 3p $TMP)" ] || exit 1
# Kitty virtual placements use U+10EEEE placeholder cells. Keep their rows at
# fixed coordinates when narrowing the terminal, clipping instead of reflowing
# the second half onto the following row.
$TMUX resize-window -x 10 -y 4 || exit 1
sleep 1
$TMUX2 new-window -d "
printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205'
printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205'
sleep 10" || exit 1
$TMUX2 select-window -t:4 || exit 1
sleep 1
$TMUX resize-window -x 5 -y 4 || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -n "$(sed -n 1p $TMP)" ] || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
$TMUX resize-window -x 10 -y 4 || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP | wc -c)" = 61 ] || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
# A placement ID supplied with transmit-and-place is reused by a later place.
# This is the sequence used by chawan: the newline moves the cursor down before
# the second command, so the folder moves down one row without leaving a copy.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_GC=1,s=26,v=26,p=1,q=2,i=1,a=T,f=100,m=0;iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAACmUlEQVR4XmOgF2CEWVRfX8/w//9/BkZGRgy7YeKNjY1kuwtsam1tLQMTE5MG0MBsoEWW6KYBxf8DxScD6UVNTU1kWcZYU1MD0qjLzMx8EmgYJy5TgJb8/ffvn+2vX7+Od3Z2kmwZC9AnIE0FIEuAhl0H4ulA9k9kk4Bi4UB1TkA8H2iRYXl5+Xd2dna8loGCG2Q2LLgZQXED1PEIaLjs379/g4Ds9VDLwQYBxRiAPpFmZWW9DFQjCDRgLxDfJeQloJpdQHPWA9X9A1kGs+gZ0BBJoKQtUOIIeqSDgheoKR6IF5ASZkAH3gKaCzLzFQsxGltaWhjq6uoWguIJ6hi82oCG8wIV+AEdpga0LBfIrmUh1oWg1AYM5iVA9UuI1JMGVDcTiCNIsghkOChIQ0NDwfasXr0ap33QeL8GUgD0HTeIZmIgAUAtYQZqYYZZSKx2oi2CWaKpqbkIhEm1jOg40tLSAjleEBgUUSAGkJ8PpN8Q6yOCFoF8AvQBTvOAqZHh+vXrDPjijGAcIcXJUqBPQOXda5iNQP5rkBiQv5SYYMTrIxUVFZC5IkDCCk8QWQHVgdS8xBeMeBNDe3s7A7CwfQl0uSmwKDIBYhdQGQbCIDZIDCQHUgNSS7ZFII2gUgEU6UADzwJz+kUgDa6zoOyzIDmoGgaKLAJpBheKkArxC7BIeQDCQPEvIEuJrQyJzkdQA38ADbcHYaBFP0ipcVGqCWDYBwPxOkYs1TkDCeA/pEkQBDRnLZD9GKhVjgUkCGTsBgomAekWIJYG4p8MlAFQrZgJMgJo/m4QzQJ1fT9QIBLI1gTiSRRaAm/gAM38DjSvH2QeuHECLW1xNk7IsRhoyXGgJVOBem+AExPMEHzNLTItYiAlVTJQCwAAJxkMkgbHo2UAAAAASUVORK5CYII=\033\\'
printf '\n'
sleep 1
printf '\033_GC=1,s=26,v=26,p=1,q=2,i=1,a=p;\033\\'
printf '\n'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 1p $TMP)" ] || exit 1
[ -n "$(sed -n 2p $TMP)" ] || exit 1
# A nonzero Kitty placement ID identifies one placement of an image. Reusing
# the same image and placement IDs moves it rather than leaving the old cells.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=11;/////w==\033\\'
printf '\033[2;2H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\'
printf '\033[4;6H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1
# Deleting one placement ID leaves other placements of the image intact.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=12;/////w==\033\\'
printf '\033[2;2H\033_Ga=p,q=2,C=1,i=12,p=7,c=2,r=1\033\\'
printf '\033[4;6H\033_Ga=p,q=2,C=1,i=12,p=8,c=2,r=1\033\\'
printf '\033_Ga=d,d=i,q=2,i=12,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1
# Image layers retain the cells beneath them. Deleting a transparent image
# must reveal the original text rather than replacing it with spaces.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf 'XY'
printf '\033[H'
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=1,c=2,r=1,i=13,p=7;////AA==\033\\'
printf '\033_Ga=d,d=i,q=2,i=13,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
[ "$($TMUX2 capture-pane -pS0 -E0)" = "XY" ] || exit 1
# Text written over a Kitty placement updates its underlay without deleting
# the image. Deleting the placement afterwards reveals the updated text.
TEXT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf 'test\r'
printf '\033_Ga=T,q=2,C=1,f=32,s=2,v=2,c=2,r=2,i=14,p=7;/wAA/wD/AP8AAP///////w==\033\\'
printf '\r'
printf 'test\n'
printf 'test\n'
$TEST_TMUX wait-for image-text-delete-$$
printf '\033_Ga=d,d=i,q=2,i=14,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$TEXT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = ".*st" ] || exit 1
[ "$(sed -n 2p $TMP)" = " @st" ] || exit 1
$TMUX2 wait-for -S image-text-delete-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "test" ] || exit 1
[ "$(sed -n 2p $TMP)" = "test" ] || exit 1
# Kitty z-indexes and SIXEL's temporal plane are properties of the input, not
# of the outer terminal. Exercise the logical scene through the fallback
# backend: higher nonnegative z-indexes cover lower ones and text, ordinary
# negative z-indexes cover backgrounds but not text, and very negative ones
# remain below the background.
Z_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=21;/wAA/w==\033\\'
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=22;/////w==\033\\'
printf 'X\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=1,z=2,c=1,r=1\033\\'
printf '\033_Ga=p,q=2,C=1,i=22,p=2,z=1,c=1,r=1\033\\'
printf '\033[2;1H\033[41m \033[0m\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=3,z=-1,c=1,r=1\033\\'
printf '\033[3;1HY\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=4,z=-1,c=1,r=1\033\\'
printf '\033[4;1H\033[41m \033[0m\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=5,z=-1073741825,c=1,r=1\033\\'
$TEST_TMUX wait-for image-z-top-$$
printf '\033_Ga=d,d=z,q=2,z=2\033\\'
$TEST_TMUX wait-for image-z-lower-$$
printf '\033_Ga=d,d=z,q=2,z=1\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$Z_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "." ] || exit 1
[ "$(sed -n 2p $TMP)" = "." ] || exit 1
[ "$(sed -n 3p $TMP)" = "Y" ] || exit 1
[ -z "$(sed -n 4p $TMP)" ] || exit 1
$TMUX2 wait-for -S image-z-top-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "@" ] || exit 1
$TMUX2 wait-for -S image-z-lower-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "X" ] || exit 1
# A weighted median at the maximum channel level must still leave colours on
# both sides of the split. This skewed black, grey and white image used to stop
# palette generation after one colour instead of producing three.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 20 -y 4 "
printf '\033_Ga=T,q=2,C=1,f=24,s=100,v=1,c=10,r=1;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 20 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
grep -a '#2;2;' $TMP >/dev/null || exit 1
# A pane redraw on a SIXEL terminal must not clear the status line, which is
# outside the window scene and may not be redrawn after the pane scrolls.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 20 -y 8 || exit 1
$TMUX2 set -g 'status-format[0]' 'STATUS' || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 20 -y 8 || exit 1
$TMUX set -g status off || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX2 send-keys -l 'seq 30' || exit 1
$TMUX2 send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -p >$TMP || exit 1
[ "$(tail -n 1 $TMP)" = "STATUS" ] || exit 1
# A partial redraw of a tiled pane must clear only its visible scene spans.
# Clearing its entire rectangle would erase a floating pane above it, while
# the tiled pane redraw correctly skips the obscured area and cannot restore it.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 40 -y 15 \
"printf 'TILED PANE'; sleep 10" || exit 1
TILED_PANE=$($TMUX2 display-message -p '#{pane_id}') || exit 1
FLOATING_PANE=$($TMUX2 new-pane -dP -F '#{pane_id}' -x 20 -y 6 -X 10 -Y 4 \
"printf 'FLOATING PANE'; sleep 10") || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX2 select-pane -t"$FLOATING_PANE" || exit 1
$TMUX new-session -d -x 40 -y 15 || exit 1
$TMUX set -g status off || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX2 copy-mode -t"$TILED_PANE" || exit 1
sleep 1
$TMUX capture-pane -p >$TMP || exit 1
grep 'TILED PANE' $TMP >/dev/null || exit 1
grep 'FLOATING PANE' $TMP >/dev/null || exit 1
# An obscured pane is normally redrawn a line at a time when it scrolls, but
# that path cannot restore graphical image layers. It must defer to the scene
# redraw so the visible part of the image is emitted around the floating pane.
IMAGE_PANE=$($TMUX2 new-window -dP -F '#{pane_id}' \
"cat '$FIXTURE'; exec sh") || exit 1
$TMUX2 set -g pane-border-status top || exit 1
$TMUX2 set -pt"$IMAGE_PANE" pane-border-format 'IMAGE STATUS' || exit 1
FLOATING_PANE=$($TMUX2 new-pane -dP -F '#{pane_id}' -x 20 -y 6 -X 10 -Y 4 \
"printf 'FLOATING PANE'; sleep 10") || exit 1
$TMUX2 set -pt"$FLOATING_PANE" pane-border-format 'FLOAT STATUS' || exit 1
$TMUX2 select-pane -t"$FLOATING_PANE" || exit 1
sleep 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX2 send-keys -t"$IMAGE_PANE" -l \
"i=0; while [ \$i -lt 5 ]; do printf 'image-scroll-%d\\n' \$i; i=\$((i + 1)); done" || exit 1
$TMUX2 send-keys -t"$IMAGE_PANE" Enter || exit 1
sleep 1
$TMUX pipe-pane || exit 1
grep -a 'image-scroll-4' $TMP >/dev/null || exit 1
grep -a '"1;1;' $TMP >/dev/null || exit 1
$TMUX capture-pane -p >$TMP || exit 1
grep 'IMAGE STATUS' $TMP >/dev/null || exit 1
grep 'FLOAT STATUS' $TMP >/dev/null || exit 1
exit 0

View File

@@ -4,6 +4,9 @@ TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
# Keep panes alive until the next case or cleanup, without reading replies.
INPUT_HOLD='while :; do sleep 3600; done'
TMP=$(mktemp)
EXP=$(mktemp)
trap 'rm -f "$TMP" "$EXP"; $TMUX kill-server 2>/dev/null' 0 1 15
@@ -37,10 +40,10 @@ start_pane_hlimit()
$TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 1 -y 1 -s test-setup "sleep 2" || exit 1
$TMUX new-session -d -x 1 -y 1 -s test-setup "$INPUT_HOLD" || exit 1
$TMUX set-option -g history-limit "$hlimit" || exit 1
$TMUX new-session -d -x "$sx" -y "$sy" -s "$name" \
"printf '$seq'; sleep 2" || exit 1
"printf '$seq'; $INPUT_HOLD" || exit 1
$TMUX kill-session -t test-setup
sleep 0.3
}

View File

@@ -22,7 +22,19 @@ assert_key () {
clear-history -t$W \; \
send-keys -t$W "$key" 'EOL' || exit 1
actual_code=$($TMUX capturep -pt$W | \
# cat echoes the keys back asynchronously, so wait for the EOL marker
# to reach the pane instead of capturing straight away.
i=0
while [ $i -lt 50 ]; do
screen=$($TMUX capturep -pt$W)
case "$screen" in
*EOL*) break ;;
esac
i=$((i + 1))
sleep 0.1
done
actual_code=$(printf '%s\n' "$screen" | \
head -1 | \
sed -e 's/EOL.*$//')

View File

@@ -2,6 +2,38 @@
. ./input-common.inc
# Wait for the terminator and following text to reach the parser.
check_discard()
{
name=$1
printf 'OK\n' >"$EXP"
i=0
while [ "$i" -lt 300 ]; do
capture_grid "$name" >"$TMP"
cmp -s "$TMP" "$EXP" && return 0
sleep 0.05
i=$((i + 1))
done
fail "$name (timed out waiting for discard)"
}
# The missing-terminator timer may expire before the string limit is reached
# on a slow build. Clear any payload printed after the timeout, then check
# that the string did not change the title and normal output has resumed.
test_discard()
{
name=$1
prefix=$2
start_cmd "$name" 8 3 "$INPUT_HOLD"
$TMUX select-pane -T discard-test || exit 1
$TMUX respawn-pane -k \
"perl -e 'print qq{$prefix}, q{x} x 1100000, qq{\e\\\\\e[H\e[2JOK}'; $INPUT_HOLD" || exit 1
check_discard "$name"
$TMUX display-message -p -t "$name:" '#{pane_title}' >"$TMP" || exit 1
printf 'discard-test\n' >"$EXP"
cmp "$TMP" "$EXP" || fail "$name title"
}
start_cmd csi-param-discard 8 3 \
"perl -e 'print qq{\e[}, q{1} x 80, qq{\030OK}'; sleep 2"
check_capture csi-param-discard 'OK'
@@ -10,13 +42,8 @@ start_cmd csi-interm-discard 8 3 \
"perl -e 'print qq{\e[ \030OK}'; sleep 2"
check_capture csi-interm-discard 'OK'
start_cmd osc-discard 8 3 \
"perl -e 'print qq{\e]2;}, q{x} x 1100000, qq{\e\\\\OK}'; sleep 2"
check_capture osc-discard 'OK'
start_cmd apc-discard 8 3 \
"perl -e 'print qq{\e_}, q{x} x 1100000, qq{\e\\\\OK}'; sleep 2"
check_capture apc-discard 'OK'
test_discard osc-discard '\e]2;'
test_discard apc-discard '\e_'
start_pane unknown-csi 8 3 '\033[?9999zOK'
check_capture unknown-csi 'OK'

View File

@@ -51,7 +51,7 @@ $TMUX kill-server 2>/dev/null
sleep 0.1
$TMUX new-session -d -x 5 -y 3 -s history \; \
set-option -g history-limit 3 \; \
respawn-pane -k "printf '01\n02\n03\n04\n05\n06'; sleep 2" || exit 1
respawn-pane -k "printf '01\n02\n03\n04\n05\n06'; $INPUT_HOLD" || exit 1
sleep 0.3
$TMUX capture-pane -pN -t history: -S - -E - | normalize_capture >"$TMP"
printf "%s\n" '01

137
regress/json.sh Executable file
View File

@@ -0,0 +1,137 @@
#!/bin/sh
# Test parsing and printing JSON with display-message -j.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
fail()
{
echo "$1"
$TMUX kill-server 2>/dev/null
exit 1
}
check()
{
actual=$($TMUX display-message -plj "$1" 2>&1) ||
fail "JSON was rejected: $1: $actual"
[ "$actual" = "$2" ] ||
fail "JSON output mismatch: $1: expected $2, got $actual"
}
check_fail()
{
if $TMUX display-message -plj "$1" >/dev/null 2>&1; then
fail "invalid JSON was accepted: $1"
fi
}
$TMUX new-session -d || exit 1
check '{}' '{}'
check ' { "z":true, "a":-9223372036854775808, "m":[{}, {"x":false}] } ' \
'{"a":-9223372036854775808,"m":[{},{"x":false}],"z":true}'
check '{"max":9223372036854775807,"min":-9223372036854775808}' \
'{"max":9223372036854775807,"min":-9223372036854775808}'
check '{" key":" value","{key":"[value"}' \
'{" key":" value","{key":"[value"}'
check '{"brace":"{value","bracket":"[value","colon":":value",'\
'"comma":",value","space":" value"}' \
'{"brace":"{value","bracket":"[value","colon":":value",'\
'"comma":",value","space":" value"}'
check '{"esc":"\"\\\/\b\f\n\r\t\u0041"}' \
'{"esc":"\"\\\/\b\f\n\r\t\u0041"}'
check '{"unicode":"\u0123\uabcd\uABCD"}' \
'{"unicode":"\u0123\uabcd\uABCD"}'
actual=$($TMUX display-message -palj '{}' 2>&1) ||
fail "display-message -aj rejected JSON: $actual"
[ "$actual" = '{}' ] ||
fail "display-message -aj did not ignore -a: got $actual"
actual=$($TMUX display-message -pIlj '{}' 2>&1) ||
fail "display-message -Ij rejected JSON: $actual"
[ "$actual" = '{}' ] ||
fail "display-message -Ij did not ignore -I: got $actual"
check_fail ''
check_fail ' '
check_fail '[]'
check_fail 'true'
check_fail '1'
check_fail '"string"'
check_fail '{}{}'
check_fail '{'
check_fail '{x:1}'
check_fail '{"":1}'
check_fail '{"x" 1}'
check_fail '{"x",1}'
check_fail '{"x":}'
check_fail '{"x":,}'
check_fail '{"x":1'
check_fail '{"x":1 "y":2}'
check_fail '{"x":1,,"y":2}'
check_fail '{"x":1,}'
check_fail '{"x":1,"x":2}'
check_fail '{"x":{}}{}'
check_fail '{"x":{}} trailing'
check_fail '{"x":{'
check_fail '{"x":{}'
check_fail '{"x":['
check_fail '{"x":[}'
check_fail '{"x":[{}'
check_fail '{"x":[{},}'
check_fail '{"x":[{},]}'
check_fail '{"x":[{}{}]}'
check_fail '{"x":[{},,{}]}'
check_fail '{"x":[{"y":}]}'
check_fail '{"x":""}'
check_fail '{"x":null}'
check_fail '{"x":[null]}'
check_fail '{"x":[true]}'
check_fail '{"x":["string"]}'
check_fail '{"x":[[]]}'
check_fail '{"x":1.0}'
check_fail '{"x":1e2}'
check_fail '{"x":+1}'
check_fail '{"x":-}'
check_fail '{"x":0x10}'
check_fail '{"x":01}'
check_fail '{"x":-01}'
check_fail '{"x":[1]}'
check_fail '{"x":9223372036854775808}'
check_fail '{"x":-9223372036854775809}'
check_fail '{"x":tru}'
check_fail '{"x":True}'
check_fail '{"x":falsee}'
check_fail '{"x":"bad\q"}'
check_fail '{"x":"bad\x20"}'
check_fail '{"x":"bad\u123"}'
check_fail '{"x":"bad\u12x4"}'
check_fail '{"x":"unterminated}'
check_fail '{"x":"escaped quote\"}'
check_fail '{"x":"line
break"}'
# The maximum object nesting depth is 200.
json='{}'
n=1
while [ "$n" -lt 200 ]; do
json='{"x":'"$json"'}'
n=$((n + 1))
done
check "$json" "$json"
check_fail '{"x":'"$json"'}'
if $TMUX display-message -pj >/dev/null 2>&1; then
fail "display-message -j accepted a missing message"
fi
$TMUX kill-server 2>/dev/null
exit 0

28
regress/kill-session-zoomed.sh Executable file
View File

@@ -0,0 +1,28 @@
#!/bin/sh
# Killing a session whose window is zoomed must not crash the server:
# window_destroy used to unzoom the window, which resized the panes and
# fired pane-resized with the window in the payload, and releasing that
# reference destroyed the window a second time.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX -f/dev/null new -d -sfoo || exit 1
$TMUX split-window -d -h -tfoo:0 || exit 1
$TMUX resize-pane -Z -tfoo:0 || exit 1
$TMUX new -d -sbar || exit 1
$TMUX kill-session -tfoo || exit 1
$TMUX has-session -tbar || exit 1
# The same with the zoomed window in a session killed by kill-server.
$TMUX new -d -sbaz || exit 1
$TMUX split-window -d -v -tbaz:0 || exit 1
$TMUX resize-pane -Z -tbaz:0 || exit 1
$TMUX kill-server || exit 1
exit 0

1020
regress/layout-custom.sh Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
#!/bin/sh
# Version 1 layouts must reject excessive nesting without losing the session or
# changing its layout. Check both split types and the depth limit boundary.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX="$TEST_TMUX -S$DIR/socket -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0
trap 'exit 1' 1 2 15
# Generate a checksum-valid v1 layout with one child in every nested group.
layout()
{
awk -v depth="$1" -v open="$2" 'BEGIN {
closing = (open == "{" ? "}" : "]")
body = ""
for (i = 0; i < depth; i++)
body = body "1x1,0,0" open
body = body "1x1,0,0"
for (i = 0; i < depth; i++)
body = body closing
ord["0"] = 48; ord["1"] = 49; ord["x"] = 120
ord[","] = 44; ord["{"] = 123; ord["}"] = 125
ord["["] = 91; ord["]"] = 93
csum = 0
for (i = 1; i <= length(body); i++) {
c = substr(body, i, 1)
bit = csum % 2
csum = int(csum / 2) + bit * 32768
csum = (csum + ord[c]) % 65536
}
printf "%04x,%s", csum, body
}'
}
$TMUX new-session -d -s deep -x80 -y24 'exec sleep 100' || exit 1
for open in '{' '['; do
# A valid checksum and nesting up to the limit must still be accepted.
value=$(layout 1000 "$open") || fail "could not generate layout"
$TMUX select-layout -t deep "$value" ||
fail "layout at the depth limit was rejected ($open)"
before=$($TMUX display-message -p -t deep \
'#{pane_id} #{pane_width} #{pane_height} #{window_layout}') || exit 1
for depth in 1001 1500; do
value=$(layout "$depth" "$open") || fail "could not generate layout"
$TMUX select-layout -t deep "$value" >/dev/null 2>&1 &&
fail "excessive nesting was accepted ($open, $depth)"
$TMUX has-session -t deep ||
fail "server died on excessive nesting ($open, $depth)"
after=$($TMUX display-message -p -t deep \
'#{pane_id} #{pane_width} #{pane_height} #{window_layout}') || exit 1
[ "$before" = "$after" ] ||
fail "pane or layout changed after rejection ($open, $depth)"
done
done
exit 0

126
regress/layout-set.sh Normal file
View File

@@ -0,0 +1,126 @@
#!/bin/sh
# Exercise every predefined layout, next/previous cycling, lookup and restore
# paths, one-pane early returns, minimum-size handling and floating panes.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -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"
SEEN="$DIR/layouts"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
layout()
{
$TMUX display-message -p -t layouts:many '#{window_layout}'
}
check_layout()
{
name=$1
$TMUX select-layout -t layouts:many "$name" ||
fail "select-layout $name failed"
value=$(layout)
[ -n "$value" ] || fail "$name produced an empty layout"
if grep -Fxq "$value" "$SEEN"; then
fail "$name produced the same layout as an earlier preset"
fi
printf '%s\n' "$value" >>"$SEEN"
[ "$($TMUX list-panes -t layouts:many -F x | wc -l)" -eq 5 ] ||
fail "$name lost a pane"
[ "$($TMUX list-panes -t layouts:many -F '#{pane_floating_flag}' | \
grep -c '^1$')" -eq 1 ] || fail "$name did not preserve the floating pane"
}
$TMUX new-session -d -s layouts -n many -x100 -y40 'exec sleep 100' || exit 1
$TMUX set-option -g window-size manual || exit 1
$TMUX split-window -d -h -t layouts:many 'exec sleep 100' || exit 1
$TMUX split-window -d -v -t layouts:many 'exec sleep 100' || exit 1
$TMUX split-window -d -h -t layouts:many 'exec sleep 100' || exit 1
floating=$($TMUX new-pane -dP -F '#{pane_id}' -t layouts:many \
-x20 -y8 -X10 -Y5 'exec sleep 100') || exit 1
: >"$SEEN"
# Options exercise percentage and explicit secondary-size calculations.
$TMUX set-option -w -t layouts:many main-pane-height 30% || exit 1
$TMUX set-option -w -t layouts:many other-pane-height 12 || exit 1
$TMUX set-option -w -t layouts:many main-pane-width 35% || exit 1
$TMUX set-option -w -t layouts:many other-pane-width 20 || exit 1
for name in even-horizontal even-vertical main-horizontal \
main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do
check_layout "$name"
done
# Exact and unique-prefix lookup work; ambiguous and invalid names fail.
$TMUX select-layout -t layouts:many even-h || fail "unique layout prefix failed"
$TMUX select-layout -t layouts:many main >/dev/null 2>&1 &&
fail "ambiguous layout prefix succeeded"
$TMUX select-layout -t layouts:many no-such-layout >/dev/null 2>&1 &&
fail "invalid layout name succeeded"
# Cycle through all layouts in both directions, including wraparound. The
# aliases and select-layout flags share layout_set_next/previous.
first=$(layout)
i=0
while [ "$i" -lt 7 ]; do
$TMUX next-layout -t layouts:many || exit 1
i=$((i + 1))
done
[ "$(layout)" = "$first" ] || fail "next-layout did not wrap"
i=0
while [ "$i" -lt 7 ]; do
$TMUX previous-layout -t layouts:many || exit 1
i=$((i + 1))
done
[ "$(layout)" = "$first" ] || fail "previous-layout did not wrap"
$TMUX select-layout -t layouts:many -n || exit 1
$TMUX select-layout -t layouts:many -p || exit 1
# No argument reapplies the last preset; -o restores the previous layout; -E
# spreads the current cell without changing the pane set.
$TMUX kill-pane -t "$floating" || exit 1
$TMUX select-layout -t layouts:many tiled || exit 1
tiled=$(layout)
$TMUX select-layout -t layouts:many even-horizontal || exit 1
$TMUX select-layout -t layouts:many -o || exit 1
[ "$(layout)" = "$tiled" ] || fail "select-layout -o did not restore layout"
$TMUX select-layout -t layouts:many || exit 1
$TMUX select-layout -t layouts:many -E || exit 1
# Each arranger has a deliberate one-pane early return.
$TMUX new-window -d -t layouts: -n one 'exec sleep 100' || exit 1
one=$($TMUX display-message -p -t layouts:one '#{window_layout}')
for name in even-horizontal even-vertical main-horizontal \
main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do
$TMUX select-layout -t layouts:one "$name" || exit 1
[ "$($TMUX display-message -p -t layouts:one '#{window_layout}')" = "$one" ] ||
fail "$name changed a one-pane layout"
done
# Force the minimum-size paths without requiring a terminal client.
$TMUX resize-window -t layouts:many -x10 -y6 || exit 1
for name in even-horizontal even-vertical main-horizontal \
main-horizontal-mirrored main-vertical main-vertical-mirrored tiled; do
$TMUX select-layout -t layouts:many "$name" || exit 1
done
$TMUX has-session -t layouts || fail "server died during layout tests"
exit 0

46
regress/list-commands.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/bin/sh
# Exercise list-commands for the full table, a single command, aliases, custom
# and empty formats, and lookup errors.
PATH=/bin:/usr/bin
TERM=screen
export PATH TERM
[ -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"
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
# Keep a server alive across commands. Otherwise a STARTSERVER-only command
# may exit between invocations and leave a short socket-cleanup race.
$TMUX new-session -d -s list-commands 'exec sleep 100' || exit 1
all=$($TMUX list-commands) || exit 1
printf '%s\n' "$all" | grep -q '^attach-session (attach)' || exit 1
printf '%s\n' "$all" | grep -q '^list-commands (lscm)' || exit 1
one=$($TMUX list-commands -F \
'#{command_list_name}|#{command_list_alias}|#{command_list_usage}' \
list-commands) || exit 1
case "$one" in
'list-commands|lscm|'*) ;;
*) exit 1 ;;
esac
# Aliases resolve to their command, while an empty expansion prints no line.
[ "$($TMUX list-commands -F '#{command_list_name}' lscm)" = list-commands ] ||
exit 1
[ -z "$($TMUX list-commands -F '' list-commands)" ] || exit 1
$TMUX list-commands tmux-no-command-$$ >/dev/null 2>&1 && exit 1
$TMUX list-commands list >/dev/null 2>&1 && exit 1
exit 0

View File

@@ -105,6 +105,24 @@ drag()
sleep 1
}
meta_drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<8;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<40;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<8;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
check_ok new-session -d -s modal -x 80 -y 24 'cat'
@@ -127,12 +145,16 @@ case "$(fmt "$modal" '#{pane_flags}')" in
*O*) ;;
*) fail "modal pane flags do not include O" ;;
esac
case "$(fmt "$modal" '#{pane_flags}')" in
*A*) ;;
*) fail "modal pane flags do not include A" ;;
esac
case "$(fmt modal:0 '#{window_flags}')" in
*O*) ;;
*) fail "modal window flags do not include O" ;;
esac
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
check_fail "window already has a modal pane" \
new-pane -O -x 10 -y 4 'cat'
@@ -159,12 +181,14 @@ under=$($TMUX split-window -PF '#{pane_id}' -t "$p0" 'cat') ||
sleep 1
must_equal "$(fmt modal:0 '#{pane_id}')" "$modal"
must_equal "$(fmt "$under" '#{pane_active}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
float=$($TMUX new-pane -PF '#{pane_id}' -x 10 -y 4 -X 5 -Y 3 'cat') ||
fail "new floating pane under modal failed"
sleep 1
must_equal "$(fmt modal:0 '#{pane_id}')" "$modal"
must_equal "$(fmt "$float" '#{pane_active}')" 0
must_equal "$(fmt modal:0 '#{window_zoomed_flag}')" 1
check_ok new-window -d -t modal: -n other 'cat'
check_ok select-window -t modal:other
@@ -303,6 +327,306 @@ check_ok kill-pane -t "$modal"
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
must_equal "$(fmt modal:0 '#{pane_id}')" "$p0"
must_equal "$(fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 0:0
ignored=$($TMUX new-pane -KdPF '#{pane_id}' -t "$p0" 'cat') ||
fail "new-pane -K without -O failed"
check_ok kill-pane -t "$ignored"
$TMUX set -g @modal-prefix no
$TMUX set -g @modal-root no
$TMUX bind -n z set -g @modal-root yes
modal=$($TMUX new-pane -ODKPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "new-pane -ODK failed"
sleep 1
$TMUX2 send-keys -t "$OUTER" C-b x z Enter
sleep 1
must_equal "$($TMUX show -gv @modal-prefix)" no
must_equal "$($TMUX show -gv @modal-root)" no
case "$($TMUX capture-pane -pt "$modal")" in
*xz*) ;;
*) fail "keys did not reach key-capturing modal pane" ;;
esac
left=$(fmt "$modal" '#{pane_left}')
top=$(fmt "$modal" '#{pane_top}')
meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4))
new_left=$(fmt "$modal" '#{pane_left}')
new_top=$(fmt "$modal" '#{pane_top}')
[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] ||
fail "key-capturing modal pane did not move"
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
modal=$($TMUX new-pane -ODKPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'trap "" INT; exec cat') ||
fail "new-pane -ODK failed"
sleep 1
$TMUX2 send-keys -t "$OUTER" C-c
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
# A dead modal does not close on Escape or C-c without -D.
modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'sleep 1') ||
fail "new-pane -O failed"
check_ok set-option -p -t "$modal" remain-on-exit on
sleep 2
must_equal "$(fmt "$modal" '#{pane_dead}:#{pane_modal_flag}')" 1:1
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
check_ok kill-pane -t "$modal"
sleep 1
# failed-key closes successful panes and retains failed panes until a key.
modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'sleep 1') ||
fail "new-pane -O failed"
check_ok set-option -p -t "$modal" remain-on-exit failed-key
sleep 2
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
modal=$($TMUX new-pane -OPF '#{pane_id}' -t "$p0" \
-x 20 -y 5 -X 20 -Y 10 'sleep 1; exit 1') ||
fail "new-pane -O failed"
check_ok set-option -p -t "$modal" remain-on-exit failed-key
sleep 2
must_equal "$(fmt "$modal" '#{pane_dead}:#{pane_modal_flag}')" 1:1
must_equal "$($TMUX show-options -pv -t "$modal" remain-on-exit)" failed-key
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
$TMUX bind P display-popup -E -t "$p0" -w 20 -h 5 -T popup-title 'cat'
$TMUX2 send-keys -t "$OUTER" C-b P
sleep 1
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "display-popup did not create a modal pane"
must_equal "$(fmt "$modal" '#{pane_title}')" popup-title
must_equal "$($TMUX show-options -pv -t "$modal" pane-border-status)" top
must_equal "$($TMUX show-options -pv -t "$modal" pane-border-format)" \
'#{pane_title}'
$TMUX2 send-keys -t "$OUTER" C-b x z Enter
sleep 1
must_equal "$($TMUX show -gv @modal-prefix)" no
must_equal "$($TMUX show -gv @modal-root)" no
case "$($TMUX capture-pane -pt "$modal")" in
*xz*) ;;
*) fail "keys did not reach display-popup pane" ;;
esac
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
$TMUX2 send-keys -t "$OUTER" C-c
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
# Creating a popup pane must not fire the split-window hook.
check_ok set-hook -t modal after-split-window \
"set-option -g @popup-after-split yes"
check_ok set-option -g @popup-after-split no
check_ok display-popup -E -t "$p0" true
must_equal "$($TMUX show-option -gv @popup-after-split)" no
check_ok set-hook -u -t modal after-split-window
# A borderless popup must not create a zero-sized pane in a tiny window.
check_ok new-window -d -t modal: -n popup-small 'cat'
check_ok set-option -w -t modal:popup-small window-size manual
check_ok resize-window -t modal:popup-small -x 1 -y 1
small=$(fmt modal:popup-small '#{pane_id}')
check_ok bind Z display-popup -B -t "$small" 'cat'
$TMUX2 send-keys -t "$OUTER" C-b Z
sleep 1
must_equal "$(fmt modal:popup-small '#{window_panes}')" 1
must_equal "$(fmt modal:popup-small '#{window_modal_pane}')" ''
$TMUX bind D display-popup -t "$p0" -w 20 -h 5 'printf done'
$TMUX2 send-keys -t "$OUTER" C-b D
sleep 2
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "retained display-popup was not created"
must_equal "$(fmt "$modal" '#{pane_dead}')" 1
case "$($TMUX capture-pane -pt "$modal")" in
*'Pane is dead'*) fail "display-popup showed remain-on-exit message" ;;
esac
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
$TMUX bind K display-popup -k -t "$p0" -w 20 -h 5 'printf done'
$TMUX2 send-keys -t "$OUTER" C-b K
sleep 2
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "display-popup -k was not created"
must_equal "$(fmt "$modal" '#{pane_dead}')" 1
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
$TMUX bind F display-popup -EE -t "$p0" -w 20 -h 5 'exit 1'
$TMUX2 send-keys -t "$OUTER" C-b F
sleep 2
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "failed display-popup -EE did not remain"
must_equal "$(fmt "$modal" '#{pane_dead}')" 1
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
$TMUX2 send-keys -t "$OUTER" Escape
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" "$modal"
check_ok kill-pane -t "$modal"
sleep 1
check_ok display-popup -EE -t "$p0" true
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
$TMUX bind G display-popup -EE -k -t "$p0" -w 20 -h 5 'exit 1'
$TMUX2 send-keys -t "$OUTER" C-b G
sleep 2
modal=$(fmt modal:0 '#{window_modal_pane}')
[ -n "$modal" ] || fail "failed display-popup -EE -k did not remain"
must_equal "$(fmt "$modal" '#{pane_dead}')" 1
must_equal "$($TMUX show-options -pv -t "$modal" remain-on-exit)" failed-key
$TMUX2 send-keys -t "$OUTER" a
sleep 1
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
check_ok display-popup -EE -k -t "$p0" true
must_equal "$(fmt modal:0 '#{window_modal_pane}')" ''
# A nonmodal floating pane may remain above zoom, and switching between it and
# the zoomed tiled pane must not unzoom the window.
check_ok new-window -d -t modal: -n float-over-zoom 'cat'
base=$(fmt modal:float-over-zoom '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
check_ok resize-pane -Z -t "$base"
over=$($TMUX new-pane -APF '#{pane_id}' -t "$base" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "new-pane -A failed"
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1
case "$(fmt "$over" '#{pane_flags}')" in
*A*) ;;
*) fail "float-over-zoom pane flags do not include A" ;;
esac
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok select-pane -t "$base"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok select-pane -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
client=$($TMUX list-clients -F '#{client_name}' | head -1)
[ -n "$client" ] || fail "no client for switch-client test"
check_ok switch-client -c "$client" -t "$base"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok switch-client -c "$client" -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok kill-pane -t "$over"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok resize-pane -Z -t "$base"
ignored=$($TMUX new-pane -ALdPF '#{pane_id}' -t "$base" 'cat') ||
fail "new-pane -A -L failed"
case "$(fmt "$ignored" '#{pane_flags}')" in
*A*) fail "tiled pane flags include A" ;;
*) ;;
esac
# Existing floating panes are filtered when zoom begins: -A panes remain in the
# visible layout and ordinary floating panes do not.
check_ok new-window -d -t modal: -n existing-over-zoom 'cat'
base=$(fmt modal:existing-over-zoom '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 20 -y 5 -X 20 -Y 10 'cat') ||
fail "pre-existing new-pane -A failed"
under=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 15 -y 4 -X 2 -Y 2 'cat') ||
fail "pre-existing ordinary new-pane failed"
check_ok select-window -t modal:existing-over-zoom
check_ok select-pane -t "$base"
check_ok resize-pane -Z -t "$base"
must_equal "$(fmt "$over" '#{pane_floating_flag}')" 1
must_equal "$(fmt "$under" '#{pane_floating_flag}')" 0
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
# Geometry changed in the visible zoom layout is copied back when unzooming.
check_ok select-pane -t "$over"
left=$(fmt "$over" '#{pane_left}')
top=$(fmt "$over" '#{pane_top}')
meta_drag $((left + 2)) $((top + 2)) $((left + 7)) $((top + 4))
new_left=$(fmt "$over" '#{pane_left}')
new_top=$(fmt "$over" '#{pane_top}')
[ "$new_left" -gt "$left" ] || [ "$new_top" -gt "$top" ] ||
fail "float-over-zoom pane did not move"
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
check_ok resize-pane -Z -t "$base"
must_equal "$(fmt "$over" '#{pane_left}:#{pane_top}')" \
"$new_left:$new_top"
must_equal "$(fmt "$under" '#{pane_floating_flag}')" 1
# Resizing with the over-zoom pane active restores the original zoom target.
check_ok resize-pane -Z -t "$base"
check_ok select-pane -t "$over"
check_ok resize-window -t modal:existing-over-zoom -x 90 -y 30
must_equal "$(fmt "$base" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}')" \
90x30:1:1
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_active}')" 1:1
# Natural pane exit uses a different removal path from kill-pane and must also
# preserve zoom.
dying=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 12 -y 4 -X 4 -Y 3 'true') ||
fail "short-lived new-pane -A failed"
i=0
while $TMUX list-panes -a -F '#{pane_id}' | grep -qx "$dying"; do
i=$((i + 1))
[ $i -gt 50 ] && fail "short-lived float-over-zoom pane did not exit"
sleep 0.1
done
must_equal "$(fmt "$base" '#{window_zoomed_flag}:#{pane_zoomed_flag}')" 1:1
# A pane with -A is also above a zoom target which was itself floating. The
# temporary tiled target must sit behind retained floating panes, then return
# to the normal floating z order when unzoomed.
check_ok new-window -d -t modal: -n floating-zoom-target 'cat'
base=$(fmt modal:floating-zoom-target '#{pane_id}')
check_ok split-window -dh -t "$base" 'cat'
target=$($TMUX new-pane -dPF '#{pane_id}' -t "$base" \
-x 30 -y 10 -X 10 -Y 5 'cat') ||
fail "floating zoom target creation failed"
over=$($TMUX new-pane -AdPF '#{pane_id}' -t "$base" \
-x 15 -y 5 -X 15 -Y 8 'cat') ||
fail "float-over-zoom pane creation failed"
check_ok select-window -t modal:floating-zoom-target
check_ok select-pane -t "$target"
must_equal "$(fmt "$target" '#{pane_z}')" 0
must_equal "$(fmt "$over" '#{pane_z}')" 1
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$target" \
'#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_floating_flag}:#{pane_z}')" \
1:1:0:2
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0
check_ok select-pane -t "$over"
must_equal "$(fmt "$over" '#{window_zoomed_flag}:#{pane_active}')" 1:1
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$over" '#{pane_active}:#{pane_z}')" 1:0
must_equal "$(fmt "$target" '#{pane_floating_flag}:#{pane_z}')" 1:1
# If the target remains active, it returns to the front on unzoom.
check_ok select-pane -t "$target"
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$over" '#{pane_floating_flag}:#{pane_z}')" 1:0
check_ok resize-pane -Z -t "$target"
must_equal "$(fmt "$target" '#{pane_active}:#{pane_z}')" 1:0
must_equal "$(fmt "$over" '#{pane_z}')" 1
cleanup
exit 0

0
regress/mode-kill.sh Executable file → Normal file
View File

View File

@@ -0,0 +1,71 @@
#!/bin/sh
# Chooser menus are centred on the mouse using their displayed width. A
# hidden title must not move a borderless menu away from the mouse.
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
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
$TMUX new-session -d -s short -x 100 -y 30 'sleep 100' || exit 1
$TMUX set -g mouse on || exit 1
$TMUX set -g status off || exit 1
$TMUX2 new-session -d -x 100 -y 30 "$TMUX attach" || exit 1
sleep 1
for name in short aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
$TMUX rename-session "$name" || exit 1
for lines in none single padded; do
$TMUX set -g menu-border-lines "$lines" || exit 1
$TMUX choose-tree -s || exit 1
sleep 1
# Right-button press at window (60, 0), away from either edge.
seq=$(printf '\033[<2;61;1M')
$TMUX2 send-keys -l "$seq" || exit 1
sleep 1
row=2
column=54
if [ "$lines" = none ]; then
row=1
elif [ "$name" != short ]; then
column=31
fi
text=$($TMUX2 capture-pane -p | awk -v row="$row" \
-v column="$column" 'NR == row {
# Keep byte offsets equal to columns with non-Unicode awk.
gsub(/│/, "|")
print substr($0, column, 7)
}')
[ "$text" = 'Select ' ] || \
fail "$lines menu for $name: expected Select at ($column, $row), got '$text'"
# Close the menu, release the button, then leave the chooser.
$TMUX2 send-keys Escape || exit 1
seq=$(printf '\033[<2;61;1m')
$TMUX2 send-keys -l "$seq" || exit 1
$TMUX2 send-keys q || exit 1
sleep 1
done
done
exit 0

File diff suppressed because one or more lines are too long

View File

@@ -86,6 +86,11 @@ check_value "-gv status-keys" "vi"
check_fail "unknown value: bogus" set -g status-keys bogus
check_value "-gv status-keys" "vi"
# pane-border-lines accepts rounded as a pane border style.
check_ok set -gw pane-border-lines rounded
check_value "-gwv pane-border-lines" "rounded"
check_ok set -gw pane-border-lines single
# --- flag options ---------------------------------------------------------
#
# focus-events is an on/off flag. Setting with no value toggles it; explicit

View File

@@ -19,7 +19,10 @@
# - respawn-pane/respawn-window refusing a live pane without -k, working on a
# dead pane (remain-on-exit) and killing with -k;
# - resize-pane -x/-y in cells and percent, -L/-R/-U/-D adjustments and -Z
# zoom/unzoom (including implicit unzoom on split).
# zoom/unzoom, layout round trips, zoom target selection, pane removal,
# window resizing and implicit unzoom on layout changes;
# - swap-pane preserving one or two zoomed windows, including cross-window
# swaps of hidden panes and zoom targets.
#
# window-ops.sh covers window-level commands and buffers.sh paste buffers.
@@ -101,6 +104,25 @@ assert_alive()
fi
}
# Compare layout geometry and pane ordering, ignoring the active pane and
# last-pane history in the JSON layout. Selection is checked separately.
pane_layout()
{
$TMUX display-message -p -t "$1" '#{window_layout}' |
sed 's/,"a":true//g; s/,"l":[0-9][0-9]*//g'
}
check_layout()
{
out=$(pane_layout "$1")
if [ "$out" != "$2" ]; then
echo "Layout for '$1' wrong."
echo "Expected: '$2'"
echo "But got: '$out'"
exit 1
fi
}
# ---------------------------------------------------------------------------
# split-window geometry.
@@ -144,7 +166,8 @@ check_panes P:0 "0:$p3 1:$p0 2:$p2 3:$p1"
# break-pane and join-pane.
# break-pane moves a pane to a new window; -P -F prints where it went and -n
# names the new window.
# names the new window. Moving a pane out of a zoomed window unzooms it first.
check_ok resize-pane -Z -t "$p0"
out=$($TMUX break-pane -d -P -F '#{window_index}:#{pane_id}' -n broken \
-s "$p1" -t P:)
if [ "$out" != "1:$p1" ]; then
@@ -153,10 +176,14 @@ if [ "$out" != "1:$p1" ]; then
fi
check_fmt 'P:1' '#{window_name}:#{window_panes}' 'broken:1'
check_fmt 'P:0' '#{window_panes}' '3'
check_fmt 'P:0' '#{window_zoomed_flag}' '0'
# join-pane -v moves it back (the source window, left empty, is destroyed).
# join-pane -v moves it back (the source window, left empty, is destroyed) and
# also unzooms the destination before changing its layout.
check_ok resize-pane -Z -t "$p0"
check_ok join-pane -d -v -s P:broken.0 -t "$p2"
check_fmt 'P:0' '#{window_panes}' '4'
check_fmt 'P:0' '#{window_zoomed_flag}' '0'
if $TMUX has-session -t P:broken 2>/dev/null; then
echo "Window 'broken' still exists after join-pane."
exit 1
@@ -299,6 +326,122 @@ check_fmt 'P:0' '#{window_panes}' '5'
p6=$($TMUX display-message -p -t P:0.2 '#{pane_id}')
check_ok kill-pane -t "$p6"
# Zoom and unzoom preserve the exact tiled layout. Selecting another pane
# without -Z unzooms, while -Z transfers zoom to the selected pane.
layout=$(pane_layout P:0)
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -t "$p2"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1'
check_layout P:0 "$layout"
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -Z -t "$p2"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p2"
check_layout P:0 "$layout"
# Directional selection temporarily restores the full layout to find its
# neighbour, then follows the same unzoom or -Z transfer rules.
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -t "$p0"
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok select-pane -D -t "$p0"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_active}' '0:1'
check_ok select-pane -t "$p0"
check_ok resize-pane -Z -t "$p0"
check_ok select-pane -D -Z -t "$p0"
check_fmt "$p2" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p2"
check_layout P:0 "$layout"
# The last-pane path has separate zoom handling, both with and without -Z.
check_ok select-pane -t "$p0"
check_ok select-pane -t "$p2"
check_ok resize-pane -Z -t "$p2"
check_ok select-pane -l -t P:0
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'0:0:1'
check_ok select-pane -t "$p0"
check_ok select-pane -t "$p2"
check_ok resize-pane -Z -t "$p2"
check_ok select-pane -l -Z -t P:0
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}:#{pane_active}' \
'1:1:1'
check_ok resize-pane -Z -t "$p0"
check_layout P:0 "$layout"
# Killing either a hidden ordinary pane or the zoom target unzooms.
check_ok new-window -d -t P:12 -n zoom-kill 'cat'
zk0=$($TMUX display-message -p -t P:12.0 '#{pane_id}')
zk1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat')
zk2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:12.0 'cat')
check_ok resize-pane -Z -t "$zk0"
check_ok kill-pane -t "$zk1"
check_fmt "$zk0" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'2:0:0'
check_ok resize-pane -Z -t "$zk0"
check_ok kill-pane -t "$zk0"
check_fmt "$zk2" '#{window_panes}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'1:0:0'
check_ok kill-window -t P:12
# The same cases through natural process exit exercise server_destroy_pane.
check_ok new-window -d -t P:13 -n zoom-exit 'cat'
ze0=$($TMUX display-message -p -t P:13.0 '#{pane_id}')
ze1=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat')
ze2=$($TMUX split-window -d -P -F '#{pane_id}' -t P:13.0 'cat')
check_ok resize-pane -Z -t "$ze0"
check_ok send-keys -t "$ze1" C-d
i=0
while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 2 ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "Hidden pane did not exit."; exit 1; }
sleep 0.1
done
check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
check_ok resize-pane -Z -t "$ze2"
check_ok send-keys -t "$ze2" C-d
i=0
while [ "$($TMUX display-message -p -t P:13 '#{window_panes}')" != 1 ]; do
i=$((i + 1))
[ $i -gt 50 ] && { echo "Zoomed pane did not exit."; exit 1; }
sleep 0.1
done
check_fmt "$ze0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
check_ok kill-window -t P:13
# resize-window restores both tiled and floating zoom targets after rebuilding
# the layout at the new size.
check_ok new-window -d -t P:14 -n zoom-resize 'cat'
zr0=$($TMUX display-message -p -t P:14.0 '#{pane_id}')
check_ok split-window -d -t P:14.0 'cat'
check_ok resize-pane -Z -t "$zr0"
check_ok resize-window -t P:14 -x 90 -y 30
check_fmt "$zr0" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'90x30:1:1'
check_ok resize-pane -Z -t "$zr0"
zrf=$($TMUX new-pane -dP -F '#{pane_id}' -t P:14 -x 20 -y 6 'cat')
zrf_size=$($TMUX display-message -p -t "$zrf" \
'#{pane_width}x#{pane_height}')
check_ok resize-pane -Z -t "$zrf"
check_ok resize-window -t P:14 -x 100 -y 32
check_fmt "$zrf" \
'#{window_width}x#{window_height}:#{window_zoomed_flag}:#{pane_zoomed_flag}' \
'100x32:1:1'
check_ok resize-pane -Z -t "$zrf"
check_fmt "$zrf" '#{pane_floating_flag}:#{pane_width}x#{pane_height}' \
"1:$zrf_size"
check_ok kill-window -t P:14
# ---------------------------------------------------------------------------
# kill-pane.
@@ -307,8 +450,10 @@ check_ok kill-pane -t "$p3"
check_panes P:0 "0:$p0 1:$p2 2:$p1"
# -a kills every pane except the target.
check_ok resize-pane -Z -t "$p0"
check_ok kill-pane -a -t "$p0"
check_panes P:0 "0:$p0"
check_fmt "$p0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '0:0'
# Killing the last pane in a window kills the window.
check_ok new-window -d -t P:7 -n goner
@@ -516,6 +661,10 @@ check_panes P:3 "0:$r0 1:$r1 2:$r2"
# Swapping a pane with itself quietly does nothing.
check_ok swap-pane -d -s "$r1" -t "$r1"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_ok resize-pane -Z -t "$r1"
check_ok swap-pane -d -Z -s "$r1" -t "$r1"
check_fmt "$r1" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1'
check_ok resize-pane -Z -t "$r1"
# Panes can be swapped between different windows.
check_ok swap-pane -d -s "$o0" -t "$r1"
@@ -525,10 +674,38 @@ check_ok swap-pane -d -s "$r1" -t "$o0"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_panes P:5 "0:$o0"
# With both windows zoomed, -Z preserves each window's zoom when hidden panes
# are exchanged and when the zoom targets themselves are exchanged.
o1=$($TMUX split-window -dP -F '#{pane_id}' -t P:5.0)
check_ok resize-pane -Z -t "$r0"
check_ok resize-pane -Z -t "$o0"
check_ok swap-pane -d -Z -s "$r1" -t "$o1"
check_fmt "$r0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1'
check_fmt "$o0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1'
check_fmt "$r1" '#{window_name}' 'other'
check_fmt "$o1" '#{window_name}' 'swaps'
check_ok swap-pane -d -Z -s "$r0" -t "$o0"
check_fmt "$o0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'swaps:1:1'
check_fmt "$r0" \
'#{window_name}:#{window_zoomed_flag}:#{pane_zoomed_flag}' 'other:1:1'
check_ok resize-pane -Z -t "$o0"
check_ok resize-pane -Z -t "$r0"
# Restore the original panes before the remaining swap tests.
check_ok swap-pane -d -s "$r0" -t "$o0"
check_ok swap-pane -d -s "$r1" -t "$o1"
check_ok kill-pane -t "$o1"
check_panes P:3 "0:$r0 1:$r1 2:$r2"
check_panes P:5 "0:$o0"
# -Z keeps the window zoomed across the swap.
check_ok resize-pane -Z -t "$r0"
check_ok swap-pane -d -Z -s "$r0" -t "$r1"
check_fmt 'P:3' '#{window_zoomed_flag}' '1'
check_fmt "$r0" '#{window_zoomed_flag}:#{pane_zoomed_flag}' '1:1'
check_ok resize-pane -Z -t P:3
check_panes P:3 "0:$r1 1:$r0 2:$r2"

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