diff --git a/docs/tui-runtime-internals.md b/docs/tui-runtime-internals.md index c4fb02030..20beca245 100644 --- a/docs/tui-runtime-internals.md +++ b/docs/tui-runtime-internals.md @@ -56,7 +56,7 @@ A forced render (`requestRender(true)`) queues a viewport repaint or explicit se 4. Creates a `StdinBuffer` to split partial escape chunks into complete sequences. 5. Queries Kitty keyboard protocol support (`CSI ? u`), then enables protocol flags if supported; otherwise enables modifyOtherKeys fallback after a short timeout. 6. Queries OSC 11 background color and Mode 2031 appearance notifications for dark/light theme detection. -7. Queries OSC 99 notification capabilities and Kitty temp-file graphics support. +7. Queries OSC 99 notification capabilities. 8. Starts periodic OSC 11 polling only where safe, then probes DEC private modes 2026/2048/2031 via DECRQM. `StdinBuffer` behavior: diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 99cb1fe28..33928af46 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -27,7 +27,7 @@ export * from "./fuzzy"; export * from "./keybindings"; // Kitty keyboard protocol helpers export * from "./keys"; -// Kitty graphics: Unicode placeholders + temp-file transmission +// Kitty graphics: Unicode placeholders export * from "./kitty-graphics"; // Mermaid diagram support // Input buffering for batch splitting diff --git a/packages/tui/src/kitty-graphics.ts b/packages/tui/src/kitty-graphics.ts index 8b65bf5d8..0b403d217 100644 --- a/packages/tui/src/kitty-graphics.ts +++ b/packages/tui/src/kitty-graphics.ts @@ -1,6 +1,6 @@ /** - * Kitty graphics: Unicode placeholder placement (`U=1` + U+10EEEE) and temp-file - * (`t=t`) image transmission, with runtime feature state and env overrides. + * Kitty graphics: Unicode placeholder placement (`U=1` + U+10EEEE), with + * runtime feature state and env overrides. * * Unicode placeholders let a transmitted image be displayed by writing ordinary * text cells — the placeholder char U+10EEEE plus row/column combining @@ -14,10 +14,6 @@ * dependency stays one-way (capabilities → kitty-graphics) and no import cycle * forms. Protocol gating (`imageProtocol === Kitty`) lives in the caller. */ -import * as fs from "node:fs"; -import * as os from "node:os"; -import * as path from "node:path"; -import { $env, logger } from "@oh-my-pi/pi-utils"; /** Kitty Unicode placeholder base character (U+10EEEE, Plane 16 PUA). */ export const KITTY_PLACEHOLDER = "\u{10eeee}"; @@ -55,25 +51,9 @@ const ROWCOLUMN_DIACRITICS: readonly number[] = [ /** Largest row/column index expressible with the diacritic table (one cell each). */ export const KITTY_PLACEHOLDER_MAX_CELLS = ROWCOLUMN_DIACRITICS.length; -/** A minimal opaque 1x1 PNG (base64) used for the temp-file support probe. */ -const PROBE_PNG_BASE64 = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - -export type KittyTransmissionMedium = "direct" | "temp-file"; - export interface KittyGraphicsFeatures { /** Display images via Unicode placeholders instead of direct `a=p` placement. */ unicodePlaceholders: boolean; - /** How image data reaches the terminal: in-band base64 or a temp file. */ - transmissionMedium: KittyTransmissionMedium; -} - -/** Explicit transmission medium from `PI_KITTY_IMAGE_TRANSMISSION`, else "auto". */ -function transmissionOverride(): KittyTransmissionMedium | "auto" { - const raw = $env.PI_KITTY_IMAGE_TRANSMISSION?.trim().toLowerCase(); - if (raw === "temp-file") return "temp-file"; - if (raw === "direct") return "direct"; - return "auto"; } /** @@ -106,8 +86,6 @@ let features: KittyGraphicsFeatures = { // Off until `terminal-capabilities` seeds it from the detected terminal id — // the default-on path corrupts wezterm and tmux-passthrough sessions. unicodePlaceholders: false, - // Start direct; a successful probe (or explicit `temp-file` override) promotes. - transmissionMedium: transmissionOverride() === "temp-file" ? "temp-file" : "direct", }; export function getKittyGraphics(): Readonly { @@ -118,29 +96,11 @@ export function setKittyGraphics(partial: Partial): void features = { ...features, ...partial }; } -/** - * Whether temp-file transmission may be promoted at runtime: forced via env, - * disabled via env, otherwise auto (local sessions only — a temp file written - * locally is not readable by a terminal on the far side of an SSH link). - */ -export function kittyTempFileAllowed(): boolean { - const override = transmissionOverride(); - if (override === "temp-file") return true; - if (override === "direct") return false; - // Auto: local sessions only — a temp file written here is not on the SSH peer. - return !$env.SSH_CONNECTION && !$env.SSH_CLIENT && !$env.SSH_TTY; -} - /** Whether a `columns`×`rows` placeholder grid fits within the diacritic table. */ export function kittyPlaceholdersFit(columns: number, rows: number): boolean { return columns >= 1 && rows >= 1 && columns <= KITTY_PLACEHOLDER_MAX_CELLS && rows <= KITTY_PLACEHOLDER_MAX_CELLS; } -/** True when the base64 payload is a PNG (kitty `f=100` / temp-file path only). */ -export function isPngBase64(base64Data: string): boolean { - return base64Data.startsWith("iVBORw0KGgo"); -} - function diacritic(index: number): string { const cp = ROWCOLUMN_DIACRITICS[index]; return cp === undefined ? "" : String.fromCodePoint(cp); @@ -209,62 +169,3 @@ export function renderKittyPlaceholderLines(opts: { } return grid; } - -/** - * Path for a temp-file transmission. Kitty deletes a `t=t` file after reading it - * **only** when the path contains the substring `tty-graphics-protocol`, so it is - * embedded in the filename to keep the temp dir self-cleaning. - */ -function tempGraphicsPath(tag: string): string { - return path.join(os.tmpdir(), `tty-graphics-protocol-${tag}-${process.pid}-${Date.now()}.png`); -} - -/** - * Transmit a PNG via a temp file (`t=t`): decode the base64 to bytes once, write - * them to a temp file, and send the base64-encoded file path as payload. Returns - * the APC string, or `null` on any failure (caller falls back to direct base64). - * - * Synchronous filesystem writes are mandated by the synchronous render pipeline - * (`Image.render` → `renderImage` are sync); there is no async seam here. - */ -export function encodeKittyTempFileTransmit(base64Png: string, imageId: number): string | null { - try { - const bytes = Buffer.from(base64Png, "base64"); - if (bytes.length === 0) return null; - const file = tempGraphicsPath(`i${imageId}`); - fs.writeFileSync(file, bytes); - const encodedPath = Buffer.from(file, "utf8").toString("base64"); - return `\x1b_Ga=t,f=100,t=t,S=${bytes.length},q=2,i=${imageId};${encodedPath}\x1b\\`; - } catch (err) { - logger.debug("Kitty temp-file transmit failed; using direct base64", { err: String(err) }); - return null; - } -} - -/** - * Encode a temp-file support probe: write a tiny PNG to a temp file and ask the - * terminal to query it (`a=q,t=t`). A conforming terminal replies - * `ESC _ G i=;OK ESC \`. Returns the query APC plus a `cleanup` that - * removes the probe file (best-effort; kitty self-deletes the magic-named file). - * Returns `null` if the temp file cannot be written. - */ -export function encodeKittyTempFileProbe(probeId: number): { sequence: string; cleanup: () => void } | null { - try { - const bytes = Buffer.from(PROBE_PNG_BASE64, "base64"); - const file = tempGraphicsPath(`probe${probeId}`); - fs.writeFileSync(file, bytes); - const encodedPath = Buffer.from(file, "utf8").toString("base64"); - const sequence = `\x1b_Ga=q,t=t,f=100,S=${bytes.length},q=2,i=${probeId};${encodedPath}\x1b\\`; - const cleanup = () => { - try { - fs.rmSync(file, { force: true }); - } catch { - // Best effort; kitty deletes tty-graphics-protocol files itself. - } - }; - return { sequence, cleanup }; - } catch (err) { - logger.debug("Kitty temp-file probe setup failed", { err: String(err) }); - return null; - } -} diff --git a/packages/tui/src/terminal-capabilities.ts b/packages/tui/src/terminal-capabilities.ts index 9324386a3..dfea3bd4a 100644 --- a/packages/tui/src/terminal-capabilities.ts +++ b/packages/tui/src/terminal-capabilities.ts @@ -2,9 +2,7 @@ import { encodeSixel } from "@oh-my-pi/pi-natives"; import { $env, isBunTestRuntime } from "@oh-my-pi/pi-utils"; import { detectKittyUnicodePlaceholdersSupport, - encodeKittyTempFileTransmit, getKittyGraphics, - isPngBase64, KITTY_PLACEHOLDER, kittyPlaceholdersFit, renderKittyPlaceholderLines, @@ -807,16 +805,11 @@ export function renderImage( if (options.imageId != null) { const placementId = options.placementId ?? options.imageId; const graphics = getKittyGraphics(); - // Transmit-once (keyed by id). Prefer a local temp file for PNGs when the - // medium has been promoted; otherwise send in-band base64. Repaints reuse - // the stored image, so the transmit is only emitted when requested. + // Transmit-once (keyed by id). Repaints reuse the stored image, so the + // transmit is only emitted when requested. let transmit: string | undefined; if (options.includeTransmit) { - const tempFile = - graphics.transmissionMedium === "temp-file" && isPngBase64(base64Data) - ? encodeKittyTempFileTransmit(base64Data, options.imageId) - : null; - transmit = tempFile ?? encodeKittyTransmit(base64Data, options.imageId); + transmit = encodeKittyTransmit(base64Data, options.imageId); } // Unicode placeholders render the image as real text cells (which survive // horizontal slicing, reflow and overlaps) instead of a cursor-positioned diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index b7c8db2e9..8f8ac4337 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -2,9 +2,8 @@ import { dlopen, FFIType, ptr } from "bun:ffi"; import * as fs from "node:fs"; import { $env, isBunTestRuntime, logger } from "@oh-my-pi/pi-utils"; import { setKittyProtocolActive } from "./keys"; -import { encodeKittyTempFileProbe, getKittyGraphics, kittyTempFileAllowed, setKittyGraphics } from "./kitty-graphics"; import { StdinBuffer } from "./stdin-buffer"; -import { ImageProtocol, NotifyProtocol, setCellDimensions, setOsc99Supported, TERMINAL } from "./terminal-capabilities"; +import { NotifyProtocol, setCellDimensions, setOsc99Supported, TERMINAL } from "./terminal-capabilities"; const TERMINAL_PROGRESS_KEEPALIVE_MS = 1000; const TERMINAL_PROGRESS_ACTIVE_SEQUENCE = "\x1b]9;4;3\x07"; @@ -159,11 +158,9 @@ type Da1SentinelOwner = | { kind: "keyboard" } | { kind: "osc11" } | { kind: "privateMode"; mode: number } - | { kind: "kittyGraphicsProbe"; id: number } | { kind: "osc99Probe"; id: string }; let nextOsc99ProbeId = 1; -let nextKittyGraphicsProbeId = 1; function parseOsc99KeyValues(section: string): Map { const values = new Map(); @@ -198,8 +195,6 @@ export class ProcessTerminal implements Terminal { #osc99PendingId: string | undefined; #osc99ResponseBuffer = ""; #osc99Capabilities = new Map(); - #kittyGraphicsPendingId: number | undefined; - #kittyGraphicsProbeCleanup: (() => void) | undefined; #privateCsiResponseBuffer = ""; #da1SentinelOwners: Da1SentinelOwner[] = []; /** Resolved DECRQM support per private mode (mode → supported). */ @@ -285,11 +280,6 @@ export class ProcessTerminal implements Terminal { // without leaking probe bytes to application input. this.#queryOsc99Support(); - // Probe Kitty temp-file (`t=t`) graphics transmission support. Rides the - // same DA1 sentinel FIFO; promotes the transmission medium to temp-file - // only on an explicit `OK`, so unsupported terminals stay on direct base64. - this.#queryKittyGraphicsTempFile(); - // Subscribe to Mode 2031 appearance change notifications. // When the terminal reports a change, we re-query OSC 11 to get the // actual background color (following Neovim convention) with 100ms debounce. @@ -508,9 +498,6 @@ export class ProcessTerminal implements Terminal { this.#resolveOsc99Support(owner.id, false); break; } - case "kittyGraphicsProbe": - this.#resolveKittyGraphicsTempFile(owner.id, false); - break; } return; } @@ -576,21 +563,6 @@ export class ProcessTerminal implements Terminal { } } - // Kitty graphics temp-file probe reply: ESC _ G i=;OK ESC \. The - // owner remains in the FIFO and is drained by its DA1 sentinel (no-op - // once resolved here). - if (this.#kittyGraphicsPendingId !== undefined && sequence.startsWith("\x1b_G")) { - const graphicsMatch = sequence.match(/^\x1b_G([^;]*);([\s\S]*?)\x1b\\$/u); - if (graphicsMatch) { - const idMatch = graphicsMatch[1]!.match(/(?:^|,)i=(\d+)(?:,|$)/); - const replyId = idMatch ? parseInt(idMatch[1]!, 10) : undefined; - if (replyId === this.#kittyGraphicsPendingId) { - this.#resolveKittyGraphicsTempFile(replyId, graphicsMatch[2]!.trim() === "OK"); - return; - } - } - } - // Mode 2031 change notification: re-query OSC 11 with 100ms debounce // (Neovim convention — coalesces rapid notifications during transitions) const appearanceMatch = sequence.match(appearanceDsrPattern); @@ -686,37 +658,6 @@ export class ProcessTerminal implements Terminal { setOsc99Supported(supported); } - #shouldQueryKittyGraphicsTempFile(): boolean { - if (TERMINAL.imageProtocol !== ImageProtocol.Kitty) return false; - // Honor the remote/explicit env gate, and skip when temp-file is already on. - if (!kittyTempFileAllowed() || getKittyGraphics().transmissionMedium === "temp-file") return false; - return !isBunTestRuntime() || $env.PI_TUI_KITTY_GRAPHICS_PROBE === "1"; - } - - #queryKittyGraphicsTempFile(): void { - this.#clearKittyGraphicsProbe(); - if (this.#dead || !this.#shouldQueryKittyGraphicsTempFile()) return; - - const id = nextKittyGraphicsProbeId++; - const probe = encodeKittyTempFileProbe(id); - if (!probe) return; - this.#kittyGraphicsPendingId = id; - this.#kittyGraphicsProbeCleanup = probe.cleanup; - this.#da1SentinelOwners.push({ kind: "kittyGraphicsProbe", id }); - this.#safeWrite(`${probe.sequence}\x1b[c`); - } - - #resolveKittyGraphicsTempFile(id: number, supported: boolean): void { - if (this.#kittyGraphicsPendingId !== id) return; - if (supported) setKittyGraphics({ transmissionMedium: "temp-file" }); - this.#clearKittyGraphicsProbe(); - } - - #clearKittyGraphicsProbe(): void { - this.#kittyGraphicsPendingId = undefined; - this.#kittyGraphicsProbeCleanup?.(); - this.#kittyGraphicsProbeCleanup = undefined; - } /** * Parse an OSC 11 background color response and compute BT.601 luminance. * Handles 1-, 2-, 3-, and 4-digit XParseColor hex components. @@ -965,7 +906,6 @@ export class ProcessTerminal implements Terminal { this.#osc99ResponseBuffer = ""; this.#osc99Capabilities.clear(); setOsc99Supported(false); - this.#clearKittyGraphicsProbe(); this.#privateCsiResponseBuffer = ""; this.#da1SentinelOwners.length = 0; this.#privateModeCallbacks = []; diff --git a/packages/tui/test/image-budget.test.ts b/packages/tui/test/image-budget.test.ts index 03d9cd9e9..608f9d6a4 100644 --- a/packages/tui/test/image-budget.test.ts +++ b/packages/tui/test/image-budget.test.ts @@ -259,7 +259,7 @@ describe("Image budget + Unicode placeholders", () => { originalCellDims = { ...getCellDimensions() }; setCellDimensions({ widthPx: 10, heightPx: 10 }); terminal.imageProtocol = ImageProtocol.Kitty; - setKittyGraphics({ unicodePlaceholders: true, transmissionMedium: "direct" }); + setKittyGraphics({ unicodePlaceholders: true }); }); afterEach(() => { diff --git a/packages/tui/test/image-render.test.ts b/packages/tui/test/image-render.test.ts index ce6cd2ad9..4358373a8 100644 --- a/packages/tui/test/image-render.test.ts +++ b/packages/tui/test/image-render.test.ts @@ -69,6 +69,18 @@ describe("terminal image rendering", () => { 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, { diff --git a/packages/tui/test/kitty-graphics.test.ts b/packages/tui/test/kitty-graphics.test.ts index b2e81fb6c..bab897e04 100644 --- a/packages/tui/test/kitty-graphics.test.ts +++ b/packages/tui/test/kitty-graphics.test.ts @@ -1,25 +1,17 @@ import { afterEach, describe, expect, it } from "bun:test"; -import * as fs from "node:fs"; import { visibleWidth } from "@oh-my-pi/pi-natives"; import { detectKittyUnicodePlaceholdersSupport, encodeKittyPlaceholderGrid, - encodeKittyTempFileProbe, - encodeKittyTempFileTransmit, encodeKittyVirtualPlacement, getKittyGraphics, - isPngBase64, KITTY_PLACEHOLDER, KITTY_PLACEHOLDER_MAX_CELLS, kittyPlaceholdersFit, - kittyTempFileAllowed, renderKittyPlaceholderLines, setKittyGraphics, } from "@oh-my-pi/pi-tui/kitty-graphics"; -const ONE_PIXEL_PNG = - "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; - const ORIGINAL = { ...getKittyGraphics() }; afterEach(() => { @@ -80,81 +72,12 @@ describe("kitty Unicode placeholder encoding", () => { }); }); -describe("kitty temp-file transmission", () => { - it("isPngBase64 recognizes PNG payloads only", () => { - expect(isPngBase64(ONE_PIXEL_PNG)).toBe(true); - expect(isPngBase64("/9j/4AAQSkZJRgABAQ")).toBe(false); // JPEG magic - expect(isPngBase64("")).toBe(false); - }); - - it("encodeKittyTempFileTransmit writes the bytes to a magic-named file and sends its path", () => { - const seq = encodeKittyTempFileTransmit(ONE_PIXEL_PNG, 12); - expect(seq).not.toBeNull(); - const match = seq!.match(/^\x1b_Ga=t,f=100,t=t,S=(\d+),q=2,i=12;([^\x1b]+)\x1b\\$/); - expect(match).not.toBeNull(); - const [, sizeRaw, encodedPath] = match!; - const filePath = Buffer.from(encodedPath!, "base64").toString("utf8"); - // Kitty only deletes temp files whose path contains this substring. - expect(filePath).toContain("tty-graphics-protocol"); - try { - const written = fs.readFileSync(filePath); - expect(written.equals(Buffer.from(ONE_PIXEL_PNG, "base64"))).toBe(true); - expect(Number(sizeRaw)).toBe(written.length); - } finally { - fs.rmSync(filePath, { force: true }); - } - }); - - it("encodeKittyTempFileProbe builds an a=q,t=t query and cleans up its file", () => { - const probe = encodeKittyTempFileProbe(99); - expect(probe).not.toBeNull(); - expect(probe!.sequence).toContain("\x1b_Ga=q,t=t,f=100,"); - expect(probe!.sequence).toContain("i=99;"); - const encodedPath = probe!.sequence.match(/;([^\x1b]+)\x1b\\$/)?.[1]; - const filePath = Buffer.from(encodedPath!, "base64").toString("utf8"); - expect(fs.existsSync(filePath)).toBe(true); - probe!.cleanup(); - expect(fs.existsSync(filePath)).toBe(false); - }); -}); - describe("kitty graphics feature state", () => { it("getKittyGraphics/setKittyGraphics round-trips overrides", () => { - setKittyGraphics({ unicodePlaceholders: false, transmissionMedium: "temp-file" }); - expect(getKittyGraphics()).toEqual({ unicodePlaceholders: false, transmissionMedium: "temp-file" }); + setKittyGraphics({ unicodePlaceholders: false }); + expect(getKittyGraphics()).toEqual({ unicodePlaceholders: false }); setKittyGraphics({ unicodePlaceholders: true }); expect(getKittyGraphics().unicodePlaceholders).toBe(true); - expect(getKittyGraphics().transmissionMedium).toBe("temp-file"); - }); - - it("kittyTempFileAllowed honors env force/off and the remote-session guard", () => { - const keys = ["PI_KITTY_IMAGE_TRANSMISSION", "SSH_CONNECTION", "SSH_CLIENT", "SSH_TTY"]; - const saved: Record = {}; - for (const k of keys) saved[k] = Bun.env[k]; - const restore = () => { - for (const k of keys) { - const v = saved[k]; - if (v === undefined) delete Bun.env[k]; - else Bun.env[k] = v; - } - }; - try { - for (const k of keys) delete Bun.env[k]; - // Auto + local session: allowed. - expect(kittyTempFileAllowed()).toBe(true); - // Auto + SSH session: not allowed. - Bun.env.SSH_CONNECTION = "1.2.3.4 5 6.7.8.9 22"; - expect(kittyTempFileAllowed()).toBe(false); - // Explicit force overrides the remote guard. - Bun.env.PI_KITTY_IMAGE_TRANSMISSION = "temp-file"; - expect(kittyTempFileAllowed()).toBe(true); - // Explicit off wins even on a local session. - delete Bun.env.SSH_CONNECTION; - Bun.env.PI_KITTY_IMAGE_TRANSMISSION = "direct"; - expect(kittyTempFileAllowed()).toBe(false); - } finally { - restore(); - } }); }); diff --git a/packages/tui/test/terminal-appearance.test.ts b/packages/tui/test/terminal-appearance.test.ts index 03bd9bbee..88712e02c 100644 --- a/packages/tui/test/terminal-appearance.test.ts +++ b/packages/tui/test/terminal-appearance.test.ts @@ -1,14 +1,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "bun:test"; -import { getKittyGraphics, type KittyGraphicsFeatures, setKittyGraphics } from "@oh-my-pi/pi-tui/kitty-graphics"; import { ProcessTerminal } from "@oh-my-pi/pi-tui/terminal"; import { type CellDimensions, getCellDimensions, getTerminalInfo, - ImageProtocol, setCellDimensions, - setTerminalImageProtocol, - TERMINAL, } from "@oh-my-pi/pi-tui/terminal-capabilities"; const stdinIsTtyDescriptor = Object.getOwnPropertyDescriptor(process.stdin, "isTTY"); @@ -618,70 +614,6 @@ describe("ProcessTerminal DECRQM + in-band resize (DEC 2026/2048)", () => { }); }); -describe("ProcessTerminal Kitty graphics temp-file probe", () => { - const originalProtocol = TERMINAL.imageProtocol; - let originalGraphics: KittyGraphicsFeatures; - - beforeEach(() => { - Object.defineProperty(process.stdin, "isTTY", { value: true, configurable: true }); - Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true }); - Object.defineProperty(process.stdin, "setRawMode", { value: vi.fn(), configurable: true }); - originalGraphics = { ...getKittyGraphics() }; - Bun.env.PI_TUI_KITTY_GRAPHICS_PROBE = "1"; - setTerminalImageProtocol(ImageProtocol.Kitty); - setKittyGraphics({ transmissionMedium: "direct" }); - }); - - afterEach(() => { - vi.restoreAllMocks(); - restoreProperty(process.stdin, "isTTY", stdinIsTtyDescriptor); - restoreProperty(process.stdout, "isTTY", stdoutIsTtyDescriptor); - restoreProperty(process.stdin, "setRawMode", stdinSetRawModeDescriptor); - delete Bun.env.PI_TUI_KITTY_GRAPHICS_PROBE; - setTerminalImageProtocol(originalProtocol); - setKittyGraphics(originalGraphics); - }); - - function startProbed() { - const writes: string[] = []; - vi.spyOn(process, "kill").mockReturnValue(true); - vi.spyOn(process.stdin, "resume").mockImplementation(() => process.stdin); - vi.spyOn(process.stdin, "pause").mockImplementation(() => process.stdin); - vi.spyOn(process.stdin, "setEncoding").mockImplementation(() => process.stdin); - vi.spyOn(process.stdout, "write").mockImplementation(chunk => { - writes.push(typeof chunk === "string" ? chunk : chunk.toString()); - return true; - }); - const terminal = new ProcessTerminal(); - terminal.start( - () => {}, - () => {}, - ); - return { terminal, writes }; - } - - it("emits an a=q,t=t probe and promotes to temp-file transmission on OK", () => { - const { terminal, writes } = startProbed(); - const probe = writes.find(w => w.includes("\x1b_Ga=q,t=t")); - expect(probe).toBeDefined(); - const id = probe?.match(/i=(\d+)/)?.[1]; - expect(id).toBeDefined(); - expect(getKittyGraphics().transmissionMedium).toBe("direct"); - process.stdin.emit("data", `\x1b_Gi=${id};OK\x1b\\`); - expect(getKittyGraphics().transmissionMedium).toBe("temp-file"); - terminal.stop(); - }); - - it("stays on direct transmission when the probe reports an error", () => { - const { terminal, writes } = startProbed(); - const id = writes.find(w => w.includes("\x1b_Ga=q,t=t"))?.match(/i=(\d+)/)?.[1]; - expect(id).toBeDefined(); - process.stdin.emit("data", `\x1b_Gi=${id};ENOTSUP:bad\x1b\\`); - expect(getKittyGraphics().transmissionMedium).toBe("direct"); - terminal.stop(); - }); -}); - describe("OSC 66 text-sizing capability", () => { it("advertises text sizing only for Kitty", () => { // OSC 66 is a Kitty-only protocol; any other terminal must report the