This repository has been archived on 2026-03-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
openclaw-extensions/wecom/node_modules/@mariozechner/pi-tui/dist/terminal-image.js
T

288 lines
9.9 KiB
JavaScript

let cachedCapabilities = null;
// Default cell dimensions - updated by TUI when terminal responds to query
let cellDimensions = { widthPx: 9, heightPx: 18 };
export function getCellDimensions() {
return cellDimensions;
}
export function setCellDimensions(dims) {
cellDimensions = dims;
}
export function detectCapabilities() {
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
const term = process.env.TERM?.toLowerCase() || "";
const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
return { images: "kitty", trueColor: true, hyperlinks: true };
}
if (termProgram === "ghostty" || term.includes("ghostty") || process.env.GHOSTTY_RESOURCES_DIR) {
return { images: "kitty", trueColor: true, hyperlinks: true };
}
if (process.env.WEZTERM_PANE || termProgram === "wezterm") {
return { images: "kitty", trueColor: true, hyperlinks: true };
}
if (process.env.ITERM_SESSION_ID || termProgram === "iterm.app") {
return { images: "iterm2", trueColor: true, hyperlinks: true };
}
if (termProgram === "vscode") {
return { images: null, trueColor: true, hyperlinks: true };
}
if (termProgram === "alacritty") {
return { images: null, trueColor: true, hyperlinks: true };
}
const trueColor = colorTerm === "truecolor" || colorTerm === "24bit";
return { images: null, trueColor, hyperlinks: true };
}
export function getCapabilities() {
if (!cachedCapabilities) {
cachedCapabilities = detectCapabilities();
}
return cachedCapabilities;
}
export function resetCapabilitiesCache() {
cachedCapabilities = null;
}
const KITTY_PREFIX = "\x1b_G";
const ITERM2_PREFIX = "\x1b]1337;File=";
export function isImageLine(line) {
// Fast path: sequence at line start (single-row images)
if (line.startsWith(KITTY_PREFIX) || line.startsWith(ITERM2_PREFIX)) {
return true;
}
// Slow path: sequence elsewhere (multi-row images have cursor-up prefix)
return line.includes(KITTY_PREFIX) || line.includes(ITERM2_PREFIX);
}
/**
* Generate a random image ID for Kitty graphics protocol.
* Uses random IDs to avoid collisions between different module instances
* (e.g., main app vs extensions).
*/
export function allocateImageId() {
// Use random ID in range [1, 0xffffffff] to avoid collisions
return Math.floor(Math.random() * 0xfffffffe) + 1;
}
export function encodeKitty(base64Data, options = {}) {
const CHUNK_SIZE = 4096;
const params = ["a=T", "f=100", "q=2"];
if (options.columns)
params.push(`c=${options.columns}`);
if (options.rows)
params.push(`r=${options.rows}`);
if (options.imageId)
params.push(`i=${options.imageId}`);
if (base64Data.length <= CHUNK_SIZE) {
return `\x1b_G${params.join(",")};${base64Data}\x1b\\`;
}
const chunks = [];
let offset = 0;
let isFirst = true;
while (offset < base64Data.length) {
const chunk = base64Data.slice(offset, offset + CHUNK_SIZE);
const isLast = offset + CHUNK_SIZE >= base64Data.length;
if (isFirst) {
chunks.push(`\x1b_G${params.join(",")},m=1;${chunk}\x1b\\`);
isFirst = false;
}
else if (isLast) {
chunks.push(`\x1b_Gm=0;${chunk}\x1b\\`);
}
else {
chunks.push(`\x1b_Gm=1;${chunk}\x1b\\`);
}
offset += CHUNK_SIZE;
}
return chunks.join("");
}
/**
* Delete a Kitty graphics image by ID.
* Uses uppercase 'I' to also free the image data.
*/
export function deleteKittyImage(imageId) {
return `\x1b_Ga=d,d=I,i=${imageId}\x1b\\`;
}
/**
* Delete all visible Kitty graphics images.
* Uses uppercase 'A' to also free the image data.
*/
export function deleteAllKittyImages() {
return `\x1b_Ga=d,d=A\x1b\\`;
}
export function encodeITerm2(base64Data, options = {}) {
const params = [`inline=${options.inline !== false ? 1 : 0}`];
if (options.width !== undefined)
params.push(`width=${options.width}`);
if (options.height !== undefined)
params.push(`height=${options.height}`);
if (options.name) {
const nameBase64 = Buffer.from(options.name).toString("base64");
params.push(`name=${nameBase64}`);
}
if (options.preserveAspectRatio === false) {
params.push("preserveAspectRatio=0");
}
return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
}
export function calculateImageRows(imageDimensions, targetWidthCells, cellDimensions = { widthPx: 9, heightPx: 18 }) {
const targetWidthPx = targetWidthCells * cellDimensions.widthPx;
const scale = targetWidthPx / imageDimensions.widthPx;
const scaledHeightPx = imageDimensions.heightPx * scale;
const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);
return Math.max(1, rows);
}
export function getPngDimensions(base64Data) {
try {
const buffer = Buffer.from(base64Data, "base64");
if (buffer.length < 24) {
return null;
}
if (buffer[0] !== 0x89 || buffer[1] !== 0x50 || buffer[2] !== 0x4e || buffer[3] !== 0x47) {
return null;
}
const width = buffer.readUInt32BE(16);
const height = buffer.readUInt32BE(20);
return { widthPx: width, heightPx: height };
}
catch {
return null;
}
}
export function getJpegDimensions(base64Data) {
try {
const buffer = Buffer.from(base64Data, "base64");
if (buffer.length < 2) {
return null;
}
if (buffer[0] !== 0xff || buffer[1] !== 0xd8) {
return null;
}
let offset = 2;
while (offset < buffer.length - 9) {
if (buffer[offset] !== 0xff) {
offset++;
continue;
}
const marker = buffer[offset + 1];
if (marker >= 0xc0 && marker <= 0xc2) {
const height = buffer.readUInt16BE(offset + 5);
const width = buffer.readUInt16BE(offset + 7);
return { widthPx: width, heightPx: height };
}
if (offset + 3 >= buffer.length) {
return null;
}
const length = buffer.readUInt16BE(offset + 2);
if (length < 2) {
return null;
}
offset += 2 + length;
}
return null;
}
catch {
return null;
}
}
export function getGifDimensions(base64Data) {
try {
const buffer = Buffer.from(base64Data, "base64");
if (buffer.length < 10) {
return null;
}
const sig = buffer.slice(0, 6).toString("ascii");
if (sig !== "GIF87a" && sig !== "GIF89a") {
return null;
}
const width = buffer.readUInt16LE(6);
const height = buffer.readUInt16LE(8);
return { widthPx: width, heightPx: height };
}
catch {
return null;
}
}
export function getWebpDimensions(base64Data) {
try {
const buffer = Buffer.from(base64Data, "base64");
if (buffer.length < 30) {
return null;
}
const riff = buffer.slice(0, 4).toString("ascii");
const webp = buffer.slice(8, 12).toString("ascii");
if (riff !== "RIFF" || webp !== "WEBP") {
return null;
}
const chunk = buffer.slice(12, 16).toString("ascii");
if (chunk === "VP8 ") {
if (buffer.length < 30)
return null;
const width = buffer.readUInt16LE(26) & 0x3fff;
const height = buffer.readUInt16LE(28) & 0x3fff;
return { widthPx: width, heightPx: height };
}
else if (chunk === "VP8L") {
if (buffer.length < 25)
return null;
const bits = buffer.readUInt32LE(21);
const width = (bits & 0x3fff) + 1;
const height = ((bits >> 14) & 0x3fff) + 1;
return { widthPx: width, heightPx: height };
}
else if (chunk === "VP8X") {
if (buffer.length < 30)
return null;
const width = (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) + 1;
const height = (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) + 1;
return { widthPx: width, heightPx: height };
}
return null;
}
catch {
return null;
}
}
export function getImageDimensions(base64Data, mimeType) {
if (mimeType === "image/png") {
return getPngDimensions(base64Data);
}
if (mimeType === "image/jpeg") {
return getJpegDimensions(base64Data);
}
if (mimeType === "image/gif") {
return getGifDimensions(base64Data);
}
if (mimeType === "image/webp") {
return getWebpDimensions(base64Data);
}
return null;
}
export function renderImage(base64Data, imageDimensions, options = {}) {
const caps = getCapabilities();
if (!caps.images) {
return null;
}
const maxWidth = options.maxWidthCells ?? 80;
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 });
return { sequence, rows, imageId: options.imageId };
}
if (caps.images === "iterm2") {
const sequence = encodeITerm2(base64Data, {
width: maxWidth,
height: "auto",
preserveAspectRatio: options.preserveAspectRatio ?? true,
});
return { sequence, rows };
}
return null;
}
export function imageFallback(mimeType, dimensions, filename) {
const parts = [];
if (filename)
parts.push(filename);
parts.push(`[${mimeType}]`);
if (dimensions)
parts.push(`${dimensions.widthPx}x${dimensions.heightPx}`);
return `[Image: ${parts.join(" ")}]`;
}
//# sourceMappingURL=terminal-image.js.map