fix(tui): crop boxed images scrolled above the fullscreen viewport

Image placeholder rows inside padded or colored boxes contain spaces and
SGR codes, which stopped the backward search for the image line, so the
whole image disappeared once its first row scrolled out of view.
This commit is contained in:
triagent 2026-10-23 07:59:59 +00:00 • committed by Petr Houška
parent a7b94a03f0
commit 4464442f95
2 changed files with 28 additions and 1 deletions

View file

@ -369,7 +369,9 @@ function paintBox(box: LayoutBox, screen: string[], totalWidth: number): void {
}
break;
}
if (imageLine !== "") break;
// Image placeholder rows inside padded or colored boxes contain only
// spaces and SGR codes; keep searching past them for the image line.
if (imageLine.replace(/\x1b\[[0-9;:]*m/g, "").trim() !== "") break;
}
}

View file

@ -5,6 +5,7 @@ import {
AltScreenSearchIndex,
findAltScreenSearchMatches,
} from "../src/alt-screen-search.ts";
import { Box } from "../src/components/box.ts";
import { HStack } from "../src/components/h-stack.ts";
import { Image } from "../src/components/image.ts";
import { MouseRegion } from "../src/components/mouse-region.ts";
@ -1074,6 +1075,30 @@ describe("TuiAltScreen", () => {
tui.stop();
});
it("crops a Kitty image in a padded, colored box whose first line is above the viewport", async () => {
const terminal = new RecordingTerminal(20, 3);
const tui = new TuiAltScreen(terminal);
const imageId = 11271;
const imageLine = encodeKitty("AAAA", { columns: 2, rows: 3, imageId, moveCursor: false });
registerKittyImageMetadata({ imageId, columns: 2, rows: 3, widthPx: 100, heightPx: 100 });
const box = new Box(1, 0, (text: string) => `\x1b[44m${text}\x1b[49m`);
box.addChild({ render: () => [imageLine, "", ""], invalidate: () => {} });
tui.addChild({ render: () => ["before"], invalidate: () => {} });
tui.addChild(box);
tui.addChild({ render: () => ["after", "end"], invalidate: () => {} });
tui.start();
await terminal.waitForRender();
assert.strictEqual(tui.viewportTop, 3);
assert.ok(
terminal.events.some(
(event) => event.type === "write" && event.data.includes("i=11271") && event.data.includes("y=66,h=34,r=1"),
),
);
tui.stop();
});
it("redraws WezTerm Kitty images after writes to covered rows", async () => {
// Regression test for #10319: a scrollbar update below an unchanged image anchor erased its cells.
const weztermPane = process.env.WEZTERM_PANE;