fix(tui): keep kitty image redraws inside TUI

Fixes #4208.
This commit is contained in:
Armin Ronacher
2026-05-07 10:34:37 +02:00
parent 50993d743d
commit b8712457d2
5 changed files with 273 additions and 19 deletions

View File

@@ -22,6 +22,8 @@ export interface ImageRenderOptions {
preserveAspectRatio?: boolean;
/** Kitty image ID. If provided, reuses/replaces existing image with this ID. */
imageId?: number;
/** Whether Kitty should apply its default cursor movement after placement. */
moveCursor?: boolean;
}
let cachedCapabilities: TerminalCapabilities | null = null;
@@ -46,10 +48,7 @@ export function detectCapabilities(): TerminalCapabilities {
// sequences differently). Force hyperlinks off whenever we detect them, even
// when the outer terminal would otherwise support OSC 8. Image protocols are
// also unreliable under tmux/screen, so leave `images: null` for safety.
// cmux currently also gets this conservative fallback due to image corruption:
// https://github.com/badlogic/pi-mono/issues/4208
const inTmuxOrScreen =
!!process.env.TMUX || !!process.env.CMUX_WORKSPACE_ID || term.startsWith("tmux") || term.startsWith("screen");
const inTmuxOrScreen = !!process.env.TMUX || term.startsWith("tmux") || term.startsWith("screen");
if (inTmuxOrScreen) {
const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
return { images: null, trueColor, hyperlinks: false };
@@ -131,12 +130,15 @@ export function encodeKitty(
columns?: number;
rows?: number;
imageId?: number;
/** Whether Kitty should apply its default cursor movement after placement. Default: true. */
moveCursor?: boolean;
} = {},
): string {
const CHUNK_SIZE = 4096;
const params: string[] = ["a=T", "f=100", "q=2"];
if (options.moveCursor === false) params.push("C=1");
if (options.columns) params.push(`c=${options.columns}`);
if (options.rows) params.push(`r=${options.rows}`);
if (options.imageId) params.push(`i=${options.imageId}`);
@@ -173,7 +175,7 @@ export function encodeKitty(
* Uses uppercase 'I' to also free the image data.
*/
export function deleteKittyImage(imageId: number): string {
return `\x1b_Ga=d,d=I,i=${imageId}\x1b\\`;
return `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`;
}
/**
@@ -181,7 +183,7 @@ export function deleteKittyImage(imageId: number): string {
* Uses uppercase 'A' to also free the image data.
*/
export function deleteAllKittyImages(): string {
return `\x1b_Ga=d,d=A\x1b\\`;
return "\x1b_Ga=d,d=A,q=2\x1b\\";
}
export function encodeITerm2(
@@ -377,8 +379,12 @@ export function renderImage(
const rows = calculateImageRows(imageDimensions, maxWidth, getCellDimensions());
if (caps.images === "kitty") {
// Only use imageId if explicitly provided - static images don't need IDs
const sequence = encodeKitty(base64Data, { columns: maxWidth, rows, imageId: options.imageId });
const sequence = encodeKitty(base64Data, {
columns: maxWidth,
rows,
imageId: options.imageId,
moveCursor: options.moveCursor,
});
return { sequence, rows, imageId: options.imageId };
}