From 04dbc6d89b5421af7babcbac57914b8ea55938d7 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 22 Sep 2026 16:13:53 +0100 Subject: [PATCH 1/2] screen-redraw: don't let one client's pane title leak to another redraw_damage_refresh_status() force-regenerates a pane's border-status title when a damage rectangle touches it (window_make_pane_status()'s own content-diff cache can't tell "physically disturbed" from "never changed"), guarded by the per-pane PANE_NEWSTATUS flag. But the rendered content is per-client - window_make_pane_status() formats pane-border-format using the requesting client's own context, so fields like #{client_name} genuinely differ per client - while 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 ran first rendered its own text and set the flag; every other client's damage pass in that tick, or any later one, since nothing else clears the flag on this path, found it already set and skipped rendering - silently reusing the first client's text. Fix: a per-pass serial (redraw_status_serial, bumped once per redraw_client_damage() call - one call is one client's one redraw pass) instead of a sticky flag. Deduplicates repeated calls within the same client's same pass exactly as before, but forces a fresh, correctly-client-formatted render whenever a different client or a later pass touches the same pane's status - without needing to track and later invalidate a client pointer with its own lifetime. Also added a permanent log_debug() line for the actual regenerate, since this class of bug (a damage pass silently trusting stale per-pane state that should have been per-client) is otherwise invisible to any external capture: an unrelated periodic per-client status refresh reliably repaints each client's title correctly again within the very same tick, before anything is ever flushed to either terminal, so the wrong content this bug produces was never actually observable in a capture-pane-based test - confirmed by direct instrumentation while building the regression test below, which is exactly why the fix is verified via this log rather than a capture. regress/floating-pane-status-cross-client.sh attaches two clients to one session, each with its own pane-border-format referencing #{client_name}, and triggers a damage-only palette update (OSC 4) in a tiled pane whose geometry overlaps a floating pane's own border-status row - carefully picked so the trigger has no side effect that would otherwise force a normal, already-correct, full per-client status re-render in the same pass, which would mask the result either way. Verified failing 3/3 against the pre-fix code (neither client's damage pass regenerates at all - both silently reuse whatever a much earlier full redraw left in the shared buffer) and passing 5/5 against the fix. Co-Authored-By: Claude Sonnet 5 --- regress/floating-pane-status-cross-client.sh | 117 +++++++++++++++++++ screen-redraw.c | 24 +++- tmux.h | 1 + 3 files changed, 141 insertions(+), 1 deletion(-) create mode 100755 regress/floating-pane-status-cross-client.sh diff --git a/regress/floating-pane-status-cross-client.sh b/regress/floating-pane-status-cross-client.sh new file mode 100755 index 000000000..ee3f19259 --- /dev/null +++ b/regress/floating-pane-status-cross-client.sh @@ -0,0 +1,117 @@ +#!/bin/sh + +# 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 +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +cd "$DIR" || exit 1 +INNER="$TEST_TMUX -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 + exit 1 +} + +cleanup() +{ + $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 + +BASEEMITTER=$DIR/base-emitter.pl +cat >"$BASEEMITTER" <<'PERL' +use strict; +use warnings; + +$| = 1; +my $line = ; +print "\e]4;1;rgb:11/22/33\e\\"; +sleep 100; +PERL + +$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 + +$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 +NAME1=$($INNER list-clients -F '#{client_name}') || exit 1 + +$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 diff --git a/screen-redraw.c b/screen-redraw.c index 40beda5de..31b760fae 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -228,6 +228,12 @@ struct redraw_build_cell { static struct redraw_build_cell *redraw_cells; static size_t redraw_ncells; +/* + * Bumped once per redraw_client_damage() call (one client's one redraw + * pass) - see redraw_damage_refresh_status(). + */ +static u_int redraw_status_serial; + /* Context for building the scene. */ struct redraw_build_ctx { struct client *c; @@ -2031,6 +2037,17 @@ redraw_pane_scrollbar(struct client *c, struct window_pane *wp) * 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, @@ -2039,13 +2056,17 @@ redraw_damage_refresh_status(struct redraw_draw_ctx *dctx, struct redraw_span *first; u_int width; - if (wp->flags & PANE_NEWSTATUS) + if ((wp->flags & PANE_NEWSTATUS) && + wp->status_serial == redraw_status_serial) return; width = redraw_pane_status_width(dctx, wp, &first); 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; } /* @@ -2231,6 +2252,7 @@ redraw_client_damage(struct client *c) if (TAILQ_EMPTY(&w->damage)) return; + redraw_status_serial++; scene = redraw_get_scene(c); if (scene == NULL) diff --git a/tmux.h b/tmux.h index 3097bfdbb..a8e142468 100644 --- a/tmux.h +++ b/tmux.h @@ -1401,6 +1401,7 @@ struct window_pane { struct screen base; struct screen status_screen; + u_int status_serial; TAILQ_HEAD(, window_mode_entry) modes; From d4adce7e004bf5855aad7f68dbf89eb913ddc96c Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Tue, 22 Sep 2026 17:16:25 +0100 Subject: [PATCH 2/2] screen-redraw: use the prompt's own screen to clip its damage range redraw_damage_draw_pane_prompt() recomposes a pane's separately rendered prompt (wp->prompt) over a damaged sub-range of a REDRAW_SPAN_PANE span, reusing the (x, n) range that redraw_client_damage() already clipped and grew against the pane's own *content* grid (wp->screen) via redraw_damage_grow_span_clip(). That growing exists specifically to avoid splitting a wide character at the range's edge - but the prompt is drawn into its own, freshly allocated one-line screen with no relationship to the content grid, so a range that's clean (or correctly grown) for the content can still land mid-character in the prompt's own grid. This is invisible whenever the pane's content is plain ASCII: the content grid has no padding cells to find, so redraw_damage_grow_span_clip() never grows the range at all, and the raw geometric range - however it landed - is passed straight through to the prompt. If that range's end lands right after a base cell whose padding half falls just outside it, tty_draw_line() has no room left for that cell's second column (tty_draw_line_get_empty()'s gc->data.width > nx check) and blanks it entirely, even though the pane's own content never needed the fix at all. Reproduced with a floating pane's CJK prompt straddling the boundary between two tiled panes underneath it: a palette change (OSC 4) in one of the tiled panes triggers a redraw of its own rectangle, which is occluded by the floating pane but still geometrically overlaps its prompt row, recomposing a partial range of the prompt that cuts through a character. Fix: extract the grid-probing tail of redraw_span_cell_is_padding() into redraw_screen_cell_is_padding(), usable against any screen, and add redraw_damage_grow_screen_clip() - the same left/right one-step growth as redraw_damage_grow_span_clip(), but against an explicit screen and column origin. redraw_damage_draw_pane_prompt() now re-derives its own (x0, x1) range from the caller's (x, n) by growing it against the prompt's own screen before drawing, clamped to the span so it can't bleed into a neighbouring one. New regress/floating-pane-prompt-wide-character.sh constructs the exact tiled-pane-boundary scenario above and checks the CJK prompt text is intact after the trigger. Verified failing 3/3 against the pre-fix code (a character is blanked) and passing 5/5 standalone plus 2/2 in the full regress suite (twice, since this touches the same damage-composition path as every other redraw) against the fix, with no other tests newly broken (prompt-words-history.sh and the untracked image-support scratch tests are pre-existing, unrelated failures). Co-Authored-By: Claude Sonnet 5 --- .../floating-pane-prompt-wide-character.sh | 179 ++++++++++++++++++ screen-redraw.c | 59 +++++- 2 files changed, 229 insertions(+), 9 deletions(-) create mode 100755 regress/floating-pane-prompt-wide-character.sh diff --git a/regress/floating-pane-prompt-wide-character.sh b/regress/floating-pane-prompt-wide-character.sh new file mode 100755 index 000000000..cdd892a54 --- /dev/null +++ b/regress/floating-pane-prompt-wide-character.sh @@ -0,0 +1,179 @@ +#!/bin/sh + +# A damage rectangle's clip range is grown to avoid splitting a wide +# character, but redraw_damage_grow_span_clip() (screen-redraw.c) only ever +# checks the span's own pane *content* grid (wp->screen) for that. For a +# REDRAW_SPAN_PANE span, that same range is then also handed to +# redraw_damage_draw_pane_prompt() to recompose the pane's separately +# rendered prompt (wp->prompt, e.g. from "command-prompt -P") over the +# damaged sub-range - but the prompt is drawn into its own, freshly +# allocated one-line screen, unrelated to the pane's content grid, so a +# range grown (or left ungrown) against the content is not necessarily +# grown correctly for the prompt's own wide characters. +# +# This is invisible when the pane's own content is plain ASCII (as here): +# redraw_damage_grow_span_clip() never finds anything to grow against, so +# the raw, ungrown geometric range is passed straight through to the +# prompt - and if that range's edge lands mid-character in the *prompt's* +# grid, tty_draw_line() clears the character it cuts through +# (tty_draw_line_get_empty()'s gc->data.width > nx check, for a trailing +# base cell with no room left for its padding half). +# +# The trigger is a palette change (OSC 4) in a tiled pane that is one half +# of a vertical split running the full height of the window - occluded +# under the floating pane, but still geometrically triggering a redraw of +# its own rectangle. Positioned so the split boundary falls inside the +# floating pane's own CJK prompt, this reproduces exactly Codex's report: +# "a floating pane containing a CJK prompt across a tiled-pane boundary - +# a palette update in the tiled pane blanks a prompt character." + +PATH=/bin:/usr/bin +TERM=screen +LC_ALL=C.UTF-8 +export PATH TERM LC_ALL + +[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux) + +DIR=$(mktemp -d) || exit 1 +cd "$DIR" || exit 1 +INNER="$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null" +OUTER="$TEST_TMUX -Lpromptwide-outer-$$ -f/dev/null" +EMITTER=$DIR/emitter.pl +CAPTURE=$DIR/capture +FLOAT= + +fail() +{ + echo "$*" >&2 + [ -s "$CAPTURE" ] && cat "$CAPTURE" >&2 + exit 1 +} + +cleanup() +{ + $OUTER kill-server 2>/dev/null + $INNER kill-server 2>/dev/null + cd / + rm -rf "$DIR" +} +trap cleanup 0 1 15 + +wait_for_client() +{ + i=0 + while [ "$i" -lt 50 ]; do + CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null) + [ -n "$CLIENT" ] && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "inner client did not attach" +} + +wait_outer_has() +{ + marker=$1 + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + grep -q "$marker" "$CAPTURE" && return 0 + sleep 0.1 + i=$((i + 1)) + done + fail "outer client did not show $marker" +} + +wait_prompt_row_intact() +{ + i=0 + while [ "$i" -lt 50 ]; do + $OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true + line=$(sed -n "${PROMPTROW}p" "$CAPTURE") + case $line in + *"$PROMPTTEXT"*) return 0 ;; + esac + sleep 0.1 + i=$((i + 1)) + done + fail "the CJK prompt was not intact after the palette-triggered damage - got: $line" +} + +cat >"$EMITTER" <<'PERL' +use strict; +use warnings; + +$| = 1; +my $line = ; +print "\e]4;1;rgb:11/22/33\e\\"; +sleep 100; +PERL + +$INNER new-session -d -s inner -x 60 -y 12 "perl '$EMITTER'" || exit 1 +$INNER set-option -g status off || exit 1 +$INNER set-option -g window-size manual || exit 1 +LEFT=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1 + +# Split so the boundary between the two tiled panes falls at column 17 - +# used below to pick a floating-pane column that lands the boundary +# mid-character inside the prompt. +RIGHT=$($INNER split-window -t inner -h -l 43 -PF '#{pane_id}' \ + 'sleep 100') || exit 1 +RX=$($INNER display-message -p -t "$RIGHT" '#{pane_left}') || exit 1 + +# Create the floating pane, then check its actual resulting position (the +# border-framing offset added to -X is not something to hand-compute). Try +# adjacent starting columns until the split boundary lands on an odd +# (padding-half) column of the prompt's own numbering, and within the +# prompt's 12-column width. +startx=10 +tries=0 +while [ "$tries" -lt 4 ]; do + [ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null + FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 24 -y 5 -X "$startx" \ + -Y 2 "sh -c 'i=0; while [ \$i -lt 10 ]; do \ +printf AAAAAAAAAAAAAAAAAAAAAA\\\\n; i=\$((i+1)); done; sleep 100'") || + exit 1 + sleep 0.2 + X1=$($INNER display-message -p -t "$FLOAT" '#{pane_left}') + Y1=$($INNER display-message -p -t "$FLOAT" '#{pane_top}') + H1=$($INNER display-message -p -t "$FLOAT" '#{pane_height}') + local=$((RX - X1 - 1)) + if [ "$local" -ge 1 ] && [ "$local" -le 11 ] && + [ $((local % 2)) -eq 1 ]; then + break + fi + startx=$((startx + 1)) + tries=$((tries + 1)) +done +local=$((RX - X1 - 1)) +[ "$local" -ge 1 ] && [ "$local" -le 11 ] && [ $((local % 2)) -eq 1 ] || + fail "could not find bad-parity starting column" + +$OUTER new-session -d -s outer -x 60 -y 12 'sleep 100' || exit 1 +$OUTER set-option -g status off || exit 1 +$OUTER set-option -g window-size manual || exit 1 +$OUTER set-option -g default-terminal screen-256color || exit 1 +$OUTER respawn-pane -k -t outer:0.0 \ + "$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null attach-session -t inner" || + exit 1 + +wait_for_client +wait_outer_has AAAAAAAAAAAAAAAAAAAAAA +CLIENT=$($INNER list-clients -F '#{client_name}') || exit 1 + +$INNER select-pane -t "$FLOAT" || exit 1 +PROMPTTEXT=$(printf '\344\270\255' | perl -CSD -ne 'print $_ x 6') +$INNER command-prompt -b -P -t "$CLIENT" -p "$PROMPTTEXT" \ + 'display-message -- %1' || exit 1 +wait_outer_has "$PROMPTTEXT" + +PROMPTROW=$((Y1 + H1)) + +# Trigger the damage: unblock the emitter so it fires the palette change in +# the left tiled pane, which is occluded under (but geometrically overlaps) +# the floating pane's prompt row. +$INNER send-keys -t "$LEFT" Enter || exit 1 + +wait_prompt_row_intact + +exit 0 diff --git a/screen-redraw.c b/screen-redraw.c index 31b760fae..d8f4bc0fa 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -2069,6 +2069,18 @@ redraw_damage_refresh_status(struct redraw_draw_ctx *dctx, wp->status_serial = redraw_status_serial; } +/* 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 @@ -2092,7 +2104,6 @@ static int redraw_span_cell_is_padding(struct redraw_span *span, u_int x) { struct screen *s; - struct grid_cell gc; u_int px, py; switch (span->data.type) { @@ -2114,10 +2125,7 @@ redraw_span_cell_is_padding(struct redraw_span *span, u_int x) default: return (1); } - if (px >= screen_size_x(s)) - return (0); - grid_view_get_cell(s->grid, px, py, &gc); - return ((gc.flags & GRID_FLAG_PADDING) != 0); + return (redraw_screen_cell_is_padding(s, px, py)); } /* @@ -2144,6 +2152,26 @@ redraw_damage_grow_span_clip(struct redraw_span *span, u_int *xp, u_int *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, @@ -2153,7 +2181,7 @@ redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx, struct window_pane *wp = span->data.p.wp; struct tty *tty = &scene->c->tty; struct screen screen; - u_int px, width, prompt_y; + u_int px, width, prompt_y, x0, x1; if (wp->prompt == NULL || wp->sx == 0 || wp->sy == 0) return; @@ -2165,12 +2193,25 @@ redraw_damage_draw_pane_prompt(struct redraw_draw_ctx *dctx, return; redraw_make_pane_prompt(wp, &screen); - px = span->data.p.px + (x - span->x); + + /* + * 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 = n; + width = x1 - x0; if (width > screen_size_x(&screen) - px) width = screen_size_x(&screen) - px; - tty_draw_line(tty, &screen, px, 0, width, x, y, NULL); + tty_draw_line(tty, &screen, px, 0, width, x0, y, NULL); } screen_free(&screen); }