mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(tui): add terminal capability overrides closes #8665
This commit is contained in:
parent
42f7f29ad1
commit
e86823096c
13 changed files with 181 additions and 8 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added environment and programmatic overrides for OSC 8 hyperlinks, inline image protocols, and truecolor terminal capabilities ([#8665](https://github.com/earendil-works/pi/issues/8665)).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed main-screen rendering crashing when image-heavy output exceeded V8's string length limit ([#8028](https://github.com/earendil-works/pi/issues/8028)).
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ export {
|
|||
renderImage,
|
||||
resetCapabilitiesCache,
|
||||
setCapabilities,
|
||||
setCapabilityOverrides,
|
||||
setCellDimensions,
|
||||
type TerminalCapabilities,
|
||||
} from "./terminal-image.ts";
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export interface ImageRenderOptions {
|
|||
}
|
||||
|
||||
let cachedCapabilities: TerminalCapabilities | null = null;
|
||||
let capabilityOverrides: Partial<TerminalCapabilities> = {};
|
||||
|
||||
// Default cell dimensions - updated by TUI when terminal responds to query
|
||||
let cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 };
|
||||
|
|
@ -65,7 +66,7 @@ function probeTmuxHyperlinks(): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
export function detectCapabilities(tmuxForwardsHyperlink: () => boolean = probeTmuxHyperlinks): TerminalCapabilities {
|
||||
function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink: () => boolean): TerminalCapabilities {
|
||||
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
|
||||
const terminalEmulator = process.env.TERMINAL_EMULATOR?.toLowerCase() || "";
|
||||
const term = process.env.TERM?.toLowerCase() || "";
|
||||
|
|
@ -135,9 +136,38 @@ export function detectCapabilities(tmuxForwardsHyperlink: () => boolean = probeT
|
|||
return { images: null, trueColor: hasTrueColorHint, hyperlinks: false };
|
||||
}
|
||||
|
||||
function parseBooleanCapabilityOverride(value: string | undefined): boolean | undefined {
|
||||
return value === "1" ? true : value === "0" ? false : undefined;
|
||||
}
|
||||
|
||||
export function detectCapabilities(tmuxForwardsHyperlink: () => boolean = probeTmuxHyperlinks): TerminalCapabilities {
|
||||
const hyperlinks = parseBooleanCapabilityOverride(process.env.PI_HYPERLINKS);
|
||||
const detected = detectCapabilitiesFromEnvironment(
|
||||
hyperlinks === undefined ? tmuxForwardsHyperlink : () => hyperlinks,
|
||||
);
|
||||
const imageProtocol = process.env.PI_IMAGE_PROTOCOL?.toLowerCase();
|
||||
const images =
|
||||
imageProtocol === "kitty" || imageProtocol === "iterm2"
|
||||
? imageProtocol
|
||||
: imageProtocol === "none" || imageProtocol === "0"
|
||||
? null
|
||||
: undefined;
|
||||
const trueColor = parseBooleanCapabilityOverride(process.env.PI_TRUE_COLOR);
|
||||
return {
|
||||
...detected,
|
||||
...(images !== undefined ? { images } : {}),
|
||||
...(trueColor !== undefined ? { trueColor } : {}),
|
||||
...(hyperlinks !== undefined ? { hyperlinks } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function getCapabilities(): TerminalCapabilities {
|
||||
if (!cachedCapabilities) {
|
||||
cachedCapabilities = detectCapabilities();
|
||||
const hyperlinks = capabilityOverrides.hyperlinks;
|
||||
cachedCapabilities = {
|
||||
...detectCapabilities(hyperlinks === undefined ? undefined : () => hyperlinks),
|
||||
...capabilityOverrides,
|
||||
};
|
||||
}
|
||||
return cachedCapabilities;
|
||||
}
|
||||
|
|
@ -146,6 +176,19 @@ export function resetCapabilitiesCache(): void {
|
|||
cachedCapabilities = null;
|
||||
}
|
||||
|
||||
/** Override selected auto-detected capabilities. */
|
||||
export function setCapabilityOverrides(overrides: Partial<TerminalCapabilities>): void {
|
||||
if (
|
||||
capabilityOverrides.images === overrides.images &&
|
||||
capabilityOverrides.trueColor === overrides.trueColor &&
|
||||
capabilityOverrides.hyperlinks === overrides.hyperlinks
|
||||
) {
|
||||
return;
|
||||
}
|
||||
capabilityOverrides = { ...overrides };
|
||||
cachedCapabilities = null;
|
||||
}
|
||||
|
||||
/** Override the cached capabilities. Useful in tests to exercise both code paths. */
|
||||
export function setCapabilities(caps: TerminalCapabilities): void {
|
||||
cachedCapabilities = caps;
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
detectCapabilities,
|
||||
encodeITerm2,
|
||||
encodeKitty,
|
||||
getCapabilities,
|
||||
getKittyImageMetadata,
|
||||
getKittyImagePlacement,
|
||||
hyperlink,
|
||||
|
|
@ -24,6 +25,7 @@ import {
|
|||
renderImage,
|
||||
resetCapabilitiesCache,
|
||||
setCapabilities,
|
||||
setCapabilityOverrides,
|
||||
setCellDimensions,
|
||||
} from "../src/terminal-image.ts";
|
||||
import { visibleWidth } from "../src/utils.ts";
|
||||
|
|
@ -42,9 +44,12 @@ const ENV_KEYS = [
|
|||
"CMUX_WORKSPACE_ID",
|
||||
"WARP_SESSION_ID",
|
||||
"WARP_TERMINAL_SESSION_UUID",
|
||||
"PI_HYPERLINKS",
|
||||
"PI_IMAGE_PROTOCOL",
|
||||
"PI_TRUE_COLOR",
|
||||
] as const;
|
||||
|
||||
function withEnv(overrides: Record<string, string | undefined>, fn: () => void): void {
|
||||
function withEnv<T>(overrides: Record<string, string | undefined>, fn: () => T): T {
|
||||
const saved: Record<string, string | undefined> = {};
|
||||
for (const key of ENV_KEYS) {
|
||||
saved[key] = process.env[key];
|
||||
|
|
@ -55,7 +60,7 @@ function withEnv(overrides: Record<string, string | undefined>, fn: () => void):
|
|||
if (v === undefined) delete process.env[k];
|
||||
else process.env[k] = v;
|
||||
}
|
||||
fn();
|
||||
return fn();
|
||||
} finally {
|
||||
for (const key of ENV_KEYS) {
|
||||
if (saved[key] === undefined) delete process.env[key];
|
||||
|
|
@ -219,6 +224,63 @@ describe("detectCapabilities", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("applies environment overrides", () => {
|
||||
assert.deepStrictEqual(
|
||||
withEnv({ PI_HYPERLINKS: "1", PI_IMAGE_PROTOCOL: "kitty", PI_TRUE_COLOR: "1" }, () => detectCapabilities()),
|
||||
{ images: "kitty", trueColor: true, hyperlinks: true },
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
withEnv({ TERM_PROGRAM: "iterm.app", PI_HYPERLINKS: "0", PI_IMAGE_PROTOCOL: "none", PI_TRUE_COLOR: "0" }, () =>
|
||||
detectCapabilities(),
|
||||
),
|
||||
{ images: null, trueColor: false, hyperlinks: false },
|
||||
);
|
||||
});
|
||||
|
||||
it("preserves auto-detection for auto environment overrides", () => {
|
||||
assert.deepStrictEqual(
|
||||
withEnv(
|
||||
{
|
||||
TERM_PROGRAM: "ghostty",
|
||||
PI_HYPERLINKS: "auto",
|
||||
PI_IMAGE_PROTOCOL: "auto",
|
||||
PI_TRUE_COLOR: "auto",
|
||||
},
|
||||
() => detectCapabilities(),
|
||||
),
|
||||
{ images: "kitty", trueColor: true, hyperlinks: true },
|
||||
);
|
||||
});
|
||||
|
||||
it("applies and clears programmatic overrides", () => {
|
||||
withEnv({ PI_HYPERLINKS: "1", PI_IMAGE_PROTOCOL: "kitty", PI_TRUE_COLOR: "1" }, () => {
|
||||
setCapabilityOverrides({ images: null, trueColor: false, hyperlinks: false });
|
||||
try {
|
||||
assert.deepStrictEqual(getCapabilities(), { images: null, trueColor: false, hyperlinks: false });
|
||||
setCapabilityOverrides({});
|
||||
assert.deepStrictEqual(getCapabilities(), { images: "kitty", trueColor: true, hyperlinks: true });
|
||||
} finally {
|
||||
setCapabilityOverrides({});
|
||||
resetCapabilitiesCache();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("bypasses the tmux probe when hyperlinks are overridden", () => {
|
||||
let probed = false;
|
||||
const caps = withEnv(
|
||||
{ TMUX: "/tmp/tmux-1000/default,1234,0", PI_HYPERLINKS: "1", PI_IMAGE_PROTOCOL: "kitty" },
|
||||
() =>
|
||||
detectCapabilities(() => {
|
||||
probed = true;
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
assert.strictEqual(probed, false);
|
||||
assert.strictEqual(caps.hyperlinks, true);
|
||||
assert.strictEqual(caps.images, "kitty");
|
||||
});
|
||||
|
||||
it("enables hyperlinks under tmux when the client forwards them", () => {
|
||||
withEnv({ TMUX: "/tmp/tmux-1000/default,1234,0", TERM_PROGRAM: "ghostty" }, () => {
|
||||
const caps = detectCapabilities(() => true);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue