Compare commits

..

54 Commits

Author SHA1 Message Date
Nicholas Marriott
6973c6b221 Damage rectanges. 2026-09-24 11:49:24 +01:00
tmux update bot
a4a2501b2f Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add margins for WezTerm and ghostty, from Michael Grant.
2026-09-24 09:44:56 +00:00
nicm
aa4123ab58 Add margins for WezTerm and ghostty, from Michael Grant. 2026-09-24 09:44:54 +00:00
Nicholas Marriott
6bab150c58 Fix test. 2026-09-24 09:15:21 +01:00
tmux update bot
72e83b000b Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Note about default-shell, from Meriel Sartha Mittelbach.
2026-09-23 16:48:10 +00:00
nicm
a4c051f27d Note about default-shell, from Meriel Sartha Mittelbach. 2026-09-23 16:48:08 +00:00
Nicholas Marriott
56373a08ee Merge pull request #5628 from arthurscchan/new-fuzzer
OSS-Fuzz: Add new fuzzer targets layout processing
2026-09-23 12:03:00 +01:00
tmux update bot
5e860c4e6c Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Get x and y the right way round, fixes crash when opening a mode prompt when the client is offset into the window; may be the crash reported by sthen@.
2026-09-22 20:02:41 +00:00
nicm
183c3432b0 Get x and y the right way round, fixes crash when opening a mode prompt
when the client is offset into the window; may be the crash reported by
sthen@.
2026-09-22 20:02:40 +00:00
tmux update bot
d1ee699dd1 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Some terminals have extended DECSCUSR to allow it to reset to the default style and put that as the Se capability in their terminfo entry. Change so that if Ss and Se exist, tmux does not overwrite them with its own. GitHub issue 5476.
2026-09-22 16:48:21 +00:00
nicm
e7618f41c2 Some terminals have extended DECSCUSR to allow it to reset to the
default style and put that as the Se capability in their terminfo entry.
Change so that if Ss and Se exist, tmux does not overwrite them with its
own. GitHub issue 5476.
2026-09-22 16:48:18 +00:00
Arthur Chan
be75a67b1f OSS-Fuzz: Add new fuzzer targets layout processing
Signed-off-by: Arthur Chan <arthur.chan@adalogics.com>
2026-09-22 16:45:24 +01:00
Nicholas Marriott
83c8962cf9 Some tests. 2026-09-22 14:25:51 +01:00
tmux update bot
fca57a5fc9 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add a feature for mintty's application escape to avoid dodgy terminals whinging about not supporting it, GitHub issue 5626 from Pete Dietl.
  Only rely on the previous line wrapping when the cursor is actually on it, GitHub issue 5625 from Ben Maurer.
  Correctly write Z indexes to v2 layouts if zoomed, GitHub issue 5624.
  Do not leak waiting clients (in wait-for) if they are killed, GitHub issue 5614.
2026-09-22 11:58:43 +00:00
nicm
83f62d0acc Add a feature for mintty's application escape to avoid dodgy terminals
whinging about not supporting it, GitHub issue 5626 from Pete Dietl.
2026-09-22 11:58:41 +00:00
nicm
19b7660778 Only rely on the previous line wrapping when the cursor is actually on
it, GitHub issue 5625 from Ben Maurer.
2026-09-22 11:58:41 +00:00
nicm
4624bc2129 Correctly write Z indexes to v2 layouts if zoomed, GitHub issue 5624. 2026-09-22 11:58:41 +00:00
nicm
0e1f6ed80d Do not leak waiting clients (in wait-for) if they are killed, GitHub
issue 5614.
2026-09-22 11:58:41 +00:00
Nicholas Marriott
21ea1c91c5 Merge pull request #5627 from pdietl/regress-input-keys-race
Wait for the pane to echo the key before capturing in input-keys.sh
2026-09-22 07:55:13 +01:00
Nicholas Marriott
cb9f9b588f Fix test. 2026-09-22 07:52:51 +01:00
Pete Dietl
d6db719b19 Wait for the pane to echo the key before capturing in input-keys.sh 2026-09-22 00:21:23 -05:00
tmux update bot
9860e60afa Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not cancel mark when click on border.
2026-09-21 22:31:27 +00:00
nicm
ca5c28d224 Do not cancel mark when click on border. 2026-09-21 22:31:26 +00:00
Nicholas Marriott
c3326194b6 Turn on Linux arm also. 2026-09-21 22:34:05 +01:00
Nicholas Marriott
6a8c21fb41 macOS parallel issues. 2026-09-21 22:27:43 +01:00
Nicholas Marriott
326b9a4f16 Another timing race. 2026-09-21 22:18:44 +01:00
Nicholas Marriott
6871fcb157 Add missing test cleanup. 2026-09-21 22:16:06 +01:00
Nicholas Marriott
1d9943c6e6 Keep window alive. 2026-09-21 22:12:33 +01:00
Nicholas Marriott
f5c4998db3 Make regress parallel friendly. 2026-09-21 22:00:47 +01:00
Nicholas Marriott
cc81429d8a Do not make test so timing sensitive. 2026-09-21 21:51:30 +01:00
tmux update bot
3a6c2e7877 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Do not do anything in session_detach and rely on the caller destroying the session, otherwise grouped sessions can reuse a session with no windows. From Jeong, Heon in GitHub issue 5620.
2026-09-21 18:56:57 +00:00
nicm
21b3da3bab Do not do anything in session_detach and rely on the caller destroying
the session, otherwise grouped sessions can reuse a session with no
windows. From Jeong, Heon in GitHub issue 5620.
2026-09-21 18:56:55 +00:00
tmux update bot
a7bd2415ba Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Add support for borderless menus, like panes. GitHub issue 5447 from harikp2002 at gmail dot com.
  Do not trim all lines to make a zero line grid which reflow does not like, from Kaixuan Li.
  Do not unzoom when resizing a floating pane that was created with -A. Similarly, skip hidden floating panes when changing Z order. Reported by Clark Wang.
2026-09-21 13:44:41 +00:00
nicm
dda0e4d489 Add support for borderless menus, like panes. GitHub issue 5447 from harikp2002
at gmail dot com.
2026-09-21 13:44:39 +00:00
nicm
e361b8f8ff Do not trim all lines to make a zero line grid which reflow does not like, from
Kaixuan Li.
2026-09-21 13:44:39 +00:00
nicm
9be3a35178 Do not unzoom when resizing a floating pane that was created with -A.
Similarly, skip hidden floating panes when changing Z order. Reported by Clark
Wang.
2026-09-21 13:44:39 +00:00
Nicholas Marriott
324c636c5c Test for 5447 and for copy-mode zero line crash. 2026-09-21 13:14:44 +01:00
Nicholas Marriott
166851bf87 Update tests and bits after popups removed. 2026-09-21 12:05:37 +01:00
Nicholas Marriott
5aa17a0cfa Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  Remove popups and all the associated overlay machinery (they were the last user of it). display-popup stays but becomes an (undocumented) compatibility command to open a floating pane.
  Fix typo (too few 0s) in CLIENT_CONTROL_DISCARD, from someone in GitHub issue 5622.
2026-09-21 12:03:10 +01:00
nicm
34cd5da4e3 Remove popups and all the associated overlay machinery (they were the last user
of it). display-popup stays but becomes an (undocumented) compatibility command
to open a floating pane.
2026-09-21 11:01:12 +00:00
nicm
3d5f946f35 Fix typo (too few 0s) in CLIENT_CONTROL_DISCARD, from someone in GitHub issue
5622.
2026-09-21 11:01:12 +00:00
Nicholas Marriott
81794f3047 Test for newp -A and resize. 2026-09-21 11:39:09 +01:00
Thomas Adam
c612f35fd8 Merge remote-tracking branch 'refs/remotes/tmux-openbsd/master'
* refs/remotes/tmux-openbsd/master:
  build tmux with debug symbols, ok claudio nicm
  If no floating panes, reset default starting position, and reset if any part of the pane goes outside the window.
  Empty string for invalid old-style formats causes old iTerm2 versions to crash, so emit "0000," instead.
  Fix session_*_flag format variables which loop over the windows (they should only be false if all windows do not have the flag, not the first one). GitHub issue 5599.
  Do not loop forever if someone tries to give WCHAR_MAX a width, GitHub issue 5602.
  Expand -c for run-shell like the other -c flags, reported by Saúl Nogueras.
  Reset layout manually instead of calling window_unzoom which can go down the notification path and end up double freeing the pane (this was previously removed in 2015 but added back to fix a problem with late destroy - this is a better fix). GitHub issue 5591 from Romain Francoise.
2026-09-20 20:05:57 +01:00
sthen
56b36d671e build tmux with debug symbols, ok claudio nicm 2026-09-20 12:06:27 +00:00
nicm
eee95e6479 If no floating panes, reset default starting position, and reset if any part of
the pane goes outside the window.
2026-09-20 12:06:27 +00:00
nicm
bbd00148de Empty string for invalid old-style formats causes old iTerm2 versions to crash,
so emit "0000," instead.
2026-09-20 12:06:27 +00:00
nicm
2e9189c01d Fix session_*_flag format variables which loop over the windows (they should
only be false if all windows do not have the flag, not the first one). GitHub
issue 5599.
2026-09-20 12:06:27 +00:00
nicm
d4dc032599 Do not loop forever if someone tries to give WCHAR_MAX a width, GitHub issue
5602.
2026-09-20 12:06:27 +00:00
nicm
d1d07f9790 Expand -c for run-shell like the other -c flags, reported by Saúl Nogueras. 2026-09-20 12:06:27 +00:00
nicm
b25af0a08c Reset layout manually instead of calling window_unzoom which can go down the
notification path and end up double freeing the pane (this was previously
removed in 2015 but added back to fix a problem with late destroy - this is a
better fix). GitHub issue 5591 from Romain Francoise.
2026-09-20 12:06:27 +00:00
Nicholas Marriott
1eed85c344 Change tests for 0000, for invalid layouts. 2026-09-20 09:37:59 +01:00
Nicholas Marriott
73042686d8 Test for 5599. 2026-09-20 09:19:35 +01:00
Nicholas Marriott
313ad6fdc5 Tests for 5602 and for run-shell -c. 2026-09-20 09:11:24 +01:00
Nicholas Marriott
594465fbe7 Regress test for 5591 and another one I forget. 2026-09-20 08:44:56 +01:00
114 changed files with 5309 additions and 3679 deletions

View File

@@ -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
@@ -72,14 +72,14 @@ 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()

12
CHANGES
View File

@@ -111,18 +111,6 @@ CHANGES FROM 3.7c TO 3.8
* Change show-options and show-hooks to use formats and add a -F flag to each.
* Add support for the Kitty keyboard protocol. When extended-keys is on,
tmux uses it with terminals which support it, detected when a client
attaches or set with a new kittykeys terminal feature, and standard
extended keys or VT10x keys with other terminals, so terminals with
different capabilities may be attached at the same time. Programs inside
tmux may ask for either the Kitty protocol or modifyOtherKeys and get keys
in the form they asked for. The Kitty protocol can report the Super and
Hyper modifiers (key name prefixes s- and H-).
* Add a client_key_mode format giving the extended key reporting mode in
use for a client, alongside the existing pane_key_mode for a pane.
* 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.

View File

@@ -171,7 +171,6 @@ dist_tmux_SOURCES = \
grid.c \
hooks.c \
hyperlinks.c \
input-kitty.c \
input-keys.c \
input.c \
job.c \
@@ -189,7 +188,6 @@ dist_tmux_SOURCES = \
options-table.c \
options.c \
paste.c \
popup.c \
proc.c \
prompt-history.c \
prompt.c \
@@ -213,7 +211,6 @@ dist_tmux_SOURCES = \
tty-acs.c \
tty-draw.c \
tty-features.c \
tty-kitty.c \
tty-keys.c \
tty-term.c \
tty.c \
@@ -260,7 +257,8 @@ check_PROGRAMS = \
fuzz/input-fuzzer \
fuzz/cmd-parse-fuzzer \
fuzz/format-fuzzer \
fuzz/style-fuzzer
fuzz/style-fuzzer \
fuzz/layout-fuzzer
fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_input_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_cmd_parse_fuzzer_LDFLAGS = $(FUZZING_LIBS)
@@ -269,6 +267,8 @@ fuzz_format_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_format_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_style_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_style_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_layout_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_layout_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
endif
# Install tmux.1 in the right format.

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

@@ -91,8 +91,10 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag,
wp = target.wp;
if (wl != NULL) {
if (wp != NULL)
if (wp != NULL) {
window_redraw_active_switch(wp->window, wp);
window_set_active_pane(wp->window, wp, 1);
}
session_set_current(s, wl);
if (wp != NULL)
cmd_find_from_winlink_pane(current, wl, wp, 0);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-display-menu.c,v 1.53 2026/08/31 07:46:55 nicm Exp $ */
/* $OpenBSD: cmd-display-menu.c,v 1.55 2026/09/21 12:14:32 nicm Exp $ */
/*
* Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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,148 +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 (tc->flags & CLIENT_CONTROL)
return (CMD_RETURN_NORMAL);
if (!modify && tc->overlay_draw != NULL)
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-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);
@@ -299,7 +307,7 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
struct window *w;
struct window_pane *wp;
struct layout_cell *lc;
int y, ly, x, lx;
int y, ly, x, lx, oxoff, oyoff, osx, osy;
wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) {
@@ -321,10 +329,16 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
ly = m->statusat - 1;
if (x != lx || y != ly) {
oxoff = wp->xoff;
oyoff = wp->yoff;
osx = wp->sx;
osy = wp->sy;
lc->g.xoff += x - lx;
lc->g.yoff += y - ly;
layout_fix_panes(w, NULL);
server_redraw_window(w);
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
server_redraw_window_borders(w);
}
}
@@ -347,8 +361,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 +459,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-queue.c,v 1.123 2026/08/24 20:34:26 nicm Exp $ */
/* $OpenBSD: cmd-queue.c,v 1.124 2026/09/22 06:46:50 nicm Exp $ */
/*
* Copyright (c) 2013 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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) {

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-resize-pane.c,v 1.68 2026/08/31 07:44:39 nicm Exp $ */
/* $OpenBSD: cmd-resize-pane.c,v 1.69 2026/09/21 10:33:16 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -91,7 +91,8 @@ 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')) {
@@ -236,8 +237,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
struct window_pane *wp;
struct layout_cell *lc;
int y, ly, x, lx, sx, sy, new_sx, new_sy;
int left, right;
int new_xoff, new_yoff, resizes = 0;
int left, right, resizes = 0;
int new_xoff, new_yoff, old_xoff, old_yoff;
wp = cmd_mouse_pane(m, NULL, &wl);
if (wp == NULL) {
@@ -248,6 +249,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
lc = wp->layout_cell;
sx = wp->sx;
sy = wp->sy;
old_xoff = wp->xoff;
old_yoff = wp->yoff;
left = wp->xoff - 1;
right = wp->xoff + sx;
if (window_pane_scrollbar_reserve(wp) &&
@@ -347,7 +350,7 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
}
if (resizes != 0) {
layout_fix_panes(w, NULL);
server_redraw_window(w);
window_redraw_floating_pane(wp, old_xoff, old_yoff, sx, sy);
server_redraw_window_borders(w);
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: cmd-run-shell.c,v 1.94 2026/08/25 06:04:33 nicm Exp $ */
/* $OpenBSD: cmd-run-shell.c,v 1.95 2026/09/20 07:59:55 nicm Exp $ */
/*
* Copyright (c) 2009 Tiago Cunha <me@tiagocunha.org>
@@ -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));

View File

@@ -362,6 +362,7 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
enum pane_lines lines;
u_int sx, sy;
int x, y, xoff, yoff, border;
int oxoff, oyoff, osx, osy;
if (c->tty.mouse_last_pane == -1)
return;
@@ -417,8 +418,14 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
if (sy < PANE_MINIMUM)
sy = PANE_MINIMUM;
oxoff = wp->xoff;
oyoff = wp->yoff;
osx = wp->sx;
osy = wp->sy;
layout_set_size(lc, sx, sy, xoff, yoff);
layout_fix_panes(w, NULL);
server_redraw_window(w);
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
server_redraw_window_borders(w);
}

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,4 +1,4 @@
/* $OpenBSD: format.c,v 1.417 2026/09/08 15:42:26 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>
@@ -1617,41 +1617,6 @@ format_cb_client_height(struct format_tree *ft)
return (NULL);
}
/* Callback for client_key_mode. */
static void *
format_cb_client_key_mode(struct format_tree *ft)
{
struct client *c = ft->c;
const char *mode;
char *s;
int kitty, eks;
if (c == NULL || (~c->tty.flags & TTY_STARTED))
return (NULL);
if (c->tty.flags & TTY_KKBPUSHED) {
xasprintf(&s, "Kitty %u", c->tty.kitty_keys);
return (s);
}
/* This mirrors the choice made in tty_update_features(). */
kitty = ((c->tty.term->flags & TERM_KITTYKEYS) != 0);
eks = (options_get_number(global_options, "extended-keys") != 0);
if (kitty)
eks = 0;
if (eks && tty_term_has(c->tty.term, TTYC_ENEKS))
mode = "Ext";
else
mode = "VT10x";
/* The terminal supports Kitty keys but they are not in use. */
if (kitty) {
xasprintf(&s, "%s (Kitty)", mode);
return (s);
}
return (xstrdup(mode));
}
/* Callback for client_key_table. */
static void *
format_cb_client_key_table(struct format_tree *ft)
@@ -2522,14 +2487,7 @@ format_cb_pane_unseen_changes(struct format_tree *ft)
static void *
format_cb_pane_key_mode(struct format_tree *ft)
{
char *s;
if (ft->wp != NULL && ft->wp->screen != NULL) {
if (ft->wp->screen->kitty_keys.flags != 0) {
xasprintf(&s, "Kitty %u",
ft->wp->screen->kitty_keys.flags);
return (s);
}
switch (ft->wp->screen->mode & EXTENDED_KEY_MODES) {
case MODE_KEYS_EXTENDED:
return (xstrdup("Ext 1"));
@@ -2918,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);
}
@@ -2936,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);
}
@@ -2950,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);
}
@@ -3684,9 +3642,6 @@ static const struct format_table_entry format_table[] = {
{ "client_height", FORMAT_TABLE_STRING,
format_cb_client_height
},
{ "client_key_mode", FORMAT_TABLE_STRING,
format_cb_client_key_mode
},
{ "client_key_table", FORMAT_TABLE_STRING,
format_cb_client_key_table
},

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);

111
fuzz/layout-fuzzer.c Normal file
View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2026 Arthur Chan <arthur.chan@adalogics.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
/*
* Fuzz the custom layout parser.
*
* A layout string such as "bb62,80x24,0,0{40x24,0,0,1,39x24,41,0,2}" is
* accepted by select-layout and is what tmux stores and restores for a
* window, so it is parsed from configuration and from commands. It drives
* layout-custom.c (the string parser and the checksum), then layout.c, which
* resizes and assigns the cells.
*
* layout_parse() refuses a window with no panes, and refuses a layout whose
* cell count is smaller than the pane count, so the window is given a fixed
* number of panes. The count is fixed rather than derived from the input, so
* a mutation always means a different layout string.
*/
#include <sys/types.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
#define FUZZER_MAXLEN 1024
#define FUZZER_PANES 4
struct event_base *libevent;
int
LLVMFuzzerTestOneInput(const u_char *data, size_t size)
{
struct window *w;
struct window_pane *wp;
char *buf, *cause = NULL, *dump;
u_int i;
if (size == 0 || size > FUZZER_MAXLEN)
return 0;
/* layout_parse() takes a C string. */
buf = malloc(size + 1);
if (buf == NULL)
return 0;
memcpy(buf, data, size);
buf[size] = '\0';
w = window_create(80, 24, 0, 0);
if (w == NULL) {
free(buf);
return 0;
}
window_add_ref(w, __func__);
for (i = 0; i < FUZZER_PANES; i++) {
wp = window_add_pane(w, NULL, 0, 0);
if (w->active == NULL)
w->active = wp;
}
if (layout_parse(w, buf, &cause) == 0) {
dump = layout_dump(w, w->layout_root, 1);
free(dump);
}
free(cause);
window_remove_ref(w, __func__);
free(buf);
return 0;
}
int
LLVMFuzzerInitialize(__unused int *argc, __unused char ***argv)
{
const struct options_table_entry *oe;
global_environ = environ_create();
global_options = options_create(NULL);
global_s_options = options_create(NULL);
global_w_options = options_create(NULL);
for (oe = options_table; oe->name != NULL; oe++) {
if (oe->scope & OPTIONS_TABLE_SERVER)
options_default(global_options, oe);
if (oe->scope & OPTIONS_TABLE_SESSION)
options_default(global_s_options, oe);
if (oe->scope & OPTIONS_TABLE_WINDOW)
options_default(global_w_options, oe);
}
libevent = osdep_event_init();
socket_path = xstrdup("dummy");
return 0;
}

17
fuzz/layout-fuzzer.dict Normal file
View File

@@ -0,0 +1,17 @@
# tmux custom layout strings: "<checksum>,<sx>x<sy>,<x>,<y>{...}" / "[...]"
","
"x"
"{"
"}"
"["
"]"
"0"
"1"
"80x24,0,0"
"40x24,0,0"
"bb62,"
"cafe,"
",0,0,0"
",0,0{"
",0,0["
"@"

View File

@@ -0,0 +1,2 @@
[libfuzzer]
max_len = 1024

View File

@@ -464,8 +464,7 @@ input_key_extended(struct bufferevent *bev, key_code key)
} else
key &= KEYC_MASK_KEY;
if (options_get_number(global_options, "extended-keys-format") ==
EXTENDED_KEYS_XTERM)
if (options_get_number(global_options, "extended-keys-format") == 1)
xsnprintf(tmp, sizeof tmp, "\033[27;%c;%llu~", modifier, key);
else
xsnprintf(tmp, sizeof tmp, "\033[%llu;%cu", key, modifier);
@@ -589,10 +588,6 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key)
return (0);
}
/* Kitty keys take precedence if the application asked for them. */
if (input_key_kitty(s, bev, key) == 0)
return (0);
/* Is this backspace? */
if ((key & KEYC_MASK_KEY) == KEYC_BSPACE) {
newkey = options_get_number(global_options, "backspace");
@@ -682,10 +677,6 @@ input_key(struct screen *s, struct bufferevent *bev, key_code key)
log_debug("%s: ignoring key 0x%llx", __func__, key);
return (0);
}
if (key & (KEYC_SUPER|KEYC_HYPER))
return (input_key_vt10x(bev, key));
if (s->kitty_keys.flags & KITTY_KEY_SUPPORTED)
return (input_key_vt10x(bev, key));
/*
* No builtin key sequence; construct an extended key sequence

View File

@@ -1,313 +0,0 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 Michael Grant <mgrant@grant.org>
*
* 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 <string.h>
#include <wchar.h>
#include "tmux.h"
/* Track pane keyboard state and encode keys for applications. */
struct input_kitty_key {
key_code key;
u_int number;
char final;
};
static const struct input_kitty_key input_kitty_keys[] = {
{ C0_ESC, 27, 'u' },
{ C0_CR, 13, 'u' },
{ C0_HT, 9, 'u' },
{ KEYC_BSPACE, 127, 'u' },
{ KEYC_IC, 2, '~' },
{ KEYC_DC, 3, '~' },
{ KEYC_LEFT, 1, 'D' },
{ KEYC_RIGHT, 1, 'C' },
{ KEYC_UP, 1, 'A' },
{ KEYC_DOWN, 1, 'B' },
{ KEYC_PPAGE, 5, '~' },
{ KEYC_NPAGE, 6, '~' },
{ KEYC_HOME, 1, 'H' },
{ KEYC_END, 1, 'F' },
{ KEYC_F1, 1, 'P' },
{ KEYC_F2, 1, 'Q' },
{ KEYC_F3, 13, '~' },
{ KEYC_F4, 1, 'S' },
{ KEYC_F5, 15, '~' },
{ KEYC_F6, 17, '~' },
{ KEYC_F7, 18, '~' },
{ KEYC_F8, 19, '~' },
{ KEYC_F9, 20, '~' },
{ KEYC_F10, 21, '~' },
{ KEYC_F11, 23, '~' },
{ KEYC_F12, 24, '~' },
{ KEYC_CAPSLOCK, 57358, 'u' },
{ KEYC_SCROLLLOCK, 57359, 'u' },
{ KEYC_NUMLOCK, 57360, 'u' },
{ KEYC_PRINTSCREEN, 57361, 'u' },
{ KEYC_PAUSE, 57362, 'u' },
{ KEYC_MENU, 57363, 'u' },
{ KEYC_F13, 57376, 'u' },
{ KEYC_F14, 57377, 'u' },
{ KEYC_F15, 57378, 'u' },
{ KEYC_F16, 57379, 'u' },
{ KEYC_F17, 57380, 'u' },
{ KEYC_F18, 57381, 'u' },
{ KEYC_F19, 57382, 'u' },
{ KEYC_F20, 57383, 'u' },
{ KEYC_F21, 57384, 'u' },
{ KEYC_F22, 57385, 'u' },
{ KEYC_F23, 57386, 'u' },
{ KEYC_F24, 57387, 'u' },
{ KEYC_F25, 57388, 'u' },
{ KEYC_F26, 57389, 'u' },
{ KEYC_F27, 57390, 'u' },
{ KEYC_F28, 57391, 'u' },
{ KEYC_F29, 57392, 'u' },
{ KEYC_F30, 57393, 'u' },
{ KEYC_F31, 57394, 'u' },
{ KEYC_F32, 57395, 'u' },
{ KEYC_F33, 57396, 'u' },
{ KEYC_F34, 57397, 'u' },
{ KEYC_F35, 57398, 'u' },
{ KEYC_KP_ZERO|KEYC_KEYPAD, 57399, 'u' },
{ KEYC_KP_ONE|KEYC_KEYPAD, 57400, 'u' },
{ KEYC_KP_TWO|KEYC_KEYPAD, 57401, 'u' },
{ KEYC_KP_THREE|KEYC_KEYPAD, 57402, 'u' },
{ KEYC_KP_FOUR|KEYC_KEYPAD, 57403, 'u' },
{ KEYC_KP_FIVE|KEYC_KEYPAD, 57404, 'u' },
{ KEYC_KP_SIX|KEYC_KEYPAD, 57405, 'u' },
{ KEYC_KP_SEVEN|KEYC_KEYPAD, 57406, 'u' },
{ KEYC_KP_EIGHT|KEYC_KEYPAD, 57407, 'u' },
{ KEYC_KP_NINE|KEYC_KEYPAD, 57408, 'u' },
{ KEYC_KP_PERIOD|KEYC_KEYPAD, 57409, 'u' },
{ KEYC_KP_SLASH|KEYC_KEYPAD, 57410, 'u' },
{ KEYC_KP_STAR|KEYC_KEYPAD, 57411, 'u' },
{ KEYC_KP_MINUS|KEYC_KEYPAD, 57412, 'u' },
{ KEYC_KP_PLUS|KEYC_KEYPAD, 57413, 'u' },
{ KEYC_KP_ENTER|KEYC_KEYPAD, 57414, 'u' },
{ KEYC_KP_EQUAL|KEYC_KEYPAD, 57415, 'u' },
{ KEYC_KP_SEPARATOR|KEYC_KEYPAD, 57416, 'u' },
{ KEYC_KP_LEFT|KEYC_KEYPAD, 57417, 'u' },
{ KEYC_KP_RIGHT|KEYC_KEYPAD, 57418, 'u' },
{ KEYC_KP_UP|KEYC_KEYPAD, 57419, 'u' },
{ KEYC_KP_DOWN|KEYC_KEYPAD, 57420, 'u' },
{ KEYC_KP_PPAGE|KEYC_KEYPAD, 57421, 'u' },
{ KEYC_KP_NPAGE|KEYC_KEYPAD, 57422, 'u' },
{ KEYC_KP_HOME|KEYC_KEYPAD, 57423, 'u' },
{ KEYC_KP_END|KEYC_KEYPAD, 57424, 'u' },
{ KEYC_KP_IC|KEYC_KEYPAD, 57425, 'u' },
{ KEYC_KP_DC|KEYC_KEYPAD, 57426, 'u' },
{ KEYC_KP_BEGIN|KEYC_KEYPAD, 57427, '~' },
{ KEYC_MEDIA_PLAY, 57428, 'u' },
{ KEYC_MEDIA_PAUSE, 57429, 'u' },
{ KEYC_MEDIA_PLAYPAUSE, 57430, 'u' },
{ KEYC_MEDIA_REVERSE, 57431, 'u' },
{ KEYC_MEDIA_STOP, 57432, 'u' },
{ KEYC_MEDIA_FASTFORWARD, 57433, 'u' },
{ KEYC_MEDIA_REWIND, 57434, 'u' },
{ KEYC_MEDIA_NEXT, 57435, 'u' },
{ KEYC_MEDIA_PREVIOUS, 57436, 'u' },
{ KEYC_MEDIA_RECORD, 57437, 'u' },
{ KEYC_VOLUME_DOWN, 57438, 'u' },
{ KEYC_VOLUME_UP, 57439, 'u' },
{ KEYC_VOLUME_MUTE, 57440, 'u' },
{ KEYC_LEFT_SHIFT, 57441, 'u' },
{ KEYC_LEFT_CTRL, 57442, 'u' },
{ KEYC_LEFT_ALT, 57443, 'u' },
{ KEYC_LEFT_SUPER, 57444, 'u' },
{ KEYC_LEFT_HYPER, 57445, 'u' },
{ KEYC_LEFT_META, 57446, 'u' },
{ KEYC_RIGHT_SHIFT, 57447, 'u' },
{ KEYC_RIGHT_CTRL, 57448, 'u' },
{ KEYC_RIGHT_ALT, 57449, 'u' },
{ KEYC_RIGHT_SUPER, 57450, 'u' },
{ KEYC_RIGHT_HYPER, 57451, 'u' },
{ KEYC_RIGHT_META, 57452, 'u' },
{ KEYC_ISO_LEVEL3_SHIFT, 57453, 'u' },
{ KEYC_ISO_LEVEL5_SHIFT, 57454, 'u' }
};
void
input_kitty_reset(struct screen *s)
{
memset(&s->kitty_keys, 0, sizeof s->kitty_keys);
}
void
input_kitty_alternate_on(struct screen *s)
{
s->saved_kitty_keys = s->kitty_keys;
input_kitty_reset(s);
}
void
input_kitty_alternate_off(struct screen *s)
{
s->kitty_keys = s->saved_kitty_keys;
memset(&s->saved_kitty_keys, 0, sizeof s->saved_kitty_keys);
}
void
input_kitty_set(struct screen *s, u_int flags, int mode)
{
flags &= KITTY_KEY_SUPPORTED;
if (mode == 2)
s->kitty_keys.flags |= flags;
else if (mode == 3)
s->kitty_keys.flags &= ~flags;
else
s->kitty_keys.flags = flags;
}
void
input_kitty_push(struct screen *s, u_int flags)
{
s->kitty_keys.saved_flags = s->kitty_keys.flags;
s->kitty_keys.have_saved = 1;
s->kitty_keys.flags = flags & KITTY_KEY_SUPPORTED;
}
void
input_kitty_pop(struct screen *s, u_int count)
{
if (count == 0)
return;
if (s->kitty_keys.have_saved && count == 1)
s->kitty_keys.flags = s->kitty_keys.saved_flags;
else
s->kitty_keys.flags = 0;
s->kitty_keys.have_saved = 0;
}
static u_int
input_kitty_modifiers(key_code key)
{
u_int modifiers = 1;
if (key & KEYC_SHIFT)
modifiers += 1;
if (key & KEYC_META)
modifiers += 2;
if (key & KEYC_CTRL)
modifiers += 4;
if (key & KEYC_SUPER)
modifiers += 8;
if (key & KEYC_HYPER)
modifiers += 16;
return (modifiers);
}
static const struct input_kitty_key *
input_kitty_lookup(key_code key)
{
key_code lookup;
u_int i;
lookup = key & (KEYC_MASK_KEY|KEYC_KEYPAD);
for (i = 0; i < nitems(input_kitty_keys); i++) {
if (input_kitty_keys[i].key == lookup)
return (&input_kitty_keys[i]);
}
return (NULL);
}
int
input_key_kitty(struct screen *s, struct bufferevent *bev, key_code key)
{
const struct input_kitty_key *ikk;
struct utf8_data ud;
wchar_t wc;
key_code modifiers, onlykey;
u_int flags, number, modifier;
char tmp[64];
size_t size;
flags = s->kitty_keys.flags & KITTY_KEY_SUPPORTED;
if (flags == 0)
return (-1);
if ((key & KEYC_MASK_KEY) == KEYC_BTAB)
key = C0_HT|(key & ~KEYC_MASK_KEY)|KEYC_SHIFT;
modifiers = key & (KEYC_SHIFT|KEYC_META|KEYC_CTRL|KEYC_SUPER|KEYC_HYPER);
ikk = input_kitty_lookup(key);
if (ikk != NULL) {
if ((flags & KITTY_KEY_REPORT_ALL) == 0) {
if (ikk->key == C0_CR || ikk->key == C0_HT ||
ikk->key == KEYC_BSPACE)
return (-1);
if (ikk->final != 'u' &&
(modifiers & (KEYC_SUPER|KEYC_HYPER)) == 0)
return (-1);
}
number = ikk->number;
modifier = input_kitty_modifiers(key);
if (ikk->final == 'u') {
if (modifier == 1)
size = xsnprintf(tmp, sizeof tmp, "\033[%uu", number);
else
size = xsnprintf(tmp, sizeof tmp, "\033[%u;%uu",
number, modifier);
} else if (ikk->final == '~') {
if (modifier == 1)
size = xsnprintf(tmp, sizeof tmp, "\033[%u~", number);
else
size = xsnprintf(tmp, sizeof tmp, "\033[%u;%u~",
number, modifier);
} else {
if (modifier == 1)
size = xsnprintf(tmp, sizeof tmp, "\033[%c",
ikk->final);
else
size = xsnprintf(tmp, sizeof tmp, "\033[1;%u%c",
modifier, ikk->final);
}
goto write;
}
if ((flags & KITTY_KEY_REPORT_ALL) == 0 &&
(modifiers & (KEYC_META|KEYC_CTRL|KEYC_SUPER|KEYC_HYPER)) == 0)
return (-1);
onlykey = key & KEYC_MASK_KEY;
if (KEYC_IS_UNICODE(key)) {
utf8_to_data(onlykey, &ud);
if (utf8_towc(&ud, &wc) != UTF8_DONE)
return (-1);
number = wc;
} else if (onlykey <= 0x7f)
number = onlykey;
else
return (-1);
if ((key & KEYC_SHIFT) && number >= 'A' && number <= 'Z')
number += 'a' - 'A';
modifier = input_kitty_modifiers(key);
if (modifier == 1)
size = xsnprintf(tmp, sizeof tmp, "\033[%uu", number);
else
size = xsnprintf(tmp, sizeof tmp, "\033[%u;%uu", number,
modifier);
write:
log_debug("%s: %.*s", __func__, (int)size, tmp);
bufferevent_write(bev, tmp, size);
return (0);
}

53
input.c
View File

@@ -1,4 +1,4 @@
/* $OpenBSD: input.c,v 1.271 2026/08/31 19:34:09 nicm Exp $ */
/* $OpenBSD: input.c,v 1.272 2026/09/21 10:22:31 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -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;
@@ -279,10 +278,6 @@ enum input_csi_type {
INPUT_CSI_HPA,
INPUT_CSI_ICH,
INPUT_CSI_IL,
INPUT_CSI_KITTY_POP,
INPUT_CSI_KITTY_PUSH,
INPUT_CSI_KITTY_QUERY,
INPUT_CSI_KITTY_SET,
INPUT_CSI_MODOFF,
INPUT_CSI_MODSET,
INPUT_CSI_QUERY,
@@ -348,11 +343,7 @@ static const struct input_table_entry input_csi_table[] = {
{ 'r', "", INPUT_CSI_DECSTBM },
{ 's', "", INPUT_CSI_SCP },
{ 't', "", INPUT_CSI_WINOPS },
{ 'u', "", INPUT_CSI_RCP },
{ 'u', "<", INPUT_CSI_KITTY_POP },
{ 'u', "=", INPUT_CSI_KITTY_SET },
{ 'u', ">", INPUT_CSI_KITTY_PUSH },
{ 'u', "?", INPUT_CSI_KITTY_QUERY }
{ 'u', "", INPUT_CSI_RCP }
};
/* Input transition. */
@@ -882,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;
@@ -890,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);
@@ -1565,33 +1555,6 @@ input_csi_dispatch(struct input_ctx *ictx)
if (options_get_number(global_options, "extended-keys") == 2)
screen_write_mode_set(sctx, MODE_KEYS_EXTENDED);
break;
case INPUT_CSI_KITTY_QUERY:
if (options_get_number(global_options, "extended-keys") == 0)
break;
input_reply(ictx, 1, "\033[?%uu", s->kitty_keys.flags);
break;
case INPUT_CSI_KITTY_SET:
if (options_get_number(global_options, "extended-keys") == 0)
break;
n = input_get(ictx, 0, 0, 0);
m = input_get(ictx, 1, 1, 1);
if (n >= 0 && m >= 1 && m <= 3)
input_kitty_set(s, n, m);
break;
case INPUT_CSI_KITTY_PUSH:
if (options_get_number(global_options, "extended-keys") == 0)
break;
n = input_get(ictx, 0, 0, 0);
if (n >= 0)
input_kitty_push(s, n);
break;
case INPUT_CSI_KITTY_POP:
if (options_get_number(global_options, "extended-keys") == 0)
break;
n = input_get(ictx, 0, 1, 1);
if (n >= 0)
input_kitty_pop(s, n);
break;
case INPUT_CSI_WINOPS:
input_csi_dispatch_winops(ictx);
break;
@@ -3460,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);

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 }",

View File

