diff --git a/packages/tui/src/layout.ts b/packages/tui/src/layout.ts index 897c51de2..5e089078c 100644 --- a/packages/tui/src/layout.ts +++ b/packages/tui/src/layout.ts @@ -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; } } diff --git a/packages/tui/test/tui-alt-screen.test.ts b/packages/tui/test/tui-alt-screen.test.ts index acead60ce..412d662d0 100644 --- a/packages/tui/test/tui-alt-screen.test.ts +++ b/packages/tui/test/tui-alt-screen.test.ts @@ -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;