| import { useStdout } from "src/ink"; |
| import fs from "node:fs"; |
| import React, { useCallback, useEffect, useRef, useState } from "react"; |
| import { useOnRender } from "../../InkPictureProvider.js"; |
| import { cursorForward } from "../../utils/ansiEscapes.js"; |
| import { useImage } from "../../hooks/useImage.js"; |
| import { useMeasuredSize } from "../../hooks/useMeasuredSize.js"; |
| import usePosition from "../../hooks/usePosition.js"; |
| import { defaultVisibility } from "../../hooks/useVisibility.js"; |
| import { useTerminalInfo } from "../../InkPictureProvider.js"; |
| import { |
| makeKittyDeletion, |
| makeKittyPlacement, |
| makeKittyTransmitChunks, |
| } from "../../renderers/kitty.js"; |
| import generateKittyId from "../../utils/generateKittyId.js"; |
| import ImageBox from "../ImageBox.js"; |
| import type { ImageProps } from "./protocol.js"; |
|
|
| function KittyImage(props: ImageProps) { |
| const terminalInfo = useTerminalInfo(); |
| const { stdout } = useStdout(); |
| const { src, width, height, pixelWidth, pixelHeight, alt } = props; |
|
|
| const { containerRef, resolvedWidth, resolvedHeight } = useMeasuredSize( |
| width, |
| height, |
| ); |
| const position = usePosition(containerRef); |
|
|
| |
| const actualPixelWidth = pixelWidth ?? resolvedWidth * (terminalInfo?.cellWidth ?? 0); |
| const actualPixelHeight = pixelHeight ?? resolvedHeight * (terminalInfo?.cellHeight ?? 0); |
|
|
| const { imageData, error } = useImage({ |
| src, |
| pixelWidth: actualPixelWidth, |
| pixelHeight: actualPixelHeight, |
| mode: "png", |
| }); |
|
|
| const [imageId, setImageId] = useState<number | undefined>(undefined); |
| const shouldCleanupRef = useRef(true); |
| const imageIdRef = useRef<number | undefined>(undefined); |
| const dimsRef = useRef({ w: resolvedWidth, h: resolvedHeight }); |
| dimsRef.current = { w: resolvedWidth, h: resolvedHeight }; |
|
|
| |
| |
| useEffect(() => { |
| if (!imageData) return; |
|
|
| const id = generateKittyId(); |
| const base64Data = imageData.data.toString("base64"); |
| const chunks = makeKittyTransmitChunks(id, base64Data); |
| const fd = process.stdout.fd; |
| for (const chunk of chunks) { |
| fs.writeSync(fd, chunk); |
| } |
| imageIdRef.current = id; |
| setImageId(id); |
| }, [imageData]); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| useOnRender(() => { |
| const id = imageIdRef.current; |
| if (!id) return; |
| const pos = position; |
| if (!pos) return; |
| const { w, h } = dimsRef.current; |
| if (h <= 0) return; |
|
|
| |
| const visibility = defaultVisibility(pos, stdout.rows, stdout.columns); |
| if (visibility !== "full") return; |
|
|
| |
| |
| const appHeight = pos.appHeight; |
| const terminalHeight = stdout.rows; |
| const cursorUpCount = appHeight - pos.row; |
| const movementCount = |
| appHeight >= terminalHeight ? cursorUpCount - 1 : cursorUpCount; |
| if (movementCount <= 0) return; |
|
|
| |
| |
| const fd = process.stdout.fd; |
| const buf = Buffer.concat([ |
| Buffer.from(`\x1b7`), |
| Buffer.from(`\x1b[${movementCount}A`), |
| Buffer.from(`\r`), |
| Buffer.from(cursorForward(pos.col)), |
| Buffer.from(makeKittyPlacement(id, 1, w, h)), |
| Buffer.from(`\x1b8`), |
| ]); |
| fs.writeSync(fd, buf); |
| }); |
|
|
| const onExit = useCallback(() => { |
| shouldCleanupRef.current = false; |
| }, []); |
|
|
| const onSigInt = useCallback(() => { |
| shouldCleanupRef.current = false; |
| process.exit(); |
| }, []); |
|
|
| useEffect(() => { |
| process.on("exit", onExit); |
| process.on("SIGINT", onSigInt); |
| process.on("SIGTERM", onSigInt); |
|
|
| return () => { |
| process.removeListener("exit", onExit); |
| process.removeListener("SIGINT", onSigInt); |
| process.removeListener("SIGTERM", onSigInt); |
| if (!shouldCleanupRef.current) return; |
| if (!imageId) return; |
| const fd = process.stdout.fd; |
| fs.writeSync(fd, makeKittyDeletion(imageId)); |
| }; |
| }, [imageId, onExit, onSigInt]); |
|
|
| return ( |
| <ImageBox |
| ref={containerRef} |
| width={width} |
| height={height} |
| alt={alt} |
| error={error} |
| loaded={!!imageId} |
| /> |
| ); |
| } |
|
|
| export default KittyImage; |
|
|