@@ -45,29 +45,6 @@ static const struct {
{ "F10", KEYC_F10|KEYC_IMPLIED_META },
{ "F11", KEYC_F11|KEYC_IMPLIED_META },
{ "F12", KEYC_F12|KEYC_IMPLIED_META },
{ "F13", KEYC_F13|KEYC_IMPLIED_META },
{ "F14", KEYC_F14|KEYC_IMPLIED_META },
{ "F15", KEYC_F15|KEYC_IMPLIED_META },
{ "F16", KEYC_F16|KEYC_IMPLIED_META },
{ "F17", KEYC_F17|KEYC_IMPLIED_META },
{ "F18", KEYC_F18|KEYC_IMPLIED_META },
{ "F19", KEYC_F19|KEYC_IMPLIED_META },
{ "F20", KEYC_F20|KEYC_IMPLIED_META },
{ "F21", KEYC_F21|KEYC_IMPLIED_META },
{ "F22", KEYC_F22|KEYC_IMPLIED_META },
{ "F23", KEYC_F23|KEYC_IMPLIED_META },
{ "F24", KEYC_F24|KEYC_IMPLIED_META },
{ "F25", KEYC_F25|KEYC_IMPLIED_META },
{ "F26", KEYC_F26|KEYC_IMPLIED_META },
{ "F27", KEYC_F27|KEYC_IMPLIED_META },
{ "F28", KEYC_F28|KEYC_IMPLIED_META },
{ "F29", KEYC_F29|KEYC_IMPLIED_META },
{ "F30", KEYC_F30|KEYC_IMPLIED_META },
{ "F31", KEYC_F31|KEYC_IMPLIED_META },
{ "F32", KEYC_F32|KEYC_IMPLIED_META },
{ "F33", KEYC_F33|KEYC_IMPLIED_META },
{ "F34", KEYC_F34|KEYC_IMPLIED_META },
{ "F35", KEYC_F35|KEYC_IMPLIED_META },
{ "IC", KEYC_IC|KEYC_IMPLIED_META },
{ "Insert", KEYC_IC|KEYC_IMPLIED_META },
{ "DC", KEYC_DC|KEYC_IMPLIED_META },
@@ -145,54 +122,6 @@ static const struct {
{ "KPEnter", KEYC_KP_ENTER|KEYC_KEYPAD },
{ "KP0", KEYC_KP_ZERO|KEYC_KEYPAD },
{ "KP.", KEYC_KP_PERIOD|KEYC_KEYPAD },
{ "KP=", KEYC_KP_EQUAL|KEYC_KEYPAD },
{ "KP,", KEYC_KP_SEPARATOR|KEYC_KEYPAD },
{ "KPLeft", KEYC_KP_LEFT|KEYC_KEYPAD },
{ "KPRight", KEYC_KP_RIGHT|KEYC_KEYPAD },
{ "KPUp", KEYC_KP_UP|KEYC_KEYPAD },
{ "KPDown", KEYC_KP_DOWN|KEYC_KEYPAD },
{ "KPPageUp", KEYC_KP_PPAGE|KEYC_KEYPAD },
{ "KPPageDown", KEYC_KP_NPAGE|KEYC_KEYPAD },
{ "KPHome", KEYC_KP_HOME|KEYC_KEYPAD },
{ "KPEnd", KEYC_KP_END|KEYC_KEYPAD },
{ "KPInsert", KEYC_KP_IC|KEYC_KEYPAD },
{ "KPDelete", KEYC_KP_DC|KEYC_KEYPAD },
{ "KPBegin", KEYC_KP_BEGIN|KEYC_KEYPAD },
/* Other function keys. */
{ "CapsLock", KEYC_CAPSLOCK },
{ "ScrollLock", KEYC_SCROLLLOCK },
{ "NumLock", KEYC_NUMLOCK },
{ "PrintScreen", KEYC_PRINTSCREEN },
{ "Pause", KEYC_PAUSE },
{ "Menu", KEYC_MENU },
{ "MediaPlay", KEYC_MEDIA_PLAY },
{ "MediaPause", KEYC_MEDIA_PAUSE },
{ "MediaPlayPause", KEYC_MEDIA_PLAYPAUSE },
{ "MediaReverse", KEYC_MEDIA_REVERSE },
{ "MediaStop", KEYC_MEDIA_STOP },
{ "MediaFastForward", KEYC_MEDIA_FASTFORWARD },
{ "MediaRewind", KEYC_MEDIA_REWIND },
{ "MediaNext", KEYC_MEDIA_NEXT },
{ "MediaPrevious", KEYC_MEDIA_PREVIOUS },
{ "MediaRecord", KEYC_MEDIA_RECORD },
{ "VolumeDown", KEYC_VOLUME_DOWN },
{ "VolumeUp", KEYC_VOLUME_UP },
{ "VolumeMute", KEYC_VOLUME_MUTE },
{ "LeftShift", KEYC_LEFT_SHIFT },
{ "LeftControl", KEYC_LEFT_CTRL },
{ "LeftAlt", KEYC_LEFT_ALT },
{ "LeftSuper", KEYC_LEFT_SUPER },
{ "LeftHyper", KEYC_LEFT_HYPER },
{ "LeftMeta", KEYC_LEFT_META },
{ "RightShift", KEYC_RIGHT_SHIFT },
{ "RightControl", KEYC_RIGHT_CTRL },
{ "RightAlt", KEYC_RIGHT_ALT },
{ "RightSuper", KEYC_RIGHT_SUPER },
{ "RightHyper", KEYC_RIGHT_HYPER },
{ "RightMeta", KEYC_RIGHT_META },
{ "ISOLevel3Shift", KEYC_ISO_LEVEL3_SHIFT },
{ "ISOLevel5Shift", KEYC_ISO_LEVEL5_SHIFT },
/* Mouse keys. */
KEYC_MOUSE_STRING(MOUSEDOWN1, MouseDown1),
@@ -297,13 +226,8 @@ key_string_get_modifiers(const char **string)
modifiers |= KEYC_META;
break;
case 'S':
modifiers |= KEYC_SHIFT;
break;
case 's':
modifiers |= KEYC_SUPER;
break;
case 'H':
modifiers |= KEYC_HYPER;
modifiers |= KEYC_SHIFT;
break;
default:
*string = NULL;
@@ -425,10 +349,6 @@ key_string_lookup_key(key_code key, int with_flags)
strlcat(out, "M-", sizeof out);
if (key & KEYC_SHIFT)
strlcat(out, "S-", sizeof out);
if (key & KEYC_SUPER)
strlcat(out, "s-", sizeof out);
if (key & KEYC_HYPER)
strlcat(out, "H-", sizeof out);
key &= KEYC_MASK_KEY;
/* Handle no key. */

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: layout-custom.c,v 1.41 2026/09/09 09:01:19 nicm Exp $ */
/* $OpenBSD: layout-custom.c,v 1.43 2026/09/22 06:48:01 nicm Exp $ */
/*
* Copyright (c) 2010 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -283,26 +283,59 @@ layout_checksum(const char *layout)
char *
layout_dump(__unused struct window *w, struct layout_cell *lcroot, int flags)
{
struct layout_string layout_string;
char *out = NULL;
struct layout_string layout_string = { 0 };
char *out;
if (lcroot == NULL)
return NULL;
goto bad;
layout_string_init(&layout_string);
if (layout_append(lcroot, &layout_string, flags) == 0) {
if (flags & LAYOUT_CUSTOM_OLD_FORMAT)
xasprintf(&out, "%04hx,%s",
layout_checksum(layout_string.dat),
layout_string.dat);
else
xasprintf(&out, "{\"V\":2,\"L\":%s}",
layout_string.dat);
if (layout_append(lcroot, &layout_string, flags) != 0)
goto bad;
if (~flags & LAYOUT_CUSTOM_OLD_FORMAT)
xasprintf(&out, "{\"V\":2,\"L\":%s}", layout_string.dat);
else {
xasprintf(&out, "%04hx,%s", layout_checksum(layout_string.dat),
layout_string.dat);
}
layout_string_free(&layout_string);
return (out);
bad:
layout_string_free(&layout_string);
if (~flags & LAYOUT_CUSTOM_OLD_FORMAT)
return (NULL);
return (xstrdup("0000,"));
}
/* Get a floating pane cell's z-index in the layout being dumped. */
static u_int
layout_cell_zindex(struct layout_cell *lc)
{
struct window_pane *wp = lc->wp, *wq;
struct window *w = wp->window;
struct layout_cell *other;
int saved = (lc == wp->saved_layout_cell);
u_int i = 0;
if (saved &&
w->active != NULL &&
(w->active->flags & PANE_ZOOMED) &&
(w->active->saved_layout_cell->flags & LAYOUT_CELL_FLOATING)) {
if (wp == w->active)
return (0);
i++;
}
TAILQ_FOREACH(wq, &w->z_index, zentry) {
if (wq == wp)
break;
if (saved)
other = wq->saved_layout_cell;
else
other = wq->layout_cell;
if (other != NULL && (other->flags & LAYOUT_CELL_FLOATING))
i++;
}
return (i);
}
/* Append information for a single cell in a JSON (v2) format. */
@@ -313,7 +346,7 @@ layout_append_v2(struct layout_cell *lc, struct layout_string *ls)
struct window_pane *wp;
enum layout_type type;
char c;
u_int i, n;
u_int i, n, z;
if (lc == NULL)
return (-1);
@@ -354,9 +387,10 @@ layout_append_v2(struct layout_cell *lc, struct layout_string *ls)
if (window_pane_index(wp, &i) != 0)
return (-1);
layout_string_write(ls, ",\"i\":%u", i);
if ((lc->flags & LAYOUT_CELL_FLOATING) &&
window_pane_zindex(wp, &i) == 0)
layout_string_write(ls, ",\"z\":%u", i);
if (lc->flags & LAYOUT_CELL_FLOATING) {
z = layout_cell_zindex(lc);
layout_string_write(ls, ",\"z\":%u", z);
}
layout_string_write(ls, ",\"I\":\"%%%u\"", wp->id);
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: layout.c,v 1.100 2026/09/11 08:16:14 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>
@@ -469,7 +469,6 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
sx = PANE_MINIMUM;
else
sx = sx - sb_w - sb_pad;
wp->flags |= PANE_REDRAWSCROLLBAR;
}
window_pane_resize(wp, sx, sy);
@@ -477,8 +476,11 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
if (wp->xoff != old_xoff ||
wp->yoff != old_yoff ||
wp->sx != old_sx ||
wp->sy != old_sy)
wp->sy != old_sy) {
if (window_pane_scrollbar_reserve(wp))
wp->flags |= PANE_REDRAWSCROLLBAR;
changed = 1;
}
}
if (changed)
redraw_invalidate_scene(w);
@@ -1728,7 +1730,7 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
enum pane_lines lines, struct window *w, struct layout_geometry *lg,
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;
@@ -1777,12 +1779,20 @@ layout_floating_args_parse(struct cmdq_item *item, struct args *args,
}
}
if (!window_has_floating_panes(w)) {
w->last_new_pane_x = 0;
w->last_new_pane_y = 0;
}
if (ox == INT_MAX) {
if (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;
@@ -1793,8 +1803,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: options-table.c,v 1.246 2026/09/11 10:17:16 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>
@@ -79,7 +79,7 @@ static const char *options_table_pane_border_lines_list[] = {
"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[] = {
@@ -104,7 +104,7 @@ static const char *options_table_extended_keys_list[] = {
"off", "on", "always", NULL
};
static const char *options_table_extended_keys_format_list[] = {
"csi-u", "xterm", "kitty", NULL
"csi-u", "xterm", NULL
};
static const char *options_table_allow_passthrough_list[] = {
"off", "on", "all", NULL
@@ -493,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."
@@ -1666,33 +1666,6 @@ 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,

View File

@@ -1400,12 +1400,6 @@ options_push_changes(const char *name)
TAILQ_FOREACH(loop, &clients, entry)
server_client_set_key_table(loop, NULL);
}
if (strcmp(name, "extended-keys") == 0) {
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->tty.flags & TTY_STARTED)
tty_update_features(&loop->tty);
}
}
if (strcmp(name, "user-keys") == 0) {
TAILQ_FOREACH(loop, &clients, entry) {
if (loop->tty.flags & TTY_OPENED)

661
popup.c
View File

@@ -1,661 +0,0 @@
/* $OpenBSD: popup.c,v 1.77 2026/08/28 08:02:16 nicm Exp $ */
/*
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "tmux.h"
struct popup_data {
struct client *c;
struct cmdq_item *item;
int flags;
char *title;
char *style;
char *border_style;
struct grid_cell border_cell;
enum box_lines border_lines;
struct screen s;
struct grid_cell defaults;
struct colour_palette palette;
struct visible_ranges r;
struct job *job;
struct input_ctx *ictx;
int status;
popup_close_cb cb;
void *arg;
int close;
/* Current position and size. */
u_int px;
u_int py;
u_int sx;
u_int sy;
/* Preferred position and size. */
u_int ppx;
u_int ppy;
u_int psx;
u_int psy;
enum { OFF, MOVE, SIZE } dragging;
u_int dx;
u_int dy;
u_int lx;
u_int ly;
u_int lb;
};
static void
popup_free(struct popup_data *pd)
{
server_client_unref(pd->c);
if (pd->job != NULL)
job_free(pd->job);
if (pd->ictx != NULL)
input_free(pd->ictx);
free(pd->r.ranges);
screen_free(&pd->s);
colour_palette_free(&pd->palette);
free(pd->title);
free(pd->style);
free(pd->border_style);
free(pd);
}
static void
popup_reapply_styles(struct popup_data *pd)
{
struct client *c = pd->c;
struct session *s = c->session;
struct options *o;
struct format_tree *ft;
struct style sytmp;
if (s == NULL)
return;
o = s->curw->window->options;
ft = format_create_defaults(NULL, c, s, s->curw, NULL);
/* Reapply popup style from options. */
memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
style_apply(&pd->defaults, o, "popup-style", ft);
if (pd->style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->defaults, pd->style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
pd->defaults.attr = 0;
/* Reapply border style from options. */
memcpy(&pd->border_cell, &grid_default_cell, sizeof pd->border_cell);
style_apply(&pd->border_cell, o, "popup-border-style", ft);
if (pd->border_style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->border_cell,
pd->border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
pd->border_cell.attr = 0;
format_free(ft);
}
static void
popup_redraw_cb(const struct tty_ctx *ttyctx)
{
struct popup_data *pd = ttyctx->arg;
pd->c->flags |= CLIENT_REDRAWOVERLAY;
}
static int
popup_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
struct popup_data *pd = ttyctx->arg;
if (c != pd->c)
return (0);
if (pd->c->flags & CLIENT_REDRAWOVERLAY)
return (0);
ttyctx->wox = 0;
ttyctx->woy = 0;
ttyctx->wsx = c->tty.sx;
ttyctx->wsy = c->tty.sy;
if (pd->border_lines == BOX_LINES_NONE) {
ttyctx->xoff = ttyctx->rxoff = pd->px;
ttyctx->yoff = ttyctx->ryoff = pd->py;
} else {
ttyctx->xoff = ttyctx->rxoff = pd->px + 1;
ttyctx->yoff = ttyctx->ryoff = pd->py + 1;
}
return (1);
}
static void
popup_init_ctx_cb(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
{
struct popup_data *pd = ctx->arg;
memcpy(&ttyctx->defaults, &pd->defaults, sizeof ttyctx->defaults);
ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER;
ttyctx->style_ctx.defaults = &ttyctx->defaults;
ttyctx->style_ctx.palette = &pd->palette;
ttyctx->redraw_cb = popup_redraw_cb;
ttyctx->set_client_cb = popup_set_client_cb;
ttyctx->arg = pd;
}
static struct screen *
popup_mode_cb(__unused struct client *c, void *data, u_int *cx, u_int *cy)
{
struct popup_data *pd = data;
if (pd->border_lines == BOX_LINES_NONE) {
*cx = pd->px + pd->s.cx;
*cy = pd->py + pd->s.cy;
} else {
*cx = pd->px + 1 + pd->s.cx;
*cy = pd->py + 1 + pd->s.cy;
}
return (&pd->s);
}
/* Return parts of the input range which are not obstructed by the popup. */
static struct visible_ranges *
popup_check_cb(__unused struct client* c, void *data, u_int px, u_int py,
u_int nx)
{
struct popup_data *pd = data;
struct visible_ranges *r = &pd->r;
server_client_overlay_range(pd->px, pd->py, pd->sx, pd->sy, px, py, nx,
r);
return (r);
}
static void
popup_draw_cb(struct client *c, void *data)
{
struct popup_data *pd = data;
struct tty *tty = &c->tty;
struct screen s;
struct screen_write_ctx ctx;
u_int i, px = pd->px, py = pd->py;
struct grid_cell defaults;
struct tty_style_ctx style_ctx;
popup_reapply_styles(pd);
screen_init(&s, pd->sx, pd->sy, 0);
if (pd->s.hyperlinks != NULL) {
hyperlinks_free(s.hyperlinks);
s.hyperlinks = hyperlinks_copy(pd->s.hyperlinks);
}
screen_write_start(&ctx, &s);
screen_write_clearscreen(&ctx, 8);
if (pd->border_lines == BOX_LINES_NONE) {
screen_write_cursormove(&ctx, 0, 0, 0);
screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx, pd->sy);
} else if (pd->sx > 2 && pd->sy > 2) {
screen_write_box(&ctx, pd->sx, pd->sy, pd->border_lines,
&pd->border_cell, pd->title);
screen_write_cursormove(&ctx, 1, 1, 0);
screen_write_fast_copy(&ctx, &pd->s, 0, 0, pd->sx - 2,
pd->sy - 2);
}
screen_write_stop(&ctx);
memcpy(&defaults, &pd->defaults, sizeof defaults);
if (defaults.fg == 8)
defaults.fg = pd->palette.fg;
if (defaults.bg == 8)
defaults.bg = pd->palette.bg;
style_ctx.defaults = &defaults;
style_ctx.palette = &pd->palette;
style_ctx.dim = 0;
style_ctx.hyperlinks = s.hyperlinks;
c->overlay_check = NULL;
c->overlay_data = NULL;
for (i = 0; i < pd->sy; i++)
tty_draw_line(tty, &s, 0, i, pd->sx, px, py + i, &style_ctx);
screen_free(&s);
c->overlay_check = popup_check_cb;
c->overlay_data = pd;
}
static void
popup_free_cb(__unused struct client *c, void *data)
{
struct popup_data *pd = data;
struct cmdq_item *item = pd->item;
if (pd->cb != NULL)
pd->cb(pd->status, pd->arg);
if (item != NULL) {
if (cmdq_get_client(item) != NULL &&
cmdq_get_client(item)->session == NULL)
cmdq_get_client(item)->retval = pd->status;
cmdq_continue(item);
}
popup_free(pd);
}
static void
popup_resize_cb(__unused struct client *c, void *data)
{
struct popup_data *pd = data;
struct tty *tty = &c->tty;
if (pd == NULL)
return;
/* Adjust position and size. */
if (pd->psy > tty->sy)
pd->sy = tty->sy;
else
pd->sy = pd->psy;
if (pd->psx > tty->sx)
pd->sx = tty->sx;
else
pd->sx = pd->psx;
if (pd->ppy + pd->sy > tty->sy)
pd->py = tty->sy - pd->sy;
else
pd->py = pd->ppy;
if (pd->ppx + pd->sx > tty->sx)
pd->px = tty->sx - pd->sx;
else
pd->px = pd->ppx;
/* Avoid zero size screens. */
if (pd->border_lines == BOX_LINES_NONE) {
screen_resize(&pd->s, pd->sx, pd->sy, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx, pd->sy );
} else if (pd->sx > 2 && pd->sy > 2) {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
}
static void
popup_handle_drag(struct client *c, struct popup_data *pd,
struct mouse_event *m)
{
u_int px, py;
if (!MOUSE_DRAG(m->b))
pd->dragging = OFF;
else if (pd->dragging == MOVE) {
if (m->x < pd->dx)
px = 0;
else if (m->x - pd->dx + pd->sx > c->tty.sx)
px = c->tty.sx - pd->sx;
else
px = m->x - pd->dx;
if (m->y < pd->dy)
py = 0;
else if (m->y - pd->dy + pd->sy > c->tty.sy)
py = c->tty.sy - pd->sy;
else
py = m->y - pd->dy;
pd->px = px;
pd->py = py;
pd->dx = m->x - pd->px;
pd->dy = m->y - pd->py;
pd->ppx = px;
pd->ppy = py;
server_redraw_client(c);
} else if (pd->dragging == SIZE) {
if (pd->border_lines == BOX_LINES_NONE) {
if (m->x < pd->px + 1)
return;
if (m->y < pd->py + 1)
return;
} else {
if (m->x < pd->px + 3)
return;
if (m->y < pd->py + 3)
return;
}
pd->sx = m->x - pd->px;
pd->sy = m->y - pd->py;
pd->psx = pd->sx;
pd->psy = pd->sy;
if (pd->border_lines == BOX_LINES_NONE) {
screen_resize(&pd->s, pd->sx, pd->sy, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx, pd->sy);
} else {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 0);
if (pd->job != NULL)
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
server_redraw_client(c);
}
}
static int
popup_key_cb(struct client *c, void *data, struct key_event *event)
{
struct popup_data *pd = data;
struct mouse_event *m = &event->m;
const char *buf;
size_t len;
u_int px, py;
enum { NONE, LEFT, RIGHT, TOP, BOTTOM } border = NONE;
if (KEYC_IS_MOUSE(event->key)) {
if (pd->dragging != OFF) {
popup_handle_drag(c, pd, m);
goto out;
}
if (m->x < pd->px ||
m->x > pd->px + pd->sx - 1 ||
m->y < pd->py ||
m->y > pd->py + pd->sy - 1) {
return (0);
}
if (pd->border_lines != BOX_LINES_NONE) {
if (m->x == pd->px)
border = LEFT;
else if (m->x == pd->px + pd->sx - 1)
border = RIGHT;
else if (m->y == pd->py)
border = TOP;
else if (m->y == pd->py + pd->sy - 1)
border = BOTTOM;
}
if ((m->b & MOUSE_MASK_MODIFIERS) == 0 &&
MOUSE_BUTTONS(m->b) == MOUSE_BUTTON_3 &&
(border == LEFT || border == TOP))
goto out;
if (((m->b & MOUSE_MASK_MODIFIERS) == MOUSE_MASK_META) ||
(border != NONE && !MOUSE_DRAG(m->lb))) {
if (!MOUSE_DRAG(m->b))
goto out;
if (MOUSE_BUTTONS(m->lb) == MOUSE_BUTTON_1)
pd->dragging = MOVE;
else if (MOUSE_BUTTONS(m->lb) == MOUSE_BUTTON_3)
pd->dragging = SIZE;
pd->dx = m->lx - pd->px;
pd->dy = m->ly - pd->py;
goto out;
}
}
if ((((pd->flags & (POPUP_CLOSEEXIT|POPUP_CLOSEEXITZERO)) == 0) ||
pd->job == NULL) &&
(event->key == '\033' || event->key == ('c'|KEYC_CTRL)))
return (1);
if (pd->job == NULL && (pd->flags & POPUP_CLOSEANYKEY) &&
!KEYC_IS_MOUSE(event->key) && !KEYC_IS_PASTE(event->key))
return (1);
if (pd->job != NULL) {
if (KEYC_IS_MOUSE(event->key)) {
/* Must be inside, checked already. */
if (pd->border_lines == BOX_LINES_NONE) {
px = m->x - pd->px;
py = m->y - pd->py;
} else {
px = m->x - pd->px - 1;
py = m->y - pd->py - 1;
}
if (!input_key_get_mouse(&pd->s, m, px, py, &buf, &len))
return (0);
bufferevent_write(job_get_event(pd->job), buf, len);
return (0);
}
input_key(&pd->s, job_get_event(pd->job), event->key);
}
return (0);
out:
pd->lx = m->x;
pd->ly = m->y;
pd->lb = m->b;
return (0);
}
static void
popup_job_update_cb(struct job *job)
{
struct popup_data *pd = job_get_data(job);
struct evbuffer *evb = job_get_event(job)->input;
struct client *c = pd->c;
struct screen *s = &pd->s;
void *data = EVBUFFER_DATA(evb);
size_t size = EVBUFFER_LENGTH(evb);
if (size == 0)
return;
c->overlay_check = NULL;
c->overlay_data = NULL;
input_parse_screen(pd->ictx, s, popup_init_ctx_cb, pd, data, size);
c->overlay_check = popup_check_cb;
c->overlay_data = pd;
evbuffer_drain(evb, size);
}
static void
popup_job_complete_cb(struct job *job)
{
struct popup_data *pd = job_get_data(job);
int status;
status = job_get_status(pd->job);
if (WIFEXITED(status))
pd->status = WEXITSTATUS(status);
else if (WIFSIGNALED(status))
pd->status = WTERMSIG(status);
else
pd->status = 0;
pd->job = NULL;
if ((pd->flags & POPUP_CLOSEEXIT) ||
((pd->flags & POPUP_CLOSEEXITZERO) && pd->status == 0))
server_client_clear_overlay(pd->c);
}
int
popup_present(struct client *c)
{
return (c->overlay_draw == popup_draw_cb);
}
int
popup_modify(struct client *c, const char *title, const char *style,
const char *border_style, enum box_lines lines, int flags)
{
struct popup_data *pd = c->overlay_data;
struct style sytmp;
if (title != NULL) {
if (pd->title != NULL)
free(pd->title);
pd->title = xstrdup(title);
}
if (border_style != NULL) {
free(pd->border_style);
pd->border_style = xstrdup(border_style);
style_set(&sytmp, &pd->border_cell);
if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
if (style != NULL) {
free(pd->style);
pd->style = xstrdup(style);
style_set(&sytmp, &pd->defaults);
if (style_parse(&sytmp, &pd->defaults, style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
if (lines != BOX_LINES_DEFAULT) {
if (lines == BOX_LINES_NONE && pd->border_lines != lines) {
screen_resize(&pd->s, pd->sx, pd->sy, 1);
job_resize(pd->job, pd->sx, pd->sy);
} else if (pd->border_lines == BOX_LINES_NONE &&
pd->border_lines != lines) {
screen_resize(&pd->s, pd->sx - 2, pd->sy - 2, 1);
job_resize(pd->job, pd->sx - 2, pd->sy - 2);
}
pd->border_lines = lines;
tty_resize(&c->tty);
}
if (flags != -1)
pd->flags = flags;
server_redraw_client(c);
return (0);
}
int
popup_display(int flags, enum box_lines lines, struct cmdq_item *item, u_int px,
u_int py, u_int sx, u_int sy, struct environ *env, const char *shellcmd,
int argc, char **argv, const char *cwd, const char *title, struct client *c,
struct session *s, const char *style, const char *border_style,
popup_close_cb cb, void *arg)
{
struct popup_data *pd;
u_int jx, jy;
struct options *o;
struct style sytmp;
if (s != NULL)
o = s->curw->window->options;
else
o = c->session->curw->window->options;
if (lines == BOX_LINES_DEFAULT)
lines = options_get_number(o, "popup-border-lines");
if (lines == BOX_LINES_NONE) {
if (sx < 1 || sy < 1)
return (-1);
jx = sx;
jy = sy;
} else {
if (sx < 3 || sy < 3)
return (-1);
jx = sx - 2;
jy = sy - 2;
}
if (c->tty.sx < sx || c->tty.sy < sy)
return (-1);
pd = xcalloc(1, sizeof *pd);
pd->item = item;
pd->flags = flags;
if (title != NULL)
pd->title = xstrdup(title);
if (style != NULL)
pd->style = xstrdup(style);
if (border_style != NULL)
pd->border_style = xstrdup(border_style);
pd->c = c;
pd->c->references++;
pd->cb = cb;
pd->arg = arg;
pd->status = 128 + SIGHUP;
pd->border_lines = lines;
memcpy(&pd->border_cell, &grid_default_cell, sizeof pd->border_cell);
style_apply(&pd->border_cell, o, "popup-border-style", NULL);
if (border_style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->border_cell, border_style) == 0) {
pd->border_cell.fg = sytmp.gc.fg;
pd->border_cell.bg = sytmp.gc.bg;
}
}
pd->border_cell.attr = 0;
screen_init(&pd->s, jx, jy, 0);
screen_set_default_cursor(&pd->s, global_w_options);
colour_palette_init(&pd->palette);
colour_palette_from_option(&pd->palette, global_w_options);
memcpy(&pd->defaults, &grid_default_cell, sizeof pd->defaults);
style_apply(&pd->defaults, o, "popup-style", NULL);
if (style != NULL) {
style_set(&sytmp, &grid_default_cell);
if (style_parse(&sytmp, &pd->defaults, style) == 0) {
pd->defaults.fg = sytmp.gc.fg;
pd->defaults.bg = sytmp.gc.bg;
}
}
pd->defaults.attr = 0;
pd->px = px;
pd->py = py;
pd->sx = sx;
pd->sy = sy;
pd->ppx = px;
pd->ppy = py;
pd->psx = sx;
pd->psy = sy;
pd->job = job_run(shellcmd, argc, argv, env, s, cwd,
popup_job_update_cb, popup_job_complete_cb, NULL, pd,
JOB_NOWAIT|JOB_PTY|JOB_KEEPWRITE|JOB_DEFAULTSHELL, jx, jy);
if (pd->job == NULL) {
popup_free(pd);
return (-1);
}
pd->ictx = input_init(NULL, job_get_event(pd->job), &pd->palette, c);
server_client_set_overlay(c, 0, popup_check_cb, popup_mode_cb,
popup_draw_cb, popup_key_cb, popup_free_cb, popup_resize_cb, pd);
return (0);
}

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

@@ -0,0 +1,79 @@
#!/bin/sh
# Selecting a pane with attach-session from an already attached client must
# redraw pane contents when window-style and window-active-style differ.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lattach-redraw-outer-$$ -f/dev/null"
BEFORE=$DIR/before
AFTER=$DIR/after
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
LEFT=$($INNER new-session -dPF '#{pane_id}' -s inner -x 40 -y 8 \
"printf 'LEFT'; exec sleep 100") || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g default-terminal screen || exit 1
$INNER split-window -h -t "$LEFT" "printf 'RIGHT'; exec sleep 100" || exit 1
$INNER set-option -w -t "$LEFT" window-style bg=red || exit 1
$INNER set-option -w -t "$LEFT" window-active-style bg=blue || exit 1
$INNER bind-key -n x attach-session -t "$LEFT" || exit 1
$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
sleep 1
$OUTER send-keys -t outer:0.0 x || exit 1
sleep 1
[ "$($INNER display-message -p -t inner '#{pane_id}')" = "$LEFT" ] ||
fail "attach-session did not select the target pane"
$OUTER capture-pane -pe -t outer:0.0 >"$BEFORE" || exit 1
# A forced redraw produces the correct scene. It must be identical to the
# scene drawn immediately by attach-session.
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 1
$OUTER capture-pane -pe -t outer:0.0 >"$AFTER" || exit 1
cmp -s "$BEFORE" "$AFTER" ||
fail "attach-session left stale active/inactive pane styles"
exit 0

View File

@@ -5,6 +5,15 @@
PATH=/bin:/usr/bin
TERM=screen
# The pane's shell must not be the user's own interactive shell: a custom
# PS1/PROMPT_COMMAND that sets the terminal title (as many do, for tmux/xterm
# TERM types) would redraw over the titles this test sets and checks on
# every prompt, regardless of how long it waits first.
shell=
if command -v bash >/dev/null 2>&1; then
shell='bash --noprofile --norc +o history'
fi
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
@@ -30,7 +39,7 @@ must_equal()
[ "$got" = "$want" ] || fail "got '$got', expected '$want'"
}
$TMUX new-session -d -x 80 -y 24 || exit 1
$TMUX new-session -d -x 80 -y 24 -- $shell || exit 1
$TMUX set-option -qg allow-set-title on || exit 1
$TMUX set-option -qg allow-rename on || exit 1
$TMUX set-option -qg automatic-rename off || exit 1

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

@@ -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

@@ -1,6 +1,7 @@
#!/bin/sh
# Popups require a tty overlay and cannot be displayed by a control client.
# 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.

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
OUT=$(mktemp)

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)

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

@@ -153,11 +153,13 @@ wait_format "$p0" '#{pane_in_mode}' '0'
wait_option @picked "$p1"
# Commands after display-panes run immediately while the mode remains.
# Record the mode in the same command queue so client startup cannot race
# the 500ms timeout.
$TMUX set -g @after none || fail "set @after failed"
$TMUX display-panes -Nd 500 -t "$p0" \; set -g @after fast ||
$TMUX display-panes -Nd 500 -t "$p0" \; \
set -gF -t "$p0" @after '#{pane_mode}' ||
fail "display-panes immediate command failed"
wait_option @after fast
wait_format "$p0" '#{pane_mode}' 'panes-mode'
wait_option @after 'panes-mode'
wait_format "$p0" '#{pane_in_mode}' '0'
# Existing zoom is restored on exit.

View File

@@ -0,0 +1,135 @@
#!/bin/sh
# Damage-only redraws must evaluate both active and inactive border styles
# for each client, even though the cached border cells belong to the pane.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lborder-style-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lborder-style-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_for_marker()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client in $target did not receive $marker"
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $phase (1 .. 2) {
while (!-e "$ENV{TRIGGER}-$phase") {
select undef, undef, undef, 0.01;
}
# Change an unused palette entry and acknowledge it in the pane body.
print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase";
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 20 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g pane-border-lines simple || exit 1
$INNER set -g pane-border-status top || exit 1
$INNER set -g pane-border-format 'CLIENT=#{client_name}' || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \
'sleep 100') || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1
RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1
for target in "$LEFT" "$RIGHT"; do
$OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" ||
exit 1
done
wait_for_clients
NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1
NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1
STYLE="fg=#{?#{==:#{client_name},$NAME1},red,blue}"
$INNER set -g pane-border-style "$STYLE" || exit 1
$INNER set -g pane-active-border-style "$STYLE" || exit 1
# Capture just the floating pane's rows, excluding the acknowledgement in
# the tiled pane. Exercise each cache by changing the floating pane's focus.
phase=1
while [ "$phase" -le 2 ]; do
if [ "$phase" -eq 2 ]; then
$INNER select-pane -t "$FLOAT" || exit 1
fi
$INNER refresh-client -t "$NAME1" || exit 1
$INNER refresh-client -t "$NAME2" || exit 1
sleep 0.5
for target in "$LEFT" "$RIGHT"; do
$OUTER capture-pane -pe -S 5 -E 10 -t "$target" \
>"$DIR/before-$target" || exit 1
done
RED=$(printf '\033[31m')
BLUE=$(printf '\033[34m')
grep -Fq "$RED" "$DIR/before-$LEFT" || fail "missing red border"
grep -Fq "$BLUE" "$DIR/before-$RIGHT" || fail "missing blue border"
: >"$DIR/trigger-$phase"
wait_for_marker "$LEFT" "DAMAGE$phase"
wait_for_marker "$RIGHT" "DAMAGE$phase"
sleep 0.2
for target in "$LEFT" "$RIGHT"; do
$OUTER capture-pane -pe -S 5 -E 10 -t "$target" \
>"$CAPTURE" || exit 1
diff -u "$DIR/before-$target" "$CAPTURE" ||
fail "phase $phase changed $target's border style"
done
phase=$((phase + 1))
done
exit 0

View File

@@ -0,0 +1,108 @@
#!/bin/sh
# Regression test for a floating-pane drag bug: cmd_resize_pane_redraw_floating()
# (cmd-resize-pane.c) reported damage for just a dragged floating pane's
# content rectangle, not the one-cell border frame drawn around it (see the
# "floating" case in screen-redraw.c, which draws that frame at
# xoff-1/yoff-1 through xoff+sx/yoff+sy - one cell outside the pane's own
# content area). Damage scoped to only the content area left the frame's
# previous position undrawn as the pane moved, so dragging it left a trail
# of un-erased border frames behind - visible as several "corners" stacked
# up rather than just the pane's current one.
#
# This bug has nothing to do with images - it reproduces with a plain
# floating pane and no image support required.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
#
# Write a plain (unmodified) SGR button-1 press, drag update and release at
# 1-based positions to the outer pane holding the inner client - this
# matches the default MouseDown1Border/MouseDrag1Border bindings used to
# move or resize a floating pane by its border.
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on
$TMUX set -g default-command 'sh -c "sleep 100"'
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Sanity check: exactly one floating pane, so exactly one top-left corner,
# before dragging anything.
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "sanity: expected 1 corner before drag, found $n"
# Drag the floating pane by its top border (row FTOP-1, some column within
# its width) down several rows in a few separate steps, then release. A
# single drag() call already does press/motion/release, so call it several
# times in a row to simulate a multi-step real drag.
GRABCOL=$((FLEFT + FWIDTH / 2))
STARTROW=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((STARTROW + i + 1))
drag $((GRABCOL + 1)) $((STARTROW + i)) $((GRABCOL + 1)) $newrow
i=$((i + 1))
done
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
# Exactly one top-left corner should remain - the pane's current position.
# This is expected to fail before the fix: multiple corners (a trail of
# un-erased frames) would remain from the intermediate drag positions.
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "expected exactly 1 corner after drag, found $n (ghost frames left behind)"
exit 0

View File

@@ -0,0 +1,88 @@
#!/bin/sh
# Each drag motion must draw the floating pane content once.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status-interval 0 || exit 1
$INNER set-option -g automatic-rename off || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
"printf 'DRAGMARK'; exec sleep 100") || exit 1
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# Begin capture after focus changes from the initial mouse press have settled.
mouse 0 "$GRABCOL" "$BORDERROW" M
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1
$INNER refresh-client || exit 1
sleep 0.5
grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content"
grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row"
offset=$(wc -c <"$DIR/output")
i=0
steps=6
while [ "$i" -lt "$steps" ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns"
tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output"
n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output")
[ "$n" -eq "$steps" ] ||
fail "$steps drag motions wrote the floating pane content $n times"
exit 0

View File

@@ -0,0 +1,159 @@
#!/bin/sh
# Regression test: dragging a floating pane across another pane's ordinary
# content must not redraw that other pane's scrollbar, unless the drag
# actually crosses the scrollbar's own strip.
#
# cmd_resize_pane_redraw_floating() (cmd-resize-pane.c) used to flag
# PANE_REDRAWSCROLLBAR on any pane whose whole *body* intersected the
# floating pane's old or new rectangle, rather than just its narrow
# scrollbar strip - so dragging a floating pane back and forth over an
# ordinary tiled pane's content (never touching its scrollbar) still
# needlessly redrew that pane's scrollbar on every motion step. See
# tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by giving the non-dragged pane a distinctive scrollbar
# colour and counting how many times its SGR code appears in the client's
# raw output while the floating pane is dragged vertically over that pane's
# body, well clear of its scrollbar column: with the fix, it should never
# reappear after the initial draw.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.5
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on || fail "set mouse failed"
$TMUX set -g default-command 'sh -c "sleep 100"' || fail "set default-command failed"
$TMUX set -g pane-scrollbars on || fail "set pane-scrollbars failed"
$TMUX split-window -h -t inner 'sh -c "sleep 100"' || fail "split-window failed"
PANES=$($TMUX list-panes -t inner -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
[ -n "$LEFT" ] || fail "could not identify left pane"
# A distinctive scrollbar colour for the non-dragged (left) pane only.
$TMUX set -p -t "$LEFT" pane-scrollbars-style 'fg=colour201,bg=colour17' ||
fail "set pane-scrollbars-style failed"
ALEFT=$($TMUX display-message -p -t "$LEFT" '#{pane_left}')
ATOP=$($TMUX display-message -p -t "$LEFT" '#{pane_top}')
AWIDTH=$($TMUX display-message -p -t "$LEFT" '#{pane_width}')
AHEIGHT=$($TMUX display-message -p -t "$LEFT" '#{pane_height}')
[ "$AWIDTH" -gt 15 ] || fail "left pane too narrow for this test ($AWIDTH)"
# A small floating pane placed well inside the left pane's content area,
# clear of its (right-hand) scrollbar column by several columns.
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 8 -y 5 \
-X $((ALEFT + 2)) -Y $((ATOP + 2))) || fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
[ $((FLEFT + FWIDTH + 3)) -lt $((ALEFT + AWIDTH)) ] ||
fail "sanity: floating pane too close to the scrollbar column"
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which draws the scrollbars) has already happened, missing it.
$TMUX2 new-session -d -x 60 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
# Sanity check: the distinctive scrollbar colour reaches the client at all.
grep -qa '48;5;201' $TMP || fail "sanity: scrollbar colour never reached the client"
: >$TMP
# Drag the floating pane straight up and down by its top border, staying at
# a fixed column the whole time - this never crosses the left pane's
# scrollbar strip, only its ordinary content.
GRABCOL=$((FLEFT + FWIDTH / 2))
row=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((row + 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
i=0
while [ $i -lt 6 ]; do
newrow=$((row - 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
# The scrollbar colour should never reappear - its geometry never changed,
# and the drag never crossed its column. This is expected to fail before
# the fix - see the header comment.
n=$(grep -ac '48;5;201' $TMP)
[ "$n" -eq 0 ] ||
fail "left pane's scrollbar was redrawn $n times while dragging over its body only"
# Cross the scrollbar and return, comparing each damaged redraw with a full
# refresh to check that covering and uncovering its cells leaves no artifacts.
offset=$((ALEFT + AWIDTH - FLEFT - FWIDTH / 2))
for delta in "$offset" "$((-offset))"; do
oldleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
newcol=$((GRABCOL + delta))
drag "$GRABCOL" "$row" "$newcol" "$row"
GRABCOL=$newcol
newleft=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
[ "$newleft" -eq "$((oldleft + delta))" ] ||
fail "floating pane did not move across the scrollbar as expected"
before=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))")
$TMUX refresh-client || fail "refresh-client failed"
sleep 0.5
after=$($TMUX2 capture-pane -pe -t "$OUTER" -S 0 -E "$((AHEIGHT - 1))")
[ "$before" = "$after" ] ||
fail "scrollbar crossing differs from a full redraw"
done
exit 0

View File

@@ -0,0 +1,93 @@
#!/bin/sh
# Synchronized drag output must not cause a full redraw of untouched rows.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status-interval 0 || exit 1
$INNER set-option -g automatic-rename off || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
"printf 'DRAGMARK'; exec sleep 100") || exit 1
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$INNER set-option -as terminal-features ',screen-256color:sync' || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# Begin capture after focus changes from the initial mouse press have settled.
mouse 0 "$GRABCOL" "$BORDERROW" M
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output'" || exit 1
$INNER refresh-client || exit 1
sleep 0.5
grep -aq DRAGMARK "$DIR/output" || fail "capture missed floating pane content"
grep -aq OUTSIDE "$DIR/output" || fail "capture missed untouched row"
offset=$(wc -c <"$DIR/output")
i=0
steps=6
while [ "$i" -lt "$steps" ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" -eq "$((XOFF + steps))" ] || fail "floating pane did not move six columns"
tail -c +"$((offset + 1))" "$DIR/output" >"$DIR/drag-output"
n=$(perl -0777 -ne '$n = () = /DRAGMARK/g; print "$n\n"' "$DIR/drag-output")
[ "$n" -ge "$steps" ] || fail "drag output did not reach the client"
perl -0777 -ne 'exit(/\e\[\?2026h/ ? 0 : 1)' "$DIR/drag-output" ||
fail "drag did not use synchronized output"
if grep -aq OUTSIDE "$DIR/drag-output"; then
fail "synchronized drag redrew an untouched row"
fi
exit 0

View File

@@ -0,0 +1,206 @@
#!/bin/sh
# Mirror of floating-pane-drag-wide-character.sh for the *right* edge of a
# damage rectangle. redraw_damage_grow_span_clip() (screen-redraw.c) widens
# a damage rectangle's right edge by one cell whenever it isn't already at
# the span's own edge, to pull in a wide character's base half when the
# edge lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell (a character fully
# outside the range), growing right pulls in just that base cell -
# tty_draw_line() sees it can't fit that character's full width in the
# remaining range (tty_draw_line_get_empty()'s gc->data.width > nx check)
# and clears it, exactly as the left-edge bug cleared a neighbouring
# character's padding half.
#
# As with the left-edge test, the exact column parity needs to be
# deterministic to actually catch it. This constructs it by creating the
# floating pane, checking its real position, and recreating it one column
# over if necessary until the vacated rectangle's right edge lands on a
# base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidecharr-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
PANEWIDTH=12
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated right edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's right edge
# (xoff + PANEWIDTH + 1, the border-grown exclusive end - see
# window_pane_damage_floating(), window.c) lands on an even (base-cell)
# column.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x "$PANEWIDTH" -y 3 \
-X "$startx" -Y 5 \
'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldright=$((XOFF + PANEWIDTH + 1))
if [ $((oldright % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF + PANEWIDTH + 1) % 2)) -eq 0 ] ||
fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF + PANEWIDTH - 2))
COL2=$((XOFF + PANEWIDTH + 2))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle - far enough right
# that its new position starts past the checked columns above (which sit
# just past the pane's *original* right edge).
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0

View File

@@ -0,0 +1,200 @@
#!/bin/sh
# Damage at a floating pane's vacated edge must always redraw a complete
# grid character. redraw_damage_grow_span_clip() (screen-redraw.c) widens a
# damage rectangle's left edge by one cell whenever it isn't already at the
# span's own edge, to pull in a wide character's base half when the edge
# lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell, growing left
# walks into the *previous*, unrelated character's padding cell and blanks
# it (tty_draw_line() treats any leading padding cell as proof its own
# range starts mid-character).
#
# This is a general damage-composition bug, not specific to any one kind of
# pane, but the exact column parity needs to be deterministic to actually
# catch it (a lucky parity draws fine). This constructs it by creating the
# floating pane, checking its real position (the border-framing offset
# added to -X is not something to hand-compute), and recreating it one
# column over if necessary until the vacated rectangle's left edge lands on
# a base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidechar-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's left edge
# (xoff - 1) lands on an even (base-cell) column - the odd case is the one
# every earlier manual test happened to land on by chance.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 12 -y 3 -X "$startx" \
-Y 5 'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldleft=$((XOFF - 1))
if [ $((oldleft % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF - 1) % 2)) -eq 0 ] || fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF - 4))
COL2=$((XOFF + 4))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle.
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0

