mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 02:03:16 +00:00
- allocateImageId() now returns random IDs instead of sequential - Static images no longer auto-allocate IDs (transient display) - Only explicit imageId usage (like DOSBox) gets tracked IDs - Suppress emulators exit logging in DOSBox dispose Fixes image replacement bug when extension and main app both allocated sequential IDs starting at 1.
104 lines
2.7 KiB
TypeScript
104 lines
2.7 KiB
TypeScript
import {
|
|
getCapabilities,
|
|
getImageDimensions,
|
|
type ImageDimensions,
|
|
imageFallback,
|
|
renderImage,
|
|
} from "../terminal-image.js";
|
|
import type { Component } from "../tui.js";
|
|
|
|
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.min(width - 2, this.options.maxWidthCells ?? 60);
|
|
|
|
const caps = getCapabilities();
|
|
let lines: string[];
|
|
|
|
if (caps.images) {
|
|
const result = renderImage(this.base64Data, this.dimensions, {
|
|
maxWidthCells: maxWidth,
|
|
imageId: this.imageId,
|
|
});
|
|
|
|
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("");
|
|
}
|
|
// 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)];
|
|
}
|
|
} else {
|
|
const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
|
|
lines = [this.theme.fallbackColor(fallback)];
|
|
}
|
|
|
|
this.cachedLines = lines;
|
|
this.cachedWidth = width;
|
|
|
|
return lines;
|
|
}
|
|
}
|