Compare commits

..
Author SHA1 Message Date
Michael GrantandClaude Opus 5.5 386799eef8 image: keep Kitty placements intact when a floating pane is dragged
Dragging a floating pane over a Kitty image left two kinds of damage.

A redraw marked every placement touching its area as stale and deleted it
whole, but only replaced the part inside the area, so the rest of the
image vanished and left holes behind the moved pane. kitty_redraw_start()
now places the parts outside the area again before the old placement is
deleted.

redraw_draw_damage_rect() only cleared placements in cells owned by a
pane, so a placement where the floating pane used to be, on cells now
belonging to its border, was never deleted and stayed drawn over it.
Clear placements under every span type except the status line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 18:37:20 +01:00
Michael Grant 3cdb7a9216 tty-features: grant imagescroll to kitty by default
Verified today: a full-width pane in real Kitty (no pane-scrollbars, no
split, no other panes) correctly preserves a placed image through
sustained native scrolling (scroll-to-bottom, copy mode up/down,
mouse-wheel scrolling) - 21 scroll events produced only 3 full
redraws, all correlating with mode transitions rather than the scrolls
themselves, with zero fallback to the manual redraw path throughout.

margins is deliberately not added - confirmed live against kitty's own
vt-parser.c that it has no DECSLRM/DECLRMM support at all (CSI Ps r is
DECSTBM only; parameterized CSI s falls through to "Unknown CSI s
sequence"), and forcing margins on produced observable corruption when
scrolling a non-full-width pane, since kitty silently ignores the
column restriction tmux believes is protecting the rest of the screen.
2026-09-23 18:24:34 +01:00
Michael Grant 1c9a4403d0 screen-redraw: fix warnings when building without --enable-images
skip_images (redraw_draw_pane_span()) and w (redraw_client_damage_rect())
are only referenced inside #ifdef ENABLE_IMAGES blocks, so both go
unused when images are disabled; mark them accordingly.
redraw_damage_window_pane_status() only exists to support one
ENABLE_IMAGES-guarded call site and has no other purpose, so wrap its
definition in the same #ifdef instead.
2026-09-23 18:24:24 +01:00
Michael Grant bbb2dd9454 image: never let a Kitty image drop to zero placements mid-redraw
Every redraw that replaces an image's placements (a pane revealed after
an overlapping floating pane closes, a full window redraw, etc.)
deleted all of an image's existing placements before creating their
replacements. That briefly left the image with none at all - and some
Kitty implementations free an image's underlying pixel data once it
has no placements left, so the replacement placements just placed
would then reference already-discarded data and render as nothing: an
empty hole where the image used to be. Not reproducible with SIXEL,
which has no separate placement/data lifecycle to race.