View File

@@ -0,0 +1,102 @@
#!/bin/sh
# Returning from the alternate screen must redraw the visible part of a
# floating pane clipped at the left edge, the top edge, or both.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Loffscreen-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $row (1 .. 5) {
print "\e[$row;1H", 'A' x 15;
}
while (!-e "$ENV{TRIGGER}-alternate") {
select undef, undef, undef, 0.01;
}
print "\e[?1049h";
for my $row (1 .. 5) {
print "\e[$row;1H", 'B' x 15;
}
while (!-e "$ENV{TRIGGER}-restore") {
select undef, undef, undef, 0.01;
}
print "\e[?1049l";
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || 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 -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
for position in left top both; do
case "$position" in
left) x=-5; y=2 ;;
top) x=5; y=-2 ;;
both) x=-5; y=-2 ;;
esac
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X "$x" -Y "$y" \
"TRIGGER='$DIR/$position' perl '$EMITTER'") || exit 1
[ "$($INNER display-message -p -t "$FLOAT" '#{pane_left},#{pane_top}')" = "$((x + 1)),$((y + 1))" ] ||
fail "$position: floating pane has unexpected position"
wait_outer_has AAAAA
cp "$CAPTURE" "$DIR/primary"
: >"$DIR/$position-alternate"
wait_outer_has BBBBB
: >"$DIR/$position-restore"
wait_outer_has AAAAA
cmp -s "$DIR/primary" "$CAPTURE" ||
fail "$position: primary screen was not completely restored"
$INNER kill-pane -t "$FLOAT" || exit 1
done
exit 0

View File

@@ -0,0 +1,179 @@
#!/bin/sh
# A damage rectangle's clip range is grown to avoid splitting a wide
# character, but redraw_damage_grow_span_clip() (screen-redraw.c) only ever
# checks the span's own pane *content* grid (wp->screen) for that. For a
# REDRAW_SPAN_PANE span, that same range is then also handed to
# redraw_damage_draw_pane_prompt() to recompose the pane's separately
# rendered prompt (wp->prompt, e.g. from "command-prompt -P") over the
# damaged sub-range - but the prompt is drawn into its own, freshly
# allocated one-line screen, unrelated to the pane's content grid, so a
# range grown (or left ungrown) against the content is not necessarily
# grown correctly for the prompt's own wide characters.
#
# This is invisible when the pane's own content is plain ASCII (as here):
# redraw_damage_grow_span_clip() never finds anything to grow against, so
# the raw, ungrown geometric range is passed straight through to the
# prompt - and if that range's edge lands mid-character in the *prompt's*
# grid, tty_draw_line() clears the character it cuts through
# (tty_draw_line_get_empty()'s gc->data.width > nx check, for a trailing
# base cell with no room left for its padding half).
#
# The trigger is a palette change (OSC 4) in a tiled pane that is one half
# of a vertical split running the full height of the window - occluded
# under the floating pane, but still geometrically triggering a redraw of
# its own rectangle. Positioned so the split boundary falls inside the
# floating pane's own CJK prompt, this reproduces exactly Codex's report:
# "a floating pane containing a CJK prompt across a tiled-pane boundary -
# a palette update in the tiled pane blanks a prompt character."
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpromptwide-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_prompt_row_intact()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
line=$(sed -n "${PROMPTROW}p" "$CAPTURE")
case $line in
*"$PROMPTTEXT"*) return 0 ;;
esac
sleep 0.1
i=$((i + 1))
done
fail "the CJK prompt was not intact after the palette-triggered damage - got: $line"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $line = <STDIN>;
print "\e]4;1;rgb:11/22/33\e\\";
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 12 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
LEFT=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1
# Split so the boundary between the two tiled panes falls at column 17 -
# used below to pick a floating-pane column that lands the boundary
# mid-character inside the prompt.
RIGHT=$($INNER split-window -t inner -h -l 43 -PF '#{pane_id}' \
'sleep 100') || exit 1
RX=$($INNER display-message -p -t "$RIGHT" '#{pane_left}') || exit 1
# Create the floating pane, then check its actual resulting position (the
# border-framing offset added to -X is not something to hand-compute). Try
# adjacent starting columns until the split boundary lands on an odd
# (padding-half) column of the prompt's own numbering, and within the
# prompt's 12-column width.
startx=10
tries=0
while [ "$tries" -lt 4 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 24 -y 5 -X "$startx" \
-Y 2 "sh -c 'i=0; while [ \$i -lt 10 ]; do \
printf AAAAAAAAAAAAAAAAAAAAAA\\\\n; i=\$((i+1)); done; sleep 100'") ||
exit 1
sleep 0.2
X1=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
Y1=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
H1=$($INNER display-message -p -t "$FLOAT" '#{pane_height}')
local=$((RX - X1 - 1))
if [ "$local" -ge 1 ] && [ "$local" -le 11 ] &&
[ $((local % 2)) -eq 1 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
local=$((RX - X1 - 1))
[ "$local" -ge 1 ] && [ "$local" -le 11 ] && [ $((local % 2)) -eq 1 ] ||
fail "could not find bad-parity starting column"
$OUTER new-session -d -s outer -x 60 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has AAAAAAAAAAAAAAAAAAAAAA
CLIENT=$($INNER list-clients -F '#{client_name}') || exit 1
$INNER select-pane -t "$FLOAT" || exit 1
PROMPTTEXT=$(printf '\344\270\255' | perl -CSD -ne 'print $_ x 6')
$INNER command-prompt -b -P -t "$CLIENT" -p "$PROMPTTEXT" \
'display-message -- %1' || exit 1
wait_outer_has "$PROMPTTEXT"
PROMPTROW=$((Y1 + H1))
# Trigger the damage: unblock the emitter so it fires the palette change in
# the left tiled pane, which is occluded under (but geometrically overlaps)
# the floating pane's prompt row.
$INNER send-keys -t "$LEFT" Enter || exit 1
wait_prompt_row_intact
exit 0

View File

@@ -0,0 +1,151 @@
#!/bin/sh
# Damage redraws must show each client's own pane status, even though the
# cached status screen is shared between clients.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lstatuscc-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_for_marker()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client in $target did not receive $marker"
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $phase (1 .. 4) {
while (!-e "$ENV{TRIGGER}-$phase") {
select undef, undef, undef, 0.01;
}
# Change an unused palette entry and acknowledge it in the pane body.
print "\e]4;200;rgb:11/22/0$phase\a\e[1;1HDAMAGE$phase";
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 20 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g pane-border-lines simple || exit 1
$INNER set -g pane-border-status top || exit 1
$INNER set -g pane-border-format 'CLIENT=<#{client_name}>' || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 35 -y 6 -X 5 -Y 5 \
'sleep 100') || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
LEFT=$($OUTER display-message -p -t outer:0.0 '#{pane_id}') || exit 1
RIGHT=$($OUTER split-window -h -PF '#{pane_id}' 'sleep 100') || exit 1
for target in "$LEFT" "$RIGHT"; do
$OUTER respawn-pane -k -t "$target" "$INNER attach-session -t inner" ||
exit 1
done
wait_for_clients
NAME1=$($OUTER display-message -p -t "$LEFT" '#{pane_tty}') || exit 1
NAME2=$($OUTER display-message -p -t "$RIGHT" '#{pane_tty}') || exit 1
# Disable periodic status updates above and trigger damage without a command
# that also requests a status redraw. Each client must keep its own title.
$INNER refresh-client -t "$NAME1" || exit 1
$INNER refresh-client -t "$NAME2" || exit 1
sleep 0.5
for phase in 0 1 2; do
if [ "$phase" -ne 0 ]; then
: >"$DIR/trigger-$phase"
wait_for_marker "$LEFT" "DAMAGE$phase"
wait_for_marker "$RIGHT" "DAMAGE$phase"
sleep 0.2
fi
for target in "$LEFT" "$RIGHT"; do
if [ "$target" = "$LEFT" ]; then
name=$NAME1
other=$NAME2
else
name=$NAME2
other=$NAME1
fi
$OUTER capture-pane -p -S 5 -E 10 -t "$target" \
>"$CAPTURE" || exit 1
grep -Fq "CLIENT=<$name>" "$CAPTURE" ||
fail "phase $phase: missing $name's pane status"
if grep -Fq "CLIENT=<$other>" "$CAPTURE"; then
fail "phase $phase: $name received $other's pane status"
fi
done
done
# Leave just one client so a cache keyed only by client would remain stale.
$OUTER respawn-pane -k -t "$RIGHT" 'sleep 100' || exit 1
i=0
while [ "$($INNER list-clients | wc -l)" -ne 1 ]; do
[ "$i" -lt 50 ] || fail "second client did not detach"
sleep 0.1
i=$((i + 1))
done
$INNER set-environment -g TEST_STATUS_VALUE initial || exit 1
$INNER set -g pane-border-format 'VALUE=#{TEST_STATUS_VALUE}' || exit 1
$INNER refresh-client -t "$NAME1" || exit 1
wait_for_marker "$LEFT" VALUE=initial
for phase in 3 4; do
# Changing the environment does not itself request a status redraw.
$INNER set-environment -g TEST_STATUS_VALUE "phase$phase" || exit 1
: >"$DIR/trigger-$phase"
wait_for_marker "$LEFT" "DAMAGE$phase"
wait_for_marker "$LEFT" "VALUE=phase$phase"
done
exit 0

View File

@@ -0,0 +1,102 @@
#!/bin/sh
# Resizing a floating pane must refresh session status formats which depend on
# its geometry, not only the pane scene and borders.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lfloating-status-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has_status()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
tail -1 "$CAPTURE" | grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client status did not show $marker"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
$INNER new-session -d -s inner -x 50 -y 12 'sleep 100' || exit 1
FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 10 -y 5 -X 5 -Y 3 \
'sleep 100') || fail "could not create floating pane"
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status on || exit 1
$INNER set-option -g status-position bottom || exit 1
$INNER set-option -g status-left 'WIDTH=#{pane_width}' || exit 1
$INNER set-option -g status-right '' || exit 1
$INNER set-option -g status-interval 0 || exit 1
$OUTER new-session -d -s outer -x 50 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
OLD_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
wait_outer_has_status "WIDTH=$OLD_WIDTH"
RIGHT=$($INNER display-message -p -t "$FLOAT" '#{pane_right}')
TOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
X=$((RIGHT + 2))
Y=$((TOP + 2))
# Grab the right frame and enlarge the floating pane.
mouse 0 "$X" "$Y" M
mouse 32 "$((X + 1))" "$Y" M
mouse 32 "$((X + 8))" "$Y" M
mouse 0 "$((X + 8))" "$Y" m
NEW_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
[ "$NEW_WIDTH" -ne "$OLD_WIDTH" ] || fail "floating pane was not resized"
wait_outer_has_status "WIDTH=$NEW_WIDTH"
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

@@ -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

@@ -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

@@ -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

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

View File

