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

@@ -6,6 +6,10 @@
- Added terminal background color query support for OSC 11 replies.
### Fixed
- Fixed WezTerm inline Kitty image rendering during full redraw fallbacks so image padding rows are reserved before the placement is drawn without regressing tall-image placement ([#5618](https://github.com/earendil-works/pi/issues/5618), [#4415](https://github.com/earendil-works/pi/issues/4415)).
## [0.79.3] - 2026-06-13
## [0.79.2] - 2026-06-12

View File

@@ -1244,7 +1244,20 @@ export class TUI extends Container {
}
for (let i = 0; i < newLines.length; i++) {
if (i > 0) buffer += "\r\n";
buffer += newLines[i];
const line = newLines[i];
const isImage = isImageLine(line);
const imageReservedRows = isImage ? this.getKittyImageReservedRows(newLines, i) : 1;
if (imageReservedRows > 1 && imageReservedRows <= height) {
for (let row = 1; row < imageReservedRows; row++) {
buffer += "\r\n";
}
buffer += `\x1b[${imageReservedRows - 1}A`;
buffer += line;
buffer += `\x1b[${imageReservedRows - 1}B`;
i += imageReservedRows - 1;
continue;
}
buffer += line;
}
buffer += "\x1b[?2026l"; // End synchronized output
this.terminal.write(buffer);

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);