chore(vendor): import pi 1.3.0
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: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
1 parent
719370c8f7
commit
6081e3e325
1743 files changed
+205459
-131704
No files matched your search
+80
-13
@@ -2,6 +2,7 @@ import { execSync } from "node:child_process";
|
||||
import { homedir } from "node:os";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import type { TerminalColorMode } from "./colors.ts";
|
||||
|
||||
export type ImageProtocol = "kitty" | "iterm2" | null;
|
||||
|
||||
@@ -71,7 +72,7 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink: () => boolean)
|
||||
const terminalEmulator = process.env.TERMINAL_EMULATOR?.toLowerCase() || "";
|
||||
const term = process.env.TERM?.toLowerCase() || "";
|
||||
const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
|
||||
const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit";
|
||||
const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit" || term.endsWith("-direct");
|
||||
const isWindowsConsole = process.platform === "win32";
|
||||
|
||||
// Emit OSC 8 hyperlinks only when tmux confirms it forwards.
|
||||
@@ -85,6 +86,12 @@ function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink: () => boolean)
|
||||
return { images: null, trueColor: hasTrueColorHint, hyperlinks: false };
|
||||
}
|
||||
|
||||
// Herdr forwards OSC 8 hyperlinks and supports the Kitty graphics protocol. Check it before
|
||||
// outer-terminal detection because variables like KITTY_WINDOW_ID can leak into its panes.
|
||||
if (termProgram === "herdr") {
|
||||
return { images: "kitty", trueColor: hasTrueColorHint, hyperlinks: true };
|
||||
}
|
||||
|
||||
if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
|
||||
return { images: "kitty", trueColor: true, hyperlinks: true };
|
||||
}
|
||||
@@ -168,6 +175,10 @@ export function getCapabilities(): TerminalCapabilities {
|
||||
return cachedCapabilities;
|
||||
}
|
||||
|
||||
export function getTerminalColorMode(capabilities: TerminalCapabilities = getCapabilities()): TerminalColorMode {
|
||||
return capabilities.trueColor ? "truecolor" : "256color";
|
||||
}
|
||||
|
||||
export function resetCapabilitiesCache(): void {
|
||||
cachedCapabilities = null;
|
||||
}
|
||||
@@ -327,6 +338,7 @@ export interface KittyImagePlacement {
|
||||
transmissionGeneration: number;
|
||||
transmissionBytes: number;
|
||||
estimatedDecodedBytes: number;
|
||||
rows: number;
|
||||
sequence: string;
|
||||
replacementLine: string;
|
||||
}
|
||||
@@ -344,13 +356,27 @@ export function registerKittyImageMetadata(metadata: KittyImageMetadata): void {
|
||||
}
|
||||
}
|
||||
|
||||
function getRegisteredKittyImageMetadata(line: string): RegisteredKittyImageMetadata | undefined {
|
||||
const controls = /\x1b_G([^;]*);/.exec(line)?.[1];
|
||||
if (!controls) return undefined;
|
||||
function getRegisteredKittyImageMetadataFromControls(controls: string): RegisteredKittyImageMetadata | undefined {
|
||||
const imageId = /(?:^|,)i=(\d+)(?:,|$)/.exec(controls)?.[1];
|
||||
return imageId === undefined ? undefined : kittyImageMetadata.get(Number.parseInt(imageId, 10));
|
||||
}
|
||||
|
||||
function getRegisteredKittyImageMetadata(line: string): RegisteredKittyImageMetadata | undefined {
|
||||
const controls = /\x1b_G([^;]*);/.exec(line)?.[1];
|
||||
return controls === undefined ? undefined : getRegisteredKittyImageMetadataFromControls(controls);
|
||||
}
|
||||
|
||||
function getExplicitKittyImageRows(controls: string): number | undefined {
|
||||
const value = /(?:^|,)r=(\d+)(?:,|$)/.exec(controls)?.[1];
|
||||
if (value === undefined) return undefined;
|
||||
const rows = Number.parseInt(value, 10);
|
||||
return rows > 0 ? rows : undefined;
|
||||
}
|
||||
|
||||
function getKittyImageRowsFromControls(controls: string, fallbackRows: number): number {
|
||||
return getExplicitKittyImageRows(controls) ?? fallbackRows;
|
||||
}
|
||||
|
||||
export function getKittyImageMetadata(line: string): KittyImageMetadata | undefined {
|
||||
const metadata = getRegisteredKittyImageMetadata(line);
|
||||
if (!metadata) return undefined;
|
||||
@@ -383,11 +409,21 @@ const KITTY_PLACEMENT_CONTROL_KEYS = new Set([
|
||||
"V",
|
||||
]);
|
||||
|
||||
/** Read the number of rows covered by an image placement without scanning its payload. */
|
||||
export function getKittyImagePlacementRows(line: string): number | undefined {
|
||||
const controls = /\x1b_G([^;]*);/.exec(line)?.[1];
|
||||
if (controls === undefined) return undefined;
|
||||
const explicitRows = getExplicitKittyImageRows(controls);
|
||||
if (explicitRows !== undefined) return explicitRows;
|
||||
return getRegisteredKittyImageMetadataFromControls(controls)?.rows;
|
||||
}
|
||||
|
||||
/** Build a placement-only command for an image line emitted by {@link renderImage}. */
|
||||
export function getKittyImagePlacement(line: string): KittyImagePlacement | undefined {
|
||||
const match = /\x1b_G([^;]*);/.exec(line);
|
||||
const metadata = getRegisteredKittyImageMetadata(line);
|
||||
if (!match || !metadata) return undefined;
|
||||
if (!match) return undefined;
|
||||
const metadata = getRegisteredKittyImageMetadataFromControls(match[1]);
|
||||
if (!metadata) return undefined;
|
||||
|
||||
let commandStart = match.index;
|
||||
let commandControls = match[1];
|
||||
@@ -413,6 +449,7 @@ export function getKittyImagePlacement(line: string): KittyImagePlacement | unde
|
||||
transmissionGeneration: metadata.transmissionGeneration,
|
||||
transmissionBytes: transmissionEnd - match.index,
|
||||
estimatedDecodedBytes: metadata.widthPx * metadata.heightPx * 4,
|
||||
rows: getKittyImageRowsFromControls(match[1], metadata.rows),
|
||||
sequence,
|
||||
replacementLine: `${line.slice(0, match.index)}${sequence}${line.slice(transmissionEnd)}`,
|
||||
};
|
||||
@@ -432,11 +469,21 @@ export function cropKittyImageLine(line: string, hiddenRows: number, visibleRows
|
||||
return `${line.slice(0, match.index)}\x1b_G${controls.join(",")};${line.slice(match.index + match[0].length)}`;
|
||||
}
|
||||
|
||||
function chooseLessDistortedCellCount(upperCount: number, idealCount: number): number {
|
||||
if (upperCount <= 1) return upperCount;
|
||||
|
||||
const lowerCount = upperCount - 1;
|
||||
const upperDistortion = Math.max(upperCount / idealCount, idealCount / upperCount);
|
||||
const lowerDistortion = Math.max(lowerCount / idealCount, idealCount / lowerCount);
|
||||
return lowerDistortion < upperDistortion ? lowerCount : upperCount;
|
||||
}
|
||||
|
||||
export function calculateImageCellSize(
|
||||
imageDimensions: ImageDimensions,
|
||||
maxWidthCells: number,
|
||||
maxHeightCells?: number,
|
||||
cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 },
|
||||
optimizeAspectRatio = false,
|
||||
): ImageCellSize {
|
||||
const maxWidth = Math.max(1, Math.floor(maxWidthCells));
|
||||
const maxHeight = maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells));
|
||||
@@ -449,13 +496,26 @@ export function calculateImageCellSize(
|
||||
|
||||
const scaledWidthPx = imageWidth * scale;
|
||||
const scaledHeightPx = imageHeight * scale;
|
||||
const columns = Math.ceil(scaledWidthPx / cellDimensions.widthPx);
|
||||
const rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);
|
||||
let columns = Math.max(1, Math.min(maxWidth, Math.ceil(scaledWidthPx / cellDimensions.widthPx)));
|
||||
const heightRows = scaledHeightPx / cellDimensions.heightPx;
|
||||
let rows = Math.max(1, Math.ceil(heightRows));
|
||||
if (maxHeight !== undefined) {
|
||||
rows = Math.min(maxHeight, rows);
|
||||
}
|
||||
|
||||
return {
|
||||
columns: Math.max(1, Math.min(maxWidth, columns)),
|
||||
rows: Math.max(1, maxHeight === undefined ? rows : Math.min(maxHeight, rows)),
|
||||
};
|
||||
if (!optimizeAspectRatio) {
|
||||
return { columns, rows };
|
||||
}
|
||||
|
||||
if (widthScale <= heightScale) {
|
||||
const idealRows = (columns * cellDimensions.widthPx * imageHeight) / (imageWidth * cellDimensions.heightPx);
|
||||
rows = chooseLessDistortedCellCount(rows, idealRows);
|
||||
} else {
|
||||
const idealColumns = (rows * cellDimensions.heightPx * imageWidth) / (imageHeight * cellDimensions.widthPx);
|
||||
columns = chooseLessDistortedCellCount(columns, idealColumns);
|
||||
}
|
||||
|
||||
return { columns, rows };
|
||||
}
|
||||
|
||||
export function calculateImageRows(
|
||||
@@ -619,7 +679,14 @@ export function renderImage(
|
||||
}
|
||||
|
||||
const maxWidth = options.maxWidthCells ?? 80;
|
||||
const size = calculateImageCellSize(imageDimensions, maxWidth, options.maxHeightCells, getCellDimensions());
|
||||
// Reduce Kitty's cell-aligned distortion without shrinking iTerm2 reservations.
|
||||
const size = calculateImageCellSize(
|
||||
imageDimensions,
|
||||
maxWidth,
|
||||
options.maxHeightCells,
|
||||
getCellDimensions(),
|
||||
caps.images === "kitty",
|
||||
);
|
||||
|
||||
if (caps.images === "kitty") {
|
||||
if (options.imageId !== undefined) {
|
||||
|
||||
Reference in new issue
Block a user