mirror of
https://github.com/tmux/tmux.git
synced 2026-09-24 13:46:28 +00:00
Compare commits
31 Commits
redraw-dam
...
redraw-dam
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a17365322 | ||
|
|
6933bc417c | ||
|
|
238067acbf | ||
|
|
20f76fb069 | ||
|
|
d4adce7e00 | ||
|
|
04dbc6d89b | ||
|
|
104a0cee99 | ||
|
|
a7f74730d7 | ||
|
|
4a1e445b38 | ||
|
|
9a3aa26a33 | ||
|
|
1ce64bd087 | ||
|
|
6f65c31887 | ||
|
|
bc20fc6da4 | ||
|
|
d74b980d80 | ||
|
|
ac4daa57c9 | ||
|
|
541ca0e844 | ||
|
|
07d40d5fa9 | ||
|
|
80e87aaf16 | ||
|
|
f41b983e04 | ||
|
|
48e33179d4 | ||
|
|
aed1209c02 | ||
|
|
6739bd03f5 | ||
|
|
f66eeef8a5 | ||
|
|
2d220d9a7f | ||
|
|
da4895d559 | ||
|
|
b254f557e4 | ||
|
|
b26eeb5229 | ||
|
|
48cdd85886 | ||
|
|
f255f089fc | ||
|
|
d6074895bb | ||
|
|
faba411289 |
@@ -257,8 +257,7 @@ check_PROGRAMS = \
|
||||
fuzz/input-fuzzer \
|
||||
fuzz/cmd-parse-fuzzer \
|
||||
fuzz/format-fuzzer \
|
||||
fuzz/style-fuzzer \
|
||||
fuzz/layout-fuzzer
|
||||
fuzz/style-fuzzer
|
||||
fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS)
|
||||
fuzz_input_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
|
||||
fuzz_cmd_parse_fuzzer_LDFLAGS = $(FUZZING_LIBS)
|
||||
@@ -267,8 +266,6 @@ 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.
|
||||
|
||||
@@ -307,7 +307,8 @@ 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, oxoff, oyoff, osx, osy;
|
||||
int y, ly, x, lx;
|
||||
int old_xoff, old_yoff, old_sx, old_sy;
|
||||
|
||||
wp = cmd_mouse_pane(m, NULL, &wl);
|
||||
if (wp == NULL) {
|
||||
@@ -329,16 +330,17 @@ 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;
|
||||
old_xoff = wp->xoff;
|
||||
old_yoff = wp->yoff;
|
||||
old_sx = wp->sx;
|
||||
old_sy = wp->sy;
|
||||
|
||||
lc->g.xoff += x - lx;
|
||||
lc->g.yoff += y - ly;
|
||||
layout_fix_panes(w, NULL);
|
||||
|
||||
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
|
||||
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
|
||||
old_sy);
|
||||
server_redraw_window_borders(w);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,8 +237,9 @@ 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, resizes = 0;
|
||||
int new_xoff, new_yoff, old_xoff, old_yoff;
|
||||
int left, right;
|
||||
int new_xoff, new_yoff, resizes = 0;
|
||||
int old_xoff, old_yoff, old_sx, old_sy;
|
||||
|
||||
wp = cmd_mouse_pane(m, NULL, &wl);
|
||||
if (wp == NULL) {
|
||||
@@ -251,6 +252,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
|
||||
sy = wp->sy;
|
||||
old_xoff = wp->xoff;
|
||||
old_yoff = wp->yoff;
|
||||
old_sx = (int)wp->sx;
|
||||
old_sy = (int)wp->sy;
|
||||
left = wp->xoff - 1;
|
||||
right = wp->xoff + sx;
|
||||
if (window_pane_scrollbar_reserve(wp) &&
|
||||
@@ -350,7 +353,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
|
||||
}
|
||||
if (resizes != 0) {
|
||||
layout_fix_panes(w, NULL);
|
||||
window_redraw_floating_pane(wp, old_xoff, old_yoff, sx, sy);
|
||||
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
|
||||
old_sy);
|
||||
server_redraw_window_borders(w);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +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;
|
||||
int old_xoff, old_yoff, old_sx, old_sy;
|
||||
|
||||
if (c->tty.mouse_last_pane == -1)
|
||||
return;
|
||||
@@ -418,14 +418,15 @@ 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;
|
||||
old_xoff = wp->xoff;
|
||||
old_yoff = wp->yoff;
|
||||
old_sx = wp->sx;
|
||||
old_sy = wp->sy;
|
||||
|
||||
layout_set_size(lc, sx, sy, xoff, yoff);
|
||||
layout_fix_panes(w, NULL);
|
||||
|
||||
window_redraw_floating_pane(wp, oxoff, oyoff, osx, osy);
|
||||
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
|
||||
old_sy);
|
||||
server_redraw_window_borders(w);
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# 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["
|
||||
"@"
|
||||
@@ -1,2 +0,0 @@
|
||||
[libfuzzer]
|
||||
max_len = 1024
|
||||
2
layout.c
2
layout.c
@@ -477,9 +477,9 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
|
||||
wp->yoff != old_yoff ||
|
||||
wp->sx != old_sx ||
|
||||
wp->sy != old_sy) {
|
||||
changed = 1;
|
||||
if (window_pane_scrollbar_reserve(wp))
|
||||
wp->flags |= PANE_REDRAWSCROLLBAR;
|
||||
changed = 1;
|
||||
}
|
||||
}
|
||||
if (changed)
|
||||
|
||||
@@ -153,13 +153,11 @@ 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 -gF -t "$p0" @after '#{pane_mode}' ||
|
||||
$TMUX display-panes -Nd 500 -t "$p0" \; set -g @after fast ||
|
||||
fail "display-panes immediate command failed"
|
||||
wait_option @after 'panes-mode'
|
||||
wait_option @after fast
|
||||
wait_format "$p0" '#{pane_mode}' 'panes-mode'
|
||||
wait_format "$p0" '#{pane_in_mode}' '0'
|
||||
|
||||
# Existing zoom is restored on exit.
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,6 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Each drag motion must draw the floating pane content once.
|
||||
# server_client_check_redraw() had `(~c->flags & CLIENT_ALLREDRAWFLAGS)` as
|
||||
# a fallback condition guarding a call to redraw_client_damage() - for a
|
||||
# multi-bit mask, `~x & MASK` means "at least one of these bits is unset"
|
||||
# (almost always true), not "none of these bits are set" as the comment
|
||||
# and surrounding logic clearly intend. Every floating-pane drag command
|
||||
# unconditionally sets CLIENT_REDRAWBORDERS (server_redraw_window_borders()
|
||||
# in cmd-resize-pane.c/cmd-join-pane.c/cmd-split-window.c) alongside
|
||||
# reporting window damage, so this fallback fired on every single drag
|
||||
# step, composing the exact same damage rectangle a second time a few
|
||||
# lines later at the CLIENT_ALLREDRAWFLAGS block - wasted work, not a
|
||||
# correctness issue, but a clean, deterministic signal to check for via
|
||||
# the server's own -vv log.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -11,7 +22,7 @@ export PATH TERM LC_ALL
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
cd "$DIR" || exit 1
|
||||
INNER="$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null"
|
||||
INNER="$TEST_TMUX -vv -Ldoublecomp-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null"
|
||||
|
||||
fail()
|
||||
@@ -36,16 +47,14 @@ mouse()
|
||||
sleep 0.15
|
||||
}
|
||||
|
||||
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 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 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
|
||||
'sleep 100') || exit 1
|
||||
|
||||
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || 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
|
||||
@@ -59,18 +68,13 @@ 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.
|
||||
# Top-border drag (a move): each step both reports window damage and sets
|
||||
# CLIENT_REDRAWBORDERS (server_redraw_window_borders() in the caller),
|
||||
# which is exactly the combination the buggy fallback misfired on.
|
||||
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
|
||||
while [ $i -lt $steps ]; do
|
||||
GRABCOL=$((GRABCOL + 1))
|
||||
mouse 32 "$GRABCOL" "$BORDERROW" M
|
||||
i=$((i + 1))
|
||||
@@ -79,10 +83,18 @@ 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"
|
||||
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
|
||||
|
||||
# Each drag step should compose its damage exactly once. If any rectangle
|
||||
# was composed twice, the same "x,y WxH" text appears on two consecutive
|
||||
# composing-damage lines - compare the position+size together, since
|
||||
# distinct steps commonly share the same size (only the position differs).
|
||||
dup=$(grep "composing damage" "$LOG" | awk '{print $(NF-1), $NF}' |
|
||||
uniq -d | wc -l)
|
||||
[ "$dup" -eq 0 ] ||
|
||||
fail "$dup damage rectangle(s) were composed twice in the same pass"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -137,23 +137,4 @@ 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
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Synchronized drag output must not cause a full redraw of untouched rows.
|
||||
# server_client_key_callback()'s mouse-drag dispatch opens a synchronized-
|
||||
# output frame (tty_sync_start()) before running the drag callback, on
|
||||
# every single drag motion event. server_client_check_redraw() then checks
|
||||
# EVBUFFER_LENGTH(tty->out) != 0 later in the same pass to decide whether
|
||||
# to defer this pass's redraw - nothing drains tty->out in between, so the
|
||||
# frame-open sequence just queued (8 bytes: "\033[?2026h") makes that check
|
||||
# see "outstanding output" and defer against itself, escalating the drag's
|
||||
# damage to a full-window redraw on every motion event on any
|
||||
# synchronized-output-capable terminal. This checks the server's own -vv
|
||||
# log for that exact self-inflicted "8 left" deferral pattern during a
|
||||
# drag, and requires it never appears.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -11,7 +21,7 @@ export PATH TERM LC_ALL
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
cd "$DIR" || exit 1
|
||||
INNER="$TEST_TMUX -Lsyncdefer-inner-$$ -f/dev/null"
|
||||
INNER="$TEST_TMUX -vv -Lsyncdefer-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null"
|
||||
|
||||
fail()
|
||||
@@ -36,20 +46,18 @@ mouse()
|
||||
sleep 0.15
|
||||
}
|
||||
|
||||
$INNER new-session -d -s inner -x 40 -y 15 "printf '\\033[15;1HOUTSIDE'; exec sleep 100" || exit 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 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
|
||||
'sleep 100') || exit 1
|
||||
|
||||
$OUTER new-session -d -s outer -x 40 -y 15 'sleep 100' || 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
|
||||
$INNER set-option -as terminal-features ',screen-256color:sync' || exit 1
|
||||
$OUTER 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
|
||||
@@ -60,18 +68,13 @@ 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.
|
||||
# Plain (non-Alt) top-border drag: "MouseDrag1Border" -> resize-pane -M ->
|
||||
# a move, since grabbing the top border moves rather than resizes. Several
|
||||
# small steps, each its own drag-motion event and so its own pass through
|
||||
# the code under test.
|
||||
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
|
||||
while [ $i -lt 6 ]; do
|
||||
GRABCOL=$((GRABCOL + 1))
|
||||
mouse 32 "$GRABCOL" "$BORDERROW" M
|
||||
i=$((i + 1))
|
||||
@@ -80,14 +83,13 @@ 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
|
||||
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
|
||||
|
||||
n=$(grep -c "redraw deferred (8 left)" "$LOG")
|
||||
[ "$n" -eq 0 ] ||
|
||||
fail "drag self-deferred against its own queued sync bytes $n time(s)"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
#!/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.
|
||||
# A floating pane positioned partly off the window's left/top edge (e.g.
|
||||
# created with -X -5) has a negative wp->xoff/wp->yoff. screen_write_
|
||||
# redraw_cb() (screen-write.c) used to pass these straight through as u_int
|
||||
# to redraw_damage_window(), which wraps a negative offset to a huge value
|
||||
# - redraw_damage_window()'s own bounds check then rejects the whole
|
||||
# rectangle, so nothing gets redrawn, not even the pane's visible portion.
|
||||
#
|
||||
# This fires on returning from the alternate screen (screen_write_
|
||||
# alternateoff()) among other paths. This test exercises exactly that:
|
||||
# fills the pane's primary screen, switches it to the alternate screen and
|
||||
# back, and checks the client actually receives the restored primary
|
||||
# content in the pane's visible (on-screen) columns - using an attached
|
||||
# client's own received bytes (via a nested outer client), not
|
||||
# capture-pane, which reads the grid directly and would pass regardless of
|
||||
# whether the client was ever actually told to redraw it.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -44,24 +57,28 @@ wait_outer_has()
|
||||
fail "outer client did not show $marker"
|
||||
}
|
||||
|
||||
wait_visible_restored()
|
||||
{
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
|
||||
sed -n "${CONTENTROW}p" "$CAPTURE" | grep -q '^AAAAA' && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "primary-screen content was not restored in the pane's visible columns after returning from the alternate screen"
|
||||
}
|
||||
|
||||
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[1;1H", 'A' x 15;
|
||||
sleep 2;
|
||||
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[1;1H", 'B' x 15;
|
||||
sleep 2;
|
||||
print "\e[?1049l";
|
||||
sleep 100;
|
||||
PERL
|
||||
@@ -70,6 +87,15 @@ $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
|
||||
|
||||
# Content pane spans window columns -5..9 (partly off the left edge); only
|
||||
# columns 0..9 are ever visible.
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X -5 -Y 2 \
|
||||
"perl '$EMITTER'") || exit 1
|
||||
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
|
||||
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
|
||||
[ "$XOFF" -lt 0 ] || fail "sanity: floating pane is not off-screen (xoff=$XOFF)"
|
||||
CONTENTROW=$((YOFF + 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
|
||||
@@ -78,25 +104,8 @@ $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
|
||||
wait_outer_has AAAAA
|
||||
wait_outer_has BBBBB
|
||||
wait_visible_restored
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Damage redraws must show each client's own pane status, even though the
|
||||
# cached status screen is shared between clients.
|
||||
# redraw_damage_refresh_status() (screen-redraw.c) force-regenerates a
|
||||
# pane's border-status title when a damage rectangle touches it, guarded
|
||||
# by the per-pane PANE_NEWSTATUS flag. window_make_pane_status() formats
|
||||
# pane-border-format using the requesting client's own context (so e.g.
|
||||
# #{client_name} differs per client), but wp->status_screen/PANE_NEWSTATUS
|
||||
# are shared by every client viewing the pane. With two clients attached
|
||||
# to the same session, whichever client's damage pass runs first renders
|
||||
# its own text and sets the flag; the other client's damage pass, finding
|
||||
# the flag already set, used to skip rendering entirely and reuse
|
||||
# whatever was already there.
|
||||
#
|
||||
# This checks the actual server-side decision via the -vv log rather than
|
||||
# a visual capture: an unrelated periodic client status-refresh reliably
|
||||
# repaints each client's title correctly again within the same tick right
|
||||
# after the buggy decision is made, before anything is ever flushed to
|
||||
# either terminal, so the wrong content this bug produces is never
|
||||
# visible to any external capture - the log is the only place the actual
|
||||
# bug (or its absence) can be observed.
|
||||
#
|
||||
# A floating pane with its own pane-border-status is positioned so that a
|
||||
# damage rectangle from an *unrelated* palette change (OSC 4) in the
|
||||
# underlying tiled pane - whose own geometry spans the whole window -
|
||||
# overlaps the floating pane's title row without touching its content,
|
||||
# giving a damage-only trigger with no side effect that would otherwise
|
||||
# force a normal (non-buggy) full per-client status re-render in the same
|
||||
# pass and mask the result either way.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -11,141 +35,83 @@ 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
|
||||
cd "$DIR" || exit 1
|
||||
INNER="$TEST_TMUX -vv -Lstatuscc-inner-$$ -f/dev/null"
|
||||
OUTER1="$TEST_TMUX -Lstatuscc-outer1-$$ -f/dev/null"
|
||||
OUTER2="$TEST_TMUX -Lstatuscc-outer2-$$ -f/dev/null"
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$OUTER1 kill-server 2>/dev/null
|
||||
$OUTER2 kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
cd /
|
||||
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'
|
||||
BASEEMITTER=$DIR/base-emitter.pl
|
||||
cat >"$BASEEMITTER" <<'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";
|
||||
}
|
||||
my $line = <STDIN>;
|
||||
print "\e]4;1;rgb:11/22/33\e\\";
|
||||
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 \
|
||||
$INNER new-session -d -s inner -x 40 -y 10 "perl '$BASEEMITTER'" || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
$INNER set-option -g pane-border-status top || exit 1
|
||||
$INNER set-option -g pane-border-format 'C=#{client_name}' || exit 1
|
||||
BASE=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1
|
||||
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 20 -y 3 -X 5 -Y 4 \
|
||||
'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
|
||||
$OUTER1 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
|
||||
$OUTER1 set-option -g status off || exit 1
|
||||
$OUTER1 set-option -g window-size manual || exit 1
|
||||
$OUTER1 set-option -g default-terminal screen-256color || exit 1
|
||||
$OUTER1 respawn-pane -k -t outer:0.0 \
|
||||
"$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null attach-session -t inner" ||
|
||||
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
|
||||
NAME1=$($INNER list-clients -F '#{client_name}') || exit 1
|
||||
|
||||
# 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
|
||||
$OUTER2 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
|
||||
$OUTER2 set-option -g status off || exit 1
|
||||
$OUTER2 set-option -g window-size manual || exit 1
|
||||
$OUTER2 set-option -g default-terminal screen-256color || exit 1
|
||||
$OUTER2 respawn-pane -k -t outer:0.0 \
|
||||
"$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null attach-session -t inner" ||
|
||||
exit 1
|
||||
sleep 0.5
|
||||
ALLNAMES=$($INNER list-clients -F '#{client_name}') || exit 1
|
||||
NAME2=$(echo "$ALLNAMES" | grep -v "^$NAME1\$")
|
||||
[ -n "$NAME2" ] || fail "sanity: could not identify the second client"
|
||||
|
||||
# Let any attach-driven full redraw (and its own, non-buggy, per-client
|
||||
# status render) finish completely before triggering the damage-only
|
||||
# palette update.
|
||||
sleep 1.5
|
||||
|
||||
$INNER send-keys -t "$BASE" Enter || exit 1
|
||||
sleep 0.5
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
|
||||
|
||||
n1=$(grep -c "regenerated pane .* status for $NAME1\$" "$LOG")
|
||||
n2=$(grep -c "regenerated pane .* status for $NAME2\$" "$LOG")
|
||||
[ "$n1" -ge 1 ] || fail "damage pass never regenerated $NAME1's own status - it reused whatever the other client's render left behind"
|
||||
[ "$n2" -ge 1 ] || fail "damage pass never regenerated $NAME2's own status - it reused whatever the other client's render left behind"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -131,14 +131,6 @@ 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'
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,140 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,112 +0,0 @@
|
||||
#!/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
|
||||
@@ -1,7 +1,23 @@
|
||||
#!/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.
|
||||
# A pane that doesn't span the terminal's full width - because
|
||||
# pane-scrollbars is on (the scrollbar occupies a column) or the pane is
|
||||
# one of a side-by-side split - needs DECSLRM (left/right margin) support
|
||||
# to use the fast native-scroll path (tty_cmd_linefeed()/scrollup()/
|
||||
# scrolldown()/reverseindex(), tty.c:
|
||||
# "(!tty_full_width(tty, ctx) && !tty_use_margin(tty))"). Without it, every
|
||||
# single scroll falls back to tty_redraw_region()'s full manual repaint of
|
||||
# the whole region - a real, confirmed source of flicker (and, separately,
|
||||
# of image content not surviving a scroll in branches with image support).
|
||||
#
|
||||
# tty_default_features() (tty-features.c) grants the "margins" feature to
|
||||
# several terminals it can positively identify via XTVERSION/DA2 (mintty,
|
||||
# iTerm2, WezTerm, ghostty, XTerm-as-VT420) - this checks the actual
|
||||
# server-side scroll decision via the -vv log for the underlying mechanism
|
||||
# those table entries all rely on, using the terminal-features option
|
||||
# directly (which any of them - or a user's own terminal-overrides -
|
||||
# ultimately feed into) rather than simulating any one terminal's
|
||||
# identification handshake.
|
||||
|
||||
PATH=/bin:/usr/bin
|
||||
TERM=screen
|
||||
@@ -11,7 +27,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"
|
||||
cd "$DIR" || exit 1
|
||||
INNER="$TEST_TMUX -vv -Lmarginsscrollbar-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Lmarginsscrollbar-outer-$$ -f/dev/null"
|
||||
|
||||
fail()
|
||||
@@ -24,70 +41,77 @@ cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
cd /
|
||||
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"
|
||||
wait_for_client()
|
||||
{
|
||||
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
|
||||
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
|
||||
[ -n "$CLIENT" ] && return 0
|
||||
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"
|
||||
fail "inner client did not attach"
|
||||
}
|
||||
|
||||
run_scroll_phase()
|
||||
{
|
||||
label=$1
|
||||
margins=$2
|
||||
|
||||
rm -f tmux-server*.log
|
||||
|
||||
$INNER new-session -d -s inner -x 40 -y 6 'exec sh' || exit 1
|
||||
$INNER set -g status 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 ',*:margins' || exit 1
|
||||
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 new-session -d -x 40 -y 6 || exit 1
|
||||
OUTERPANE=$($OUTER list-panes -F '#{pane_id}') || 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 send-keys -t "$OUTERPANE" -l "$INNER attach -t inner" || exit 1
|
||||
$OUTER send-keys -t "$OUTERPANE" Enter || exit 1
|
||||
sleep 1
|
||||
|
||||
wait_for_client
|
||||
|
||||
i=0
|
||||
while [ "$i" -lt 8 ]; do
|
||||
$INNER send-keys -t inner Enter || exit 1
|
||||
sleep 0.2
|
||||
i=$((i + 1))
|
||||
done
|
||||
sleep 0.3
|
||||
|
||||
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
|
||||
[ -n "$LOG" ] || fail "$label: sanity: no server -vv log was produced"
|
||||
|
||||
n=$(grep -c "tty_redraw_region.*large region redraw" "$LOG")
|
||||
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
done
|
||||
|
||||
echo "$n"
|
||||
}
|
||||
|
||||
# Phase 1: margins granted - must never fall back to a full region redraw.
|
||||
n_with=$(run_scroll_phase "with margins" "on")
|
||||
[ "$n_with" -eq 0 ] ||
|
||||
fail "with margins granted, scrolling a scrollbar-enabled pane still fell back to a full region redraw ($n_with times)"
|
||||
|
||||
# Phase 2: sanity check - without margins, the same scenario must actually
|
||||
# hit the fallback, proving phase 1 wasn't accidentally trivial.
|
||||
n_without=$(run_scroll_phase "without margins" "off")
|
||||
[ "$n_without" -gt 0 ] ||
|
||||
fail "sanity: without margins, scrolling a scrollbar-enabled pane never fell back to a full region redraw - this scenario no longer exercises the bug this test checks for"
|
||||
|
||||
exit 0
|
||||
|
||||
310
screen-redraw.c
310
screen-redraw.c
@@ -204,19 +204,19 @@ struct redraw_scene {
|
||||
u_int oy;
|
||||
};
|
||||
|
||||
/* A damaged rectangle in a window. */
|
||||
/* A single damaged window-coordinate rectangle. */
|
||||
struct redraw_damage {
|
||||
u_int x;
|
||||
u_int y;
|
||||
u_int sx;
|
||||
u_int sy;
|
||||
u_int x;
|
||||
u_int y;
|
||||
u_int sx;
|
||||
u_int sy;
|
||||
|
||||
TAILQ_ENTRY(redraw_damage) entry;
|
||||
TAILQ_ENTRY(redraw_damage) entry;
|
||||
};
|
||||
|
||||
/*
|
||||
* If there are more damage rectangles than this, they are collapsed into
|
||||
* one.
|
||||
* Cap on the number of pending damage rectangles per window before they are
|
||||
* collapsed into a single rectangle covering their union.
|
||||
*/
|
||||
#define REDRAW_DAMAGE_MAX 16
|
||||
|
||||
@@ -224,15 +224,15 @@ struct redraw_damage {
|
||||
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.
|
||||
* Bumped once per redraw_client_damage() call (one client's one redraw
|
||||
* pass) - see redraw_damage_refresh_status().
|
||||
*/
|
||||
static u_int redraw_status_generation;
|
||||
static u_int redraw_status_serial;
|
||||
|
||||
/* Context for building the scene. */
|
||||
struct redraw_build_ctx {
|
||||
@@ -1103,7 +1103,7 @@ redraw_free_damage(struct window *w)
|
||||
w->damage_count = 0;
|
||||
}
|
||||
|
||||
/* Collapse all pending damage for a window into one rectangle. */
|
||||
/* Collapse all pending damage for a window into one rectangle - its union. */
|
||||
static void
|
||||
redraw_collapse_damage(struct window *w)
|
||||
{
|
||||
@@ -1140,7 +1140,14 @@ redraw_collapse_damage(struct window *w)
|
||||
w->damage_count = 1;
|
||||
}
|
||||
|
||||
/* Record window damage, merging nearby rectangles and limiting the count. */
|
||||
/*
|
||||
* Record a damaged window-coordinate rectangle. Clips it to the window,
|
||||
* merges it with an existing rectangle where doing so does not make the
|
||||
* result substantially larger than the two combined, and collapses the
|
||||
* whole list to its union once it grows past a modest cap.
|
||||
*
|
||||
* This only records damage - nothing consumes it yet.
|
||||
*/
|
||||
void
|
||||
redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy)
|
||||
{
|
||||
@@ -1157,6 +1164,7 @@ redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy)
|
||||
return;
|
||||
|
||||
TAILQ_FOREACH(rd, &w->damage, entry) {
|
||||
/* Skip unless overlapping or directly adjacent. */
|
||||
if (x > rd->x + rd->sx || rd->x > x + sx ||
|
||||
y > rd->y + rd->sy || rd->y > y + sy)
|
||||
continue;
|
||||
@@ -1184,7 +1192,9 @@ redraw_damage_window(struct window *w, u_int x, u_int y, u_int sx, u_int sy)
|
||||
rd->sx = sx;
|
||||
rd->sy = sy;
|
||||
TAILQ_INSERT_TAIL(&w->damage, rd, entry);
|
||||
if (++w->damage_count > REDRAW_DAMAGE_MAX)
|
||||
w->damage_count++;
|
||||
|
||||
if (w->damage_count > REDRAW_DAMAGE_MAX)
|
||||
redraw_collapse_damage(w);
|
||||
}
|
||||
|
||||
@@ -1500,10 +1510,14 @@ redraw_draw_menu_span(struct redraw_draw_ctx *dctx,
|
||||
tty_draw_line(tty, s, px, span->data.m.py, n, x, y, NULL);
|
||||
}
|
||||
|
||||
/* Draw a span. */
|
||||
/*
|
||||
* Draw a span, restricted to [clip_x, clip_x + clip_n) - a caller drawing
|
||||
* the whole span passes the span's own x/width here; a caller drawing only
|
||||
* a damaged sub-range passes that range instead.
|
||||
*/
|
||||
static void
|
||||
redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
|
||||
u_int y)
|
||||
u_int y, u_int clip_x, u_int clip_n)
|
||||
{
|
||||
struct redraw_span_data *data = &span->data;
|
||||
enum redraw_span_type type = data->type;
|
||||
@@ -1513,21 +1527,21 @@ redraw_draw_span(struct redraw_draw_ctx *dctx, struct redraw_span *span,
|
||||
|
||||
switch (span->data.type) {
|
||||
case REDRAW_SPAN_PANE:
|
||||
redraw_draw_pane_span(dctx, span, span->x, y, span->width);
|
||||
redraw_draw_pane_span(dctx, span, clip_x, y, clip_n);
|
||||
break;
|
||||
case REDRAW_SPAN_BORDER:
|
||||
case REDRAW_SPAN_EMPTY:
|
||||
case REDRAW_SPAN_OUTSIDE:
|
||||
redraw_draw_border_span(dctx, span, span->x, y, span->width);
|
||||
redraw_draw_border_span(dctx, span, clip_x, y, clip_n);
|
||||
break;
|
||||
case REDRAW_SPAN_STATUS:
|
||||
redraw_draw_status_span(dctx, span, span->x, y, span->width);
|
||||
redraw_draw_status_span(dctx, span, clip_x, y, clip_n);
|
||||
break;
|
||||
case REDRAW_SPAN_SCROLLBAR:
|
||||
redraw_draw_scrollbar_span(dctx, span, span->x, y, span->width);
|
||||
redraw_draw_scrollbar_span(dctx, span, clip_x, y, clip_n);
|
||||
break;
|
||||
case REDRAW_SPAN_MENU:
|
||||
redraw_draw_menu_span(dctx, span, span->x, y, span->width);
|
||||
redraw_draw_menu_span(dctx, span, clip_x, y, clip_n);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1562,15 +1576,19 @@ redraw_draw_pane_lines(struct redraw_draw_ctx *dctx, struct window_pane *wp,
|
||||
if (flags & REDRAW_PANE) {
|
||||
spans = &line->spans[REDRAW_SPAN_PANE];
|
||||
TAILQ_FOREACH(span, spans, entry) {
|
||||
if (span->data.p.wp == wp)
|
||||
redraw_draw_span(dctx, span, cy);
|
||||
if (span->data.p.wp == wp) {
|
||||
redraw_draw_span(dctx, span, cy,
|
||||
span->x, span->width);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (flags & REDRAW_PANE_SCROLLBAR) {
|
||||
spans = &line->spans[REDRAW_SPAN_SCROLLBAR];
|
||||
TAILQ_FOREACH(span, spans, entry) {
|
||||
if (span->data.sb.wp == wp)
|
||||
redraw_draw_span(dctx, span, cy);
|
||||
if (span->data.sb.wp == wp) {
|
||||
redraw_draw_span(dctx, span, cy,
|
||||
span->x, span->width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1628,8 +1646,10 @@ redraw_draw_lines(struct redraw_draw_ctx *dctx, int flags)
|
||||
}
|
||||
}
|
||||
spans = &line->spans[type];
|
||||
TAILQ_FOREACH(span, spans, entry)
|
||||
redraw_draw_span(dctx, span, cy);
|
||||
TAILQ_FOREACH(span, spans, entry) {
|
||||
redraw_draw_span(dctx, span, cy, span->x,
|
||||
span->width);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1649,8 +1669,10 @@ redraw_draw_menu_lines(struct redraw_draw_ctx *dctx)
|
||||
cy = dctx->status_lines + y;
|
||||
else
|
||||
cy = y;
|
||||
TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry)
|
||||
redraw_draw_span(dctx, span, cy);
|
||||
TAILQ_FOREACH(span, &line->spans[REDRAW_SPAN_MENU], entry) {
|
||||
redraw_draw_span(dctx, span, cy, span->x,
|
||||
span->width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1730,7 +1752,7 @@ redraw_set_draw_context(struct redraw_draw_ctx *dctx,
|
||||
dctx->flags |= REDRAW_ISOLATES;
|
||||
}
|
||||
|
||||
/* Build a pane prompt. */
|
||||
/* Build a pane prompt into a one-line screen. */
|
||||
static void
|
||||
redraw_make_pane_prompt(struct window_pane *wp, struct screen *screen)
|
||||
{
|
||||
@@ -1818,8 +1840,7 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags)
|
||||
redraw = status_prompt_redraw(c);
|
||||
else
|
||||
redraw = status_redraw(c);
|
||||
if (!redraw &&
|
||||
(~c->flags & CLIENT_REDRAWSTATUSALWAYS) &&
|
||||
if (!redraw && (~c->flags & CLIENT_REDRAWSTATUSALWAYS) &&
|
||||
!REDRAW_IS_ALL(flags)) {
|
||||
flags &= ~REDRAW_STATUS;
|
||||
if (flags == 0)
|
||||
@@ -2005,34 +2026,162 @@ redraw_pane_scrollbar(struct client *c, struct window_pane *wp)
|
||||
redraw_draw(c, wp, REDRAW_PANE_SCROLLBAR);
|
||||
}
|
||||
|
||||
/* Rebuild damaged pane status. */
|
||||
/*
|
||||
* A REDRAW_SPAN_STATUS span within a damaged rectangle needs its content
|
||||
* rebuilt and force-drawn regardless of whether that content has logically
|
||||
* changed. window_make_pane_status()'s grid_compare() only tells us
|
||||
* whether the *content* changed, not whether the physical cells were
|
||||
* disturbed by something else (e.g. a floating pane sliding across this
|
||||
* row) - and being inside a damage rectangle already proves that happened.
|
||||
* Without this, redraw_draw_span() silently skips REDRAW_SPAN_STATUS spans
|
||||
* whenever PANE_NEWSTATUS is not set, leaving a pane's border-status title
|
||||
* blank until some unrelated redraw happens to touch it (e.g. a focus
|
||||
* change or window resize).
|
||||
*
|
||||
* wp->status_screen/PANE_NEWSTATUS are per-pane, but the formatted content
|
||||
* (window_make_pane_status() expands pane-border-format, which can read
|
||||
* per-client fields like #{client_name}) is per-client. Gating purely on
|
||||
* PANE_NEWSTATUS would let one client's damage pass render its own text,
|
||||
* set the flag, and leave every other client's pass - this tick or any
|
||||
* later one, since nothing else clears it here - reusing that stale,
|
||||
* wrong-client text. redraw_status_serial (bumped once per
|
||||
* redraw_client_damage() call, i.e. once per client per pass) still
|
||||
* dedupes repeat calls within that same pass, but forces a fresh,
|
||||
* correctly-client-formatted render on every distinct client/pass.
|
||||
*/
|
||||
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;
|
||||
u_int width;
|
||||
|
||||
if ((wp->flags & PANE_NEWSTATUS) && g == redraw_status_generation)
|
||||
if ((wp->flags & PANE_NEWSTATUS) &&
|
||||
wp->status_serial == redraw_status_serial)
|
||||
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;
|
||||
}
|
||||
if (width == 0)
|
||||
return;
|
||||
log_debug("%s: regenerated pane %%%u status for %s", __func__, wp->id,
|
||||
dctx->scene->c->name);
|
||||
window_make_pane_status(wp, dctx->scene->c, width, first);
|
||||
wp->flags |= PANE_NEWSTATUS;
|
||||
wp->status_serial = redraw_status_serial;
|
||||
}
|
||||
|
||||
/* Draw a pane's prompt over a damaged span. */
|
||||
/* Whether the cell at (px, py) in screen s is a padding cell. */
|
||||
static int
|
||||
redraw_screen_cell_is_padding(struct screen *s, u_int px, u_int py)
|
||||
{
|
||||
struct grid_cell gc;
|
||||
|
||||
if (px >= screen_size_x(s))
|
||||
return (0);
|
||||
grid_view_get_cell(s->grid, px, py, &gc);
|
||||
return ((gc.flags & GRID_FLAG_PADDING) != 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* Whether the cell at scene x-coordinate x within this span is the second
|
||||
* (padding) half of a wide character - the condition under which growing a
|
||||
* damage clip's edge toward it, to pull in the rest of that character, is
|
||||
* correct. True unconditionally for span types with no real backing screen
|
||||
* (border, scrollbar) - these only ever draw single synthesized cells, so
|
||||
* growing them is always harmless. For span types with a real screen (pane
|
||||
* content, a pane's status line, a menu), only true when x is actually a
|
||||
* padding cell there - if x is instead the start of an unrelated,
|
||||
* already-complete character, growing toward it would walk into that
|
||||
* character's opposite half and corrupt it: tty_draw_line() clears a
|
||||
* leading padding cell in its draw range (proof the range starts
|
||||
* mid-character) and, via a different check (tty_draw_line_get_empty()'s
|
||||
* gc->data.width > nx test), also clears a trailing base cell that has no
|
||||
* room left for its own padding (proof the range ends mid-character) - so
|
||||
* growing either edge onto a base cell is equally destructive to whatever
|
||||
* character lies just outside the range, just through a different part of
|
||||
* tty_draw_line().
|
||||
*/
|
||||
static int
|
||||
redraw_span_cell_is_padding(struct redraw_span *span, u_int x)
|
||||
{
|
||||
struct screen *s;
|
||||
u_int px, py;
|
||||
|
||||
switch (span->data.type) {
|
||||
case REDRAW_SPAN_PANE:
|
||||
s = span->data.p.wp->screen;
|
||||
px = span->data.p.px + (x - span->x);
|
||||
py = span->data.p.py;
|
||||
break;
|
||||
case REDRAW_SPAN_STATUS:
|
||||
s = &span->data.st.wp->status_screen;
|
||||
px = span->data.st.offset + (x - span->x);
|
||||
py = 0;
|
||||
break;
|
||||
case REDRAW_SPAN_MENU:
|
||||
s = menu_screen(span->data.m.md);
|
||||
px = span->data.m.px + (x - span->x);
|
||||
py = span->data.m.py;
|
||||
break;
|
||||
default:
|
||||
return (1);
|
||||
}
|
||||
return (redraw_screen_cell_is_padding(s, px, py));
|
||||
}
|
||||
|
||||
/*
|
||||
* Grow a clipped span range by one cell on either edge that isn't already at
|
||||
* the span's own boundary. A clip edge that lands mid-character (this is a
|
||||
* damage rectangle, so its edges are geometric and have no idea what's in
|
||||
* the grid) may be sitting on the second, padding half of a wide character
|
||||
* whose other half falls just outside the requested range - growing by one
|
||||
* cell is enough to pull the whole character back in, since no grid cell is
|
||||
* ever wider than two columns, and clamping to the span's own x and width
|
||||
* keeps this from bleeding into a neighbouring span. Both edges need the
|
||||
* same padding check before growing: growing onto a cell that isn't padding
|
||||
* (an unrelated, already-complete character just outside the range) is
|
||||
* destructive on either side, not just the left - see
|
||||
* redraw_span_cell_is_padding().
|
||||
*/
|
||||
static void
|
||||
redraw_damage_grow_span_clip(struct redraw_span *span, u_int *xp, u_int *endp)
|
||||
{
|
||||
if (*xp > span->x && redraw_span_cell_is_padding(span, *xp))
|
||||
(*xp)--;
|
||||
if (*endp < span->x + span->width &&
|
||||
redraw_span_cell_is_padding(span, *endp))
|
||||
(*endp)++;
|
||||
}
|
||||
|
||||
/*
|
||||
* As redraw_damage_grow_span_clip(), but against an explicit screen: px0 is
|
||||
* the column in that screen corresponding to span->x, py the row. Used for
|
||||
* a span's separately rendered content (e.g. a pane's prompt) that isn't
|
||||
* span->data.p.wp->screen (or whichever grid redraw_span_cell_is_padding()
|
||||
* would otherwise consult for this span's type), and so has its own,
|
||||
* unrelated wide-character boundaries at the same columns.
|
||||
*/
|
||||
static void
|
||||
redraw_damage_grow_screen_clip(struct redraw_span *span, struct screen *s,
|
||||
u_int px0, u_int py, u_int *xp, u_int *endp)
|
||||
{
|
||||
if (*xp > span->x &&
|
||||
redraw_screen_cell_is_padding(s, px0 + (*xp - span->x), py))
|
||||
(*xp)--;
|
||||
if (*endp < span->x + span->width &&
|
||||
redraw_screen_cell_is_padding(s, px0 + (*endp - span->x), py))
|
||||
(*endp)++;
|
||||
}
|
||||
|
||||
/* Recompose a pane's prompt over a damaged section of its display row. */
|
||||
static void
|
||||
redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx,
|
||||
struct redraw_span *span, u_int y)
|
||||
struct redraw_span *span, u_int y, u_int x, u_int n)
|
||||
{
|
||||
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;
|
||||
u_int px, width, prompt_y, x0, x1;
|
||||
|
||||
if (wp->prompt == NULL || wp->sx == 0 || wp->sy == 0)
|
||||
return;
|
||||
@@ -2042,26 +2191,46 @@ redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx,
|
||||
prompt_y = wp->sy - 1;
|
||||
if (span->data.p.py != prompt_y)
|
||||
return;
|
||||
|
||||
redraw_make_pane_prompt(wp, &screen);
|
||||
|
||||
/*
|
||||
* x and n were clipped and grown against wp->screen, whose character
|
||||
* boundaries have nothing to do with the prompt's separately
|
||||
* rendered screen - realign the range on the prompt's own grid
|
||||
* instead, clamped to this span so it cannot bleed into a
|
||||
* neighbouring one.
|
||||
*/
|
||||
x0 = x;
|
||||
x1 = x + n;
|
||||
redraw_damage_grow_screen_clip(span, &screen, span->data.p.px, 0, &x0,
|
||||
&x1);
|
||||
|
||||
px = span->data.p.px + (x0 - span->x);
|
||||
if (px < screen_size_x(&screen)) {
|
||||
width = span->width;
|
||||
width = x1 - x0;
|
||||
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);
|
||||
tty_draw_line(tty, &screen, px, 0, width, x0, y, NULL);
|
||||
}
|
||||
screen_free(&screen);
|
||||
}
|
||||
|
||||
/* Draw the spans intersecting a damaged rectangle. */
|
||||
/*
|
||||
* Compose exactly the cells within a damaged rectangle (already in this
|
||||
* client's own scene coordinates), rather than a whole pane. For each row
|
||||
* in range, every span of every type whose x-range intersects the
|
||||
* rectangle is drawn restricted to just the intersected sub-range.
|
||||
*/
|
||||
static void
|
||||
redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
|
||||
redraw_draw_damage_rect(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;
|
||||
u_int cy, yy, clip_x, clip_end, type;
|
||||
|
||||
if (x >= scene->sx || y >= scene->sy)
|
||||
return;
|
||||
@@ -2081,30 +2250,42 @@ redraw_draw_damage_rectangle(struct redraw_draw_ctx *dctx, u_int x, u_int y,
|
||||
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)
|
||||
clip_x = (span->x > x) ? span->x : x;
|
||||
clip_end = (span->x + span->width < x + sx) ?
|
||||
span->x + span->width : x + sx;
|
||||
if (clip_end <= clip_x)
|
||||
continue;
|
||||
if (type == REDRAW_SPAN_STATUS) {
|
||||
redraw_damage_refresh_status(dctx,
|
||||
span->data.st.wp);
|
||||
}
|
||||
redraw_draw_span(dctx, span, cy);
|
||||
redraw_damage_grow_span_clip(span, &clip_x,
|
||||
&clip_end);
|
||||
redraw_draw_span(dctx, span, cy, clip_x,
|
||||
clip_end - clip_x);
|
||||
if (type == REDRAW_SPAN_PANE) {
|
||||
redraw_damage_draw_pane_prompt(dctx,
|
||||
span, cy);
|
||||
span, cy, clip_x,
|
||||
clip_end - clip_x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Draw pending window damage on this client. */
|
||||
/*
|
||||
* Consume a client's window's pending damage by composing exactly the
|
||||
* damaged cells, after clipping each rectangle to what this client can see
|
||||
* and translating it into this client's own scene coordinates.
|
||||
*
|
||||
* Unlike redraw_pane(), this does not redraw a whole pane's worth of cells
|
||||
* for a small disturbance - only the cells within the (clipped) rectangle
|
||||
* are touched, via redraw_draw_damage_rect().
|
||||
*/
|
||||
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;
|
||||
@@ -2112,7 +2293,7 @@ redraw_client_damage(struct client *c)
|
||||
|
||||
if (TAILQ_EMPTY(&w->damage))
|
||||
return;
|
||||
redraw_status_generation++;
|
||||
redraw_status_serial++;
|
||||
|
||||
scene = redraw_get_scene(c);
|
||||
if (scene == NULL)
|
||||
@@ -2120,11 +2301,6 @@ redraw_client_damage(struct client *c)
|
||||
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);
|
||||
|
||||
@@ -2133,9 +2309,11 @@ redraw_client_damage(struct client *c)
|
||||
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);
|
||||
}
|
||||
if (x0 >= x1 || y0 >= y1)
|
||||
continue;
|
||||
log_debug("%s: %s composing damage %u,%u %ux%u", __func__,
|
||||
c->name, x0 - ox, y0 - oy, x1 - x0, y1 - y0);
|
||||
redraw_draw_damage_rect(&dctx, x0 - ox, y0 - oy, x1 - x0,
|
||||
y1 - y0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +120,18 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy)
|
||||
evtimer_add(&w->offset_timer, &tv);
|
||||
}
|
||||
|
||||
/* Redraw lines. */
|
||||
/*
|
||||
* Called when a write could not be applied directly to the terminal and
|
||||
* needs a redraw instead. Report damage for the requested rows. wp->yoff is
|
||||
* already adjusted past any top pane-border-status row, so wp->yoff + py is
|
||||
* the correct window-coordinate row. wp->xoff/wp->yoff are signed and can be
|
||||
* negative for a floating pane positioned partly off the window's left or
|
||||
* top edge, so clip to the window's own origin here before converting to
|
||||
* the unsigned coordinates redraw_damage_window() takes - passing a
|
||||
* negative offset through unclipped wraps to a huge value that its own
|
||||
* bounds check then silently rejects, losing the pane's visible portion
|
||||
* entirely rather than just the off-screen part.
|
||||
*/
|
||||
static void
|
||||
screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny)
|
||||
{
|
||||
@@ -138,8 +149,10 @@ screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny)
|
||||
x0 = 0;
|
||||
if (y0 < 0)
|
||||
y0 = 0;
|
||||
if (x1 > x0 && y1 > y0)
|
||||
redraw_damage_window(wp->window, x0, y0, x1 - x0, y1 - y0);
|
||||
if (x1 <= x0 || y1 <= y0)
|
||||
return;
|
||||
redraw_damage_window(wp->window, (u_int)x0, (u_int)y0,
|
||||
(u_int)(x1 - x0), (u_int)(y1 - y0));
|
||||
}
|
||||
|
||||
/* Update context for client. */
|
||||
|
||||
@@ -358,10 +358,30 @@ server_client_set_session(struct client *c, struct session *s)
|
||||
server_client_fire_session_changed(c, old);
|
||||
|
||||
/*
|
||||
* Redraw if the session or displayed window changed. Use the
|
||||
* cached scene because the session's current window is already set.
|
||||
* A full redraw is only needed if the client's session or
|
||||
* current window actually changed - not if this merely
|
||||
* confirmed the client is still looking at the same window
|
||||
* (as happens when switch-client -t targets a pane in the
|
||||
* already-current window, e.g. clicking a pane name in a
|
||||
* second #{P:} status line: the default MouseDown1Status
|
||||
* binding resolves that click to switch-client -t=, which
|
||||
* reaches here regardless of whether anything besides the
|
||||
* active pane changed). Redrawing unconditionally here
|
||||
* forced a full window redraw for what should have been
|
||||
* just an active-pane change, already handled narrowly by
|
||||
* window_set_active_pane() and window_redraw_active_switch()
|
||||
* before this is reached.
|
||||
*
|
||||
* old and s may be the same session object, whose curw was
|
||||
* already updated to the new window before this function was
|
||||
* called - old->curw and s->curw would then read the same,
|
||||
* already-current value, so comparing them can never detect
|
||||
* a same-session window change. Compare against the client's
|
||||
* own cached scene instead, which only reflects what it has
|
||||
* actually drawn.
|
||||
*/
|
||||
if (old != s || !redraw_client_has_window(c, s->curw->window))
|
||||
if (old == NULL || old != s ||
|
||||
!redraw_client_has_window(c, s->curw->window))
|
||||
server_redraw_client(c);
|
||||
}
|
||||
|
||||
@@ -1355,8 +1375,18 @@ server_client_key_callback(struct cmdq_item *item, void *data)
|
||||
m->key = key;
|
||||
|
||||
/*
|
||||
* Synchronize direct drag output with the later damage redraw
|
||||
* before invoking the drag callback.
|
||||
* Mouse drag is in progress, so fire the callback (now that
|
||||
* the mouse event is valid).
|
||||
*
|
||||
* Start a synchronized-output region here rather than
|
||||
* leaving it to whatever redraw eventually follows: a drag
|
||||
* callback may write directly via the pane's fast path
|
||||
* immediately, with any correction only arriving later via
|
||||
* redraw_client_damage(), which opens its own sync region.
|
||||
* Since tty_sync_end() is only called once, at the very end
|
||||
* of this client's pass in server_client_reset_state(),
|
||||
* starting it here merges both into one atomic terminal
|
||||
* update instead of two visible frames.
|
||||
*/
|
||||
if ((key & KEYC_MASK_KEY) == KEYC_DRAGGING) {
|
||||
tty_sync_start(&c->tty);
|
||||
@@ -1786,8 +1816,13 @@ server_client_loop(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* Clear window redraw state after processing all clients. Deferred
|
||||
* redraws are preserved in client flags.
|
||||
* Any windows will have been redrawn as part of clients, so clear
|
||||
* their flags now. A client whose redraw was deferred this pass
|
||||
* (waiting for outstanding tty output to drain) has already
|
||||
* escalated to CLIENT_REDRAWWINDOW or CLIENT_REDRAWSCROLLBARS in
|
||||
* server_client_check_redraw() to cover whatever it is about to
|
||||
* lose here, so PANE_REDRAW/PANE_REDRAWSCROLLBAR and window damage
|
||||
* can simply be cleared unconditionally.
|
||||
*/
|
||||
RB_FOREACH(w, windows, &windows) {
|
||||
TAILQ_FOREACH(wp, &w->panes, entry) {
|
||||
@@ -2363,7 +2398,6 @@ 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;
|
||||
@@ -2389,12 +2423,28 @@ server_client_check_redraw(struct client *c)
|
||||
return;
|
||||
}
|
||||
|
||||
/* Ignore output queued within the current synchronized frame. */
|
||||
/*
|
||||
* 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. server_client_loop() clears PANE_REDRAW,
|
||||
* PANE_REDRAWSCROLLBAR and window damage unconditionally every pass,
|
||||
* so escalate to a coarser, persistent client flag that survives
|
||||
* that clear and forces a full catch-up redraw once this client is
|
||||
* unblocked, rather than trying to keep the fine-grained state
|
||||
* around for a retry.
|
||||
*
|
||||
* If a synchronized-output frame is open, discount anything queued
|
||||
* since it started (down to sync_offset, the length when it opened):
|
||||
* those bytes are already part of the frame this pass is committed
|
||||
* to flushing (see tty_sync_start()), not a reason to defer this
|
||||
* pass's redraw - without this, a mouse-drag callback that itself
|
||||
* opens the frame before writing anything would see its own
|
||||
* just-queued bytes as "outstanding output" and defer against
|
||||
* itself every single motion event.
|
||||
*/
|
||||
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);
|
||||
@@ -2406,10 +2456,8 @@ server_client_check_redraw(struct client *c)
|
||||
log_debug("redraw timer started");
|
||||
evtimer_add(&ev, &tv);
|
||||
}
|
||||
if (damaged) {
|
||||
if (!TAILQ_EMPTY(&w->damage))
|
||||
c->flags |= CLIENT_REDRAWWINDOW;
|
||||
return;
|
||||
}
|
||||
TAILQ_FOREACH(wp, &w->panes, entry) {
|
||||
if (wp->flags & PANE_REDRAW) {
|
||||
c->flags |= CLIENT_REDRAWWINDOW;
|
||||
@@ -2444,8 +2492,18 @@ server_client_check_redraw(struct client *c)
|
||||
}
|
||||
}
|
||||
|
||||
/* Draw damage here if no client redraw flags will handle it. */
|
||||
if (damaged && (c->flags & CLIENT_ALLREDRAWFLAGS) == 0)
|
||||
/*
|
||||
* Window damage is also what makes server_client_any_pane_
|
||||
* redraw() decide a redraw is needed at all, independently of
|
||||
* any CLIENT_ALLREDRAWFLAGS bit. Every current damage source
|
||||
* happens to set one of those flags too, so the block below
|
||||
* always consumes it - but consume it here too in case that
|
||||
* ever stops holding, since server_client_loop() clears
|
||||
* window damage unconditionally every pass regardless of
|
||||
* whether it was actually drawn.
|
||||
*/
|
||||
if (!TAILQ_EMPTY(&w->damage) &&
|
||||
(c->flags & CLIENT_ALLREDRAWFLAGS) == 0)
|
||||
redraw_client_damage(c);
|
||||
}
|
||||
|
||||
|
||||
14
tmux.1
14
tmux.1
@@ -1,4 +1,4 @@
|
||||
.\" $OpenBSD: tmux.1,v 1.1173 2026/09/23 12:37:50 nicm Exp $
|
||||
.\" $OpenBSD: tmux.1,v 1.1172 2026/09/22 06:58:05 nicm Exp $
|
||||
.\"
|
||||
.\" Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||
.\"
|
||||
@@ -14,7 +14,7 @@
|
||||
.\" IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
|
||||
.\" OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
.\"
|
||||
.Dd $Mdocdate: September 23 2026 $
|
||||
.Dd $Mdocdate: September 22 2026 $
|
||||
.Dt TMUX 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
@@ -994,19 +994,13 @@ Will run:
|
||||
/bin/sh \-c \[aq]vi \[ti]/.tmux.conf\[aq]
|
||||
.Ed
|
||||
.Pp
|
||||
Unless specified, tmux uses the value of
|
||||
.Ic default\-shell
|
||||
in place of
|
||||
.Pa /bin/sh .
|
||||
.Pp
|
||||
Additionally, the
|
||||
.Ic new\-window ,
|
||||
.Ic new\-session ,
|
||||
.Ic split\-window ,
|
||||
.Ic respawn\-window ,
|
||||
.Ic respawn\-pane
|
||||
.Ic respawn\-window
|
||||
and
|
||||
.Ic display\-popup
|
||||
.Ic respawn\-pane
|
||||
commands allow
|
||||
.Ar shell\-command
|
||||
to be given as multiple arguments and executed directly (without
|
||||
|
||||
12
tmux.h
12
tmux.h
@@ -1403,7 +1403,7 @@ struct window_pane {
|
||||
struct screen base;
|
||||
|
||||
struct screen status_screen;
|
||||
u_int status_generation;
|
||||
u_int status_serial;
|
||||
|
||||
TAILQ_HEAD(, window_mode_entry) modes;
|
||||
|
||||
@@ -1806,6 +1806,12 @@ struct tty {
|
||||
struct event timer;
|
||||
size_t discarded;
|
||||
|
||||
/*
|
||||
* Buffer length at the instant a synchronized-output frame opened
|
||||
* (tty_sync_start()), so server_client_check_redraw()'s "is there
|
||||
* already outstanding output" check can discount whatever this
|
||||
* pass itself queued into that frame - see tty_sync_start().
|
||||
*/
|
||||
size_t sync_offset;
|
||||
|
||||
struct termios tio;
|
||||
@@ -3821,9 +3827,9 @@ int window_pane_get_pane_status(struct window_pane *);
|
||||
struct style_range *window_pane_status_get_range(struct window_pane *, u_int,
|
||||
u_int);
|
||||
int window_pane_is_floating(struct window_pane *);
|
||||
void window_pane_redraw_floating(struct window *,
|
||||
struct window_pane *, int, int, int, int);
|
||||
int window_pane_is_floating_with_hidden(struct window_pane *);
|
||||
void window_redraw_floating_pane(struct window_pane *, int, int,
|
||||
int, int);
|
||||
|
||||
/* window-border.c */
|
||||
void window_set_fill_cells(struct window *);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* $OpenBSD: tty-features.c,v 1.46 2026/09/24 08:16:13 nicm Exp $ */
|
||||
/* $OpenBSD: tty-features.c,v 1.45 2026/09/22 14:10:26 nicm Exp $ */
|
||||
|
||||
/*
|
||||
* Copyright (c) 2020 Nicholas Marriott <nicholas.marriott@gmail.com>
|
||||
|
||||
103
window.c
103
window.c
@@ -2942,21 +2942,31 @@ window_pane_is_floating_with_hidden(struct window_pane *wp)
|
||||
return (1);
|
||||
}
|
||||
|
||||
/* Report damage for a floating pane, including its border and scrollbar. */
|
||||
/*
|
||||
* Report damage for a floating pane's rectangle, grown by one cell on every
|
||||
* side - a floating pane draws its border frame at xoff-1/yoff-1 through
|
||||
* xoff+sx/yoff+sy (see the "floating" case in screen-redraw.c), one cell
|
||||
* outside its own content area, so damage for just the content area leaves
|
||||
* the frame's previous position undrawn as the pane moves. If a scrollbar
|
||||
* is reserved, its side of the frame is pushed out further still by its
|
||||
* width and padding (also matched in screen-redraw.c), so grow that side
|
||||
* to match.
|
||||
*/
|
||||
static void
|
||||
window_damage_floating_pane(struct window_pane *wp, int xoff, int yoff,
|
||||
int sx, int sy)
|
||||
window_pane_damage_floating(struct window *w, struct window_pane *wp,
|
||||
int xoff, int yoff, int sx, int sy)
|
||||
{
|
||||
struct window *w = wp->window;
|
||||
int x0, x1, y0, y1, sb_left = 0, sb_right = 0;
|
||||
struct style *sb_sy = &wp->scrollbar_style;
|
||||
int x0, x1, y0, y1, sb_left = 0, sb_right = 0;
|
||||
|
||||
if (window_pane_scrollbar_reserve(wp)) {
|
||||
if (w->sb_pos == PANE_SCROLLBARS_LEFT)
|
||||
sb_left = sb_sy->width + sb_sy->pad;
|
||||
sb_left = wp->scrollbar_style.width +
|
||||
wp->scrollbar_style.pad;
|
||||
else
|
||||
sb_right = sb_sy->width + sb_sy->pad;
|
||||
sb_right = wp->scrollbar_style.width +
|
||||
wp->scrollbar_style.pad;
|
||||
}
|
||||
|
||||
x0 = xoff - 1 - sb_left;
|
||||
x1 = xoff + sx + sb_right;
|
||||
y0 = yoff - 1;
|
||||
@@ -2965,16 +2975,75 @@ window_damage_floating_pane(struct window_pane *wp, int xoff, int yoff,
|
||||
x0 = 0;
|
||||
if (y0 < 0)
|
||||
y0 = 0;
|
||||
if (x1 >= x0 && y1 >= y0)
|
||||
redraw_damage_window(w, x0, y0, x1 - x0 + 1U, y1 - y0 + 1U);
|
||||
if (x1 < x0 || y1 < y0)
|
||||
return;
|
||||
redraw_damage_window(w, (u_int)x0, (u_int)y0, (u_int)(x1 - x0) + 1,
|
||||
(u_int)(y1 - y0) + 1);
|
||||
}
|
||||
|
||||
/* Report damage for a floating pane's old and new areas. */
|
||||
void
|
||||
window_redraw_floating_pane(struct window_pane *wp, int oxoff, int oyoff,
|
||||
int osx, int osy)
|
||||
/*
|
||||
* Whether a pane's scrollbar strip - not its whole body - intersects a
|
||||
* window-coordinate rectangle. A reserved scrollbar occupies a strip of
|
||||
* scrollbar_style.width+pad columns just outside the pane's own content
|
||||
* area (see the scrollbar-reserve case in layout_fix_panes(), layout.c),
|
||||
* on whichever side w->sb_pos points to.
|
||||
*/
|
||||
static int
|
||||
window_pane_scrollbar_intersects(struct window *w, struct window_pane *wp,
|
||||
u_int x, u_int y, u_int sx, u_int sy)
|
||||
{
|
||||
window_damage_floating_pane(wp, oxoff, oyoff, osx, osy);
|
||||
window_damage_floating_pane(wp, wp->xoff, wp->yoff, wp->sx, wp->sy);
|
||||
server_status_window(wp->window);
|
||||
int sb_x, sb_w, ix = (int)x, iy = (int)y, isx = (int)sx;
|
||||
int isy = (int)sy;
|
||||
|
||||
if (!window_pane_scrollbar_reserve(wp))
|
||||
return (0);
|
||||
sb_w = wp->scrollbar_style.width + wp->scrollbar_style.pad;
|
||||
if (w->sb_pos == PANE_SCROLLBARS_LEFT)
|
||||
sb_x = (int)wp->xoff - sb_w;
|
||||
else
|
||||
sb_x = (int)wp->xoff + (int)wp->sx;
|
||||
|
||||
return (sb_x < ix + isx && sb_x + sb_w > ix &&
|
||||
(int)wp->yoff < iy + isy && (int)wp->yoff + (int)wp->sy > iy);
|
||||
}
|
||||
|
||||
/*
|
||||
* Report damage for only a floating pane's old and new area, rather than
|
||||
* the whole window - a floating pane move or resize only disturbs what it
|
||||
* was covering and what it now covers. Scrollbars aren't covered by the
|
||||
* damage system, so a pane whose *scrollbar strip* (not its whole body)
|
||||
* intersects either area is still flagged directly for a scrollbar redraw.
|
||||
* Checking the whole pane body here, rather than just its narrow scrollbar
|
||||
* strip, meant merely dragging over a pane's ordinary content set
|
||||
* PANE_REDRAWSCROLLBAR on every such pane on every motion event, triggering
|
||||
* a needless scrollbar redraw (and the redraw pass it forces) each time
|
||||
* even though the scrollbar itself never moved.
|
||||
*
|
||||
* Shared by every command that drags a floating pane around by the mouse:
|
||||
* resize-pane's own border drag (cmd-resize-pane.c), move-pane -M's
|
||||
* alternate Alt-drag (cmd-join-pane.c), and split-window/new-pane's
|
||||
* interactive resize of a newly-created floating pane (cmd-split-window.c).
|
||||
*/
|
||||
void
|
||||
window_pane_redraw_floating(struct window *w, struct window_pane *wp,
|
||||
int old_xoff, int old_yoff, int old_sx, int old_sy)
|
||||
{
|
||||
struct window_pane *loop;
|
||||
|
||||
window_pane_damage_floating(w, wp, old_xoff, old_yoff, old_sx,
|
||||
old_sy);
|
||||
window_pane_damage_floating(w, wp, wp->xoff, wp->yoff, wp->sx,
|
||||
wp->sy);
|
||||
|
||||
TAILQ_FOREACH(loop, &w->panes, entry) {
|
||||
if (window_pane_scrollbar_intersects(w, loop,
|
||||
(u_int)old_xoff, (u_int)old_yoff, (u_int)old_sx,
|
||||
(u_int)old_sy) ||
|
||||
window_pane_scrollbar_intersects(w, loop, wp->xoff,
|
||||
wp->yoff, wp->sx, wp->sy))
|
||||
loop->flags |= PANE_REDRAWSCROLLBAR;
|
||||
}
|
||||
|
||||
/* Session status formats may depend on the pane's new geometry. */
|
||||
server_status_window(w);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user