@@ -1,146 +0,0 @@
#!/bin/sh
# Test per-client keyboard protocol detection and pane-side encoding.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
KCONF=$(mktemp)
LCONF=$(mktemp)
OUT=$(mktemp)
TMP=$(mktemp)
SOCKETS="testKmc$$ testKka$$ testKua$$ testKrt$$ testKru$$ testKlq$$ testKlu$$"
printf '%s\n' 'set -g extended-keys on' >"$KCONF"
printf '%s\n' 'set -g extended-keys off' >"$LCONF"
cleanup()
{
rm -f "$KCONF" "$LCONF" "$OUT" "$TMP"
for socket in $SOCKETS; do
$TEST_TMUX -L"$socket" kill-server 2>/dev/null
done
}
trap cleanup 0 1 15
wait_for_mode()
{
tmux=$1
wanted=$2
i=0
while [ "$($tmux display-message -pt: '#{pane_key_mode}')" != "$wanted" ] &&
[ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$($tmux display-message -pt: '#{pane_key_mode}')" = "$wanted" ]
}
wait_for_output()
{
i=0
while [ ! -s "$1" ] && [ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ -s "$1" ]
}
client_mode()
{
tmux=$1
session=$2
if [ -z "$session" ]; then
$tmux list-clients -F '#{client_key_mode}'
else
$tmux list-clients -F '#{client_session}:#{client_key_mode}' |
sed -n "s/^$session://p"
fi
}
wait_for_client_mode()
{
tmux=$1
session=$2
wanted=$3
i=0
while [ "$(client_mode "$tmux" "$session")" != "$wanted" ] &&
[ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$(client_mode "$tmux" "$session")" = "$wanted" ]
}
# Each client must be negotiated independently. A tmux with extended-keys off
# stands in for a terminal without Kitty keys, since it does not answer the
# query.
M="$TEST_TMUX -LtestKmc$$ -f$KCONF"
KA="$TEST_TMUX -LtestKka$$ -f$KCONF"
UA="$TEST_TMUX -LtestKua$$ -f$LCONF"
$M new-session -d -x80 -y24 -s kitty || exit 1
$M new-session -d -x80 -y24 -s csiu || exit 1
$KA new-session -d -x80 -y24 "$M attach-session -t kitty" || exit 1
$UA new-session -d -x80 -y24 "$M attach-session -t csiu" || exit 1
wait_for_mode "$KA" 'Kitty 1' || exit 1
# Each client must report the protocol its own terminal negotiated.
wait_for_client_mode "$M" kitty 'Kitty 1' || exit 1
wait_for_client_mode "$M" csiu 'Ext' || exit 1
uc=$($M list-clients -F '#{client_name} #{client_session}' |
awk '$2 == "csiu" { print $1 }')
[ -n "$uc" ] || exit 1
$M command-prompt -t"$uc" -k 'display-message -pl "%%"' >"$TMP" &
pid=$!
sleep 0.2
$UA send-keys Escape '[97;5u'
wait "$pid"
[ "$(tr -d '[:space:]' <"$TMP")" = 'C-a' ] || exit 1
# A Kitty-capable terminal must still be reported when it is not in use.
$M set-option -g extended-keys off
wait_for_client_mode "$M" kitty 'VT10x (Kitty)' || exit 1
wait_for_client_mode "$M" csiu 'VT10x' || exit 1
$M set-option -g extended-keys on
wait_for_client_mode "$M" kitty 'Kitty 1' || exit 1
$KA kill-server 2>/dev/null
$UA kill-server 2>/dev/null
$M kill-server 2>/dev/null
# Standard extended input must be translated for a Kitty-requesting pane.
: >"$OUT"
R="$TEST_TMUX -LtestKrt$$ -f$KCONF"
RU="$TEST_TMUX -LtestKru$$ -f$LCONF"
$R new-session -d -x80 -y24 \
"stty raw -echo; printf '\033[>1u'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" || exit 1
$RU new-session -d -x80 -y24 "$R attach-session" || exit 1
wait_for_client_mode "$R" '' 'Ext' || exit 1
$RU send-keys C-a
wait_for_output "$OUT" || exit 1
[ "$(tr -d ' \n' <"$OUT")" = '1b5b39373b3575' ] || exit 1
$RU kill-server 2>/dev/null
$R kill-server 2>/dev/null
# An application is told the Kitty flags it asked for even when a client that
# only supports VT10x is attached, and keys from that client are encoded as it
# asked.
: >"$OUT"
: >"$TMP"
printf '%s\n' "set -as terminal-overrides ',*:Eneks@:Dseks@'" >>"$KCONF"
LQ="$TEST_TMUX -LtestKlq$$ -f$KCONF"
LU="$TEST_TMUX -LtestKlu$$ -f$LCONF"
$LQ new-session -d -x80 -y24 \
"stty raw -echo; sleep 2; printf '\033[>1u\033[?u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$TMP'; sleep 5" || exit 1
$LU new-session -d -x80 -y24 "$LQ attach-session" || exit 1
wait_for_output "$OUT" || exit 1
[ "$(tr -d ' \n' <"$OUT")" = '1b5b3f3175' ] || exit 1
wait_for_client_mode "$LQ" '' 'VT10x' || exit 1
$LU send-keys C-a
wait_for_output "$TMP" || exit 1
[ "$(tr -d ' \n' <"$TMP")" = '1b5b39373b3575' ] || exit 1
exit 0

View File

@@ -1,116 +0,0 @@
#!/bin/sh
# Test client-side Kitty keyboard protocol support: an outer tmux acts as a
# Kitty-capable terminal for the inner tmux which is the one under test. The
# inner tmux queries the outer one, the outer one replies, and the inner one
# then parses the key sequences sent to it.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
CONF=$(mktemp)
TMP=$(mktemp)
printf '%s\n' 'set -g extended-keys on' >"$CONF"
TMUX="$TEST_TMUX -LtestKIA$$ -f$CONF"
TMUX2="$TEST_TMUX -LtestKIB$$ -f$CONF"
trap 'rm -f "$CONF" "$TMP"; $TMUX kill-server 2>/dev/null; \
$TMUX2 kill-server 2>/dev/null' 0 1 15
$TMUX2 new-session -d -x80 -y24 || exit 1
$TMUX new-session -d -x80 -y24 "$TMUX2 attach" || exit 1
exit_status=0
wait_for_mode()
{
i=0
while [ "$($TMUX display-message -pt: '#{pane_key_mode}')" != "$1" ] &&
[ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$($TMUX display-message -pt: '#{pane_key_mode}')" = "$1" ]
}
assert_key()
{
keys=$1
expected=$2
$TMUX2 command-prompt -k 'display-message -pl "%%"' >"$TMP" &
sleep 0.15
# Send raw bytes so the outer tmux does not encode them itself.
raw=$(printf '%s' "$keys" | sed -e 's/Escape /\\033/g' -e 's/ //g')
$TMUX send-keys -H $(printf "$raw" | od -An -v -t x1)
wait
actual=$(tr -d '[:space:]' <"$TMP")
if [ "$actual" = "$expected" ]; then
[ -n "$VERBOSE" ] && echo "[PASS] $keys -> $actual"
else
echo "[FAIL] $keys -> $expected (got '$actual')"
exit_status=1
fi
}
# The inner tmux must detect the outer one and push its flags.
if ! wait_for_mode 'Kitty 1'; then
echo "[FAIL] inner tmux did not enable the Kitty protocol"
exit 1
fi
# Modifiers, including the Kitty-only Super and Hyper.
assert_key 'Escape [97;2u' 'S-a'
assert_key 'Escape [97;3u' 'M-a'
assert_key 'Escape [97;5u' 'C-a'
assert_key 'Escape [97;6u' 'C-S-a'
assert_key 'Escape [97;7u' 'C-M-a'
assert_key 'Escape [97;9u' 's-a'
assert_key 'Escape [97;17u' 'H-a'
assert_key 'Escape [65;2u' 'S-A'
# Keys with a dedicated tmux key code.
assert_key 'Escape [9;5u' 'C-Tab'
assert_key 'Escape [9;2u' 'BTab'
assert_key 'Escape [13;5u' 'C-Enter'
assert_key 'Escape [27u' 'Escape'
assert_key 'Escape [32;5u' 'C-Space'
assert_key 'Escape [127;5u' 'C-BSpace'
# Keys with a CSI final byte other than u.
assert_key 'Escape [1;5A' 'C-Up'
assert_key 'Escape [1;3D' 'M-Left'
assert_key 'Escape [1;5H' 'C-Home'
assert_key 'Escape [1;5P' 'C-F1'
# Functional and non-ASCII keys.
assert_key 'Escape [57399;5u' 'C-KP0'
assert_key 'Escape [233;5u' 'C-é'
# The alternate key and associated text fields are not requested but must be
# ignored if a terminal sends them anyway.
assert_key 'Escape [97:65;2u' 'S-a'
assert_key 'Escape [97:65:97;5u' 'C-a'
assert_key 'Escape [97;5;97u' 'C-a'
# A repeat event is a key press, a release event is dropped. The C-b that
# follows shows that the sequence was consumed and not passed through.
assert_key 'Escape [97;5:1u' 'C-a'
assert_key 'Escape [97;5:2u' 'C-a'
assert_key 'Escape [97;5:3u Escape [98;5u' 'C-b'
# The Meta modifier bit is not representable and is dropped.
assert_key 'Escape [97;33u Escape [98;5u' 'C-b'
# Lock modifiers are ignored rather than dropping the key.
assert_key 'Escape [97;65u' 'a'
assert_key 'Escape [97;129u' 'a'
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
exit $exit_status

View File

@@ -1,85 +0,0 @@
#!/bin/sh
# Test the Kitty keyboard protocol against a real terminal rather than against
# another copy of tmux. This drives kitty through its remote control interface
# so that kitty itself encodes the key presses.
#
# This has to be run by hand from this directory. The Makefile runs tests with
# env -i, which removes DISPLAY and HOME, so kitty cannot start and this exits
# immediately without testing anything. It is also skipped unless kitty is
# installed and is new enough to have send-key.
PATH=/bin:/usr/bin
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
command -v kitty >/dev/null 2>&1 || exit 0
[ -n "$DISPLAY" ] || [ -n "$WAYLAND_DISPLAY" ] || exit 0
kitty @ --help 2>/dev/null | grep -q 'send-key' || exit 0
DIR=$(mktemp -d)
CONF=$DIR/conf
SOCKET=$DIR/kitty
OUT=$DIR/out
TMUX="$TEST_TMUX -LtestKrt$$ -f$CONF"
printf '%s\n' 'set -g extended-keys on' \
'set -g extended-keys-format kitty' >"$CONF"
cleanup()
{
$TMUX kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
kitty -o allow_remote_control=yes --listen-on "unix:$SOCKET" \
-o confirm_os_window_close=0 -e \
$TMUX new-session -s r "sleep 120" >"$DIR/err" 2>&1 &
# The terminfo entry for kitty may be missing, so treat a server which never
# appears as a reason to skip rather than as a failure.
i=0
while ! $TMUX has-session -t r 2>/dev/null && [ "$i" -lt 150 ]; do
sleep 0.1
i=$((i + 1))
done
$TMUX has-session -t r 2>/dev/null || exit 0
# The client must negotiate the protocol with the real terminal.
i=0
while [ "$($TMUX list-clients -F '#{client_key_mode}')" != 'Kitty 1' ] &&
[ "$i" -lt 100 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$($TMUX list-clients -F '#{client_key_mode}')" = 'Kitty 1' ] || exit 1
# kitty encodes the key, tmux parses it and encodes it again for the pane.
try_key()
{
key=$1
expected=$2
: >"$OUT"
$TMUX respawn-pane -k -t r: \
"stty raw -echo; printf '\033[>1u'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 20" || return 1
sleep 1
kitty @ --to "unix:$SOCKET" send-key "$key" 2>/dev/null || return 1
i=0
while [ ! -s "$OUT" ] && [ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$(tr -d ' \n' <"$OUT")" = "$expected" ]
}
try_key ctrl+a '1b5b39373b3575' || exit 1
try_key alt+a '1b5b39373b3375' || exit 1
try_key ctrl+shift+a '1b5b39373b3675' || exit 1
# Super cannot be represented by the standard extended keys at all, so this
# only succeeds if the whole chain used the Kitty protocol.
try_key super+a '1b5b39373b3975' || exit 1
exit 0

View File

@@ -1,108 +0,0 @@
#!/bin/sh
# Test pane-side Kitty keyboard protocol state and key encoding. This uses
# synthetic protocol sequences and does not require a particular terminal.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
CONF=$(mktemp)
OUT=$(mktemp)
TMUX="$TEST_TMUX -LtestK$$ -f$CONF"
trap 'rm -f "$CONF" "$OUT"; $TMUX kill-server 2>/dev/null' 0 1 15
printf '%s\n' 'set -g extended-keys on' >"$CONF"
wait_for_output()
{
i=0
while [ ! -s "$OUT" ] && [ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ -s "$OUT" ]
}
wait_for_mode()
{
wanted=$1
i=0
while [ "$($TMUX display-message -pt: '#{pane_key_mode}')" != "$wanted" ] &&
[ "$i" -lt 50 ]; do
sleep 0.1
i=$((i + 1))
done
[ "$($TMUX display-message -pt: '#{pane_key_mode}')" = "$wanted" ]
}
check_output()
{
expected=$1
actual=$(tr -d ' \n' <"$OUT")
if [ "$actual" != "$expected" ]; then
echo "expected $expected, got $actual"
exit 1
fi
}
$TMUX new-session -d -x80 -y24 \
"stty raw -echo; printf '\033[>1u'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5" || exit 1
wait_for_mode 'Kitty 1'
$TMUX send-keys -t: C-S-a
wait_for_output
check_output 1b5b39373b3675
: >"$OUT"
$TMUX respawn-pane -k -t: \
"stty raw -echo; printf '\033[>1u'; dd bs=1 count=7 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5"
wait_for_mode 'Kitty 1'
$TMUX send-keys -t: s-a
wait_for_output
check_output 1b5b39373b3975
: >"$OUT"
$TMUX respawn-pane -k -t: \
"stty raw -echo; printf '\033[>8u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5"
wait_for_mode 'Kitty 8'
$TMUX send-keys -t: a
wait_for_output
check_output 1b5b393775
: >"$OUT"
$TMUX respawn-pane -k -t: \
"stty raw -echo; printf '\033[>1u\033[?u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5"
wait_for_output
check_output 1b5b3f3175
: >"$OUT"
$TMUX respawn-pane -k -t: \
"stty raw -echo; printf '\033[>4;2m\033[?u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5"
wait_for_output
check_output 1b5b3f3075
: >"$OUT"
$TMUX respawn-pane -k -t: \
"stty raw -echo; printf '\033[>1u\033[>8u\033[<u\033[?u\033[>8u\033[<2u\033[?u'; dd bs=1 count=10 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5"
wait_for_output
check_output 1b5b3f31751b5b3f3075
: >"$OUT"
# "always" forces modifyOtherKeys mode 1, not Kitty keys.
$TMUX set-option -g extended-keys always
$TMUX respawn-pane -k -t: \
"stty raw -echo; printf '\033[>0u\033[?u'; dd bs=1 count=5 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5"
wait_for_output
check_output 1b5b3f3075
wait_for_mode 'Ext 1'
$TMUX set-option -g extended-keys on
: >"$OUT"
$TMUX respawn-pane -k -t: \
"stty raw -echo; printf '\033[>1u\033[?u\033[?1049h\033[?u\033[>8u\033[?u\033[?1049l\033[?u'; dd bs=1 count=20 2>/dev/null | od -An -v -t x1 >'$OUT'; sleep 5"
wait_for_output
check_output 1b5b3f31751b5b3f30751b5b3f38751b5b3f3175
$TMUX kill-server 2>/dev/null
exit 0

View File

@@ -50,7 +50,8 @@
# layout it names is a single cell or a split;
# - a window whose only tiled pane has been killed, which leaves it with a
# floating cell as its layout root or with a root node holding nothing but
# floating cells, producing no v1 dump at all, and being parsed as v1;
# floating cells, producing an empty v1 body with a checksum, and being
# parsed as v1;
# - the %layout-change notification, in both formats at once: two control
# clients watching one layout change, only one of which has asked for new
# layouts, and the number of notifications a change produces in each format;
@@ -376,10 +377,7 @@ must_equal 'Layout after select-pane back' "$(layout L:two)" "$SPLIT"
# With nothing zoomed the two layout formats agree.
#
# The zoomed case is deliberately not covered here. While a pane is zoomed
# #{window_layout} dumps the saved (unzoomed) layout and
# #{window_visible_layout} the zoomed one, but that depends on how zooming
# stashes the layout root rather than on anything in layout-custom.c.
# Zoomed layouts are checked below with floating panes.
must_equal 'Visible layout' "$(visible_layout L:two)" "$SPLIT"
# ---------------------------------------------------------------------------
@@ -703,6 +701,42 @@ must_contain 'Floating layout back z-index' "$floating" '"z":1'
check_ok select-layout -t L:float "$floating"
must_equal 'Floating layout after round trip' "$(raw_layout L:float)" "$floating"
# A dump taken while zoomed must retain the unzoomed floating z-indexes.
# Check hidden floats, floats above zoom, and a mixture of the two.
for flags in '' A mixed; do
check_ok new-window -d -t L: -n zoom
zt=$($TMUX display-message -p -t L:zoom '#{pane_id}')
check_ok split-window -d -h -t "$zt"
case "$flags" in
A|mixed) first=-Ad ;;
*) first=-d ;;
esac
case "$flags" in
A) second=-Ad ;;
*) second=-d ;;
esac
check_ok new-pane "$first" -t "$zt" -x 20 -y 6 ''
check_ok new-pane "$second" -t "$zt" -x 30 -y 8 ''
for pane in 0 2 3; do
check_ok select-pane -t "L:zoom.$pane"
before=$(raw_layout L:zoom)
legacy=$(v1_layout L:zoom)
check_ok resize-pane -Z -t "L:zoom.$pane"
during=$(raw_layout L:zoom)
must_equal 'Layout while zoomed' "$during" "$before"
must_equal 'Legacy layout while zoomed' \
"$(v1_layout L:zoom)" "$legacy"
must_equal 'Zoom after dumping layout' \
"$($TMUX display-message -p -t L:zoom '#{window_zoomed_flag}')" 1
must_differ 'Visible layout while zoomed' \
"$(visible_layout L:zoom)" "$(layout L:zoom)"
check_ok select-layout -t L:zoom "$during"
must_equal 'Round trip from zoomed layout' \
"$(raw_layout L:zoom)" "$before"
done
check_ok kill-window -t L:zoom
done
# ---------------------------------------------------------------------------
# Floating panes and the legacy (v1) format.
#
@@ -838,12 +872,12 @@ must_equal 'Panes left with one floating pane' \
"$($TMUX display-message -p -t L:gone1 '#{window_panes}')" '1'
# The floating cell is the root and there is nothing tiled under it, so there is
# no v1 dump to make. In particular the floating cell must not be written out on
# only an empty v1 body to dump. The floating cell must not be written out on
# its own, which would be a layout claiming the window is the size and position
# of the floating pane with no pane in it at all.
got=$(v1_layout L:gone1)
check_ok display-message -p alive
must_equal 'v1 dump with one floating pane and no tiled panes' "$got" ''
must_equal 'v1 dump with one floating pane and no tiled panes' "$got" '0000,'
# Two floating panes left, so the node keeps two children, does not collapse,
# and stays the root with nothing but floating cells in it.
@@ -856,11 +890,11 @@ must_equal 'Panes left with two floating panes' \
"$($TMUX display-message -p -t L:gone2 '#{window_panes}')" '2'
# The node is the root this time rather than the floating cell, but it has no
# tiled cell anywhere under it either, so there is still no v1 dump to make -
# tiled cell anywhere under it either, so the v1 body is still empty -
# and making one must not take the server with it.
got=$(v1_layout L:gone2)
check_ok display-message -p alive
must_equal 'v1 dump with two floating panes and no tiled panes' "$got" ''
must_equal 'v1 dump with two floating panes and no tiled panes' "$got" '0000,'
# Nor must parsing a v1 layout against it. There is no tiled pane for the
# layout to name, so whether it is applied or rejected is the format's business;

View File

@@ -400,6 +400,107 @@ $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'

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

View File

@@ -0,0 +1,126 @@
#!/bin/sh
# Damage at a popup edge must redraw complete grid characters. Drawing only a
# wide character's base or padding cell leaves a two-cell hole behind.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpopup-wide-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
POPUP_PID=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
sed -n '3,5p' "$BASE" >"$DIR/want"
sed -n '3,5p' "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the old popup edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
$INNER display-popup -t "$CLIENT" -x 5 -y 5 -w 10 -h 3 -E \
"sh -c 'printf POPUP; exec sleep 100'" &
POPUP_PID=$!
wait_outer_has POPUP
# The first motion starts the drag; the second moves the popup away from its
# old rectangle. Its odd x coordinate bisects the underlying double-width
# cells at both edges.
mouse 0 10 3 M
mouse 32 11 3 M
mouse 32 28 7 M
mouse 0 28 7 m
wait_old_rows_restored
exit 0

View File

@@ -131,6 +131,14 @@ printf '%s\n' "$captured" | grep -Fq 'show-buffer' ||
printf '%s\n' "$captured" | grep -Fq 'show-environment' ||
fail "ambiguous completion list was incomplete"
$OUTER send-keys Escape || exit 1
# Wait until Escape has closed the prompt before sending another Meta key.
# Otherwise the two can be parsed together as a single escape sequence.
i=0
while capture | grep -Fq '(word)'; do
[ "$i" -lt 50 ] || fail "completion prompt did not close"
sleep 0.1
i=$((i + 1))
done
# Add entries to both history rings through real prompts.
bind_prompt 'history-command'

View File

@@ -0,0 +1,113 @@
#!/bin/sh
# Many wrapped rows crossing a panned viewport produce disjoint damage.
# Every row must survive the rectangle-count limit and subsequent merging.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Laccumulate-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Laccumulate-outer-$$ -f/dev/null"
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
wait_marker()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$DIR/capture" || exit 1
grep -q "$1" "$DIR/capture" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client did not receive $1"
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $row (1 .. 80) {
printf "\e[%d;1HROW%02d-%s", $row, $row, 'abcdefghij' x 6;
}
print "\e[1;1H";
while (!-e $ENV{TRIGGER}) {
select undef, undef, undef, 0.01;
}
my $output = '';
# Each wrapped second row needs a separate damage rectangle. Keep the
# batch below the PTY read size so all 18 arrive in the same input pass.
for my $region (0 .. 17) {
my $top = 1 + $region * 4;
$output .= "\e[$top;1H" . ('A' x 80) . ('B' x 24) .
sprintf('DAMAGE%02d', $region) . ('B' x 48);
}
$output .= "\e[80;21HDONE";
syswrite STDOUT, $output;
while (!-e "$ENV{TRIGGER}-merge") {
select undef, undef, undef, 0.01;
}
# Returning from the alternate screen requests a full-pane rectangle. A
# later wrapped-row rectangle overlaps it and must not shrink that damage.
$output = "\e[?1049h\e[40;21HALT-VISIBLE\e[?1049l";
$output .= "\e[1;1H" . ('C' x 80) . ('D' x 24) . 'MERGE00' . ('D' x 49);
$output .= "\e[80;21HMERGED";
syswrite STDOUT, $output;
sleep 100;
PERL
$INNER new-session -d -s inner -x 80 -y 80 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -as terminal-features ',screen:sync' || exit 1
$INNER set -g pane-border-lines simple || exit 1
$OUTER new-session -d -s outer -x 40 -y 80 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 "$INNER attach -t inner" || exit 1
wait_marker ROW80
CLIENT=$($OUTER display -p -t outer:0.0 '#{pane_tty}') || exit 1
$INNER refresh-client -t "$CLIENT" -R 20 || exit 1
# Keep the emitter inactive so its writes use a synchronized frame. Otherwise
# its own queued output can force a full redraw and hide lost rectangles.
$INNER new-pane -x 6 -y 3 -X 65 -Y 74 'sleep 100' || exit 1
sleep 0.2
: >"$DIR/trigger"
wait_marker DONE
sleep 0.2
$OUTER capture-pane -p -t outer:0.0 >"$DIR/before" || exit 1
region=0
while [ "$region" -lt 18 ]; do
marker=$(printf 'DAMAGE%02d' "$region")
sed -n "$((2 + region * 4))p" "$DIR/before" | grep -q "$marker" ||
fail "missing damage for region $region"
region=$((region + 1))
done
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 0.2
$OUTER capture-pane -p -t outer:0.0 >"$DIR/after" || exit 1
diff -u "$DIR/before" "$DIR/after" || fail "accumulated redraw differs from full redraw"
: >"$DIR/trigger-merge"
wait_marker MERGED
sleep 0.2
$OUTER capture-pane -p -t outer:0.0 >"$DIR/before" || exit 1
sed -n '2p' "$DIR/before" | grep -q MERGE00 || fail "merged damage lost the wrapped row"
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 0.2
$OUTER capture-pane -p -t outer:0.0 >"$DIR/after" || exit 1
diff -u "$DIR/before" "$DIR/after" || fail "merged redraw differs from full redraw"
exit 0

View File

@@ -0,0 +1,140 @@
#!/bin/sh
# One terminal stops reading while another consumes floating-pane damage.
# The slow client must catch up after the window's shared damage is cleared.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lblocked-inner-$$ -f/dev/null"
FAST="$TEST_TMUX -Lblocked-fast-$$ -f/dev/null"
SLOW="$TEST_TMUX -Lblocked-slow-$$ -f/dev/null"
STOPPED=
cleanup()
{
[ -n "$STOPPED" ] && kill -CONT "$STOPPED" 2>/dev/null
$FAST kill-server 2>/dev/null
$SLOW kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
wait_marker()
{
terminal=$1
marker=$2
i=0
while [ "$i" -lt 100 ]; do
$terminal capture-pane -p >"$DIR/capture" || exit 1
grep -q "$marker" "$DIR/capture" && return 0
sleep 0.1
i=$((i + 1))
done
fail "client did not receive $marker"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$FAST send-keys -l "$sequence" || exit 1
sleep 0.2
}
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
print 'READY';
while (!-e "$ENV{TRIGGER}-flood") {
select undef, undef, undef, 0.01;
}
my $frame = 0;
while (!-e "$ENV{TRIGGER}-stop") {
my $output = '';
for my $row (1 .. 20) {
$output .= "\e[$row;1H" . join('', map { chr(33 + ($_ + $frame) % 80) } 0 .. 77);
}
print $output;
$frame++;
select undef, undef, undef, 0.01;
}
for my $row (1 .. 20) {
printf "\e[%d;1HFINAL%02d-%s", $row, $row, '0123456789' x 7;
}
print "\e[20;1HDONE";
sleep 100;
PERL
$INNER new-session -d -s inner -x 160 -y 80 \
"TRIGGER='$DIR/trigger' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g mouse on || exit 1
$INNER set -g pane-border-lines simple || exit 1
FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5 \
'printf FLOAT; exec sleep 100') || exit 1
for terminal in "$FAST" "$SLOW"; do
$terminal new-session -d -x 160 -y 80 'sleep 100' || exit 1
$terminal set -g status off || exit 1
$terminal set -g window-size manual || exit 1
$terminal set -g default-terminal screen || exit 1
$terminal respawn-pane -k "$INNER attach -t inner" || exit 1
wait_marker "$terminal" READY
done
FASTCLIENT=$($FAST display -p '#{pane_tty}') || exit 1
SLOWCLIENT=$($SLOW display -p '#{pane_tty}') || exit 1
SLOWPID=$($SLOW display -p '#{pid}') || exit 1
before=$($INNER display -p -c "$FASTCLIENT" '#{client_written}') || exit 1
slowbefore=$($INNER display -p -c "$SLOWCLIENT" '#{client_written}') || exit 1
# The large terminal keeps this backlog below the discard threshold, so
# automatic recovery from discarded output cannot mask lost damage.
# Stop only our outer server, leaving its inner client attached to a PTY
# whose master is no longer read. This creates real terminal backpressure.
STOPPED=$SLOWPID
kill -STOP "$SLOWPID" || exit 1
: >"$DIR/trigger-flood"
i=0
while :; do
written=$($INNER display -p -c "$SLOWCLIENT" '#{client_written}') || exit 1
fastwritten=$($INNER display -p -c "$FASTCLIENT" '#{client_written}') || exit 1
# More than a PTY can buffer has been queued for the stopped terminal,
# while the other terminal is still receiving the same output.
[ "$written" -gt "$((slowbefore + 65536))" ] &&
[ "$fastwritten" -gt "$((before + 65536))" ] && break
[ "$i" -lt 100 ] || fail "slow client did not become blocked"
sleep 0.1
i=$((i + 1))
done
: >"$DIR/trigger-stop"
wait_marker "$FAST" DONE
[ "$($INNER display -p -c "$FASTCLIENT" '#{client_discarded}')" -eq 0 ] ||
fail "fast client also became blocked"
mouse 0 12 6 M
mouse 32 42 6 M
mouse 0 42 6 m
[ "$($INNER display -p -t "$FLOAT" '#{pane_left}')" -eq 36 ] || fail "pane did not move"
$FAST capture-pane -p >"$DIR/fast-before" || exit 1
# Ensure the fast client already restored the vacated frame.
sed -n '6p' "$DIR/fast-before" | grep -q '^FINAL06-0123456789' ||
fail "fast client did not restore old footprint"
kill -CONT "$SLOWPID" || exit 1
STOPPED=
wait_marker "$SLOW" DONE
# Allow queued terminal output and deferred redraws to drain.
sleep 0.5
[ "$($INNER display -p -c "$SLOWCLIENT" '#{client_discarded}')" -eq 0 ] ||
fail "discard recovery could mask lost deferred damage"
$SLOW capture-pane -p >"$DIR/slow-before" || exit 1
diff -u "$DIR/fast-before" "$DIR/slow-before" || fail "slow client did not catch up"
$INNER refresh-client -t "$FASTCLIENT" || exit 1
sleep 0.2
$FAST capture-pane -p >"$DIR/after" || exit 1
diff -u "$DIR/fast-before" "$DIR/after" || fail "damage redraw differed from full redraw"
exit 0

112
regress/redraw-damage-only.sh Executable file
View File

@@ -0,0 +1,112 @@
#!/bin/sh
# Check a redraw callback which has no accompanying client redraw flags. A
# wrapped row crossing a panned viewport cannot use the direct tty path.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldamage-only-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
TRIGGER=$DIR/trigger
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_inner_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 2>/dev/null |
grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane did not contain $marker"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $row (1 .. 12) {
print "\e[$row;1H", 'o' x 79;
}
print "\e[1;1H";
while (!-e $ENV{TRIGGER}) {
select undef, undef, undef, 0.01;
}
my $second = ('B' x 24) . 'DAMAGE-ONLY' . ('B' x 45);
print "\e[5;1H", ('A' x 80), $second;
sleep 100;
PERL
$INNER new-session -d -s inner -x 80 -y 12 \
"TRIGGER='$TRIGGER' perl '$EMITTER'" || 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 -x 40 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
$INNER refresh-client -t "$CLIENT" -R 20 || exit 1
wait_outer_has oooooooooo
: >"$TRIGGER"
wait_inner_has DAMAGE-ONLY
wait_outer_has DAMAGE-ONLY
exit 0

View File

@@ -0,0 +1,112 @@
#!/bin/sh
# Moving and interactively resizing floating panes must restore both clients,
# including a smaller client panned horizontally and vertically.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lviewports-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lviewports-outer-$$ -f/dev/null"
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
fail()
{
echo "$*" >&2
exit 1
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t wide:0.0 -l "$sequence" || exit 1
sleep 0.2
}
assert_scene()
{
# Compare what each terminal actually received with a fresh full redraw.
# Capture both first: refreshing one client must not repair the other.
for target in wide small; do
$OUTER capture-pane -p -t "$target:0.0" >"$DIR/$target-before" || exit 1
done
$INNER refresh-client -t "$WIDE" || exit 1
$INNER refresh-client -t "$SMALL" || exit 1
sleep 0.2
for target in wide small; do
$OUTER capture-pane -p -t "$target:0.0" >"$DIR/$target-after" || exit 1
diff -u "$DIR/$target-before" "$DIR/$target-after" ||
fail "$1: $target client differed from a full redraw"
done
}
cat >"$DIR/background.pl" <<'PERL'
$| = 1;
for my $row (1 .. 24) {
printf "\e[%d;1HROW%02d-", $row, $row;
print '0123456789' x 7;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 80 -y 24 "perl '$DIR/background.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g mouse on || exit 1
$INNER set -g default-command 'sleep 100' || exit 1
$INNER set -g pane-border-lines simple || exit 1
$OUTER new-session -d -s wide -x 80 -y 24 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen || exit 1
$OUTER new-session -d -s small -x 40 -y 12 'sleep 100' || exit 1
for target in wide small; do
$OUTER respawn-pane -k -t "$target:0.0" "$INNER attach -t inner" || exit 1
done
i=0
while [ "$($INNER list-clients | wc -l)" -ne 2 ]; do
[ "$i" -lt 50 ] || fail "two clients did not attach"
sleep 0.1
i=$((i + 1))
done
WIDE=$($OUTER display -p -t wide:0.0 '#{pane_tty}') || exit 1
SMALL=$($OUTER display -p -t small:0.0 '#{pane_tty}') || exit 1
$INNER refresh-client -t "$SMALL" -R 20 || exit 1
$INNER refresh-client -t "$SMALL" -D 6 || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 16 -y 6 -X 25 -Y 9) || exit 1
sleep 0.3
assert_scene initial
# Confirm that the two clients really have different viewports.
[ "$(head -1 "$DIR/wide-before" | cut -c1-6)" = ROW01- ] || fail "wrong wide viewport"
[ "$(head -1 "$DIR/small-before" | cut -c1-6)" != ROW01- ] || fail "small client was not panned"
# Meta-drag the body using move-pane -M, including a partially clipped position.
mouse 8 30 12 M
mouse 40 48 15 M
[ "$($INNER display -p -t "$FLOAT" '#{pane_left}')" -eq 44 ] || fail "Meta-drag did not move pane"
assert_scene move-right
mouse 40 28 10 M
assert_scene move-back
mouse 8 28 10 m
# Ctrl-drag creates a new floating pane, then changes its size while held.
mouse 16 52 14 M
mouse 48 75 23 M
NEW=$($INNER display -p '#{pane_id}') || exit 1
[ "$NEW" != "$FLOAT" ] || fail "Ctrl-drag did not create a pane"
[ "$($INNER display -p -t "$NEW" '#{pane_floating_flag}')" -eq 1 ] || fail "new pane is not floating"
assert_scene create
mouse 48 64 19 M
[ "$($INNER display -p -t "$NEW" '#{pane_width}')" -eq 11 ] || fail "Ctrl-drag did not shrink pane"
assert_scene shrink
mouse 16 64 19 m
assert_scene release
exit 0

186
regress/redraw-multiclient.sh Executable file
View File

@@ -0,0 +1,186 @@
#!/bin/sh
# Redraw a moved floating pane on both attached clients viewing the same
# window. Window redraw work must not be consumed by only one client.
#
# Uses ASCII pane borders (rather than the default UTF-8 box-drawing) because
# this test nests a real tmux client inside another tmux's pane to get a
# genuine terminal to capture from; that nested-tmux relay has been observed
# to mis-render a cell that previously held a multi-byte UTF-8 border
# character being overwritten later by plain content, on the outer instance's
# own interpretation, independent of anything the inner tmux sends. That is a
# nested-test-harness artifact, not a real tmux bug - confirmed by replaying
# the exact same drag sequence against a real terminal (xterm), where it
# never reproduces. ASCII borders avoid the artifact entirely.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lredraw-multi-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_outer_has()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer pane $target did not show $marker"
}
wait_float_left()
{
comparison=$1
limit=$2
i=0
while [ "$i" -lt 50 ]; do
left=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
if [ "$comparison" = gt ] && [ "$left" -gt "$limit" ]; then
return 0
fi
if [ "$comparison" = lt ] && [ "$left" -lt "$limit" ]; then
return 0
fi
sleep 0.1
i=$((i + 1))
done
fail "floating pane did not move"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$2" "$3" "$4" "$5")
$OUTER send-keys -t "$1" -l "$sequence" || exit 1
sleep 0.1
}
drag_float()
{
target=$1
startcol=$2
startrow=$3
endcol=$4
mouse "$target" 0 "$startcol" "$startrow" M
mouse "$target" 32 "$endcol" "$startrow" M
mouse "$target" 0 "$endcol" "$startrow" m
}
assert_scene()
{
target=$1
base=$2
firstcol=$3
lastcol=$4
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
# With ASCII (simple) borders every corner and junction is the same
# '+', so one rectangular floating pane always draws exactly 4 of
# them; more means a stale frame was left behind somewhere.
corners=$(grep -o '+' "$CAPTURE" | wc -l)
[ "$corners" -eq 4 ] ||
fail "outer pane $target had $corners floating frames"
sed -n '6,11p' "$base" | cut -c"$firstcol-$lastcol" >"$DIR/want"
sed -n '6,11p' "$CAPTURE" | cut -c"$firstcol-$lastcol" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" ||
fail "outer pane $target did not restore the old floating area"
}
C="sh -c 'i=0; while [ \$i -lt 20 ]; do printf \"\\033[%d;1HBG-ROW-%02d-abcdefghijklmnopqrstuvwxyz0123456789\" \$((i + 1)) \$i; i=\$((i + 1)); done; exec sleep 100'"
$INNER new-session -d -s inner -x 60 -y 20 "$C" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g default-command 'sleep 100' || exit 1
$INNER set-option -g pane-border-lines simple || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER split-window -h -t outer:0.0 'sleep 100' || exit 1
PANES=$($OUTER list-panes -t outer:0 -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
RIGHT=$(echo "$PANES" | sort -k2 -n | tail -1 | cut -d' ' -f1)
[ -n "$LEFT" ] && [ -n "$RIGHT" ] || fail "could not find outer panes"
$OUTER respawn-pane -k -t "$LEFT" \
"$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER respawn-pane -k -t "$RIGHT" \
"$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_clients
wait_outer_has "$LEFT" BG-ROW-19
wait_outer_has "$RIGHT" BG-ROW-19
$OUTER capture-pane -p -t "$LEFT" >"$DIR/base-left" || exit 1
$OUTER capture-pane -p -t "$RIGHT" >"$DIR/base-right" || exit 1
FLOAT=$($INNER new-pane -dPF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "could not create floating pane"
wait_outer_has "$LEFT" '+'
wait_outer_has "$RIGHT" '+'
FTOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
GRABCOL=$((FLEFT + FWIDTH / 2 + 1))
# Move right through one client and require both clients to restore the old
# left-hand footprint.
drag_float "$LEFT" "$GRABCOL" "$FTOP" $((GRABCOL + 30))
wait_float_left gt 30
assert_scene "$LEFT" "$DIR/base-left" 1 20
assert_scene "$RIGHT" "$DIR/base-right" 1 20
# Move back through the other client and check the old right-hand footprint.
FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
GRABCOL=$((FLEFT + FWIDTH / 2 + 1))
drag_float "$RIGHT" "$GRABCOL" "$FTOP" $((GRABCOL - 30))
wait_float_left lt 10
assert_scene "$LEFT" "$DIR/base-left" 35 60
assert_scene "$RIGHT" "$DIR/base-right" 35 60
exit 0

208
regress/redraw-screen-write.sh Executable file
View File

@@ -0,0 +1,208 @@
#!/bin/sh
# Check that full and region screen-write fallbacks update an attached client,
# not only tmux's internal pane grid.
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
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
INNER=
OUTER=
N=0
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_outer_lacks()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" || return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client still showed $marker"
}
wait_inner_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 2>/dev/null |
grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane did not contain $marker"
}
wait_inner_lacks()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" || return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane still contained $marker"
}
setup()
{
mode=$1
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
N=$((N + 1))
INNER="$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null"
OUTER="$TEST_TMUX -Lredraw-write-outer-$$-$N -f/dev/null"
$INNER new-session -d -s inner -x 40 -y 12 \
"MODE=$mode READY='$DIR/ready-$N' TRIGGER='$DIR/trigger-$N' perl '$EMITTER'" ||
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 -x 40 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null attach-session -t inner" ||
exit 1
}
trigger()
{
: >"$DIR/trigger-$N-${1:-1}"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $mode = $ENV{MODE};
my $ready = $ENV{READY};
my $trigger = $ENV{TRIGGER};
sub fill_screen {
my ($prefix) = @_;
print "\e[2J\e[H";
for my $row (0 .. 11) {
printf "\e[%d;1H%s-ROW-%02d", $row + 1, $prefix, $row;
}
}
if ($mode eq 'ris') {
fill_screen('RIS');
} elsif ($mode eq 'alternate') {
fill_screen('BASE');
} elsif ($mode eq 'scroll') {
fill_screen('SCROLL');
} else {
die "unknown mode $mode\n";
}
open my $fh, '>', $ready or die "$ready: $!\n";
close $fh;
while (!-e "$trigger-1") {
select undef, undef, undef, 0.01;
}
if ($mode eq 'ris') {
print "\ec";
} elsif ($mode eq 'alternate') {
print "\e[?1049h";
fill_screen('ALT');
while (!-e "$trigger-2") {
select undef, undef, undef, 0.01;
}
print "\e[?1049l";
} else {
print "\e[12;1H\r\nSCROLL-NEW";
}
sleep 100;
PERL
# RIS clears the complete screen. The source pane and attached client must both
# lose every old row.
setup ris
wait_outer_has RIS-ROW-11
trigger
wait_inner_lacks RIS-ROW
wait_outer_lacks RIS-ROW
# Leaving the alternate screen restores every row of the base screen.
setup alternate
wait_outer_has BASE-ROW-11
trigger
wait_inner_has ALT-ROW-11
wait_outer_has ALT-ROW-11
wait_outer_lacks BASE-ROW
trigger 2
wait_inner_has BASE-ROW-11
wait_outer_has BASE-ROW-11
wait_outer_lacks ALT-ROW
# Scrolling a pane which is narrower than the terminal redraws its complete
# region. Check the physical client row by row after the source grid shifts.
setup scroll
$INNER split-window -h -t inner:0 'sleep 100' || exit 1
wait_outer_has SCROLL-ROW-11
trigger
wait_inner_has SCROLL-NEW
wait_outer_has SCROLL-NEW
row=1
while [ "$row" -le 11 ]; do
expected=$(printf 'SCROLL-ROW-%02d' "$row")
actual=$(sed -n "${row}p" "$CAPTURE")
case "$actual" in
"$expected"*) ;;
*) fail "outer row $row was not redrawn as $expected" ;;
esac
row=$((row + 1))
done
actual=$(sed -n '12p' "$CAPTURE")
case "$actual" in
SCROLL-NEW*) ;;
*) fail "outer bottom row was not redrawn as SCROLL-NEW" ;;
esac
exit 0

