All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
81 lines
2.0 KiB
TypeScript
81 lines
2.0 KiB
TypeScript
import { useState, useCallback, useEffect } from 'react';
|
|
import { useStdout } from 'ink';
|
|
|
|
export interface UseViewportOptions {
|
|
totalItems: number;
|
|
reservedLines?: number;
|
|
}
|
|
|
|
export interface UseViewportReturn {
|
|
scrollOffset: number;
|
|
viewportSize: number;
|
|
isScrolledUp: boolean;
|
|
scrollToBottom: () => void;
|
|
scrollBy: (delta: number) => void;
|
|
scrollTo: (offset: number) => void;
|
|
canScrollUp: boolean;
|
|
canScrollDown: boolean;
|
|
}
|
|
|
|
export function useViewport({
|
|
totalItems,
|
|
reservedLines = 10,
|
|
}: UseViewportOptions): UseViewportReturn {
|
|
const { stdout } = useStdout();
|
|
const rows = stdout?.rows ?? 24;
|
|
const viewportSize = Math.max(1, rows - reservedLines);
|
|
|
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
const [autoFollow, setAutoFollow] = useState(true);
|
|
|
|
// Compute the maximum valid scroll offset
|
|
const maxOffset = Math.max(0, totalItems - viewportSize);
|
|
|
|
// Auto-follow: when new items arrive and auto-follow is on, snap to bottom
|
|
useEffect(() => {
|
|
if (autoFollow) {
|
|
setScrollOffset(maxOffset);
|
|
}
|
|
}, [autoFollow, maxOffset]);
|
|
|
|
const scrollTo = useCallback(
|
|
(offset: number) => {
|
|
const clamped = Math.max(0, Math.min(offset, maxOffset));
|
|
setScrollOffset(clamped);
|
|
setAutoFollow(clamped >= maxOffset);
|
|
},
|
|
[maxOffset],
|
|
);
|
|
|
|
const scrollBy = useCallback(
|
|
(delta: number) => {
|
|
setScrollOffset((prev) => {
|
|
const next = Math.max(0, Math.min(prev + delta, maxOffset));
|
|
setAutoFollow(next >= maxOffset);
|
|
return next;
|
|
});
|
|
},
|
|
[maxOffset],
|
|
);
|
|
|
|
const scrollToBottom = useCallback(() => {
|
|
setScrollOffset(maxOffset);
|
|
setAutoFollow(true);
|
|
}, [maxOffset]);
|
|
|
|
const isScrolledUp = scrollOffset < maxOffset;
|
|
const canScrollUp = scrollOffset > 0;
|
|
const canScrollDown = scrollOffset < maxOffset;
|
|
|
|
return {
|
|
scrollOffset,
|
|
viewportSize,
|
|
isScrolledUp,
|
|
scrollToBottom,
|
|
scrollBy,
|
|
scrollTo,
|
|
canScrollUp,
|
|
canScrollDown,
|
|
};
|
|
}
|