Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## [Unreleased]

### Added

- Added tmux support for Kitty graphics protocol using unicode placeholders, enabling inline images to render correctly inside tmux without getting stuck during screen redraws ([#908](https://github.com/badlogic/pi-mono/pull/908) by [@ogulcancelik](https://github.com/ogulcancelik))

## [0.50.1] - 2026-01-26

## [0.50.0] - 2026-01-26
Expand Down
59 changes: 32 additions & 27 deletions packages/tui/src/components/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ export interface ImageOptions {
maxWidthCells?: number;
maxHeightCells?: number;
filename?: string;
/** Kitty image ID. If provided, reuses this ID (for animations/updates). */
imageId?: number;
}

export class Image implements Component {
Expand All @@ -25,10 +23,11 @@ export class Image implements Component {
private dimensions: ImageDimensions;
private theme: ImageTheme;
private options: ImageOptions;
private imageId?: number;

private cachedLines?: string[];
private cachedWidth?: number;
/** Track whether image data has been transmitted (tmux mode only) */
private imageTransmitted = false;

constructor(
base64Data: string,
Expand All @@ -42,17 +41,12 @@ export class Image implements Component {
this.theme = theme;
this.options = options;
this.dimensions = dimensions || getImageDimensions(base64Data, mimeType) || { widthPx: 800, heightPx: 600 };
this.imageId = options.imageId;
}

/** Get the Kitty image ID used by this image (if any). */
getImageId(): number | undefined {
return this.imageId;
}

invalidate(): void {
this.cachedLines = undefined;
this.cachedWidth = undefined;
this.imageTransmitted = false;
}

render(width: number): string[] {
Expand All @@ -66,27 +60,38 @@ export class Image implements Component {
let lines: string[];

if (caps.images) {
const result = renderImage(this.base64Data, this.dimensions, {
maxWidthCells: maxWidth,
imageId: this.imageId,
});
const result = renderImage(this.base64Data, this.dimensions, { maxWidthCells: maxWidth });

if (result) {
// Store the image ID for later cleanup
if (result.imageId) {
this.imageId = result.imageId;
}

// Return `rows` lines so TUI accounts for image height
// First (rows-1) lines are empty (TUI clears them)
// Last line: move cursor back up, then output image sequence
lines = [];
for (let i = 0; i < result.rows - 1; i++) {
lines.push("");
if (result.placeholderLines) {
// tmux mode: transmit image data once, then use placeholder lines
// Placeholders reference the image by ID encoded in foreground color
lines = [];
if (!this.imageTransmitted) {
// First render: transmit image data + first placeholder row
lines.push(result.sequence + result.placeholderLines[0]);
this.imageTransmitted = true;
} else {
// Subsequent renders: placeholders only (image already in terminal memory)
lines.push(result.placeholderLines[0]);
}
// Rest of the placeholder rows
for (let i = 1; i < result.placeholderLines.length; i++) {
lines.push(result.placeholderLines[i]);
}
} else {
// Direct placement mode (non-tmux)
// Return `rows` lines so TUI accounts for image height
// First (rows-1) lines are empty (TUI clears them)
// Last line: move cursor back up, then output image sequence
lines = [];
for (let i = 0; i < result.rows - 1; i++) {
lines.push("");
}
// Move cursor up to first row, then output image
const moveUp = result.rows > 1 ? `\x1b[${result.rows - 1}A` : "";
lines.push(moveUp + result.sequence);
}
// Move cursor up to first row, then output image
const moveUp = result.rows > 1 ? `\x1b[${result.rows - 1}A` : "";
lines.push(moveUp + result.sequence);
} else {
const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
lines = [this.theme.fallbackColor(fallback)];
Expand Down
36 changes: 36 additions & 0 deletions packages/tui/src/diacritics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Diacritics used for encoding row/column values in Kitty graphics protocol Unicode placeholders.
*
* The index in this array IS the encoded value (0-296).
* These are Unicode combining characters (class 230, "above base") that were carefully
* chosen to avoid normalization issues.
*
* Source: https://sw.kovidgoyal.net/kitty/_downloads/f0a0de9ec8d9ff4456206db8e0814937/rowcolumn-diacritics.txt
*
* Used by terminals like Kitty, Ghostty, and WezTerm to decode row/column positions
* for virtual image placements in multiplexer-safe rendering mode.
*/
export const KITTY_DIACRITICS: readonly number[] = [
0x0305, 0x030d, 0x030e, 0x0310, 0x0312, 0x033d, 0x033e, 0x033f, 0x0346, 0x034a, 0x034b, 0x034c, 0x0350, 0x0351,
0x0352, 0x0357, 0x035b, 0x0363, 0x0364, 0x0365, 0x0366, 0x0367, 0x0368, 0x0369, 0x036a, 0x036b, 0x036c, 0x036d,
0x036e, 0x036f, 0x0483, 0x0484, 0x0485, 0x0486, 0x0487, 0x0592, 0x0593, 0x0594, 0x0595, 0x0597, 0x0598, 0x0599,
0x059c, 0x059d, 0x059e, 0x059f, 0x05a0, 0x05a1, 0x05a8, 0x05a9, 0x05ab, 0x05ac, 0x05af, 0x05c4, 0x0610, 0x0611,
0x0612, 0x0613, 0x0614, 0x0615, 0x0616, 0x0617, 0x0657, 0x0658, 0x0659, 0x065a, 0x065b, 0x065d, 0x065e, 0x06d6,
0x06d7, 0x06d8, 0x06d9, 0x06da, 0x06db, 0x06dc, 0x06df, 0x06e0, 0x06e1, 0x06e2, 0x06e4, 0x06e7, 0x06e8, 0x06eb,
0x06ec, 0x0730, 0x0732, 0x0733, 0x0735, 0x0736, 0x073a, 0x073d, 0x073f, 0x0740, 0x0741, 0x0743, 0x0745, 0x0747,
0x0749, 0x074a, 0x07eb, 0x07ec, 0x07ed, 0x07ee, 0x07ef, 0x07f0, 0x07f1, 0x07f3, 0x0816, 0x0817, 0x0818, 0x0819,
0x081b, 0x081c, 0x081d, 0x081e, 0x081f, 0x0820, 0x0821, 0x0822, 0x0823, 0x0825, 0x0826, 0x0827, 0x0829, 0x082a,
0x082b, 0x082c, 0x082d, 0x0951, 0x0953, 0x0954, 0x0f82, 0x0f83, 0x0f86, 0x0f87, 0x135d, 0x135e, 0x135f, 0x17dd,
0x193a, 0x1a17, 0x1a75, 0x1a76, 0x1a77, 0x1a78, 0x1a79, 0x1a7a, 0x1a7b, 0x1a7c, 0x1b6b, 0x1b6d, 0x1b6e, 0x1b6f,
0x1b70, 0x1b71, 0x1b72, 0x1b73, 0x1cd0, 0x1cd1, 0x1cd2, 0x1cda, 0x1cdb, 0x1ce0, 0x1dc0, 0x1dc1, 0x1dc3, 0x1dc4,
0x1dc5, 0x1dc6, 0x1dc7, 0x1dc8, 0x1dc9, 0x1dcb, 0x1dcc, 0x1dd1, 0x1dd2, 0x1dd3, 0x1dd4, 0x1dd5, 0x1dd6, 0x1dd7,
0x1dd8, 0x1dd9, 0x1dda, 0x1ddb, 0x1ddc, 0x1ddd, 0x1dde, 0x1ddf, 0x1de0, 0x1de1, 0x1de2, 0x1de3, 0x1de4, 0x1de5,
0x1de6, 0x1dfe, 0x20d0, 0x20d1, 0x20d4, 0x20d5, 0x20d6, 0x20d7, 0x20db, 0x20dc, 0x20e1, 0x20e7, 0x20e9, 0x20f0,
0x2cef, 0x2cf0, 0x2cf1, 0x2de0, 0x2de1, 0x2de2, 0x2de3, 0x2de4, 0x2de5, 0x2de6, 0x2de7, 0x2de8, 0x2de9, 0x2dea,
0x2deb, 0x2dec, 0x2ded, 0x2dee, 0x2def, 0x2df0, 0x2df1, 0x2df2, 0x2df3, 0x2df4, 0x2df5, 0x2df6, 0x2df7, 0x2df8,
0x2df9, 0x2dfa, 0x2dfb, 0x2dfc, 0x2dfd, 0x2dfe, 0x2dff, 0xa66f, 0xa67c, 0xa67d, 0xa6f0, 0xa6f1, 0xa8e0, 0xa8e1,
0xa8e2, 0xa8e3, 0xa8e4, 0xa8e5, 0xa8e6, 0xa8e7, 0xa8e8, 0xa8e9, 0xa8ea, 0xa8eb, 0xa8ec, 0xa8ed, 0xa8ee, 0xa8ef,
0xa8f0, 0xa8f1, 0xaab0, 0xaab2, 0xaab3, 0xaab7, 0xaab8, 0xaabe, 0xaabf, 0xaac1, 0xfe20, 0xfe21, 0xfe22, 0xfe23,
0xfe24, 0xfe25, 0xfe26, 0x10a0f, 0x10a38, 0x1d185, 0x1d186, 0x1d187, 0x1d188, 0x1d189, 0x1d1aa, 0x1d1ab, 0x1d1ac,
0x1d1ad, 0x1d242, 0x1d243, 0x1d244,
];
10 changes: 7 additions & 3 deletions packages/tui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,33 @@ export { StdinBuffer, type StdinBufferEventMap, type StdinBufferOptions } from "
export { ProcessTerminal, type Terminal } from "./terminal.js";
// Terminal image support
export {
allocateImageId,
type CellDimensions,
calculateImageRows,
deleteAllKittyImages,
deleteKittyImage,
detectCapabilities,
encodeITerm2,
encodeKitty,
generatePlaceholderRows,
getCapabilities,
getCellDimensions,
getGifDimensions,
getImageDimensions,
getJpegDimensions,
getNextImageId,
getPngDimensions,
getWebpDimensions,
type ImageDimensions,
type ImageProtocol,
type ImageRenderOptions,
type ImageRenderResult,
imageFallback,
isInsideTmux,
isTmuxPassthroughEnabled,
renderImage,
resetCapabilitiesCache,
resetTmuxPassthroughCache,
setCellDimensions,
type TerminalCapabilities,
wrapTmuxPassthrough,
} from "./terminal-image.js";
export {
type Component,
Expand Down
Loading