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 };