import { createSignal, onCleanup } from "solid-js"; import { ASCII_VIDEO_FPS, ASCII_VIDEO_FRAMES } from "../../dist/ascii-video-animation.js"; // On the home screen the logo slot shares the column with (above it) a flexible // spacer + a 4-row gap, and (below it) the prompt box and footer. Those lower // elements do not shrink, so we must keep the logo within `terminalHeight minus // this reserve` or its top/bottom gets clipped off the screen. const VERTICAL_CHROME_ROWS = 9; // Fallbacks used only before the renderer reports a real size. const FALLBACK_COLUMNS = 120; const FALLBACK_ROWS = 40; type BoundingBox = { lines: string[]; left: number; width: number; }; function boundingBox(frame: string): BoundingBox | null { const lines = frame.split("\n"); const nonEmptyRows = lines .map((line, index) => (line.trim() ? index : -1)) .filter(index => index >= 0); if (nonEmptyRows.length === 0) return null; const top = nonEmptyRows[0]; const bottom = nonEmptyRows[nonEmptyRows.length - 1]; const visibleLines = lines.slice(top, bottom + 1).map(line => line.replace(/\s+$/, "")); const left = Math.min( ...visibleLines.map(line => (line.length ? line.length - line.trimStart().length : Number.POSITIVE_INFINITY)) ); const right = Math.max(...visibleLines.map(line => line.length), 0); return { lines: visibleLines, left: Number.isFinite(left) ? left : 0, width: Math.max(0, right - left) }; } // Sample `count` evenly spaced indices across a span of `length` items. function sampleIndices(length: number, count: number): number[] { if (count >= length) return Array.from({ length }, (_, index) => index); const indices: number[] = []; for (let i = 0; i < count; i++) { indices.push(Math.min(length - 1, Math.round((i * (length - 1)) / (count - 1 || 1)))); } return indices; } function fitFrame(frame: string, terminalColumns: number, terminalRows: number): string { const box = boundingBox(frame); if (!box) return ""; const { lines, left, width } = box; const height = lines.length; const availableColumns = Math.max(20, terminalColumns - 2); const availableRows = Math.max(6, terminalRows - VERTICAL_CHROME_ROWS); // Single uniform scale so the whole logo fits without distorting its aspect. const scale = Math.min(1, availableColumns / Math.max(1, width), availableRows / height); const targetHeight = Math.max(1, Math.round(height * scale)); const targetWidth = Math.max(1, Math.round(width * scale)); const rowIndices = sampleIndices(height, targetHeight); const columnIndices = sampleIndices(width, targetWidth); return rowIndices .map(rowIndex => { const line = lines[rowIndex]; return columnIndices .map(columnIndex => line[left + columnIndex] ?? " ") .join("") .replace(/\s+$/, ""); }) .join("\n"); } function AnimatedLogo(props: { renderer?: any }) { const [frameIndex, setFrameIndex] = createSignal(0); // Track the real terminal size from the TUI renderer (process.stdout.rows is // unreliable inside the plugin runtime). Re-fit whenever the terminal resizes. const renderer = props.renderer; const readSize = () => ({ columns: Math.max(40, renderer?.width ?? process.stdout.columns ?? FALLBACK_COLUMNS), rows: Math.max(10, renderer?.height ?? process.stdout.rows ?? FALLBACK_ROWS), }); const [size, setSize] = createSignal(readSize()); if (typeof renderer?.on === "function") { const onResize = () => setSize(readSize()); renderer.on("resize", onResize); onCleanup(() => { if (typeof renderer.off === "function") renderer.off("resize", onResize); else if (typeof renderer.removeListener === "function") renderer.removeListener("resize", onResize); }); } const frameDelayMs = Math.max(16, Math.round(1000 / ASCII_VIDEO_FPS)); const timer = setInterval(() => { setFrameIndex(index => (index + 1) % ASCII_VIDEO_FRAMES.length); }, frameDelayMs); onCleanup(() => clearInterval(timer)); return ( {fitFrame(ASCII_VIDEO_FRAMES[frameIndex()] ?? "", size().columns, size().rows)} ); } const tui = async (api: any) => { api.slots.register({ slots: { home_logo() { return ; }, }, }); }; // The TUI plugin loader reads `mod.default` (strict mode) and requires an object // with a tui() function — a bare `export const tui` is silently skipped. export default { id: "infinicode-home-logo", tui };