fix(tui): preserve WezTerm Kitty images on full redraw

Reserve visible Kitty image rows before drawing placements in full redraws, while preserving the first-row path for taller-than-viewport images so #4415 stays fixed.

Closes #5618.
This commit is contained in:
Armin Ronacher
2026-06-14 23:16:59 +02:00
parent d683a581b7
commit 93b3b7c1fe
3 changed files with 107 additions and 1 deletions

View File

@@ -152,6 +152,95 @@ describe("TUI Kitty image cleanup", () => {
}
});
it("reserves Kitty image rows before drawing during full redraw fallbacks", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
setCellDimensions({ widthPx: 10, heightPx: 10 });
try {
const terminal = new LoggingVirtualTerminal(40, 5);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["l0", "l1", "l2", "l3", "l4"];
tui.start();
await terminal.waitForRender();
const redrawsBeforeImage = tui.fullRedraws;
terminal.clearWrites();
const image = new Image(
"AAAA",
"image/png",
{ fallbackColor: (value) => value },
{ maxWidthCells: 3 },
{ widthPx: 30, heightPx: 30 },
);
const imageLines = image.render(40);
const imageSequence = imageLines[0];
component.lines = ["l0", "l1", "l2", "l3", "l4", ...imageLines, "after"];
tui.requestRender();
await terminal.waitForRender();
const writes = terminal.getWrites();
assert.ok(tui.fullRedraws > redrawsBeforeImage, "scrolling image append should force a full redraw");
assert.ok(
writes.includes(`\r\n\r\n\x1b[2A${imageSequence}\x1b[2B`),
"full redraw should reserve visible image rows before drawing the placement",
);
assert.ok(
!writes.includes(`${imageSequence}\r\n\x1b[0m`),
"full redraw must not write reserved padding rows after drawing the placement",
);
tui.stop();
} finally {
resetCapabilitiesCache();
setCellDimensions({ widthPx: 9, heightPx: 18 });
}
});
it("does not use cursor-up placement for Kitty images taller than the viewport", async () => {
setCapabilities({ images: "kitty", trueColor: true, hyperlinks: true });
setCellDimensions({ widthPx: 10, heightPx: 10 });
try {
const terminal = new LoggingVirtualTerminal(40, 5);
const tui = new TUI(terminal);
const component = new TestComponent();
tui.addChild(component);
component.lines = ["before"];
tui.start();
await terminal.waitForRender();
terminal.clearWrites();
const image = new Image(
"AAAA",
"image/png",
{ fallbackColor: (value) => value },
{ maxWidthCells: 6 },
{ widthPx: 60, heightPx: 60 },
);
const imageLines = image.render(40);
const imageSequence = imageLines[0];
assert.ok(imageLines.length > terminal.rows, "test image should exceed the viewport height");
component.lines = ["before", ...imageLines, "after"];
tui.requestRender(true);
await terminal.waitForRender();
const writes = terminal.getWrites();
assert.ok(writes.includes(imageSequence), "image placement should be drawn");
assert.ok(
!writes.includes(`\x1b[${imageLines.length - 1}A${imageSequence}`),
"taller-than-viewport images must keep the #4461 first-row placement path",
);
tui.stop();
} finally {
resetCapabilitiesCache();
setCellDimensions({ widthPx: 9, heightPx: 18 });
}
});
it("deletes changed image ids before drawing moved placements", async () => {
const terminal = new LoggingVirtualTerminal(40, 10);
const tui = new TUI(terminal);