View File

@@ -15,7 +15,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)

55
regress/run-shell-cwd.sh Normal file
View File

@@ -0,0 +1,55 @@
#!/bin/sh
# run-shell -c should expand formats using the target pane.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMP=$(mktemp -d)
trap '$TMUX kill-server 2>/dev/null; rm -rf "$TMP"' 0 1 15
TMP=$(cd "$TMP" && pwd -P)
mkdir "$TMP/first" "$TMP/second dir" || exit 1
check_directory()
{
actual=$(cat "$TMP/out")
if [ "$actual" != "$1" ]; then
echo "Expected directory '$1', got '$actual'"
exit 1
fi
}
# The start path is available immediately, before the child is scheduled.
# Use it to test the format context in the same command queue as creation.
$TMUX new-session -d -s test -c "$TMP/first" 'sleep 60' \; \
run-shell -c '#{pane_start_path}' "pwd >'$TMP/out'" || exit 1
check_directory "$TMP/first"
# An explicit target must supply the format context, including with a delay.
pane=$($TMUX new-window -d -P -F '#{pane_id}' -t test \
-c "$TMP/second dir" 'sleep 60') || exit 1
# The current path depends on the operating system finding the child process.
i=0
while [ "$($TMUX display-message -p -t "$pane" '#{pane_current_path}')" != \
"$TMP/second dir" ]; do
if [ "$i" -ge 100 ]; then
echo "Timed out waiting for pane current directory"
exit 1
fi
sleep 0.05
i=$((i + 1))
done
$TMUX run-shell -t "$pane" -d 0.1 -c '#{pane_current_path}' \
"pwd >'$TMP/out'" || exit 1
check_directory "$TMP/second dir"
# Literal directories and the default client directory still work.
$TMUX run-shell -c "$TMP/second dir" "pwd >'$TMP/out'" || exit 1
check_directory "$TMP/second dir"
(cd "$TMP/first" && $TMUX run-shell "pwd >'$TMP/out'") || exit 1
check_directory "$TMP/first"
exit 0

View File

@@ -106,9 +106,28 @@ compare menu-over-split
# Menu with no border lines.
setup 40 14
menu -b none -x6 -y8
$TMUX2 display-menu -T "This title must not reserve width" -C 1 \
-b none -x6 -y8 \
"Alpha item" a "" \
"Beta item" b "" \
"" "" "" \
"Gamma item" g "" || exit 1
sleep 1
compare menu-noborder
# Menu border style follows the explicitly targeted window, not the current
# window when a different pane is selected.
setup 40 14
$TMUX2 set menu-border-lines none || exit 1
$TMUX2 neww || exit 1
$TMUX2 display-menu -t%0 -T "Menu" -C 1 \
"Alpha item" a "" \
"Beta item" b "" \
"" "" "" \
"Gamma item" g "" || exit 1
$TMUX2 selectw -t%0 || exit 1
compare menu-target-window-noborder
# Menu with double border lines.
setup 40 14
menu -b double -x6 -y8

View File

