@fractaal/pi-tui 0.85.3 → 0.86.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -19
- package/dist/alt-screen-search.d.ts +46 -0
- package/dist/alt-screen-search.d.ts.map +1 -0
- package/dist/alt-screen-search.js +282 -0
- package/dist/alt-screen-search.js.map +1 -0
- package/dist/autocomplete.d.ts +2 -1
- package/dist/autocomplete.d.ts.map +1 -1
- package/dist/autocomplete.js +85 -24
- package/dist/autocomplete.js.map +1 -1
- package/dist/colors.d.ts +73 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +273 -0
- package/dist/colors.js.map +1 -0
- package/dist/components/alt-screen-flash.d.ts +13 -0
- package/dist/components/alt-screen-flash.d.ts.map +1 -0
- package/dist/components/alt-screen-flash.js +37 -0
- package/dist/components/alt-screen-flash.js.map +1 -0
- package/dist/components/box.d.ts +3 -1
- package/dist/components/box.d.ts.map +1 -1
- package/dist/components/box.js +38 -8
- package/dist/components/box.js.map +1 -1
- package/dist/components/cancellable-loader.d.ts.map +1 -1
- package/dist/components/cancellable-loader.js.map +1 -1
- package/dist/components/editor.d.ts +6 -1
- package/dist/components/editor.d.ts.map +1 -1
- package/dist/components/editor.js +118 -24
- package/dist/components/editor.js.map +1 -1
- package/dist/components/h-stack.d.ts +7 -0
- package/dist/components/h-stack.d.ts.map +1 -0
- package/dist/components/h-stack.js +43 -0
- package/dist/components/h-stack.js.map +1 -0
- package/dist/components/image.d.ts +12 -0
- package/dist/components/image.d.ts.map +1 -1
- package/dist/components/image.js +36 -3
- package/dist/components/image.js.map +1 -1
- package/dist/components/input.d.ts +12 -1
- package/dist/components/input.d.ts.map +1 -1
- package/dist/components/input.js +44 -5
- package/dist/components/input.js.map +1 -1
- package/dist/components/loader.d.ts +2 -0
- package/dist/components/loader.d.ts.map +1 -1
- package/dist/components/loader.js +10 -3
- package/dist/components/loader.js.map +1 -1
- package/dist/components/markdown.d.ts +5 -0
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +150 -9
- package/dist/components/markdown.js.map +1 -1
- package/dist/components/mouse-region.d.ts +12 -0
- package/dist/components/mouse-region.d.ts.map +1 -0
- package/dist/components/mouse-region.js +21 -0
- package/dist/components/mouse-region.js.map +1 -0
- package/dist/components/scroll-view.d.ts +59 -0
- package/dist/components/scroll-view.d.ts.map +1 -0
- package/dist/components/scroll-view.js +195 -0
- package/dist/components/scroll-view.js.map +1 -0
- package/dist/components/select-list.d.ts +4 -1
- package/dist/components/select-list.d.ts.map +1 -1
- package/dist/components/select-list.js +49 -2
- package/dist/components/select-list.js.map +1 -1
- package/dist/components/settings-list.d.ts +13 -3
- package/dist/components/settings-list.d.ts.map +1 -1
- package/dist/components/settings-list.js +82 -15
- package/dist/components/settings-list.js.map +1 -1
- package/dist/components/spacer.d.ts.map +1 -1
- package/dist/components/spacer.js.map +1 -1
- package/dist/components/stack.d.ts +32 -0
- package/dist/components/stack.d.ts.map +1 -0
- package/dist/components/stack.js +111 -0
- package/dist/components/stack.js.map +1 -0
- package/dist/components/text.d.ts.map +1 -1
- package/dist/components/text.js +7 -5
- package/dist/components/text.js.map +1 -1
- package/dist/components/truncated-text.d.ts.map +1 -1
- package/dist/components/truncated-text.js.map +1 -1
- package/dist/components/v-stack.d.ts +8 -0
- package/dist/components/v-stack.d.ts.map +1 -0
- package/dist/components/v-stack.js +26 -0
- package/dist/components/v-stack.js.map +1 -0
- package/dist/editor-component.d.ts.map +1 -1
- package/dist/fuzzy.d.ts.map +1 -1
- package/dist/fuzzy.js +23 -22
- package/dist/fuzzy.js.map +1 -1
- package/dist/index.d.ts +18 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -6
- package/dist/index.js.map +1 -1
- package/dist/keybindings.d.ts +82 -2
- package/dist/keybindings.d.ts.map +1 -1
- package/dist/keybindings.js +61 -2
- package/dist/keybindings.js.map +1 -1
- package/dist/keys.d.ts.map +1 -1
- package/dist/keys.js.map +1 -1
- package/dist/kill-ring.d.ts.map +1 -1
- package/dist/kill-ring.js.map +1 -1
- package/dist/latex.d.ts +10 -0
- package/dist/latex.d.ts.map +1 -0
- package/dist/latex.js +1378 -0
- package/dist/latex.js.map +1 -0
- package/dist/layout-node.d.ts +40 -0
- package/dist/layout-node.d.ts.map +1 -0
- package/dist/layout-node.js +6 -0
- package/dist/layout-node.js.map +1 -0
- package/dist/layout.d.ts +42 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +344 -0
- package/dist/layout.js.map +1 -0
- package/dist/native-modifiers.d.ts +2 -1
- package/dist/native-modifiers.d.ts.map +1 -1
- package/dist/native-modifiers.js +3 -43
- package/dist/native-modifiers.js.map +1 -1
- package/dist/native-module-path.d.ts +7 -0
- package/dist/native-module-path.d.ts.map +1 -0
- package/dist/native-module-path.js +19 -0
- package/dist/native-module-path.js.map +1 -0
- package/dist/native-platform.d.ts +20 -0
- package/dist/native-platform.d.ts.map +1 -0
- package/dist/native-platform.js +42 -0
- package/dist/native-platform.js.map +1 -0
- package/dist/oklab.d.ts +43 -0
- package/dist/oklab.d.ts.map +1 -0
- package/dist/oklab.js +210 -0
- package/dist/oklab.js.map +1 -0
- package/dist/stdin-buffer.d.ts +8 -2
- package/dist/stdin-buffer.d.ts.map +1 -1
- package/dist/stdin-buffer.js +7 -2
- package/dist/stdin-buffer.js.map +1 -1
- package/dist/terminal-colors.d.ts +19 -2
- package/dist/terminal-colors.d.ts.map +1 -1
- package/dist/terminal-colors.js +13 -8
- package/dist/terminal-colors.js.map +1 -1
- package/dist/terminal-image.d.ts +29 -1
- package/dist/terminal-image.d.ts.map +1 -1
- package/dist/terminal-image.js +223 -18
- package/dist/terminal-image.js.map +1 -1
- package/dist/terminal.d.ts +16 -0
- package/dist/terminal.d.ts.map +1 -1
- package/dist/terminal.js +62 -46
- package/dist/terminal.js.map +1 -1
- package/dist/tui-alt-screen.d.ts +166 -0
- package/dist/tui-alt-screen.d.ts.map +1 -0
- package/dist/tui-alt-screen.js +1546 -0
- package/dist/tui-alt-screen.js.map +1 -0
- package/dist/tui-main-screen.d.ts +39 -0
- package/dist/tui-main-screen.d.ts.map +1 -0
- package/dist/tui-main-screen.js +594 -0
- package/dist/tui-main-screen.js.map +1 -0
- package/dist/tui.d.ts +178 -60
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +303 -641
- package/dist/tui.js.map +1 -1
- package/dist/undo-stack.d.ts.map +1 -1
- package/dist/undo-stack.js.map +1 -1
- package/dist/utils.d.ts +22 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +245 -63
- package/dist/utils.js.map +1 -1
- package/dist/wheel-scroll.d.ts +23 -0
- package/dist/wheel-scroll.d.ts.map +1 -0
- package/dist/wheel-scroll.js +73 -0
- package/dist/wheel-scroll.js.map +1 -0
- package/dist/word-navigation.d.ts.map +1 -1
- package/dist/word-navigation.js.map +1 -1
- package/native/clipboard.h +208 -0
- package/native/darwin/README.md +22 -0
- package/native/darwin/build.sh +69 -0
- package/native/darwin/prebuilds/darwin-arm64/{darwin-modifiers.node → darwin-platform.node} +0 -0
- package/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
- package/native/darwin/src/darwin-platform.m +132 -0
- package/native/linux/README.md +23 -0
- package/native/linux/build.sh +65 -0
- package/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
- package/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
- package/native/linux/src/clipboard-worker.h +70 -0
- package/native/linux/src/linux-platform-x11.c +274 -0
- package/native/napi.h +91 -0
- package/native/win32/README.md +31 -0
- package/native/win32/build.mjs +232 -0
- package/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
- package/native/win32/src/win32-platform.c +241 -0
- package/package.json +19 -4
- package/native/darwin/prebuilds/darwin-x64/darwin-modifiers.node +0 -0
- package/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terminal-colors.js","sourceRoot":"","sources":["../src/terminal-colors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"terminal-colors.js","sourceRoot":"","sources":["../src/terminal-colors.ts"],"names":[],"mappings":"AAkBA,SAAS,QAAQ,CAAC,GAAW;IAC5B,MAAM,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC5D,MAAM,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/C,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;AACpB,CAAC;AAED,SAAS,kBAAkB,CAAC,OAAe;IAC1C,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACnC,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,MAAM,GAAG,GAAG,EAAE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC;IACrC,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;QACd,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;AACxD,CAAC;AAKD,MAAM,0BAA0B,GAAG,gEAAgE,CAAC;AACpG,MAAM,2BAA2B,GAAG,2BAA2B,CAAC;AAEhE;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IACjD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAC;IACrD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,MAAM,MAAM,GACX,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACrG,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACtD,CAAC;AAED,SAAS,kBAAkB,CAAC,QAAgB;IAC3C,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YAChC,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC;QACxB,CAAC;QACD,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACjC,MAAM,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC9C,MAAM,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC9C,MAAM,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;YAC/C,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;QACxF,CAAC;QACD,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC/C,IAAI,GAAG,KAAK,SAAS,IAAI,KAAK,KAAK,SAAS,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACpE,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,MAAM,CAAC,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAClC,MAAM,CAAC,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACpC,MAAM,CAAC,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;IACnC,OAAO,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;AACxF,CAAC;AAED,MAAM,UAAU,8BAA8B,CAAC,IAAY;IAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;IACtD,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,SAAS,CAAC;IAClB,CAAC;IACD,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;AAC5C,CAAC","sourcesContent":["export interface RgbColor {\n\tr: number;\n\tg: number;\n\tb: number;\n}\n\nexport type TerminalColorScheme = \"dark\" | \"light\";\n\n/** Colors the terminal reports for its current theme. */\nexport interface TerminalColors {\n\t/** Default foreground (OSC 10). */\n\tforeground?: RgbColor;\n\t/** Default background (OSC 11). */\n\tbackground?: RgbColor;\n\t/** ANSI colors 0-15 (OSC 4). Only set when the terminal reported all 16. */\n\tpalette?: RgbColor[];\n}\n\nfunction hexToRgb(hex: string): RgbColor {\n\tconst normalized = hex.startsWith(\"#\") ? hex.slice(1) : hex;\n\tconst r = parseInt(normalized.slice(0, 2), 16);\n\tconst g = parseInt(normalized.slice(2, 4), 16);\n\tconst b = parseInt(normalized.slice(4, 6), 16);\n\treturn { r, g, b };\n}\n\nfunction parseOscHexChannel(channel: string): number | undefined {\n\tif (!/^[0-9a-f]+$/i.test(channel)) {\n\t\treturn undefined;\n\t}\n\tconst max = 16 ** channel.length - 1;\n\tif (max <= 0) {\n\t\treturn undefined;\n\t}\n\treturn Math.round((parseInt(channel, 16) / max) * 255);\n}\n\n/** What an OSC color reply reports: the default foreground (OSC 10), background (OSC 11), or a palette index (OSC 4). */\nexport type OscColorTarget = \"foreground\" | \"background\" | number;\n\nconst OSC_COLOR_RESPONSE_PATTERN = /^\\x1b\\](?:(1[01])|4;(\\d{1,3}));([^\\x07\\x1b]*)(?:\\x07|\\x1b\\\\)$/i;\nconst COLOR_SCHEME_REPORT_PATTERN = /^(?:\\x1b\\[\\?997;(1|2)n)+$/;\n\n/**\n * Parse an OSC 10, 11, or 4 color reply. Returns undefined when `data` is not such a reply;\n * `rgb` is undefined when it is a reply with an unparseable color.\n */\nexport function parseOscColorResponse(data: string): { target: OscColorTarget; rgb: RgbColor | undefined } | undefined {\n\tconst match = data.match(OSC_COLOR_RESPONSE_PATTERN);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\tconst target: OscColorTarget =\n\t\tmatch[1] === \"10\" ? \"foreground\" : match[1] === \"11\" ? \"background\" : Number.parseInt(match[2], 10);\n\treturn { target, rgb: parseOscColorValue(match[3]) };\n}\n\nfunction parseOscColorValue(rawValue: string): RgbColor | undefined {\n\tconst value = rawValue.trim();\n\tif (value.startsWith(\"#\")) {\n\t\tconst hex = value.slice(1);\n\t\tif (/^[0-9a-f]{6}$/i.test(hex)) {\n\t\t\treturn hexToRgb(value);\n\t\t}\n\t\tif (/^[0-9a-f]{12}$/i.test(hex)) {\n\t\t\tconst r = parseOscHexChannel(hex.slice(0, 4));\n\t\t\tconst g = parseOscHexChannel(hex.slice(4, 8));\n\t\t\tconst b = parseOscHexChannel(hex.slice(8, 12));\n\t\t\treturn r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tconst rgbValue = value.replace(/^rgba?:/i, \"\");\n\tconst [red, green, blue] = rgbValue.split(\"/\");\n\tif (red === undefined || green === undefined || blue === undefined) {\n\t\treturn undefined;\n\t}\n\tconst r = parseOscHexChannel(red);\n\tconst g = parseOscHexChannel(green);\n\tconst b = parseOscHexChannel(blue);\n\treturn r !== undefined && g !== undefined && b !== undefined ? { r, g, b } : undefined;\n}\n\nexport function parseTerminalColorSchemeReport(data: string): TerminalColorScheme | undefined {\n\tconst match = data.match(COLOR_SCHEME_REPORT_PATTERN);\n\tif (!match) {\n\t\treturn undefined;\n\t}\n\treturn match[1] === \"2\" ? \"light\" : \"dark\";\n}\n"]}
|
package/dist/terminal-image.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TerminalColorMode } from "./colors.ts";
|
|
1
2
|
export type ImageProtocol = "kitty" | "iterm2" | null;
|
|
2
3
|
export interface TerminalCapabilities {
|
|
3
4
|
images: ImageProtocol;
|
|
@@ -25,7 +26,10 @@ export declare function getCellDimensions(): CellDimensions;
|
|
|
25
26
|
export declare function setCellDimensions(dims: CellDimensions): void;
|
|
26
27
|
export declare function detectCapabilities(tmuxForwardsHyperlink?: () => boolean): TerminalCapabilities;
|
|
27
28
|
export declare function getCapabilities(): TerminalCapabilities;
|
|
29
|
+
export declare function getTerminalColorMode(capabilities?: TerminalCapabilities): TerminalColorMode;
|
|
28
30
|
export declare function resetCapabilitiesCache(): void;
|
|
31
|
+
/** Override selected auto-detected capabilities. */
|
|
32
|
+
export declare function setCapabilityOverrides(overrides: Partial<TerminalCapabilities>): void;
|
|
29
33
|
/** Override the cached capabilities. Useful in tests to exercise both code paths. */
|
|
30
34
|
export declare function setCapabilities(caps: TerminalCapabilities): void;
|
|
31
35
|
export declare function isImageLine(line: string): boolean;
|
|
@@ -52,6 +56,8 @@ export declare function deleteKittyImage(imageId: number): string;
|
|
|
52
56
|
* Uses uppercase 'A' to also free the image data.
|
|
53
57
|
*/
|
|
54
58
|
export declare function deleteAllKittyImages(): string;
|
|
59
|
+
/** Delete all visible Kitty placements while retaining their uploaded image data. */
|
|
60
|
+
export declare function deleteAllKittyPlacements(): string;
|
|
55
61
|
export declare function encodeITerm2(base64Data: string, options?: {
|
|
56
62
|
width?: number | string;
|
|
57
63
|
height?: number | string;
|
|
@@ -63,7 +69,28 @@ export interface ImageCellSize {
|
|
|
63
69
|
columns: number;
|
|
64
70
|
rows: number;
|
|
65
71
|
}
|
|
66
|
-
export
|
|
72
|
+
export interface KittyImageMetadata extends ImageCellSize {
|
|
73
|
+
imageId: number;
|
|
74
|
+
widthPx: number;
|
|
75
|
+
heightPx: number;
|
|
76
|
+
}
|
|
77
|
+
export interface KittyImagePlacement {
|
|
78
|
+
imageId: number;
|
|
79
|
+
transmissionGeneration: number;
|
|
80
|
+
transmissionBytes: number;
|
|
81
|
+
estimatedDecodedBytes: number;
|
|
82
|
+
rows: number;
|
|
83
|
+
sequence: string;
|
|
84
|
+
replacementLine: string;
|
|
85
|
+
}
|
|
86
|
+
export declare function registerKittyImageMetadata(metadata: KittyImageMetadata): void;
|
|
87
|
+
export declare function getKittyImageMetadata(line: string): KittyImageMetadata | undefined;
|
|
88
|
+
/** Read the number of rows covered by an image placement without scanning its payload. */
|
|
89
|
+
export declare function getKittyImagePlacementRows(line: string): number | undefined;
|
|
90
|
+
/** Build a placement-only command for an image line emitted by {@link renderImage}. */
|
|
91
|
+
export declare function getKittyImagePlacement(line: string): KittyImagePlacement | undefined;
|
|
92
|
+
export declare function cropKittyImageLine(line: string, hiddenRows: number, visibleRows: number): string;
|
|
93
|
+
export declare function calculateImageCellSize(imageDimensions: ImageDimensions, maxWidthCells: number, maxHeightCells?: number, cellDimensions?: CellDimensions, optimizeAspectRatio?: boolean): ImageCellSize;
|
|
67
94
|
export declare function calculateImageRows(imageDimensions: ImageDimensions, targetWidthCells: number, cellDimensions?: CellDimensions): number;
|
|
68
95
|
export declare function getPngDimensions(base64Data: string): ImageDimensions | null;
|
|
69
96
|
export declare function getJpegDimensions(base64Data: string): ImageDimensions | null;
|
|
@@ -72,6 +99,7 @@ export declare function getWebpDimensions(base64Data: string): ImageDimensions |
|
|
|
72
99
|
export declare function getImageDimensions(base64Data: string, mimeType: string): ImageDimensions | null;
|
|
73
100
|
export declare function renderImage(base64Data: string, imageDimensions: ImageDimensions, options?: ImageRenderOptions): {
|
|
74
101
|
sequence: string;
|
|
102
|
+
columns: number;
|
|
75
103
|
rows: number;
|
|
76
104
|
imageId?: number;
|
|
77
105
|
} | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"terminal-image.d.ts","sourceRoot":"","sources":["../src/terminal-image.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from \"node:child_process\";\nimport { homedir } from \"node:os\";\nimport { isAbsolute } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\n\nexport type ImageProtocol = \"kitty\" | \"iterm2\" | null;\n\nexport interface TerminalCapabilities {\n\timages: ImageProtocol;\n\ttrueColor: boolean;\n\thyperlinks: boolean;\n}\n\nexport interface CellDimensions {\n\twidthPx: number;\n\theightPx: number;\n}\n\nexport interface ImageDimensions {\n\twidthPx: number;\n\theightPx: number;\n}\n\nexport interface ImageRenderOptions {\n\tmaxWidthCells?: number;\n\tmaxHeightCells?: number;\n\tpreserveAspectRatio?: boolean;\n\t/** Kitty image ID. If provided, reuses/replaces existing image with this ID. */\n\timageId?: number;\n\t/** Whether Kitty should apply its default cursor movement after placement. */\n\tmoveCursor?: boolean;\n}\n\nlet cachedCapabilities: TerminalCapabilities | null = null;\n\n// Default cell dimensions - updated by TUI when terminal responds to query\nlet cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 };\n\nexport function getCellDimensions(): CellDimensions {\n\treturn cellDimensions;\n}\n\nexport function setCellDimensions(dims: CellDimensions): void {\n\tcellDimensions = dims;\n}\n\n/**\n * Checks whether the attached tmux client forwards OSC 8 hyperlinks to the\n * outer terminal. tmux only re-emits them when its `client_termfeatures` lists\n * `hyperlinks`, and strips them otherwise. On any error fallbacks `false`.\n */\nfunction probeTmuxHyperlinks(): boolean {\n\ttry {\n\t\tconst termfeatures = execSync(\"tmux display-message -p '#{client_termfeatures}'\", {\n\t\t\tencoding: \"utf8\",\n\t\t\ttimeout: 250,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"ignore\"],\n\t\t});\n\t\treturn termfeatures\n\t\t\t.split(\",\")\n\t\t\t.map((feature) => feature.trim())\n\t\t\t.includes(\"hyperlinks\");\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nexport function detectCapabilities(tmuxForwardsHyperlink: () => boolean = probeTmuxHyperlinks): TerminalCapabilities {\n\tconst termProgram = process.env.TERM_PROGRAM?.toLowerCase() || \"\";\n\tconst terminalEmulator = process.env.TERMINAL_EMULATOR?.toLowerCase() || \"\";\n\tconst term = process.env.TERM?.toLowerCase() || \"\";\n\tconst colorTerm = process.env.COLORTERM?.toLowerCase() || \"\";\n\tconst hasTrueColorHint = colorTerm === \"truecolor\" || colorTerm === \"24bit\";\n\n\t// Emit OSC 8 hyperlinks only when tmux confirms it forwards.\n\t// Image protocols are unreliable under tmux, so leave `images: null`.\n\tif (process.env.TMUX || term.startsWith(\"tmux\")) {\n\t\treturn { images: null, trueColor: hasTrueColorHint, hyperlinks: tmuxForwardsHyperlink() };\n\t}\n\n\t// screen does not forward OSC 8 hyperlinks, so keep them off there.\n\tif (term.startsWith(\"screen\")) {\n\t\treturn { images: null, trueColor: hasTrueColorHint, hyperlinks: false };\n\t}\n\n\tif (process.env.KITTY_WINDOW_ID || termProgram === \"kitty\") {\n\t\treturn { images: \"kitty\", trueColor: true, hyperlinks: true };\n\t}\n\n\tif (termProgram === \"ghostty\" || term.includes(\"ghostty\") || process.env.GHOSTTY_RESOURCES_DIR) {\n\t\treturn { images: \"kitty\", trueColor: true, hyperlinks: true };\n\t}\n\n\tif (process.env.WEZTERM_PANE || termProgram === \"wezterm\") {\n\t\treturn { images: \"kitty\", trueColor: true, hyperlinks: true };\n\t}\n\n\t// Warp supports the Kitty graphics protocol and OSC 8 hyperlinks.\n\tif (termProgram === \"warpterminal\" || process.env.WARP_SESSION_ID || process.env.WARP_TERMINAL_SESSION_UUID) {\n\t\treturn { images: \"kitty\", trueColor: true, hyperlinks: true };\n\t}\n\n\tif (process.env.ITERM_SESSION_ID || termProgram === \"iterm.app\") {\n\t\treturn { images: \"iterm2\", trueColor: true, hyperlinks: true };\n\t}\n\n\tif (process.env.WT_SESSION) {\n\t\treturn { images: null, trueColor: true, hyperlinks: true };\n\t}\n\n\tif (termProgram === \"vscode\") {\n\t\treturn { images: null, trueColor: true, hyperlinks: true };\n\t}\n\n\tif (termProgram === \"alacritty\") {\n\t\treturn { images: null, trueColor: true, hyperlinks: true };\n\t}\n\n\tif (terminalEmulator === \"jetbrains-jediterm\") {\n\t\treturn { images: null, trueColor: true, hyperlinks: false };\n\t}\n\n\t// Unknown terminal: be conservative. OSC 8 is rendered invisibly as \"just\n\t// text\" on terminals that swallow it, which means the URL disappears from\n\t// the rendered output. Default to the legacy `text (url)` behavior unless we\n\t// have positively identified a hyperlink-capable terminal above.\n\treturn { images: null, trueColor: hasTrueColorHint, hyperlinks: false };\n}\n\nexport function getCapabilities(): TerminalCapabilities {\n\tif (!cachedCapabilities) {\n\t\tcachedCapabilities = detectCapabilities();\n\t}\n\treturn cachedCapabilities;\n}\n\nexport function resetCapabilitiesCache(): void {\n\tcachedCapabilities = null;\n}\n\n/** Override the cached capabilities. Useful in tests to exercise both code paths. */\nexport function setCapabilities(caps: TerminalCapabilities): void {\n\tcachedCapabilities = caps;\n}\n\nconst KITTY_PREFIX = \"\\x1b_G\";\nconst ITERM2_PREFIX = \"\\x1b]1337;File=\";\n\nexport function isImageLine(line: string): boolean {\n\t// Fast path: sequence at line start (single-row images)\n\tif (line.startsWith(KITTY_PREFIX) || line.startsWith(ITERM2_PREFIX)) {\n\t\treturn true;\n\t}\n\t// Slow path: sequence elsewhere (multi-row images have cursor-up prefix)\n\treturn line.includes(KITTY_PREFIX) || line.includes(ITERM2_PREFIX);\n}\n\n/**\n * Generate a random image ID for Kitty graphics protocol.\n * Uses random IDs to avoid collisions between different module instances\n * (e.g., main app vs extensions).\n */\nexport function allocateImageId(): number {\n\t// Use random ID in range [1, 0xffffffff] to avoid collisions\n\treturn Math.floor(Math.random() * 0xfffffffe) + 1;\n}\n\nexport function encodeKitty(\n\tbase64Data: string,\n\toptions: {\n\t\tcolumns?: number;\n\t\trows?: number;\n\t\timageId?: number;\n\t\t/** Whether Kitty should apply its default cursor movement after placement. Default: true. */\n\t\tmoveCursor?: boolean;\n\t} = {},\n): string {\n\tconst CHUNK_SIZE = 4096;\n\n\tconst params: string[] = [\"a=T\", \"f=100\", \"q=2\"];\n\n\tif (options.moveCursor === false) params.push(\"C=1\");\n\tif (options.columns) params.push(`c=${options.columns}`);\n\tif (options.rows) params.push(`r=${options.rows}`);\n\tif (options.imageId) params.push(`i=${options.imageId}`);\n\n\tif (base64Data.length <= CHUNK_SIZE) {\n\t\treturn `\\x1b_G${params.join(\",\")};${base64Data}\\x1b\\\\`;\n\t}\n\n\tconst chunks: string[] = [];\n\tlet offset = 0;\n\tlet isFirst = true;\n\n\twhile (offset < base64Data.length) {\n\t\tconst chunk = base64Data.slice(offset, offset + CHUNK_SIZE);\n\t\tconst isLast = offset + CHUNK_SIZE >= base64Data.length;\n\n\t\tif (isFirst) {\n\t\t\tchunks.push(`\\x1b_G${params.join(\",\")},m=1;${chunk}\\x1b\\\\`);\n\t\t\tisFirst = false;\n\t\t} else if (isLast) {\n\t\t\tchunks.push(`\\x1b_Gm=0;${chunk}\\x1b\\\\`);\n\t\t} else {\n\t\t\tchunks.push(`\\x1b_Gm=1;${chunk}\\x1b\\\\`);\n\t\t}\n\n\t\toffset += CHUNK_SIZE;\n\t}\n\n\treturn chunks.join(\"\");\n}\n\n/**\n * Delete a Kitty graphics image by ID.\n * Uses uppercase 'I' to also free the image data.\n */\nexport function deleteKittyImage(imageId: number): string {\n\treturn `\\x1b_Ga=d,d=I,i=${imageId},q=2\\x1b\\\\`;\n}\n\n/**\n * Delete all visible Kitty graphics images.\n * Uses uppercase 'A' to also free the image data.\n */\nexport function deleteAllKittyImages(): string {\n\treturn \"\\x1b_Ga=d,d=A,q=2\\x1b\\\\\";\n}\n\nexport function encodeITerm2(\n\tbase64Data: string,\n\toptions: {\n\t\twidth?: number | string;\n\t\theight?: number | string;\n\t\tname?: string;\n\t\tpreserveAspectRatio?: boolean;\n\t\tinline?: boolean;\n\t} = {},\n): string {\n\tconst params: string[] = [`inline=${options.inline !== false ? 1 : 0}`];\n\n\tif (options.width !== undefined) params.push(`width=${options.width}`);\n\tif (options.height !== undefined) params.push(`height=${options.height}`);\n\tif (options.name) {\n\t\tconst nameBase64 = Buffer.from(options.name).toString(\"base64\");\n\t\tparams.push(`name=${nameBase64}`);\n\t}\n\tif (options.preserveAspectRatio === false) {\n\t\tparams.push(\"preserveAspectRatio=0\");\n\t}\n\n\treturn `\\x1b]1337;File=${params.join(\";\")}:${base64Data}\\x07`;\n}\n\nexport interface ImageCellSize {\n\tcolumns: number;\n\trows: number;\n}\n\nexport function calculateImageCellSize(\n\timageDimensions: ImageDimensions,\n\tmaxWidthCells: number,\n\tmaxHeightCells?: number,\n\tcellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 },\n): ImageCellSize {\n\tconst maxWidth = Math.max(1, Math.floor(maxWidthCells));\n\tconst maxHeight = maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells));\n\tconst imageWidth = Math.max(1, imageDimensions.widthPx);\n\tconst imageHeight = Math.max(1, imageDimensions.heightPx);\n\n\tconst widthScale = (maxWidth * cellDimensions.widthPx) / imageWidth;\n\tconst heightScale = maxHeight === undefined ? widthScale : (maxHeight * cellDimensions.heightPx) / imageHeight;\n\tconst scale = Math.min(widthScale, heightScale);\n\n\tconst scaledWidthPx = imageWidth * scale;\n\tconst scaledHeightPx = imageHeight * scale;\n\tconst columns = Math.ceil(scaledWidthPx / cellDimensions.widthPx);\n\tconst rows = Math.ceil(scaledHeightPx / cellDimensions.heightPx);\n\n\treturn {\n\t\tcolumns: Math.max(1, Math.min(maxWidth, columns)),\n\t\trows: Math.max(1, maxHeight === undefined ? rows : Math.min(maxHeight, rows)),\n\t};\n}\n\nexport function calculateImageRows(\n\timageDimensions: ImageDimensions,\n\ttargetWidthCells: number,\n\tcellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 },\n): number {\n\treturn calculateImageCellSize(imageDimensions, targetWidthCells, undefined, cellDimensions).rows;\n}\n\nexport function getPngDimensions(base64Data: string): ImageDimensions | null {\n\ttry {\n\t\tconst buffer = Buffer.from(base64Data, \"base64\");\n\n\t\tif (buffer.length < 24) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (buffer[0] !== 0x89 || buffer[1] !== 0x50 || buffer[2] !== 0x4e || buffer[3] !== 0x47) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst width = buffer.readUInt32BE(16);\n\t\tconst height = buffer.readUInt32BE(20);\n\n\t\treturn { widthPx: width, heightPx: height };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function getJpegDimensions(base64Data: string): ImageDimensions | null {\n\ttry {\n\t\tconst buffer = Buffer.from(base64Data, \"base64\");\n\n\t\tif (buffer.length < 2) {\n\t\t\treturn null;\n\t\t}\n\n\t\tif (buffer[0] !== 0xff || buffer[1] !== 0xd8) {\n\t\t\treturn null;\n\t\t}\n\n\t\tlet offset = 2;\n\t\twhile (offset < buffer.length - 9) {\n\t\t\tif (buffer[offset] !== 0xff) {\n\t\t\t\toffset++;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst marker = buffer[offset + 1];\n\n\t\t\tif (marker >= 0xc0 && marker <= 0xc2) {\n\t\t\t\tconst height = buffer.readUInt16BE(offset + 5);\n\t\t\t\tconst width = buffer.readUInt16BE(offset + 7);\n\t\t\t\treturn { widthPx: width, heightPx: height };\n\t\t\t}\n\n\t\t\tif (offset + 3 >= buffer.length) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst length = buffer.readUInt16BE(offset + 2);\n\t\t\tif (length < 2) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\toffset += 2 + length;\n\t\t}\n\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function getGifDimensions(base64Data: string): ImageDimensions | null {\n\ttry {\n\t\tconst buffer = Buffer.from(base64Data, \"base64\");\n\n\t\tif (buffer.length < 10) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst sig = buffer.slice(0, 6).toString(\"ascii\");\n\t\tif (sig !== \"GIF87a\" && sig !== \"GIF89a\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst width = buffer.readUInt16LE(6);\n\t\tconst height = buffer.readUInt16LE(8);\n\n\t\treturn { widthPx: width, heightPx: height };\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function getWebpDimensions(base64Data: string): ImageDimensions | null {\n\ttry {\n\t\tconst buffer = Buffer.from(base64Data, \"base64\");\n\n\t\tif (buffer.length < 30) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst riff = buffer.slice(0, 4).toString(\"ascii\");\n\t\tconst webp = buffer.slice(8, 12).toString(\"ascii\");\n\t\tif (riff !== \"RIFF\" || webp !== \"WEBP\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst chunk = buffer.slice(12, 16).toString(\"ascii\");\n\t\tif (chunk === \"VP8 \") {\n\t\t\tif (buffer.length < 30) return null;\n\t\t\tconst width = buffer.readUInt16LE(26) & 0x3fff;\n\t\t\tconst height = buffer.readUInt16LE(28) & 0x3fff;\n\t\t\treturn { widthPx: width, heightPx: height };\n\t\t} else if (chunk === \"VP8L\") {\n\t\t\tif (buffer.length < 25) return null;\n\t\t\tconst bits = buffer.readUInt32LE(21);\n\t\t\tconst width = (bits & 0x3fff) + 1;\n\t\t\tconst height = ((bits >> 14) & 0x3fff) + 1;\n\t\t\treturn { widthPx: width, heightPx: height };\n\t\t} else if (chunk === \"VP8X\") {\n\t\t\tif (buffer.length < 30) return null;\n\t\t\tconst width = (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) + 1;\n\t\t\tconst height = (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) + 1;\n\t\t\treturn { widthPx: width, heightPx: height };\n\t\t}\n\n\t\treturn null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nexport function getImageDimensions(base64Data: string, mimeType: string): ImageDimensions | null {\n\tif (mimeType === \"image/png\") {\n\t\treturn getPngDimensions(base64Data);\n\t}\n\tif (mimeType === \"image/jpeg\") {\n\t\treturn getJpegDimensions(base64Data);\n\t}\n\tif (mimeType === \"image/gif\") {\n\t\treturn getGifDimensions(base64Data);\n\t}\n\tif (mimeType === \"image/webp\") {\n\t\treturn getWebpDimensions(base64Data);\n\t}\n\treturn null;\n}\n\nexport function renderImage(\n\tbase64Data: string,\n\timageDimensions: ImageDimensions,\n\toptions: ImageRenderOptions = {},\n): { sequence: string; rows: number; imageId?: number } | null {\n\tconst caps = getCapabilities();\n\n\tif (!caps.images) {\n\t\treturn null;\n\t}\n\n\tconst maxWidth = options.maxWidthCells ?? 80;\n\tconst size = calculateImageCellSize(imageDimensions, maxWidth, options.maxHeightCells, getCellDimensions());\n\n\tif (caps.images === \"kitty\") {\n\t\tconst sequence = encodeKitty(base64Data, {\n\t\t\tcolumns: size.columns,\n\t\t\trows: size.rows,\n\t\t\timageId: options.imageId,\n\t\t\tmoveCursor: options.moveCursor,\n\t\t});\n\t\treturn { sequence, rows: size.rows, imageId: options.imageId };\n\t}\n\n\tif (caps.images === \"iterm2\") {\n\t\tconst sequence = encodeITerm2(base64Data, {\n\t\t\twidth: size.columns,\n\t\t\theight: \"auto\",\n\t\t\tpreserveAspectRatio: options.preserveAspectRatio ?? true,\n\t\t});\n\t\treturn { sequence, rows: size.rows };\n\t}\n\n\treturn null;\n}\n\n/**\n * Wrap text in an OSC 8 hyperlink sequence.\n * The text is rendered as a clickable hyperlink in terminals that support OSC 8\n * (Ghostty, Kitty, WezTerm, iTerm2, VSCode, and others).\n * In terminals that do not support OSC 8, the escape sequences are ignored\n * and only the plain text is displayed.\n *\n * @param text - The visible text to display\n * @param url - The URL to link to\n */\nexport function hyperlink(text: string, url: string): string {\n\treturn `\\x1b]8;;${url}\\x1b\\\\${text}\\x1b]8;;\\x1b\\\\`;\n}\n\n/** Shorten home-prefixed absolute paths to ~/... for compact display. */\nfunction shortenImagePath(filename: string): string {\n\tconst home = homedir();\n\tif (home && (filename === home || filename.startsWith(`${home}/`) || filename.startsWith(`${home}\\\\`))) {\n\t\treturn `~${filename.slice(home.length)}`;\n\t}\n\treturn filename;\n}\n\n/**\n * Text fallback when the terminal cannot render inline images.\n * Absolute paths are shown shortened (~/...) and, when OSC 8 hyperlinks are\n * available, linked to file:// so the full path remains openable.\n */\nexport function imageFallback(mimeType: string, dimensions?: ImageDimensions, filename?: string): string {\n\tconst parts: string[] = [];\n\tif (filename) {\n\t\tconst display = shortenImagePath(filename);\n\t\tif (getCapabilities().hyperlinks && isAbsolute(filename)) {\n\t\t\tparts.push(hyperlink(display, pathToFileURL(filename).href));\n\t\t} else {\n\t\t\tparts.push(display);\n\t\t}\n\t}\n\tparts.push(`[${mimeType}]`);\n\tif (dimensions) parts.push(`${dimensions.widthPx}x${dimensions.heightPx}`);\n\treturn `[Image: ${parts.join(\" \")}]`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"terminal-image.d.ts","sourceRoot":"","sources":["../src/terminal-image.ts"],"names":[],"mappings":"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"}
|
package/dist/terminal-image.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { isAbsolute } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
let cachedCapabilities = null;
|
|
6
|
+
let capabilityOverrides = {};
|
|
6
7
|
// Default cell dimensions - updated by TUI when terminal responds to query
|
|
7
8
|
let cellDimensions = { widthPx: 9, heightPx: 18 };
|
|
8
9
|
export function getCellDimensions() {
|
|
@@ -32,12 +33,13 @@ function probeTmuxHyperlinks() {
|
|
|
32
33
|
return false;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
|
|
36
|
+
function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink) {
|
|
36
37
|
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
|
|
37
38
|
const terminalEmulator = process.env.TERMINAL_EMULATOR?.toLowerCase() || "";
|
|
38
39
|
const term = process.env.TERM?.toLowerCase() || "";
|
|
39
40
|
const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
|
|
40
|
-
const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit";
|
|
41
|
+
const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit" || term.endsWith("-direct");
|
|
42
|
+
const isWindowsConsole = process.platform === "win32";
|
|
41
43
|
// Emit OSC 8 hyperlinks only when tmux confirms it forwards.
|
|
42
44
|
// Image protocols are unreliable under tmux, so leave `images: null`.
|
|
43
45
|
if (process.env.TMUX || term.startsWith("tmux")) {
|
|
@@ -66,30 +68,70 @@ export function detectCapabilities(tmuxForwardsHyperlink = probeTmuxHyperlinks)
|
|
|
66
68
|
if (process.env.WT_SESSION) {
|
|
67
69
|
return { images: null, trueColor: true, hyperlinks: true };
|
|
68
70
|
}
|
|
69
|
-
if (termProgram === "vscode") {
|
|
70
|
-
return { images: null, trueColor: true, hyperlinks: true };
|
|
71
|
-
}
|
|
72
|
-
if (termProgram === "alacritty") {
|
|
71
|
+
if (termProgram === "alacritty" || termProgram === "vscode" || termProgram === "zed") {
|
|
73
72
|
return { images: null, trueColor: true, hyperlinks: true };
|
|
74
73
|
}
|
|
75
74
|
if (terminalEmulator === "jetbrains-jediterm") {
|
|
76
75
|
return { images: null, trueColor: true, hyperlinks: false };
|
|
77
76
|
}
|
|
77
|
+
// Windows Terminal does not always set WT_SESSION, for example when it hosts
|
|
78
|
+
// a cmd.exe launched directly from Win+R. Modern Windows consoles support
|
|
79
|
+
// truecolor; keep hyperlinks off unless we positively detected support above.
|
|
80
|
+
if (isWindowsConsole) {
|
|
81
|
+
return { images: null, trueColor: true, hyperlinks: false };
|
|
82
|
+
}
|
|
78
83
|
// Unknown terminal: be conservative. OSC 8 is rendered invisibly as "just
|
|
79
84
|
// text" on terminals that swallow it, which means the URL disappears from
|
|
80
85
|
// the rendered output. Default to the legacy `text (url)` behavior unless we
|
|
81
86
|
// have positively identified a hyperlink-capable terminal above.
|
|
82
87
|
return { images: null, trueColor: hasTrueColorHint, hyperlinks: false };
|
|
83
88
|
}
|
|
89
|
+
function parseBooleanCapabilityOverride(value) {
|
|
90
|
+
return value === "1" ? true : value === "0" ? false : undefined;
|
|
91
|
+
}
|
|
92
|
+
export function detectCapabilities(tmuxForwardsHyperlink = probeTmuxHyperlinks) {
|
|
93
|
+
const hyperlinks = parseBooleanCapabilityOverride(process.env.PI_HYPERLINKS);
|
|
94
|
+
const detected = detectCapabilitiesFromEnvironment(hyperlinks === undefined ? tmuxForwardsHyperlink : () => hyperlinks);
|
|
95
|
+
const imageProtocol = process.env.PI_IMAGE_PROTOCOL?.toLowerCase();
|
|
96
|
+
const images = imageProtocol === "kitty" || imageProtocol === "iterm2"
|
|
97
|
+
? imageProtocol
|
|
98
|
+
: imageProtocol === "none" || imageProtocol === "0"
|
|
99
|
+
? null
|
|
100
|
+
: undefined;
|
|
101
|
+
const trueColor = parseBooleanCapabilityOverride(process.env.PI_TRUE_COLOR);
|
|
102
|
+
return {
|
|
103
|
+
...detected,
|
|
104
|
+
...(images !== undefined ? { images } : {}),
|
|
105
|
+
...(trueColor !== undefined ? { trueColor } : {}),
|
|
106
|
+
...(hyperlinks !== undefined ? { hyperlinks } : {}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
84
109
|
export function getCapabilities() {
|
|
85
110
|
if (!cachedCapabilities) {
|
|
86
|
-
|
|
111
|
+
const hyperlinks = capabilityOverrides.hyperlinks;
|
|
112
|
+
cachedCapabilities = {
|
|
113
|
+
...detectCapabilities(hyperlinks === undefined ? undefined : () => hyperlinks),
|
|
114
|
+
...capabilityOverrides,
|
|
115
|
+
};
|
|
87
116
|
}
|
|
88
117
|
return cachedCapabilities;
|
|
89
118
|
}
|
|
119
|
+
export function getTerminalColorMode(capabilities = getCapabilities()) {
|
|
120
|
+
return capabilities.trueColor ? "truecolor" : "256color";
|
|
121
|
+
}
|
|
90
122
|
export function resetCapabilitiesCache() {
|
|
91
123
|
cachedCapabilities = null;
|
|
92
124
|
}
|
|
125
|
+
/** Override selected auto-detected capabilities. */
|
|
126
|
+
export function setCapabilityOverrides(overrides) {
|
|
127
|
+
if (capabilityOverrides.images === overrides.images &&
|
|
128
|
+
capabilityOverrides.trueColor === overrides.trueColor &&
|
|
129
|
+
capabilityOverrides.hyperlinks === overrides.hyperlinks) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
capabilityOverrides = { ...overrides };
|
|
133
|
+
cachedCapabilities = null;
|
|
134
|
+
}
|
|
93
135
|
/** Override the cached capabilities. Useful in tests to exercise both code paths. */
|
|
94
136
|
export function setCapabilities(caps) {
|
|
95
137
|
cachedCapabilities = caps;
|
|
@@ -161,8 +203,15 @@ export function deleteKittyImage(imageId) {
|
|
|
161
203
|
export function deleteAllKittyImages() {
|
|
162
204
|
return "\x1b_Ga=d,d=A,q=2\x1b\\";
|
|
163
205
|
}
|
|
206
|
+
/** Delete all visible Kitty placements while retaining their uploaded image data. */
|
|
207
|
+
export function deleteAllKittyPlacements() {
|
|
208
|
+
return "\x1b_Ga=d,d=a,q=2\x1b\\";
|
|
209
|
+
}
|
|
164
210
|
export function encodeITerm2(base64Data, options = {}) {
|
|
165
|
-
const params = [
|
|
211
|
+
const params = [
|
|
212
|
+
`inline=${options.inline !== false ? 1 : 0}`,
|
|
213
|
+
`size=${Buffer.byteLength(base64Data, "base64")}`,
|
|
214
|
+
];
|
|
166
215
|
if (options.width !== undefined)
|
|
167
216
|
params.push(`width=${options.width}`);
|
|
168
217
|
if (options.height !== undefined)
|
|
@@ -176,7 +225,141 @@ export function encodeITerm2(base64Data, options = {}) {
|
|
|
176
225
|
}
|
|
177
226
|
return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
|
|
178
227
|
}
|
|
179
|
-
|
|
228
|
+
const kittyImageMetadata = new Map();
|
|
229
|
+
let kittyTransmissionGeneration = 0;
|
|
230
|
+
export function registerKittyImageMetadata(metadata) {
|
|
231
|
+
kittyTransmissionGeneration += 1;
|
|
232
|
+
kittyImageMetadata.delete(metadata.imageId);
|
|
233
|
+
kittyImageMetadata.set(metadata.imageId, { ...metadata, transmissionGeneration: kittyTransmissionGeneration });
|
|
234
|
+
if (kittyImageMetadata.size > 1000) {
|
|
235
|
+
const oldestImageId = kittyImageMetadata.keys().next().value;
|
|
236
|
+
if (oldestImageId !== undefined)
|
|
237
|
+
kittyImageMetadata.delete(oldestImageId);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function getRegisteredKittyImageMetadataFromControls(controls) {
|
|
241
|
+
const imageId = /(?:^|,)i=(\d+)(?:,|$)/.exec(controls)?.[1];
|
|
242
|
+
return imageId === undefined ? undefined : kittyImageMetadata.get(Number.parseInt(imageId, 10));
|
|
243
|
+
}
|
|
244
|
+
function getRegisteredKittyImageMetadata(line) {
|
|
245
|
+
const controls = /\x1b_G([^;]*);/.exec(line)?.[1];
|
|
246
|
+
return controls === undefined ? undefined : getRegisteredKittyImageMetadataFromControls(controls);
|
|
247
|
+
}
|
|
248
|
+
function getExplicitKittyImageRows(controls) {
|
|
249
|
+
const value = /(?:^|,)r=(\d+)(?:,|$)/.exec(controls)?.[1];
|
|
250
|
+
if (value === undefined)
|
|
251
|
+
return undefined;
|
|
252
|
+
const rows = Number.parseInt(value, 10);
|
|
253
|
+
return rows > 0 ? rows : undefined;
|
|
254
|
+
}
|
|
255
|
+
function getKittyImageRowsFromControls(controls, fallbackRows) {
|
|
256
|
+
return getExplicitKittyImageRows(controls) ?? fallbackRows;
|
|
257
|
+
}
|
|
258
|
+
export function getKittyImageMetadata(line) {
|
|
259
|
+
const metadata = getRegisteredKittyImageMetadata(line);
|
|
260
|
+
if (!metadata)
|
|
261
|
+
return undefined;
|
|
262
|
+
return {
|
|
263
|
+
imageId: metadata.imageId,
|
|
264
|
+
columns: metadata.columns,
|
|
265
|
+
rows: metadata.rows,
|
|
266
|
+
widthPx: metadata.widthPx,
|
|
267
|
+
heightPx: metadata.heightPx,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const KITTY_PLACEMENT_CONTROL_KEYS = new Set([
|
|
271
|
+
"i",
|
|
272
|
+
"p",
|
|
273
|
+
"x",
|
|
274
|
+
"y",
|
|
275
|
+
"w",
|
|
276
|
+
"h",
|
|
277
|
+
"X",
|
|
278
|
+
"Y",
|
|
279
|
+
"c",
|
|
280
|
+
"r",
|
|
281
|
+
"C",
|
|
282
|
+
"U",
|
|
283
|
+
"z",
|
|
284
|
+
"P",
|
|
285
|
+
"Q",
|
|
286
|
+
"H",
|
|
287
|
+
"V",
|
|
288
|
+
]);
|
|
289
|
+
/** Read the number of rows covered by an image placement without scanning its payload. */
|
|
290
|
+
export function getKittyImagePlacementRows(line) {
|
|
291
|
+
const controls = /\x1b_G([^;]*);/.exec(line)?.[1];
|
|
292
|
+
if (controls === undefined)
|
|
293
|
+
return undefined;
|
|
294
|
+
const explicitRows = getExplicitKittyImageRows(controls);
|
|
295
|
+
if (explicitRows !== undefined)
|
|
296
|
+
return explicitRows;
|
|
297
|
+
return getRegisteredKittyImageMetadataFromControls(controls)?.rows;
|
|
298
|
+
}
|
|
299
|
+
/** Build a placement-only command for an image line emitted by {@link renderImage}. */
|
|
300
|
+
export function getKittyImagePlacement(line) {
|
|
301
|
+
const match = /\x1b_G([^;]*);/.exec(line);
|
|
302
|
+
if (!match)
|
|
303
|
+
return undefined;
|
|
304
|
+
const metadata = getRegisteredKittyImageMetadataFromControls(match[1]);
|
|
305
|
+
if (!metadata)
|
|
306
|
+
return undefined;
|
|
307
|
+
let commandStart = match.index;
|
|
308
|
+
let commandControls = match[1];
|
|
309
|
+
let transmissionEnd;
|
|
310
|
+
while (true) {
|
|
311
|
+
const terminator = line.indexOf("\x1b\\", commandStart + KITTY_PREFIX.length);
|
|
312
|
+
if (terminator === -1)
|
|
313
|
+
return undefined;
|
|
314
|
+
transmissionEnd = terminator + 2;
|
|
315
|
+
if (!/(?:^|,)m=1(?:,|$)/.test(commandControls))
|
|
316
|
+
break;
|
|
317
|
+
commandStart = transmissionEnd;
|
|
318
|
+
if (!line.startsWith(KITTY_PREFIX, commandStart))
|
|
319
|
+
return undefined;
|
|
320
|
+
const controlsEnd = line.indexOf(";", commandStart + KITTY_PREFIX.length);
|
|
321
|
+
if (controlsEnd === -1)
|
|
322
|
+
return undefined;
|
|
323
|
+
commandControls = line.slice(commandStart + KITTY_PREFIX.length, controlsEnd);
|
|
324
|
+
}
|
|
325
|
+
const controls = match[1]
|
|
326
|
+
.split(",")
|
|
327
|
+
.filter((control) => KITTY_PLACEMENT_CONTROL_KEYS.has(control.split("=", 1)[0] ?? ""));
|
|
328
|
+
const sequence = `\x1b_Ga=p,q=2,${controls.join(",")}\x1b\\`;
|
|
329
|
+
return {
|
|
330
|
+
imageId: metadata.imageId,
|
|
331
|
+
transmissionGeneration: metadata.transmissionGeneration,
|
|
332
|
+
transmissionBytes: transmissionEnd - match.index,
|
|
333
|
+
estimatedDecodedBytes: metadata.widthPx * metadata.heightPx * 4,
|
|
334
|
+
rows: getKittyImageRowsFromControls(match[1], metadata.rows),
|
|
335
|
+
sequence,
|
|
336
|
+
replacementLine: `${line.slice(0, match.index)}${sequence}${line.slice(transmissionEnd)}`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
export function cropKittyImageLine(line, hiddenRows, visibleRows) {
|
|
340
|
+
const metadata = getKittyImageMetadata(line);
|
|
341
|
+
const match = /\x1b_G([^;]*);/.exec(line);
|
|
342
|
+
if (!metadata || !match || hiddenRows < 0 || hiddenRows >= metadata.rows || visibleRows <= 0)
|
|
343
|
+
return line;
|
|
344
|
+
const croppedRows = Math.min(visibleRows, metadata.rows - hiddenRows);
|
|
345
|
+
if (hiddenRows === 0 && croppedRows === metadata.rows)
|
|
346
|
+
return line;
|
|
347
|
+
const sourceY = Math.floor((metadata.heightPx * hiddenRows) / metadata.rows);
|
|
348
|
+
const sourceEnd = Math.ceil((metadata.heightPx * (hiddenRows + croppedRows)) / metadata.rows);
|
|
349
|
+
const sourceHeight = Math.max(1, Math.min(metadata.heightPx, sourceEnd) - sourceY);
|
|
350
|
+
const controls = match[1].split(",").filter((control) => !/^[yhr]=/.test(control));
|
|
351
|
+
controls.push(`y=${sourceY}`, `h=${sourceHeight}`, `r=${croppedRows}`);
|
|
352
|
+
return `${line.slice(0, match.index)}\x1b_G${controls.join(",")};${line.slice(match.index + match[0].length)}`;
|
|
353
|
+
}
|
|
354
|
+
function chooseLessDistortedCellCount(upperCount, idealCount) {
|
|
355
|
+
if (upperCount <= 1)
|
|
356
|
+
return upperCount;
|
|
357
|
+
const lowerCount = upperCount - 1;
|
|
358
|
+
const upperDistortion = Math.max(upperCount / idealCount, idealCount / upperCount);
|
|
359
|
+
const lowerDistortion = Math.max(lowerCount / idealCount, idealCount / lowerCount);
|
|
360
|
+
return lowerDistortion < upperDistortion ? lowerCount : upperCount;
|
|
361
|
+
}
|
|
362
|
+
export function calculateImageCellSize(imageDimensions, maxWidthCells, maxHeightCells, cellDimensions = { widthPx: 9, heightPx: 18 }, optimizeAspectRatio = false) {
|
|
180
363
|
const maxWidth = Math.max(1, Math.floor(maxWidthCells));
|
|
181
364
|
const maxHeight = maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells));
|
|
182
365
|
const imageWidth = Math.max(1, imageDimensions.widthPx);
|
|
@@ -186,12 +369,24 @@ export function calculateImageCellSize(imageDimensions, maxWidthCells, maxHeight
|
|
|
186
369
|
const scale = Math.min(widthScale, heightScale);
|
|
187
370
|
const scaledWidthPx = imageWidth * scale;
|
|
188
371
|
const scaledHeightPx = imageHeight * scale;
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
rows
|
|
194
|
-
}
|
|
372
|
+
let columns = Math.max(1, Math.min(maxWidth, Math.ceil(scaledWidthPx / cellDimensions.widthPx)));
|
|
373
|
+
const heightRows = scaledHeightPx / cellDimensions.heightPx;
|
|
374
|
+
let rows = Math.max(1, Math.ceil(heightRows));
|
|
375
|
+
if (maxHeight !== undefined) {
|
|
376
|
+
rows = Math.min(maxHeight, rows);
|
|
377
|
+
}
|
|
378
|
+
if (!optimizeAspectRatio) {
|
|
379
|
+
return { columns, rows };
|
|
380
|
+
}
|
|
381
|
+
if (widthScale <= heightScale) {
|
|
382
|
+
const idealRows = (columns * cellDimensions.widthPx * imageHeight) / (imageWidth * cellDimensions.heightPx);
|
|
383
|
+
rows = chooseLessDistortedCellCount(rows, idealRows);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
const idealColumns = (rows * cellDimensions.heightPx * imageWidth) / (imageHeight * cellDimensions.widthPx);
|
|
387
|
+
columns = chooseLessDistortedCellCount(columns, idealColumns);
|
|
388
|
+
}
|
|
389
|
+
return { columns, rows };
|
|
195
390
|
}
|
|
196
391
|
export function calculateImageRows(imageDimensions, targetWidthCells, cellDimensions = { widthPx: 9, heightPx: 18 }) {
|
|
197
392
|
return calculateImageCellSize(imageDimensions, targetWidthCells, undefined, cellDimensions).rows;
|
|
@@ -328,15 +523,25 @@ export function renderImage(base64Data, imageDimensions, options = {}) {
|
|
|
328
523
|
return null;
|
|
329
524
|
}
|
|
330
525
|
const maxWidth = options.maxWidthCells ?? 80;
|
|
331
|
-
|
|
526
|
+
// Reduce Kitty's cell-aligned distortion without shrinking iTerm2 reservations.
|
|
527
|
+
const size = calculateImageCellSize(imageDimensions, maxWidth, options.maxHeightCells, getCellDimensions(), caps.images === "kitty");
|
|
332
528
|
if (caps.images === "kitty") {
|
|
529
|
+
if (options.imageId !== undefined) {
|
|
530
|
+
registerKittyImageMetadata({
|
|
531
|
+
imageId: options.imageId,
|
|
532
|
+
columns: size.columns,
|
|
533
|
+
rows: size.rows,
|
|
534
|
+
widthPx: imageDimensions.widthPx,
|
|
535
|
+
heightPx: imageDimensions.heightPx,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
333
538
|
const sequence = encodeKitty(base64Data, {
|
|
334
539
|
columns: size.columns,
|
|
335
540
|
rows: size.rows,
|
|
336
541
|
imageId: options.imageId,
|
|
337
542
|
moveCursor: options.moveCursor,
|
|
338
543
|
});
|
|
339
|
-
return { sequence, rows: size.rows, imageId: options.imageId };
|
|
544
|
+
return { sequence, columns: size.columns, rows: size.rows, imageId: options.imageId };
|
|
340
545
|
}
|
|
341
546
|
if (caps.images === "iterm2") {
|
|
342
547
|
const sequence = encodeITerm2(base64Data, {
|
|
@@ -344,7 +549,7 @@ export function renderImage(base64Data, imageDimensions, options = {}) {
|
|
|
344
549
|
height: "auto",
|
|
345
550
|
preserveAspectRatio: options.preserveAspectRatio ?? true,
|
|
346
551
|
});
|
|
347
|
-
return { sequence, rows: size.rows };
|
|
552
|
+
return { sequence, columns: size.columns, rows: size.rows };
|
|
348
553
|
}
|
|
349
554
|
return null;
|
|
350
555
|
}
|