/** * Pure cursor logic for vim-style list navigation in the TUI. * * The two list panels (FeedList, ArticleList) share the same `j/k/g/G` shape * but used to hand-roll it twice with subtly different scroll-echo guards. * This Module owns the math; `useListCursor` and `useScrollEchoGuard` are * thin React adapters over it. */ export type NavKey = 'down' | 'up' | 'home' | 'end'; /** * Translate a raw keyboard event into a navigation intent. Returns `null` * when the keystroke isn't one of `j/k/g/G/ArrowUp/ArrowDown` — the caller * should let other handlers see those keys. */ export function navKeyFromInput(name: string, shift: boolean): NavKey | null { if (name === 'j' || name === 'down') return 'down'; if (name === 'k' || name === 'up') return 'up'; if (name === 'g') return shift ? 'end' : 'home'; return null; } /** New cursor position after applying `key`, clamped to `[0, length-1]`. */ export function nextCursor(prev: number, key: NavKey, length: number): number { if (length <= 0) return 0; switch (key) { case 'down': return Math.min(prev + 1, length - 1); case 'up': return Math.max(prev - 1, 0); case 'home': return 0; case 'end': return length - 1; } } /** * Some terminals fire a phantom arrow key shortly after a mouse-wheel scroll. * `armEcho` records the scroll direction + an expiry; `shouldSwallowEcho` * suppresses *only* the next matching-direction key, leaving real keypresses * (including opposite-direction ones) to pass through. */ export interface ScrollEcho { direction: 'up' | 'down'; expiresAt: number; } export function shouldSwallowEcho(echo: ScrollEcho | null, key: 'up' | 'down', now: number): boolean { if (!echo) return false; if (now > echo.expiresAt) return false; return echo.direction === key; }