@@ -1,101 +0,0 @@
#!/bin/sh
# Exercise drawing of popups (display-popup) over the window scene. A popup is an
# overlay drawn on top of the redraw scene (the overlay_draw path in
# screen-redraw.c), so this guards against regressions in how popups appear.
#
# A popup is modal and stays open until its command exits, so each scene fully
# re-creates the servers and re-attaches; the popup is opened in the background
# (display-popup blocks the client that runs it) and the outer pane is captured
# while it is open.
#
# Run with GENERATE=1 to (re)create the golden files.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX=
TMUX2=
SETUP=0
RESULTS=screen-redraw-results
TMP=$(mktemp)
cleanup() {
rm -f "$TMP"
[ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null
[ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null
}
trap cleanup 0 1 15
fail() {
echo "$*" >&2
exit 1
}
compare() {
sleep 1
$TMUX capturep -p >$TMP || exit 1
if [ -n "$GENERATE" ]; then
cp $TMP "$RESULTS/$1.result" || exit 1
echo "generated $1"
else
cmp -s $TMP "$RESULTS/$1.result" || \
fail "scene $1 differs from $RESULTS/$1.result"
fi
}
C="sh -c 'i=0; while [ \$i -lt 13 ]; do printf \"POP%02d abcdefghij\n\" \$i; i=\$((i + 1)); done; exec sleep 100'"
# setup: fresh inner window attached inside a fresh outer pane, 40x14.
setup() {
[ -n "$TMUX" ] && $TMUX kill-server 2>/dev/null
[ -n "$TMUX2" ] && $TMUX2 kill-server 2>/dev/null
SETUP=$((SETUP + 1))
TMUX="$TEST_TMUX -LtestA$$-$SETUP -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$-$SETUP -f/dev/null"
$TMUX2 new -d -x40 -y14 "$C" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -g window-size manual || exit 1
$TMUX2 resizew -x40 -y14 || exit 1
$TMUX new -d -x40 -y14 || exit 1
$TMUX set -g status off || exit 1
$TMUX set -g window-size manual || exit 1
$TMUX set -g default-terminal "tmux-256color" || exit 1
$TMUX send -l "$TMUX2 attach" || exit 1
$TMUX send Enter || exit 1
sleep 1
}
# popup <args>: open a popup running a fixed command, in the background (it stays
# open because the command sleeps; the servers are killed at the next setup).
popup() {
$TMUX2 display-popup "$@" -E "sh -c 'printf POPUP; exec sleep 100'" &
sleep 1
}
# Basic popup over a single pane.
setup
popup -w20 -h6 -x6 -y3
compare popup-basic
# Popup over a split: drawn on top of the pane border.
setup
$TMUX2 splitw -h "$C" || exit 1
popup -w24 -h8 -x8 -y3
compare popup-over-split
# Popup with no border lines (-B).
setup
popup -B -w20 -h6 -x6 -y3
compare popup-noborder
# Popup with double border lines.
setup
popup -b double -w20 -h6 -x6 -y3
compare popup-double
exit 0

View File

@@ -1,11 +1,11 @@
MENU00 abcdefghij
MENU01 abcdefghij
MENU02 Menu 
MENU03  Alpha item (a)  
MENU04  Beta item (b)  
MENU05 
MENU06  Gamma item (g)  
MENU07 
MENU02 abcdefghij
MENU03 abcdefghij
MENU04 Alpha item (a) 
MENU05 Beta item (b) 
MENU06 
MENU07 Gamma item (g) 
MENU08 abcdefghij
MENU09 abcdefghij
MENU10 abcdefghij

View File

@@ -0,0 +1,13 @@
MENU00 abcdefghij
MENU01 abcdefghij
MENU02 abcdefghij
MENU03 abcdefghij
MENU04 abcd Alpha item (a) 
MENU05 abcd Beta item (b) 
MENU06 abcd 
MENU07 abcd Gamma item (g) 
MENU08 abcdefghij
MENU09 abcdefghij
MENU10 abcdefghij
MENU11 abcdefghij
MENU12 abcdefghij

View File

@@ -1,14 +0,0 @@
POP00 ┌──────────────────┐
POP01 │POPUP │
POP02 │ │
POP03 │ │
POP04 │ │
POP05 └──────────────────┘
POP06 abcdefghij
POP07 abcdefghij
POP08 abcdefghij
POP09 abcdefghij
POP10 abcdefghij
POP11 abcdefghij
POP12 abcdefghij

View File

@@ -1,14 +0,0 @@
POP00 ╔══════════════════╗
POP01 ║POPUP ║
POP02 ║ ║
POP03 ║ ║
POP04 ║ ║
POP05 ╚══════════════════╝
POP06 abcdefghij
POP07 abcdefghij
POP08 abcdefghij
POP09 abcdefghij
POP10 abcdefghij
POP11 abcdefghij
POP12 abcdefghij

View File

@@ -1,14 +0,0 @@
POP00 POPUP
POP01
POP02
POP03
POP04
POP05
POP06 abcdefghij
POP07 abcdefghij
POP08 abcdefghij
POP09 abcdefghij
POP10 abcdefghij
POP11 abcdefghij
POP12 abcdefghij

View File

@@ -1,14 +0,0 @@
POP00 ab┌──────────────────────┐fghij
POP01 ab│POPUP │fghij
POP02 ab│ │fghij
POP03 ab│ │fghij
POP04 ab│ │fghij
POP05 ab│ │fghij
POP06 ab│ │fghij
POP07 ab└──────────────────────┘fghij
POP08 abcdefghij │POP08 abcdefghij
POP09 abcdefghij │POP09 abcdefghij
POP10 abcdefghij │POP10 abcdefghij
POP11 abcdefghij │POP11 abcdefghij
POP12 abcdefghij │POP12 abcdefghij

View File

@@ -95,7 +95,8 @@ layout_tee_down() { # top/bottom, bottom split: a top tee
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new -d -x40 -y14 "sh -c 'exec sleep 100'" || exit 1
# Keep the anchor window alive until cleanup, even on slow test runs.
$TMUX2 new -d -x40 -y14 'exec cat' || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -g window-size manual || exit 1
$TMUX2 set -g pane-border-format " #{pane_index} " || exit 1

81
regress/switch-client-redraw.sh Executable file
View File

@@ -0,0 +1,81 @@
#!/bin/sh
# Switching windows in the same session must redraw the attached client. The
# session object is shared, so its current winlink cannot be compared after it
# has already been changed.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lswitch-redraw-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
$INNER new-session -d -s inner -x 40 -y 8 \
"printf '\033[2J\033[HA-WINDOW'; exec sleep 100" || exit 1
$INNER new-window -d -t inner:1 \
"printf '\033[2J\033[HB-WINDOW'; exec sleep 100" || 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 -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null attach-session -t inner:0" ||
exit 1
wait_for_client
wait_outer_has A-WINDOW
$INNER switch-client -c "$CLIENT" -t inner:1.0 || exit 1
[ "$($INNER display-message -p -t inner '#{window_index}')" -eq 1 ] ||
fail "server did not select window 1"
wait_outer_has B-WINDOW
exit 0

116
regress/sync-output-wrapped.sh Executable file
View File

@@ -0,0 +1,116 @@
#!/bin/sh
# When only some lines are redrawn after a synchronized update, a line
# following a wrapped line must be drawn at its own position, not where the
# cursor happens to be.
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 -Li$$ -f/dev/null"
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
CONTROL=$DIR/control
EMITTER=$DIR/emitter.pl
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_file()
{
i=0
while [ "$i" -lt 50 ] && [ ! -e "$1" ]; do
sleep 0.1
i=$((i + 1))
done
[ -e "$1" ] || fail "$2"
}
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
$INNER list-clients -F '#{client_termfeatures}' 2>/dev/null |
grep -q 'sync' && return 0
sleep 0.1
i=$((i + 1))
done
fail "sync-capable client did not attach"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
my $control = $ENV{CONTROL};
(my $dir = $control) =~ s{/[^/]+$}{};
# Fill the screen with 80 column lines and no newlines so every line wraps.
my $out = "\e[H\e[2J";
for my $row (1 .. 24) {
$out .= substr(sprintf("ROW%02d_", $row) . ('x' x 80), 0, 80);
}
syswrite STDOUT, $out . "\e[1;1H";
open my $painted, '>', "$dir/painted" or die "$dir/painted: $!\n";
close $painted;
while (!-e $control) {
select undef, undef, undef, 0.01;
}
# Change rows 2 and 6 in one synchronized update.
my $frame = "\e[?2026h" .
"\e[2;1H" . substr("NEW02_" . ('#' x 80), 0, 80) .
"\e[6;1H" . substr("NEW06_" . ('%' x 80), 0, 80) .
"\e[24;1H" .
"\e[?2026l";
my $written = syswrite STDOUT, $frame;
die "short synchronized frame write\n"
unless defined $written && $written == length $frame;
open my $done, '>', "$dir/done" or die "$dir/done: $!\n";
close $done;
select undef, undef, undef, 3;
PERL
$INNER new-session -d -s inner -x 80 -y 24 \
"CONTROL='$CONTROL' perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -as terminal-features '*:sync' || exit 1
$OUTER new-session -d -s outer -x 80 -y 24 \
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
wait_for_client
wait_for_file "$DIR/painted" "application did not paint"
sleep 1
: >"$CONTROL"
wait_for_file "$DIR/done" "application did not finish"
sleep 1
$OUTER capture-pane -p -t outer:0.0 >"$DIR/screen" || exit 1
sed -n 2p "$DIR/screen" | grep -q '^NEW02_#' || fail "row 2 not redrawn"
sed -n 3p "$DIR/screen" | grep -q '^ROW03_x' || fail "row 3 overwritten"
sed -n 6p "$DIR/screen" | grep -q '^NEW06_%' || fail "row 6 not redrawn"
exit 0

View File

@@ -9,14 +9,37 @@ $TMUX kill-server 2>/dev/null
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP" 0 1 15
TMP=$(mktemp) || exit 1
prompt_pid=
cleanup () {
if [ -n "$prompt_pid" ]; then
kill "$prompt_pid" 2>/dev/null
wait "$prompt_pid" 2>/dev/null
fi
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
rm -f "$TMP"
}
trap cleanup 0
trap 'exit 1' 1 2 15
fail () {
echo "[FAIL] $*" >&2
exit 1
}
$TMUX2 -f/dev/null new -d || exit 1
$TMUX -f/dev/null new -d "$TMUX2 attach" || exit 1
sleep 1
i=0
while [ -z "$($TMUX2 list-clients -F '#{client_name}')" ]; do
[ "$i" -lt 100 ] || fail "inner client did not attach"
sleep 0.05
i=$((i + 1))
done
exit_status=0
assertion=0
format_string () {
case $1 in
@@ -34,12 +57,33 @@ assert_key () {
expected_name=$2
format_string=$(format_string "$expected_name")
$TMUX2 command-prompt -k 'display-message -pl '"$format_string" > "$TMP" &
sleep 0.05
# Use a different prompt each time so a stale redraw cannot look ready.
assertion=$((assertion + 1))
prompt="tty-keys-$assertion:"
$TMUX2 command-prompt -k -p "$prompt" \
'display-message -pl '"$format_string" > "$TMP" &
prompt_pid=$!
i=0
while ! $TMUX capture-pane -p | grep -Fq "$prompt"; do
kill -0 "$prompt_pid" 2>/dev/null || \
fail "$keys -> $expected_name: prompt exited before becoming ready"
[ "$i" -lt 100 ] || \
fail "$keys -> $expected_name: timed out waiting for prompt"
sleep 0.05
i=$((i + 1))
done
$TMUX send-keys $keys
$TMUX send-keys $keys || fail "could not send $keys"
wait
i=0
while kill -0 "$prompt_pid" 2>/dev/null; do
[ "$i" -lt 100 ] || \
fail "$keys -> $expected_name: timed out waiting for key"
sleep 0.05
i=$((i + 1))
done
wait "$prompt_pid" || fail "$keys -> $expected_name: prompt failed"
prompt_pid=
keys=$(printf '%s' "$keys" | sed -e 's/Escape/\\\\033/g' | tr -d '[:space:]')
actual_name=$(tr -d '[:space:]' < "$TMP")
@@ -293,6 +337,16 @@ assert_key 'Escape [24@' 'C-S-F12'
assert_key 'Escape [I' 'FocusIn'
assert_key 'Escape [O' 'FocusOut'
# Focus events do not leave the prefix table.
$TMUX2 bind C-c set -g @focus-prefix yes
$TMUX2 set -g focus-events on
$TMUX send-keys C-b Escape '[O' Escape '[I' C-c
sleep 0.05
if [ "$($TMUX2 show -gv @focus-prefix)" != yes ]; then
echo "[FAIL] FocusIn and FocusOut left the prefix table"
exit_status=1
fi
# Paste keys
assert_key 'Escape [200~' 'PasteStart'
assert_key 'Escape [201~' 'PasteEnd'
@@ -355,7 +409,4 @@ assert_key 'Escape [32;2u' 'S-Space'
assert_key 'Escape [9;5u' 'C-Tab'
assert_key 'Escape [1;5Z' 'C-S-Tab'
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
exit $exit_status

View File

@@ -0,0 +1,93 @@
#!/bin/sh
# With left/right margins, a pane narrowed by a scrollbar can scroll without
# retransmitting its existing rows. Both paths must produce the same screen.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lmarginsscrollbar-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lmarginsscrollbar-outer-$$ -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
cat >"$DIR/emitter.pl" <<'PERL'
use strict;
use warnings;
$| = 1;
print join("\r\n", map { "KEEP0$_" } 1 .. 6);
while (!-e $ENV{TRIGGER}) {
select undef, undef, undef, 0.01;
}
print "\r\nNEWROW";
sleep 100;
PERL
for margins in on off; do
$INNER new-session -d -s inner -x 40 -y 6 \
"TRIGGER='$DIR/trigger-$margins' perl '$DIR/emitter.pl'" || exit 1
$INNER set -g status off || exit 1
$INNER set -g status-interval 0 || exit 1
$INNER set -g automatic-rename off || exit 1
$INNER set -g window-size manual || exit 1
$INNER set -g pane-scrollbars on || exit 1
if [ "$margins" = on ]; then
$INNER set -as terminal-features ',screen-256color:margins' || exit 1
fi
$OUTER new-session -d -s outer -x 40 -y 6 'sleep 100' || exit 1
$OUTER set -g status off || exit 1
$OUTER set -g window-size manual || exit 1
$OUTER set -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 "$INNER attach -t inner" || exit 1
sleep 0.5
$OUTER pipe-pane -O -t outer:0.0 "cat >'$DIR/output-$margins'" || exit 1
$INNER refresh-client || exit 1
sleep 0.5
grep -aq KEEP02 "$DIR/output-$margins" || fail "initial rows not captured"
offset=$(wc -c <"$DIR/output-$margins")
: >"$DIR/trigger-$margins"
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$DIR/screen-$margins" || exit 1
grep -q NEWROW "$DIR/screen-$margins" && break
sleep 0.1
i=$((i + 1))
done
[ "$i" -lt 50 ] || fail "$margins: scroll did not reach the terminal"
sleep 0.2
tail -c +"$((offset + 1))" "$DIR/output-$margins" >"$DIR/scroll-$margins"
grep -aq NEWROW "$DIR/scroll-$margins" || fail "scroll output not captured"
if [ "$margins" = on ]; then
if grep -aq KEEP02 "$DIR/scroll-$margins"; then
fail "scroll with margins retransmitted an existing row"
fi
else
grep -aq KEEP02 "$DIR/scroll-$margins" ||
fail "scroll without margins did not exercise the redraw fallback"
fi
printf 'KEEP02\nKEEP03\nKEEP04\nKEEP05\nKEEP06\nNEWROW\n' >"$DIR/expected"
cmp -s "$DIR/expected" "$DIR/screen-$margins" ||
fail "$margins: terminal did not contain the expected scrolled rows"
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
done
exit 0

View File

@@ -0,0 +1,164 @@
#!/bin/sh
# A disconnected waiter must release both its wait and its command queue.
PATH=/bin:/usr/bin
TERM=screen
export TERM
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
OUT=$(mktemp -d)
TMUX="$TEST_TMUX -S$OUT/socket -f/dev/null"
pids=
cleanup()
{
for pid in $pids; do
kill "$pid" 2>/dev/null || true
done
$TMUX kill-server 2>/dev/null || true
rm -rf "$OUT"
}
trap cleanup EXIT
fail()
{
echo "$*" >&2
exit 1
}
wait_list()
{
expected=$1
shift
i=0
while [ $i -lt 100 ]; do
value=$($TMUX wait-for -l "$@") || fail "list waiters failed"
[ "$value" = "$expected" ] && return
i=$((i + 1))
sleep 0.05
done
fail "expected waiters '$expected', got '$value'"
}
wait_free()
{
client_pid=$1
i=0
while [ $i -lt 100 ]; do
awk -v pid="$client_pid" '
$4 == "IDENTIFY_CLIENTPID" && $5 == pid { address = $3 }
address != "" && $2 == "free" && $3 == "client" &&
$4 == address && $5 == "(0" { freed = 1 }
END { exit !freed }
' "$OUT"/tmux-server-*.log && return
i=$((i + 1))
sleep 0.05
done
fail "client $client_pid was not freed"
}
cd "$OUT" || exit 1
$TMUX -vv new-session -d -s test || fail "new-session failed"
$TMUX set -g @continued 0 || fail "set option failed"
for signal in TERM KILL; do
for kind in channel lock event; do
name="$kind-$signal"
case $kind in
channel) args="$name"; list_args="$name" ;;
lock)
$TMUX wait-for -L "$name" || fail "lock failed"
args="-L $name"; list_args="$name"
;;
event) args="-E @$name"; list_args="-E @$name" ;;
esac
$TMUX wait-for $args \; set -g @continued 1 &
dead=$!
pids="$dead"
wait_list "client-$dead" $list_args
$TMUX wait-for $args &
live=$!
pids="$dead $live"
wait_list "$(printf 'client-%s\nclient-%s' "$dead" "$live")" \
$list_args
kill -"$signal" "$dead" || fail "kill failed"
wait "$dead" 2>/dev/null || true
pids="$live"
wait_list "client-$live" $list_args
wait_free "$dead"
[ "$($TMUX show -gqv @continued)" = 0 ] ||
fail "disconnected client's next command ran"
case $kind in
channel) $TMUX wait-for -S "$name" || fail "signal failed" ;;
lock) $TMUX wait-for -U "$name" || fail "unlock failed" ;;
event)
$TMUX wait-for -E -w "client-$live" "@$name" ||
fail "wake event waiter failed"
;;
esac
wait_list "" $list_args
wait_free "$live"
wait "$live" || fail "surviving waiter failed"
pids=
if [ "$kind" = lock ]; then
$TMUX wait-for -U "$name" || fail "final unlock failed"
fi
done
done
# Removing the last waiter must remove its otherwise unused channel, and must
# not create a pending signal. Unrelated pending signals must be preserved.
$TMUX wait-for -S saved || fail "save signal failed"
$TMUX wait-for last &
dead=$!
pids="$dead"
wait_list "client-$dead" last
kill -KILL "$dead" || fail "kill last waiter failed"
wait "$dead" 2>/dev/null || true
pids=
wait_list "" last
wait_free "$dead"
grep -F 'remove empty wait channel last' "$OUT"/tmux-server-*.log \
>/dev/null || fail "empty channel was not removed"
$TMUX wait-for saved \; wait-for last &
live=$!
pids="$live"
wait_list "client-$live" last
$TMUX wait-for -S last || fail "signal last failed"
wait_free "$live"
wait "$live" || fail "pending signal was not preserved"
pids=
# Source-file leaves a cleanup callback behind the blocked command.
printf 'wait-for sourced\nset -g @continued 1\n' >"$OUT/source.conf"
$TMUX source-file "$OUT/source.conf" &
dead=$!
pids="$dead"
wait_list "client-$dead" sourced
kill -KILL "$dead" || fail "kill source-file waiter failed"
wait "$dead" 2>/dev/null || true
pids=
wait_list "" sourced
wait_free "$dead"
[ "$($TMUX show -gqv @continued)" = 0 ] ||
fail "disconnected source-file client's next command ran"
# Firing an event after cancellation must not call the removed event sink.
$TMUX wait-for -E window-renamed &
dead=$!
pids="$dead"
wait_list "client-$dead" -E window-renamed
kill -KILL "$dead" || fail "kill event waiter failed"
wait "$dead" 2>/dev/null || true
pids=
wait_list "" -E window-renamed
wait_free "$dead"
$TMUX rename-window -t test:0 renamed || fail "rename-window failed"
$TMUX display-message -p '#{pid}' >/dev/null || fail "server exited"
exit 0

View File

@@ -0,0 +1,123 @@
#!/bin/sh
# Dragging an inactive pane's scrollbar slider changes which pane is
# active (window_copy_scroll(), window-copy.c). Every other place in the
# codebase that changes the active pane pairs it with
# window_redraw_active_switch() (or falls back to a full window redraw) so
# the old/new active panes' window-active-style/window-style colours get
# repainted - window_copy_scroll() was the one caller that did neither, so
# a scrollbar-slider drag changed focus but left both panes' body colours
# stale. This used to be masked by window_set_active_pane() itself doing
# an unconditional full redraw on every active-pane change, until that was
# narrowed to borders/status-only for the common case.
#
# This creates two tiled panes with clearly distinguishable
# window-active-style/window-style backgrounds, puts the *inactive* one in
# copy mode, drags its scrollbar slider, and checks - via an attached
# client's own received bytes, since window-style is applied during
# redraw composition rather than stored in the grid, so capture-pane
# alone would not reflect it - that the newly active pane's marker text
# immediately shows the active-style colour, not the stale one.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lsbfocus-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsbfocus-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
ESC=$(printf '\033')
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat -A "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
get_mark_color()
{
$OUTER capture-pane -e -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
LC_ALL=C grep -a "MARK1" "$CAPTURE" |
LC_ALL=C grep -aoE "${ESC}\[4[12]m" | tail -1
}
wait_mark_color()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
[ "$(get_mark_color)" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "MARK1's pane never showed the expected background (wanted '$want', got '$(get_mark_color)')"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars on || exit 1
$INNER setw pane-scrollbars-position right || exit 1
$INNER setw pane-scrollbars-style 'width=1,pad=0' || exit 1
$INNER set-option -g window-active-style 'bg=red' || exit 1
$INNER set-option -g window-style 'bg=green' || exit 1
$INNER split-window -h -t inner -d 'printf MARK1; sleep 100' || exit 1
PANE1=$($INNER list-panes -t inner -F '#{pane_id}' | sed -n 2p)
[ "$($INNER display-message -p -t "$PANE1" '#{pane_active}')" = 0 ] ||
fail "sanity: MARK1's pane is already active before the drag"
# Put the inactive pane in copy mode without switching focus (copy-mode -t
# targets a pane without making it active).
$INNER copy-mode -t "$PANE1" || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lsbfocus-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_mark_color "${ESC}[42m"
XOFF=$($INNER display-message -p -t "$PANE1" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$PANE1" '#{pane_top}')
SX=$($INNER display-message -p -t "$PANE1" '#{pane_width}')
SBCOL=$((XOFF + SX + 1))
SBROW=$((YOFF + 1))
# With no scrollback beyond the pane's own content, the slider fills the
# whole scrollbar track, so any point on it (here, its very first row) is
# on the slider.
mouse 0 "$SBCOL" "$SBROW" M
mouse 32 "$SBCOL" "$((SBROW + 1))" M
mouse 0 "$SBCOL" "$((SBROW + 1))" m
[ "$($INNER display-message -p -t "$PANE1" '#{pane_active}')" = 1 ] ||
fail "sanity: dragging the scrollbar slider did not change focus"
wait_mark_color "${ESC}[41m"
exit 0

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-redraw.c,v 1.159 2026/09/09 08:31:42 nicm Exp $ */
/* $OpenBSD: screen-redraw.c,v 1.161 2026/09/22 16:56:07 nicm Exp $ */
/*
* Copyright (c) 2026 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -33,7 +33,7 @@
* this is done at various points, such as when a pane is moved or resized. The
* scene only includes the part of the client used for the window: panes, pane
* status lines, borders, scrollbars, and any area outside the window. The
* client status line and overlay are not included.
* client status line is not included.
*
* A scene is made from spans. A span is a horizontal run of cells on one
* visible line that can be drawn in the same way. Each span has a type, for
@@ -93,7 +93,6 @@ enum redraw_span_type {
#define REDRAW_PANE_SCROLLBAR 0x20
#define REDRAW_STATUS 0x40
#define REDRAW_MENU 0x80
#define REDRAW_OVERLAY 0x100
/* Draw everything. */
#define REDRAW_ALL 0x7fffffff
@@ -205,14 +204,36 @@ struct redraw_scene {
u_int oy;
};
/* A damaged rectangle in a window. */
struct redraw_damage {
u_int x;
u_int y;
u_int sx;
u_int sy;
TAILQ_ENTRY(redraw_damage) entry;
};
/*
* If there are more damage rectangles than this, they are collapsed into
* one.
*/
#define REDRAW_DAMAGE_MAX 16
/* Cell for building the scene. */
struct redraw_build_cell {
struct redraw_span_data data;
};
static struct redraw_build_cell *redraw_cells;
static size_t redraw_ncells;
/*
* We can reuse the same pane status lines during one damage redraw, but when
* we enter a new one, the client or format variables may have changed, so we
* need to make them again. The generation is increased so this happens.
*/
static u_int redraw_status_generation;
/* Context for building the scene. */
struct redraw_build_ctx {
struct client *c;
@@ -264,8 +285,6 @@ redraw_flags_to_string(int flags)
strlcat(s, "scrollbar ", sizeof s);
if (flags & REDRAW_MENU)
strlcat(s, "menu ", sizeof s);
if (flags & REDRAW_OVERLAY)
strlcat(s, "overlay ", sizeof s);
if (REDRAW_IS_ALL(flags))
strlcat(s, "all ", sizeof s);
if (*s != '\0')
@@ -1057,6 +1076,13 @@ redraw_free_scene(struct redraw_scene *scene)
free(scene);
}
/* Does a client's cached scene show this window? */
int
redraw_client_has_window(struct client *c, struct window *w)
{
return (c->redraw_scene != NULL && c->redraw_scene->w == w);
}
/* Mark a window's cached redraw scenes as out of date. */
void
redraw_invalidate_scene(struct window *w)
@@ -1064,6 +1090,104 @@ redraw_invalidate_scene(struct window *w)
w->redraw_scene_generation++;
}
/* Free all pending damage for a window. */
void
redraw_free_damage(struct window *w)
{
struct redraw_damage *rd, *rd1;
TAILQ_FOREACH_SAFE(rd, &w->damage, entry, rd1) {
TAILQ_REMOVE(&w->damage, rd, entry);
free(rd);
}
w->damage_count = 0;
}
/* Collapse all pending damage for a window into one rectangle. */
static void
redraw_collapse_damage(struct window *w)
{
struct redraw_damage *rd, *rd1, *first;
u_int x0, y0, x1, y1;
first = TAILQ_FIRST(&w->damage);
if (first == NULL)
return;
x0 = first->x;
y0 = first->y;
x1 = first->x + first->sx;
y1 = first->y + first->sy;
TAILQ_FOREACH_SAFE(rd, &w->damage, entry, rd1) {
if (rd->x < x0)
x0 = rd->x;
if (rd->y < y0)
y0 = rd->y;
if (rd->x + rd->sx > x1)
x1 = rd->x + rd->sx;
if (rd->y + rd->sy > y1)
y1 = rd->y + rd->sy;
if (rd != first) {
TAILQ_REMOVE(&w->damage, rd, entry);
free(rd);
}
}
first->x = x0;
first->y = y0;
first->sx = x1 - x0;
first->sy = y1 - y0;
w->damage_count = 1;
}
/* Record window damage, merging nearby rectangles and limiting the count. */
void
redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy)
{
struct redraw_damage *rd;
u_int x0, y0, x1, y1, area, union_area;
if (x >= w->sx || y >= w->sy)
return;
if (x + sx > w->sx)
sx = w->sx - x;
if (y + sy > w->sy)
sy = w->sy - y;
if (sx == 0 || sy == 0)
return;
TAILQ_FOREACH(rd, &w->damage, entry) {
if (x > rd->x + rd->sx || rd->x > x + sx ||
y > rd->y + rd->sy || rd->y > y + sy)
continue;
x0 = (x < rd->x) ? x : rd->x;
y0 = (y < rd->y) ? y : rd->y;
x1 = (x + sx > rd->x + rd->sx) ? x + sx : rd->x + rd->sx;
y1 = (y + sy > rd->y + rd->sy) ? y + sy : rd->y + rd->sy;
area = sx * sy + rd->sx * rd->sy;
union_area = (x1 - x0) * (y1 - y0);
if (union_area > 2 * area)
continue;
rd->x = x0;
rd->y = y0;
rd->sx = x1 - x0;
rd->sy = y1 - y0;
return;
}
rd = xcalloc(1, sizeof *rd);
rd->x = x;
rd->y = y;
rd->sx = sx;
rd->sy = sy;
TAILQ_INSERT_TAIL(&w->damage, rd, entry);
if (++w->damage_count > REDRAW_DAMAGE_MAX)
redraw_collapse_damage(w);
}
/* Mark all cached redraw scenes as out of date. */
void
redraw_invalidate_all_scenes(void)
@@ -1381,45 +1505,30 @@ static void
redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
u_int y)
{
struct redraw_scene *scene = dctx->scene;
struct redraw_span_data *data = &span->data;
enum redraw_span_type type = data->type;
struct client *c = scene->c;
struct tty *tty = &c->tty;
struct visible_ranges *r;
struct visible_range *rr;
u_int i, x, n;
if (type == REDRAW_SPAN_STATUS && ~data->st.wp->flags & PANE_NEWSTATUS)
return;
r = tty_check_overlay_range(tty, span->x, y, span->width);
for (i = 0; i < r->used; i++) {
rr = &r->ranges[i];
if (rr->nx == 0)
continue;
x = rr->px;
n = rr->nx;
switch (span->data.type) {
case REDRAW_SPAN_PANE:
redraw_draw_pane_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_BORDER:
case REDRAW_SPAN_EMPTY:
case REDRAW_SPAN_OUTSIDE:
redraw_draw_border_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_STATUS:
redraw_draw_status_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_SCROLLBAR:
redraw_draw_scrollbar_span(dctx, span, x, y, n);
break;
case REDRAW_SPAN_MENU:
redraw_draw_menu_span(dctx, span, x, y, n);
break;
}
switch (span->data.type) {
case REDRAW_SPAN_PANE:
redraw_draw_pane_span(dctx, span, span->x, y, span->width);
break;
case REDRAW_SPAN_BORDER:
case REDRAW_SPAN_EMPTY:
case REDRAW_SPAN_OUTSIDE:
redraw_draw_border_span(dctx, span, span->x, y, span->width);
break;
case REDRAW_SPAN_STATUS:
redraw_draw_status_span(dctx, span, span->x, y, span->width);
break;
case REDRAW_SPAN_SCROLLBAR:
redraw_draw_scrollbar_span(dctx, span, span->x, y, span->width);
break;
case REDRAW_SPAN_MENU:
redraw_draw_menu_span(dctx, span, span->x, y, span->width);
break;
}
}
@@ -1621,6 +1730,24 @@ redraw_set_draw_context(struct redraw_draw_ctx *dctx,
dctx->flags |= REDRAW_ISOLATES;
}
/* Build a pane prompt. */
static void
redraw_make_pane_prompt(struct window_pane *wp, struct screen *screen)
{
struct screen_write_ctx ctx;
struct prompt_draw_data pdd;
screen_init(screen, wp->sx, 1, 0);
screen_write_start(&ctx, screen);
pdd.ctx = &ctx;
pdd.cursor_x = &wp->prompt_cx;
pdd.area_x = 0;
pdd.area_width = wp->sx;
pdd.prompt_line = 0;
prompt_draw(wp->prompt, &pdd);
screen_write_stop(&ctx);
}
/* Draw a pane's prompt over its content. */
static void
redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
@@ -1629,8 +1756,6 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
struct client *c = scene->c;
struct tty *tty = &c->tty;
struct screen screen;
struct screen_write_ctx ctx;
struct prompt_draw_data pdd;
int ox = scene->ox, oy = scene->oy;
int sx = scene->sx, sy = scene->sy;
int line, cy, px, offset, width, wy;
@@ -1663,17 +1788,8 @@ redraw_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp)
if (px + width > sx)
width = sx - px;
screen_init(&screen, wp->sx, 1, 0);
screen_write_start(&ctx, &screen);
pdd.ctx = &ctx;
pdd.cursor_x = &wp->prompt_cx;
pdd.area_x = 0;
pdd.area_width = wp->sx;
pdd.prompt_line = 0;
prompt_draw(wp->prompt, &pdd);
screen_write_stop(&ctx);
tty_draw_line(tty, &screen, 0, offset, width, px, cy, NULL);
redraw_make_pane_prompt(wp, &screen);
tty_draw_line(tty, &screen, offset, 0, width, px, cy, NULL);
screen_free(&screen);
}
@@ -1688,10 +1804,8 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
struct screen *sl;
struct redraw_scene *scene;
struct window_pane *loop;
u_int width, i, y, lines, j;
u_int width, i, y, lines;
struct redraw_span *first;
struct visible_ranges *r;
struct visible_range *rr;
int redraw;
if (c->flags & CLIENT_SUSPENDED)
@@ -1704,7 +1818,9 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
redraw = status_prompt_redraw(c);
else
redraw = status_redraw(c);
if (!redraw && !REDRAW_IS_ALL(flags)) {
if (!redraw &&
(~c->flags & CLIENT_REDRAWSTATUSALWAYS) &&
!REDRAW_IS_ALL(flags)) {
flags &= ~REDRAW_STATUS;
if (flags == 0)
return;
@@ -1799,19 +1915,9 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
else
y = c->tty.sy - lines;
sl = c->status.active;
for (i = 0; i < lines; i++) {
r = tty_check_overlay_range(tty, 0, y + i, tty->sx);
for (j = 0; j < r->used; j++) {
rr = &r->ranges[j];
if (rr->nx == 0)
continue;
tty_draw_line(tty, sl, rr->px, i, rr->nx,
rr->px, y + i, NULL);
}
}
for (i = 0; i < lines; i++)
tty_draw_line(tty, sl, 0, i, tty->sx, 0, y + i, NULL);
}
if (c->overlay_draw != NULL && (flags & REDRAW_OVERLAY))
c->overlay_draw(c, c->overlay_data);
tty_reset(tty);
@@ -1867,18 +1973,13 @@ redraw_screen(struct client *c)
{
int flags = 0;
if (c->flags & CLIENT_REDRAWWINDOW) {
if (c->flags & CLIENT_REDRAWOVERLAY)
redraw_draw(c, NULL, REDRAW_ALL);
else
redraw_draw(c, NULL, REDRAW_ALL & ~REDRAW_OVERLAY);
} else {
if (c->flags & CLIENT_REDRAWWINDOW)
redraw_draw(c, NULL, REDRAW_ALL);
else {
if (c->flags & CLIENT_REDRAWBORDERS)
flags |= (REDRAW_PANE_BORDER|REDRAW_PANE_STATUS);
if (c->flags & CLIENT_REDRAWSTATUS)
if (c->flags & (CLIENT_REDRAWSTATUS|CLIENT_REDRAWSTATUSALWAYS))
flags |= (REDRAW_STATUS|REDRAW_PANE_STATUS);
if (c->flags & CLIENT_REDRAWOVERLAY)
flags |= REDRAW_OVERLAY;
if (c->flags & CLIENT_REDRAWMENU)
flags |= REDRAW_MENU;
if (c->session->curw->window->menu != NULL)
@@ -1903,3 +2004,138 @@ redraw_pane_scrollbar(struct client *c, struct window_pane *wp)
{
redraw_draw(c, wp, REDRAW_PANE_SCROLLBAR);
}
/* Rebuild damaged pane status. */
static void
redraw_damage_refresh_status(struct redraw_draw_ctx *dctx,
struct window_pane *wp)
{
struct redraw_span *first;
u_int g = wp->status_generation, width;
if ((wp->flags & PANE_NEWSTATUS) && g == redraw_status_generation)
return;
width = redraw_pane_status_width(dctx, wp, &first);
if (width != 0) {
window_make_pane_status(wp, dctx->scene->c, width, first);
wp->flags |= PANE_NEWSTATUS;
wp->status_generation = redraw_status_generation;
}
}
/* Draw a pane's prompt over a damaged span. */
static void
redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx,
struct redraw_span *span, u_int y)
{
struct redraw_scene *scene = dctx->scene;
struct window_pane *wp = span->data.p.wp;
struct tty *tty = &scene->c->tty;
struct screen screen;
u_int px = span->data.p.px, width, prompt_y;
if (wp->prompt == NULL || wp->sx == 0 || wp->sy == 0)
return;
if (dctx->flags & REDRAW_STATUS_TOP)
prompt_y = 0;
else
prompt_y = wp->sy - 1;
if (span->data.p.py != prompt_y)
return;
redraw_make_pane_prompt(wp, &screen);
if (px < screen_size_x(&screen)) {
width = span->width;
if (width > screen_size_x(&screen) - px)
width = screen_size_x(&screen) - px;
tty_draw_line(tty, &screen, px, 0, width, span->x, y, NULL);
}
screen_free(&screen);
}
/* Draw the spans intersecting a damaged rectangle. */
static void
redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
u_int sx, u_int sy)
{
struct redraw_scene *scene = dctx->scene;
struct redraw_line *line;
struct redraw_spans *spans;
struct redraw_span *span;
u_int cy, yy, type;
if (x >= scene->sx || y >= scene->sy)
return;
if (x + sx > scene->sx)
sx = scene->sx - x;
if (y + sy > scene->sy)
sy = scene->sy - y;
if (sx == 0 || sy == 0)
return;
for (yy = y; yy < y + sy; yy++) {
line = &scene->lines[yy];
if (dctx->flags & REDRAW_STATUS_TOP)
cy = dctx->status_lines + yy;
else
cy = yy;
for (type = 0; type < REDRAW_SPAN_TYPES; type++) {
spans = &line->spans[type];
TAILQ_FOREACH(span, spans, entry) {
if (span->x >= x + sx)
continue;
if (span->x + span->width <= x)
continue;
if (type == REDRAW_SPAN_STATUS) {
redraw_damage_refresh_status(dctx,
span->data.st.wp);
}
redraw_draw_span(dctx, span, cy);
if (type == REDRAW_SPAN_PANE) {
redraw_damage_draw_pane_prompt(dctx,
span, cy);
}
}
}
}
}
/* Draw pending window damage on this client. */
void
redraw_client_damage(struct client *c)
{
struct window *w = c->session->curw->window;
struct window_pane *wp;
struct redraw_scene *scene;
struct redraw_draw_ctx dctx;
struct redraw_damage *rd;
u_int ox, oy, sx, sy, x0, y0, x1, y1;
if (TAILQ_EMPTY(&w->damage))
return;
redraw_status_generation++;
scene = redraw_get_scene(c);
if (scene == NULL)
return;
redraw_set_draw_context(&dctx, scene);
redraw_get_window_offset(c, &ox, &oy, &sx, &sy);
TAILQ_FOREACH(wp, &w->panes, entry) {
wp->border_gc_set = 0;
wp->active_border_gc_set = 0;
}
tty_sync_start(&c->tty);
tty_update_mode(&c->tty, c->tty.mode & ~CURSOR_MODES, NULL);
TAILQ_FOREACH(rd, &w->damage, entry) {
x0 = (rd->x > ox) ? rd->x : ox;
y0 = (rd->y > oy) ? rd->y : oy;
x1 = (rd->x + rd->sx < ox + sx) ? rd->x + rd->sx : ox + sx;
y1 = (rd->y + rd->sy < oy + sy) ? rd->y + rd->sy : oy + sy;
if (x0 < x1 && y0 < y1) {
redraw_draw_damage_rectangle(&dctx, x0 - ox, y0 - oy,
x1 - x0, y1 - y0);
}
}
}

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.290 2026/08/24 15:05:26 nicm Exp $ */
/* $OpenBSD: screen-write.c,v 1.292 2026/09/21 12:14:32 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -120,14 +120,26 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy)
evtimer_add(&w->offset_timer, &tv);
}
/* Do a full redraw. */
/* Redraw lines. */
static void
screen_write_redraw_cb(const struct tty_ctx *ttyctx)
screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny)
{
struct window_pane *wp = ttyctx->arg;
int x0, y0, x1, y1;
if (wp != NULL)
wp->flags |= PANE_REDRAW;
if (wp == NULL)
return;
x0 = wp->xoff;
y0 = wp->yoff + (int)py;
x1 = x0 + (int)wp->sx;
y1 = y0 + (int)ny;
if (x0 < 0)
x0 = 0;
if (y0 < 0)
y0 = 0;
if (x1 > x0 && y1 > y0)
redraw_damage_window(wp->window, x0, y0, x1 - x0, y1 - y0);
}
/* Update context for client. */
@@ -305,21 +317,16 @@ screen_write_initctx(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
if (~ctx->flags & SCREEN_WRITE_SYNC) {
/*
* For the active pane showing its base screen or for an
* overlay (no pane), only use synchronized updates if
* requested (commands that move the cursor); for other panes
* or a pane in a mode, always use it, since the cursor will
* have to move.
* For the active pane showing its base screen, only use
* synchronized updates if requested (commands that move the
* cursor); for other panes or a pane in a mode, always use it,
* since the cursor will have to move.
*/
if (ctx->wp != NULL && (ctx->wp != ctx->wp->window->active ||
ctx->wp->screen != &ctx->wp->base))
ttyctx->flags |= TTY_CTX_SYNC;
else {
if (ctx->wp == NULL)
ttyctx->flags |= TTY_CTX_OVERLAY_SYNC;
if (is_sync)
ttyctx->flags |= TTY_CTX_SYNC;
}
else if (is_sync)
ttyctx->flags |= TTY_CTX_SYNC;
tty_write(tty_cmd_syncstart, ttyctx);
ctx->flags |= SCREEN_WRITE_SYNC;
}
@@ -439,7 +446,6 @@ screen_write_reset(struct screen_write_ctx *ctx)
if (options_get_number(global_options, "extended-keys") == 2)
s->mode = (s->mode & ~EXTENDED_KEY_MODES)|MODE_KEYS_EXTENDED;
input_kitty_reset(s);
screen_write_clearscreen(ctx, 8);
screen_write_set_cursor(ctx, 0, 0);
@@ -715,7 +721,7 @@ screen_write_fast_copy(struct screen_write_ctx *ctx, struct screen *src,
if (!window_position_is_visible(r, xoff + s->cx))
break;
ttyctx.cell = &gc;
ttyctx.flags &= (TTY_CTX_OVERLAY_SYNC|TTY_CTX_SYNC);
ttyctx.flags &= TTY_CTX_SYNC;
tty_write(tty_cmd_cell, &ttyctx);
ttyctx.ocx++;
@@ -839,34 +845,39 @@ screen_write_menu(struct screen_write_ctx *ctx, struct menu *menu, int choice,
struct screen *s = ctx->s;
struct grid_cell default_gc;
const struct grid_cell *gc = &default_gc;
u_int cx, cy, i, j, width = menu->width;
u_int border, cx = s->cx, cy = s->cy, i, j, width;
const char *name;
cx = s->cx;
cy = s->cy;
memcpy(&default_gc, menu_gc, sizeof default_gc);
screen_write_box(ctx, menu->width + 4, menu->count + 2, lines,
border_gc, menu->title);
if (lines == BOX_LINES_NONE) {
border = 0;
width = menu->item_width;
} else {
border = 1;
width = menu->width;
screen_write_box(ctx, width + 4, menu->count + 2, lines,
border_gc, menu->title);
}
for (i = 0; i < menu->count; i++) {
name = menu->items[i].name;
if (name == NULL) {
screen_write_cursormove(ctx, cx, cy + 1 + i, 0);
screen_write_hline(ctx, width + 4, 1, 1, lines,
border_gc);
screen_write_cursormove(ctx, cx, cy + border + i, 0);
screen_write_hline(ctx, width + 2 + (2 * border), 1, 1,
lines, border_gc);
continue;
}
if (choice >= 0 && i == (u_int)choice && *name != '-')
gc = choice_gc;
screen_write_cursormove(ctx, cx + 1, cy + 1 + i, 0);
screen_write_cursormove(ctx, cx + border, cy + border + i, 0);
for (j = 0; j < width + 2; j++)
screen_write_putc(ctx, gc, ' ');
screen_write_cursormove(ctx, cx + 2, cy + 1 + i, 0);
screen_write_cursormove(ctx, cx + border + 1, cy + border + i,
0);
if (*name == '-') {
default_gc.attr |= GRID_ATTR_DIM;
format_draw(ctx, gc, width, name + 1, NULL, 0);
@@ -2156,7 +2167,7 @@ screen_write_fullredraw(struct screen_write_ctx *ctx)
screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx);
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy);
}
/* Trim collected items. */
@@ -3176,7 +3187,7 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc,
screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx);
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy);
}
/* Turn alternate screen off. */
@@ -3201,5 +3212,5 @@ screen_write_alternateoff(struct screen_write_ctx *ctx, struct grid_cell *gc,
screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx);
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy);
}

