42a05f7b09
- Changed transcript commit tracking from a volatile boolean to a cooldown counter that decays over clean frames. - Normalized row equality checks to ignore ANSI and trailing-space noise so equivalent renders do not trigger rewrites. - Removed the Bun test-runtime exception from Ghostty image paint deferral so normal delay logic always runs.
154 lines
4.7 KiB
TypeScript
154 lines
4.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
import { Image } from "@oh-my-pi/pi-tui/components/image";
|
|
import {
|
|
type CellDimensions,
|
|
getCellDimensions,
|
|
ImageProtocol,
|
|
isWindowsTerminalPreviewSixelSupported,
|
|
renderImage,
|
|
setCellDimensions,
|
|
TERMINAL,
|
|
} from "@oh-my-pi/pi-tui/terminal-capabilities";
|
|
|
|
type MutableTerminalInfo = {
|
|
imageProtocol: ImageProtocol | null;
|
|
};
|
|
|
|
const terminal = TERMINAL as unknown as MutableTerminalInfo;
|
|
const BASE64_DUMMY = "AA==";
|
|
const SQUARE_DIMENSIONS = { widthPx: 100, heightPx: 100 };
|
|
const BASE64_ONE_PIXEL_PNG =
|
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNgAAAAAgABSK+kcQAAAABJRU5ErkJggg==";
|
|
|
|
function parseKittyParam(sequence: string, key: "c" | "r"): number | null {
|
|
const match = sequence.match(new RegExp(`${key}=(\\d+)`));
|
|
if (!match) return null;
|
|
return Number.parseInt(match[1], 10);
|
|
}
|
|
|
|
function parseITermWidth(sequence: string): string | null {
|
|
const match = sequence.match(/width=([^;:]+)/);
|
|
return match?.[1] ?? null;
|
|
}
|
|
|
|
describe("terminal image rendering", () => {
|
|
const originalProtocol = TERMINAL.imageProtocol;
|
|
let originalCellDims: CellDimensions;
|
|
|
|
beforeEach(() => {
|
|
originalCellDims = { ...getCellDimensions() };
|
|
setCellDimensions({ widthPx: 10, heightPx: 10 });
|
|
terminal.imageProtocol = null;
|
|
});
|
|
|
|
afterEach(() => {
|
|
setCellDimensions(originalCellDims);
|
|
terminal.imageProtocol = originalProtocol;
|
|
});
|
|
|
|
it("fits Kitty images within max width and max height while preserving aspect ratio", () => {
|
|
terminal.imageProtocol = ImageProtocol.Kitty;
|
|
const result = renderImage(BASE64_DUMMY, SQUARE_DIMENSIONS, {
|
|
maxWidthCells: 10,
|
|
maxHeightCells: 2,
|
|
});
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.rows).toBe(2);
|
|
expect(parseKittyParam(result?.sequence ?? "", "c")).toBe(2);
|
|
expect(parseKittyParam(result?.sequence ?? "", "r")).toBe(2);
|
|
});
|
|
|
|
it("uses intrinsic image size when no bounds are provided", () => {
|
|
terminal.imageProtocol = ImageProtocol.Kitty;
|
|
const result = renderImage(BASE64_DUMMY, SQUARE_DIMENSIONS);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.rows).toBe(10);
|
|
expect(parseKittyParam(result?.sequence ?? "", "c")).toBe(10);
|
|
expect(parseKittyParam(result?.sequence ?? "", "r")).toBe(10);
|
|
});
|
|
|
|
it("transmits stable Kitty images in-band before placement", () => {
|
|
terminal.imageProtocol = ImageProtocol.Kitty;
|
|
const result = renderImage(BASE64_ONE_PIXEL_PNG, SQUARE_DIMENSIONS, {
|
|
imageId: 42,
|
|
includeTransmit: true,
|
|
});
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.transmit).toBe(`\x1b_Ga=t,f=100,q=2,i=42;${BASE64_ONE_PIXEL_PNG}\x1b\\`);
|
|
expect(result?.transmit).not.toContain("t=t");
|
|
});
|
|
|
|
it("reduces iTerm2 width when max height is the limiting bound", () => {
|
|
terminal.imageProtocol = ImageProtocol.Iterm2;
|
|
const result = renderImage(BASE64_DUMMY, SQUARE_DIMENSIONS, {
|
|
maxWidthCells: 10,
|
|
maxHeightCells: 2,
|
|
});
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.rows).toBe(2);
|
|
expect(parseITermWidth(result?.sequence ?? "")).toBe("2");
|
|
expect(result?.sequence).toContain("height=auto");
|
|
});
|
|
|
|
it("encodes SIXEL output when protocol is SIXEL", () => {
|
|
terminal.imageProtocol = ImageProtocol.Sixel;
|
|
const result = renderImage(BASE64_ONE_PIXEL_PNG, SQUARE_DIMENSIONS, {
|
|
maxWidthCells: 10,
|
|
maxHeightCells: 2,
|
|
});
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(result?.rows).toBe(2);
|
|
expect((result?.sequence ?? "").startsWith("\x1bP")).toBe(true);
|
|
});
|
|
|
|
it("Image component forwards maxHeightCells to terminal rendering", () => {
|
|
terminal.imageProtocol = ImageProtocol.Kitty;
|
|
const image = new Image(
|
|
BASE64_DUMMY,
|
|
"image/png",
|
|
{ fallbackColor: text => text },
|
|
{ maxWidthCells: 10, maxHeightCells: 2 },
|
|
SQUARE_DIMENSIONS,
|
|
);
|
|
|
|
const lines = image.render(20);
|
|
|
|
expect(lines[0]).toBe("\x1b[0m");
|
|
expect(lines).toHaveLength(2);
|
|
expect(lines[1]).toContain("\x1b[1A");
|
|
expect(lines[1]).toContain("c=2");
|
|
expect(lines[1]).toContain("r=2");
|
|
});
|
|
});
|
|
|
|
describe("Windows Terminal Preview SIXEL detection", () => {
|
|
it("requires Windows platform, WT session, and known version 1.22+", () => {
|
|
expect(
|
|
isWindowsTerminalPreviewSixelSupported(
|
|
{ WT_SESSION: "1", TERM_PROGRAM: "Windows_Terminal", TERM_PROGRAM_VERSION: "1.22.2362.0" },
|
|
"win32",
|
|
),
|
|
).toBe(true);
|
|
expect(
|
|
isWindowsTerminalPreviewSixelSupported(
|
|
{ WT_SESSION: "1", TERM_PROGRAM: "Windows_Terminal", TERM_PROGRAM_VERSION: "1.21.0.0" },
|
|
"win32",
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
isWindowsTerminalPreviewSixelSupported({ WT_SESSION: "1", TERM_PROGRAM: "Windows_Terminal" }, "win32"),
|
|
).toBe(false);
|
|
expect(
|
|
isWindowsTerminalPreviewSixelSupported(
|
|
{ WT_SESSION: "1", TERM_PROGRAM: "Windows_Terminal", TERM_PROGRAM_VERSION: "1.22.2362.0" },
|
|
"linux",
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|