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 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 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
67 changed files with 10583 additions and 704 deletions
+5 -4
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.
@@ -246,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.
+3 -1
View File
@@ -91,8 +91,10 @@ cmd_attach_session(struct cmdq_item *item, const char *tflag, int dflag,
wp = target.wp;
if (wl != NULL) {
if (wp != NULL)
if (wp != NULL) {
window_redraw_active_switch(wp->window, wp);
window_set_active_pane(wp->window, wp, 1);
}
session_set_current(s, wl);
if (wp != NULL)
cmd_find_from_winlink_pane(current, wl, wp, 0);
+9 -1
View File
@@ -308,6 +308,7 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
struct window_pane *wp;
struct layout_cell *lc;
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,10 +330,17 @@ cmd_join_pane_mouse_move(struct client *c, struct mouse_event *m)
ly = m->statusat - 1;
if (x != lx || y != ly) {
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);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w);
}
}
+7 -1
View File
@@ -239,6 +239,7 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
int y, ly, x, lx, sx, sy, new_sx, new_sy;
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) {
@@ -249,6 +250,10 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
lc = wp->layout_cell;
sx = wp->sx;
sy = wp->sy;
old_xoff = wp->xoff;
old_yoff = wp->yoff;
old_sx = (int)wp->sx;
old_sy = (int)wp->sy;
left = wp->xoff - 1;
right = wp->xoff + sx;
if (window_pane_scrollbar_reserve(wp) &&
@@ -348,7 +353,8 @@ cmd_resize_pane_mouse_resize_move_floating(struct client *c,
}
if (resizes != 0) {
layout_fix_panes(w, NULL);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w);
}
}
+9 -1
View File
@@ -362,6 +362,7 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
enum pane_lines lines;
u_int sx, sy;
int x, y, xoff, yoff, border;
int old_xoff, old_yoff, old_sx, old_sy;
if (c->tty.mouse_last_pane == -1)
return;
@@ -417,8 +418,15 @@ cmd_split_window_mouse_resize(struct client *c, struct mouse_event *m)
if (sy < PANE_MINIMUM)
sy = PANE_MINIMUM;
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);
server_redraw_window(w);
window_pane_redraw_floating(w, wp, old_xoff, old_yoff, old_sx,
old_sy);
server_redraw_window_borders(w);
}
+27 -7
View File
@@ -530,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)
@@ -1137,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
},
+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);
+4 -2
View File
@@ -469,7 +469,6 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
sx = PANE_MINIMUM;
else
sx = sx - sb_w - sb_pad;
wp->flags |= PANE_REDRAWSCROLLBAR;
}
window_pane_resize(wp, sx, sy);
@@ -477,8 +476,11 @@ layout_fix_panes(struct window *w, struct window_pane *skip)
if (wp->xoff != old_xoff ||
wp->yoff != old_yoff ||
wp->sx != old_sx ||
wp->sy != old_sy)
wp->sy != old_sy) {
changed = 1;
if (window_pane_scrollbar_reserve(wp))
wp->flags |= PANE_REDRAWSCROLLBAR;
}
}
if (changed)
redraw_invalidate_scene(w);
+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 = ",",
+79
View File
@@ -0,0 +1,79 @@
#!/bin/sh
# Selecting a pane with attach-session from an already attached client must
# redraw pane contents when window-style and window-active-style differ.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lattach-redraw-outer-$$ -f/dev/null"
BEFORE=$DIR/before
AFTER=$DIR/after
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
LEFT=$($INNER new-session -dPF '#{pane_id}' -s inner -x 40 -y 8 \
"printf 'LEFT'; exec sleep 100") || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g default-terminal screen || exit 1
$INNER split-window -h -t "$LEFT" "printf 'RIGHT'; exec sleep 100" || exit 1
$INNER set-option -w -t "$LEFT" window-style bg=red || exit 1
$INNER set-option -w -t "$LEFT" window-active-style bg=blue || exit 1
$INNER bind-key -n x attach-session -t "$LEFT" || exit 1
$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lattach-redraw-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
sleep 1
$OUTER send-keys -t outer:0.0 x || exit 1
sleep 1
[ "$($INNER display-message -p -t inner '#{pane_id}')" = "$LEFT" ] ||
fail "attach-session did not select the target pane"
$OUTER capture-pane -pe -t outer:0.0 >"$BEFORE" || exit 1
# A forced redraw produces the correct scene. It must be identical to the
# scene drawn immediately by attach-session.
$INNER refresh-client -t "$CLIENT" || exit 1
sleep 1
$OUTER capture-pane -pe -t outer:0.0 >"$AFTER" || exit 1
cmp -s "$BEFORE" "$AFTER" ||
fail "attach-session left stale active/inactive pane styles"
exit 0
+10 -1
View File
@@ -5,6 +5,15 @@
PATH=/bin:/usr/bin
TERM=screen
# The pane's shell must not be the user's own interactive shell: a custom
# PS1/PROMPT_COMMAND that sets the terminal title (as many do, for tmux/xterm
# TERM types) would redraw over the titles this test sets and checks on
# every prompt, regardless of how long it waits first.
shell=
if command -v bash >/dev/null 2>&1; then
shell='bash --noprofile --norc +o history'
fi
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
$TMUX kill-server 2>/dev/null
@@ -30,7 +39,7 @@ must_equal()
[ "$got" = "$want" ] || fail "got '$got', expected '$want'"
}
$TMUX new-session -d -x 80 -y 24 || exit 1
$TMUX new-session -d -x 80 -y 24 -- $shell || exit 1
$TMUX set-option -qg allow-set-title on || exit 1
$TMUX set-option -qg allow-rename on || exit 1
$TMUX set-option -qg automatic-rename off || exit 1
+108
View File
@@ -0,0 +1,108 @@
#!/bin/sh
# Regression test for a floating-pane drag bug: cmd_resize_pane_redraw_floating()
# (cmd-resize-pane.c) reported damage for just a dragged floating pane's
# content rectangle, not the one-cell border frame drawn around it (see the
# "floating" case in screen-redraw.c, which draws that frame at
# xoff-1/yoff-1 through xoff+sx/yoff+sy - one cell outside the pane's own
# content area). Damage scoped to only the content area left the frame's
# previous position undrawn as the pane moved, so dragging it left a trail
# of un-erased border frames behind - visible as several "corners" stacked
# up rather than just the pane's current one.
#
# This bug has nothing to do with images - it reproduces with a plain
# floating pane and no image support required.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
#
# Write a plain (unmodified) SGR button-1 press, drag update and release at
# 1-based positions to the outer pane holding the inner client - this
# matches the default MouseDown1Border/MouseDrag1Border bindings used to
# move or resize a floating pane by its border.
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 1
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on
$TMUX set -g default-command 'sh -c "sleep 100"'
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
$TMUX2 new-session -d -x 60 -y 20 "$TMUX attach -t inner" || exit 1
sleep 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
# Sanity check: exactly one floating pane, so exactly one top-left corner,
# before dragging anything.
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "sanity: expected 1 corner before drag, found $n"
# Drag the floating pane by its top border (row FTOP-1, some column within
# its width) down several rows in a few separate steps, then release. A
# single drag() call already does press/motion/release, so call it several
# times in a row to simulate a multi-step real drag.
GRABCOL=$((FLEFT + FWIDTH / 2))
STARTROW=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((STARTROW + i + 1))
drag $((GRABCOL + 1)) $((STARTROW + i)) $((GRABCOL + 1)) $newrow
i=$((i + 1))
done
$TMUX2 capturep -p -t "$OUTER" >$TMP || fail "capture failed"
# Exactly one top-left corner should remain - the pane's current position.
# This is expected to fail before the fix: multiple corners (a trail of
# un-erased frames) would remain from the intermediate drag positions.
n=$(grep -o '┌' $TMP | wc -l)
[ "$n" -eq 1 ] || fail "expected exactly 1 corner after drag, found $n (ghost frames left behind)"
exit 0
+100
View File
@@ -0,0 +1,100 @@
#!/bin/sh
# 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
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Ldoublecomp-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldoublecomp-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 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
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
'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
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldoublecomp-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# 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
i=0
steps=6
while [ $i -lt $steps ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$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
+140
View File
@@ -0,0 +1,140 @@
#!/bin/sh
# Regression test: dragging a floating pane across another pane's ordinary
# content must not redraw that other pane's scrollbar, unless the drag
# actually crosses the scrollbar's own strip.
#
# cmd_resize_pane_redraw_floating() (cmd-resize-pane.c) used to flag
# PANE_REDRAWSCROLLBAR on any pane whose whole *body* intersected the
# floating pane's old or new rectangle, rather than just its narrow
# scrollbar strip - so dragging a floating pane back and forth over an
# ordinary tiled pane's content (never touching its scrollbar) still
# needlessly redrew that pane's scrollbar on every motion step. See
# tmux-image-redraw-known-bugs.md for the full write-up.
#
# This is checked by giving the non-dragged pane a distinctive scrollbar
# colour and counting how many times its SGR code appears in the client's
# raw output while the floating pane is dragged vertically over that pane's
# body, well clear of its scrollbar column: with the fix, it should never
# reappear after the initial draw.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
TMUX="$TEST_TMUX -LtestA$$ -f/dev/null"
TMUX2="$TEST_TMUX -LtestB$$ -f/dev/null"
cleanup()
{
$TMUX kill-server >/dev/null 2>&1
$TMUX2 kill-server >/dev/null 2>&1
}
fail()
{
echo "$*" >&2
cleanup
exit 1
}
# drag STARTCOL STARTROW ENDCOL ENDROW
drag()
{
scol="$1"
srow="$2"
ecol="$3"
erow="$4"
seq=$(printf '\033[<0;%s;%sM' "$scol" "$srow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<32;%s;%sM' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.2
seq=$(printf '\033[<0;%s;%sm' "$ecol" "$erow")
$TMUX2 send-keys -t "$OUTER" -l "$seq" 2>/dev/null
sleep 0.5
}
cleanup
TMP=$(mktemp)
trap "cleanup; rm -f $TMP" 0 1 15
$TMUX new-session -d -s inner -x 60 -y 20 'sh -c "sleep 100"' || exit 1
$TMUX set -g mouse on || fail "set mouse failed"
$TMUX set -g default-command 'sh -c "sleep 100"' || fail "set default-command failed"
$TMUX set -g pane-scrollbars on || fail "set pane-scrollbars failed"
$TMUX split-window -h -t inner 'sh -c "sleep 100"' || fail "split-window failed"
PANES=$($TMUX list-panes -t inner -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
[ -n "$LEFT" ] || fail "could not identify left pane"
# A distinctive scrollbar colour for the non-dragged (left) pane only.
$TMUX set -p -t "$LEFT" pane-scrollbars-style 'fg=colour201,bg=colour17' ||
fail "set pane-scrollbars-style failed"
ALEFT=$($TMUX display-message -p -t "$LEFT" '#{pane_left}')
ATOP=$($TMUX display-message -p -t "$LEFT" '#{pane_top}')
AWIDTH=$($TMUX display-message -p -t "$LEFT" '#{pane_width}')
AHEIGHT=$($TMUX display-message -p -t "$LEFT" '#{pane_height}')
[ "$AWIDTH" -gt 15 ] || fail "left pane too narrow for this test ($AWIDTH)"
# A small floating pane placed well inside the left pane's content area,
# clear of its (right-hand) scrollbar column by several columns.
FLOAT=$($TMUX new-pane -d -PF '#{pane_id}' -x 8 -y 5 \
-X $((ALEFT + 2)) -Y $((ATOP + 2))) || fail "new-pane -X -Y failed"
FTOP=$($TMUX display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($TMUX display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($TMUX display-message -p -t "$FLOAT" '#{pane_width}')
[ $((FLEFT + FWIDTH + 3)) -lt $((ALEFT + AWIDTH)) ] ||
fail "sanity: floating pane too close to the scrollbar column"
# Start the outer session with a plain shell, then start capturing before
# triggering the attach - starting the attach as the outer pane's initial
# command would mean pipe-pane only starts after the attach-driven initial
# redraw (which draws the scrollbars) has already happened, missing it.
$TMUX2 new-session -d -x 60 -y 20 || exit 1
OUTER=$($TMUX2 list-panes -F '#{pane_id}' | head -1)
[ -n "$OUTER" ] || fail "No outer pane."
$TMUX2 pipe-pane -t "$OUTER" -O "cat >$TMP" || fail "pipe-pane failed"
$TMUX2 send-keys -t "$OUTER" -l "$TMUX attach -t inner" || fail "send attach failed"
$TMUX2 send-keys -t "$OUTER" Enter || fail "send enter failed"
sleep 1
# Sanity check: the distinctive scrollbar colour reaches the client at all.
grep -qa '48;5;201' $TMP || fail "sanity: scrollbar colour never reached the client"
: >$TMP
# Drag the floating pane straight up and down by its top border, staying at
# a fixed column the whole time - this never crosses the left pane's
# scrollbar strip, only its ordinary content.
GRABCOL=$((FLEFT + FWIDTH / 2))
row=$FTOP
i=0
while [ $i -lt 6 ]; do
newrow=$((row + 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
i=0
while [ $i -lt 6 ]; do
newrow=$((row - 1))
drag $GRABCOL $row $GRABCOL $newrow
row=$newrow
i=$((i + 1))
done
# The scrollbar colour should never reappear - its geometry never changed,
# and the drag never crossed its column. This is expected to fail before
# the fix - see the header comment.
n=$(grep -ac '48;5;201' $TMP)
[ "$n" -eq 0 ] ||
fail "left pane's scrollbar was redrawn $n times while dragging over its body only"
exit 0
+95
View File
@@ -0,0 +1,95 @@
#!/bin/sh
# 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
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Lsyncdefer-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsyncdefer-outer-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.15
}
$INNER new-session -d -s inner -x 40 -y 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
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X 5 -Y 2 \
'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
$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
sleep 0.5
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
GRABCOL=$((XOFF + 3))
BORDERROW=$YOFF
# 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
i=0
while [ $i -lt 6 ]; do
GRABCOL=$((GRABCOL + 1))
mouse 32 "$GRABCOL" "$BORDERROW" M
i=$((i + 1))
done
mouse 0 "$GRABCOL" "$BORDERROW" m
sleep 0.3
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$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
+206
View File
@@ -0,0 +1,206 @@
#!/bin/sh
# Mirror of floating-pane-drag-wide-character.sh for the *right* edge of a
# damage rectangle. redraw_damage_grow_span_clip() (screen-redraw.c) widens
# a damage rectangle's right edge by one cell whenever it isn't already at
# the span's own edge, to pull in a wide character's base half when the
# edge lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell (a character fully
# outside the range), growing right pulls in just that base cell -
# tty_draw_line() sees it can't fit that character's full width in the
# remaining range (tty_draw_line_get_empty()'s gc->data.width > nx check)
# and clears it, exactly as the left-edge bug cleared a neighbouring
# character's padding half.
#
# As with the left-edge test, the exact column parity needs to be
# deterministic to actually catch it. This constructs it by creating the
# floating pane, checking its real position, and recreating it one column
# over if necessary until the vacated rectangle's right edge lands on a
# base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidecharr-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
PANEWIDTH=12
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated right edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidecharr-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's right edge
# (xoff + PANEWIDTH + 1, the border-grown exclusive end - see
# window_pane_damage_floating(), window.c) lands on an even (base-cell)
# column.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x "$PANEWIDTH" -y 3 \
-X "$startx" -Y 5 \
'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldright=$((XOFF + PANEWIDTH + 1))
if [ $((oldright % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF + PANEWIDTH + 1) % 2)) -eq 0 ] ||
fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF + PANEWIDTH - 2))
COL2=$((XOFF + PANEWIDTH + 2))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle - far enough right
# that its new position starts past the checked columns above (which sit
# just past the pane's *original* right edge).
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 20))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0
+200
View File
@@ -0,0 +1,200 @@
#!/bin/sh
# Damage at a floating pane's vacated edge must always redraw a complete
# grid character. redraw_damage_grow_span_clip() (screen-redraw.c) widens a
# damage rectangle's left edge by one cell whenever it isn't already at the
# span's own edge, to pull in a wide character's base half when the edge
# lands on its padding half - but it did this unconditionally, with no
# check of which half it was actually touching. When the edge instead
# already lands cleanly on a fresh character's base cell, growing left
# walks into the *previous*, unrelated character's padding cell and blanks
# it (tty_draw_line() treats any leading padding cell as proof its own
# range starts mid-character).
#
# This is a general damage-composition bug, not specific to any one kind of
# pane, but the exact column parity needs to be deterministic to actually
# catch it (a lucky parity draws fine). This constructs it by creating the
# floating pane, checking its real position (the border-framing offset
# added to -X is not something to hand-compute), and recreating it one
# column over if necessary until the vacated rectangle's left edge lands on
# a base cell.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lwidechar-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
BASE=$DIR/base
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
slice_columns()
{
# Extract terminal columns [COL1, COL2) from lines [ROW1, ROW2] of
# $1, decoding UTF-8 - the pane's own new position (well clear of
# this range) must not affect the comparison, so this only looks at
# the narrow strip actually vacated, not the whole line. capture-pane
# text has one decoded character per double-width cell pair (every
# character here is width 2), so terminal columns are converted to
# character indices by halving before slicing.
perl -CSD -e '
my ($row1, $row2, $col1, $col2, $file) = @ARGV;
open my $fh, "<:encoding(UTF-8)", $file or die $!;
my @lines = <$fh>;
my $c1 = int($col1 / 2);
my $c2 = int(($col2 + 1) / 2);
for my $n ($row1 .. $row2) {
my $line = $lines[$n - 1];
$line =~ s/\R\z//;
print substr($line, $c1, $c2 - $c1), "\n";
}
' "$ROW1" "$ROW2" "$COL1" "$COL2" "$1"
}
wait_old_rows_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
slice_columns "$BASE" >"$DIR/want"
slice_columns "$CAPTURE" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" && return 0
sleep 0.1
i=$((i + 1))
done
fail "wide characters under the floating pane's vacated edge were not restored"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
binmode STDOUT, ':encoding(UTF-8)';
$| = 1;
for my $row (1 .. 10) {
print "\e[$row;1H", chr(0x754c) x 20;
}
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars off || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lwidechar-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has '界界界'
$OUTER capture-pane -p -t outer:0.0 >"$BASE" || exit 1
# Create the floating pane, then check its actual resulting position. Try
# adjacent starting columns until the vacated rectangle's left edge
# (xoff - 1) lands on an even (base-cell) column - the odd case is the one
# every earlier manual test happened to land on by chance.
startx=5
tries=0
while [ "$tries" -lt 2 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 12 -y 3 -X "$startx" \
-Y 5 'sh -c "printf FLOATMARK; exec sleep 100"') || exit 1
sleep 0.2
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
oldleft=$((XOFF - 1))
if [ $((oldleft % 2)) -eq 0 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
[ $(((XOFF - 1) % 2)) -eq 0 ] || fail "could not find bad-parity starting column"
wait_outer_has FLOATMARK
ROW1=$((YOFF + 1))
ROW2=$((YOFF + 3))
COL1=$((XOFF - 4))
COL2=$((XOFF + 4))
# Grab the pane's top border a couple of columns in (avoiding the corner
# cells) and drag it well clear of its old rectangle.
GRABCOL=$((XOFF + 3))
BORDERROW=$((YOFF))
seq=$(printf '\033[<0;%s;%sM' "$GRABCOL" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<32;%s;%sM' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
seq=$(printf '\033[<0;%s;%sm' "$((GRABCOL + 15))" "$BORDERROW")
$OUTER send-keys -t outer:0.0 -l "$seq" || exit 1
sleep 0.1
NEWXOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
[ "$NEWXOFF" != "$XOFF" ] || fail "sanity: floating pane did not move (still at $XOFF)"
wait_old_rows_restored
exit 0
+111
View File
@@ -0,0 +1,111 @@
#!/bin/sh
# A floating pane positioned partly off the window's left/top edge (e.g.
# created with -X -5) has a negative wp->xoff/wp->yoff. screen_write_
# redraw_cb() (screen-write.c) used to pass these straight through as u_int
# to redraw_damage_window(), which wraps a negative offset to a huge value
# - redraw_damage_window()'s own bounds check then rejects the whole
# rectangle, so nothing gets redrawn, not even the pane's visible portion.
#
# This fires on returning from the alternate screen (screen_write_
# alternateoff()) among other paths. This test exercises exactly that:
# fills the pane's primary screen, switches it to the alternate screen and
# back, and checks the client actually receives the restored primary
# content in the pane's visible (on-screen) columns - using an attached
# client's own received bytes (via a nested outer client), not
# capture-pane, which reads the grid directly and would pass regardless of
# whether the client was ever actually told to redraw it.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Loffscreen-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_visible_restored()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
sed -n "${CONTENTROW}p" "$CAPTURE" | grep -q '^AAAAA' && return 0
sleep 0.1
i=$((i + 1))
done
fail "primary-screen content was not restored in the pane's visible columns after returning from the alternate screen"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
print "\e[1;1H", 'A' x 15;
sleep 2;
print "\e[?1049h";
print "\e[1;1H", 'B' x 15;
sleep 2;
print "\e[?1049l";
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
# Content pane spans window columns -5..9 (partly off the left edge); only
# columns 0..9 are ever visible.
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 15 -y 5 -X -5 -Y 2 \
"perl '$EMITTER'") || exit 1
XOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
[ "$XOFF" -lt 0 ] || fail "sanity: floating pane is not off-screen (xoff=$XOFF)"
CONTENTROW=$((YOFF + 1))
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Loffscreen-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_outer_has AAAAA
wait_outer_has BBBBB
wait_visible_restored
exit 0
+179
View File
@@ -0,0 +1,179 @@
#!/bin/sh
# A damage rectangle's clip range is grown to avoid splitting a wide
# character, but redraw_damage_grow_span_clip() (screen-redraw.c) only ever
# checks the span's own pane *content* grid (wp->screen) for that. For a
# REDRAW_SPAN_PANE span, that same range is then also handed to
# redraw_damage_draw_pane_prompt() to recompose the pane's separately
# rendered prompt (wp->prompt, e.g. from "command-prompt -P") over the
# damaged sub-range - but the prompt is drawn into its own, freshly
# allocated one-line screen, unrelated to the pane's content grid, so a
# range grown (or left ungrown) against the content is not necessarily
# grown correctly for the prompt's own wide characters.
#
# This is invisible when the pane's own content is plain ASCII (as here):
# redraw_damage_grow_span_clip() never finds anything to grow against, so
# the raw, ungrown geometric range is passed straight through to the
# prompt - and if that range's edge lands mid-character in the *prompt's*
# grid, tty_draw_line() clears the character it cuts through
# (tty_draw_line_get_empty()'s gc->data.width > nx check, for a trailing
# base cell with no room left for its padding half).
#
# The trigger is a palette change (OSC 4) in a tiled pane that is one half
# of a vertical split running the full height of the window - occluded
# under the floating pane, but still geometrically triggering a redraw of
# its own rectangle. Positioned so the split boundary falls inside the
# floating pane's own CJK prompt, this reproduces exactly Codex's report:
# "a floating pane containing a CJK prompt across a tiled-pane boundary -
# a palette update in the tiled pane blanks a prompt character."
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lpromptwide-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
FLOAT=
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_prompt_row_intact()
{
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
line=$(sed -n "${PROMPTROW}p" "$CAPTURE")
case $line in
*"$PROMPTTEXT"*) return 0 ;;
esac
sleep 0.1
i=$((i + 1))
done
fail "the CJK prompt was not intact after the palette-triggered damage - got: $line"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $line = <STDIN>;
print "\e]4;1;rgb:11/22/33\e\\";
sleep 100;
PERL
$INNER new-session -d -s inner -x 60 -y 12 "perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
LEFT=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1
# Split so the boundary between the two tiled panes falls at column 17 -
# used below to pick a floating-pane column that lands the boundary
# mid-character inside the prompt.
RIGHT=$($INNER split-window -t inner -h -l 43 -PF '#{pane_id}' \
'sleep 100') || exit 1
RX=$($INNER display-message -p -t "$RIGHT" '#{pane_left}') || exit 1
# Create the floating pane, then check its actual resulting position (the
# border-framing offset added to -X is not something to hand-compute). Try
# adjacent starting columns until the split boundary lands on an odd
# (padding-half) column of the prompt's own numbering, and within the
# prompt's 12-column width.
startx=10
tries=0
while [ "$tries" -lt 4 ]; do
[ -n "$FLOAT" ] && $INNER kill-pane -t "$FLOAT" 2>/dev/null
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 24 -y 5 -X "$startx" \
-Y 2 "sh -c 'i=0; while [ \$i -lt 10 ]; do \
printf AAAAAAAAAAAAAAAAAAAAAA\\\\n; i=\$((i+1)); done; sleep 100'") ||
exit 1
sleep 0.2
X1=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
Y1=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
H1=$($INNER display-message -p -t "$FLOAT" '#{pane_height}')
local=$((RX - X1 - 1))
if [ "$local" -ge 1 ] && [ "$local" -le 11 ] &&
[ $((local % 2)) -eq 1 ]; then
break
fi
startx=$((startx + 1))
tries=$((tries + 1))
done
local=$((RX - X1 - 1))
[ "$local" -ge 1 ] && [ "$local" -le 11 ] && [ $((local % 2)) -eq 1 ] ||
fail "could not find bad-parity starting column"
$OUTER new-session -d -s outer -x 60 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lpromptwide-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
wait_outer_has AAAAAAAAAAAAAAAAAAAAAA
CLIENT=$($INNER list-clients -F '#{client_name}') || exit 1
$INNER select-pane -t "$FLOAT" || exit 1
PROMPTTEXT=$(printf '\344\270\255' | perl -CSD -ne 'print $_ x 6')
$INNER command-prompt -b -P -t "$CLIENT" -p "$PROMPTTEXT" \
'display-message -- %1' || exit 1
wait_outer_has "$PROMPTTEXT"
PROMPTROW=$((Y1 + H1))
# Trigger the damage: unblock the emitter so it fires the palette change in
# the left tiled pane, which is occluded under (but geometrically overlaps)
# the floating pane's prompt row.
$INNER send-keys -t "$LEFT" Enter || exit 1
wait_prompt_row_intact
exit 0
+117
View File
@@ -0,0 +1,117 @@
#!/bin/sh
# redraw_damage_refresh_status() (screen-redraw.c) force-regenerates a
# pane's border-status title when a damage rectangle touches it, guarded
# by the per-pane PANE_NEWSTATUS flag. window_make_pane_status() formats
# pane-border-format using the requesting client's own context (so e.g.
# #{client_name} differs per client), but wp->status_screen/PANE_NEWSTATUS
# are shared by every client viewing the pane. With two clients attached
# to the same session, whichever client's damage pass runs first renders
# its own text and sets the flag; the other client's damage pass, finding
# the flag already set, used to skip rendering entirely and reuse
# whatever was already there.
#
# This checks the actual server-side decision via the -vv log rather than
# a visual capture: an unrelated periodic client status-refresh reliably
# repaints each client's title correctly again within the same tick right
# after the buggy decision is made, before anything is ever flushed to
# either terminal, so the wrong content this bug produces is never
# visible to any external capture - the log is the only place the actual
# bug (or its absence) can be observed.
#
# A floating pane with its own pane-border-status is positioned so that a
# damage rectangle from an *unrelated* palette change (OSC 4) in the
# underlying tiled pane - whose own geometry spans the whole window -
# overlaps the floating pane's title row without touching its content,
# giving a damage-only trigger with no side effect that would otherwise
# force a normal (non-buggy) full per-client status re-render in the same
# pass and mask the result either way.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
cd "$DIR" || exit 1
INNER="$TEST_TMUX -vv -Lstatuscc-inner-$$ -f/dev/null"
OUTER1="$TEST_TMUX -Lstatuscc-outer1-$$ -f/dev/null"
OUTER2="$TEST_TMUX -Lstatuscc-outer2-$$ -f/dev/null"
fail()
{
echo "$*" >&2
exit 1
}
cleanup()
{
$OUTER1 kill-server 2>/dev/null
$OUTER2 kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
cd /
rm -rf "$DIR"
}
trap cleanup 0 1 15
BASEEMITTER=$DIR/base-emitter.pl
cat >"$BASEEMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $line = <STDIN>;
print "\e]4;1;rgb:11/22/33\e\\";
sleep 100;
PERL
$INNER new-session -d -s inner -x 40 -y 10 "perl '$BASEEMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g pane-border-status top || exit 1
$INNER set-option -g pane-border-format 'C=#{client_name}' || exit 1
BASE=$($INNER list-panes -t inner -F '#{pane_id}') || exit 1
FLOAT=$($INNER new-pane -d -PF '#{pane_id}' -x 20 -y 3 -X 5 -Y 4 \
'sleep 100') || exit 1
$OUTER1 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER1 set-option -g status off || exit 1
$OUTER1 set-option -g window-size manual || exit 1
$OUTER1 set-option -g default-terminal screen-256color || exit 1
$OUTER1 respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
NAME1=$($INNER list-clients -F '#{client_name}') || exit 1
$OUTER2 new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER2 set-option -g status off || exit 1
$OUTER2 set-option -g window-size manual || exit 1
$OUTER2 set-option -g default-terminal screen-256color || exit 1
$OUTER2 respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lstatuscc-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
sleep 0.5
ALLNAMES=$($INNER list-clients -F '#{client_name}') || exit 1
NAME2=$(echo "$ALLNAMES" | grep -v "^$NAME1\$")
[ -n "$NAME2" ] || fail "sanity: could not identify the second client"
# Let any attach-driven full redraw (and its own, non-buggy, per-client
# status render) finish completely before triggering the damage-only
# palette update.
sleep 1.5
$INNER send-keys -t "$BASE" Enter || exit 1
sleep 0.5
LOG=$(ls tmux-server*.log 2>/dev/null | head -1)
[ -n "$LOG" ] || fail "sanity: no server -vv log was produced"
n1=$(grep -c "regenerated pane .* status for $NAME1\$" "$LOG")
n2=$(grep -c "regenerated pane .* status for $NAME2\$" "$LOG")
[ "$n1" -ge 1 ] || fail "damage pass never regenerated $NAME1's own status - it reused whatever the other client's render left behind"
[ "$n2" -ge 1 ] || fail "damage pass never regenerated $NAME2's own status - it reused whatever the other client's render left behind"
exit 0
+102
View File
@@ -0,0 +1,102 @@
#!/bin/sh
# Resizing a floating pane must refresh session status formats which depend on
# its geometry, not only the pane scene and borders.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lfloating-status-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has_status()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
tail -1 "$CAPTURE" | grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client status did not show $marker"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
$INNER new-session -d -s inner -x 50 -y 12 'sleep 100' || exit 1
FLOAT=$($INNER new-pane -PF '#{pane_id}' -x 10 -y 5 -X 5 -Y 3 \
'sleep 100') || fail "could not create floating pane"
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g status on || exit 1
$INNER set-option -g status-position bottom || exit 1
$INNER set-option -g status-left 'WIDTH=#{pane_width}' || exit 1
$INNER set-option -g status-right '' || exit 1
$INNER set-option -g status-interval 0 || exit 1
$OUTER new-session -d -s outer -x 50 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lfloating-status-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
OLD_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
wait_outer_has_status "WIDTH=$OLD_WIDTH"
RIGHT=$($INNER display-message -p -t "$FLOAT" '#{pane_right}')
TOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
X=$((RIGHT + 2))
Y=$((TOP + 2))
# Grab the right frame and enlarge the floating pane.
mouse 0 "$X" "$Y" M
mouse 32 "$((X + 1))" "$Y" M
mouse 32 "$((X + 8))" "$Y" M
mouse 0 "$((X + 8))" "$Y" m
NEW_WIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
[ "$NEW_WIDTH" -ne "$OLD_WIDTH" ] || fail "floating pane was not resized"
wait_outer_has_status "WIDTH=$NEW_WIDTH"
exit 0
+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'
File diff suppressed because one or more lines are too long
+112
View File
@@ -0,0 +1,112 @@
#!/bin/sh
# Check a redraw callback which has no accompanying client redraw flags. A
# wrapped row crossing a panned viewport cannot use the direct tty path.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Ldamage-only-outer-$$ -f/dev/null"
EMITTER=$DIR/emitter.pl
TRIGGER=$DIR/trigger
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_inner_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 2>/dev/null |
grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane did not contain $marker"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
for my $row (1 .. 12) {
print "\e[$row;1H", 'o' x 79;
}
print "\e[1;1H";
while (!-e $ENV{TRIGGER}) {
select undef, undef, undef, 0.01;
}
my $second = ('B' x 24) . 'DAMAGE-ONLY' . ('B' x 45);
print "\e[5;1H", ('A' x 80), $second;
sleep 100;
PERL
$INNER new-session -d -s inner -x 80 -y 12 \
"TRIGGER='$TRIGGER' perl '$EMITTER'" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Ldamage-only-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_client
$INNER refresh-client -t "$CLIENT" -R 20 || exit 1
wait_outer_has oooooooooo
: >"$TRIGGER"
wait_inner_has DAMAGE-ONLY
wait_outer_has DAMAGE-ONLY
exit 0
+186
View File
@@ -0,0 +1,186 @@
#!/bin/sh
# Redraw a moved floating pane on both attached clients viewing the same
# window. Window redraw work must not be consumed by only one client.
#
# Uses ASCII pane borders (rather than the default UTF-8 box-drawing) because
# this test nests a real tmux client inside another tmux's pane to get a
# genuine terminal to capture from; that nested-tmux relay has been observed
# to mis-render a cell that previously held a multi-byte UTF-8 border
# character being overwritten later by plain content, on the outer instance's
# own interpretation, independent of anything the inner tmux sends. That is a
# nested-test-harness artifact, not a real tmux bug - confirmed by replaying
# the exact same drag sequence against a real terminal (xterm), where it
# never reproduces. ASCII borders avoid the artifact entirely.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lredraw-multi-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_clients()
{
i=0
while [ "$i" -lt 50 ]; do
count=$($INNER list-clients 2>/dev/null | wc -l)
[ "$count" -eq 2 ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "two inner clients did not attach"
}
wait_outer_has()
{
target=$1
marker=$2
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t "$target" >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer pane $target did not show $marker"
}
wait_float_left()
{
comparison=$1
limit=$2
i=0
while [ "$i" -lt 50 ]; do
left=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
if [ "$comparison" = gt ] && [ "$left" -gt "$limit" ]; then
return 0
fi
if [ "$comparison" = lt ] && [ "$left" -lt "$limit" ]; then
return 0
fi
sleep 0.1
i=$((i + 1))
done
fail "floating pane did not move"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$2" "$3" "$4" "$5")
$OUTER send-keys -t "$1" -l "$sequence" || exit 1
sleep 0.1
}
drag_float()
{
target=$1
startcol=$2
startrow=$3
endcol=$4
mouse "$target" 0 "$startcol" "$startrow" M
mouse "$target" 32 "$endcol" "$startrow" M
mouse "$target" 0 "$endcol" "$startrow" m
}
assert_scene()
{
target=$1
base=$2
firstcol=$3
lastcol=$4
$OUTER capture-pane -p -t "$target" >"$CAPTURE" || exit 1
# With ASCII (simple) borders every corner and junction is the same
# '+', so one rectangular floating pane always draws exactly 4 of
# them; more means a stale frame was left behind somewhere.
corners=$(grep -o '+' "$CAPTURE" | wc -l)
[ "$corners" -eq 4 ] ||
fail "outer pane $target had $corners floating frames"
sed -n '6,11p' "$base" | cut -c"$firstcol-$lastcol" >"$DIR/want"
sed -n '6,11p' "$CAPTURE" | cut -c"$firstcol-$lastcol" >"$DIR/got"
cmp -s "$DIR/want" "$DIR/got" ||
fail "outer pane $target did not restore the old floating area"
}
C="sh -c 'i=0; while [ \$i -lt 20 ]; do printf \"\\033[%d;1HBG-ROW-%02d-abcdefghijklmnopqrstuvwxyz0123456789\" \$((i + 1)) \$i; i=\$((i + 1)); done; exec sleep 100'"
$INNER new-session -d -s inner -x 60 -y 20 "$C" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g default-command 'sleep 100' || exit 1
$INNER set-option -g pane-border-lines simple || exit 1
$OUTER new-session -d -s outer -x 121 -y 20 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER split-window -h -t outer:0.0 'sleep 100' || exit 1
PANES=$($OUTER list-panes -t outer:0 -F '#{pane_id} #{pane_left}')
LEFT=$(echo "$PANES" | sort -k2 -n | head -1 | cut -d' ' -f1)
RIGHT=$(echo "$PANES" | sort -k2 -n | tail -1 | cut -d' ' -f1)
[ -n "$LEFT" ] && [ -n "$RIGHT" ] || fail "could not find outer panes"
$OUTER respawn-pane -k -t "$LEFT" \
"$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
$OUTER respawn-pane -k -t "$RIGHT" \
"$TEST_TMUX -Lredraw-multi-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_for_clients
wait_outer_has "$LEFT" BG-ROW-19
wait_outer_has "$RIGHT" BG-ROW-19
$OUTER capture-pane -p -t "$LEFT" >"$DIR/base-left" || exit 1
$OUTER capture-pane -p -t "$RIGHT" >"$DIR/base-right" || exit 1
FLOAT=$($INNER new-pane -dPF '#{pane_id}' -x 16 -y 5 -X 5 -Y 5) ||
fail "could not create floating pane"
wait_outer_has "$LEFT" '+'
wait_outer_has "$RIGHT" '+'
FTOP=$($INNER display-message -p -t "$FLOAT" '#{pane_top}')
FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
FWIDTH=$($INNER display-message -p -t "$FLOAT" '#{pane_width}')
GRABCOL=$((FLEFT + FWIDTH / 2 + 1))
# Move right through one client and require both clients to restore the old
# left-hand footprint.
drag_float "$LEFT" "$GRABCOL" "$FTOP" $((GRABCOL + 30))
wait_float_left gt 30
assert_scene "$LEFT" "$DIR/base-left" 1 20
assert_scene "$RIGHT" "$DIR/base-right" 1 20
# Move back through the other client and check the old right-hand footprint.
FLEFT=$($INNER display-message -p -t "$FLOAT" '#{pane_left}')
GRABCOL=$((FLEFT + FWIDTH / 2 + 1))
drag_float "$RIGHT" "$GRABCOL" "$FTOP" $((GRABCOL - 30))
wait_float_left lt 10
assert_scene "$LEFT" "$DIR/base-left" 35 60
assert_scene "$RIGHT" "$DIR/base-right" 35 60
exit 0
+208
View File
@@ -0,0 +1,208 @@
#!/bin/sh
# Check that full and region screen-write fallbacks update an attached client,
# not only tmux's internal pane grid.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
EMITTER=$DIR/emitter.pl
CAPTURE=$DIR/capture
INNER=
OUTER=
N=0
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
wait_outer_lacks()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" || return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client still showed $marker"
}
wait_inner_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 2>/dev/null |
grep -q "$marker" && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane did not contain $marker"
}
wait_inner_lacks()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$INNER capture-pane -p -t inner:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" || return 0
sleep 0.1
i=$((i + 1))
done
fail "inner pane still contained $marker"
}
setup()
{
mode=$1
[ -n "$OUTER" ] && $OUTER kill-server 2>/dev/null
[ -n "$INNER" ] && $INNER kill-server 2>/dev/null
N=$((N + 1))
INNER="$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null"
OUTER="$TEST_TMUX -Lredraw-write-outer-$$-$N -f/dev/null"
$INNER new-session -d -s inner -x 40 -y 12 \
"MODE=$mode READY='$DIR/ready-$N' TRIGGER='$DIR/trigger-$N' perl '$EMITTER'" ||
exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 12 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lredraw-write-inner-$$-$N -f/dev/null attach-session -t inner" ||
exit 1
}
trigger()
{
: >"$DIR/trigger-$N-${1:-1}"
}
cat >"$EMITTER" <<'PERL'
use strict;
use warnings;
$| = 1;
my $mode = $ENV{MODE};
my $ready = $ENV{READY};
my $trigger = $ENV{TRIGGER};
sub fill_screen {
my ($prefix) = @_;
print "\e[2J\e[H";
for my $row (0 .. 11) {
printf "\e[%d;1H%s-ROW-%02d", $row + 1, $prefix, $row;
}
}
if ($mode eq 'ris') {
fill_screen('RIS');
} elsif ($mode eq 'alternate') {
fill_screen('BASE');
} elsif ($mode eq 'scroll') {
fill_screen('SCROLL');
} else {
die "unknown mode $mode\n";
}
open my $fh, '>', $ready or die "$ready: $!\n";
close $fh;
while (!-e "$trigger-1") {
select undef, undef, undef, 0.01;
}
if ($mode eq 'ris') {
print "\ec";
} elsif ($mode eq 'alternate') {
print "\e[?1049h";
fill_screen('ALT');
while (!-e "$trigger-2") {
select undef, undef, undef, 0.01;
}
print "\e[?1049l";
} else {
print "\e[12;1H\r\nSCROLL-NEW";
}
sleep 100;
PERL
# RIS clears the complete screen. The source pane and attached client must both
# lose every old row.
setup ris
wait_outer_has RIS-ROW-11
trigger
wait_inner_lacks RIS-ROW
wait_outer_lacks RIS-ROW
# Leaving the alternate screen restores every row of the base screen.
setup alternate
wait_outer_has BASE-ROW-11
trigger
wait_inner_has ALT-ROW-11
wait_outer_has ALT-ROW-11
wait_outer_lacks BASE-ROW
trigger 2
wait_inner_has BASE-ROW-11
wait_outer_has BASE-ROW-11
wait_outer_lacks ALT-ROW
# Scrolling a pane which is narrower than the terminal redraws its complete
# region. Check the physical client row by row after the source grid shifts.
setup scroll
$INNER split-window -h -t inner:0 'sleep 100' || exit 1
wait_outer_has SCROLL-ROW-11
trigger
wait_inner_has SCROLL-NEW
wait_outer_has SCROLL-NEW
row=1
while [ "$row" -le 11 ]; do
expected=$(printf 'SCROLL-ROW-%02d' "$row")
actual=$(sed -n "${row}p" "$CAPTURE")
case "$actual" in
"$expected"*) ;;
*) fail "outer row $row was not redrawn as $expected" ;;
esac
row=$((row + 1))
done
actual=$(sed -n '12p' "$CAPTURE")
case "$actual" in
SCROLL-NEW*) ;;
*) fail "outer bottom row was not redrawn as SCROLL-NEW" ;;
esac
exit 0
+81
View File
@@ -0,0 +1,81 @@
#!/bin/sh
# Switching windows in the same session must redraw the attached client. The
# session object is shared, so its current winlink cannot be compared after it
# has already been changed.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lswitch-redraw-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
wait_for_client()
{
i=0
while [ "$i" -lt 50 ]; do
CLIENT=$($INNER list-clients -F '#{client_name}' 2>/dev/null)
[ -n "$CLIENT" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "inner client did not attach"
}
wait_outer_has()
{
marker=$1
i=0
while [ "$i" -lt 50 ]; do
$OUTER capture-pane -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
grep -q "$marker" "$CAPTURE" && return 0
sleep 0.1
i=$((i + 1))
done
fail "outer client did not show $marker"
}
$INNER new-session -d -s inner -x 40 -y 8 \
"printf '\033[2J\033[HA-WINDOW'; exec sleep 100" || exit 1
$INNER new-window -d -t inner:1 \
"printf '\033[2J\033[HB-WINDOW'; exec sleep 100" || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$OUTER new-session -d -s outer -x 40 -y 8 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lswitch-redraw-inner-$$ -f/dev/null attach-session -t inner:0" ||
exit 1
wait_for_client
wait_outer_has A-WINDOW
$INNER switch-client -c "$CLIENT" -t inner:1.0 || exit 1
[ "$($INNER display-message -p -t inner '#{window_index}')" -eq 1 ] ||
fail "server did not select window 1"
wait_outer_has B-WINDOW
exit 0
+123
View File
@@ -0,0 +1,123 @@
#!/bin/sh
# Dragging an inactive pane's scrollbar slider changes which pane is
# active (window_copy_scroll(), window-copy.c). Every other place in the
# codebase that changes the active pane pairs it with
# window_redraw_active_switch() (or falls back to a full window redraw) so
# the old/new active panes' window-active-style/window-style colours get
# repainted - window_copy_scroll() was the one caller that did neither, so
# a scrollbar-slider drag changed focus but left both panes' body colours
# stale. This used to be masked by window_set_active_pane() itself doing
# an unconditional full redraw on every active-pane change, until that was
# narrowed to borders/status-only for the common case.
#
# This creates two tiled panes with clearly distinguishable
# window-active-style/window-style backgrounds, puts the *inactive* one in
# copy mode, drags its scrollbar slider, and checks - via an attached
# client's own received bytes, since window-style is applied during
# redraw composition rather than stored in the grid, so capture-pane
# alone would not reflect it - that the newly active pane's marker text
# immediately shows the active-style colour, not the stale one.
PATH=/bin:/usr/bin
TERM=screen
LC_ALL=C.UTF-8
export PATH TERM LC_ALL
[ -z "$TEST_TMUX" ] && TEST_TMUX=$(readlink -f ../tmux)
DIR=$(mktemp -d) || exit 1
INNER="$TEST_TMUX -Lsbfocus-inner-$$ -f/dev/null"
OUTER="$TEST_TMUX -Lsbfocus-outer-$$ -f/dev/null"
CAPTURE=$DIR/capture
ESC=$(printf '\033')
fail()
{
echo "$*" >&2
[ -s "$CAPTURE" ] && cat -A "$CAPTURE" >&2
exit 1
}
cleanup()
{
$OUTER kill-server 2>/dev/null
$INNER kill-server 2>/dev/null
rm -rf "$DIR"
}
trap cleanup 0 1 15
get_mark_color()
{
$OUTER capture-pane -e -p -t outer:0.0 >"$CAPTURE" 2>/dev/null || true
LC_ALL=C grep -a "MARK1" "$CAPTURE" |
LC_ALL=C grep -aoE "${ESC}\[4[12]m" | tail -1
}
wait_mark_color()
{
want=$1
i=0
while [ "$i" -lt 50 ]; do
[ "$(get_mark_color)" = "$want" ] && return 0
sleep 0.1
i=$((i + 1))
done
fail "MARK1's pane never showed the expected background (wanted '$want', got '$(get_mark_color)')"
}
mouse()
{
sequence=$(printf '\033[<%s;%s;%s%s' "$1" "$2" "$3" "$4")
$OUTER send-keys -t outer:0.0 -l "$sequence" || exit 1
sleep 0.1
}
$INNER new-session -d -s inner -x 40 -y 10 'sleep 100' || exit 1
$INNER set-option -g status off || exit 1
$INNER set-option -g window-size manual || exit 1
$INNER set-option -g mouse on || exit 1
$INNER set-option -g pane-scrollbars on || exit 1
$INNER setw pane-scrollbars-position right || exit 1
$INNER setw pane-scrollbars-style 'width=1,pad=0' || exit 1
$INNER set-option -g window-active-style 'bg=red' || exit 1
$INNER set-option -g window-style 'bg=green' || exit 1
$INNER split-window -h -t inner -d 'printf MARK1; sleep 100' || exit 1
PANE1=$($INNER list-panes -t inner -F '#{pane_id}' | sed -n 2p)
[ "$($INNER display-message -p -t "$PANE1" '#{pane_active}')" = 0 ] ||
fail "sanity: MARK1's pane is already active before the drag"
# Put the inactive pane in copy mode without switching focus (copy-mode -t
# targets a pane without making it active).
$INNER copy-mode -t "$PANE1" || exit 1
$OUTER new-session -d -s outer -x 40 -y 10 'sleep 100' || exit 1
$OUTER set-option -g status off || exit 1
$OUTER set-option -g window-size manual || exit 1
$OUTER set-option -g default-terminal screen-256color || exit 1
$OUTER respawn-pane -k -t outer:0.0 \
"$TEST_TMUX -Lsbfocus-inner-$$ -f/dev/null attach-session -t inner" ||
exit 1
wait_mark_color "${ESC}[42m"
XOFF=$($INNER display-message -p -t "$PANE1" '#{pane_left}')
YOFF=$($INNER display-message -p -t "$PANE1" '#{pane_top}')
SX=$($INNER display-message -p -t "$PANE1" '#{pane_width}')
SBCOL=$((XOFF + SX + 1))
SBROW=$((YOFF + 1))
# With no scrollback beyond the pane's own content, the slider fills the
# whole scrollbar track, so any point on it (here, its very first row) is
# on the slider.
mouse 0 "$SBCOL" "$SBROW" M
mouse 32 "$SBCOL" "$((SBROW + 1))" M
mouse 0 "$SBCOL" "$((SBROW + 1))" m
[ "$($INNER display-message -p -t "$PANE1" '#{pane_active}')" = 1 ] ||
fail "sanity: dragging the scrollbar slider did not change focus"
wait_mark_color "${ESC}[41m"
exit 0
+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;
}
+858 -59
View File
File diff suppressed because it is too large Load Diff
+103 -112
View File
@@ -34,6 +34,10 @@ 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 *,
@@ -120,14 +124,39 @@ screen_write_set_cursor(struct screen_write_ctx *ctx, int cx, int cy)
evtimer_add(&w->offset_timer, &tv);
}
/* Do a full redraw. */
/*
* 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)
screen_write_redraw_cb(const struct tty_ctx *ttyctx, u_int py, u_int ny)
{
struct window_pane *wp = ttyctx->arg;
int x0, y0, x1, y1;
if (wp != NULL)
wp->flags |= PANE_REDRAW;
if (wp == NULL)
return;
x0 = wp->xoff;
y0 = wp->yoff + (int)py;
x1 = x0 + (int)wp->sx;
y1 = y0 + (int)ny;
if (x0 < 0)
x0 = 0;
if (y0 < 0)
y0 = 0;
if (x1 <= x0 || y1 <= y0)
return;
redraw_damage_window(wp->window, (u_int)x0, (u_int)y0,
(u_int)(x1 - x0), (u_int)(y1 - y0));
}
/* Update context for client. */
@@ -366,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,
@@ -1331,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++) {
@@ -1378,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);
@@ -1419,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);
@@ -1460,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);
@@ -1495,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) {
@@ -1563,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) {
@@ -1636,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;
@@ -1675,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);
@@ -1702,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)
@@ -1756,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);
@@ -1808,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);
@@ -1831,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);
@@ -1863,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++) {
@@ -1892,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++)
@@ -1933,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);
@@ -2014,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);
@@ -2087,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);
@@ -2155,7 +2181,7 @@ screen_write_fullredraw(struct screen_write_ctx *ctx)
screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx);
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy);
}
/* Trim collected items. */
@@ -2592,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,
@@ -2724,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) {
@@ -2951,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);
@@ -3086,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
@@ -3175,7 +3166,7 @@ screen_write_alternateon(struct screen_write_ctx *ctx, struct grid_cell *gc,
screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx);
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy);
}
/* Turn alternate screen off. */
@@ -3200,5 +3191,5 @@ screen_write_alternateoff(struct screen_write_ctx *ctx, struct grid_cell *gc,
screen_write_initctx(ctx, &ttyctx, 1, 0);
if (ttyctx.redraw_cb != NULL)
ttyctx.redraw_cb(&ttyctx);
ttyctx.redraw_cb(&ttyctx, 0, ttyctx.sy);
}
-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)
+88 -3
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);
@@ -356,7 +357,33 @@ server_client_set_session(struct client *c, struct session *s)
tty_update_client_offset(c);
status_timer_start(c);
server_client_fire_session_changed(c, old);
server_redraw_client(c);
/*
* 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 == NULL || old != s ||
!redraw_client_has_window(c, s->curw->window))
server_redraw_client(c);
}
server_check_unattached();
@@ -407,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);
@@ -1351,8 +1379,19 @@ server_client_key_callback(struct cmdq_item *item, void *data)
/*
* 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);
c->tty.mouse_drag_update(c, m);
goto out;
}
@@ -1780,7 +1819,14 @@ server_client_loop(void)
/*
* Any windows will have been redrawn as part of clients, so clear
* their flags now.
* 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) {
@@ -1791,6 +1837,8 @@ server_client_loop(void)
wp->flags &= ~(PANE_REDRAW|PANE_REDRAWSCROLLBAR|
PANE_ACTIVITY);
}
redraw_free_damage(w);
check_window_name(w);
}
@@ -2336,6 +2384,10 @@ server_client_any_pane_redraw(struct client *c)
if (c->flags & CLIENT_REDRAWWINDOW)
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);
@@ -2380,9 +2432,26 @@ server_client_check_redraw(struct client *c)
/*
* 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.
* 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;
if (n != 0 || (tty->flags & TTY_BLOCK)) {
if (n != 0)
log_debug("%s: redraw deferred (%zu left)", c->name, n);
@@ -2394,6 +2463,8 @@ server_client_check_redraw(struct client *c)
log_debug("redraw timer started");
evtimer_add(&ev, &tv);
}
if (!TAILQ_EMPTY(&w->damage))
redraw_defer_damage(c);
TAILQ_FOREACH(wp, &w->panes, entry) {
if (wp->flags & PANE_REDRAW) {
c->flags |= CLIENT_REDRAWWINDOW;
@@ -2427,6 +2498,19 @@ server_client_check_redraw(struct client *c)
redraw_pane_scrollbar(c, wp);
}
}
/*
* 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);
}
/*
@@ -2440,6 +2524,7 @@ server_client_check_redraw(struct client *c)
}
server_client_set_progress_bar(c);
redraw_screen(c);
redraw_client_damage(c);
}
/* Put the tty back how it was. */
+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,
+8
View File
@@ -5100,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
+242 -37
View File
@@ -69,6 +69,7 @@ struct options_array_item;
struct options_entry;
struct prompt;
struct window_pane_prompt;
struct redraw_damage;
struct redraw_scene;
struct redraw_span;
struct screen_write_citem;
@@ -76,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
@@ -862,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;
@@ -873,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;
@@ -891,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. */
@@ -915,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. */
@@ -934,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. */
@@ -1025,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. */
@@ -1101,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;
@@ -1123,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;
@@ -1402,6 +1451,7 @@ struct window_pane {
struct screen base;
struct screen status_screen;
u_int status_serial;
TAILQ_HEAD(, window_mode_entry) modes;
@@ -1432,6 +1482,7 @@ struct window_pane {
TAILQ_HEAD(window_panes, window_pane);
TAILQ_HEAD(window_panes_zindex, window_pane);
RB_HEAD(window_pane_tree, window_pane);
TAILQ_HEAD(redraw_damages, redraw_damage);
/* Window structure. */
struct window {
@@ -1475,6 +1526,13 @@ struct window {
uint64_t redraw_scene_generation;
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;
u_int menu_last_py;
@@ -1742,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;
@@ -1800,6 +1864,14 @@ 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;
struct grid_cell cell;
@@ -1844,10 +1916,17 @@ 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. */
typedef void (*tty_ctx_redraw_cb)(const struct tty_ctx *);
typedef void (*tty_ctx_redraw_cb)(const struct tty_ctx *, u_int, u_int);
typedef int (*tty_ctx_set_client_cb)(struct tty_ctx *, struct client *);
struct tty_ctx {
struct screen *s;
@@ -1879,7 +1958,7 @@ struct tty_ctx {
size_t size;
} sel;
#ifdef ENABLE_SIXEL
#ifdef ENABLE_IMAGES
struct image *image;
#endif
};
@@ -2240,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;
@@ -2978,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 *);
@@ -3023,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);
@@ -3620,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
@@ -3634,8 +3727,19 @@ void redraw_screen(struct client *);
void redraw_pane(struct client *, struct window_pane *);
void redraw_pane_scrollbar(struct client *, struct window_pane *);
void redraw_free_scene(struct redraw_scene *);
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 */
@@ -3809,6 +3913,8 @@ 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 *);
/* window-border.c */
@@ -4198,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 *);
@@ -4217,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) {
+71 -2
View File
@@ -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)
{
+76 -127
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);
@@ -1115,7 +1153,7 @@ tty_redraw_region(struct tty *tty, const struct tty_ctx *ctx)
*/
if (tty_large_region(tty, ctx) || ctx->flags & TTY_CTX_PANE_OBSCURED) {
log_debug("%s: %s large region redraw", __func__, c->name);
ctx->redraw_cb(ctx);
ctx->redraw_cb(ctx, ctx->orupper, ctx->orlower - ctx->orupper + 1);
return;
}
@@ -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)
@@ -1517,6 +1502,7 @@ tty_sync_start(struct tty *tty)
if (tty->flags & TTY_SYNCING)
return;
tty->flags |= TTY_SYNCING;
tty->sync_offset = EVBUFFER_LENGTH(tty->out);
if (tty_term_has(tty->term, TTYC_SYNC)) {
log_debug("%s sync start", tty->client->name);
@@ -1582,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)
{
@@ -1729,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);
@@ -1757,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);
@@ -1795,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);
@@ -1833,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);
@@ -1923,7 +1924,7 @@ tty_cmd_alignmenttest(struct tty *tty, const struct tty_ctx *ctx)
u_int i, j;
if (ctx->flags & TTY_CTX_WINDOW_BIGGER) {
ctx->redraw_cb(ctx);
ctx->redraw_cb(ctx, 0, ctx->sy);
return;
}
@@ -1982,7 +1983,7 @@ tty_cmd_cells(struct tty *tty, const struct tty_ctx *ctx)
tty->cy == tty->rlower)
tty_draw_pane(tty, ctx, ctx->ocy);
else
ctx->redraw_cb(ctx);
ctx->redraw_cb(ctx, ctx->ocy, 1);
return;
}
@@ -2028,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);
+133
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);
@@ -768,6 +779,7 @@ window_copy_scroll(struct window_pane *wp, int sl_mpos, u_int my,
struct window_mode_entry *wme = TAILQ_FIRST(&wp->modes);
if (wme != NULL) {
window_redraw_active_switch(wp->window, wp);
window_set_active_pane(wp->window, wp, 0);
window_copy_scroll1(wme, wp, sl_mpos, my, tty_oy, scroll_exit);
}
@@ -1197,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);
@@ -5223,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) {
@@ -5471,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') {
@@ -5531,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)
{
@@ -5540,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++)
@@ -6928,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) {
@@ -6999,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) {
+128 -2
View File
@@ -415,6 +415,7 @@ window_create(u_int sx, u_int sy, u_int xpixel, u_int ypixel)
TAILQ_INIT(&w->panes);
TAILQ_INIT(&w->z_index);
TAILQ_INIT(&w->last_panes);
TAILQ_INIT(&w->damage);
w->active = NULL;
w->lastlayout = -1;
@@ -468,6 +469,7 @@ window_destroy(struct window *w)
menu_destroy(w);
window_destroy_panes(w);
redraw_free_damage(w);
if (event_initialized(&w->name_event))
evtimer_del(&w->name_event);
@@ -718,6 +720,7 @@ int
window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
{
struct window_pane *lastwp;
int unzoomed;
log_debug("%s: pane %%%u", __func__, wp->id);
@@ -725,7 +728,8 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
return (0);
if (w->modal != NULL && wp != w->modal)
return (0);
if ((w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp))
unzoomed = (w->flags & WINDOW_ZOOMED) && !window_pane_is_visible(wp);
if (unzoomed)
window_unzoom(w, 1);
lastwp = w->active;
@@ -742,7 +746,19 @@ window_set_active_pane(struct window *w, struct window_pane *wp, int notify)
}
tty_update_window_offset(w);
server_redraw_window(w);
/*
* Unzooming changes every pane's geometry and needs a full window
* redraw. Otherwise, only the previous and new active pane's border
* and status appearance changed, so avoid redrawing unaffected pane
* content.
*/
if (unzoomed)
server_redraw_window(w);
else {
server_redraw_window_borders(w);
server_status_window(w);
}
if (notify)
window_fire_pane_changed(w, w->active, lastwp);
@@ -1676,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)
@@ -2925,3 +2945,109 @@ window_pane_is_floating_with_hidden(struct window_pane *wp)
return (0);
return (1);
}
/*
* 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_pane_damage_floating(struct window *w, struct window_pane *wp,
int xoff, int yoff, int sx, int sy)
{
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 = wp->scrollbar_style.width +
wp->scrollbar_style.pad;
else
sb_right = wp->scrollbar_style.width +
wp->scrollbar_style.pad;
}
x0 = xoff - 1 - sb_left;
x1 = xoff + sx + sb_right;
y0 = yoff - 1;
y1 = yoff + sy;
if (x0 < 0)
x0 = 0;
if (y0 < 0)
y0 = 0;
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);
}
/*
* 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)
{
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);
}