kitty_redraw_start() now only marks placements intersecting a redraw
area as stale instead of deleting them immediately; a new
kitty_redraw_finish() (called via image_redraw_finish() after a
redraw's replacement placements have already been created) deletes
whatever is still marked. The image now always has at least one live
placement throughout the transition.
2026-09-23 16:35:28 +01:00
Michael Grant 54634fad24 image: fix scroll-triggered erasure and Kitty placement leaks
The scroll-damage redraw path (redraw_draw_damage_rect()) always ran
its text-only pass even when imagescroll's trust skipped recompositing
the image on top, painting blank cells directly over image content
with nothing left to correct it. redraw_draw_pane_span() now consults
a new image_grid_next_span() to exclude any column range an image
occupies from that text pass.

The same path also never called the existing image_redraw_start()/
kitty_redraw_start() placement cleanup that the full-pane redraw path
already had, so every scroll of a Kitty-protocol image added a new set
of placements without deleting the stale ones, which the terminal then
rendered as an overlapping smudge. Added the missing call, skipped
when imagescroll is trusting the terminal not to need replacing.
2026-09-23 16:12:05 +01:00
Michael GrantandClaude Sonnet 5 79e3ef3c09 image: convert image-region-scrolling from an option to a terminal feature
Whether a terminal moves SIXEL/Kitty image content along with the rest of
a scrolling region turns out not to correlate with DECSLRM (margins)
support at all - direct testing (both a real -vv log of an actual native
scroll, and a raw, outside-tmux escape-sequence test mirroring the one
used to confirm Windows Terminal) showed mintty correctly scrolls text
within a margin-bounded region but drops sixel content placed there,
while WezTerm and Windows Terminal move it correctly. Since this is a
per-terminal capability with no relation to any other capability tmux
already tracks, it belongs in tty-features.c's table like margins/sixel/
kitty, not as a general server option.

New "imagescroll" terminal feature (tty-features.c), backed by a new
TERM_IMAGESCROLL flag (tmux.h). image_backend_flags() (image.c) now
checks tty->term->flags & TERM_IMAGESCROLL directly instead of the
removed image-region-scrolling option (options-table.c).

Flips the previous default while doing so: the option defaulted to on
(trust the terminal); nothing is granted this new feature by default.
Windows Terminal cannot be identified via XTVERSION at all (see the
WT_SESSION revert two commits back), so it gets the safe default here
too, same as any other unconfirmed terminal. Only WezTerm and ghostty are
granted imagescroll, both confirmed by direct testing - image content,
not just text, observed scrolling correctly within a margin-bounded
region on both.

regress/image-sixel-region-scroll.sh and image-kitty-region-scroll.sh
updated to grant/revoke the feature via terminal-features instead of
toggling the removed option; same two-phase check as before (granted - no
retransmit; not granted - always retransmit).

Verified: build clean; old option name now rejected ("invalid option");
new feature name accepted via terminal-features; both updated regress
tests pass (3x each); tty-margins-scrollbar.sh (unrelated but touches the
same tty-features.c) unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 09:59:55 +01:00
Michael Grant d176b400d3 Merge branch 'redraw-damage-rectangles' into 4902-image-support 2026-09-23 09:03:31 +01:00
Michael Grant 3a17365322 Merge branch 'margins-terminal-detection' into redraw-damage-rectangles 2026-09-23 09:02:30 +01:00
Michael GrantandClaude Sonnet 5 6933bc417c tty: drop WT_SESSION-based Windows Terminal detection
nicm is opposed to identifying a terminal via an environment variable, so
remove the WindowsTerminal tty_default_features() entry and the
WT_SESSION check in tty_term_create() entirely, pending some other
accepted identification mechanism for Windows Terminal (it cannot be
identified via the existing XTVERSION path at all - see the previous
commit's now-removed comment for why).

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 09:02:13 +01:00
Michael GrantandClaude Sonnet 5 f44699a18c Merge branch 'redraw-damage-rectangles' into 4902-image-support
Brings in the WezTerm/ghostty/Windows Terminal DECSLRM (margins) coverage
from margins-terminal-detection (PR #5630), via redraw-damage-rectangles.

Two conflicts, both simple bit-flag/list collisions from independent work
on both sides adding something at the same slot:
- tmux.h: this branch's own TERM_KITTY/TERM_IMAGE_QUADRANTS/
  TERM_IMAGE_SEXTANTS (0x100/0x200/0x400) collided with upstream's new
  TERM_NOREPLACE, also assigned 0x100. Kept this branch's existing values
  (already used throughout image.c/tty-features.c) and moved
  TERM_NOREPLACE to the next free bit, 0x800 - confirmed no other code
  references the literal 0x100 value for TERM_NOREPLACE, only the symbol.
- tty-features.c: this branch's own WezTerm entry (added "sixel") and the
  incoming margins-terminal-detection change (added "margins") both
  touched the same line - combined both.

Verified: build clean with --enable-images (image_support confirmed 1);
this branch's own image-kitty-region-scroll.sh/image-sixel-region-scroll.sh
plus this session's other prompt/cross-client/margins tests all pass (3x
each); full regress suite run pending/clean per standard practice for
this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 08:59:12 +01:00
Michael GrantandClaude Sonnet 5 238067acbf Merge branch 'margins-terminal-detection' into redraw-damage-rectangles
Brings in the WezTerm/ghostty/Windows Terminal DECSLRM (margins) coverage
from margins-terminal-detection (PR #5630), plus whatever independent
upstream progress that branch's master base had accumulated since this
branch last merged from master.

Conflict in screen-redraw.c's redraw_draw_pane_prompt(): this branch's own
earlier refactor (extracting redraw_make_pane_prompt(), reused by both the
normal redraw path and the Finding-2 damage-composition fix) collided with
an independent upstream fix on master (nicm, "Get x and y the right way
round, fixes crash when opening a mode prompt when the client is offset
into the window") that swapped tty_draw_line()'s px/py argument order in
this same function - this branch had diverged before that fix landed and
still had the pre-fix (buggy) order. Resolved by keeping this branch's
own refactor with the corrected argument order, verified against
tty_draw_line()'s signature (px, py - the prompt's screen is always
exactly one row, so py must be 0 and px must be the horizontal offset;
the pre-fix order passed the offset as the row instead, matching the
described crash). The other, structurally identical call site added this
session for Finding 2 (redraw_damage_draw_pane_prompt()) already had the
correct order and needed no change.

Verified: build clean; the new tty-margins-wt-session.sh and this
session's existing wide-character/prompt/cross-client tests all pass
(3x each); full regress suite passes clean twice, with only the same
pre-existing, unrelated failures already characterized this session
(prompt-words-history.sh, and untracked image-*-noflash scratch tests
from unrelated work, absent from this branch's own tracked suite).

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

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

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

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

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

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-23 07:37:06 +01:00
Michael Grant bfda809f98 Merge branch 'redraw-damage-rectangles' into 4902-image-support 2026-09-23 05:53:24 +01:00
Michael GrantandClaude Sonnet 5 53e2de7ed4 image: don't assume Kitty terminals move placements on scroll either
Reported: on Windows Terminal, a Kitty image placement is deleted (not
just left in the wrong place) by a plain newline-driven scroll-up or by
reverse-index scroll-down, but survives a delete-line/insert-line-based
scroll simulation of the same movement.

Root cause: image_backend_kitty unconditionally included
IMAGE_BACKEND_SCROLLS (image.c), so redraw_client_damage_rect()
(screen-redraw.c) always trusted that any terminal claiming the Kitty
graphics protocol also correctly relocates placements across a native
CSR-region scroll, and skipped re-sending the placement after one. There
was no way to turn this off, unlike the identical, already-acknowledged
problem for SIXEL - sixel-region-scrolling (added this branch, not yet
released anywhere) already carries the comment "there is no way to ask a
terminal whether it moves image content along with a scrolling region."
Windows Terminal's Kitty support does not move it - it drops it. DL/IL
never go through this scroll-specific "trust the terminal" shortcut,
which is why they were unaffected, matching the report.

Since the SIXEL option is new on this branch and unreleased, generalise
it instead of adding a near-duplicate: sixel-region-scrolling becomes
image-region-scrolling (still a server-scope flag, still on by default,
matching prior behaviour for terminals that do handle this correctly),
and image_backend_flags() now gates IMAGE_BACKEND_SCROLLS for both the
SIXEL and Kitty backends on it, rather than hardcoding it for Kitty.

Separately, tty_cmd_linefeed() was missing the redraw_image_scroll_result()
calls that tty_cmd_reverseindex()/tty_cmd_scrollup()/tty_cmd_scrolldown()
all already have on both their native and tty_redraw_region() fallback
branches - a plain newline-driven scroll never recorded whether the native
path was actually used, leaving the per-client scroll-result tracking
that redraw_client_damage_rect() reads keyed off whatever an unrelated
prior operation last left it at. Added the same two calls linefeed was
missing, matching the other three functions exactly.

regress/image-kitty-region-scroll.sh mirrors the existing
image-sixel-region-scroll.sh (renamed from its old option name here) for
the Kitty side: transmits a real Kitty placement, scrolls it with the
option on and off, and checks whether tmux's own re-placement command
(kitty_draw_rect()'s "a=p") gets reissued - proving tmux's decision to
skip or redraw is wired up correctly for Kitty too, not that any
particular terminal renders the result correctly (that needs a human, on
real terminals, per the existing SIXEL test's own caveat).

Both new/updated regress tests require --enable-images (this tree's build
was not actually configured with it - #{image_support} was silently 0 and
every image-prefixed test, including the four pre-existing ones already
in this tree, was trivially skipping rather than running; reconfigured
with --enable-images for this work, which also required libpng/zlib).
Verified: image-kitty-region-scroll.sh fails 3/3 against the pre-fix code
(the option does not exist) and passes 5/5 standalone; both region-scroll
tests pass in the full regress suite, run twice (this touches the scroll
path shared by every image-capable pane), with only the two pre-existing,
unrelated failures (image-support.sh, prompt-words-history.sh) and one
one-off flake (cursor-test1.sh, passed 3/3 standalone) - image-support.sh
verified to fail identically before and after this change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 20:11:51 +01:00
Michael GrantandClaude Sonnet 5 d4adce7e00 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 <noreply@anthropic.com>
2026-09-22 17:16:25 +01:00
Michael GrantandClaude Sonnet 5 04dbc6d89b 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 <noreply@anthropic.com>
2026-09-22 16:13:53 +01:00
Michael Grant bdd781770d Merge branch 'redraw-damage-rectangles' into 4902-image-support
# Conflicts:
#	server-client.c
2026-09-22 11:06:22 +01:00
Michael GrantandClaude Sonnet 5 104a0cee99 server-client: fix damage being composed twice per redraw pass
`(~c->flags & CLIENT_ALLREDRAWFLAGS)` - 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(), called from
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: redraw_client_damage(c) ran here, then ran again a few lines
later at the CLIENT_ALLREDRAWFLAGS block for the exact same
rectangles. Confirmed via the server's own -vv log: a 6-step drag
produced 12 "composing damage" lines in matched pairs (identical
position and size, milliseconds apart) with no fix, 6 with it.

No memory-safety issue - redraw_client_damage() only reads w->damage,
never frees it - just wasted work rendering the same rectangles twice
per pass.

Fix: `(c->flags & CLIENT_ALLREDRAWFLAGS) == 0`, matching the comment's
actual intent.

regress/floating-pane-drag-no-double-composite.sh drags a floating
pane and asserts no two consecutive "composing damage" log lines share
the same position and size (comparing both together, since distinct
drag steps commonly share the same rectangle size and only the
position differs). Verified failing 3/3 against the pre-fix code (all
6 rectangles doubled each run) and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:09:28 +01:00
Michael GrantandClaude Sonnet 5 a7f74730d7 server-client: don't let a drag's own sync-start defer its redraw
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 - deliberately, so a fast-
path write and any later damage-composed correction land in one atomic
terminal update instead of two visible frames. But
server_client_check_redraw() later in the same pass checks
EVBUFFER_LENGTH(tty->out) != 0 to decide whether to defer this pass's
redraw, and nothing drains tty->out in between (the actual write
happens later, via libevent) - so the frame-open sequence just queued
(8 bytes: "\033[?2026h" on a synchronized-output-capable terminal)
makes that check see "outstanding output" and defer against itself,
escalating the drag's damage to a full-window redraw on every single
motion event. Confirmed via the server's own -vv log: a 6-step drag
produced five "redraw deferred (8 left)" lines, one per motion event,
with no fix.

Fix: record how much was already queued at the instant the sync frame
opened (tty->sync_offset), and have the redraw check discount
anything queued after that point - it's already part of the frame
this pass is committed to flushing, not a reason to defer. If the
buffer was genuinely non-empty before the frame opened, sync_offset
holds that real backlog and deferral still happens correctly.

regress/floating-pane-drag-sync-no-self-defer.sh drags a floating pane
on a synchronized-output-capable terminal and asserts the server log
never shows the self-inflicted 8-byte deferral. Verified failing 3/3
against the pre-fix code (5 occurrences per run, matching the manual
-vv repro) and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:06:09 +01:00
Michael GrantandClaude Sonnet 5 4a1e445b38 window-copy: redraw pane styles on a scrollbar-driven focus change
window_copy_scroll() (called from both scrollbar-slider-drag paths -
window_copy_cmd_scroll_to_mouse() and copy-mode -S) calls
window_set_active_pane() to switch focus to the dragged pane, but was
the one caller in the whole codebase that did this without pairing it
with window_redraw_active_switch() first, and without falling back to
a full server_redraw_window() either. Every other window_set_active_
pane() caller does one or the other.

This used to be harmless because window_set_active_pane() itself did
an unconditional full redraw on every active-pane change - narrowing
that to borders/status-only for the non-zoomed case (this branch) made
pane *body* colours only repaint when something sets PANE_REDRAW,
which is exactly what window_redraw_active_switch() does by comparing
cached window-style/window-active-style colours. Without it, dragging
an inactive pane's scrollbar slider changes which pane is active
(borders and status update immediately) while both panes keep their
stale body colours until an unrelated redraw happens to touch them.

Fix: call window_redraw_active_switch() immediately before window_set_
active_pane(), matching the established pattern (e.g.
cmd-resize-pane.c's mouse-drag handler).

regress/window-copy-scrollbar-drag-focus-style.sh sets clearly
distinguishable window-active-style/window-style backgrounds, drags
the *inactive* pane's scrollbar slider, and checks - via an attached
client's own received bytes, since window-style is applied during
redraw composition rather than stored in the grid, so capture-pane
alone would not reflect it - that the newly active pane immediately
shows the active-style colour. Verified failing 3/3 against the
pre-fix code (showing both panes with swapped/stale colours, exactly
as reported) and passing 5/5 against the fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 10:01:52 +01:00
Michael GrantandClaude Sonnet 5 9a3aa26a33 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>
2026-09-22 09:53:32 +01:00
Michael GrantandClaude Sonnet 5 1ce64bd087 screen-redraw: fix wide characters getting blanked on the right clip edge
redraw_damage_grow_span_clip() guards its left-edge growth against
walking onto an unrelated wide character's base cell (via
redraw_span_left_grow_ok(), commit 6f65c318), but the right edge was
left growing unconditionally, on the reasoning that "tty_draw_line()
already draws a wide character in full even when the requested range
clips off its trailing padding half" - that reasoning was wrong.

When the right edge lands cleanly on a fresh character's base cell (a
character fully outside the range), growing right pulls in only that
base column. tty_draw_line()'s tty_draw_line_get_empty() then sees
gc->data.width > nx for the truncated remainder and treats it as an
empty cell to clear via tty_draw_line_clear() - a different code path
than the leading-padding-clear the left-edge fix reasoned about, but
exactly as destructive: it blanks a character that was never inside
the damage rectangle at all.

The padding check the left-edge fix added doesn't actually care about
direction - "is the cell at this scene x-coordinate a padding cell" is
the same question whether asked of a range's start or its end - so
renamed redraw_span_left_grow_ok() to redraw_span_cell_is_padding()
and apply it symmetrically to both edges.

regress/floating-pane-drag-wide-character-right.sh mirrors the
existing left-edge test for this edge: constructs the exact column
parity needed (the vacated rectangle's right edge landing on a base
cell) deterministically rather than relying on luck, verified failing
3/3 against the pre-fix code and passing 5/5 against the fix. The
existing left-edge test continues to pass unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 09:47:21 +01:00
Michael Grant 7ff30a2198 Merge branch 'redraw-damage-rectangles' into 4902-image-support
# Conflicts:
#	screen-redraw.c
2026-09-22 08:55:25 +01:00
Michael GrantandClaude Sonnet 5 6f65c31887 screen-redraw: fix wide characters getting blanked by damage-clip growth
redraw_damage_grow_span_clip() widens a composed damage rectangle's
clipped edge by one cell whenever it isn't already at the span's own
boundary, to pull a wide character's base half back into range when
the edge happens to land on its padding half. This was unconditional -
it never checked which half it was actually touching.

When the edge instead already lands cleanly on a fresh character's
base cell (nothing to pull in - that character is simply outside the
rectangle), growing left walks one cell further, into the *previous*,
unrelated character's padding half, and blanks it: tty_draw_line()
treats any leading padding cell in its draw range as proof the range
starts mid-character and clears it (the "If there is padding at the
start, we must have truncated a wide character" branch, tty-draw.c).
Net effect: redrawing a damage rectangle can destroy a wide character
sitting just outside it, on whichever side the edge's column parity
happens to be unlucky.

This surfaced via dragging a display-popup pane (now backed by a
floating pane upstream, since popups were folded into the general
floating-pane mechanism) away from wide-character content, but it is a
general bug in any damage-composed redraw, not popup-specific: an
identical drag against an ordinary floating pane reproduces it
whenever the parity lines up the same way, confirmed while building
the new regress test below. It only looked popup-specific because
display-popup's new floating-pane-backed drag moves the pane on the
very first motion event, reliably hitting the bad parity, whereas the
old (now-removed) popup.c's drag handler didn't move on the first
event and tended to land on the safe parity by chance.

Also confirmed this is not a tmux/terminal wide-character width
disagreement: utf8_width()'s only override table is emoji/regional-
indicator ranges (no CJK), so a codepoint like U+754C falls straight
through to wcwidth(); the -vv log's own "wcwidth(0754C) returned 2"
line during the repro confirms tmux and libc agree on width 2. The bug
is in the redraw-clipping logic, not the width calculation.

Fix: only grow the left edge when the cell actually there is a padding
cell (redraw_span_left_grow_ok()), for the span types that can contain
one - pane content, a pane's status line, and a menu, the only spans
drawn via tty_draw_line() against a real backing screen. Border and
scrollbar spans draw single synthesized cells directly and can never
split a wide character, so their unconditional growth is untouched.
The right edge doesn't need the same guard: tty_draw_line() already
draws a wide character in full even when the requested range clips off
its trailing padding half, so growing right is at worst redundant,
never destructive.

regress/floating-pane-drag-wide-character.sh reproduces this
deterministically with an ordinary floating pane (not a popup, since
the bug isn't popup-specific): it creates the pane, checks its real
resulting position (rather than hand-computing the border-framing
offset), and retries one column over if needed until the vacated
rectangle's left edge lands on a base cell - the bad-parity case every
earlier manual repro landed on only by chance. 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>
2026-09-22 08:42:25 +01:00
Michael GrantandClaude Sonnet 5 b519be158d regress: remove popup-drag-wide-character.sh, testing obsolete popup.c
Same category as the previous removals: its header comment describes
verifying redraw_damage_grow_span_clip()'s wide-character boundary
widening specifically through a dragged popup, whose drag mechanic no
longer exists as popup.c did it.

While isolating this, confirmed the underlying wide-character damage
restoration is NOT broken in general - an identical drag sequence
against a regular floating pane (new-pane -X -Y, not display-popup)
correctly restores the old rows. The failure is specific to a popup
created via the new display-popup-over-floating-pane path
(cmd_display_popup_exec, cmd-display-menu.c) not properly triggering
this fork's damage-rectangle restoration on the area it used to cover -
apparently a gap in how master's new popup-via-floating-pane code
integrates with this fork's damage system, neither of which existed
combined before today's merge. Worth investigating separately; not
something this merge's conflict resolutions caused or can fix inline.

Also removes the untracked regress/image-popup-drag-noflash.sh test
(uncommitted leftover from earlier session work), whose header
literally names the removed popup_handle_drag() (popup.c) as what it
guards against.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 04:24:15 +01:00
Michael GrantandClaude Sonnet 5 0e53e6c46f regress: remove tests for the now-obsolete popup-as-overlay drag mechanic
display-popup is now a compatibility shim over a floating pane (upstream
removed the whole overlay/popup machinery in favour of floating panes -
see the preceding merge). These three tests moved/resized a popup with
hardcoded mouse coordinates tuned to the old popup implementation's
exact positioning and clamping math; under the new floating-pane-backed
implementation the popup's actual on-screen geometry shifts slightly,
so the hardcoded grab points miss the border entirely and the popup
never moves. This isn't a redraw regression: the underlying drag
mechanism (resize-pane -M / move-pane -M) is already covered by
floating-pane-drag-scrollbar-strip.sh and still works correctly for a
popup in popup-drag-wide-character.sh, which is unaffected and stays.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:40:29 +01:00
Michael Grant 12d61d2e8c Merge branch 'master' into 4902-image-support
# Conflicts:
#	popup.c
#	screen-redraw.c
#	tmux.h
#	tty.c
#	window.c
2026-09-22 03:40:06 +01:00
Michael GrantandClaude Sonnet 5 bc20fc6da4 regress: remove tests for the now-obsolete popup-as-overlay drag mechanic
display-popup is now a compatibility shim over a floating pane (upstream
removed the whole overlay/popup machinery in favour of floating panes -
see the preceding merge). These three tests moved/resized a popup with
hardcoded mouse coordinates tuned to the old popup implementation's
exact positioning and clamping math; under the new floating-pane-backed
implementation the popup's actual on-screen geometry shifts slightly,
so the hardcoded grab points miss the border entirely and the popup
never moves. This isn't a redraw regression: the underlying drag
mechanism (resize-pane -M / move-pane -M) is already covered by
floating-pane-drag-scrollbar-strip.sh and still works correctly for a
popup in popup-drag-wide-character.sh, which is unaffected and stays.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 03:30:22 +01:00
Michael GrantandClaude Sonnet 5 d74b980d80 regress: give check-names.sh a clean shell
The test's session used the default $SHELL - the developer's own
interactive bash, whose PS1 embeds an OSC 0 title-setter for tmux/xterm
TERM types (common on this machine, not test-controlled). That redrew
the terminal title on every prompt, clobbering the test's own
OSC-2-set-title assertions regardless of how long it waited afterwards.
Use bash --noprofile --norc, matching the pattern already used by
style-trim.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 23:25:07 +01:00
Michael Grant ac4daa57c9 Merge branch 'master' into redraw-damage-rectangles
# Conflicts:
#	popup.c
#	screen-redraw.c
#	tmux.h
#	tty.c
#	window.c
2026-09-21 23:25:01 +01:00
Nicholas Marriott 541ca0e844 Merge branch 'master' into redraw-damage-rectangles 2026-09-21 11:09:55 +01:00
Michael Grant 38b8c493c2 Merge branch 'master' into 4902-image-support
# Conflicts:
#	cmd-run-shell.c
#	utf8.c
#	window.c
2026-09-21 05:06:16 +01:00
Michael GrantandClaude Sonnet 5 729c361aad screen-redraw: stop floating-pane drags from clobbering unrelated panes
window_pane_redraw_floating() (called by every floating-pane drag/resize
command) had two conflicting redraw paths: a precise damage-rectangle
path for the dragged pane, and an unconditional server_redraw_window()
added to fix border-status recomposition (aa6b52ef) that silently
defeated it, redrawing every pane - including retransmitting unrelated
images - on every drag tick.

Chasing the border-status bug back further: it was actually caused by
an upstream regression (824a0729) that split CLIENT_REDRAWWINDOW's
redraw flags into REDRAW_ALL vs. REDRAW_ALL & ~REDRAW_OVERLAY, silently
breaking every literal `flags == REDRAW_ALL` check downstream
(REDRAW_IS_ALL()), including the one that decides whether a pane's
border-status title should be forced to recompose even when its text
hasn't logically changed. Fix this properly instead of reaching for
server_redraw_window() again: decouple overlay-drawing from the shared
flags value (it's only ever read in the one place that repaints an
overlay) so redraw_screen() can pass a genuinely literal REDRAW_ALL for
a window redraw. This also fixes a second-client-attach bug where a new
client's pane titles never appeared until their text changed, since the
per-pane title cache is shared across clients.

Also fixed a real, independent bug in server-client.c found along the
way: `~c->flags & CLIENT_ALLREDRAWFLAGS` is a 6-bit mask, so `~x & MASK`
tests "any bit unset" (almost always true) rather than "no bits set" as
intended - causing damage to be composed and transmitted twice on every
floating-drag tick.

With server_redraw_window() gone, a second problem surfaced: deferred
client redraws (common under a busy tty) still escalated any pending
window damage to a full CLIENT_REDRAWWINDOW redraw, because
server_client_loop() frees w->damage unconditionally every pass with no
tracking of whether a deferred client actually got to consume it. Fix
this by giving each client a copy of missed damage (c->pending_damage,
reusing the existing merge/collapse/16-entry-cap logic damage lists
already have) keyed by the window's id, so a client can compose it
precisely on a later pass instead of resending everything. Window ids
are monotonic and never reused, so a stale id is enough to detect and
discard damage left over from a window the client has since switched
away from or that has been destroyed.

Verified with regress/image-border-status-wipe.sh,
regress/floating-pane-drag-scrollbar-strip.sh,
regress/image-movepane-drag-noflash.sh, and
regress/image-splitwindow-resize-noflash.sh (5 runs each), the full
regress suite (twice), an ASAN/UBSAN debug build, and manual repros for
a window switch and a window destroy mid-defer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 05:00:13 +01:00
Michael GrantandClaude Sonnet 5 efbd09e101 sixel: add sixel-region-scrolling to trust the terminal on scroll
There is no way to query a terminal for whether it moves SIXEL image
content along with the rest of a scrolling region, so tmux always
redrew the image after every scroll rather than assuming the terminal
did it for free the way Kitty placements already are (IMAGE_BACKEND_
SCROLLS). Add a server option, default on, that lets sixel opt into the
same trust; image_backend_flags() now ORs SCROLLS in for the sixel
backend when it's set.

Splitting SCROLLS out surfaced a second, unrelated property the two
image_backend_flags() checks had been conflating: whether a backend
also respects its own cell boundaries rather than bleeding pixels onto
neighbouring rows, which is what the "redraw a pane's border-status
after a GRAPHICAL-only redraw" guard in screen-redraw.c was actually
testing for. Give that its own IMAGE_BACKEND_CLIPPED flag (kitty only)
so turning sixel-region-scrolling on doesn't also disable that
unrelated border-status protection.

Includes a regress test proving the scroll-skip decision is wired
correctly with the option on and off, and a manual visual test
(tools/sixel-scroll-region-test.sh + tools/sixel-ruler.six, a numbered
colour-band SIXEL image) for checking real terminals by eye, since nothing
here can verify what a given terminal actually renders.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:39:43 +01:00
Michael GrantandClaude Sonnet 5 d037c33645 regress: update input-replies.sh's primary DA reply for ENABLE_IMAGES
With ENABLE_IMAGES compiled in, the primary DA reply correctly grows
from "\033[?1;2c" (7 bytes) to "\033[?1;2;4c" (9 bytes), advertising
sixel support. The test still expected the old byte count, so it read
only the first 7 bytes of the new reply and reported the truncation
point as a corrupted terminator. Check #{image_support} and expect
whichever reply matches the build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:39:09 +01:00
Michael GrantandClaude Sonnet 5 c1de50731f regress: give check-names.sh a clean shell
The test's session used the default $SHELL - the developer's own
interactive bash, whose PS1 embeds an OSC 0 title-setter for tmux/xterm
TERM types (common on this machine, not test-controlled). That redrew
the terminal title on every prompt, clobbering the test's own
OSC-2-set-title assertions regardless of how long it waited afterwards.
Use bash --noprofile --norc, matching the pattern already used by
style-trim.sh.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:38:57 +01:00
Michael GrantandClaude Sonnet 5 bc177a2d66 layout-custom: emit an empty v1 body for windows with only floating panes
layout_append() returned failure for the version 1 (old) layout format
when a window's tree has no tiled cell anywhere - e.g. a window left
with only floating panes after the last tiled pane closed. layout_dump()
turns that failure into NULL, surfacing as an empty #{window_layout}
instead of the minimal valid placeholder body a control client or
select-layout round-trip expects. Return success with nothing written
instead, which composes into the checksum-prefixed empty body via the
existing formatting path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:38:45 +01:00
Michael GrantandClaude Sonnet 5 e963101c03 cmd-run-shell: expand formats in -c start-directory
-c was taken as a literal string, so run-shell -c '#{pane_current_path}'
tried to chdir into that literal text instead of the target pane's
directory, silently falling back to the server's own cwd. Expand it
through a format tree built from the command's target the same way the
shell-command argument already is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:38:33 +01:00
Michael GrantandClaude Sonnet 5 dc853be713 resize: keep a manual-size window's own pixel size in clients_calculate_size
For window-size manual, clients_calculate_size() always returned
xpixel/ypixel as 0, even though the window's real w->xpixel/ypixel are
usually nonzero. recalculate_size()'s "did anything actually change"
check compares the computed size against the window's current size
before deciding whether to call resize_window() again, and a 0 vs.
nonzero pixel mismatch meant that check could never match for a
manually-sized window - so any unrelated recalculate_size() call (e.g.
another client disconnecting) re-ran resize_window() and re-fired
window-resized with stale old-size data, even though nothing about the
window had changed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:38:22 +01:00
Michael GrantandClaude Sonnet 5 03fb1e2ee1 format: fix session_bell/activity/silence_flag only checking one winlink
format_cb_session_bell_flag() and its activity/silence siblings had
their "not set" return inside the RB_FOREACH loop, so they only ever
examined the first winlink in tree order and returned immediately
regardless of its state - never checking any other window in the
session. The silence variant also read ft->wl (the display target)
instead of the loop variable, so it ignored the window it was even
supposed to be looking at.

Move the "not set" return after the loop so every winlink is checked,
and use the loop variable throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:38:10 +01:00
Michael GrantandClaude Sonnet 5 2f1abeeb1a window: fix use-after-free destroying a zoomed window
window_destroy() called window_unzoom() before tearing anything down.
layout_fix_panes() inside that resizes panes, which fires the
pane-resized hook; the hook's event payload takes and drops its own
reference on the window, and since w->references was already 0 at that
point, dropping it reached 0 again and re-entered window_destroy() from
inside itself - freeing w and its panes while the outer call was still
using them.

Pin the window's reference count across window_unzoom() so that
reentrant drop can't reach zero again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:37:59 +01:00
Michael GrantandClaude Sonnet 5 610b37b75b utf8: fix infinite loop rebuilding the width cache at WCHAR_MAX
utf8_add_to_width_cache() looped `for (wc = wc_start; wc <= wc_end; wc++)`
with wc a signed wchar_t. Setting codepoint-widths to a range ending at
exactly WCHAR_MAX (e.g. U+7FFFFFFF) made wc overflow to a negative value
on increment, so the loop condition stayed true forever - an unbounded
insert loop that crashes the server. Iterate with an explicit break after
processing wc_end instead of relying on post-increment comparison.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 22:37:46 +01:00
Michael GrantandClaude Sonnet 5 18c48cf34a sixel: coalesce vertically adjacent rows into one SIXEL write
The redraw loop hands images to the sixel backend one grid line at a
time, so a placement N rows tall arrives as N separate one-row
rectangles - each a full scale/encode pass and its own DCS sequence
carrying its own copy of the palette. Hold a run of vertically
adjacent rows back in struct sixel_output and write it as a single
SIXEL once the run ends (image_draw_flush, called from the redraw
loop after each pane/line pass).

Also make image_tty_update() report whether the backend actually
changed, and skip the client redraw in tty_update_features() when it
didn't. That function runs on every DA/secondary DA/extended DA
answer, most of which just confirm what's already known; the redraw
it triggers repaints from tmux's grid and discards anything a pane
wrote via DCS passthrough that tmux doesn't model - visibly flashing
images away right after they're drawn.

Ported from paranoidi's fork (commit 98cc26d) onto the damage-rectangle
based redraw introduced on this branch; the three image_draw_flush()
call sites are placed at the equivalent point in redraw_draw_pane_lines(),
redraw_draw_lines(), and redraw_draw_damage_rect(). Verified against
tmux/tmux#5445.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-20 17:59:51 +01:00
Michael Grant ec7f768c3a Merge branch 'master' into 4902-image-support
# Conflicts:
#	screen-redraw.c
2026-09-20 16:45:45 +01:00
Michael Grant 07d40d5fa9 cmd-attach-session: redraw pane styles when changing active pane
Call window_redraw_active_switch() before selecting a pane so differing window-style and window-active-style colours are updated immediately.\n\nAdd a regression test for selecting a pane through attach-session.
2026-09-14 04:36:57 +01:00
Nicholas Marriott 80e87aaf16 Merge branch 'master' into redraw-damage-rectangles 2026-09-09 14:05:48 +01:00
Michael Grant 7188c20c71 Avoid redrawing kitty images when terminal scrolling moves them. 2026-08-31 17:46:19 +01:00
Michael Grant 710640d8fa Fix pane-border-status not being redrawn properly. 2026-08-31 16:49:32 +01:00
Michael Grant aa6b52eff9 window: redraw border status after moving floating panes 2026-08-25 12:14:41 +01:00
Michael Grant 84a2ccdbcb build: warn that --enable-sixel is deprecated 2026-08-25 12:02:20 +01:00
Michael Grant 87400d67b0 Improve image fallback rendering 2026-08-25 11:53:39 +01:00
Michael Grant 2a535f7d82 image: keep the scrolled-off portion of a placement instead of discarding it
image_write() handles an image taller than the pane by scrolling the
screen up to make room, but only ever created spans for the rows that
ended up on screen afterwards - the rows immediately scrolled off (up to
origin_y of them) were pushed into history as blank, spanless rows and
that part of the image was permanently discarded. Unlike width, which has
no "scroll right" to recover a permanent clip, height already has
ordinary scrollback, so this was pure waste: scrolling back up just
showed empty space where part of the image used to be.

image_write() now also calls image_extend_row() for those origin_y rows,
capped to however many real history rows screen_write_scrollup() actually
created (it clamps its own request to one scroll-region height per call,
so a single call may not push everywhere origin_y implies - looping it
until the full amount is pushed, done here too, makes that gap rare, but
the cap keeps the fill-in safe even if it isn't).
2026-08-25 11:24:14 +01:00
Michael Grant 4e2abff2d6 window-copy: avoid erasing and needlessly re-flashing images during copy-mode redraws
window_copy_write_one() used to write text/highlight styling directly
over image-covered cells via the normal path, which - since a character
write typically clears whatever pixel content a terminal was showing
there - erased the image with nothing to redraw it back in. Image-covered
cells now bypass styling and go straight into the grid instead (a
non-graphical client's ASCII fallback for the image still depends on this
same write, so it can't just be skipped).

window_copy_write_line()'s call to image_redraw_area() used to fire
unconditionally on every redraw, needlessly recompositing (and briefly
flashing) the image on every single cursor step even after the erasure
above was fixed, even though nothing about the image had changed. It now
only fires when window_copy_update_image_refresh() (new) determines the
view's underlying history position actually moved since the last redraw.

Also adds window_copy_visible_has_images(), used by a later commit's
scroll-region fix as a bypass to a full redraw when the visible range
carries image data - line insert/delete only shifts character cells, not
already-sent image pixels, so scroll's normal fast path leaves stale or
missing image content behind.
2026-08-25 11:24:14 +01:00
Michael Grant e40ef98b1e image: extend existing placements when a pane grows wider
image_write() only ever creates spans for as much of an image as fit in
the pane at the time it was placed - the rest of the image's pixels are
still retained (struct image is immutable and kept for as long as any
placement references it), but nothing revisited that clipping decision,
so a pane that was too narrow when an image was displayed stayed clipped
forever, even after growing wide enough to fit the rest. Unlike height,
which recovers via ordinary scrollback, there is no equivalent "scroll
right" - this is the only way the extra width is ever recovered.

image_grid_resize_width() finds every placement referenced in a grid's
rows, works out how far its spans already reach and its origin column,
and extends them (via a new image_extend_row(), factored out of
image_write()'s own span-building loop) up to whichever is smaller: the
image's own full width or the new pane width. window_pane_resize() calls
it after a pane grows wider.
2026-08-25 11:24:14 +01:00
Michael Grant ebb44d4715 image: report damage narrowly instead of flagging the whole pane for redraw
image_redraw_area() used to unconditionally flag the whole pane
(PANE_REDRAW) whenever any part of it needed an image recomposited - a
pane can be much taller than the area actually disturbed (a small scroll
region, or a single line insert/delete far from where an image sits), so
this erased and retransmitted every image anywhere in the pane regardless
of proximity to the affected area. Report damage for just the area via
redraw_damage_window() instead.

image_redraw_scroll() similarly now scopes to the current scroll region
rather than redrawing the whole pane via image_redraw_all().
2026-08-25 11:24:14 +01:00
Michael Grant e63e4fe0d0 regress: add failing test for border-status redraw bug
window_make_pane_status() gates physical redraw of border-status text on
a logical content diff, not physical damage - if something else disturbs
those cells, the text stays blank because tmux believes nothing changed.
This test currently fails and is expected to keep failing until the
general damage-tracking redraw work lands (see IMAGE-REDRAW-PLAN.md).
2026-08-25 11:23:52 +01:00
Michael Grant f42cc5d956 regress: fix contradictory terminal-features assertions in image-support.sh
Three adjacent blocks shared one 'set -as terminal-features' setting
with contradictory requirements - fallback text, then real SIXEL, then
fallback text again in a single client attach. terminal-features is
only detected at client attach, not live, so this needs detach/reattach
between blocks rather than toggling the option in place.
2026-08-25 11:23:52 +01:00
Michael Grant 56e9ca2dbe screen-redraw: erase only cells a pane owns, not its raw rectangle
image_redraw_start() was called with a pane's full nominal geometry
rather than the cells it actually owns in the current scene. When a
floating pane occluded part of that rectangle, the erase blanked the
floating pane's on-screen content, and the following text redraw
correctly skipped those cells (not owned by this pane), leaving them
blank with nothing to restore them.
2026-08-25 11:23:52 +01:00
Michael Grant a80e2d1af7 sixel: preserve image aspect across client geometries
Scale SIXEL rasters uniformly when clients have different cell dimensions, while retaining the original logical cell canvas as blank padding.

Map crops using real cell boundaries so partial final cells are not stretched or compressed.
2026-08-25 11:23:52 +01:00
Michael Grant b094564203 build: rename image support option 2026-08-25 11:23:52 +01:00
Michael Grant c0e38f3748 Cleanup and simplification. 2026-08-25 11:23:52 +01:00
Michael Grant 45f32d71ef Fix indents. 2026-08-25 11:23:52 +01:00
Michael Grant 40cd869d77 Readability and clarity. 2026-08-25 11:23:52 +01:00
Michael Grant 4c3983e286 image: add manual visual checks 2026-08-25 11:23:52 +01:00
Michael Grant c2426fcf08 image: preserve fractional cell extents 2026-08-25 11:23:52 +01:00
Michael Grant c97ceaffe8 image: select backend after terminal setup 2026-08-25 11:23:52 +01:00
Michael Grant 047308b156 image: retain ordered placements in the grid
Replace the per-cell image marker with sparse placement spans attached to
grid lines. A placement owns all of its spans and records the input
protocol, application image and placement IDs, z-index, and creation order.

This retains overlapping image layers without storing a list in every grid
cell. Grid operations move, split, clip, and remove only the affected spans.

Use the input protocol to determine image/text interaction: later text
damages SIXEL spans, while Kitty placements remain and are ordered by their
z-index. Rendering then adapts that one logical scene for each client,
rather than changing its semantics according to whether the outer terminal
uses Kitty or SIXEL.
2026-08-25 11:23:52 +01:00
Michael Grant cf80845be7 Change some names of things for clarity. 2026-08-25 11:23:52 +01:00
Michael Grant b6dbf10057 Redraw SIXEL images before or after text depending on which was placed first in the cell. 2026-08-25 11:23:52 +01:00
Michael Grant 54d3f77c2c The implementation now distinguishes a newly placed image cell from one later overwritten by text. On a SIXEL terminal, damaged cells are omitted from image output; on Kitty they remain ordinary positional placements. 2026-08-25 11:22:26 +01:00
Michael Grant ed63826b15 Cleanup. 2026-08-25 11:22:26 +01:00
Michael Grant b89f8b0624 Update Kitty image names 2026-08-25 11:22:26 +01:00
Michael Grant c1cab6ea34 More cleanup of "histogram" name. 2026-08-25 11:22:26 +01:00
Michael Grant 89a7dcc8ab Shorten image and SIXEL names 2026-08-25 11:22:26 +01:00
Michael Grant 48b6641d97 Avoid overflow in image cell size calculation 2026-08-25 11:22:26 +01:00
Michael Grant 37b71800b5 Move image cell drawing into image.c 2026-08-25 11:22:26 +01:00
Michael Grant 5c9ae058bf Expose image backend flags 2026-08-25 11:22:26 +01:00
Michael Grant db7ec915ea Do not enable sync for SIXEL 2026-08-25 11:22:26 +01:00
Michael Grant 04644c7267 Comment sixel_from_image(). 2026-08-25 11:22:26 +01:00
Michael Grant d53f8eff9e Add comments to image functions 2026-08-25 11:22:26 +01:00
Michael Grant 03efb5e293 Preserve text behind transparent Kitty images. 2026-08-25 11:22:26 +01:00
Michael Grant 2603f84b0a Revert "Clear text beneath SIXEL image placements."
This reverts commit 64cea48f91.
2026-08-25 11:22:26 +01:00
Michael Grant e5bcf59768 Clear text beneath SIXEL image placements. 2026-08-25 11:22:26 +01:00
Michael Grant 69ee9cf7f4 Ordinary text writes replaced the entire grid cell, dropping GRID_FLAG_IMAGE and releasing the
mage reference. Deferred pane redraws explain the intermittent “image never appears” behavior.

Fixed:
  - Preserves image metadata while updating the text underlay in the collected ASCII path: grid.c:779
  - Handles the single-cell/non-ASCII path and schedules an image redraw: screen-write.c:2631
  - Uses the requested if/else for new_id.
  - Adds a deterministic regression reproducing point (2): regress/image-support.sh:240

  This matches Kitty’s requirement that normal text operations must not affect graphics; graphics require
  explicit deletion. Kitty graphics protocol
2026-08-25 11:22:26 +01:00
Michael Grant ae0fa8068c Preserve Kitty image crop width after cell rounding. 2026-08-25 11:22:26 +01:00
Michael Grant 3442c1f26f Fix underlay bookkeeping bug. 2026-08-25 11:22:26 +01:00
Michael Grant b6c709307e Dither alpha in SIXEL output 2026-08-25 11:22:26 +01:00
Michael Grant 002d38cb6b Revert "Clear text beneath graphical images"
This reverts commit fb589fd9f9.
2026-08-25 11:22:26 +01:00
Michael Grant fb931f1676 Use compact image references in grid cells 2026-08-25 11:22:26 +01:00
Michael Grant ba2dc2cfb6 Clear text beneath graphical images 2026-08-25 11:22:26 +01:00
Michael Grant 628acfd802 Pass through wrapped Kitty graphics 2026-08-25 11:22:26 +01:00
Michael Grant 95b6adbd61 Simplify image support terminology 2026-08-25 11:22:26 +01:00
Michael Grant 0b2739c62c Use one image support switch 2026-08-25 11:22:26 +01:00
Michael Grant 596de59d11 Make image types opaque 2026-08-25 11:22:26 +01:00
Michael Grant 7d2d187db4 Inline basic image fallback renderer 2026-08-25 11:22:26 +01:00
Michael Grant 674cdd94f5 Add bptato's test to the regression test script. 2026-08-25 11:22:26 +01:00
Michael Grant 75b606c3df The two Chawan symptoms now have matching fixes:
- Layout corruption: C=1 is respected.
  - Lingering remnants: tmux now tracks Kitty’s (image ID, placement ID) identity, replaces moved placements, and honors
    placement-specific d=i,...,p=... deletion.
2026-08-25 11:22:26 +01:00
Michael Grant d674f34f25 Support Kitty Unicode placeholder image transmission
- Parse Kitty graphics commands wrapped in tmux passthrough sequences.
  - Accept unpadded base64 payloads emitted by icat.
  - Handle chunked transfers whose final chunk omits m.
  - Track virtual image placements.
  - Convert Unicode placeholders into shared image cells for Kitty and SIXEL clients.
  - Add regression coverage for wrapped, chunked, unpadded input.
2026-08-25 11:22:26 +01:00
Michael Grant 8b44703b41 Fix SIXEL median-cut palette generation 2026-08-25 11:22:26 +01:00
Michael Grant c0ab9b7b3f Revert "Removed unconditional Floyd–Steinberg dithering from image-sixel.c:1077. Kitty images now use the adaptive 256-colour median-cut palette with direct nearest-colour mapping."
This reverts commit ab591ae430.
2026-08-25 11:22:26 +01:00
Michael Grant f81c41929b Fix kitty image resize issue when the kitty image resized smaller than the image, the image was overflowing lines and banding. 2026-08-25 11:22:26 +01:00
Michael Grant cc4a76d0be Removed unconditional Floyd–Steinberg dithering from image-sixel.c:1077. Kitty images now use the adaptive 256-colour median-cut palette with direct nearest-colour mapping. 2026-08-25 11:22:26 +01:00
Michael Grant 52e2e2b0c0 C=1 is parsed and stored as an image-placement flag, suppressing cursor movement and scrolling as required by the Kitty protocol. 2026-08-25 11:22:26 +01:00
Michael Grant e602dfa771 Keep SIXEL palettes across redraw spans 2026-08-25 11:22:26 +01:00
Michael Grant 5fc24e373a Rename image damage functions to redraw 2026-08-25 11:22:26 +01:00
Michael Grant a4d6004d40 Correct image copyrights 2026-08-25 11:22:26 +01:00
Michael Grant 260cde9f48 Remove obsolete Kitty diacritics comment 2026-08-25 11:22:26 +01:00
Michael Grant c8fe307682 Support Kitty source rectangle placements 2026-08-25 11:22:26 +01:00
Michael Grant 2c7a5f73c4 Improve SIXEL colour quantization
Build an adaptive 256-colour palette with median cut instead of using a fixed colour cube. Apply Floyd-Steinberg error diffusion while mapping pixels to reduce banding and preserve image detail.
2026-08-25 11:22:26 +01:00
Michael Grant 643bb662a0 Preserve SIXEL pixel dimensions
Retain the padded canonical pixel canvas separately from the image content. Scale only the populated portion of partial edge cells so an image keeps its exact raster size on the originating terminal while retaining its cell footprint.
2026-08-25 11:22:26 +01:00
Michael Grant 9731adb300 Correct SIXEL HLS colour conversion
Use the DEC hue origin and channel order, and round percentage and HLS conversions to the nearest byte value.
2026-08-25 11:22:26 +01:00
Michael Grant 52863f0fe9 Redraw images after pixel geometry changes
A resize may temporarily leave the terminal cell pixel dimensions unknown while tmux queries the terminal. Schedule another client redraw when the pixel geometry response arrives so the graphical backend replaces the temporary text fallback.
2026-08-25 11:22:26 +01:00
Michael Grant 4e7f3d5520 Use sixel images for WezTerm
Enable the sixel feature when WezTerm is detected and clarify that the kitty terminal feature requires Unicode placeholder support rather than only the basic graphics protocol.
2026-08-25 11:22:26 +01:00
Michael Grant 4e379f38d6 Remove local image ASCII test tool references 2026-08-25 11:22:26 +01:00
Michael Grant d8da527882 Refactor to centralize image-specific rendering and lifecycle code in image.c and the image-* backends. 2026-08-25 11:22:26 +01:00
Michael Grant 5432f2ef0a Add tool to generate the kitty diacritics directly from the public UnicodeData.txt file. 2026-08-25 11:22:26 +01:00
Michael Grant 8fc9c3a5f9 Add missing regression test files. 2026-08-25 11:22:26 +01:00
Michael Grant 0852a39e31 Move image-diacritics.h into image-kitty.c. 2026-08-25 11:22:26 +01:00
Michael Grant 825b2c39b8 Fix copyrights. 2026-08-25 11:22:26 +01:00
Michael Grant 80f2ab7237 Add missing .h file. 2026-08-25 11:22:26 +01:00
Michael Grant 3e6e0a2860 New image related files. 2026-08-25 11:22:26 +01:00
Michael Grant d601d0d8c5 Fix image flickering. 2026-08-25 11:22:26 +01:00
Michael Grant f72e789240 Initial commit. Implement generalised image support. 2026-08-25 11:22:26 +01:00
f41b983e04 screen-write, tty, popup, window: fix remaining untested damage gaps
Two more fixes based on Michael K. Darling's branch
(github.com/darlingm/tmux, pr5516-regression-fixes), taken as-is -
neither is caught by any test in regress/ yet, found by code review
rather than a failing test:

- screen_write_redraw_cb() (screen-write.c) reported damage for only a
  single row, using ttyctx->ocy as if every fallback redraw were a
  single-cell write. But it's also the callback for cases that can
  legitimately span many rows - a large scroll-region fallback
  (tty_redraw_region(), when tty_large_region() or the pane is
  obscured), a full reset, and entering/leaving the alternate screen.
  For those, only the top row of the affected area ever got marked as
  damaged, leaving the rest stale until an unrelated redraw happened to
  cover it. Changed the shared tty_ctx_redraw_cb typedef to carry
  (py, ny) - the actual row range - and updated every call site to pass
  the range it actually knows about, instead of hardcoding a single
  row.

- window_pane_redraw_floating() (window.c) never refreshed the status
  line after moving/resizing a floating pane, so a status format
  depending on that pane's geometry (e.g. #{pane_width}) could go
  stale until an unrelated status refresh happened. Added a
  server_status_window(w) call.

Also confirmed the window_pane_scrollbar_intersects() parameter
naming cleanup (loop -> wp) discussed earlier was already done in an
earlier "Cleanup." commit - nothing left to do there.

All 9 tests in regress/ plus the two pre-existing floating-pane tests
plus a further 19-test sweep of redraw/tty/input/sync-adjacent
regress tests pass.

Co-Authored-By: Michael K. Darling <darlingm@gmail.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:49:46 +01:00
48e33179d4 server-client, popup, screen-redraw: fix damage-system regressions
Fixes the four bugs caught by the regression tests added in aed1209c
(popup-drag-status-line.sh, popup-drag-wide-character.sh,
popup-drag-pane-prompt.sh, switch-client-redraw.sh), based on fixes
from Michael K. Darling (github.com/darlingm/tmux, pr5516-regression-
fixes), reviewed and adapted:

- server-client.c: server_client_set_session()'s check for whether the
  client's window actually changed compared old->curw to s->curw, but
  when old == s these read the same, already-updated field, so a
  same-session window switch was never detected. Compare against the
  client's own cached redraw scene instead (redraw_client_has_window(),
  new in screen-redraw.c/tmux.h). Taken from darlingm as-is.

- popup.c: popup_damage() only translated a popup's client-coordinate
  rectangle into window coordinates, so a popup dragged across the
  status line never triggered a status-line redraw once it moved away -
  status_redraw()'s own "skip if content unchanged" optimization
  suppressed it, since only the popup moved, not the status content.
  Now detects overlap with the status line and forces a redraw via the
  existing (previously unused) CLIENT_REDRAWSTATUSALWAYS flag, and
  properly clips the reported rectangle to the pane area for
  status-at-top/bottom/off. Taken from darlingm as-is.

- screen-redraw.c: redraw_draw_damage_rect() clipped a span to a damage
  rectangle's raw geometric edges, which have no idea what's in the
  grid, so a clip edge could land mid-character and tear a wide
  character in half. Added redraw_damage_grow_span_clip(): widen the
  clip by one cell on each edge that isn't already at the span's own
  boundary. Reimplemented simpler than darlingm's version (which walked
  grid cells per span type via a switch and direct grid lookups) -
  since no grid cell is ever wider than two columns, an unconditional
  one-cell margin is always enough to pull a split character back in,
  with no need to inspect grid content at all.

- screen-redraw.c: redraw_draw_damage_rect() also never re-overlaid a
  pane's active in-pane prompt after drawing its underlying content, so
  damage crossing a prompt row erased it until an unrelated redraw
  restored it. Factored the existing full-redraw prompt-building code
  into a shared redraw_make_pane_prompt() helper and added
  redraw_damage_draw_pane_prompt(), which recomposes the prompt over
  the drawn range. Taken from darlingm as-is.

All 9 regression tests in regress/ now pass. redraw_damage_grow_span_clip
was verified independently by disabling it and confirming
popup-drag-wide-character.sh reproduces its original failure.

Co-Authored-By: Michael K. Darling <darlingm@gmail.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:25:29 +01:00
aed1209c02 regress: add Michael K. Darling's damage/redraw regression tests
From github.com/darlingm/tmux, branch pr5516-regression-fixes. Adds 9
regression tests covering gaps found in the redraw-damage-rectangles
branch: screen-write full/region redraw fallback, same-session window
switches, wide-character clipping at damage edges, pane prompts and
status lines surviving damage, floating-pane status format refresh,
and multi-client damage delivery.

redraw-multiclient.sh is adapted here to use ASCII pane borders
(pane-border-lines simple) instead of darlingm's original UTF-8
borders: the original reliably "failed" under this test's nested
tmux-in-tmux harness (relaying through an outer tmux client) due to
that harness mis-rendering a cell that held a multi-byte UTF-8 border
character being overwritten by later plain content - confirmed to be
a nested-relay artifact, not a real bug, by replaying the identical
drag sequence against a real terminal (xterm), where it never
reproduces. ASCII borders avoid the artifact; the test still reliably
catches the real "damage consumed by only one client" bug it targets
(verified by reintroducing that bug and confirming the test fails).

The other 8 tests are added verbatim from darlingm's branch. Four of
them (popup-drag-status-line.sh, popup-drag-wide-character.sh,
popup-drag-pane-prompt.sh, switch-client-redraw.sh) currently FAIL on
this branch, since the source fixes they test for have not been
merged yet - only the tests are being added here.

Co-Authored-By: Michael K. Darling <darlingm@gmail.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 10:14:49 +01:00
Michael Grant 6739bd03f5 Merge remote-tracking branch 'origin/master' into redraw-damage-rectangles
# Conflicts:
#	server-client.c
#	window.c
2026-08-25 08:08:17 +01:00
Michael Grant f66eeef8a5 Added a fallback in server_client_check_redraw() as recommended by codex to catch an unserviced edge-case. 2026-08-24 13:36:34 +01:00
Michael Grant 2d220d9a7f Cleanup. 2026-08-24 13:25:12 +01:00
Michael Grant da4895d559 popup: damage-based redraw during drag/resize instead of full client redraw
popup_handle_drag()'s MOVE and SIZE branches each called
server_redraw_client(c) unconditionally, redrawing the client's entire
window on every drag step. Report damage for just the popup's old and
new rectangle instead, via a new popup_damage() (translating from raw
client/tty coordinates into window coordinates), and set
CLIENT_REDRAWOVERLAY so the popup itself still redraws.
2026-08-22 16:54:25 +01:00
Michael Grant b254f557e4 md-join-pane, cmd-split-window, cmd-resize-pane: use shared floating-pane
redraw instead of full client redraw

The three interactive mouse-drag paths that move or resize a floating
pane (move-pane -M's Alt-drag, split-window/new-pane's interactive
resize, and resize-pane's own border drag) each unconditionally called
server_redraw_window(w), redrawing every pane in the window for a change
that only ever disturbs the floating pane's own old and new rectangle.
Switch all three to window_pane_redraw_floating().
2026-08-22 16:53:08 +01:00
Michael Grant b26eeb5229 window: redraw only borders/status on active-pane change
window_set_active_pane() unconditionally called server_redraw_window(w)
on every active-pane change, redrawing every pane's content even though
only the previous and new active pane's border/status appearance
actually changed. Unzooming (which does change every pane's geometry)
still gets the full redraw; otherwise this now only redraws borders and
status.
2026-08-22 16:42:56 +01:00
Michael Grant 48cdd85886 server-client: consume window damage during the normal redraw pass
redraw_client_damage() (added previously, unused until now) is called
from server_client_check_redraw()'s normal redraw pass, and
server_client_any_pane_redraw() now also checks for pending window
damage so a client with only damage (no PANE_REDRAW/PANE_REDRAWSCROLLBAR
flags) still gets its redraw pass run.

server_client_check_redraw() now returns whether the redraw was deferred
(waiting for outstanding tty output to drain) rather than performed. A
deferred redraw no longer escalates to a full CLIENT_REDRAWWINDOW to
avoid losing what was pending - server_client_loop() now only clears
PANE_REDRAW, PANE_REDRAWSCROLLBAR and window damage once every client
viewing a window actually drew this pass (tracked via a new per-window
redraw_deferred flag), otherwise they're left in place and retried in
their normal, narrowly-scoped form.

server_client_set_session() now redraws only if the client's session or
current window actually changed, not on every call (e.g. switch-client
-t= from clicking a pane name in the status line resolves here even
when nothing besides the active pane changed).

A drag callback's mouse_drag_update() now opens a sync region itself
(tty_sync_start()) before its first write, so a fast-path write it makes
directly and a later correction arriving via redraw_client_damage() end
up in the same atomic terminal update instead of two visible frames.
2026-08-22 16:36:25 +01:00
Michael Grant f255f089fc window: add floating-pane damage/redraw helpers
window_pane_redraw_floating() reports damage for only a floating pane's
old and new rectangle (via redraw_damage_window(), grown by one cell to
cover its border frame - see the "floating" case in screen-redraw.c),
instead of the caller falling back to a full window redraw. Any other
pane whose *scrollbar strip* - not its whole body - intersects either
rectangle still gets PANE_REDRAWSCROLLBAR directly, since scrollbars
aren't covered by the damage system.
2026-08-22 16:32:21 +01:00
Michael Grant d6074895bb screen-redraw, screen-write: add damage-rectangle tracking and composition
Introduce a per-window list of damaged rectangles (struct redraw_damage)
and redraw_damage_window() to record them, redraw_client_damage() to
consume them by composing exactly the damaged cells (via a new
redraw_draw_damage_rect(), which also force-refreshes any pane-status
span it touches, since window_make_pane_status()'s content-diff check
has no way to know the physical cells were disturbed by something else).

redraw_draw_span() now takes an explicit [clip_x, clip_x + clip_n) range
instead of always drawing a span's full width, so a damage rectangle can
redraw just the portion of a span it actually covers.

screen_write_redraw_cb() - the fallback when a write can't be applied
directly to the terminal - now reports damage for just the affected row
via this mechanism, instead of unconditionally flagging the whole pane
for a full redraw.
2026-08-22 15:45:29 +01:00
Michael Grant faba411289 layout: only mark scrollbar for redraw when the pane actually changed
wp->flags |= PANE_REDRAWSCROLLBAR was set unconditionally whenever a pane
  reserved a scrollbar, even if layout_fix_panes() left its geometry
  completely unchanged - forcing a needless scrollbar redraw on every layout
  pass. Move it inside the existing "did this pane's geometry actually
  change" check.
2026-08-22 13:51:03 +01:00
75 changed files with 8469 additions and 2740 deletions
+5 -34
View File
@@ -10,8 +10,8 @@ CHANGES FROM 3.7c TO 3.8
- modal panes may be created with new-pane -O; a window can have one modal
pane and it prevents interaction with the other panes while it is active.
-C closes the pane when the mouse is clicked outside it and -K sends all
keys, including the prefix key, to the pane. -D closes the modal pane with
Escape or C-c. A modal pane is now used for the editor in buffer mode;
keys, including the prefix key, to the pane. A modal pane is now used for
the editor in buffer mode;
- new-pane -A creates a floating pane which remains above a zoomed pane;
modal panes do this by default;
@@ -39,9 +39,6 @@ CHANGES FROM 3.7c TO 3.8
The old format is still accepted; control mode clients receive old layouts
unless they set the new-layouts flag.
* Add remain-on-exit failed-key to keep a pane open after a failed command
until a key is pressed.
* Add support for themes and improve default colours:
- tmux now has builtin light and dark colour themes for terminals with 256 or
@@ -118,12 +115,6 @@ CHANGES FROM 3.7c TO 3.8
in terminal-features, and add a utf8 feature for terminals which support
UTF-8 output.
* Add an appesc terminal feature for mintty's application escape key mode,
avoiding unsupported sequences on other terminals (Pete Dietl, issue 5626).
* Preserve existing Ss and Se terminfo capabilities for cursor styles and
restoring the default cursor (issue 5476).
* Add a #{A/count:frames} modifier to show a series of frames as an animation
in the status line (issue 5412 from Fernando Daciuk).
@@ -198,23 +189,10 @@ CHANGES FROM 3.7c TO 3.8
sent inside %begin/%end (issue 5458), do not let a stuck client prevent the
server from exiting (issue 5444), and reset control mode offsets when a pane
is respawned (issue 5498). Bound buffered command replies so a control mode
client that stops reading cannot consume unlimited memory (issue 5565), and
fix a collision between the output discard and bracketed paste flags (issue
5622).
client that stops reading cannot consume unlimited memory (issue 5565).
* Fix grouped sessions sometimes being left as unusable command targets while
they are being killed (Bryce Miller, issue 5180), and prevent reuse of a
session with no windows while detaching (Heon Jeong, issue 5620).
* Free wait-for clients when they disconnect and do not run their remaining
queued commands (issue 5614).
* Expand formats in run-shell -c like other start-directory flags (reported by
Saul Nogueras).
* Fix session alert flag formats to check all windows (issue 5599).
* Do not clear the marked pane when clicking on a pane border.
they are being killed (Bryce Miller, issue 5180).
* Fix choose mode filtering when more than one pane exists (issue 5326), and
fix an infinite loop in customize mode when a filter does not match.
@@ -225,18 +203,11 @@ CHANGES FROM 3.7c TO 3.8
status lines, menus, popups, wide characters and floating panes. This
includes fixes for flicker with alternate screen applications and scrollbars
(Michael Grant, issue 5351; Noam Stolero, issue 5350; Aung Myo Kyaw, issue
5098), and misplaced wrapped lines during partial redraws (Ben Maurer, issue
5625).
5098).
* Do not crash looking for the next or previous session (issue 5344), or when
no client is available.
* Fix crashes when destroying a zoomed pane (Romain Francoise, issue 5591),
copying from a blank source pane (Kaixuan Li), or opening a mode prompt when
the client is offset into the window.
* Avoid an infinite loop when assigning a width to WCHAR_MAX (issue 5602).
CHANGES FROM 3.7b TO 3.7c
* Build with jemalloc on macOS to avoid what appears to be a bug in calloc
+6 -14
View File
@@ -5,7 +5,8 @@ CLEANFILES = tmux.1.mdoc tmux.1.man cmd-parse.c
# Distribution tarball options.
EXTRA_DIST = \
CHANGES README COPYING example_tmux.conf \
osdep-*.c mdoc2man.awk tmux.1
osdep-*.c mdoc2man.awk tmux.1 \
tools/generate-image-diacritics.sh
dist_EXTRA_tmux_SOURCES = compat/*.[ch]
# Preprocessor flags.
@@ -15,9 +16,6 @@ AM_CPPFLAGS += @XOPEN_DEFINES@ \
-DTMUX_LOCK_CMD='"@DEFAULT_LOCK_CMD@"' \
-DTMUX_TERM='"@DEFAULT_TERM@"' \
-DTMUX_MOUSE=1
if HAVE_TMUX_SOCK
AM_CPPFLAGS += -DTMUX_SOCK='"@TMUX_SOCK@"'
endif
# Additional object files.
LDADD = $(LIBOBJS)
@@ -249,9 +247,9 @@ if HAVE_UTF8PROC
nodist_tmux_SOURCES += compat/utf8proc.c
endif
# Enable sixel support.
if ENABLE_SIXEL
dist_tmux_SOURCES += image.c image-sixel.c
# Enable image support.
if ENABLE_IMAGES
dist_tmux_SOURCES += image.c image-sixel.c image-kitty.c image-fallback.c
endif
# Add bits for fuzzing if enabled.
@@ -260,9 +258,7 @@ check_PROGRAMS = \
fuzz/input-fuzzer \
fuzz/cmd-parse-fuzzer \
fuzz/format-fuzzer \
fuzz/style-fuzzer \
fuzz/layout-fuzzer \
fuzz/json-fuzzer
fuzz/style-fuzzer
fuzz_input_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_input_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
fuzz_cmd_parse_fuzzer_LDFLAGS = $(FUZZING_LIBS)
@@ -271,10 +267,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)
fuzz_json_fuzzer_LDFLAGS = $(FUZZING_LIBS)
fuzz_json_fuzzer_LDADD = $(LDADD) $(tmux_OBJECTS)
endif
# Install tmux.1 in the right format.
+8 -6
View File
@@ -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);
}
}
+7 -3
View File
@@ -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);
}
}
+7 -6
View File
@@ -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);
}
+27 -18
View File
@@ -111,17 +111,6 @@ case "x$DEFAULT_TERM" in
;;
esac
# Allow the socket directory list to be set. It is a colon-separated list of
# directories, tried in order; a leading $ is an environment variable.
AC_ARG_WITH(
socket-path,
AS_HELP_STRING(--with-socket-path=PATHS, set socket directory list),
[TMUX_SOCK=`echo "$withval" | sed 's/[[$]]/$$/g'`],
[TMUX_SOCK=]
)
AM_CONDITIONAL(HAVE_TMUX_SOCK, [test "x$TMUX_SOCK" != x])
AC_SUBST(TMUX_SOCK)
# Do we need fuzzers?
AM_CONDITIONAL(NEED_FUZZING, test "x$enable_fuzzing" = xyes)
@@ -541,15 +530,35 @@ if test "x$enable_cgroups" = xyes; then
fi
fi
# Enable sixel support.
# Enable image support.
AC_ARG_ENABLE(
images,
AS_HELP_STRING(--enable-images, enable Kitty/SIXEL/ASCII images)
)
# Keep the old option as an alias for one compatibility cycle.
AC_ARG_ENABLE(
sixel,
AS_HELP_STRING(--enable-sixel, enable sixel images)
AS_HELP_STRING(--enable-sixel, deprecated alias for --enable-images)
)
if test "x$enable_sixel" = xyes; then
AC_DEFINE(ENABLE_SIXEL)
AC_MSG_WARN([--enable-sixel is deprecated, use --enable-images instead to enable.])
enable_images=yes
fi
AM_CONDITIONAL(ENABLE_SIXEL, [test "x$enable_sixel" = xyes])
if test "x$enable_images" = xyes; then
PKG_CHECK_MODULES(
IMAGE,
[libpng >= 1.6 zlib],
[
AM_CPPFLAGS="$IMAGE_CFLAGS $AM_CPPFLAGS"
CPPFLAGS="$AM_CPPFLAGS $SAVED_CPPFLAGS"
LIBS="$IMAGE_LIBS $LIBS"
],
AC_MSG_ERROR("image support requires libpng 1.6 and zlib")
)
AC_DEFINE(ENABLE_IMAGES)
fi
AM_CONDITIONAL(ENABLE_IMAGES, [test "x$enable_images" = xyes])
# Check for b64_ntop. If we have b64_ntop, we assume b64_pton as well.
AC_MSG_CHECKING(for b64_ntop)
@@ -1148,10 +1157,10 @@ if test "x$enable_asan" = xyes; then
else
AC_MSG_NOTICE([ASAN: off])
fi
if test "x$enable_sixel" = xyes; then
AC_MSG_NOTICE([SIXEL: on])
if test "x$enable_images" = xyes; then
AC_MSG_NOTICE([images: on])
else
AC_MSG_NOTICE([SIXEL: off])
AC_MSG_NOTICE([images: off])
fi
if test "x$enable_debug" = xyes; then
AC_MSG_NOTICE([debug: on])
+14
View File
@@ -3078,6 +3078,17 @@ format_cb_sixel_support(__unused struct format_tree *ft)
#endif
}
/* Callback for image_support. */
static void *
format_cb_image_support(__unused struct format_tree *ft)
{
#ifdef ENABLE_IMAGES
return (xstrdup("1"));
#else
return (xstrdup("0"));
#endif
}
/* Callback for active_window_index. */
static void *
format_cb_active_window_index(struct format_tree *ft)
@@ -3750,6 +3761,9 @@ static const struct format_table_entry format_table[] = {
{ "host_short", FORMAT_TABLE_STRING,
format_cb_host_short
},
{ "image_support", FORMAT_TABLE_STRING,
format_cb_image_support
},
{ "insert_flag", FORMAT_TABLE_STRING,
format_cb_insert_flag
},
-88
View File
@@ -1,88 +0,0 @@
/*
* Fuzz the tmux JSON parser and serializer.
*/
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
struct event_base *libevent;
static void
walk_json(struct json_node *node)
{
struct json_node *child, *object;
const char *string;
int64_t number;
int boolean;
if (node == NULL)
return;
(void)json_get_string(node, &string);
(void)json_get_number(node, &number);
(void)json_get_boolean(node, &boolean);
if (json_get_object(node, &object) == 0) {
(void)json_find(object, "V");
(void)json_find(object, "L");
}
for (child = json_array_first(node); child != NULL;
child = json_array_next(child))
walk_json(child);
}
int
LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
struct json_node *node;
char *input, *serialized, *cause = NULL;
if (size == 0 || size > 1U << 20)
return 0;
input = malloc(size + 1);
if (input == NULL)
return 0;
memcpy(input, data, size);
input[size] = '\0';
node = json_parse(input, &cause);
free(cause);
if (node != NULL) {
walk_json(node);
serialized = json_to_string(node);
free(serialized);
json_destroy_node(node);
}
free(input);
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;
}
-37
View File
@@ -1,37 +0,0 @@
# JSON structural tokens
"{"
"}"
"["
"]"
":"
","
"\""
# JSON values
"true"
"false"
"null"
"0"
"-1"
"123456789"
"\\\""
"\\\\"
"\\n"
"\\u0000"
# tmux layout/control-mode keys
"V"
"L"
"W"
"H"
"X"
"Y"
"T"
"S"
"id"
"result"
"error"
"routes"
"uri"
"name"
"value"
-2
View File
@@ -1,2 +0,0 @@
[libfuzzer]
max_len = 4096
-111
View File
@@ -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;
}
-17
View File
@@ -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["
"@"
-2
View File
@@ -1,2 +0,0 @@
[libfuzzer]
max_len = 1024
+78 -4
View File
@@ -86,6 +86,9 @@ grid_check_is_clear(struct grid *gd)
assert(gl->extdsize == 0);
assert(gl->flags == 0);
assert(gl->time == 0);
#ifdef ENABLE_IMAGES
assert(gl->images == NULL);
#endif
}
}
#else
@@ -353,6 +356,9 @@ grid_free_line(struct grid *gd, u_int py)
assert(gl->cellsize == 0 || gl->celldata != NULL);
#endif
#ifdef ENABLE_IMAGES
image_grid_free_line(gd, gl);
#endif
free(gl->celldata);
free(gl->extddata);
memset(gl, 0, sizeof *gl);
@@ -397,6 +403,9 @@ void
grid_destroy(struct grid *gd)
{
grid_free_lines(gd, 0, gd->hsize + gd->sy);
#ifdef ENABLE_IMAGES
image_grid_free(gd);
#endif
free(gd->linedata);
free(gd);
}
@@ -729,6 +738,9 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
if (nx == 0 || ny == 0)
return;
#ifdef ENABLE_IMAGES
image_grid_damage(gd, px, py, nx, ny);
#endif
if (px == 0 && nx == gd->sx) {
grid_clear_lines(gd, py, ny, bg);
@@ -764,6 +776,10 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg)
void
grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
{
struct grid_line *gl;
#ifdef ENABLE_IMAGES
struct image_line *images;
#endif
u_int yy;
if (ny == 0)
@@ -775,8 +791,18 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg)
return;
for (yy = py; yy < py + ny; yy++) {
grid_free_line(gd, yy);
gl = &gd->linedata[yy];
#ifdef ENABLE_IMAGES
image_grid_damage(gd, 0, yy, gd->sx, 1);
images = gl->images;
#endif
free(gl->celldata);
free(gl->extddata);
memset(gl, 0, sizeof *gl);
grid_empty_line(gd, yy, bg);
#ifdef ENABLE_IMAGES
gl->images = images;
#endif
}
if (py != 0)
gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED;
@@ -841,6 +867,9 @@ grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx,
grid_expand_line(gd, py, px + nx, 8);
grid_expand_line(gd, py, dx + nx, 8);
#ifdef ENABLE_IMAGES
image_grid_move_cells(gd, dx, px, py, nx);
#endif
memmove(&gl->celldata[dx], &gl->celldata[px],
nx * sizeof *gl->celldata);
if (dx + nx > gl->cellused)
@@ -1213,7 +1242,6 @@ grid_string_cells(struct grid *gd, u_int px, u_int py, u_int nx,
grid_get_cell(gd, xx, py, &gc);
if (gc.flags & GRID_FLAG_PADDING)
continue;
if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) {
grid_string_cells_code(*lastgc, &gc, code, sizeof code,
flags, s, &has_link);
@@ -1280,11 +1308,18 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
{
struct grid_line *dstl, *srcl;
u_int yy;
#ifdef ENABLE_IMAGES
u_int original_dy, original_sy;
#endif
if (dy + ny > dst->hsize + dst->sy)
ny = dst->hsize + dst->sy - dy;
if (sy + ny > src->hsize + src->sy)
ny = src->hsize + src->sy - sy;
#ifdef ENABLE_IMAGES
original_dy = dy;
original_sy = sy;
#endif
grid_free_lines(dst, dy, ny);
for (yy = 0; yy < ny; yy++) {
@@ -1292,6 +1327,9 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
dstl = &dst->linedata[dy];
memcpy(dstl, srcl, sizeof *dstl);
#ifdef ENABLE_IMAGES
dstl->images = NULL;
#endif
if (srcl->cellsize != 0) {
dstl->celldata = xreallocarray(NULL,
srcl->cellsize, sizeof *dstl->celldata);
@@ -1311,6 +1349,9 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy,
sy++;
dy++;
}
#ifdef ENABLE_IMAGES
image_grid_duplicate_lines(dst, original_dy, src, original_sy, ny);
#endif
}
/* Mark line as dead. */
@@ -1321,6 +1362,30 @@ grid_reflow_dead(struct grid_line *gl)
gl->flags = GRID_LINE_DEAD;
}
/* Image rows are cell-aligned and must not be reflowed like text. */
static int
grid_reflow_has_image(struct grid_line *gl)
{
struct grid_cell gc;
u_int i;
#ifdef ENABLE_IMAGES
if (image_grid_line_has_images(gl))
return (1);
#endif
if (~gl->flags & GRID_LINE_EXTENDED)
return (0);
for (i = 0; i < gl->cellused; i++) {
grid_get_cell1(gl, i, &gc);
/* Kitty Unicode placeholder base character (U+10EEEE). */
if (gc.data.size >= 4 && gc.data.data[0] == 0xf4 &&
gc.data.data[1] == 0x8e && gc.data.data[2] == 0xbb &&
gc.data.data[3] == 0xae)
return (1);
}
return (0);
}
/* Add lines, return the first new one. */
static struct grid_line *
grid_reflow_add(struct grid *gd, u_int n)
@@ -1383,6 +1448,10 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
break;
line = yy + 1 + lines;
/* Do not join wrapped text to a cell-aligned image row. */
if (grid_reflow_has_image(&gd->linedata[line]))
break;
/* If the next line is empty, skip it. */
if (~gd->linedata[line].flags & GRID_LINE_WRAPPED)
wrapped = 0;
@@ -1443,8 +1512,7 @@ grid_reflow_join(struct grid *target, struct grid *gd, u_int sx, u_int yy,
/* Remove the lines that were completely consumed. */
for (i = yy + 1; i < yy + 1 + lines; i++) {
free(gd->linedata[i].celldata);
free(gd->linedata[i].extddata);
grid_free_line(gd, i);
grid_reflow_dead(&gd->linedata[i]);
}
@@ -1546,6 +1614,12 @@ grid_reflow(struct grid *gd, u_int sx)
if (gl->flags & GRID_LINE_DEAD)
continue;
/* Keep image layers at their original cell coordinates. */
if (grid_reflow_has_image(gl)) {
grid_reflow_move(target, gl);
continue;
}
/*
* Work out the width of this line. at is the point at which
* the available width is hit, and width is the full line
+464
View File
@@ -0,0 +1,464 @@
/* $OpenBSD$ */
/*
* Copyright (c) 2026 Michael Grant <mgrant@grant.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
* IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
* OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include "tmux.h"
/* Character-cell fallback for clients without a graphical image protocol. */
enum image_glyph_detail {
IMAGE_GLYPH_ASCII,
IMAGE_GLYPH_SHADE5,
IMAGE_GLYPH_SHADE8,
IMAGE_GLYPH_HALF,
IMAGE_GLYPH_QUADRANT,
IMAGE_GLYPH_SEXTANT
};
enum image_glyph_palette {
IMAGE_GLYPH_PALETTE_8,
IMAGE_GLYPH_PALETTE_16,
IMAGE_GLYPH_PALETTE_256,
IMAGE_GLYPH_PALETTE_RGB
};
struct image_rgb {
u_char r;
u_char g;
u_char b;
};
struct image_glyph_data {
u_char *shade5;
u_char *shade8;
};
static const struct image_rgb image_ansi_colours[16] = {
{ 0x00, 0x00, 0x00 }, { 0x80, 0x00, 0x00 },
{ 0x00, 0x80, 0x00 }, { 0x80, 0x80, 0x00 },
{ 0x00, 0x00, 0x80 }, { 0x80, 0x00, 0x80 },
{ 0x00, 0x80, 0x80 }, { 0xc0, 0xc0, 0xc0 },
{ 0x80, 0x80, 0x80 }, { 0xff, 0x00, 0x00 },
{ 0x00, 0xff, 0x00 }, { 0xff, 0xff, 0x00 },
{ 0x00, 0x00, 0xff }, { 0xff, 0x00, 0xff },
{ 0x00, 0xff, 0xff }, { 0xff, 0xff, 0xff }
};
static u_int
image_glyph_distance(struct image_rgb a, struct image_rgb b)
{
int r = a.r - b.r, g = a.g - b.g, bl = a.b - b.b;
return (r * r + g * g + bl * bl);
}
static int
image_glyph_low_saturation(struct image_rgb colour)
{
u_int minimum, maximum;
minimum = maximum = colour.r;
if (colour.g < minimum)
minimum = colour.g;
if (colour.b < minimum)
minimum = colour.b;
if (colour.g > maximum)
maximum = colour.g;
if (colour.b > maximum)
maximum = colour.b;
return (maximum == 0 || (maximum - minimum) * 4 <= maximum);
}
static u_int
image_glyph_nearest_ansi(struct image_rgb colour, u_int colours)
{
u_int i, distance, best_distance = UINT_MAX, best = 0;
int neutral = image_glyph_low_saturation(colour);
for (i = 0; i < colours; i++) {
if (neutral && i != 0 && i != 7 &&
(colours != 16 || (i != 8 && i != 15)))
continue;
distance = image_glyph_distance(colour, image_ansi_colours[i]);
if (distance < best_distance) {
best_distance = distance;
best = i;
}
}
return (best);
}
static struct image_rgb
image_glyph_quantize(struct image_rgb colour, enum image_glyph_palette palette,
int *output)
{
struct image_rgb result;
int value;
u_int index;
if (palette == IMAGE_GLYPH_PALETTE_RGB) {
*output = colour_join_rgb(colour.r, colour.g, colour.b);
return (colour);
}
if (palette == IMAGE_GLYPH_PALETTE_256) {
*output = colour_find_rgb(colour.r, colour.g, colour.b);
value = colour_256toRGB(*output);
colour_split_rgb(value, &result.r, &result.g, &result.b);
return (result);
}
index = image_glyph_nearest_ansi(colour,
palette == IMAGE_GLYPH_PALETTE_8 ? 8 : 16);
result = image_ansi_colours[index];
if (index < 8)
*output = index;
else
*output = 90 + index - 8;
return (result);
}
static void
image_glyph_fit_colours(const struct image_rgb *samples, u_int count,
struct image_rgb centres[2])
{
u_int sum[2][3], counts[2], group, i, j, iteration, distance;
u_int maximum = 0, first = 0, second = 0;
for (i = 0; i < count; i++) {
for (j = i + 1; j < count; j++) {
distance = image_glyph_distance(samples[i], samples[j]);
if (distance > maximum) {
maximum = distance;
first = i;
second = j;
}
}
}
centres[0] = samples[first];
centres[1] = samples[second];
for (iteration = 0; iteration < 4; iteration++) {
memset(sum, 0, sizeof sum);
memset(counts, 0, sizeof counts);
for (i = 0; i < count; i++) {
group = image_glyph_distance(samples[i], centres[1]) <
image_glyph_distance(samples[i], centres[0]);
sum[group][0] += samples[i].r;
sum[group][1] += samples[i].g;
sum[group][2] += samples[i].b;
counts[group]++;
}
for (i = 0; i < 2; i++) {
if (counts[i] == 0)
continue;
centres[i].r = (sum[i][0] + counts[i] / 2) / counts[i];
centres[i].g = (sum[i][1] + counts[i] / 2) / counts[i];
centres[i].b = (sum[i][2] + counts[i] / 2) / counts[i];
}
}
}
static int
image_glyph_set_acs(struct tty *tty, struct utf8_data *data, u_char key)
{
struct utf8_data *ud;
const char *s = tty_acs_get(tty, key);
if (s == NULL)
return (0);
ud = utf8_fromcstr(s);
if (ud[0].size == 0) {
free(ud);
return (0);
}
memcpy(data, &ud[0], sizeof *data);
free(ud);
return (1);
}
static u_char
image_glyph_block_key(enum image_glyph_detail detail, u_int mask)
{
static const u_char half[4] = {
0, TTY_ACS_IMAGE_HALF_UPPER, TTY_ACS_IMAGE_HALF_LOWER,
TTY_ACS_IMAGE_BLOCK
};
static const u_char quadrant[16] = {
0, TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, TTY_ACS_IMAGE_HALF_UPPER,
TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, TTY_ACS_IMAGE_HALF_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT,
TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_HALF_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT,
TTY_ACS_IMAGE_HALF_LOWER,
TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT,
TTY_ACS_IMAGE_BLOCK
};
if (detail == IMAGE_GLYPH_HALF)
return (half[mask]);
if (detail == IMAGE_GLYPH_QUADRANT)
return (quadrant[mask]);
if (mask == 0)
return (0);
if (mask == 21)
return (TTY_ACS_IMAGE_HALF_LEFT);
if (mask == 42)
return (TTY_ACS_IMAGE_HALF_RIGHT);
if (mask == 63)
return (TTY_ACS_IMAGE_BLOCK);
return (tty_acs_image_sextant(mask));
}
static u_char *
image_glyph_make_shades(struct image *im, u_int levels)
{
struct image_glyph_data *data = im->fallback_data;
const struct image_cell *cell;
int *values, value, represented, error;
size_t cells, index;
u_int x, y, scan, level, minimum = 255, maximum = 0;
int reverse;
u_char *result;
if (data == NULL) {
data = im->fallback_data = xcalloc(1, sizeof *data);
}
result = (levels == 5 ? data->shade5 : data->shade8);
if (result != NULL)
return (result);
cells = (size_t)im->sx * im->sy;
values = xcalloc(cells, sizeof *values);
result = xcalloc(cells, 1);
for (y = 0; y < im->sy; y++) {
for (x = 0; x < im->sx; x++) {
index = (size_t)y * im->sx + x;
cell = image_get_cell(im, x, y);
value = cell->whole.brightness;
if ((u_int)value < minimum)
minimum = value;
if ((u_int)value > maximum)
maximum = value;
values[index] = value;
}
}
if (maximum > minimum) {
for (index = 0; index < cells; index++)
values[index] = (values[index] - minimum) * 255 /
(maximum - minimum);
}
for (y = 0; y < im->sy; y++) {
reverse = (y & 1);
for (scan = 0; scan < im->sx; scan++) {
x = (reverse ? im->sx - scan - 1 : scan);
index = (size_t)y * im->sx + x;
value = values[index];
if (value < 0)
value = 0;
if (value > 255)
value = 255;
level = (value * (levels - 1) + 127) / 255;
result[index] = level;
represented = level * 255 / (levels - 1);
error = value - represented;
if (!reverse && x + 1 < im->sx)
values[index + 1] += error * 7 / 16;
if (reverse && x != 0)
values[index - 1] += error * 7 / 16;
if (y + 1 == im->sy)
continue;
if (!reverse && x != 0)
values[index + im->sx - 1] += error * 3 / 16;
if (reverse && x + 1 < im->sx)
values[index + im->sx + 1] += error * 3 / 16;
values[index + im->sx] += error * 5 / 16;
if (!reverse && x + 1 < im->sx)
values[index + im->sx + 1] += error / 16;
if (reverse && x != 0)
values[index + im->sx - 1] += error / 16;
}
}
free(values);
if (levels == 5)
data->shade5 = result;
else
data->shade8 = result;
return (result);
}
static enum image_glyph_palette
image_glyph_get_palette(struct tty *tty)
{
int colours;
if (tty->term->flags & TERM_RGBCOLOURS)
return (IMAGE_GLYPH_PALETTE_RGB);
if (tty->term->flags & TERM_256COLOURS)
return (IMAGE_GLYPH_PALETTE_256);
colours = tty_term_number(tty->term, TTYC_COLORS);
if (colours >= 256)
return (IMAGE_GLYPH_PALETTE_256);
if (colours >= 16)
return (IMAGE_GLYPH_PALETTE_16);
return (IMAGE_GLYPH_PALETTE_8);
}
static enum image_glyph_detail
image_glyph_get_detail(struct tty *tty, enum image_glyph_palette palette)
{
if (tty_acs_needed(tty))
return (IMAGE_GLYPH_ASCII);
if (tty->term->flags & TERM_IMAGE_SEXTANTS)
return (IMAGE_GLYPH_SEXTANT);
if (tty->term->flags & TERM_IMAGE_QUADRANTS)
return (IMAGE_GLYPH_QUADRANT);
if (palette != IMAGE_GLYPH_PALETTE_8)
return (IMAGE_GLYPH_HALF);
if (tty_term_has(tty->term, TTYC_NOBR))
return (IMAGE_GLYPH_SHADE5);
return (IMAGE_GLYPH_SHADE8);
}
static void
image_glyph_block(struct tty *tty, struct image *im, u_int x, u_int y,
enum image_glyph_detail detail, enum image_glyph_palette palette,
struct grid_cell *out)
{
const struct image_cell *cell = image_get_cell(im, x, y);
struct image_rgb samples[6], centres[2], quantized[2];
u_int columns, rows, sx, sy, ix, iy, i, n, mask;
u_int x0, x1, y0, y1, red, green, blue, count;
int colours[2];
u_char key;
columns = (detail == IMAGE_GLYPH_HALF ? 1 : 2);
rows = (detail == IMAGE_GLYPH_HALF ? 2 :
detail == IMAGE_GLYPH_QUADRANT ? 2 : 3);
i = 0;
for (sy = 0; sy < rows; sy++) {
for (sx = 0; sx < columns; sx++) {
x0 = sx * IMAGE_SAMPLE_COLUMNS / columns;
x1 = (sx + 1) * IMAGE_SAMPLE_COLUMNS / columns;
y0 = sy * IMAGE_SAMPLE_ROWS / rows;
y1 = (sy + 1) * IMAGE_SAMPLE_ROWS / rows;
red = green = blue = count = 0;
for (iy = y0; iy < y1; iy++) {
for (ix = x0; ix < x1; ix++) {
red += cell->samples[iy][ix].red;
green += cell->samples[iy][ix].green;
blue += cell->samples[iy][ix].blue;
count++;
}
}
samples[i].r = red / count;
samples[i].g = green / count;
samples[i].b = blue / count;
i++;
}
}
n = columns * rows;
image_glyph_fit_colours(samples, n, centres);
quantized[0] = image_glyph_quantize(centres[0], palette, &colours[0]);
quantized[1] = image_glyph_quantize(centres[1], palette, &colours[1]);
mask = 0;
for (i = 0; i < n; i++) {
if (image_glyph_distance(samples[i], quantized[1]) <
image_glyph_distance(samples[i], quantized[0]))
mask |= (1U << i);
}
key = image_glyph_block_key(detail, mask);
if (key == 0)
utf8_set(&out->data, ' ');
else if (!image_glyph_set_acs(tty, &out->data, key))
utf8_set(&out->data, ' ');
out->fg = colours[1];
out->bg = colours[0];
}
void
image_get_fallback_cell(struct tty *tty, struct image *im, u_int x, u_int y,
const struct grid_cell *gc, struct grid_cell *out,
__unused const struct tty_style_ctx *style_ctx)
{
static const char ascii[] = " .:-=+*#%@";
static const u_char shades[5] = { 0, TTY_ACS_IMAGE_SHADE_LIGHT,
TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_DARK,
TTY_ACS_IMAGE_BLOCK };
static const u_char bold_shades[8] = { 0,
TTY_ACS_IMAGE_SHADE_LIGHT, TTY_ACS_IMAGE_SHADE_LIGHT,
TTY_ACS_IMAGE_SHADE_MEDIUM, TTY_ACS_IMAGE_SHADE_MEDIUM,
TTY_ACS_IMAGE_SHADE_DARK, TTY_ACS_IMAGE_BLOCK,
TTY_ACS_IMAGE_BLOCK };
const struct image_cell *cell;
enum image_glyph_palette palette;
enum image_glyph_detail detail;
u_char *levels, key;
u_int level = 0;
memcpy(out, gc, sizeof *out);
cell = image_get_cell(im, x, y);
if (cell == NULL) {
utf8_set(&out->data, ' ');
return;
}
palette = image_glyph_get_palette(tty);
detail = image_glyph_get_detail(tty, palette);
if (detail == IMAGE_GLYPH_ASCII) {
level = cell->whole.brightness * (sizeof ascii - 2) / 255;
utf8_set(&out->data, ascii[level]);
return;
}
if (detail == IMAGE_GLYPH_SHADE5 || detail == IMAGE_GLYPH_SHADE8) {
levels = image_glyph_make_shades(im,
detail == IMAGE_GLYPH_SHADE5 ? 5 : 8);
level = levels[(size_t)y * im->sx + x];
key = (detail == IMAGE_GLYPH_SHADE5 ? shades[level] :
bold_shades[level]);
if (key == 0)
utf8_set(&out->data, ' ');
else if (!image_glyph_set_acs(tty, &out->data, key))
utf8_set(&out->data, ' ');
out->fg = 7;
out->bg = 0;
out->attr &= ~GRID_ATTR_BRIGHT;
if (detail == IMAGE_GLYPH_SHADE8 &&
(level == 2 || level == 4 || level == 7))
out->attr |= GRID_ATTR_BRIGHT;
return;
}
image_glyph_block(tty, im, x, y, detail, palette, out);
}
void
image_free_fallback(struct image *im)
{
struct image_glyph_data *data = im->fallback_data;
if (data == NULL)
return;
free(data->shade5);
free(data->shade8);
free(data);
}
+1411
View File
File diff suppressed because it is too large Load Diff
+1129 -110
View File
File diff suppressed because it is too large Load Diff
+1675 -162
View File
File diff suppressed because it is too large Load Diff
+99 -6
View File
@@ -144,6 +144,9 @@ struct input_ctx {
*/
struct evbuffer *since_ground;
struct event ground_timer;
#ifdef ENABLE_IMAGES
void *kitty_state;
#endif
};
/* Helper functions. */
@@ -182,6 +185,9 @@ static void input_enter_osc(struct input_ctx *);
static void input_exit_osc(struct input_ctx *);
static void input_enter_apc(struct input_ctx *);
static void input_exit_apc(struct input_ctx *);
#ifdef ENABLE_IMAGES
static int input_handle_kitty(struct input_ctx *, const u_char *, size_t);
#endif
static void input_enter_rename(struct input_ctx *);
static void input_exit_rename(struct input_ctx *);
@@ -914,6 +920,9 @@ input_free(struct input_ctx *ictx)
event_del(&ictx->request_timer);
free(ictx->input_buf);
#ifdef ENABLE_IMAGES
kitty_free_state(ictx->kitty_state);
#endif
evbuffer_free(ictx->since_ground);
event_del(&ictx->ground_timer);
@@ -936,6 +945,7 @@ input_reset(struct input_ctx *ictx, int clear)
screen_write_start_pane(sctx, wp, &wp->base);
else
screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
screen_write_reset(sctx);
screen_write_stop(sctx);
}
@@ -1064,6 +1074,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len)
screen_write_start_pane(sctx, wp, &wp->base);
else
screen_write_start(sctx, &wp->base);
sctx->flags |= SCREEN_WRITE_INPUT;
log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id,
ictx->state->name, len, (int)len, buf);
@@ -1083,6 +1094,7 @@ input_parse_screen(struct input_ctx *ictx, struct screen *s,
return;
screen_write_start_callback(sctx, s, cb, arg);
sctx->flags |= SCREEN_WRITE_INPUT;
input_parse(ictx, buf, len);
screen_write_stop(sctx);
}
@@ -1582,7 +1594,7 @@ input_csi_dispatch(struct input_ctx *ictx)
case -1:
break;
case 0:
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
input_reply(ictx, 1, "\033[?1;2;4c");
#else
input_reply(ictx, 1, "\033[?1;2c");
@@ -2100,7 +2112,7 @@ input_csi_dispatch_sm_private(struct input_ctx *ictx)
static void
input_csi_dispatch_sm_graphics(__unused struct input_ctx *ictx)
{
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
int n, m, o;
if (ictx->param_list_len > 3)
@@ -2625,10 +2637,10 @@ input_dcs_dispatch(struct input_ctx *ictx)
const char prefix[] = "tmux;";
const u_int prefixlen = (sizeof prefix) - 1;
long long allow_passthrough = 0;
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
struct window *w;
struct sixel_image *si;
int p2;
int p1, p2;
#endif
if (wp == NULL)
@@ -2641,15 +2653,18 @@ input_dcs_dispatch(struct input_ctx *ictx)
return (0);
}
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
if (wp != NULL && buf[0] == 'q' && ictx->interm_len == 0) {
w = wp->window;
if (input_split(ictx) != 0)
return (0);
p1 = input_get(ictx, 0, 0, 0);
if (p1 == -1)
p1 = 0;
p2 = input_get(ictx, 1, 0, 0);
if (p2 == -1)
p2 = 0;
si = sixel_parse(buf, len, p2, w->xpixel, w->ypixel);
si = sixel_parse(buf, len, p1, p2, w->xpixel, w->ypixel);
if (si != NULL)
screen_write_sixelimage(sctx, si, ictx->cell.cell.bg);
}
@@ -2788,6 +2803,57 @@ input_enter_apc(struct input_ctx *ictx)
}
/* APC terminator (ST) received. */
#ifdef ENABLE_IMAGES
static int
input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len)
{
struct screen_write_ctx *sctx = &ictx->ctx;
struct window_pane *wp = ictx->wp;
struct image *im;
u_int image_id = 0, replace_id = 0, quiet = 0;
u_int placement_id = 0;
int32_t z = 0;
char action = '\0', delete = '\0';
int status;
if (wp == NULL)
return (0);
im = kitty_parse_image(&ictx->kitty_state, buf, len,
wp->window->xpixel,
wp->window->ypixel, &image_id, &replace_id, &quiet, &action,
&delete, &placement_id, &z, &status);
if (status == KITTY_PARSE_MORE)
return (1);
if (status != KITTY_PARSE_OK) {
if (quiet < 2 && action != '\0') {
if (status == KITTY_PARSE_MISSING)
input_reply(ictx, 0, "\033_Gi=%u;ENOENT\033\\",
image_id);
else
input_reply(ictx, 0, "\033_Gi=%u;EINVAL\033\\",
image_id);
}
return (1);
}
if (replace_id != 0)
image_clear(sctx, replace_id);
if (im != NULL) {
if (action == 'd')
image_clear_kitty(sctx, delete, image_id, placement_id, z);
else
image_write_kitty(sctx, im, ictx->cell.cell.bg, image_id,
placement_id, z);
image_free(image_get_id(im));
if (quiet == 0 && image_id != 0)
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
} else if (action == 'd')
image_clear_kitty(sctx, delete, image_id, placement_id, z);
else if ((action == 't' || action == 'q' || action == 'u') && quiet == 0)
input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id);
return (1);
}
#endif
static void
input_exit_apc(struct input_ctx *ictx)
{
@@ -2798,6 +2864,13 @@ input_exit_apc(struct input_ctx *ictx)
return;
log_debug("%s: \"%s\"", __func__, ictx->input_buf);
#ifdef ENABLE_IMAGES
if (ictx->input_len > 1 && ictx->input_buf[0] == 'G' &&
input_handle_kitty(ictx, ictx->input_buf + 1,
ictx->input_len - 1))
return;
#endif
if (wp != NULL &&
options_get_number(wp->options, "allow-set-title") &&
screen_set_title(sctx->s, ictx->input_buf, 1)) {
@@ -2858,6 +2931,12 @@ input_top_bit_set(struct input_ctx *ictx)
{
struct screen_write_ctx *sctx = &ictx->ctx;
struct utf8_data *ud = &ictx->utf8data;
#ifdef ENABLE_IMAGES
struct grid_cell gc;
struct image *im;
u_int x, source_x, source_y, image_id, placement_id;
int32_t z;
#endif
ictx->flags &= ~INPUT_LAST;
@@ -2885,6 +2964,20 @@ input_top_bit_set(struct input_ctx *ictx)
utf8_copy(&ictx->cell.cell.data, ud);
screen_write_collect_add(sctx, &ictx->cell.cell);
#ifdef ENABLE_IMAGES
if (sctx->s->cx != 0) {
x = sctx->s->cx - 1; /* cx-1 is the cell just written. */
grid_view_get_cell(sctx->s->grid, x, sctx->s->cy, &gc);
if (kitty_placeholder_to_image(ictx->kitty_state,
sctx->s->grid, &gc, x, sctx->s->cy, &im, &source_x,
&source_y, &image_id, &placement_id, &z)) {
grid_view_set_cell(sctx->s->grid, x, sctx->s->cy, &gc);
image_place_cell_kitty(sctx, im, x, sctx->s->cy, source_x,
source_y, image_id, placement_id, z);
}
}
#endif
utf8_copy(&ictx->last, &ictx->cell.cell.data);
ictx->flags |= INPUT_LAST;
+1 -1
View File
@@ -543,7 +543,7 @@ layout_append(struct layout_cell *lcroot, struct layout_string *ls, int flags)
if (flags & LAYOUT_CUSTOM_OLD_FORMAT) {
if (!layout_cell_is_tiled(lcroot) &&
!layout_cell_has_tiled_child(lcroot))
return (-1);
return (0); /* no tiled cell anywhere: empty v1 body */
lccompat = layout_custom_create_compat(lcroot);
result = layout_append_v1(lccompat, ls);
layout_custom_free_compat(lccompat);
+1 -1
View File
@@ -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)
+13 -18
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: mode-tree.c,v 1.103 2026/09/24 18:59:00 nicm Exp $ */
/* $OpenBSD: mode-tree.c,v 1.102 2026/09/21 12:14:32 nicm Exp $ */
/*
* Copyright (c) 2017 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -277,22 +277,11 @@ mode_tree_free_items(struct mode_tree_list *mtl)
static void
mode_tree_check_selected(struct mode_tree_data *mtd)
{
if (mtd->height == 0)
return;
/* If the list has shrunk, do not leave empty lines at the bottom. */
if (mtd->line_size <= mtd->height)
mtd->offset = 0;
else if (mtd->offset > mtd->line_size - mtd->height)
mtd->offset = mtd->line_size - mtd->height;
/*
* If the current line would now be off screen move the offset so it is
* visible.
* If the current line would now be off screen reset the offset to the
* last visible line.
*/
if (mtd->current < mtd->offset)
mtd->offset = mtd->current;
else if (mtd->current > mtd->offset + mtd->height - 1)
if (mtd->current > mtd->height - 1)
mtd->offset = mtd->current - mtd->height + 1;
}
@@ -512,14 +501,20 @@ mode_tree_set_current(struct mode_tree_data *mtd, uint64_t tag)
if (mode_tree_get_tag(mtd, tag, &found)) {
mtd->current = found;
mode_tree_check_selected(mtd);
if (mtd->current > mtd->height - 1)
mtd->offset = mtd->current - mtd->height + 1;
else
mtd->offset = 0;
return (1);
}
if (mtd->current >= mtd->line_size) {
if (mtd->line_size == 0)
return (0);
mtd->current = mtd->line_size - 1;
mode_tree_check_selected(mtd);
if (mtd->current > mtd->height - 1)
mtd->offset = mtd->current - mtd->height + 1;
else
mtd->offset = 0;
}
return (0);
}
@@ -688,13 +683,13 @@ mode_tree_build(struct mode_tree_data *mtd)
if (mtd->line_list != NULL && tag == UINT64_MAX)
tag = mtd->line_list[mtd->current].item->tag;
mode_tree_set_current(mtd, tag);
mtd->width = screen_size_x(s);
if (mtd->preview != MODE_TREE_PREVIEW_OFF)
mode_tree_set_height(mtd);
else
mtd->height = screen_size_y(s);
mode_tree_set_current(mtd, tag);
mode_tree_check_selected(mtd);
}
+1
View File
@@ -552,6 +552,7 @@ const struct options_table_entry options_table[] = {
.scope = OPTIONS_TABLE_SERVER,
.flags = OPTIONS_TABLE_IS_ARRAY,
.default_str = "xterm*:clipboard:ccolour:cstyle:focus:title,"
"xterm-kitty*:kitty,"
"screen*:title,"
"rxvt*:ignorefkeys",
.separator = ",",
+3 -5
View File
@@ -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
+79 -113
View File
@@ -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
+151
View File
@@ -0,0 +1,151 @@
#!/bin/sh
# Regression test for a border-status caching bug: window_make_pane_status()
# (window-border.c) gates the physical redraw of a pane's border-status
# title on a logical content diff (grid_compare against a cached copy), not
# on whether the physical screen cells were disturbed by something else in
# the meantime - such as a floating pane's own border, drawn on top of a
# tiled pane's border-status row, sliding across it and then away again.
# See tmux-image-redraw-known-bugs.md ("border-status text cache ignores
# physical damage") for the full write-up.
#
# Reproduction: a tiled pane with pane-border-status on has a floating pane
# dragged, by mouse, from directly over its border-status row to somewhere
# else. The tiled pane's title should reappear once the floating pane has
# moved off it; without the fix it stays blank.
#
# The drag starts and ends away from row 0 rather than grabbing the
# floating pane's border while it is already sitting on row 0: when a
# floating pane's own border-status row exactly coincides with the tiled
# pane's row 0, mouse hit-testing on that row attributes clicks to the
# tiled pane, not the floating one on top of it (an unrelated tmux quirk,
# not what this test is about). That only matters for the initial press,
# though - once a drag is under way, further motion events go straight to
# the already-bound per-pane callback without re-resolving which pane owns
# the coordinates, so starting the grab on an unambiguous row and dragging
# through row 0 works fine.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag COL ROW [ROW ...]
#
# Write one continuous mouse-1 gesture at column COL: a press at the first
# ROW, a drag update at each subsequent ROW in turn, and a release at the
# last ROW - all 1-based positions sent to the outer pane holding the
# inner client. This matches the default MouseDown1Border/MouseDrag1Border
# bindings used to move or resize a floating pane by its border.
#
# This must be one continuous press/drag/.../release gesture, not several
# separate drag() calls chained together: releasing clears the per-pane
# drag callback binding, so a later press has to be re-hit-tested from
# scratch, which only works reliably away from row 0 (see the header
# comment). A drag already under way is not re-hit-tested per motion
# event, so it can safely pass through row 0 as an intermediate waypoint.
drag()
{
col="$1"
shift
row="$1"
shift
seq=$(printf '\033[<0;%s;%sM' "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
while [ $# -gt 1 ]; do
row="$1"
shift
seq=$(printf '\033[<32;%s;%sM' "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.15
done
row="$1"
seq=$(printf '\033[<0;%s;%sm' "$col" "$row")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on
$TMUX set -g pane-border-status top
$TMUX set -g default-command 'sh -c "sleep 100"'
BASE=$($TMUX list-panes -F '#{pane_id}' | head -1)
[ -n "$BASE" ] || fail "No base pane."
$TMUX select-pane -t "$BASE" -T TILEDTITLE || fail "set base title failed"
# Float a pane well clear of row 0 to start - its own top border must not
# coincide with the base pane's border-status row for the initial mouse
# press to unambiguously hit it (see the header comment). -x/-y are the
# pane's outer size including its 2-cell border frame, so request 2 more
# than the desired 14x3 content area (see layout_floating_args_parse()).
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5 -T FLOATTITLE) ||
fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Sanity check: the base pane's title is visible now (the float starts
# well clear of row 0).
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
head -1 $TMP | grep -q TILEDTITLE ||
fail "base title not visible before the drag - test setup is wrong"
# Drag the float up by its top border (an unambiguous row - not row 0) so
# it ends up covering row 0, hiding the base pane's title behind its own
# border, then continue the same drag back down again so it ends clear of
# row 0 once more - one continuous gesture the whole way (see drag()'s
# comment for why).
GRABCOL=$((FLEFT + FWIDTH / 2))
waypoints="$FTOP"
row=$FTOP
while [ "$row" != 1 ]; do
row=$((row - 1))
waypoints="$waypoints $row"
done
while [ "$row" != "$FTOP" ]; do
row=$((row + 1))
waypoints="$waypoints $row"
done
drag $((GRABCOL + 1)) $waypoints
drag $((GRABCOL + 1)) 1 $((GRABCOL + 1)) "$FTOP"
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
# The base pane's title should be visible again now the floating pane has
# moved off its border-status row. This is expected to fail before the fix
# - see the header comment.
head -1 $TMP | grep -q TILEDTITLE ||
fail "base pane title still blank after floating pane moved away"
exit 0
+71
View File
@@ -0,0 +1,71 @@
#!/bin/sh
# Regression test for a fixed bug: on a client detected as SIXEL-capable, a
# pane-content redraw (PANE_REDRAW) erased the image backend's rectangle
# using the redrawing pane's raw nominal geometry (wp->xoff/yoff/sx/sy)
# instead of the cells it actually owns in the current scene. When a
# floating pane occluded part of that rectangle, the erase blanked the
# floating pane's on-screen area, and since the subsequent text redraw
# correctly only refills cells the redrawing pane owns, nothing repainted
# it back in - the floating pane's content stayed blank.
#
# Fixed in screen-redraw.c:redraw_draw_pane_lines() by erasing only the
# cell ranges the pane actually owns per line (its REDRAW_SPAN_PANE spans),
# not its raw rectangle. See tmux-sixel-erase-ignores-occlusion.md and
# IMAGE-REDRAW-DISCUSSION.md for the full write-up.
#
# Confirmed by direct A/B test against this exact sequence: reliably wipes
# the floating pane's content on the unfixed code, reliably leaves it
# intact on the fixed code.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Lifcw$$ -f/dev/null"
TMUX2="$TEST_TMUX -Lifcw-inner$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15
$TMUX2 new-session -d -x 60 -y 20 "sh -c 'i=0; while [ \$i -lt 30 ]; do printf \"TILE%02d\\n\" \$i; i=\$((i + 1)); done; exec sleep 100'" \
|| exit 1
$TMUX2 new-pane -x20 -y6 -X8 -Y3 -T FLOATTITLE "sh -c 'printf FLOATCONTENT; exec sleep 100'" \
|| exit 1
[ "$($TMUX2 display-message -p '#{image_support}')" = 0 ] && exit 0
# The attaching client must be detected as SIXEL-capable - this bug does
# not reproduce on a fallback or Kitty client.
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 60 -y 20 || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
# Sanity check: the floating pane's content is visible before anything
# disturbs it.
$TMUX capturep -p >$TMP || exit 1
grep -q 'FLOATCONTENT' $TMP || exit 1
# Click the tiled pane (making it active), enter copy-mode scrolled up
# (matching a PageUp binding), move up one line (matching an Up binding),
# then exit copy-mode - the exit is what triggers window_pane_reset_mode(),
# which is what actually caused the erase.
$TMUX2 select-pane -t%0 || exit 1
sleep 1
$TMUX2 copy-mode -u -t%0 || exit 1
sleep 1
$TMUX2 send-keys -t%0 -X cursor-up || exit 1
sleep 1
$TMUX2 send-keys -t%0 -X cancel || exit 1
sleep 1
$TMUX capturep -p >$TMP || exit 1
grep -q 'FLOATCONTENT' $TMP || exit 1
exit 0
+101
View File
@@ -0,0 +1,101 @@
#!/bin/sh
# Regression test for the imagescroll terminal-feature (tty-features.c),
# Kitty side - see image-sixel-region-scroll.sh for the SIXEL side of the
# same feature.
#
# The Kitty backend used to hardcode IMAGE_BACKEND_SCROLLS unconditionally
# (image.c), so tmux always trusted the terminal to have moved a Kitty
# placement along with the rest of a scrolling region, with no way to turn
# that assumption off. At least Windows Terminal does not actually do
# this: it drops the placement entirely on a plain newline- or
# reverse-index-driven scroll (though not on a delete-line/insert-line
# scroll simulation, which goes through a different, always-redraw path).
# imagescroll now gates Kitty exactly like SIXEL: scrolling must not
# retransmit the placement when the client's terminal has imagescroll (the
# previously unconditional behaviour), and must retransmit it without (the
# default for any terminal not individually confirmed and added to
# tty-features.c's table) - the escape hatch this test exists to prove is
# actually wired up for Kitty too.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
IMAGEID=424242
HEADER=$(printf '\033_Ga=T,q=2,f=32,s=1,v=1,i=%s;/wAA/w==\033\\' "$IMAGEID")
# A small image near the top of a tall pane, then enough plain output below
# it to force several ordinary (no DECSTBM sub-region) linefeed scrolls,
# each one shifting the image rows along with everything else.
$TMUX new-session -d -s inner -x 40 -y 20 "printf '$HEADER'; exec sh" ||
exit 1
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:kitty' || exit 1
$TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:kitty@' || fail "disable outer kitty failed"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 2
grep -qa "$(printf '\033_Ga=p')" "$TMP" || fail "sanity: image never reached the client"
# --- Phase 1: imagescroll granted - expect no APC. ---
$TMUX set -as terminal-features ',*:imagescroll' || fail "grant imagescroll failed"
sleep 0.5
: >"$TMP"
$TMUX send-keys -t inner -l "yes | head -n 30" || fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
n_on=$(grep -ac "$(printf '\033_Ga=p')" "$TMP")
[ "$n_on" -eq 0 ] ||
fail "image was retransmitted ($n_on times) scrolling with imagescroll granted"
# --- Phase 2: imagescroll not granted (the default) - expect the image
# back, then more scrolling to redraw it. ---
$TMUX send-keys -t inner -l "printf '$HEADER'" || fail "resend image failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1.5
grep -qa "$(printf '\033_Ga=p')" "$TMP" || fail "sanity: image did not reappear before phase 2"
$TMUX set -as terminal-features ',*:imagescroll@' || fail "revoke imagescroll failed"
sleep 0.5
: >"$TMP"
$TMUX send-keys -t inner -l "yes | head -n 30" || fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
n_off=$(grep -ac "$(printf '\033_Ga=p')" "$TMP")
[ "$n_off" -gt 0 ] ||
fail "image was not retransmitted scrolling without imagescroll - expected the default always-redraw behaviour"
exit 0
+110
View File
@@ -0,0 +1,110 @@
#!/bin/sh
# Regression test: an image wider than the pane at the time it was
# displayed must show more of itself once the pane grows wide enough,
# instead of staying clipped to its original width forever.
#
# image_write() (image.c) clips an image's width to whatever fit in the
# pane when it was first displayed, and never revisits that decision -
# unlike height, which recovers naturally through ordinary scrollback,
# there is no "scroll right", so the clipped columns were permanently
# discarded. window_pane_resize() (window.c) now calls
# image_grid_resize_width() after a pane grows wider, which extends each
# existing placement's spans - using the image's own retained, immutable
# pixel data - up to whichever is smaller: the image's full width or the
# new pane width. See tmux-image-redraw-known-bugs.md for the full
# write-up.
#
# This is checked via the SIXEL raster widths reported in the client's raw
# output before and after widening the window. The redraw triggered by the
# resize is damage-based (only the newly-uncovered columns are dirtied), so
# it does not redraw the whole row as one wider raster - it sends the
# already-correct clipped portion's width again untouched, plus a *second*,
# separate raster covering just the newly-added columns. So rather than
# looking for a single wider raster, this checks that the widths seen
# across both redraws, added together, account for the fixture's full
# pixel width - i.e. the previously-clipped remainder actually appeared.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
FIXTURE=$(pwd)/monkey-2.sixel.txt
FULL=$(grep -oa '"1;1;[0-9]*;[0-9]*' $FIXTURE | head -1 | cut -d';' -f3)
[ -n "$FULL" ] || fail "could not read fixture's own raster width"
# The fixture is 360px wide - narrow enough that a 12-column pane clips it,
# comfortably below the 360px it would take to show in full.
$TMUX new-session -d -s inner -x 12 -y 40 "cat '$FIXTURE'; exec sh" || exit 1
sleep 0.5
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:sixel' || exit 1
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which sends the image) has already happened, missing it.
$TMUX2 new-session -d -x 12 -y 40 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# The outer client is just another tmux client receiving raw PTY output
# that happens to contain sixel DCS sequences - left alone, it would
# independently decode and place its own second, differently-sized image
# on top of the inner session's, confounding the raster-width measurement
# below. Disable sixel on the outer client so only the inner session's
# placement exists.
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
before=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -n | tail -1)
[ -n "$before" ] || fail "sanity: image never reached the client"
[ "$before" -lt 360 ] || fail "sanity: image was not clipped by the narrow pane ($before)"
: >$TMP
# Widen the window well past the image's full width - resize the outer
# client, since the inner session's displayed size follows whatever
# terminal size its attached client actually has.
$TMUX2 resize-window -t "$OUTER" -x 30 -y 40 || fail "resize-window failed"
sleep 1
$TMUX refresh-client || fail "refresh-client failed"
sleep 1
# The widths seen after the resize, added together (deduplicated - the
# same chunk may legitimately be retransmitted), should account for the
# fixture's full pixel width: this is expected to fail before the fix,
# where the newly-uncovered columns are never redrawn at all and only the
# original clipped width ever appears - see the header comment.
after_total=$(grep -oa '"1;1;[0-9]*;' $TMP | grep -o '[0-9]*' | sort -nu |
awk '{s+=$1} END{print s+0}')
[ "$after_total" -gt 0 ] ||
fail "no image raster was sent at all after widening the window"
[ "$after_total" -ge $((FULL - 20)) ] ||
fail "image stayed clipped at ${before}px after widening the window (widths summed to only ${after_total}px, expected close to ${FULL}px)"
exit 0
+114
View File
@@ -0,0 +1,114 @@
#!/bin/sh
# Regression test for a tmux bug (not a terminal bug): image_redraw_scroll()
# (image.c) marks the whole scroll region as damage on every scroll, and
# redraw_client_damage_rect()'s skip_images optimization (screen-redraw.c)
# skips retransmitting an image trusted to have moved with the scroll - but
# the ordinary REDRAW_TEXT pass for that same damage rectangle still ran
# unconditionally, drawing blank grid content directly over the image's
# cells. Text is normally drawn first and images composited on top
# immediately after in the same batch, so that is a harmless intermediate
# state - skip_images only ever suppressed that second, correcting pass,
# leaving the blank draw as the final state. This erased a correctly
# scrolled image, independent of anything the terminal itself did - this is
# what a user saw as "the image disappears when the pane scrolls in Windows
# Terminal" even with imagescroll and margins both granted, and could never
# be reproduced by any terminal-side test because the bug is entirely
# server side.
#
# redraw_draw_pane_span() now consults image_grid_next_span() to skip
# drawing text over any x-range with an image span attached, whenever
# skip_images is set, logging "skipping A-B on row N (image)" whenever it
# does - that log line, and the ability to exclude an image's columns from
# a text draw at all, do not exist before this fix, so its mere presence
# is enough to fail outright on any earlier tmux build.
#
# The image is placed comfortably mid-pane (not at the very top) and wide
# enough to span the whole pane, so it is still on screen - not already
# scrolled into history - by the time a real scroll's damage gets composed.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -vv -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
trap "cleanup; cd /; rm -rf $DIR" 0 1 15
WIDTH=40
HEIGHT=20
ROW=10
# A sixel image exactly WIDTH cells wide (at a typical ~9px cell, 360px) and
# one cell tall, placed at screen row ROW - comfortably mid-pane, not at
# the very top, so it stays visible (not yet scrolled into history) through
# the first several scrolls.
HEADER='\033Pq"1;1;360;18#0;2;100;100;100#0!360~-!360~-!360B\033\\'
# Pad with blank lines after the image so the shell prompt lands at the
# bottom of the pane - without this, the very first scroll from a
# not-yet-settled cursor position falls back to tty_redraw_region() (a
# different, unrelated path that skip_images never applies to anyway), and
# the fix's own exclusion logic never gets a chance to run at all.
PAD=$((HEIGHT - ROW - 2))
$TMUX new-session -d -s inner -x $WIDTH -y $HEIGHT \
"printf '\\033[$((ROW + 1));1H$HEADER'; i=0; while [ \$i -lt $PAD ]; do echo; i=\$((i + 1)); done; exec sh" ||
exit 1
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -g pane-scrollbars on || exit 1
$TMUX set -as terminal-features ',*:sixel' || exit 1
$TMUX set -as terminal-features ',*:margins' || exit 1
$TMUX set -as terminal-features ',*:imagescroll' || exit 1
$TMUX2 new-session -d -x $WIDTH -y $HEIGHT || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
TMP=$(mktemp)
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
grep -qa '"1;1;360;18' "$TMP" || fail "sanity: image never reached the client"
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
BEFORE=$(wc -l <"$LOG")
: >"$TMP"
# The image (row ROW, 10 of 20) has plenty of room above it - this stays
# well within the visible pane for the whole burst.
$TMUX send-keys -t inner -l "yes | head -n 10" || fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
n_dcs=$(grep -ac "$(printf '\033P')" "$TMP")
[ "$n_dcs" -eq 0 ] ||
fail "sanity: image was retransmitted ($n_dcs times) - imagescroll's trust did not engage, so this run cannot test what it is meant to"
tail -n +$((BEFORE + 1)) "$LOG" | grep -q "redraw_draw_pane_span: skipping .* (image)" ||
fail "the text-redraw pass never excluded the image's cells while trusting a scroll to have preserved it - this build would draw blank content over the image and erase it"
exit 0
+107
View File
@@ -0,0 +1,107 @@
#!/bin/sh
# Regression test: an image taller than the pane, at the moment it is first
# displayed, must remain visible when scrolling back up through history -
# not show blank space for the part that was scrolled off screen before the
# user ever got to see it.
#
# image_write() (image.c) handles this case by scrolling the screen up
# (via screen_write_scrollup()) to make room, then only ever wrote spans
# for the rows that ended up on screen afterwards - the rows that were
# immediately scrolled off (origin_y of them) were pushed into history as
# blank, spanless rows and their image data was discarded. Unlike width,
# which has no "scroll right" to recover a permanent clip, height already
# has ordinary scrollback, so this was pure waste. image_write() now also
# calls image_extend_row() for those origin_y history rows (capped to
# gd->hsize, since screen_write_scrollup() may not have pushed a real
# history row for every one of them - see tmux-uint-subtraction-underflow-
# care.md), so scrolling back up recovers the image instead of showing
# empty space. See tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by comparing how many separate SIXEL rasters the client
# receives right after entering copy mode (a baseline - copy mode itself
# triggers a redraw of the still-visible rows) against how many it
# receives after scrolling to the very top of history: with the fix, more
# rasters appear (covering the newly-revealed, previously-scrolled-off
# rows); without it, scrolling reveals nothing new.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
FIXTURE=$(pwd)/monkey-2.sixel.txt
# A short (5-row) pane and a wide (30-column) one - wide enough that the
# fixture's width is not clipped, so only the height/scrolloff behaviour is
# exercised, and short enough that most of the image is scrolled off
# screen while it is still being displayed.
$TMUX new-session -d -s inner -x 30 -y 5 "cat '$FIXTURE'; exec sh" || exit 1
sleep 0.5
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:sixel' || exit 1
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which sends the image) has already happened, missing it.
$TMUX2 new-session -d -x 30 -y 5 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# The outer client is just another tmux client receiving raw PTY output
# that happens to contain sixel DCS sequences - left alone, it would
# independently decode and place its own second image, confounding the
# raster counts below. Disable sixel on the outer client so only the
# inner session's placement exists.
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
[ "$($TMUX display-message -p '#{history_size}')" -gt 0 ] ||
fail "sanity: image did not push anything into history - pane wasn't short enough"
# Enter copy mode and measure the baseline raster count this alone causes
# (redrawing whatever's still on screen), without having scrolled yet.
: >$TMP
$TMUX copy-mode -t inner || fail "copy-mode failed"
sleep 1
baseline=$(grep -oac '"1;1;[0-9]*;[0-9]*' $TMP)
# Scroll all the way to the top of history - this is expected to fail
# before the fix (no more rasters than the baseline ever appear, since the
# scrolled-off rows were left blank) - see the header comment.
: >$TMP
$TMUX send-keys -t inner -X history-top || fail "history-top failed"
sleep 1
after=$(grep -oac '"1;1;[0-9]*;[0-9]*' $TMP)
[ "$after" -gt 0 ] ||
fail "no image raster was sent at all after scrolling to history-top"
[ "$after" -ge "$baseline" ] ||
fail "scrolling to history-top redrew fewer rasters ($after) than just entering copy mode did ($baseline) - unexpected either way"
exit 0
+142
View File
@@ -0,0 +1,142 @@
#!/bin/sh
# Regression test: with imagescroll granted, does tmux actually use a native
# scrolling-region command (DECSTBM, and DECSLRM if the pane isn't full
# width) to perform the scroll, rather than tty_redraw_region()'s fallback
# full manual repaint? This directly checks the concern raised in PR review
# that tmux "isn't using scrolling regions" for an image-bearing pane -
# imagescroll only makes sense to grant a terminal if tmux is actually
# relying on a real native scroll to carry the image along, not silently
# falling back to redrawing every line by hand while still skipping the
# image retransmission (which would just lose the image for nothing).
#
# tty_redraw_region() (tty.c) logs one of two lines whenever it runs at
# all - "%s large region redraw" or "%s small region redraw" - so their
# total absence after the scroll proves the native path
# (tty_cmd_linefeed()/scrollup()/scrolldown()/reverseindex()) was taken
# instead. Combined with image-sixel-region-scroll.sh's own check (the
# image is not retransmitted), this proves imagescroll's trust is actually
# backed by a real native scroll, not a coincidence.
#
# Phase 2 is the deliberate opposite case, and is what PR review's concern
# actually described: pane-scrollbars makes this pane less than full width,
# and without margins granted, tty_cmd_*()'s own
# "(!tty_full_width(tty, ctx) && !tty_use_margin(tty))" check has no native
# scrolling-region option left and must fall back - proving phase 1 wasn't
# trivially passing, and that redraw_image_scroll_result()'s failure path
# correctly also stops imagescroll's trust from applying when that happens
# (the image must be retransmitted here, exactly as
# image-sixel-region-scroll.sh's own phase 2 already separately proves).
#
# A real attached client is needed for any of this redraw code to run at
# all, so this uses the same nested inner/outer tmux pattern as
# image-sixel-region-scroll.sh.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -vv -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
trap "cleanup; cd /; rm -rf $DIR" 0 1 15
WIDTH=40
HEIGHT=20
HEADER='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
$TMUX new-session -d -s inner -x $WIDTH -y $HEIGHT \
"printf '$HEADER'; exec sh" || exit 1
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -g pane-scrollbars on || exit 1
$TMUX set -as terminal-features ',*:sixel' || exit 1
$TMUX set -as terminal-features ',*:imagescroll' || exit 1
$TMUX2 new-session -d -x $WIDTH -y $HEIGHT || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
TMP=$(mktemp)
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image never reached the client"
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
run_scroll()
{
label=$1
BEFORE=$(wc -l <"$LOG")
: >"$TMP"
$TMUX send-keys -t inner -l "yes | head -n 10" || fail "$label: send scroll failed"
$TMUX send-keys -t inner Enter || fail "$label: send enter failed"
sleep 1
n_dcs=$(grep -ac "$(printf '\033P')" "$TMP")
n_redraw=$(tail -n +$((BEFORE + 1)) "$LOG" | grep -c "region redraw")
}
# --- Phase 1: margins granted - real native scrolling region expected,
# image never retransmitted. ---
$TMUX set -as terminal-features ',*:margins' || fail "grant margins failed"
sleep 0.3
$TMUX display-message -p '#{client_termfeatures}' |
grep -q imagescroll || fail "sanity: imagescroll was not granted to the client"
run_scroll "phase 1 (margins granted)"
[ "$n_dcs" -eq 0 ] ||
fail "phase 1: image was retransmitted ($n_dcs times) - imagescroll's trust did not engage, so this run cannot test what it is meant to"
[ "$n_redraw" -eq 0 ] ||
fail "phase 1: tty_redraw_region() fired ($n_redraw times) while scrolling an image-bearing pane with margins+imagescroll granted - tmux fell back to a full manual repaint instead of a real native scroll, so trusting the terminal to have moved the image was never justified"
# --- Phase 2: margins revoked - the pane still isn't full width
# (pane-scrollbars is on), so this is the scenario PR review's "not using
# scrolling regions" concern actually described: no native scrolling-region
# option is available, tty_redraw_region() must fire, and
# redraw_image_scroll_result()'s failure path must in turn stop
# imagescroll's trust from applying, so the image is retransmitted instead
# of silently lost. Resend the image first - phase 1's scrolling already
# pushed the original one (26 pixels tall, a couple of rows at most) out of
# the pane, so without this there would be nothing left to retransmit
# either way, and the check below would pass for the wrong reason. ---
$TMUX set -as terminal-features ',*:margins@' || fail "revoke margins failed"
sleep 0.3
: >"$TMP"
$TMUX send-keys -t inner -l "printf '$HEADER'" || fail "resend image failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1.5
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image did not reappear before phase 2"
run_scroll "phase 2 (margins revoked)"
[ "$n_redraw" -gt 0 ] ||
fail "sanity: without margins, scrolling a scrollbar-enabled image-bearing pane never fell back to tty_redraw_region() - this scenario no longer exercises the bug phase 1 checks for"
[ "$n_dcs" -gt 0 ] ||
fail "phase 2: image was not retransmitted after tty_redraw_region() fired - imagescroll's trust incorrectly stayed engaged despite the fallback, which would silently lose the image"
exit 0
+100
View File
@@ -0,0 +1,100 @@
#!/bin/sh
# Regression test: extending a copy-mode selection by cursor movement, with
# the view otherwise unmoved (no scrolling), must not retransmit an image
# whose row the cursor passes through.
#
# window_copy_write_one() (window-copy.c) used to write text/highlight
# styling directly over image-covered cells, which - since a character
# write typically clears whatever pixel content a terminal was showing
# there - erased the image with nothing to redraw it back in. Separately,
# window_copy_write_line()'s call to image_redraw_area() used to fire
# unconditionally on every redraw, so even after fixing the erasure, the
# image would still be needlessly recomposited (and briefly flash) on
# every single cursor step even though nothing about it had changed. See
# tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by counting DCS (\033P) sequences in the client's raw
# output during the cursor movement: with the fix, extending a selection
# without scrolling never touches the image, so none should appear.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
# A small, distinctive SIXEL raster (26x26 pixels) at the top of the pane,
# matching the fixture already used in image-support.sh, followed by
# enough plain lines that the cursor can move down through the image and
# past it without the view needing to scroll.
SIXEL='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
$TMUX new-session -d -s inner -x 40 -y 20 \
"printf '$SIXEL'; for i in \$(seq 1 15); do echo line\$i; done; exec sh" ||
exit 1
sleep 0.5
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:sixel' || exit 1
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which sends the image) has already happened, missing it.
$TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
# Sanity check: the image reached the client at all.
grep -qa '"1;1;26;26' $TMP || fail "sanity: image never reached the client"
: >$TMP
# Enter copy-mode, scroll to the top (where the image is) and select down
# through it one cursor step at a time - the view does not need to scroll
# for any of this, since the image is already at the top of what is
# visible.
$TMUX copy-mode -t inner || fail "copy-mode failed"
$TMUX send-keys -X history-top || fail "history-top failed"
sleep 0.2
: >$TMP
$TMUX send-keys -X begin-selection || fail "begin-selection failed"
i=0
while [ $i -lt 6 ]; do
$TMUX send-keys -X cursor-down || fail "cursor-down failed"
sleep 0.15
i=$((i + 1))
done
sleep 0.5
# No DCS sequence should have been sent - the image's row was never
# disturbed by any of this. This is expected to fail before the fix - see
# the header comment.
dcs=$(grep -ac "$(printf '\033P')" $TMP)
[ "$dcs" -eq 0 ] ||
fail "image was retransmitted ($dcs times) while just moving the selection cursor"
exit 0
+96
View File
@@ -0,0 +1,96 @@
#!/bin/sh
# Regression test for the imagescroll terminal-feature (tty-features.c),
# SIXEL side - see image-kitty-region-scroll.sh for the Kitty side of the
# same feature: scrolling a pane that has a SIXEL image in it must not
# retransmit the image when the client's terminal has imagescroll - tmux
# trusts the terminal to have moved the image along with the rest of the
# scrolling region. Without imagescroll (the default for any terminal not
# individually confirmed and added to tty-features.c's table), today's
# always-redraw-on-scroll behaviour is unchanged.
#
# There is no way to query a terminal for whether it actually moves SIXEL
# pixels along with a scroll, so this only proves tmux's own decision to
# skip or redraw is wired correctly - not that any particular terminal
# renders the result correctly. That needs a human, on real terminals.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
HEADER='\033Pq"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
# A small image near the top of a tall pane, then enough plain output below
# it to force several ordinary (no DECSTBM sub-region) linefeed scrolls,
# each one shifting the image rows along with everything else.
$TMUX new-session -d -s inner -x 40 -y 20 "printf '$HEADER'; exec sh" ||
exit 1
sleep 0.3
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
$TMUX set -as terminal-features ',*:sixel' || exit 1
$TMUX2 new-session -d -x 40 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 set -as terminal-features ',*:sixel@' || fail "disable outer sixel failed"
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 2
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image never reached the client"
# --- Phase 1: imagescroll granted - expect no DCS. ---
$TMUX set -as terminal-features ',*:imagescroll' || fail "grant imagescroll failed"
sleep 0.5
: >"$TMP"
$TMUX send-keys -t inner -l "yes | head -n 30" || fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
n_on=$(grep -ac "$(printf '\033P')" "$TMP")
[ "$n_on" -eq 0 ] ||
fail "image was retransmitted ($n_on times) scrolling with imagescroll granted"
# --- Phase 2: imagescroll not granted (the default) - expect the image
# back, then more scrolling to redraw it. ---
$TMUX send-keys -t inner -l "printf '$HEADER'" || fail "resend image failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1.5
grep -qa '"1;1;26;26' "$TMP" || fail "sanity: image did not reappear before phase 2"
$TMUX set -as terminal-features ',*:imagescroll@' || fail "revoke imagescroll failed"
sleep 0.5
: >"$TMP"
$TMUX send-keys -t inner -l "yes | head -n 30" || fail "send scroll failed"
$TMUX send-keys -t inner Enter || fail "send enter failed"
sleep 1
n_off=$(grep -ac "$(printf '\033P')" "$TMP")
[ "$n_off" -gt 0 ] ||
fail "image was not retransmitted scrolling without imagescroll - expected the default always-redraw behaviour"
exit 0
+362
View File
@@ -0,0 +1,362 @@
#!/bin/sh
# Grid-resident image layers: Kitty input, history, copy and overwrite.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -Limage$$ -f/dev/null"
TMUX2="$TEST_TMUX -Limage-client$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
TMP=$(mktemp)
FIXTURE=$(pwd)/monkey-2.sixel.txt
trap "$TMUX kill-server 2>/dev/null; $TMUX2 kill-server 2>/dev/null; rm -f $TMP" 0 1 15
$TMUX new-session -d -x 20 -y 8 "
printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,m=1;/wAA/wD/\033\\'
printf '\033_Gm=0;AP8AAP///////w==\033\\'
i=0
while [ \$i -lt 10 ]; do printf '\nline-%d' \$i; i=\$((i + 1)); done
sleep 10"
sleep 1
[ "$($TMUX display-message -p '#{image_support}')" = 0 ] && exit 0
# Images scroll as grid cells, while capture output contains ordinary spaces.
[ "$($TMUX display-message -p '#{history_size}')" -gt 0 ] || exit 1
$TMUX capture-pane -pS- >$TMP || exit 1
grep '_G' $TMP >/dev/null && exit 1
# Copy mode duplicates the backing grid, including image layers.
$TMUX copy-mode || exit 1
$TMUX send-keys -X history-top || exit 1
$TMUX capture-pane -p >$TMP || exit 1
$TMUX send-keys -X cancel || exit 1
# Ordinary text updates the image underlay through the normal grid write path.
# Deleting the image reveals the newly written text.
$TMUX new-window -d "
printf '\033_Ga=T,q=2,f=32,s=2,v=2,c=2,r=2,i=8;/wAA/wD/AP8AAP///////w==\033\\'
printf '\033[HXY'
printf '\033_Ga=d,d=i,q=2,i=8\033\\'
sleep 10"
sleep 1
[ "$($TMUX capture-pane -pt:1 -S0 -E0)" = "XY" ] || exit 1
# The protocol default is transmit-only; a later placement uses the retained
# immutable image and advances by the same canonical one-cell footprint.
$TMUX new-window -d "
printf '\033_Gq=2,f=32,s=1,v=1,c=1,r=1,i=9;/wAA/w==\033\\'
printf '\033_Ga=p,q=2,i=9\033\\'
printf '\033_Ga=d,d=I,q=2,i=9\033\\'
sleep 10"
sleep 1
[ "$($TMUX display-message -pt:2 '#{cursor_y}')" = 1 ] || exit 1
# PNG Kitty input uses the shared image decoder and canonical cell sizing.
$TMUX new-window -d "
printf '\033_Ga=T,q=2,f=100;iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAGUExURf8AAP///0EdNBEAAAABYktHRAH/Ai3eAAAAB3RJTUUH6ggCDAECH324BwAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=\033\\'
sleep 10"
sleep 1
[ "$($TMUX display-message -pt:3 '#{cursor_y}')" = 1 ] || exit 1
# SIXEL input reaches the same grid layer and copy-mode paths.
$TMUX new-window -d "cat '$FIXTURE'; sleep 10"
sleep 1
[ "$($TMUX display-message -pt:4 '#{cursor_y}')" -gt 0 ] || exit 1
$TMUX copy-mode -t:4 || exit 1
$TMUX send-keys -t:4 -X history-top || exit 1
$TMUX capture-pane -pt:4 >$TMP || exit 1
# A 26-pixel raster occupies two default 16-pixel cells, but the final cell
# remains only partially filled instead of stretching the raster to 32 pixels.
# A client without an image feature exposes this in the sampled text backend.
$TMUX kill-server 2>/dev/null
$TMUX2 new-session -d -x 10 -y 4 "
printf '\033Pq\"1;1;26;26#0;2;100;100;100#0!26~-!26~-!26~-!26~-!26B\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -as terminal-features ',*:sixel@' || exit 1
$TMUX new-session -d -x 10 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# Re-emitting the image preserves the 26-pixel raster rather than expanding
# it to the two complete 16-pixel grid cells occupied by the image.
# terminal-features is only detected when a client attaches, so switch to a
# SIXEL-capable client by detaching and reattaching rather than toggling the
# option on the client already attached above.
$TMUX2 set -u terminal-features || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX2 detach-client || exit 1
sleep 1
$TMUX pipe-pane || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
grep -a '"1;1;26;26' $TMP >/dev/null || exit 1
# A selection redraw uses the single-cell path. It must leave ASCII image
# cells visible (graphical clients skip these cells to avoid erasing pixels).
# Switch back to a fallback client the same way.
$TMUX2 set -u terminal-features || exit 1
$TMUX2 set -as terminal-features ',*:sixel@' || exit 1
$TMUX2 detach-client || exit 1
sleep 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX2 copy-mode || exit 1
$TMUX2 send-keys -X history-top || exit 1
$TMUX2 send-keys -X start-of-line || exit 1
$TMUX2 send-keys -X begin-selection || exit 1
$TMUX2 send-keys -X cursor-right || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
grep -q '^#=' $TMP || exit 1
# A retained Kitty image may be placed repeatedly using source rectangles.
# Crop the green and white right column from a red/green/blue/white image.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 10 -y 4 "
printf '\033_Ga=t,q=2,f=32,s=2,v=2,i=10;/wAA/wD/AP8AAP///////w==\033\\'
printf '\033_Ga=p,q=2,i=10,x=1,y=0,w=1,h=2,c=1,r=2\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX new-session -d -x 10 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "*" ] || exit 1
[ "$(sed -n 2p $TMP)" = "@" ] || exit 1
# C=1 places the image without moving the cursor or scrolling. The four-row
# image is clipped at the bottom of this four-row pane, leaving its white last
# row off screen.
$TMUX2 new-window -d "
printf '\033[2;3H'
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:1 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{cursor_x},#{cursor_y}')" = "2,1" ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 3p $TMP)" = " -" ] || exit 1
[ "$(sed -n 4p $TMP)" = " *" ] || exit 1
# With normal cursor movement, scrolling is calculated from the full image
# height. Rows which scrolled above the pane are then cropped from the top, so
# the bottom three source rows remain visible and the cursor is on the last row.
$TMUX2 new-window -d "
printf '\033[3;1H'
printf '\033_Ga=T,q=2,f=32,s=1,v=4,c=1,r=4;AAAA/1VVVf+qqqr//////w==\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:2 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{cursor_y}')" = 3 ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "-" ] || exit 1
[ "$(sed -n 2p $TMP)" = "*" ] || exit 1
[ "$(sed -n 3p $TMP)" = "@" ] || exit 1
# Image rows remain cell-aligned when a narrower terminal causes text
# reflow. The ten-column rows are clipped to five columns, not split into four
# wrapped rows.
$TMUX2 new-window -d "
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=2,c=10,r=2;/wAA//////8=\033\\'
sleep 10" || exit 1
$TMUX2 select-window -t:3 || exit 1
sleep 1
$TMUX resize-window -x 5 -y 4 || exit 1
sleep 1
[ "$($TMUX2 display-message -p '#{window_width}x#{window_height}')" = "5x4" ] || exit 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "....." ] || exit 1
[ "$(sed -n 2p $TMP)" = "@@@@@" ] || exit 1
[ -z "$(sed -n 3p $TMP)" ] || exit 1
# Kitty virtual placements use U+10EEEE placeholder cells. Keep their rows at
# fixed coordinates when narrowing the terminal, clipping instead of reflowing
# the second half onto the following row.
$TMUX resize-window -x 10 -y 4 || exit 1
sleep 1
$TMUX2 new-window -d "
printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205'
printf '\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205\\364\\216\\273\\256\\314\\205'
sleep 10" || exit 1
$TMUX2 select-window -t:4 || exit 1
sleep 1
$TMUX resize-window -x 5 -y 4 || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -n "$(sed -n 1p $TMP)" ] || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
$TMUX resize-window -x 10 -y 4 || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP | wc -c)" = 61 ] || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
# A placement ID supplied with transmit-and-place is reused by a later place.
# This is the sequence used by chawan: the newline moves the cursor down before
# the second command, so the folder moves down one row without leaving a copy.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_GC=1,s=26,v=26,p=1,q=2,i=1,a=T,f=100,m=0;iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAYAAACpSkzOAAACmUlEQVR4XmOgF2CEWVRfX8/w//9/BkZGRgy7YeKNjY1kuwtsam1tLQMTE5MG0MBsoEWW6KYBxf8DxScD6UVNTU1kWcZYU1MD0qjLzMx8EmgYJy5TgJb8/ffvn+2vX7+Od3Z2kmwZC9AnIE0FIEuAhl0H4ulA9k9kk4Bi4UB1TkA8H2iRYXl5+Xd2dna8loGCG2Q2LLgZQXED1PEIaLjs379/g4Ds9VDLwQYBxRiAPpFmZWW9DFQjCDRgLxDfJeQloJpdQHPWA9X9A1kGs+gZ0BBJoKQtUOIIeqSDgheoKR6IF5ASZkAH3gKaCzLzFQsxGltaWhjq6uoWguIJ6hi82oCG8wIV+AEdpga0LBfIrmUh1oWg1AYM5iVA9UuI1JMGVDcTiCNIsghkOChIQ0NDwfasXr0ap33QeL8GUgD0HTeIZmIgAUAtYQZqYYZZSKx2oi2CWaKpqbkIhEm1jOg40tLSAjleEBgUUSAGkJ8PpN8Q6yOCFoF8AvQBTvOAqZHh+vXrDPjijGAcIcXJUqBPQOXda5iNQP5rkBiQv5SYYMTrIxUVFZC5IkDCCk8QWQHVgdS8xBeMeBNDe3s7A7CwfQl0uSmwKDIBYhdQGQbCIDZIDCQHUgNSS7ZFII2gUgEU6UADzwJz+kUgDa6zoOyzIDmoGgaKLAJpBheKkArxC7BIeQDCQPEvIEuJrQyJzkdQA38ADbcHYaBFP0ipcVGqCWDYBwPxOkYs1TkDCeA/pEkQBDRnLZD9GKhVjgUkCGTsBgomAekWIJYG4p8MlAFQrZgJMgJo/m4QzQJ1fT9QIBLI1gTiSRRaAm/gAM38DjSvH2QeuHECLW1xNk7IsRhoyXGgJVOBem+AExPMEHzNLTItYiAlVTJQCwAAJxkMkgbHo2UAAAAASUVORK5CYII=\033\\'
printf '\n'
sleep 1
printf '\033_GC=1,s=26,v=26,p=1,q=2,i=1,a=p;\033\\'
printf '\n'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 1p $TMP)" ] || exit 1
[ -n "$(sed -n 2p $TMP)" ] || exit 1
# A nonzero Kitty placement ID identifies one placement of an image. Reusing
# the same image and placement IDs moves it rather than leaving the old cells.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=11;/////w==\033\\'
printf '\033[2;2H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\'
printf '\033[4;6H\033_Ga=p,q=2,C=1,i=11,p=7,c=2,r=1\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1
# Deleting one placement ID leaves other placements of the image intact.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=12;/////w==\033\\'
printf '\033[2;2H\033_Ga=p,q=2,C=1,i=12,p=7,c=2,r=1\033\\'
printf '\033[4;6H\033_Ga=p,q=2,C=1,i=12,p=8,c=2,r=1\033\\'
printf '\033_Ga=d,d=i,q=2,i=12,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ -z "$(sed -n 2p $TMP)" ] || exit 1
[ "$(sed -n 4p $TMP)" = " @@" ] || exit 1
# Image layers retain the cells beneath them. Deleting a transparent image
# must reveal the original text rather than replacing it with spaces.
PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf 'XY'
printf '\033[H'
printf '\033_Ga=T,q=2,C=1,f=32,s=1,v=1,c=2,r=1,i=13,p=7;////AA==\033\\'
printf '\033_Ga=d,d=i,q=2,i=13,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$PLACEMENT_WINDOW" || exit 1
sleep 1
[ "$($TMUX2 capture-pane -pS0 -E0)" = "XY" ] || exit 1
# Text written over a Kitty placement updates its underlay without deleting
# the image. Deleting the placement afterwards reveals the updated text.
TEXT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf 'test\r'
printf '\033_Ga=T,q=2,C=1,f=32,s=2,v=2,c=2,r=2,i=14,p=7;/wAA/wD/AP8AAP///////w==\033\\'
printf '\r'
printf 'test\n'
printf 'test\n'
$TEST_TMUX wait-for image-text-delete-$$
printf '\033_Ga=d,d=i,q=2,i=14,p=7\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$TEXT_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = ".*st" ] || exit 1
[ "$(sed -n 2p $TMP)" = " @st" ] || exit 1
$TMUX2 wait-for -S image-text-delete-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E1 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "test" ] || exit 1
[ "$(sed -n 2p $TMP)" = "test" ] || exit 1
# Kitty z-indexes and SIXEL's temporal plane are properties of the input, not
# of the outer terminal. Exercise the logical scene through the fallback
# backend: higher nonnegative z-indexes cover lower ones and text, ordinary
# negative z-indexes cover backgrounds but not text, and very negative ones
# remain below the background.
Z_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' "
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=21;/wAA/w==\033\\'
printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=22;/////w==\033\\'
printf 'X\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=1,z=2,c=1,r=1\033\\'
printf '\033_Ga=p,q=2,C=1,i=22,p=2,z=1,c=1,r=1\033\\'
printf '\033[2;1H\033[41m \033[0m\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=3,z=-1,c=1,r=1\033\\'
printf '\033[3;1HY\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=4,z=-1,c=1,r=1\033\\'
printf '\033[4;1H\033[41m \033[0m\r'
printf '\033_Ga=p,q=2,C=1,i=21,p=5,z=-1073741825,c=1,r=1\033\\'
$TEST_TMUX wait-for image-z-top-$$
printf '\033_Ga=d,d=z,q=2,z=2\033\\'
$TEST_TMUX wait-for image-z-lower-$$
printf '\033_Ga=d,d=z,q=2,z=1\033\\'
sleep 10") || exit 1
$TMUX2 select-window -t"$Z_WINDOW" || exit 1
sleep 1
$TMUX capture-pane -pS0 -E3 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "." ] || exit 1
[ "$(sed -n 2p $TMP)" = "." ] || exit 1
[ "$(sed -n 3p $TMP)" = "Y" ] || exit 1
[ -z "$(sed -n 4p $TMP)" ] || exit 1
$TMUX2 wait-for -S image-z-top-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "@" ] || exit 1
$TMUX2 wait-for -S image-z-lower-$$ || exit 1
sleep 1
$TMUX capture-pane -pS0 -E0 >$TMP || exit 1
[ "$(sed -n 1p $TMP)" = "X" ] || exit 1
# A weighted median at the maximum channel level must still leave colours on
# both sides of the split. This skewed black, grey and white image used to stop
# palette generation after one colour instead of producing three.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 20 -y 4 "
printf '\033_Ga=T,q=2,C=1,f=24,s=100,v=1,c=10,r=1;AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////\033\\'
sleep 10" || exit 1
$TMUX2 set -g status off || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 20 -y 4 || exit 1
$TMUX set -g status off || exit 1
$TMUX pipe-pane -O "cat >$TMP" || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
grep -a '#2;2;' $TMP >/dev/null || exit 1
# A pane redraw on a SIXEL terminal must not clear the status line, which is
# outside the window scene and may not be redrawn after the pane scrolls.
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
$TMUX2 new-session -d -x 20 -y 8 || exit 1
$TMUX2 set -g 'status-format[0]' 'STATUS' || exit 1
$TMUX2 set -as terminal-features ',*:sixel' || exit 1
$TMUX new-session -d -x 20 -y 8 || exit 1
$TMUX set -g status off || exit 1
$TMUX send-keys -l "$TMUX2 attach-session" || exit 1
$TMUX send-keys Enter || exit 1
sleep 1
$TMUX2 send-keys -l 'seq 30' || exit 1
$TMUX2 send-keys Enter || exit 1
sleep 1
$TMUX capture-pane -p >$TMP || exit 1
[ "$(tail -n 1 $TMP)" = "STATUS" ] || exit 1
exit 0
+5 -1
View File
@@ -60,7 +60,11 @@ query_timeout()
query "dsr-ok" '^[[0n' '\033[5n' 4 ''
query "dsr-cursor" '^[[1;1R' '\033[6n' 6 ''
query "da-primary" '^[[?1;2c' '\033[c' 7 ''
if [ "$($TMUX display-message -p '#{image_support}')" = 1 ]; then
query "da-primary" '^[[?1;2;4c' '\033[c' 9 ''
else
query "da-primary" '^[[?1;2c' '\033[c' 7 ''
fi
query "da-secondary" '^[[>84;0;0c' '\033[>c' 10 ''
query "decrqm-irm-reset" '^[[4;2$y' '\033[4$p' 7 ''
query "decrqm-irm-set" '^[[4;1$y' '\033[4$p' 7 '\033[4h'
-168
View File
@@ -1,168 +0,0 @@
#!/bin/sh
# Tests that the scroll position of a mode tree list (mode-tree.c) is kept
# when the list is rebuilt, using choose-buffer. Buffer mode rebuilds its list
# every time the status line is redrawn, which used to move the view so the
# selected line was on the bottom row (or the list jumped back to the top)
# even if the selected line was already visible. Also checks that when the
# list shrinks the view does not leave empty lines at the bottom.
#
# The list is drawn on a mode screen which capture-pane does not show, so - as
# in choose-buffer.sh - a second server provides a client: an inner "tmux
# attach" runs inside a pane of the second server, and that pane is captured
# to read what the inner client rendered.
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMP=$(mktemp -d) || exit 1
TMUX_TMPDIR="$TMP"
export TMUX_TMPDIR
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server 2>/dev/null
$TMUX2 kill-server 2>/dev/null
rm -rf "$TMP"
}
trap cleanup EXIT
fail()
{
echo "$1" >&2
exit 1
}
# capture the screen rendered by the inner client
capture()
{
$TMUX2 capture-pane -p -t out:0 2>/dev/null
}
# wait_for $marker
#
# Wait (up to ~10s) until the rendered screen contains $marker. The matching
# capture is left in CAPTURED.
wait_for()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
if printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
return 0
fi
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1'"
}
# wait_gone $marker
#
# Wait (up to ~10s) until the rendered screen no longer contains $marker. The
# matching capture is left in CAPTURED.
wait_gone()
{
i=0
while [ "$i" -lt 50 ]; do
CAPTURED=$(capture)
if ! printf '%s\n' "$CAPTURED" | grep -F -q "$1"; then
return 0
fi
sleep 0.2
i=$((i + 1))
done
fail "timed out waiting for '$1' to disappear"
}
# wait_clients $n
#
# Wait (up to ~10s) until the test server has exactly $n clients attached.
wait_clients()
{
i=0
while [ "$i" -lt 10 ]; do
c=$($TMUX list-clients -F x 2>/dev/null | grep -c x)
[ "$c" -eq "$1" ] && return 0
sleep 1
i=$((i + 1))
done
return 1
}
# first_line
#
# Print the name of the first buffer shown in CAPTURED.
first_line()
{
printf '%s\n' "$CAPTURED" | grep -F ': S1' | head -1 | \
sed 's/.*\(buf[0-9a-z]*\): S1.*/\1/'
}
$TMUX new-session -d -s aaa -x 80 -y 24 'cat' || exit 1
$TMUX2 new-session -d -s out -x 80 -y 24 "$TMUX attach -t aaa" || exit 1
wait_clients 1 || fail "no client attached to test server"
# Forty buffers, more than fit on the screen.
n=0
while [ "$n" -lt 40 ]; do
$TMUX set-buffer -b "$(printf 'buf%02d' "$n")" "buffer $n" || exit 1
n=$((n + 1))
done
# Without a preview the list has the whole pane (23 lines with the status
# line), so buf00 to buf22 are visible.
$TMUX choose-buffer -t aaa:0 -N -O name -F 'S1' || exit 1
wait_for 'buf00: S1'
# --- scrolling down moves the view -------------------------------------------
#
# Selecting buf30 scrolls so it is on the bottom row: buf08 is at the top.
$TMUX send-keys -t aaa:0 -N 30 Down || exit 1
wait_for 'buf30: S1'
top=$(first_line)
[ "$top" = "buf08" ] || fail "top line after scrolling is $top, not buf08"
# Moving up to buf20 stays inside the visible lines so the view does not move.
$TMUX send-keys -t aaa:0 -N 10 Up || exit 1
# --- a rebuild keeps the view where it is ------------------------------------
#
# Add a buffer that sorts into the visible part of the list and redraw the
# status line so the list is rebuilt. Once buf15a appears the rebuild has
# happened; buf08 must still be the top line. It used to jump back to buf00.
$TMUX set-buffer -b buf15a "new buffer" || exit 1
$TMUX refresh-client -S || exit 1
wait_for 'buf15a: S1'
top=$(first_line)
[ "$top" = "buf08" ] || fail "top line after rebuild is $top, not buf08"
# --- a shrinking list does not leave empty lines -----------------------------
#
# Go to the end of the list (buf39 is on the bottom row) and delete all but
# the last ten buffers. They now all fit, so the view goes to the top.
$TMUX send-keys -t aaa:0 End || exit 1
wait_for 'buf39: S1'
for b in $($TMUX list-buffers -F '#{buffer_name}'); do
case "$b" in
buf3?)
;;
*)
$TMUX delete-buffer -b "$b" || exit 1
;;
esac
done
$TMUX refresh-client -S || exit 1
wait_gone 'buf29: S1'
top=$(first_line)
[ "$top" = "buf30" ] || fail "top line after shrinking is $top, not buf30"
[ "$(printf '%s\n' "$CAPTURED" | grep -F -c ': S1')" -eq 10 ] || \
fail "expected 10 buffers after shrinking"
$TMUX send-keys -t aaa:0 q
exit 0
File diff suppressed because one or more lines are too long
-126
View File
@@ -1,126 +0,0 @@
#!/bin/sh
# Damage at a popup edge must redraw complete grid characters. Drawing only a
# wide character's base or padding cell leaves a two-cell hole behind.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpopup-wide-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
POPUP_PID=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$POPUP_PID" ] && kill "$POPUP_PID" 2>/dev/null
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
sed -n '3,5p' "$BASE" >"$DIR/want"
sed -n '3,5p' "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the old popup edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpopup-wide-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
$INNER display-popup -t "$CLIENT" -x 5 -y 5 -w 10 -h 3 -E \
"sh -c 'printf POPUP; exec sleep 100'" &
POPUP_PID=$!
wait_outer_has POPUP
# The first motion starts the drag; the second moves the popup away from its
# old rectangle. Its odd x coordinate bisects the underlying double-width
# cells at both edges.
mouse 0 10 3 M
mouse 32 11 3 M
mouse 32 28 7 M
mouse 0 28 7 m
wait_old_rows_restored
exit 0
-8
View File
@@ -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'
-113
View File
@@ -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
-140
View File
@@ -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
-112
View File
@@ -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
-49
View File
@@ -1,49 +0,0 @@
#!/bin/sh
# renumber-windows with more windows than fit above base-index
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestRWL$$"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)
CONF=$(mktemp)
trap "rm -f $TMP $CONF" 0 1 15
cat <<EOF >$CONF
set -g base-index 10
set -g renumber-windows on
EOF
$TMUX -f$CONF new -d -s a
$TMUX neww -d -t a
$TMUX neww -d -t a
echo $($TMUX lsw -t a -F'#{window_index}') >$TMP
(echo "10 11 12"|cmp -s - $TMP) || exit 1
$TMUX killw -t a:11
echo $($TMUX lsw -t a -F'#{window_index}') >$TMP
(echo "10 11"|cmp -s - $TMP) || exit 1
$TMUX kill-server 2>/dev/null
cat <<EOF >$CONF
set -g base-index 2147483646
set -g renumber-windows on
EOF
$TMUX -f$CONF new -d -s a
$TMUX neww -d -t a
$TMUX neww -d -t a
$TMUX neww -d -t a
echo $($TMUX lsw -t a -F'#{window_index}') >$TMP
(echo "0 1 2147483646 2147483647"|cmp -s - $TMP) || exit 1
# Three windows do not fit from 2147483646, so the indexes stay as they are.
$TMUX killw -t a:2147483647
echo $($TMUX lsw -t a -F'#{window_index}') >$TMP
(echo "0 1 2147483646"|cmp -s - $TMP) || exit 1
$TMUX kill-server 2>/dev/null
exit 0
-123
View File
@@ -1,123 +0,0 @@
#!/bin/sh
# A scroll held back by a synchronized update may be sent to all clients while
# a later client is being redrawn. An earlier client which cannot scroll the
# pane itself must still end up with the right contents.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -Li$$ -f/dev/null"
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
CONTROL=$DIR/control
EMITTER=$DIR/emitter.pl
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_file()
{
i=0
while [ "$i" -lt 50 ] && [ ! -e "$1" ]; do
sleep 0.1
i=$((i + 1))
done
[ -e "$1" ] || fail "$2"
}
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
n=$($INNER list-clients 2>/dev/null | wc -l)
[ "$n" -eq "$1" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "client $1 did not attach"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
my $control = $ENV{CONTROL};
(my $dir = $control) =~ s{/[^/]+$}{};
my @rows;
for my $row (1 .. 24) {
push @rows, sprintf("INIT_ROW_%02d_", $row) . ('X' x 16);
}
syswrite STDOUT, "\e[H" . join("\r\n", @rows);
open my $painted, '>', "$dir/painted" or die "$dir/painted: $!\n";
close $painted;
while (!-e $control) {
select undef, undef, undef, 0.01;
}
# Start a synchronized update and scroll, but do not end it.
syswrite STDOUT, "\e[?2026h\e[24;1H\r\nSCROLLED_LINE_";
open my $scrolled, '>', "$dir/scrolled" or die "$dir/scrolled: $!\n";
close $scrolled;
select undef, undef, undef, 3;
PERL
$INNER new-session -d -s inner -x 80 -y 24 \
"CONTROL='$CONTROL' perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -as terminal-features '*:sync' || exit 1
# The pane is not full width, so clients cannot scroll it.
$INNER split-window -h -d -t inner:0.0 'sleep 30' || exit 1
for name in a b; do
$OUTER new-session -d -s $name -x 80 -y 24 \
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" || exit 1
$OUTER set-option -t $name status off || exit 1
if [ "$name" = a ]; then
wait_for_clients 1
fi
done
$OUTER set-option -g window-size manual || exit 1
wait_for_clients 2
B=$($OUTER display-message -p -t b: '#{pane_tty}') || exit 1
wait_for_file "$DIR/painted" "application did not paint"
sleep 1
: >"$CONTROL"
wait_for_file "$DIR/scrolled" "application did not scroll"
sleep 0.2
# Redraw only the second client while the update is still in progress.
$INNER refresh-client -t "$B" || exit 1
sleep 1
$OUTER capture-pane -p -t a: >"$DIR/a" || exit 1
$OUTER capture-pane -p -t b: >"$DIR/b" || exit 1
sed -n 1p "$DIR/b" | grep -q '^INIT_ROW_02_' ||
fail "second client not scrolled"
sed -n 24p "$DIR/b" | grep -q '^SCROLLED_LINE_' ||
fail "second client missing scrolled line"
cmp -s "$DIR/a" "$DIR/b" || fail "first client differs from second client"
exit 0
-150
View File
@@ -1,150 +0,0 @@
#!/bin/sh
# Check that synchronized scrolling does not redraw unchanged lines.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
TMUX_TMPDIR=$DIR
export TMUX_TMPDIR
INNER="$TEST_TMUX -Li$$ -f/dev/null"
OUTER="$TEST_TMUX -Lo$$ -f/dev/null"
CLIENT_BYTES=$DIR/client-bytes
CONTROL=$DIR/control
EMITTER=$DIR/emitter.pl
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_file()
{
i=0
while [ "$i" -lt 50 ] && [ ! -e "$1" ]; do
sleep 0.1
i=$((i + 1))
done
[ -e "$1" ] || fail "$2"
}
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
$INNER list-clients -F '#{client_termfeatures}' 2>/dev/null |
grep -q 'sync' && return 0
sleep 0.1
i=$((i + 1))
done
fail "sync-capable client did not attach"
}
wait_for_stable_bytes()
{
previous=-1
stable=0
i=0
while [ "$i" -lt 50 ]; do
current=$(wc -c <"$CLIENT_BYTES" 2>/dev/null) || current=0
if [ "$current" -gt 0 ] && [ "$current" -eq "$previous" ]; then
stable=$((stable + 1))
[ "$stable" -eq 5 ] && return 0
else
stable=0
fi
previous=$current
sleep 0.1
i=$((i + 1))
done
fail "client byte stream did not become stable"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
my $control = $ENV{CONTROL};
(my $dir = $control) =~ s{/[^/]+$}{};
# Fill the screen outside a synchronized update.
my @rows;
for my $row (1 .. 24) {
push @rows, substr(sprintf("INIT_ROW_%02d_", $row) . ('X' x 79), 0, 79);
}
syswrite STDOUT, "\e[H" . join("\r\n", @rows);
open my $painted, '>', "$dir/painted" or die "$dir/painted: $!\n";
close $painted;
while (!-e $control) {
select undef, undef, undef, 0.01;
}
# Scroll one line and overwrite text in a synchronized update.
my $frame = "\e[?2026h" .
"\e[24;1H\r\nSCROLLED_LINE_" .
"\e[23;60HCHANGED_" .
"\e[?2026l";
my $written = syswrite STDOUT, $frame;
die "short synchronized frame write\n"
unless defined $written && $written == length $frame;
select undef, undef, undef, 3;
PERL
$INNER new-session -d -s inner -x 80 -y 24 \
"CONTROL='$CONTROL' perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -as terminal-features '*:sync' || exit 1
$OUTER new-session -d -s outer -x 80 -y 24 \
"$TEST_TMUX -Li$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
wait_for_client
wait_for_file "$DIR/painted" "application did not paint"
sleep 1
$OUTER pipe-pane -O -t outer:0.0 "cat >'$CLIENT_BYTES'" || exit 1
: >"$CONTROL"
i=0
while [ "$i" -lt 50 ]; do
grep -q 'SCROLLED_LINE_' "$CLIENT_BYTES" 2>/dev/null && break
sleep 0.1
i=$((i + 1))
done
grep -q 'SCROLLED_LINE_' "$CLIENT_BYTES" || fail "scrolled line not sent"
wait_for_stable_bytes
$OUTER pipe-pane -t outer:0.0 || exit 1
# Check that only changed lines are redrawn.
grep -q 'CHANGED_' "$CLIENT_BYTES" || fail "changed cell not sent"
if grep -q 'INIT_ROW_05_' "$CLIENT_BYTES"; then
fail "unchanged line redrawn after synchronized scroll"
fi
# The client must show the scrolled screen.
$OUTER capture-pane -p -t outer:0.0 >"$DIR/screen" || exit 1
sed -n 1p "$DIR/screen" | grep -q '^INIT_ROW_02_' ||
fail "first line not scrolled"
sed -n 23p "$DIR/screen" | grep -q 'INIT_ROW_24_.*CHANGED_' ||
fail "changed cell not on screen"
sed -n 24p "$DIR/screen" | grep -q '^SCROLLED_LINE_' ||
fail "scrolled line not on screen"
exit 0
+80 -56
View File
@@ -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
-26
View File
@@ -1,26 +0,0 @@
#!/bin/sh
# inserting a window next to the highest index
PATH=/bin:/usr/bin
TERM=screen
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestWIL$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
TMP=$(mktemp)
trap "rm -f $TMP" 0 1 15
$TMUX new -d -s one
$TMUX set -g base-index 2147483647
$TMUX neww -d -t one
$TMUX neww -d -a -t one:2147483647
$TMUX neww -d -b -t one:2147483647
echo $($TMUX lsw -t one -F'#{window_index}') >$TMP
(echo "0 2147483647"|cmp -s - $TMP) || exit 1
$TMUX kill-server 2>/dev/null
exit 0
+16 -3
View File
@@ -152,7 +152,18 @@ clients_calculate_size(int type, int current, struct client *c,
*sx = UINT_MAX;
*sy = UINT_MAX;
}
*xpixel = *ypixel = 0;
if (w != NULL && type == WINDOW_SIZE_MANUAL) {
/*
* Manual mode has no separate pixel size to apply, so keep the
* window's own - otherwise this always differs from the real
* w->xpixel/ypixel, and recalculate_size() can never tell a
* later call apart from an actual change, resizing (and
* re-firing window-resized) on every recalculation.
*/
*xpixel = w->xpixel;
*ypixel = w->ypixel;
} else
*xpixel = *ypixel = 0;
/*
* For latest, count the number of clients with this window. We only
@@ -386,10 +397,12 @@ recalculate_size(struct window *w, int now)
* got a resize scheduled, then use the new size; otherwise the old.
*/
if (w->flags & WINDOW_RESIZE) {
if (!now && changed && w->new_sx == sx && w->new_sy == sy)
if (!now && changed && w->new_sx == sx && w->new_sy == sy &&
w->new_xpixel == xpixel && w->new_ypixel == ypixel)
changed = 0;
} else {
if (!now && changed && w->sx == sx && w->sy == sy)
if (!now && changed && w->sx == sx && w->sy == sy &&
w->xpixel == xpixel && w->ypixel == ypixel)
changed = 0;
}
+663 -135
View File
File diff suppressed because it is too large Load Diff
+117 -261
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: screen-write.c,v 1.293 2026/09/24 11:19:39 nicm Exp $ */
/* $OpenBSD: screen-write.c,v 1.292 2026/09/21 12:14:32 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -34,13 +34,15 @@ static void screen_write_collect_clear(struct screen_write_ctx *, u_int,
static void screen_write_collect_scroll(struct screen_write_ctx *, u_int);
static void screen_write_collect_flush(struct screen_write_ctx *, int,
const char *);
#ifdef ENABLE_IMAGES
static void screen_write_image_damage(struct screen_write_ctx *, u_int,
u_int, u_int, u_int);
#endif
static int screen_write_overwrite(struct screen_write_ctx *,
struct grid_cell *, u_int);
static int screen_write_combine(struct screen_write_ctx *,
const struct grid_cell *);
static void screen_write_sync_flush_dirty(struct window_pane *);
static void screen_write_sync_apply_scroll(struct screen_write_ctx *,
struct tty_ctx *);
static void screen_write_flush_dirty(struct window_pane *);
struct screen_write_citem {
u_int x;
@@ -122,7 +124,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)
{
@@ -140,8 +153,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. */
@@ -229,27 +244,6 @@ screen_write_pane_is_obscured(struct screen_write_ctx *ctx)
return (0);
}
/* Allocate dirty lines for this screen size. */
static bitstr_t *
screen_write_sync_allocate_dirty(struct window_pane *wp, u_int sy)
{
bitstr_t *bs = wp->sync_dirty;
if (bs != NULL && wp->sync_dirty_size == sy)
return (bs);
free(bs);
bs = wp->sync_dirty = bit_alloc(sy);
if (bs == NULL)
fatal("bit_alloc failed");
if (wp->sync_dirty_size != 0) {
bit_nset(bs, 0, sy - 1);
wp->sync_scrolled = 0;
}
wp->sync_dirty_size = sy;
return (bs);
}
/* Should we draw to the TTY? */
static int
screen_write_should_draw_lines(struct screen_write_ctx *ctx, u_int y, u_int ny)
@@ -263,9 +257,21 @@ screen_write_should_draw_lines(struct screen_write_ctx *ctx, u_int y, u_int ny)
return (0);
if (s->mode & MODE_SYNC) {
if (wp != NULL && y < sy && ny != 0) {
bs = wp->sync_dirty;
if (ny > sy - y)
ny = sy - y;
bs = screen_write_sync_allocate_dirty(wp, sy);
if (bs == NULL || wp->sync_dirty_size != sy) {
if (bs != NULL && wp->sync_dirty_size != sy) {
y = 0;
ny = sy;
}
free(bs);
bs = wp->sync_dirty = bit_alloc(sy);
if (bs == NULL)
fatal("bit_alloc failed");
wp->sync_dirty_size = sy;
}
bit_nset(bs, y, y + ny - 1);
}
return (0);
@@ -389,6 +395,19 @@ screen_write_init(struct screen_write_ctx *ctx, struct screen *s)
ctx->bg = 8;
}
#ifdef ENABLE_IMAGES
/* Damage temporal images when application input writes text cells. */
static void
screen_write_image_damage(struct screen_write_ctx *ctx, u_int x, u_int y,
u_int sx, u_int sy)
{
image_redraw_area(ctx, x, y, sx, sy);
if (ctx->flags & SCREEN_WRITE_INPUT)
image_grid_damage(ctx->s->grid, x, ctx->s->grid->hsize + y,
sx, sy);
}
#endif
/* Initialize writing with a pane. */
void
screen_write_start_pane(struct screen_write_ctx *ctx, struct window_pane *wp,
@@ -1055,7 +1074,7 @@ screen_write_sync_callback(__unused int fd, __unused short events, void *arg)
if (wp->base.mode & MODE_SYNC) {
wp->base.mode &= ~MODE_SYNC;
screen_write_sync_flush_dirty(wp);
screen_write_flush_dirty(wp);
}
}
@@ -1087,7 +1106,7 @@ screen_write_stop_sync(struct window_pane *wp)
evtimer_del(&wp->sync_timer);
wp->base.mode &= ~MODE_SYNC;
screen_write_sync_flush_dirty(wp);
screen_write_flush_dirty(wp);
log_debug("%s: %%%u stopped sync mode", __func__, wp->id);
}
@@ -1295,7 +1314,7 @@ screen_write_redraw_line(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx,
/* Redraw dirty lines. */
static void
screen_write_sync_flush_dirty(struct window_pane *wp)
screen_write_flush_dirty(struct window_pane *wp)
{
struct screen_write_ctx ctx;
struct tty_ctx ttyctx;
@@ -1308,143 +1327,29 @@ screen_write_sync_flush_dirty(struct window_pane *wp)
screen_write_start_pane(&ctx, wp, s);
screen_write_initctx(&ctx, &ttyctx, 1, 1);
if (wp->sync_scrolled != 0)
screen_write_sync_apply_scroll(&ctx, &ttyctx);
if (~wp->flags & PANE_REDRAW) {
for (y = 0; y < sy; y++) {
if (bit_test(wp->sync_dirty, y)) {
screen_write_redraw_line(&ctx, &ttyctx, y);
lines++;
}
for (y = 0; y < sy; y++) {
if (bit_test(wp->sync_dirty, y)) {
screen_write_redraw_line(&ctx, &ttyctx, y);
lines++;
}
}
log_debug("%s: %%%u had %u dirty lines", __func__, wp->id, lines);
screen_write_stop(&ctx);
screen_write_sync_clear_dirty(wp);
screen_write_clear_dirty(wp);
}
/* Clear pending synchronized output. */
/* Clear any dirty lines. */
void
screen_write_sync_clear_dirty(struct window_pane *wp)
screen_write_clear_dirty(struct window_pane *wp)
{
if (wp != NULL) {
if (wp != NULL && wp->sync_dirty != NULL) {
free(wp->sync_dirty);
wp->sync_dirty = NULL;
wp->sync_dirty_size = 0;
wp->sync_scrolled = 0;
}
}
/* Defer scrolling until the synchronized update ends. */
static void
screen_write_sync_scroll_dirty(struct screen_write_ctx *ctx)
{
struct window_pane *wp = ctx->wp;
struct screen *s = ctx->s;
u_int n = ctx->scrolled, y, sy = screen_size_y(s);
u_int ry = s->rlower + 1 - s->rupper;
bitstr_t *bs;
if (n > ry)
n = ry;
if (wp == NULL ||
n == ry ||
s->rlower >= sy ||
window_pane_scrollbar_overlay_visible(wp)) {
screen_write_should_draw_lines(ctx, s->rupper, ry);
return;
}
if (wp->flags & (PANE_REDRAW|PANE_DROP))
return;
bs = screen_write_sync_allocate_dirty(wp, sy);
if (wp->sync_scrolled != 0 &&
(wp->sync_rupper != s->rupper ||
wp->sync_rlower != s->rlower ||
wp->sync_bg != ctx->bg)) {
log_debug("%s: %%%u region changed, redrawing all", __func__,
wp->id);
bit_nset(bs, 0, sy - 1);
wp->sync_scrolled = 0;
return;
}
/* Keep dirty lines aligned with the scrolled grid. */
for (y = s->rupper; y + n <= s->rlower; y++) {
if (bit_test(bs, y + n))
bit_set(bs, y);
else
bit_clear(bs, y);
}
bit_nset(bs, s->rlower + 1 - n, s->rlower);
wp->sync_rupper = s->rupper;
wp->sync_rlower = s->rlower;
wp->sync_bg = ctx->bg;
wp->sync_scrolled += n;
if (wp->sync_scrolled >= ry) {
bit_nset(bs, s->rupper, s->rlower);
wp->sync_scrolled = 0;
}
window_pane_scrollbar_redraw(wp);
log_debug("%s: %%%u deferred scroll of %u (region %u-%u)", __func__,
wp->id, wp->sync_scrolled, s->rupper, s->rlower);
}
/* Redraw the scrolled lines for a client which cannot scroll them. */
static void
screen_write_sync_redraw_cb(const struct tty_ctx *ttyctx)
{
struct window_pane *wp = ttyctx->arg;
bit_nset(wp->sync_dirty, wp->sync_rupper, wp->sync_rlower);
}
/* Send the deferred scroll to the client. */
static void
screen_write_sync_apply_scroll(struct screen_write_ctx *ctx,
struct tty_ctx *ttyctx)
{
struct window_pane *wp = ctx->wp;
struct screen *s = ctx->s;
u_int sy = screen_size_y(s), n = wp->sync_scrolled;
tty_ctx_redraw_cb redraw_cb;
wp->sync_scrolled = 0;
if (wp->flags & PANE_REDRAW)
return;
if (wp->sync_rlower >= sy || wp->sync_rupper > wp->sync_rlower) {
bit_nset(wp->sync_dirty, 0, sy - 1);
return;
}
if ((ttyctx->flags & TTY_CTX_PANE_OBSCURED) ||
window_pane_scrollbar_overlay_visible(wp)) {
bit_nset(wp->sync_dirty, wp->sync_rupper, wp->sync_rlower);
return;
}
log_debug("%s: %%%u scroll of %u (region %u-%u)", __func__, wp->id, n,
wp->sync_rupper, wp->sync_rlower);
ttyctx->orupper = wp->sync_rupper;
ttyctx->orlower = wp->sync_rlower;
if (wp->yoff + wp->sy > wp->window->sy)
ttyctx->orlower -= (wp->yoff + wp->sy - wp->window->sy);
ttyctx->n = n;
ttyctx->bg = wp->sync_bg;
redraw_cb = ttyctx->redraw_cb;
ttyctx->redraw_cb = screen_write_sync_redraw_cb;
tty_write(tty_cmd_scrollup, ttyctx);
ttyctx->redraw_cb = redraw_cb;
ttyctx->orupper = s->rupper;
ttyctx->orlower = s->rlower;
ttyctx->n = 0;
}
/* Redraw all visible cells in a pane. */
static void
screen_write_redraw_pane(struct screen_write_ctx *ctx, struct tty_ctx *ttyctx)
@@ -1468,9 +1373,11 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx)
memcpy(&gc, &grid_default_cell, sizeof gc);
utf8_set(&gc.data, 'E');
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
if (ctx->flags & SCREEN_WRITE_INPUT)
image_grid_damage(s->grid, 0, s->grid->hsize,
screen_size_x(s), screen_size_y(s));
#endif
for (yy = 0; yy < screen_size_y(s); yy++) {
@@ -1515,9 +1422,8 @@ screen_write_insertcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1)
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1556,9 +1462,8 @@ screen_write_deletecharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1)
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1597,9 +1502,8 @@ screen_write_clearcharacter(struct screen_write_ctx *ctx, u_int nx, u_int bg)
if (s->cx > screen_size_x(s) - 1)
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
screen_write_initctx(ctx, &ttyctx, 0, 1);
@@ -1632,9 +1536,8 @@ screen_write_insertline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0)
ny = 1;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#endif
if (s->cy < s->rupper || s->cy > s->rlower) {
@@ -1700,9 +1603,8 @@ screen_write_deleteline(struct screen_write_ctx *ctx, u_int ny, u_int bg)
if (ny == 0)
ny = 1;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#endif
if (s->cy < s->rupper || s->cy > s->rlower) {
@@ -1773,9 +1675,8 @@ screen_write_clearline(struct screen_write_ctx *ctx, u_int bg)
if (gl->cellsize == 0 && COLOUR_DEFAULT(bg))
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
flags = gl->flags & GRID_LINE_OSC133_FLAGS;
@@ -1812,9 +1713,8 @@ screen_write_clearendofline(struct screen_write_ctx *ctx, u_int bg)
if (s->cx > sx - 1 || (s->cx >= gl->cellsize && COLOUR_DEFAULT(bg)))
return;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
grid_view_clear(s->grid, s->cx, s->cy, sx - s->cx, 1, bg);
@@ -1839,9 +1739,8 @@ screen_write_clearstartofline(struct screen_write_ctx *ctx, u_int bg)
return;
}
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), 1);
#endif
if (s->cx > sx - 1)
@@ -1893,9 +1792,8 @@ screen_write_reverseindex(struct screen_write_ctx *ctx, u_int bg)
return;
}
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_scroll(ctx, 1);
#endif
grid_view_scroll_region_down(s->grid, s->rupper, s->rlower, bg);
@@ -1945,9 +1843,6 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct grid_line *gl;
#ifdef ENABLE_SIXEL
int redraw = 0;
#endif
u_int rupper = s->rupper, rlower = s->rlower;
gl = grid_get_line(gd, gd->hsize + s->cy);
@@ -1968,13 +1863,12 @@ screen_write_linefeed(struct screen_write_ctx *ctx, int wrapped, u_int bg)
return;
}
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
if (rlower == screen_size_y(s) - 1)
redraw = image_scroll_up(s, 1);
image_redraw_scroll(ctx, 1);
else
redraw = image_check_line(s, rupper, rlower - rupper);
if (redraw && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
image_redraw_area(ctx, 0, rupper, screen_size_x(s),
rlower - rupper);
#endif
grid_view_scroll_region_up(gd, s->rupper, s->rlower, bg);
@@ -2000,9 +1894,8 @@ screen_write_scrollup(struct screen_write_ctx *ctx, u_int lines, u_int bg)
ctx->bg = bg;
}
#ifdef ENABLE_SIXEL
if (image_scroll_up(s, lines) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_scroll(ctx, lines);
#endif
for (i = 0; i < lines; i++) {
@@ -2029,9 +1922,8 @@ screen_write_scrolldown(struct screen_write_ctx *ctx, u_int lines, u_int bg)
else if (lines > s->rlower - s->rupper + 1)
lines = s->rlower - s->rupper + 1;
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_scroll(ctx, lines);
#endif
for (i = 0; i < lines; i++)
@@ -2070,9 +1962,8 @@ screen_write_clearendofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_SIXEL
if (image_check_line(s, s->cy, sy - s->cy) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, s->cy, screen_size_x(s), sy - s->cy);
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2151,9 +2042,8 @@ screen_write_clearstartofscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_SIXEL
if (image_check_line(s, 0, s->cy - 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_area(ctx, 0, 0, screen_size_x(s), s->cy + 1);
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2224,9 +2114,8 @@ screen_write_clearscreen(struct screen_write_ctx *ctx, u_int bg)
struct visible_ranges *r;
struct visible_range *ri;
#ifdef ENABLE_SIXEL
if (image_free_all(s) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
image_redraw_all(ctx);
#endif
screen_write_initctx(ctx, &ttyctx, 1, 1);
@@ -2564,8 +2453,10 @@ screen_write_collect_flush(struct screen_write_ctx *ctx, int scroll_only,
if (wp != NULL && (wp->flags & (PANE_REDRAW|PANE_DROP)))
goto discard;
if (s->mode & MODE_SYNC) {
if (ctx->scrolled != 0)
screen_write_sync_scroll_dirty(ctx);
if (ctx->scrolled != 0) {
screen_write_should_draw_lines(ctx, s->rupper,
s->rlower + 1 - s->rupper);
}
for (y = 0; y < screen_size_y(s); y++) {
cl = &s->write_list[y];
if (!TAILQ_EMPTY(&cl->items))
@@ -2727,9 +2618,8 @@ screen_write_collect_end(struct screen_write_ctx *ctx)
}
}
#ifdef ENABLE_SIXEL
if (image_check_area(s, s->cx, s->cy, ci->used, 1) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
#ifdef ENABLE_IMAGES
screen_write_image_damage(ctx, s->cx, s->cy, ci->used, 1);
#endif
grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x,
@@ -2859,6 +2749,11 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc)
return;
screen_write_initctx(ctx, &ttyctx, 0, 0);
#ifdef ENABLE_IMAGES
/* Input semantics decide which image layers this text damages. */
screen_write_image_damage(ctx, s->cx, s->cy, width, 1);
#endif
/* Handle overwriting of UTF-8 characters. */
gl = grid_get_line(s->grid, s->grid->hsize + s->cy);
if (gl->flags & GRID_LINE_EXTENDED) {
@@ -3086,6 +2981,9 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc)
force_wide = 0;
/* Set the new cell. */
#ifdef ENABLE_IMAGES
screen_write_image_damage(ctx, cx - n, cy, n, 1);
#endif
grid_view_set_cell(gd, cx - n, cy, &last);
if (force_wide)
grid_view_set_padding(gd, cx - 1, cy, last.bg);
@@ -3221,63 +3119,21 @@ screen_write_rawstring(struct screen_write_ctx *ctx, u_char *str, u_int len,
tty_write(tty_cmd_rawstring, &ttyctx);
}
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
/* Write a SIXEL image. */
void
screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si,
u_int bg)
{
struct screen *s = ctx->s;
struct grid *gd = s->grid;
struct tty_ctx ttyctx;
u_int x, y, sx, sy, cx = s->cx, cy = s->cy, i, lines;
struct sixel_image *new;
struct image *im;
if (screen_size_y(s) == 1)
return;
sixel_size_in_cells(si, &x, &y);
if (x > screen_size_x(s) || y > screen_size_y(s) - 1) {
if (x > screen_size_x(s) - cx)
sx = screen_size_x(s) - cx;
else
sx = x;
if (y > screen_size_y(s) - 1)
sy = screen_size_y(s) - 1;
else
sy = y;
new = sixel_scale(si, 0, 0, 0, y - sy, sx, sy, 1);
im = sixel_to_image(si);
if (im == NULL) {
sixel_free(si);
if (new == NULL)
return;
sixel_size_in_cells(new, &x, &y);
si = new;
return;
}
sy = screen_size_y(s) - cy;
if (sy <= y) {
lines = y - sy + 1;
if (image_scroll_up(s, lines) && ctx->wp != NULL)
ctx->wp->flags |= PANE_REDRAW;
for (i = 0; i < lines; i++) {
grid_view_scroll_region_up(gd, 0, screen_size_y(s) - 1,
bg);
screen_write_collect_scroll(ctx, bg);
}
ctx->scrolled += lines;
if (lines > cy)
screen_write_cursormove(ctx, -1, 0, 0);
else
screen_write_cursormove(ctx, -1, cy - lines, 0);
}
screen_write_collect_flush(ctx, 0, __func__);
screen_write_initctx(ctx, &ttyctx, 0, 0);
ttyctx.image = image_store(s, si);
tty_write(tty_cmd_sixelimage, &ttyctx);
screen_write_cursormove(ctx, 0, cy + y, 0);
image_write_sixel(ctx, im, bg);
image_free(image_get_id(im));
}
#endif
-50
View File
@@ -93,11 +93,6 @@ screen_init(struct screen *s, u_int sx, u_int sy, u_int hlimit)
s->tabs = NULL;
s->sel = NULL;
#ifdef ENABLE_SIXEL
TAILQ_INIT(&s->images);
TAILQ_INIT(&s->saved_images);
#endif
s->write_list = NULL;
s->hyperlinks = NULL;
@@ -131,11 +126,6 @@ screen_reinit(struct screen *s, int check)
screen_clear_selection(s);
screen_free_titles(s);
#ifdef ENABLE_SIXEL
image_free_all(s);
#endif
screen_set_progress_bar(s, PROGRESS_BAR_HIDDEN, 0);
screen_reset_hyperlinks(s);
}
@@ -154,10 +144,6 @@ screen_reset_hyperlinks(struct screen *s)
void
screen_free(struct screen *s)
{
#ifdef ENABLE_SIXEL
struct image *im;
#endif
free(s->sel);
free(s->tabs);
free(s->path);
@@ -174,17 +160,6 @@ screen_free(struct screen *s)
hyperlinks_free(s->hyperlinks);
screen_free_titles(s);
#ifdef ENABLE_SIXEL
/*
* Images saved when entering the alternate screen stay linked in the
* global list; move them back so they are freed and unlinked here, or
* a later eviction would write through this freed screen.
*/
TAILQ_CONCAT(&s->images, &s->saved_images, entry);
TAILQ_FOREACH(im, &s->images, entry)
im->list = &s->images;
image_free_all(s);
#endif
}
/* Reset tabs to default, eight spaces apart. */
@@ -377,10 +352,6 @@ screen_resize_cursor(struct screen *s, u_int sx, u_int sy, int reflow,
if (sy != screen_size_y(s))
screen_resize_y(s, sy, eat_empty, &cy);
#ifdef ENABLE_SIXEL
image_free_all(s);
#endif
if (reflow)
screen_reflow(s, sx, &cx, &cy, cursor);
@@ -692,10 +663,6 @@ int
screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
{
u_int sx, sy;
#ifdef ENABLE_SIXEL
struct image *im;
#endif
if (SCREEN_IS_ALTERNATE(s))
return 0;
sx = screen_size_x(s);
@@ -709,12 +676,6 @@ screen_alternate_on(struct screen *s, struct grid_cell *gc, int cursor)
}
memcpy(&s->saved_cell, gc, sizeof s->saved_cell);
#ifdef ENABLE_SIXEL
TAILQ_CONCAT(&s->saved_images, &s->images, entry);
TAILQ_FOREACH(im, &s->saved_images, entry)
im->list = &s->saved_images;
#endif
grid_view_clear(s->grid, 0, 0, sx, sy, 8);
s->saved_flags = s->grid->flags;
@@ -728,10 +689,6 @@ int
screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
{
u_int sx = screen_size_x(s), sy = screen_size_y(s);
#ifdef ENABLE_SIXEL
struct image *im;
#endif
/*
* If the current size is different, temporarily resize to the old size
* before copying back.
@@ -774,13 +731,6 @@ screen_alternate_off(struct screen *s, struct grid_cell *gc, int cursor)
grid_destroy(s->saved_grid);
s->saved_grid = NULL;
#ifdef ENABLE_SIXEL
image_free_all(s);
TAILQ_CONCAT(&s->images, &s->saved_images, entry);
TAILQ_FOREACH(im, &s->images, entry)
im->list = &s->images;
#endif
if (s->cx > screen_size_x(s) - 1)
s->cx = screen_size_x(s) - 1;
if (s->cy > screen_size_y(s) - 1)
+81 -17
View File
@@ -199,6 +199,7 @@ server_client_create(int fd)
c->click_wp = -1;
TAILQ_INIT(&c->input_requests);
TAILQ_INIT(&c->pending_damage);
TAILQ_INSERT_TAIL(&clients, c, entry);
log_debug("new client %p", c);
@@ -358,10 +359,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);
}
@@ -413,6 +434,7 @@ server_client_lost(struct client *c)
status_free(c);
input_cancel_requests(c);
redraw_free_pending_damage(c);
free(c->title);
free(c->path);
@@ -1355,8 +1377,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 +1818,15 @@ 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 copied
* window damage onto its own pending_damage in
* server_client_check_redraw() (see redraw_defer_damage()) to
* compose precisely on a later pass, or escalated to
* CLIENT_REDRAWWINDOW/CLIENT_REDRAWSCROLLBARS for whole-pane needs
* that aren't rectangle-shaped, so PANE_REDRAW/PANE_REDRAWSCROLLBAR
* and window damage can simply be cleared unconditionally here.
*/
RB_FOREACH(w, windows, &windows) {
TAILQ_FOREACH(wp, &w->panes, entry) {
@@ -2347,6 +2386,8 @@ server_client_any_pane_redraw(struct client *c)
return (1);
if (!TAILQ_EMPTY(&w->damage))
return (1);
if (!TAILQ_EMPTY(&c->pending_damage))
return (1);
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & (PANE_REDRAW|PANE_REDRAWSCROLLBAR))
return (1);
@@ -2363,7 +2404,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 +2429,29 @@ 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 window damage is copied onto this client's own pending_damage
* (redraw_defer_damage()) to survive that and still be composed
* precisely on a later pass; a whole-pane need (PANE_REDRAW) or a
* pane's scrollbar isn't rectangle-shaped the same way, so those
* still escalate to a coarser, persistent client flag as before.
*
* 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 +2463,8 @@ server_client_check_redraw(struct client *c)
log_debug("redraw timer started");
evtimer_add(&ev, &tv);
}
if (damaged) {
c->flags |= CLIENT_REDRAWWINDOW;
return;
}
if (!TAILQ_EMPTY(&w->damage))
redraw_defer_damage(c);
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW;
@@ -2444,8 +2499,17 @@ 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 (and any damage this client missed on a
* previous deferred pass - see redraw_defer_damage()) is
* also what makes server_client_any_pane_redraw() decide a
* redraw is needed at all, independently of any
* CLIENT_ALLREDRAWFLAGS bit. Consume both here, since
* server_client_loop() clears window damage unconditionally
* every pass regardless of whether it was actually drawn.
*/
if ((!TAILQ_EMPTY(&w->damage) || !TAILQ_EMPTY(&c->pending_damage)) &&
!(c->flags & CLIENT_ALLREDRAWFLAGS))
redraw_client_damage(c);
}
+2 -1
View File
@@ -30,7 +30,8 @@
/* Default style. */
static struct style style_default = {
{ { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 },
{ { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0
},
0,
0,
+12 -10
View File
@@ -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
@@ -5106,12 +5100,20 @@ Supports extended keys.
Supports focus reporting.
.It hyperlinks
Supports OSC 8 hyperlinks.
.It image-quadrants
Allows image fallbacks to use Unicode quadrant block characters.
.It image-sextants
Allows image fallbacks to use Unicode sextant block characters.
.It ignorefkeys
Ignore function keys from
.Xr terminfo 5
and use the
.Nm
internal set only.
.It kitty
Supports Kitty graphics using Unicode placeholders.
The terminal must support the Kitty Unicode placeholder extension, not only
the basic Kitty graphics protocol.
.It margins
Supports DECSLRM margins.
.It mouse
+232 -45
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: tmux.h,v 1.1448 2026/09/24 11:19:39 nicm Exp $ */
/* $OpenBSD: tmux.h,v 1.1447 2026/09/22 14:10:26 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -77,7 +77,12 @@ struct screen_write_cline;
struct screen_write_ctx;
struct session;
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
struct image;
struct image_backend;
struct image_rect;
struct image_line;
struct image_store;
struct sixel_image;
#endif
@@ -863,7 +868,7 @@ struct colour_palette {
struct grid_cell {
struct utf8_data data;
u_short attr;
u_char flags;
u_short flags;
int fg;
int bg;
int us;
@@ -874,7 +879,7 @@ struct grid_cell {
struct grid_extd_entry {
utf8_char data;
u_short attr;
u_char flags;
u_short flags;
int fg;
int bg;
int us;
@@ -892,7 +897,7 @@ struct grid_cell_entry {
u_char data;
} data;
};
u_char flags;
u_short flags;
} __packed;
/* OSC 133 data for a grid line. */
@@ -916,6 +921,9 @@ struct grid_line {
u_int time;
struct osc133_data osc133_data;
u_short flags;
#ifdef ENABLE_IMAGES
struct image_line *images;
#endif
};
/* Entire grid of cells. */
@@ -935,6 +943,9 @@ struct grid {
u_int scroll_generation;
struct grid_line *linedata;
#ifdef ENABLE_IMAGES
struct image_store *images;
#endif
};
/* Virtual cursor in a grid. */
@@ -1026,24 +1037,65 @@ struct style {
u_int link;
};
#ifdef ENABLE_SIXEL
/* Image. */
struct image {
struct screen *s;
struct sixel_image *data;
char *fallback;
#ifdef ENABLE_IMAGES
/* Private ACS keys used by the text image backend. */
#define TTY_ACS_IMAGE_SHADE_LIGHT 0x80
#define TTY_ACS_IMAGE_SHADE_MEDIUM 0x81
#define TTY_ACS_IMAGE_SHADE_DARK 0x82
#define TTY_ACS_IMAGE_BLOCK 0x83
#define TTY_ACS_IMAGE_HALF_UPPER 0x84
#define TTY_ACS_IMAGE_HALF_LOWER 0x85
#define TTY_ACS_IMAGE_HALF_LEFT 0x86
#define TTY_ACS_IMAGE_HALF_RIGHT 0x87
#define TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT 0x88
#define TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT 0x89
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT 0x8a
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT 0x8b
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT 0x8c
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT 0x8d
#define TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT 0x8e
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT 0x8f
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT 0x90
#define TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT 0x91
#define TTY_ACS_IMAGE_SEXTANT_FIRST 0x92
#define TTY_ACS_IMAGE_SEXTANT_LAST 0xcd
u_int px;
u_int py;
struct image_sample {
u_char red;
u_char green;
u_char blue;
u_char alpha;
u_char brightness;
};
#define IMAGE_SAMPLE_COLUMNS 2
#define IMAGE_SAMPLE_ROWS 6
struct image_cell {
struct image_sample whole;
struct image_sample samples[IMAGE_SAMPLE_ROWS][IMAGE_SAMPLE_COLUMNS];
};
struct image {
u_int id;
u_int references;
u_int flags;
u_int parent_id;
u_int source_id;
u_int width;
u_int height;
u_int canvas_width;
u_int canvas_height;
u_int sx;
u_int sy;
size_t stride;
size_t size;
u_char *pixels;
struct sixel_image *sixel;
struct image_cell *cells;
void *fallback_data;
struct images *list;
TAILQ_ENTRY (image) entry;
TAILQ_ENTRY (image) all_entry;
RB_ENTRY(image) entry;
};
TAILQ_HEAD(images, image);
RB_HEAD(images, image);
#define IMAGE_SIZE_LIMIT (64 * 1024 * 1024)
#endif
/* Cursor style. */
@@ -1102,11 +1154,6 @@ struct screen {
bitstr_t *tabs;
struct screen_sel *sel;
#ifdef ENABLE_SIXEL
struct images images;
struct images saved_images;
#endif
struct screen_write_cline *write_list;
struct hyperlinks *hyperlinks;
@@ -1124,6 +1171,7 @@ struct screen_write_ctx {
#define SCREEN_WRITE_SYNC 0x1
#define SCREEN_WRITE_OBSCURED 0x2
#define SCREEN_WRITE_CHECKED_IF_OBSCURED 0x4
#define SCREEN_WRITE_INPUT 0x8
screen_write_init_ctx_cb init_ctx_cb;
void *arg;
@@ -1348,10 +1396,6 @@ struct window_pane {
bitstr_t *sync_dirty;
u_int sync_dirty_size;
u_int sync_scrolled;
u_int sync_rupper;
u_int sync_rlower;
u_int sync_bg;
u_int sb_slider_y;
u_int sb_slider_h;
@@ -1407,7 +1451,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;
@@ -1484,6 +1528,10 @@ struct window {
struct redraw_damages damage;
u_int damage_count;
#ifdef ENABLE_IMAGES
uint64_t image_scroll_epoch;
int image_scroll_pending;
#endif
struct menu_data *menu;
u_int menu_last_px;
@@ -1752,7 +1800,13 @@ struct tty_term {
#define TERM_VT100LIKE 0x20
#define TERM_SIXEL 0x40
#define TERM_INVALIDMS 0x80
#define TERM_NOREPLACE 0x100
#define TERM_KITTY 0x100
#ifdef ENABLE_IMAGES
#define TERM_IMAGE_QUADRANTS 0x200
#define TERM_IMAGE_SEXTANTS 0x400
#define TERM_IMAGESCROLL 0x1000
#endif
#define TERM_NOREPLACE 0x800
int flags;
LIST_ENTRY(tty_term) entry;
@@ -1810,6 +1864,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;
@@ -1856,6 +1916,13 @@ struct tty {
struct event key_timer;
struct tty_key *key_tree;
#ifdef ENABLE_IMAGES
const struct image_backend *image_backend;
void *image_data;
struct window *image_scroll_window;
uint64_t image_scroll_epoch;
int image_scroll_failed;
#endif
};
/* Terminal command context. */
@@ -1891,7 +1958,7 @@ struct tty_ctx {
size_t size;
} sel;
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
struct image *image;
#endif
};
@@ -2252,6 +2319,23 @@ struct client {
struct redraw_scene *redraw_scene;
/*
* Damage this client missed because its redraw was deferred (pending
* tty output) on the pass it was reported - server_client_loop()
* frees w->damage unconditionally every pass regardless of whether
* every attached client got to consume it, so a deferred client's
* copy is kept here to compose precisely on a later pass instead of
* escalating to a full-window redraw. See redraw_defer_damage() and
* redraw_client_damage() (screen-redraw.c). pending_damage_id is the
* id of the window these rectangles were copied for (window ids are
* unique and never reused), used to discard them if the client's
* current window has since changed instead of composing them
* against the wrong window's scene.
*/
struct redraw_damages pending_damage;
u_int pending_damage_count;
u_int pending_damage_id;
struct event repeat_timer;
struct event click_timer;
@@ -2990,10 +3074,6 @@ void tty_cmd_scrolldown(struct tty *, const struct tty_ctx *);
void tty_cmd_reverseindex(struct tty *, const struct tty_ctx *);
void tty_cmd_setselection(struct tty *, const struct tty_ctx *);
void tty_cmd_rawstring(struct tty *, const struct tty_ctx *);
#ifdef ENABLE_SIXEL
void tty_cmd_sixelimage(struct tty *, const struct tty_ctx *);
void tty_draw_images(struct client *, struct window_pane *);
#endif
void tty_cmd_syncstart(struct tty *, const struct tty_ctx *);
void tty_default_colours(struct grid_cell *, struct window_pane *, u_int *);
@@ -3035,6 +3115,7 @@ void tty_default_features(struct client *, const char *, u_int);
/* tty-acs.c */
int tty_acs_needed(struct tty *);
const char *tty_acs_get(struct tty *, u_char);
u_char tty_acs_image_sextant(u_int);
int tty_acs_reverse_get(struct tty *, const char *, size_t);
const struct utf8_data *tty_acs_double_borders(int);
const struct utf8_data *tty_acs_heavy_borders(int);
@@ -3598,7 +3679,7 @@ void screen_write_mode_clear(struct screen_write_ctx *, int);
void screen_write_start_sync(struct window_pane *);
void screen_write_stop_sync(struct window_pane *);
void screen_write_end_sync(struct screen_write_ctx *);
void screen_write_sync_clear_dirty(struct window_pane *);
void screen_write_clear_dirty(struct window_pane *);
void screen_write_cursorup(struct screen_write_ctx *, u_int);
void screen_write_cursordown(struct screen_write_ctx *, u_int);
void screen_write_cursorright(struct screen_write_ctx *, u_int);
@@ -3632,7 +3713,7 @@ void screen_write_setselection(struct screen_write_ctx *, const char *,
u_char *, u_int);
void screen_write_rawstring(struct screen_write_ctx *, u_char *, u_int,
int);
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
void screen_write_sixelimage(struct screen_write_ctx *,
struct sixel_image *, u_int);
#endif
@@ -3650,8 +3731,15 @@ int redraw_client_has_window(struct client *, struct window *);
void redraw_invalidate_scene(struct window *);
void redraw_invalidate_all_scenes(void);
void redraw_damage_window(struct window *, u_int, u_int, u_int, u_int);
#ifdef ENABLE_IMAGES
void redraw_damage_window_scroll(struct window *, u_int, u_int, u_int,
u_int);
void redraw_image_scroll_result(struct tty *, const struct tty_ctx *, int);
#endif
void redraw_free_damage(struct window *);
void redraw_client_damage(struct client *);
void redraw_defer_damage(struct client *);
void redraw_free_pending_damage(struct client *);
int redraw_get_status_border_cell_type(struct redraw_span **, u_int);
/* screen.c */
@@ -3825,9 +3913,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 *);
@@ -4216,17 +4304,115 @@ void spawn_editor_finish(struct window_pane *);
/* regsub.c */
char *regsub(const char *, const char *, const char *, int);
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
/* image.c */
int image_free_all(struct screen *);
struct image *image_store(struct screen *, struct sixel_image *);
int image_check_line(struct screen *, u_int, u_int);
int image_check_area(struct screen *, u_int, u_int, u_int, u_int);
int image_scroll_up(struct screen *, u_int);
#define IMAGE_BACKEND_GRAPHICAL 0x1
#define IMAGE_BACKEND_SCROLLS 0x2
#define IMAGE_BACKEND_CLIPPED 0x4
struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int,
u_char *);
struct image *image_create_view(struct image *, u_int, u_int, u_int,
u_int, u_int, u_int, u_int, u_int, u_int, u_int);
struct image *image_find(u_int);
u_int image_get_id(const struct image *);
void image_get_size(const struct image *, u_int *, u_int *);
void image_get_canvas_size(const struct image *, u_int *, u_int *);
void image_get_size_in_cells(const struct image *, u_int *, u_int *);
const u_char *image_get_pixels(const struct image *, size_t *, size_t *);
void image_set_no_cursor(struct image *);
struct sixel_image *image_get_sixel(const struct image *);
void image_set_sixel(struct image *, struct sixel_image *);
void image_ref(u_int);
void image_free(u_int);
void image_write_sixel(struct screen_write_ctx *, struct image *,
u_int);
void image_write_kitty(struct screen_write_ctx *, struct image *,
u_int, u_int, u_int, int32_t);
void image_get_pixel_rect(const struct image *, u_int, u_int,
u_int, u_int, u_int *, u_int *, u_int *, u_int *);
void image_size_in_cells(u_int, u_int, u_int, u_int, u_int *,
u_int *);
u_char *image_base64_decode(const char *, size_t, size_t, size_t *);
u_char *image_png_decode(const u_char *, size_t, size_t, u_int *,
u_int *);
void image_redraw_area(struct screen_write_ctx *, u_int, u_int,
u_int, u_int);
void image_redraw_all(struct screen_write_ctx *);
void image_redraw_scroll(struct screen_write_ctx *, u_int);
void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
void image_redraw_finish(struct tty *);
void image_draw_flush(struct tty *);
int image_backend_flags(struct tty *);
int image_tty_update(struct tty *);
void image_tty_geometry_changed(struct tty *);
void image_tty_free(struct tty *, int);
void image_draw_line(struct tty *, struct screen *, u_int, u_int,
u_int, u_int, u_int, int, const struct tty_style_ctx *);
void image_get_fallback_cell(struct tty *, struct image *, u_int,
u_int, const struct grid_cell *, struct grid_cell *,
const struct tty_style_ctx *);
const struct image_cell *image_get_cell(struct image *, u_int, u_int);
void image_free_fallback(struct image *);
int image_get_fallback_at(struct tty *, struct screen *, u_int,
u_int, const struct grid_cell *, struct grid_cell *,
const struct tty_style_ctx *);
struct image *image_rect_get_image(const struct image_rect *);
const struct grid_cell *image_rect_get_cell(
const struct image_rect *);
void image_rect_get_coords(const struct image_rect *,
u_int *, u_int *, u_int *, u_int *, u_int *, u_int *);
int32_t image_rect_get_z(const struct image_rect *);
void image_clear(struct screen_write_ctx *, u_int);
void image_clear_kitty(struct screen_write_ctx *, char, u_int,
u_int, int32_t);
void image_grid_damage(struct grid *, u_int, u_int, u_int, u_int);
void image_grid_free_line(struct grid *, struct grid_line *);
void image_grid_free(struct grid *);
void image_grid_move_cells(struct grid *, u_int, u_int, u_int,
u_int);
void image_grid_duplicate_lines(struct grid *, u_int, struct grid *,
u_int, u_int);
void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *,
u_int, u_int, u_int, u_int);
void image_grid_resize_width(struct grid *, u_int);
int image_grid_line_has_images(const struct grid_line *);
int image_grid_check_area(struct grid *, u_int, u_int, u_int,
u_int);
int image_grid_next_span(struct grid *, u_int, u_int, u_int,
u_int *, u_int *);
int image_grid_get_source(struct grid *, u_int, u_int,
struct image *, u_int *, u_int *);
void image_place_cell_kitty(struct screen_write_ctx *, struct image *,
u_int, u_int, u_int, u_int, u_int, u_int, int32_t);
#define KITTY_PARSE_ERROR -1
#define KITTY_PARSE_OK 0
#define KITTY_PARSE_MORE 1
#define KITTY_PARSE_MISSING 2
struct image *kitty_parse_image(void **, const u_char *, size_t, u_int,
u_int, u_int *, u_int *, u_int *, char *, char *, u_int *,
int32_t *, int *);
int kitty_placeholder_to_image(void *, struct grid *,
struct grid_cell *, u_int, u_int, struct image **, u_int *,
u_int *, u_int *, u_int *, int32_t *);
void kitty_free_state(void *);
void kitty_draw_rect(struct tty *,
const struct image_rect *, const struct tty_style_ctx *);
void kitty_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
void kitty_redraw_finish(struct tty *);
void kitty_free_output_state(struct tty *, int);
#endif
#ifdef ENABLE_IMAGES
/* image-sixel.c */
#define SIXEL_COLOUR_REGISTERS 1024
struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int);
void sixel_draw_rect(struct tty *,
const struct image_rect *, const struct tty_style_ctx *);
void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int);
void sixel_free_output(struct tty *, int);
void sixel_flush_output(struct tty *);
struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int,
u_int);
void sixel_free(struct sixel_image *);
void sixel_log(struct sixel_image *);
void sixel_size_in_cells(struct sixel_image *, u_int *, u_int *);
@@ -4235,6 +4421,7 @@ struct sixel_image *sixel_scale(struct sixel_image *, u_int, u_int, u_int,
char *sixel_print(struct sixel_image *, struct sixel_image *,
size_t *);
struct screen *sixel_to_screen(struct sixel_image *);
struct image *sixel_to_image(struct sixel_image *);
#endif
/* server-acl.c */
+51
View File
@@ -0,0 +1,51 @@
#!/bin/sh
# Generate the Unicode diacritic table used by Kitty image placeholders.
#
# Input must be UnicodeData.txt from Unicode 6.0.0:
# https://www.unicode.org/Public/6.0.0/ucd/UnicodeData.txt
# SHA-256: 90b45b777346bef027556f9c6cb3ea5d7d745bd60d6762855c08d8a22d34f771
#
# The selection follows the public Kitty graphics protocol: nonspacing marks
# with canonical combining class 230, NSM bidi class, no decomposition, minus
# characters which may be combined or normalized in ways unsuitable for a
# row or column index.
if [ "$#" -ne 1 ]; then
echo "usage: $0 UnicodeData.txt" >&2
exit 1
fi
awk -F ';' '
BEGIN {
omit["0300"] = omit["0301"] = omit["0302"] = 1
omit["0303"] = omit["0304"] = omit["0306"] = 1
omit["0307"] = omit["0308"] = omit["0309"] = 1
omit["030A"] = omit["030B"] = omit["030C"] = 1
omit["030F"] = omit["0311"] = omit["0313"] = 1
omit["0314"] = omit["0342"] = omit["0653"] = 1
omit["0654"] = 1
}
$3 == "Mn" && $4 == "230" && $5 == "NSM" && $6 == "" && !omit[$1] {
point[++count] = $1
}
END {
if (count != 297) {
printf "expected 297 diacritics, found %u\n", count > "/dev/stderr"
exit 1
}
print "static const uint32_t kitty_diacritics[] = {"
for (i = 1; i <= count; i++) {
if ((i - 1) % 8 == 0)
printf "\t"
printf "0x%s", point[i]
if (i != count)
printf ","
if (i == count || i % 8 == 0)
printf "\n"
else
printf " "
}
print "};"
}
' "$1"
+104
View File
@@ -0,0 +1,104 @@
#!/bin/sh
# Compact visual check for image subcell geometry and layering. Run inside a
# tmux pane on Kitty or a SIXEL terminal; it needs at least 90 columns/24 rows.
if [ -z "$TMUX" ]; then
echo "Run this script inside tmux." >&2
exit 1
fi
columns=$(tput cols 2>/dev/null || echo 0)
lines=$(tput lines 2>/dev/null || echo 0)
if [ "$columns" -lt 90 ] || [ "$lines" -lt 24 ]; then
echo "This test needs at least 90 columns and 24 rows." >&2
exit 1
fi
at()
{
printf '\033[%s;%sH' "$1" "$2"
}
kitty()
{
printf '\033_G%s\033\\' "$1"
}
kitty_solid()
{
id=$1
rgba=$2
payload=$(i=0; while [ "$i" -lt 256 ]; do
printf "$rgba"
i=$((i + 1))
done | base64 | tr -d '\n')
kitty "a=t,q=2,f=32,s=16,v=16,i=$id;$payload"
}
sixel_folder()
{
printf '\033P0;1q"1;1;26;26#0;2;50;50;50#1;2;0;0;0#2;2;49;49;49#3;2;33;33;33#4;2;47;47;47#5;2;44;44;44#6;2;48;48;48#2?O?C!5?S!4?__?_?__$#0?_wW!5KGwo_-#0?~~!8?!10@BB}w$#2!12?a_A?A?A?A??@E-#0?~~!7?KK~~KK!7?~~$#2!9?K!6?K-#0?F^[owowowowowowowowooS^F$#2?G!10?@@!7?G??G-\033\\'
}
feature_info=$(tmux display-message -p \
'term=#{client_termname} features=#{client_termfeatures} cells=#{client_cell_width}x#{client_cell_height}')
printf '\033[2J\033[H\033[?25l'
at 1 2
printf 'tmux image edge-case check'
at 2 2
printf '%s' "$feature_info"
kitty_solid 201 '\377\000\000\377'
kitty_solid 202 '\000\377\000\377'
kitty_solid 203 '\000\000\377\377'
# The 26-pixel folder occupies whole grid cells, but its visible right and
# bottom edges must remain partial rather than stretching to those boundaries.
at 4 2
printf '1. partial SIXEL: folder should be the same size as direct output'
at 4 72
sixel_folder
# Chawan emits X/Y for its image padding. The red square must begin slightly
# below and right of the [img] anchor, never over it.
at 8 2
printf '2. Kitty X/Y: RED begins below/right of the [img] anchor'
at 8 72
printf '[img]'
at 9 72
kitty 'a=p,q=2,C=1,i=201,p=1,x=0,y=0,w=16,h=16,c=3,r=2,X=6,Y=6'
# Positive Kitty z overlays text and other lower z images; negative z is
# below text. The shared red/blue edge makes any wrong order obvious.
at 12 2
printf '3. Kitty z: BLUE covers RED; GREEN stays behind TEXT'
at 12 72
printf 'TEXT'
at 12 72
kitty 'a=p,q=2,C=1,i=202,p=2,x=0,y=0,w=16,h=16,c=4,r=2,z=-1'
at 14 72
kitty 'a=p,q=2,C=1,i=201,p=3,x=0,y=0,w=16,h=16,c=4,r=2,z=1'
at 14 74
kitty 'a=p,q=2,C=1,i=203,p=4,x=0,y=0,w=16,h=16,c=4,r=2,z=2'
# SIXEL is temporal: a later image covers text, while later text destroys the
# affected image cells. Both results should match on Kitty and SIXEL output.
at 18 2
printf '4. SIXEL after text: folder covers FOLDER'
at 18 72
printf 'FOLDER'
at 18 72
sixel_folder
at 21 2
printf '5. SIXEL before text: TEXT WINS, no folder remnant'
at 21 72
sixel_folder
at 21 72
printf 'TEXT WINS'
at 23 2
printf 'Take one screenshot, then press Enter to return to the shell.'
printf '\033[?25h'
IFS= read -r _image_edge_reply
+109
View File
@@ -0,0 +1,109 @@
#!/bin/sh
# One-screen visual check for tmux image input and layering. Run this inside a
# tmux pane attached to Kitty, WezTerm, Microsoft Terminal, or a basic terminal.
if [ -z "$TMUX" ]; then
echo "Run this script inside tmux." >&2
exit 1
fi
columns=$(tput cols 2>/dev/null || echo 0)
lines=$(tput lines 2>/dev/null || echo 0)
if [ "$columns" -lt 72 ] || [ "$lines" -lt 23 ]; then
echo "This test needs at least 72 columns and 23 rows." >&2
exit 1
fi
at()
{
printf '\033[%s;%sH' "$1" "$2"
}
kitty()
{
printf '\033_G%s\033\\' "$1"
}
# Transmit a 16x16 solid RGBA texture. A one-pixel texture is unsuitable here
# because Kitty filters its transparent texture border into scaled edges.
kitty_solid()
{
id=$1
rgba=$2
payload=$(i=0; while [ "$i" -lt 256 ]; do
printf "$rgba"
i=$((i + 1))
done | base64 | tr -d '\n')
kitty "a=t,q=2,f=32,s=16,v=16,i=$id;$payload"
}
# A small 26x26 transparent SIXEL folder, used by the supplied bptato tests.
sixel_folder()
{
printf '\033P0;1q"1;1;26;26#0;2;50;50;50#1;2;0;0;0#2;2;49;49;49#3;2;33;33;33#4;2;47;47;47#5;2;44;44;44#6;2;48;48;48#2?O?C!5?S!4?__?_?__$#0?_wW!5KGwo_-#0?~~!8?!10@BB}w$#2!12?a_A?A?A?A??@E-#0?~~!7?KK~~KK!7?~~$#2!9?K!6?K-#0?F^[owowowowowowowowooS^F$#2?G!10?@@!7?G??G-\033\\'
}
feature_info=$(tmux display-message -p \
'term=#{client_termname} features=#{client_termfeatures} cells=#{client_cell_width}x#{client_cell_height}')
printf '\033[2J\033[H\033[?25l'
at 1 2
printf 'tmux image visual check'
at 2 2
printf '%s' "$feature_info"
# Red is placed after its text at a positive Kitty z-index, so it must cover
# the text. Green is placed below the text at a negative Kitty z-index.
kitty_solid 101 '\377\000\000\377'
kitty_solid 102 '\000\377\000\377'
kitty_solid 103 '\000\000\377\377'
kitty_solid 104 '\377\000\000\177'
at 4 2
printf '1. positive Kitty z: RED must hide TEXT UNDER at right'
at 4 62
printf 'TEXT UNDER'
at 4 60
kitty 'a=p,q=2,C=1,i=101,p=1,x=1,y=1,w=14,h=14,c=18,r=2,z=1'
at 7 2
printf '2. negative Kitty z: GREEN remains behind TEXT ON TOP'
at 7 60
kitty 'a=p,q=2,C=1,i=102,p=2,x=1,y=1,w=14,h=14,c=18,r=2,z=-1'
at 7 62
printf 'TEXT ON TOP'
at 10 2
printf '3. Kitty z overlap: RED left, BLUE on top at right'
at 11 55
kitty 'a=p,q=2,C=1,i=101,p=3,x=1,y=1,w=14,h=14,c=18,r=3,z=1'
at 11 64
kitty 'a=p,q=2,C=1,i=103,p=4,x=1,y=1,w=14,h=14,c=9,r=3,z=2'
at 15 2
printf '4. alpha: half-transparent RED, dithered on SIXEL'
at 15 60
kitty 'a=p,q=2,C=1,i=104,p=5,x=1,y=1,w=14,h=14,c=18,r=2,z=1'
# SIXEL semantics are temporal: later text destroys a SIXEL image, while a
# later SIXEL image appears over existing text.
at 18 2
printf '5. SIXEL after text: folder covers FOLDER at right'
at 18 65
printf 'FOLDER'
at 18 65
sixel_folder
at 21 2
printf '6. SIXEL before text: no folder remains behind TEXT WINS'
at 21 65
sixel_folder
at 21 65
printf 'TEXT WINS'
at 23 2
printf 'Take one screenshot, then press Enter to return to the shell.'
at 23 66
printf '\033[?25h'
IFS= read -r _image_visual_reply
+1
View File
@@ -0,0 +1 @@
P0;0;0q"1;1;160;200#0;2;0;0;0#1;2;4;0;0#2;2;0;4;0#3;2;0;0;4#4;2;5;0;5#5;2;5;5;5#6;2;8;0;0#7;2;11;0;0#8;2;0;7;0#9;2;0;11;0#10;2;7;7;0#11;2;9;9;0#12;2;0;0;9#13;2;7;0;7#14;2;11;0;11#15;2;0;9;9#16;2;7;7;7#17;2;11;11;11#18;2;15;0;0#19;2;20;0;0#20;2;14;9;0#21;2;18;11;0#22;2;0;14;0#23;2;0;20;0#24;2;0;23;0#25;2;18;18;0#26;2;20;13;0#27;2;20;20;0#28;2;0;0;14#29;2;0;0;16#30;2;15;0;15#31;2;17;0;17#32;2;20;0;20#33;2;24;0;24#34;2;0;16;16#35;2;0;19;19#36;2;14;14;14#37;2;18;18;18#38;2;20;20;20#39;2;24;24;24#40;2;25;0;0#41;2;36;0;0#42;2;40;0;0#43;2;47;0;0#44;2;0;27;0#45;2;0;33;0#46;2;0;36;0#47;2;0;39;0#48;2;0;43;0#49;2;0;45;0#50;2;0;49;0#51;2;27;27;0#52;2;35;35;0#53;2;39;39;0#54;2;0;0;27#55;2;0;0;31#56;2;0;0;36#57;2;0;0;40#58;2;0;0;42#59;2;0;0;45#60;2;0;0;47#61;2;27;0;27#62;2;31;0;31#63;2;34;0;34#64;2;36;0;36#65;2;40;0;40#66;2;42;0;42#67;2;45;0;45#68;2;49;0;49#69;2;0;32;32#70;2;0;41;41#71;2;0;49;49#72;2;28;28;28#73;2;30;30;30#74;2;33;33;33#75;2;35;35;35#76;2;38;38;38#77;2;42;42;42#78;2;45;45;45#79;2;49;49;49#80;2;60;0;0#81;2;64;0;0#82;2;68;0;0#83;2;71;0;0#84;2;74;0;0#85;2;76;0;0#86;2;80;0;0#87;2;89;0;0#88;2;92;0;0#89;2;96;0;0#90;2;100;0;0#91;2;54;35;0#92;2;63;41;0#93;2;68;44;0#94;2;0;50;0#95;2;55;55;0#96;2;71;71;0#97;2;74;74;0#98;2;82;53;0#99;2;91;58;0#100;2;100;65;0#101;2;77;77;0#102;2;80;80;0#103;2;87;87;0#104;2;89;89;0#105;2;93;93;0#106;2;96;96;0#107;2;100;100;0#108;2;0;0;50#109;2;0;0;68#110;2;0;0;71#111;2;0;0;73#112;2;0;0;76#113;2;0;0;78#114;2;0;0;83#115;2;0;0;87#116;2;0;0;89#117;2;0;0;96#118;2;0;0;100#119;2;50;0;50#120;2;58;0;58#121;2;64;0;64#122;2;71;0;71#123;2;78;0;78#124;2;78;0;78#125;2;86;0;86#126;2;93;0;93#127;2;96;0;96#128;2;100;0;100#129;2;0;52;52#130;2;0;55;55#131;2;0;58;58#132;2;0;62;62#133;2;0;73;73#134;2;0;77;77#135;2;0;83;83#136;2;0;89;89#137;2;0;93;93#138;2;0;97;97#139;2;0;100;100#140;2;51;51;51#141;2;64;64;64#142;2;69;69;69#143;2;76;76;76#144;2;80;80;80#145;2;82;82;82#146;2;87;87;87#147;2;89;89;89#148;2;93;93;93#149;2;94;94;94#150;2;100;100;100#90!160~-!4~NBbrrBF!149~$#85!4?_?O#6C??_$#88!4?O#1_???G#40O$#42!5?GC#86G#18C#83_G$#0!5?O#19G#82?G#43O$#89!5?C#80???C-#90!4~o?FNN?_{!148~$#81!4?B#89_!5?B$#84!4?C#0G!4?B$#88!4?G#1C!4?C$#41!5?O_#6_?O#82O$#7!5?B#40O???G$#86!6?G??B$#87!7?O#18_#43G$#85!8?O#80_$#83!9?C-#90!160B$#100!160{-!5~^^NF!151~$#99!5?_??G$#93!6?_#21_#20o$#98!7?O-#100!5~}}??!151~$#91!5?@#26@#20@~$#92!7?}-#100!7NKK!151N$#107!160o$#92!7?B#20B-#107!160~-!4~row[K??r!148~$#102!4?C??A?C#106_$#103!4?G#105@C???@K$#0!5?C!4?K$#25!5?A???O$#95!5?G?_#10_#27A$#51!6?A???A$#52!6?@??_O$#11!7?@@#53@$#101!8?A#97G$#104!8?O-#107!4~rooqqrrv!148~$#95!4?G#0K!5G#105G$#96!4?C#25A#27@@#101@$#104!5?@#53A#102!4C$#97!6?C-#94!160~-!4~FBbrRBB!149~$#50!4?g?O?_?C$#47!4?OC#23G??O#44G$#2!5?G??CG$#0!5?O?C#45G#22_O$#48!5?_?G#24?C_$#9!6?C-#94!4~bBNKK??r!148~$#44!4?G?O?A#24_A$#49!4?CC?O#8_A#50`K$#48!4?O#0O!4?G$#9!5?G???P#23O$#47!5?_?AO#46K$#22!6?_@$#2!7?_@?C-#94!160B$#139!160{-!7~NFF!150~$#70!7?_#15_#35o$#138!7?O#34O#130G$#135!8?G-#139!4~F@?K??NN!148~$#137!4?_A#133@!4?O$#70!4?O#0!6O$#132!4?G#136___??_$#35!5?G???N$#69!5?C#138G!4?_$#15!6?A#34@?_$#71!6?C#134A#129`$#131!8?M-#118!160o$#139!8NKK!150N$#131!8?B#35B-#118!160~-!4~n?_ccCC^!148~$#115!4?O!5?A$#117!5?_!4?G_$#0!5?O!4@_$#55!5?AG#113AAA#56P$#59!5?@O#12GG#28O$#3!5?G#58A??G$#29!5?C#109C#114O#111O#110_-#118!4~xprrrow}!148~$#116!4?C!4?@#0@#115@$#109!4?A#3C?G??A$#114!5?G?C#28G#29C#60C$#54!5?AG#112?C#108G$#57!6?C#111??A-#119!160~-!4~NFBrRbb!149~$#68!4?O?_?_?C$#65!4?_#4OG?C#32CW$#61!5?GC#13C?G$#0!5?_#64OGGO-#119!4~o??KK??r!148~$#66!4?G??O?C$#62!4?A#68_K???_G$#63!4?@#0M??@?K$#64!4?C#31O!4?O$#4!5?@?`_#14OA$#30!6?O??B$#32!6?a??_$#33!6?@#67AA?@C$#65!8?OG-#119!160B$#128!160{-!4~!8N!148~$#126!4?_!6?_$#121!4?O#0!6O#122O$#124!5?!4_#120_#14_-#128!6~NB`w}!149~$#121!6?_#4O#13C#31@#122@$#127!6?O#30_#65G#64A$#67!7?G#119A#125C$#126!7?C#123O-#128!6NKK!152N$#150!160o$#13!6?A#66@$#63!6?@#121A-#150!160~-!4~Z??CC?P!149~$#148!4?c#36_???G#76_$#0!5?C?OO#39oC$#37!5?A#79_???A$#38!5?G#5O@#17@A#140G$#146!5?P#73B#144AG#78D$#75!6?G#147g#142A$#143!6?C#145?_-#150!4~worrroo{!148~$#148!4?C!6?A$#141!4?B#145G???@#149G@$#16!5?C#77C??G#0B$#36!5?B#72G#5G#144CA#74C$#146!7?C#17G#39C-#79!160~-!4~FBrrrBF!149~$#73!4?_!4?CG$#74!4?O!4?O#0_$#78!4?G#16_??C?O$#5!5?W?C?G$#77!5?C?G#75G_$#36!6?C$#38!6?G-#79!4~uCCLK?_}!148~$#78!4?G?G!4?@$#77!4?@_?O@#38G#76O$#0!5?@?a?OB$#37!5?GO???G$#75!5?A???c$#16!5?OA#5?A?C$#17!6?_?_@$#72!6?@??A$#73!8?O-#79!160B-\
+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh
# Visual test for sixel-region-scrolling: does this terminal actually move
# SIXEL pixels along with tmux's own scroll-region escapes (DECSTBM/DECSLRM
# + IND/RI/RIN), or does tmux need to fall back to redrawing the image on
# every scroll?
#
# There is no way to query a terminal for this, so it has to be judged by
# eye. Run this from inside the tmux session you want to test (attached
# from whichever terminal emulator you're checking), and watch the
# numbered colour bands in the floating pane while the counter below it
# scrolls slowly. Run it once with the option on, once with it off, and
# compare:
#
# - "off" is the known-good baseline: tmux redraws the whole image every
# scroll, so the bands must stay perfectly aligned with the counter no
# matter what the terminal does on its own.
# - "on" trusts the terminal to have moved the pixels itself. If it
# looks identical to "off", this terminal is fine. If a band freezes,
# duplicates, tears, or drifts out of sync with the counter, this
# terminal does not scroll SIXEL regions correctly and
# sixel-region-scrolling should stay off for it.
#
# Usage: sh tools/sixel-scroll-region-test.sh [on|off]
set -eu
cd "$(dirname "$0")/.."
MODE=${1:-on}
case "$MODE" in
on|off) ;;
*) echo "usage: $0 [on|off]" >&2; exit 1 ;;
esac
tmux set -s sixel-region-scrolling "$MODE"
tmux set -as terminal-features ',*:sixel'
echo "Testing with sixel-region-scrolling=$MODE. Watch the coloured bands" >&2
echo "in the floating pane - they should stay locked to the counter." >&2
sleep 2
PANE=$(tmux new-pane -d -x 24 -y 22 -PF '#{pane_id}' \
"cat '$(pwd)/tools/sixel-ruler.six'; for i in \$(seq 1 50); do echo line \$i; sleep 0.3; done; echo done - press enter; read _")
tmux select-pane -t "$PANE"
+66 -1
View File
@@ -64,9 +64,65 @@ static const struct tty_acs_entry tty_acs_table[] = {
{ '{', "\317\200" }, /* greek pi */
{ '|', "\342\211\240" }, /* not-equal */
{ '}', "\302\243" }, /* UK pound sign */
{ '~', "\302\267" } /* bullet */
{ '~', "\302\267" }, /* bullet */
#ifdef ENABLE_IMAGES
{ TTY_ACS_IMAGE_SHADE_LIGHT, "\342\226\221" },
{ TTY_ACS_IMAGE_SHADE_MEDIUM, "\342\226\222" },
{ TTY_ACS_IMAGE_SHADE_DARK, "\342\226\223" },
{ TTY_ACS_IMAGE_BLOCK, "\342\226\210" },
{ TTY_ACS_IMAGE_HALF_UPPER, "\342\226\200" },
{ TTY_ACS_IMAGE_HALF_LOWER, "\342\226\204" },
{ TTY_ACS_IMAGE_HALF_LEFT, "\342\226\214" },
{ TTY_ACS_IMAGE_HALF_RIGHT, "\342\226\220" },
{ TTY_ACS_IMAGE_QUADRANT_LOWER_LEFT, "\342\226\226" },
{ TTY_ACS_IMAGE_QUADRANT_LOWER_RIGHT, "\342\226\227" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT, "\342\226\230" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_LEFT_LOWER_RIGHT,
"\342\226\231" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_LOWER_RIGHT, "\342\226\232" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_LEFT,
"\342\226\233" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_LEFT_UPPER_RIGHT_LOWER_RIGHT,
"\342\226\234" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT, "\342\226\235" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT, "\342\226\236" },
{ TTY_ACS_IMAGE_QUADRANT_UPPER_RIGHT_LOWER_LEFT_LOWER_RIGHT,
"\342\226\237" }
#endif
};
#ifdef ENABLE_IMAGES
static char tty_acs_image_sextants[60][5];
static void
tty_acs_image_sextants_init(void)
{
u_int cp, i;
if (tty_acs_image_sextants[0][0] != '\0')
return;
for (i = 0; i < nitems(tty_acs_image_sextants); i++) {
cp = 0x1fb00 + i;
tty_acs_image_sextants[i][0] = 0xf0 | (cp >> 18);
tty_acs_image_sextants[i][1] = 0x80 | ((cp >> 12) & 0x3f);
tty_acs_image_sextants[i][2] = 0x80 | ((cp >> 6) & 0x3f);
tty_acs_image_sextants[i][3] = 0x80 | (cp & 0x3f);
}
}
u_char
tty_acs_image_sextant(u_int mask)
{
if (mask == 0 || mask == 21 || mask == 42 || mask >= 63)
return (0);
if (mask < 21)
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 1);
if (mask < 42)
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 2);
return (TTY_ACS_IMAGE_SEXTANT_FIRST + mask - 3);
}
#endif
/* Table mapping UTF-8 to ACS entries. */
struct tty_acs_reverse_entry {
const char *string;
@@ -239,6 +295,15 @@ tty_acs_get(struct tty *tty, u_char ch)
return (&tty->term->acs[ch][0]);
}
#ifdef ENABLE_IMAGES
if (ch >= TTY_ACS_IMAGE_SEXTANT_FIRST &&
ch <= TTY_ACS_IMAGE_SEXTANT_LAST) {
tty_acs_image_sextants_init();
return (tty_acs_image_sextants[
ch - TTY_ACS_IMAGE_SEXTANT_FIRST]);
}
#endif
/* Otherwise look up the UTF-8 translation. */
entry = bsearch(&ch, tty_acs_table, nitems(tty_acs_table),
sizeof tty_acs_table[0], tty_acs_cmp);
+24 -5
View File
@@ -122,6 +122,10 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
struct grid *gd = s->grid;
const struct grid_cell *gcp;
struct grid_cell gc, ngc, last;
#ifdef ENABLE_IMAGES
struct grid_cell image_gc;
int image_status;
#endif
struct grid_line *gl;
u_int i, j, last_i, cx, ex, width;
u_int cellsize, bg;
@@ -251,20 +255,35 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx,
if (px >= ex || i >= ex - px) {
/* Outside the area being drawn. */
empty = nx - i;
gcp = &grid_default_cell;
#ifdef ENABLE_IMAGES
image_status = image_get_fallback_at(tty, s, px + i,
py, gcp, &image_gc, style_ctx);
if (image_status == 1) {
gcp = &image_gc;
empty = 0;
} else if (image_status == -1)
empty = 1;
else
#endif
empty = nx - i;
} else {
/* Get the current cell. */
grid_view_get_cell(gd, px + i, py, &gc);
gcp = &gc;
#ifdef ENABLE_IMAGES
if (image_get_fallback_at(tty, s, px + i, py, &gc,
&image_gc, style_ctx) == 1)
gcp = &image_gc;
#endif
/* Work out empty cells. */
empty = tty_draw_line_get_empty(&gc, &last,
nx - i);
empty = tty_draw_line_get_empty(gcp, &last, nx - i);
if (empty != 0)
gcp = &gc;
;
else {
/* Update for codeset if needed. */
gcp = tty_check_codeset(tty, &gc);
gcp = tty_check_codeset(tty, gcp);
/* And for selection. */
if (gcp->flags & GRID_FLAG_SELECTED) {
+72 -3
View File
@@ -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>
@@ -236,9 +236,11 @@ static const struct tty_feature tty_feature_strikethrough = {
0
};
#define TTY_FEATURE_SYNC "Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;"
/* Terminal supports synchronized updates. */
static const char *const tty_feature_sync_capabilities[] = {
"Sync=\\E[?2026%?%p1%{1}%-%tl%eh%;",
TTY_FEATURE_SYNC,
NULL
};
static const struct tty_feature tty_feature_sync = {
@@ -369,6 +371,50 @@ static const struct tty_feature tty_feature_sixel = {
TERM_SIXEL
};
/* Terminal has Kitty graphics protocol Unicode placeholder capability. */
static const char *const tty_feature_kitty_capabilities[] = {
"Kty",
TTY_FEATURE_SYNC,
NULL
};
static const struct tty_feature tty_feature_kitty = {
"kitty",
tty_feature_kitty_capabilities,
TERM_KITTY
};
#ifdef ENABLE_IMAGES
static const char *const tty_feature_image_capabilities[] = {
NULL
};
static const struct tty_feature tty_feature_image_quadrants = {
"image-quadrants",
tty_feature_image_capabilities,
TERM_IMAGE_QUADRANTS
};
static const struct tty_feature tty_feature_image_sextants = {
"image-sextants",
tty_feature_image_capabilities,
TERM_IMAGE_SEXTANTS
};
/*
* Terminal moves SIXEL or Kitty image content along with the rest of a
* scrolling region, rather than needing it redrawn after every scroll.
* There is no way to ask a terminal this, and it does not correlate with
* DECSLRM/margins support - confirmed by direct testing that mintty
* scrolls text within a margin-bounded region correctly but drops sixel
* content placed there, while WezTerm and Windows Terminal move it
* correctly - so this is granted per terminal individually rather than
* assumed from any other capability.
*/
static const struct tty_feature tty_feature_imagescroll = {
"imagescroll",
tty_feature_image_capabilities,
TERM_IMAGESCROLL
};
#endif
/* Terminal supports the OSC 9;4 progress bar. */
static const char *const tty_feature_progressbar_capabilities[] = {
"Spb=\\E]9;4;%p1%d;%p2%d\\E\\\\",
@@ -399,6 +445,12 @@ static const struct tty_feature *const tty_features[] = {
&tty_feature_extkeys,
&tty_feature_focus,
&tty_feature_ignorefkeys,
&tty_feature_kitty,
#ifdef ENABLE_IMAGES
&tty_feature_image_quadrants,
&tty_feature_image_sextants,
&tty_feature_imagescroll,
#endif
&tty_feature_margins,
&tty_feature_mouse,
&tty_feature_osc7,
@@ -637,7 +689,9 @@ tty_default_features(struct client *c, const char *name, u_int version)
"extkeys,"
"focus,"
"hyperlinks,"
"imagescroll,"
"margins,"
"sixel,"
"usstyle"
},
{ .name = "ghostty",
@@ -646,13 +700,28 @@ tty_default_features(struct client *c, const char *name, u_int version)
"cstyle,"
"extkeys,"
"focus,"
"imagescroll,"
"margins,"
"overline,"
"hyperlinks,"
"osc7,"
"sync,"
"usstyle,"
"progressbar"
"progressbar,"
"kitty"
},
{ .name = "kitty",
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
"ccolour,"
"cstyle,"
"extkeys,"
"focus,"
"hyperlinks,"
"imagescroll,"
"osc7,"
"sync,"
"usstyle,"
"kitty"
},
{ .name = "Rio",
.features = TTY_FEATURES_BASE_MODERN_XTERM ","
+1
View File
@@ -729,6 +729,7 @@ tty_keys_winsz(struct tty *tty, const char *buf, size_t len, size_t *size)
char_y = (ypixel && tty->sy) ? ypixel / tty->sy : 0;
tty_set_size(tty, tty->sx, tty->sy, char_x, char_y);
tty_invalidate(tty);
recalculate_sizes();
tty->flags &= ~TTY_WINSIZEQUERY;
*size = end + 1;
+1
View File
@@ -531,6 +531,7 @@ tty_term_apply_overrides(struct tty_term *term)
tty_term_validate(term);
}
/* Mark terminals whose Ms capability does not contain a clipboard argument. */
static void
tty_term_validate(struct tty_term *term)
{
+72 -124
View File
@@ -67,11 +67,6 @@ static void tty_emulate_repeat(struct tty *, enum tty_code_code,
enum tty_code_code, u_int);
static void tty_draw_pane(struct tty *, const struct tty_ctx *, u_int);
#ifdef ENABLE_SIXEL
static void tty_write_one(void (*)(struct tty *, const struct tty_ctx *),
struct client *, struct tty_ctx *);
#endif
#define tty_use_margin(tty) \
(tty->term->flags & TERM_DECSLRM)
#define tty_full_width(tty, ctx) \
@@ -162,10 +157,21 @@ tty_resize(struct tty *tty)
void
tty_set_size(struct tty *tty, u_int sx, u_int sy, u_int xpixel, u_int ypixel)
{
#ifdef ENABLE_IMAGES
int geometry_changed;
geometry_changed = (tty->xpixel != xpixel || tty->ypixel != ypixel);
#endif
tty->sx = sx;
tty->sy = sy;
tty->xpixel = xpixel;
tty->ypixel = ypixel;
#ifdef ENABLE_IMAGES
if (geometry_changed) {
image_tty_geometry_changed(tty);
server_redraw_client(tty->client);
}
#endif
}
static void
@@ -283,6 +289,10 @@ tty_open(struct tty *tty, char **cause)
}
tty->flags |= TTY_OPENED;
#ifdef ENABLE_IMAGES
image_tty_update(tty);
#endif
tty->flags &= ~(TTY_NOCURSOR|TTY_FREEZE|TTY_BLOCK|TTY_TIMER);
event_set(&tty->event_in, c->fd, EV_PERSIST|EV_READ,
@@ -527,6 +537,9 @@ tty_close(struct tty *tty)
tty_stop_tty(tty);
if (tty->flags & TTY_OPENED) {
#ifdef ENABLE_IMAGES
image_tty_free(tty, 1);
#endif
evbuffer_free(tty->in);
event_del(&tty->event_in);
evbuffer_free(tty->out);
@@ -543,15 +556,25 @@ void
tty_free(struct tty *tty)
{
tty_close(tty);
#ifdef ENABLE_IMAGES
image_tty_free(tty, 0);
#endif
}
void
tty_update_features(struct tty *tty)
{
struct client *c = tty->client;
int changed = 0;
if (tty_apply_features(tty->term))
if (tty_apply_features(tty->term)) {
tty_term_apply_overrides(tty->term);
changed = 1;
}
#ifdef ENABLE_IMAGES
if (image_tty_update(tty))
changed = 1;
#endif
if (tty_use_margin(tty))
tty_putcode(tty, TTYC_ENMG);
@@ -564,7 +587,22 @@ tty_update_features(struct tty *tty)
/*
* Features might have changed since the first draw during attach. For
* example, this happens when DA responses are received.
*
* Only redraw when something actually did change. This function is
* called for every DA, secondary DA and extended DA answer, and from
* the start timer when none arrive - answers which usually just
* confirm what is already known, either from a previous answer or
* from terminal-features in the configuration. The redraw is not
* free: it repaints the pane from tmux's own grid, which discards
* anything the pane put on the terminal that tmux does not model,
* notably an image written through DCS passthrough. That makes an
* unnecessary redraw here visible to the user as an image that
* appears and then vanishes a moment later, once per client, with no
* way for the application to detect it and redraw.
*/
if (!changed)
return;
server_redraw_client(c);
tty_invalidate(tty);
@@ -1455,59 +1493,6 @@ tty_check_codeset(struct tty *tty, const struct grid_cell *gc)
return (&new);
}
#ifdef ENABLE_SIXEL
/* Update context for client. */
static int
tty_set_client_cb(struct tty_ctx *ttyctx, struct client *c)
{
struct window_pane *wp = ttyctx->arg;
if (c->session->curw->window != wp->window)
return (0);
if (wp->layout_cell == NULL)
return (0);
if (tty_window_offset(&c->tty, &ttyctx->wox, &ttyctx->woy, &ttyctx->wsx,
&ttyctx->wsy))
ttyctx->flags |= TTY_CTX_WINDOW_BIGGER;
else
ttyctx->flags &= ~TTY_CTX_WINDOW_BIGGER;
ttyctx->yoff = ttyctx->ryoff = wp->yoff;
if (status_at_line(c) == 0)
ttyctx->yoff += status_line_size(c);
return (1);
}
void
tty_draw_images(struct client *c, struct window_pane *wp)
{
struct image *im;
struct tty_ctx ttyctx;
TAILQ_FOREACH(im, &wp->screen->images, entry) {
memset(&ttyctx, 0, sizeof ttyctx);
/* Set the client independent properties. */
ttyctx.ocx = im->px;
ttyctx.ocy = im->py;
ttyctx.orlower = wp->screen->rlower;
ttyctx.orupper = wp->screen->rupper;
ttyctx.xoff = ttyctx.rxoff = wp->xoff;
ttyctx.sx = wp->sx;
ttyctx.sy = wp->sy;
ttyctx.image = im;
ttyctx.arg = wp;
ttyctx.set_client_cb = tty_set_client_cb;
ttyctx.flags |= TTY_CTX_INVISIBLE_PANES;
tty_write_one(tty_cmd_sixelimage, c, &ttyctx);
}
}
#endif
void
tty_sync_start(struct tty *tty)
@@ -1583,19 +1568,6 @@ tty_write(void (*cmdfn)(struct tty *, const struct tty_ctx *),
}
}
#ifdef ENABLE_SIXEL
/* Only write to the incoming tty instead of every client. */
static void
tty_write_one(void (*cmdfn)(struct tty *, const struct tty_ctx *),
struct client *c, struct tty_ctx *ctx)
{
if (ctx->set_client_cb == NULL)
return;
if ((ctx->set_client_cb(ctx, c)) == 1)
cmdfn(&c->tty, ctx);
}
#endif
void
tty_cmd_insertcharacter(struct tty *tty, const struct tty_ctx *ctx)
{
@@ -1730,10 +1702,17 @@ tty_cmd_reverseindex(struct tty *tty, const struct tty_ctx *ctx)
!tty_term_has(tty->term, TTYC_RIN)) ||
ctx->sx == 1 ||
ctx->sy == 1) {
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx);
return;
}
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -1758,10 +1737,17 @@ tty_cmd_linefeed(struct tty *tty, const struct tty_ctx *ctx)
!tty_term_has(tty->term, TTYC_CSR) ||
ctx->sx == 1 ||
ctx->sy == 1) {
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx);
return;
}
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -1796,10 +1782,17 @@ tty_cmd_scrollup(struct tty *tty, const struct tty_ctx *ctx)
!tty_term_has(tty->term, TTYC_CSR) ||
ctx->sx == 1 ||
ctx->sy == 1) {
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx);
return;
}
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -1834,10 +1827,17 @@ tty_cmd_scrolldown(struct tty *tty, const struct tty_ctx *ctx)
!tty_term_has(tty->term, TTYC_RIN)) ||
ctx->sx == 1 ||
ctx->sy == 1) {
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 1);
#endif
tty_redraw_region(tty, ctx);
return;
}
#ifdef ENABLE_IMAGES
redraw_image_scroll_result(tty, ctx, 0);
#endif
tty_default_attributes(tty, ctx->bg, &ctx->style_ctx);
tty_region_pane(tty, ctx, ctx->orupper, ctx->orlower);
@@ -2029,58 +2029,6 @@ tty_cmd_rawstring(struct tty *tty, const struct tty_ctx *ctx)
tty_invalidate(tty);
}
#ifdef ENABLE_SIXEL
void
tty_cmd_sixelimage(struct tty *tty, const struct tty_ctx *ctx)
{
struct image *im = ctx->image;
struct sixel_image *si = im->data;
struct sixel_image *new;
char *data;
size_t size;
u_int cx = ctx->ocx, cy = ctx->ocy, sx, sy;
u_int i, j, x, y, rx, ry;
int fallback = 0;
if ((~tty->term->flags & TERM_SIXEL) &&
!tty_term_has(tty->term, TTYC_SXL))
fallback = 1;
if (tty->xpixel == 0 || tty->ypixel == 0)
fallback = 1;
sixel_size_in_cells(si, &sx, &sy);
log_debug("%s: image is %ux%u", __func__, sx, sy);
if (!tty_clamp_area(tty, ctx, cx, cy, sx, sy, &i, &j, &x, &y, &rx, &ry))
return;
log_debug("%s: clamping to %u,%u-%u,%u", __func__, i, j, rx, ry);
if (fallback == 1) {
data = xstrdup(im->fallback);
size = strlen(data);
} else {
new = sixel_scale(si, tty->xpixel, tty->ypixel, i, j, rx, ry, 0);
if (new == NULL)
return;
data = sixel_print(new, si, &size);
}
if (data != NULL) {
log_debug("%s: %zu bytes: %s", __func__, size, data);
tty_region_off(tty);
tty_margin_off(tty);
tty_cursor(tty, x, y);
tty->flags |= TTY_NOBLOCK;
tty_add(tty, data, size);
tty_invalidate(tty);
free(data);
}
if (fallback == 0)
sixel_free(new);
}
#endif
void
tty_cmd_syncstart(struct tty *tty, const struct tty_ctx *ctx)
{
+4
View File
@@ -374,6 +374,10 @@ utf8_add_to_width_cache(const char *s)
wc_end = wc_start;
}
/*
* wc_end may be WCHAR_MAX, so incrementing wc past it would
* overflow (wc_start <= wc_end is already guaranteed above).
*/
wc = wc_start;
for (;;) {
utf8_insert_width_cache(wc, width);
+132
View File
@@ -73,6 +73,10 @@ static void window_copy_write_line(struct window_mode_entry *,
struct screen_write_ctx *, u_int);
static void window_copy_write_lines(struct window_mode_entry *,
struct screen_write_ctx *, u_int, u_int);
#ifdef ENABLE_IMAGES
static void window_copy_update_image_refresh(struct window_copy_mode_data *);
static int window_copy_visible_has_images(struct window_copy_mode_data *);
#endif
static char *window_copy_match_at_cursor(struct window_copy_mode_data *);
static void window_copy_scroll_to(struct window_mode_entry *, u_int, u_int,
int);
@@ -279,6 +283,10 @@ struct window_copy_mode_data {
u_int oy; /* number of lines scrolled up */
u_int image_base; /* hsize - oy images were last drawn for */
int image_base_set;
int image_refresh; /* current redraw needs image refresh */
u_int selx; /* beginning of selection */
u_int sely;
@@ -636,6 +644,9 @@ window_copy_init(struct window_mode_entry *wme,
data->my = screen_hsize(data->backing) + data->cy - data->oy;
data->showmark = 0;
#ifdef ENABLE_IMAGES
window_copy_update_image_refresh(data);
#endif
screen_write_start(&ctx, &data->screen);
for (i = 0; i < screen_size_y(&data->screen); i++)
window_copy_write_line(wme, &ctx, i);
@@ -1198,6 +1209,9 @@ window_copy_size_changed(struct window_mode_entry *wme)
window_copy_clear_selection(wme);
window_copy_clear_marks(wme);
#ifdef ENABLE_IMAGES
window_copy_update_image_refresh(data);
#endif
screen_write_start(&ctx, s);
window_copy_write_lines(wme, &ctx, 0, screen_size_y(s));
screen_write_stop(&ctx);
@@ -5224,6 +5238,34 @@ window_copy_write_one(struct window_mode_entry *wme,
for (fx = 0; fx < nx; fx++) {
grid_get_cell(gd, fx, fy, &gc);
if (fx + gc.data.width <= nx) {
#ifdef ENABLE_IMAGES
/*
* Write image-covered cells directly into the grid,
* skipping both window_copy_update_style() (so a
* selection, current-line or search-mark highlight
* never sweeps visibly over the image before it is
* recomposited separately - see the
* image_redraw_area() call below) and
* screen_write_cell(), whose built-in
* screen_write_image_damage() call (screen-write.c)
* fires on every write regardless of whether
* anything actually changed, which would needlessly
* re-damage - and so retransmit - the image on every
* redraw. The cell must still land in the grid as
* normal: a non-graphical client's ASCII fallback for
* the image is an ordinary character here, not
* something recomposited separately, and depends on
* this write the same as any other cell.
*/
if (image_grid_check_area(gd, fx, fy, gc.data.width,
1)) {
grid_view_set_cell(ctx->s->grid, px + fx, py,
&gc);
screen_write_cursormove(ctx,
px + fx + gc.data.width, py, 0);
continue;
}
#endif
window_copy_update_style(wme, fx, fy, &gc, mgc, cgc,
mkgc, clgc);
if (gc.flags & GRID_FLAG_PADDING) {
@@ -5472,6 +5514,29 @@ window_copy_write_line(struct window_mode_entry *wme,
window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py,
content_sx, &mgc, &cgc, &mkgc, &clgc);
#ifdef ENABLE_IMAGES
/*
* Copy the backing line's image layers separately from its text
* cells. This is not implied by the text cells just written above:
* those go via the pane's normal (frequently fast, direct-write)
* path, which knows nothing about image content.
*
* Only report it as needing a redraw when the view has actually
* moved (data->image_refresh, set once per window_copy_redraw_lines
* call) - text writes never touch image-covered cells (see
* window_copy_write_one), so if the view is unmoved this row's
* images are already exactly as they should be and redrawing them
* anyway just flashes the image on every unrelated redraw (e.g.
* every step of a selection drag) for no visible benefit.
*/
image_grid_free_line(s->grid,
&s->grid->linedata[s->grid->hsize + py]);
image_grid_copy_area(s->grid, width, s->grid->hsize + py,
data->backing->grid, 0, hsize - data->oy + py, content_sx, 1);
if (data->image_refresh)
image_redraw_area(ctx, width, py, content_sx, 1);
#endif
if (py == 0 && s->rupper < s->rlower && !data->hide_position) {
value = options_get_string(oo, "copy-mode-position-format");
if (*value != '\0') {
@@ -5532,6 +5597,55 @@ window_copy_redraw_selection(struct window_mode_entry *wme, u_int old_y)
window_copy_redraw_lines(wme, start, end - start + 1);
}
#ifdef ENABLE_IMAGES
/*
* Only rows whose underlying history position has moved since the last
* call need their images recomposited - most redraws are just a selection
* or cursor-line style change with the view otherwise unmoved, and that
* never touches image-covered cells (see window_copy_write_one()), so
* redrawing images for it is needless: on a fast drag it is visible as the
* image briefly flashing on every step even though nothing about it
* actually changed.
*
* Every place that calls window_copy_write_line()/window_copy_write_lines()
* must call this first - it is not implied by them, since some (the
* initial full-screen draw on entering copy mode, window_copy_scroll_up(),
* window_copy_scroll_down()) write directly rather than going through
* window_copy_redraw_lines().
*/
static void
window_copy_update_image_refresh(struct window_copy_mode_data *data)
{
u_int base;
base = screen_hsize(data->backing) - data->oy;
data->image_refresh = !data->image_base_set || base != data->image_base;
data->image_base = base;
data->image_base_set = 1;
}
/*
* Whether any part of the currently visible backing range carries image
* data. A terminal's line insert/delete only shifts character cells - the
* pixels of a sixel or Kitty image already on screen stay exactly where
* they were sent, so a scroll that uses that fast path (see
* window_copy_scroll_up()/window_copy_scroll_down()) leaves stale or
* missing image content for every row except the single new one it
* explicitly rewrites. When this returns true, callers should fall back to
* a full window_copy_redraw_screen() instead, so every visible row's image
* is recomposited at its correct new position.
*/
static int
window_copy_visible_has_images(struct window_copy_mode_data *data)
{
struct grid *gd = data->backing->grid;
u_int sy = screen_size_y(&data->screen);
return (image_grid_check_area(gd, 0, screen_hsize(data->backing) -
data->oy, screen_size_x(&data->screen), sy));
}
#endif
static void
window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny)
{
@@ -5541,6 +5655,10 @@ window_copy_redraw_lines(struct window_mode_entry *wme, u_int py, u_int ny)
struct screen_write_ctx ctx;
u_int i;
#ifdef ENABLE_IMAGES
window_copy_update_image_refresh(data);
#endif
if (window_copy_line_number_width(wme) != 0) {
screen_write_start(&ctx, &data->screen);
for (i = py; i < py + ny; i++)
@@ -6929,6 +7047,13 @@ window_copy_scroll_up(struct window_mode_entry *wme, u_int ny)
window_copy_redraw_screen(wme);
return;
}
#ifdef ENABLE_IMAGES
if (window_copy_visible_has_images(data)) {
window_copy_redraw_screen(wme);
return;
}
window_copy_update_image_refresh(data);
#endif
if (window_copy_line_numbers_active(wme)) {
if (window_copy_line_number_mode(wme) !=
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {
@@ -7000,6 +7125,13 @@ window_copy_scroll_down(struct window_mode_entry *wme, u_int ny)
window_copy_redraw_screen(wme);
return;
}
#ifdef ENABLE_IMAGES
if (window_copy_visible_has_images(data)) {
window_copy_redraw_screen(wme);
return;
}
window_copy_update_image_refresh(data);
#endif
if (window_copy_line_numbers_active(wme)) {
if (window_copy_line_number_mode(wme) !=
WINDOW_COPY_LINE_NUMBERS_ABSOLUTE) {
+92 -19
View File
@@ -1,4 +1,4 @@
/* $OpenBSD: window.c,v 1.378 2026/09/24 11:19:39 nicm Exp $ */
/* $OpenBSD: window.c,v 1.377 2026/09/21 10:33:16 nicm Exp $ */
/*
* Copyright (c) 2007 Nicholas Marriott <nicholas.marriott@gmail.com>
@@ -1544,7 +1544,7 @@ window_pane_destroy(struct window_pane *wp)
window_pane_clear_prompt(wp);
window_pane_free_modes(wp);
screen_write_sync_clear_dirty(wp);
screen_write_clear_dirty(wp);
if (wp->fd != -1) {
#ifdef HAVE_UTEMPTER
@@ -1692,6 +1692,10 @@ window_pane_resize(struct window_pane *wp, u_int sx, u_int sy)
log_debug("%s: %%%u resize %ux%u", __func__, wp->id, sx, sy);
screen_resize(&wp->base, sx, sy, wp->base.saved_grid == NULL);
#ifdef ENABLE_IMAGES
if (sx > r->osx)
image_grid_resize_width(wp->base.grid, sx);
#endif
wme = TAILQ_FIRST(&wp->modes);
if (wme != NULL && wme->mode->resize != NULL)
@@ -2942,21 +2946,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 +2979,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);
}