mirror of
https://github.com/tmux/tmux.git
synced 2026-09-23 21:33:57 +00:00
screen-write: clip negative floating-pane offsets before reporting damage
screen_write_redraw_cb() passed wp->xoff/wp->yoff straight through as u_int to redraw_damage_window(). Both are genuinely signed and can be negative for a floating pane positioned partly off the window's left or top edge (layout_floating_args_parse() explicitly allows -X/-Y down to -sx/-sy). A negative value wraps to a huge u_int, redraw_damage_window()'s first bounds check (x >= w->sx) rejects the whole rectangle, and nothing gets redrawn - not even the pane's visible portion. This is broader than just the alternate-screen-exit case that first surfaced it: screen_write_pane_is_obscured() routes any scrolling output in such a pane through this same callback, so a partly off-screen floating pane lost every scroll repaint, not just its post-alternate-screen one. Fixed at the call site (matching the existing correct reference pattern in window_pane_damage_floating(), window.c): compute in signed int, clip negative offsets to the window's own origin and shrink the corresponding size to match, then convert to u_int only once the rectangle is known to be sane. Left redraw_damage_window()'s own signature alone - it has a second caller with an inclusive-bounds convention that a signature change would need to reconcile, and the bug is specific to this call site not clamping before converting. regress/floating-pane-offscreen-alternate-redraw.sh creates a floating pane with -X -5 (partly off the left edge), cycles it through the alternate screen, and checks an attached client's own received bytes (not capture-pane, which reads the grid directly and would pass regardless of whether the client was ever told to redraw) show the primary screen's content correctly restored in the pane's visible columns. Verified failing 3/3 against the pre-fix code and passing 5/5 against the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
111
regress/floating-pane-offscreen-alternate-redraw.sh
Executable file
111
regress/floating-pane-offscreen-alternate-redraw.sh
Executable file
@@ -0,0 +1,111 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 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
|
||||
LC_ALL=C.UTF-8
|
||||
export PATH TERM LC_ALL
|
||||
|
||||
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
|
||||
|
||||
DIR=$(mktemp -d) || exit 1
|
||||
INNER="$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null"
|
||||
OUTER="$TEST_TMUX -Loffscreen-outer-$$ -f/dev/null"
|
||||
EMITTER=$DIR/emitter.pl
|
||||
CAPTURE=$DIR/capture
|
||||
|
||||
fail()
|
||||
{
|
||||
echo "$*" >&2
|
||||
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cleanup()
|
||||
{
|
||||
$OUTER kill-server 2>/dev/null
|
||||
$INNER kill-server 2>/dev/null
|
||||
rm -rf "$DIR"
|
||||
}
|
||||
trap cleanup 0 1 15
|
||||
|
||||
wait_outer_has()
|
||||
{
|
||||
marker=$1
|
||||
i=0
|
||||
while [ "$i" -lt 50 ]; do
|
||||
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
|
||||
grep -q "$marker" "$CAPTURE" && return 0
|
||||
sleep 0.1
|
||||
i=$((i + 1))
|
||||
done
|
||||
fail "outer client did not show $marker"
|
||||
}
|
||||
|
||||
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;
|
||||
print "\e[1;1H", 'A' x 15;
|
||||
sleep 2;
|
||||
print "\e[?1049h";
|
||||
print "\e[1;1H", 'B' x 15;
|
||||
sleep 2;
|
||||
print "\e[?1049l";
|
||||
sleep 100;
|
||||
PERL
|
||||
|
||||
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
|
||||
$INNER set-option -g status off || exit 1
|
||||
$INNER set-option -g window-size manual || exit 1
|
||||
|
||||
# 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
|
||||
$OUTER set-option -g default-terminal screen-256color || exit 1
|
||||
$OUTER respawn-pane -k -t outer:0.0 \
|
||||
"$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null attach-session -t inner" ||
|
||||
exit 1
|
||||
|
||||
wait_outer_has AAAAA
|
||||
wait_outer_has BBBBB
|
||||
wait_visible_restored
|
||||
|
||||
exit 0
|
||||
@@ -124,16 +124,35 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy)
|
||||
* 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.
|
||||
* 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)
|
||||
{
|
||||
struct window_pane *wp = ttyctx->arg;
|
||||
int x0, y0, x1, y1;
|
||||
|
||||
if (wp == NULL)
|
||||
return;
|
||||
redraw_damage_window(wp->window, wp->xoff, wp->yoff + py, wp->sx, ny);
|
||||
|
||||
x0 = wp->xoff;
|
||||
y0 = wp->yoff + (int)py;
|
||||
x1 = x0 + (int)wp->sx;
|
||||
y1 = y0 + (int)ny;
|
||||
if (x0 < 0)
|
||||
x0 = 0;
|
||||
if (y0 < 0)
|
||||
y0 = 0;
|
||||
if (x1 <= x0 || y1 <= y0)
|
||||
return;
|
||||
redraw_damage_window(wp->window, (u_int)x0, (u_int)y0,
|
||||
(u_int)(x1 - x0), (u_int)(y1 - y0));
|
||||
}
|
||||
|
||||
/* Update context for client. */
|
||||
|
||||
Reference in New Issue
Block a user