import { Box, Spacer, Text, useInput } from "ink"; import { useEffect, useMemo, useState, type ComponentType, type ReactNode, } from "react"; export function ListDisplay({ items, Component, height, isFocused, status, onSelect, }: { items: T[]; Component: ComponentType<{ item: T; selected: boolean }>; height: number; isFocused?: boolean; status?: ReactNode; onSelect?: (item: T) => void; }) { const [start, setStart] = useState(0); const slice = Math.min(start + height - 1, items.length); const [selected, setSelected] = useState(0); useInput( (input, key) => { if (key.upArrow) { setSelected((prev) => (prev > 0 ? prev - 1 : items.length - 1)); } else if (key.downArrow) { setSelected((prev) => (prev + 1) % items.length); } else if (input === " " || key.return) { onSelect?.(items[selected]); } }, { isActive: isFocused } ); useEffect(() => { if (selected < start) setStart(selected); else if (selected >= slice) setStart( Math.max(Math.min(selected - slice + start + 1, items.length - 1), 0) ); }, [selected, start, slice, items.length]); useEffect(() => { if (selected < 0) setSelected(Math.max(items.length - 1, 0)); if (selected >= items.length) setSelected(0); }, [selected, items.length]); useEffect(() => { setSelected(0); }, [items]); const indexed = useMemo( () => items.map((item, index) => ({ item, index })), [items] ); return ( {indexed.slice(start, slice).map(({ item, index }) => ( ))} {items.length < height - 1 && ( )} {status} {items.length ? ( <> {start + 1}-{slice}/{items.length} ) : ( <>(empty) )} ); }