| import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; |
| import { useOnRender } from "../InkPictureProvider.js"; |
| import { cursorForward, cursorUp } from "../utils/ansiEscapes.js"; |
| import bgColorize from "../utils/bgColorize.js"; |
| import type { Position } from "./usePosition.js"; |
| import { defaultVisibility } from "./useVisibility.js"; |
|
|
| interface DirectRendererOptions { |
| enabled: boolean; |
| imageOutput: string; |
| position: Position | undefined; |
| stdout: NodeJS.WriteStream; |
| width: number; |
| height: number; |
| backgroundColor?: string; |
| } |
|
|
| function writeImageToStdout( |
| pos: Position, |
| output: string, |
| stream: NodeJS.WriteStream, |
| w: number, |
| h: number, |
| ): { row: number; col: number; width: number; height: number } { |
| stream.write("\x1b7"); |
| stream.write( |
| cursorUp(pos.appHeight - pos.row, { |
| appHeight: pos.appHeight, |
| terminalHeight: stream.rows, |
| }), |
| ); |
| stream.write("\r"); |
| stream.write(cursorForward(pos.col)); |
| stream.write(output); |
| stream.write("\x1b8"); |
|
|
| return { |
| row: stream.rows - pos.appHeight + pos.row, |
| col: pos.col, |
| width: w, |
| height: h, |
| }; |
| } |
|
|
| export function useDirectRenderer(options: DirectRendererOptions) { |
| const { position, stdout, backgroundColor } = options; |
| const shouldCleanupRef = useRef(true); |
| const previousBboxRef = useRef< |
| { row: number; col: number; width: number; height: number } | undefined |
| >(undefined); |
|
|
| const optionsRef = useRef(options); |
| optionsRef.current = options; |
|
|
| const writeNow = useCallback(() => { |
| const opt = optionsRef.current; |
| if (!opt.enabled || !opt.position) return; |
| if ( |
| defaultVisibility(opt.position, opt.stdout.rows, opt.stdout.columns) !== |
| "full" |
| ) |
| return; |
|
|
| previousBboxRef.current = writeImageToStdout( |
| opt.position, |
| opt.imageOutput, |
| opt.stdout, |
| opt.width, |
| opt.height, |
| ); |
| }, []); |
|
|
| |
| |
| useOnRender(writeNow); |
|
|
| useLayoutEffect(() => { |
| return () => { |
| if (!shouldCleanupRef.current) return; |
|
|
| const bbox = previousBboxRef.current; |
| if (!bbox || !position) return; |
|
|
| stdout.write("\x1b7"); |
| stdout.write( |
| cursorUp(position.appHeight - position.row, { |
| appHeight: position.appHeight, |
| terminalHeight: stdout.rows, |
| }), |
| ); |
| for (let i = 0; i < bbox.height; i++) { |
| stdout.write("\r"); |
| stdout.write(cursorForward(bbox.col)); |
| if (backgroundColor) { |
| stdout.write(bgColorize(" ".repeat(bbox.width), backgroundColor)); |
| } else { |
| stdout.write(" ".repeat(bbox.width)); |
| } |
| stdout.write("\n"); |
| } |
| stdout.write("\x1b8"); |
| }; |
| }); |
|
|
| useEffect(() => { |
| function onExit() { |
| shouldCleanupRef.current = false; |
| } |
| function onSigInt() { |
| shouldCleanupRef.current = false; |
| process.exit(); |
| } |
| 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); |
| }; |
| }, []); |
| } |
|
|