Skip to content

Sixel images are displaced by a scroll that happens entirely outside their rows #1381

Description

@hakonrk

When a scroll region is set below an image and the region is scrolled up, mintty moves the image too, even though it lies entirely outside the region. Images below the region are also moved, for a related reason.

This came out of tmux/tmux#5507, where it first looked like a tmux bug. The diagnosis and the patch below are the work of Michael Grant (@mgrant0), a tmux maintainer, who traced it to term_do_scroll() and reduced it to a case with no tmux involved. I have verified the reproduction and built mintty master with the patch applied; I am filing here on his suggestion so it reaches the right tracker. Full history is in the tmux issue.

Reproduction

In a 33-row mintty window, with the attached sixels.txt (any sixel image about 19 rows tall will do):

clear; cat sixels.txt; sleep 1 ; printf '\e[21;32r\e[12S\e[r' ; sleep 10

The image occupies rows 1-19. The escape sequence sets a scroll region of rows 21-32, scrolls it up by 12, then resets the region.

Expected: nothing above row 21 changes; the image stays where it is.

Actual: the image jumps up 12 rows, leaving the bottom portion stacked over the top of itself. Forcing a full repaint restores it.

Cause

mintty stores image positions in virtual line coordinates: img->top = term.virtuallines + scrtop (winimg.c:274), and paints at (img->top - term.virtuallines - term.disptop) * cell_height (winimg.c:571). term.virtuallines is therefore a global offset: bumping it slides every image on screen.

The scroll-up branch of term_do_scroll() does exactly that and nothing else (term.c:2237):

    else {
      int seltop = topline;

      term.virtuallines += lines;

For a full-screen scroll that is correct. For a region scroll with topline > 0 it drags along every image on screen, including ones nowhere near the region.

There is a second, related bug just above it. The scroll-down branch does have an image fixup loop, but its bounds test is cur->top - term.virtuallines >= topline, only a lower bound. The selection code immediately adjacent gets this right with p->y >= topline && p->y < botline, so the image loop also wrongly moves images that sit below the region.

Suggested patch

Against master's src/term.c: adds the missing botline bound to the scroll-down loop, and compensates images outside the region in the scroll-up branch.

--- a/src/term.c
+++ b/src/term.c
@@ -2229,7 +2229,8 @@ term_do_scroll(int topline, int botline, int lines, bool sb)
 
     // Move graphics if within the scroll region
     for (imglist * cur = term.imgs.first; cur; cur = cur->next) {
-      if (cur->top - term.virtuallines >= topline) {
+      int imgtop = cur->top - term.virtuallines;
+      if (imgtop >= topline && imgtop < botline) {
         cur->top += lines;
       }
     }
@@ -2239,6 +2240,14 @@ term_do_scroll(int topline, int botline, int lines, bool sb)
 
     term.virtuallines += lines;
 
+    // Shifting virtuallines moves every image on the screen; images outside
+    // the scroll region must stay put, so compensate for those.
+    for (imglist * cur = term.imgs.first; cur; cur = cur->next) {
+      int imgtop = cur->top - term.virtuallines + lines;
+      if (imgtop < topline || imgtop >= botline)
+        cur->top += lines;
+    }
+
     // Only push lines into the scrollback when scrolling off the top of the
     // normal screen and scrollback is actually enabled.
     if (sb && topline == 0 && !term.on_alt_screen && cfg.scrollback_lines) {

I have built master (d7132bb) with this applied and it fixes both the reduced case above and the original tmux case. Ordinary full-screen scrolling and scrollback still behave as before, so the added loop does not appear to regress the common path.

Why this shows up in practice

zsh emits a bare \e[J before drawing each prompt, so under tmux this triggers on essentially every command. Any sixel image on screen is corrupted as soon as the next prompt appears.

tmux turns that \e[J into precisely the pattern above. With the cursor on row 20 and an image on rows 1-19, tty_clear_area() in tmux's tty.c clears the rest of the screen by scrolling it away:

^[[21;32r^[[12S^[[20;1H^[[K^[[1;33r

That is legal and correct, and tmux still believes the image is on rows 1-19, so it does not repaint it, which is why the corruption persists until you detach and reattach.

Environment

  • uname -s: MINGW64_NT-10.0-26200

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions