chore(vendor): import pi 0.85.1
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: d981de1229ef899957bbe968bc8dcda02a21f477
This commit is contained in:
127
third_party/pi/packages/tui/src/components/image.ts
vendored
Normal file
127
third_party/pi/packages/tui/src/components/image.ts
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
import {
|
||||
allocateImageId,
|
||||
getCapabilities,
|
||||
getCellDimensions,
|
||||
getImageDimensions,
|
||||
type ImageDimensions,
|
||||
imageFallback,
|
||||
renderImage,
|
||||
} from "../terminal-image.ts";
|
||||
import type { Component } from "../tui.ts";
|
||||
import { truncateToWidth } from "../utils.ts";
|
||||
|
||||
export interface ImageTheme {
|
||||
fallbackColor: (str: string) => string;
|
||||
}
|
||||
|
||||
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 {
|
||||
private base64Data: string;
|
||||
private mimeType: string;
|
||||
private dimensions: ImageDimensions;
|
||||
private theme: ImageTheme;
|
||||
private options: ImageOptions;
|
||||
private imageId?: number;
|
||||
|
||||
private cachedLines?: string[];
|
||||
private cachedWidth?: number;
|
||||
|
||||
constructor(
|
||||
base64Data: string,
|
||||
mimeType: string,
|
||||
theme: ImageTheme,
|
||||
options: ImageOptions = {},
|
||||
dimensions?: ImageDimensions,
|
||||
) {
|
||||
this.base64Data = base64Data;
|
||||
this.mimeType = mimeType;
|
||||
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;
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
if (this.cachedLines && this.cachedWidth === width) {
|
||||
return this.cachedLines;
|
||||
}
|
||||
|
||||
const maxWidth = Math.max(1, Math.min(width - 2, this.options.maxWidthCells ?? 60));
|
||||
const cellDimensions = getCellDimensions();
|
||||
const defaultMaxHeight = Math.max(1, Math.ceil((maxWidth * cellDimensions.widthPx) / cellDimensions.heightPx));
|
||||
const maxHeight = this.options.maxHeightCells ?? defaultMaxHeight;
|
||||
|
||||
const caps = getCapabilities();
|
||||
let lines: string[];
|
||||
|
||||
if (caps.images) {
|
||||
if (caps.images === "kitty" && this.imageId === undefined) {
|
||||
this.imageId = allocateImageId();
|
||||
}
|
||||
const result = renderImage(this.base64Data, this.dimensions, {
|
||||
maxWidthCells: maxWidth,
|
||||
maxHeightCells: maxHeight,
|
||||
imageId: this.imageId,
|
||||
moveCursor: false,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
// Store the image ID for later cleanup
|
||||
if (result.imageId) {
|
||||
this.imageId = result.imageId;
|
||||
}
|
||||
|
||||
if (caps.images === "kitty") {
|
||||
// For Kitty: C=1 prevents cursor movement.
|
||||
// Don't need the cursor movement.
|
||||
lines = [result.sequence];
|
||||
|
||||
// Return `rows` lines so TUI accounts for image height.
|
||||
for (let i = 0; i < result.rows - 1; i++) {
|
||||
lines.push("");
|
||||
}
|
||||
} else {
|
||||
// Return `rows` lines so TUI accounts for image height.
|
||||
// First (rows-1) lines are empty and cleared before the image is drawn.
|
||||
// Last line: move cursor back up, draw the image, then move back down
|
||||
// so TUI cursor accounting stays inside the scroll area.
|
||||
lines = [];
|
||||
for (let i = 0; i < result.rows - 1; i++) {
|
||||
lines.push("");
|
||||
}
|
||||
const rowOffset = result.rows - 1;
|
||||
const moveUp = rowOffset > 0 ? `\x1b[${rowOffset}A` : "";
|
||||
lines.push(moveUp + result.sequence);
|
||||
}
|
||||
} else {
|
||||
const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
|
||||
lines = [truncateToWidth(this.theme.fallbackColor(fallback), width)];
|
||||
}
|
||||
} else {
|
||||
const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
|
||||
lines = [truncateToWidth(this.theme.fallbackColor(fallback), width)];
|
||||
}
|
||||
|
||||
this.cachedLines = lines;
|
||||
this.cachedWidth = width;
|
||||
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user