View File

@@ -88,8 +88,6 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit)
s->default_cstyle = SCREEN_CURSOR_DEFAULT;
s->mode = MODE_CURSOR;
s->default_mode = 0;
memset(&s->kitty_keys, 0, sizeof s->kitty_keys);
memset(&s->saved_kitty_keys, 0, sizeof s->saved_kitty_keys);
s->ccolour = -1;
s->default_ccolour = -1;
s->tabs = NULL;
@@ -120,9 +118,9 @@ screen_reinit(struct screen *s, int check)
if (options_get_number(global_options, "extended-keys") == 2)
s->mode = (s->mode & ~EXTENDED_KEY_MODES)|MODE_KEYS_EXTENDED;
if (SCREEN_IS_ALTERNATE(s))
screen_alternate_off(s, NULL, 0);
input_kitty_reset(s);
s->saved_cx = UINT_MAX;
s->saved_cy = UINT_MAX;
@@ -700,7 +698,6 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
if (SCREEN_IS_ALTERNATE(s))
return 0;
input_kitty_alternate_on(s);
sx = screen_size_x(s);
sy = screen_size_y(s);
@@ -761,7 +758,6 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
s->cy = screen_size_y(s) - 1;
return 0;
}
input_kitty_alternate_off(s);
/* Restore the saved grid. */
grid_duplicate_lines(s->grid, screen_hsize(s), s->saved_grid, 0,

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-client.c,v 1.512 2026/09/10 11:02:18 nicm Exp $ */
/* $OpenBSD: server-client.c,v 1.514 2026/09/22 06:46:50 nicm Exp $ */
/*
* Copyright (c) 2009 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -67,77 +67,6 @@ server_client_how_many(void)
return (n);
}
/* Overlay timer callback. */
static void
server_client_overlay_timer(__unused int fd, __unused short events, void *data)
{
server_client_clear_overlay(data);
}
/* Set an overlay on client. */
void
server_client_set_overlay(struct client *c, u_int delay,
overlay_check_cb checkcb, overlay_mode_cb modecb,
overlay_draw_cb drawcb, overlay_key_cb keycb, overlay_free_cb freecb,
overlay_resize_cb resizecb, void *data)
{
struct timeval tv;
if (c->overlay_draw != NULL)
server_client_clear_overlay(c);
tv.tv_sec = delay / 1000;
tv.tv_usec = (delay % 1000) * 1000L;
if (event_initialized(&c->overlay_timer))
evtimer_del(&c->overlay_timer);
evtimer_set(&c->overlay_timer, server_client_overlay_timer, c);
if (delay != 0)
evtimer_add(&c->overlay_timer, &tv);
c->overlay_check = checkcb;
c->overlay_mode = modecb;
c->overlay_draw = drawcb;
c->overlay_key = keycb;
c->overlay_free = freecb;
c->overlay_resize = resizecb;
c->overlay_data = data;
if (c->overlay_check == NULL)
c->tty.flags |= TTY_FREEZE;
if (c->overlay_mode == NULL)
c->tty.flags |= TTY_NOCURSOR;
window_update_focus(c->session->curw->window);
server_redraw_client(c);
}
/* Clear overlay mode on client. */
void
server_client_clear_overlay(struct client *c)
{
if (c->overlay_draw == NULL)
return;
if (event_initialized(&c->overlay_timer))
evtimer_del(&c->overlay_timer);
if (c->overlay_free != NULL)
c->overlay_free(c, c->overlay_data);
c->overlay_check = NULL;
c->overlay_mode = NULL;
c->overlay_draw = NULL;
c->overlay_key = NULL;
c->overlay_free = NULL;
c->overlay_resize = NULL;
c->overlay_data = NULL;
c->tty.flags &= ~(TTY_FREEZE|TTY_NOCURSOR);
if (c->session != NULL)
window_update_focus(c->session->curw->window);
server_redraw_client(c);
}
/* Are these ranges empty? That is, nothing is visible. */
int
server_client_ranges_is_empty(struct visible_ranges *r)
@@ -161,52 +90,6 @@ server_client_ensure_ranges(struct visible_ranges *r, u_int n)
r->size = n;
}
/*
* Given overlay position and dimensions, return parts of the input range which
* are visible.
*/
void
server_client_overlay_range(u_int x, u_int y, u_int sx, u_int sy, u_int px,
u_int py, u_int nx, struct visible_ranges *r)
{
u_int ox, onx;
/* Trivial case of no overlap in the y direction. */
if (py < y || py > y + sy - 1) {
server_client_ensure_ranges(r, 1);
r->ranges[0].px = px;
r->ranges[0].nx = nx;
r->used = 1;
return;
}
server_client_ensure_ranges(r, 2);
/* Visible bit to the left of the popup. */
if (px < x) {
r->ranges[0].px = px;
r->ranges[0].nx = x - px;
if (r->ranges[0].nx > nx)
r->ranges[0].nx = nx;
} else {
r->ranges[0].px = 0;
r->ranges[0].nx = 0;
}
/* Visible bit to the right of the popup. */
ox = x + sx;
if (px > ox)
ox = px;
onx = px + nx;
if (onx > ox) {
r->ranges[1].px = ox;
r->ranges[1].nx = onx - ox;
} else {
r->ranges[1].px = 0;
r->ranges[1].nx = 0;
}
r->used = 2;
}
/* Check if this client is inside this server. */
int
server_client_check_nested(struct client *c)
@@ -473,7 +356,13 @@ server_client_set_session(struct client *c, struct session *s)
tty_update_client_offset(c);
status_timer_start(c);
server_client_fire_session_changed(c, old);
server_redraw_client(c);
/*
* Redraw if the session or displayed window changed. Use the
* cached scene because the session's current window is already set.
*/
if (old != s || !redraw_client_has_window(c, s->curw->window))
server_redraw_client(c);
}
server_check_unattached();
@@ -490,7 +379,6 @@ server_client_lost(struct client *c)
cfg_client = NULL;
c->flags |= CLIENT_DEAD;
server_client_clear_overlay(c);
status_prompt_clear(c);
status_message_clear(c);
@@ -502,6 +390,9 @@ server_client_lost(struct client *c)
TAILQ_REMOVE(&clients, c, entry);
log_debug("lost client %p", c);
cmd_wait_for_client_lost(c);
cmdq_next(c);
if (c->flags & CLIENT_ATTACHED) {
server_client_attached_lost(c);
events_fire_client("client-detached", c);
@@ -1464,10 +1355,11 @@ server_client_key_callback(struct cmdq_item *item, void *data)
m->key = key;
/*
* Mouse drag is in progress, so fire the callback (now that
* the mouse event is valid).
* Synchronize direct drag output with the later damage redraw
* before invoking the drag callback.
*/
if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) {
tty_sync_start(&c->tty);
c->tty.mouse_drag_update(c, m);
goto out;
}
@@ -1746,10 +1638,6 @@ server_client_handle_key0(struct client *c, struct key_event *event,
if (s == NULL || (c->flags & CLIENT_UNATTACHEDFLAGS))
return (0);
/*
* Handle theme reporting keys before overlays so they work even when a
* popup is open.
*/
if (event->key == KEYC_REPORT_LIGHT_THEME) {
server_client_report_theme(c, THEME_LIGHT);
return (0);
@@ -1760,10 +1648,9 @@ server_client_handle_key0(struct client *c, struct key_event *event,
}
/*
* Key presses in overlay mode, dead panes waiting for a key, modal cancel
* keys, panes capturing all keys and the command prompt are special cases.
* The queue might be blocked so they need to be processed immediately
* rather than queued.
* Dead panes waiting for a key, modal cancel keys, panes capturing all keys
* and the command prompt are special cases. The queue might be blocked so
* they need to be processed immediately rather than queued.
*/
if (~c->flags & CLIENT_READONLY) {
if (c->message_string != NULL) {
@@ -1772,22 +1659,11 @@ server_client_handle_key0(struct client *c, struct key_event *event,
status_message_clear(c);
}
if (c->overlay_key != NULL) {
switch (c->overlay_key(c, c->overlay_data, event)) {
case 0:
return (0);
case 1:
server_client_clear_overlay(c);
return (0);
}
}
server_client_clear_overlay(c);
wp = s->curw->window->active;
if (server_client_handle_dead_key(wp, event->key))
return (0);
if (wp != NULL && wp == wp->window->modal &&
if (wp != NULL &&
wp == wp->window->modal &&
(wp->flags & PANE_CLOSEONCANCEL) &&
(event->key == '\033' || event->key == ('c'|KEYC_CTRL))) {
server_kill_pane(wp);
@@ -1910,8 +1786,8 @@ server_client_loop(void)
}
/*
* Any windows will have been redrawn as part of clients, so clear
* their flags now.
* Clear window redraw state after processing all clients. Deferred
* redraws are preserved in client flags.
*/
RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry) {
@@ -1922,6 +1798,8 @@ server_client_loop(void)
wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR|
PANE_ACTIVITY);
}
redraw_free_damage(w);
check_window_name(w);
}
@@ -2177,11 +2055,8 @@ server_client_reset_state(struct client *c)
flags = (tty->flags & TTY_BLOCK);
tty->flags &= ~TTY_BLOCK;
/* Get mode from overlay if any, else from screen. */
if (c->overlay_draw != NULL) {
if (c->overlay_mode != NULL)
s = c->overlay_mode(c, c->overlay_data, &cx, &cy);
} else if (w->menu != NULL) {
/* Get mode from the menu if any, else from the screen. */
if (w->menu != NULL) {
menu_get_cursor(w->menu, &cx, &cy);
s = menu_screen(w->menu);
} else if (wp != NULL && c->prompt == NULL)
@@ -2194,7 +2069,6 @@ server_client_reset_state(struct client *c)
log_debug("%s: client %s mode %s", __func__, c->name,
screen_mode_to_string(mode));
}
tty_update_kitty(tty, s);
/* Reset region and margin. */
tty_region_off(tty);
@@ -2204,7 +2078,7 @@ server_client_reset_state(struct client *c)
if (c->prompt != NULL) {
prompt = 1;
status_prompt_cursor(c, &cx, &cy);
} else if (wp != NULL && c->overlay_draw == NULL) {
} else if (wp != NULL) {
if (w->menu != NULL) {
tty_window_offset(tty, &ox, &oy, &sx, &sy);
if (cx < ox || cx >= ox + sx ||
@@ -2259,7 +2133,7 @@ server_client_reset_state(struct client *c)
if (!cursor)
mode &= ~MODE_CURSOR;
}
} else if (c->overlay_mode == NULL || s == NULL)
} else if (s == NULL)
mode &= ~MODE_CURSOR;
if (~pane_mode & MODE_SYNC) {
log_debug("%s: cursor to %u,%u", __func__, cx, cy);
@@ -2276,7 +2150,7 @@ server_client_reset_state(struct client *c)
* movement events.
*/
if (options_get_number(oo, "mouse")) {
if (c->overlay_draw == NULL && w->menu == NULL) {
if (w->menu == NULL) {
mode &= ~ALL_MOUSE_MODES;
TAILQ_FOREACH(loop, &w->panes, entry) {
if (loop->screen->mode & MODE_MOUSE_ALL)
@@ -2292,7 +2166,7 @@ server_client_reset_state(struct client *c)
}
/* Clear bracketed paste mode if at the prompt. */
if (c->overlay_draw == NULL && prompt)
if (prompt)
mode &= ~MODE_BRACKETPASTE;
/* Set the terminal mode and reset attributes. */
@@ -2471,6 +2345,8 @@ server_client_any_pane_redraw(struct client *c)
if (c->flags & CLIENT_REDRAWWINDOW)
return (1);
if (!TAILQ_EMPTY(&w->damage))
return (1);
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR))
return (1);
@@ -2487,6 +2363,7 @@ server_client_check_redraw(struct client *c)
struct window *w = s->curw->window;
struct window_pane *wp;
int needed, tflags, mode = tty->mode;
int damaged = !TAILQ_EMPTY(&w->damage);
struct timeval tv = { .tv_usec = 1000 };
static struct event ev;
size_t n;
@@ -2494,11 +2371,10 @@ server_client_check_redraw(struct client *c)
if (c->flags & (CLIENT_CONTROL|CLIENT_SUSPENDED))
return;
if (c->flags & CLIENT_ALLREDRAWFLAGS) {
log_debug("%s: redraw%s%s%s%s%s", c->name,
log_debug("%s: redraw%s%s%s%s", c->name,
(c->flags & CLIENT_REDRAWWINDOW) ? " window" : "",
(c->flags & CLIENT_REDRAWSTATUS) ? " status" : "",
(c->flags & CLIENT_REDRAWBORDERS) ? " borders" : "",
(c->flags & CLIENT_REDRAWOVERLAY) ? " overlay" : "",
(c->flags & CLIENT_REDRAWMENU) ? " menu" : "");
}
@@ -2513,12 +2389,12 @@ server_client_check_redraw(struct client *c)
return;
}
/*
* If there is outstanding data, defer the redraw until it has been
* consumed. We can just add a timer to get out of the event loop and
* end up back here.
*/
/* Ignore output queued within the current synchronized frame. */
n = EVBUFFER_LENGTH(tty->out);
if ((tty->flags & TTY_SYNCING) && n > tty->sync_offset)
n = tty->sync_offset;
/* Defer until output drains, preserving damage in client flags. */
if (n != 0 || (tty->flags & TTY_BLOCK)) {
if (n != 0)
log_debug("%s: redraw deferred (%zu left)", c->name, n);
@@ -2530,6 +2406,10 @@ server_client_check_redraw(struct client *c)
log_debug("redraw timer started");
evtimer_add(&ev, &tv);
}
if (damaged) {
c->flags |= CLIENT_REDRAWWINDOW;
return;
}
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW;
@@ -2563,6 +2443,10 @@ server_client_check_redraw(struct client *c)
redraw_pane_scrollbar(c, wp);
}
}
/* Draw damage here if no client redraw flags will handle it. */
if (damaged && (c->flags & CLIENT_ALLREDRAWFLAGS) == 0)
redraw_client_damage(c);
}
/*
@@ -2576,6 +2460,7 @@ server_client_check_redraw(struct client *c)
}
server_client_set_progress_bar(c);
redraw_screen(c);
redraw_client_damage(c);
}
/* Put the tty back how it was. */
@@ -2706,10 +2591,6 @@ server_client_dispatch(struct imsg *imsg, void *arg)
tty_resize(&c->tty);
tty_repeat_requests(&c->tty, 0);
recalculate_sizes();
if (c->overlay_resize == NULL)
server_client_clear_overlay(c);
else
c->overlay_resize(c, c->overlay_data);
server_redraw_client(c);
if (c->session != NULL)
server_client_fire_resized(c, old_sx, old_sy);

View File

@@ -1,4 +1,4 @@
/* $OpenBSD: server-fn.c,v 1.153 2026/09/10 11:02:18 nicm Exp $ */
/* $OpenBSD: server-fn.c,v 1.154 2026/09/21 12:43:36 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -310,8 +310,8 @@ server_link_window(struct session *src, struct winlink *srcwl,
}
if (killflag) {
/*
* Can't use session_detach as it will destroy session
* if this makes it empty.
* Can't use session_detach as it won't detach the only
* window.
*/
events_fire_winlink("window-unlinked", dstwl);
dstwl->flags &= ~WINLINK_ALERTFLAGS;

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