import type { LayoutMode } from './state/types'; export const MIN_FEED_WIDTH = 18; export const MAX_FEED_WIDTH = 60; export const MIN_ARTICLE_WIDTH = 20; export const MIN_READER_WIDTH = 20; export interface PanelWidths { feedWidth: number; articleWidth: number; minFeedWidth: number; maxFeedWidth: number; minArticleWidth: number; maxArticleWidth: number; } export interface OverlayRect { left: number; top: number; width: number; height: number; padding: number; contentWidth: number; contentHeight: number; } export interface OverlayRectOptions { widthRatio: number; minWidth: number; height: number | { ratio: number; minHeight: number }; leftRatio: number; topRatio: number; padding?: number; } /** Resolve an absolute overlay rectangle that can never exceed the terminal. */ export function computeOverlayRect( terminalWidth: number, terminalHeight: number, options: OverlayRectOptions, ): OverlayRect { const availableWidth = Math.max(0, Math.floor(Number.isFinite(terminalWidth) ? terminalWidth : 0)); const availableHeight = Math.max(0, Math.floor(Number.isFinite(terminalHeight) ? terminalHeight : 0)); const targetWidth = Math.max(options.minWidth, Math.floor(availableWidth * options.widthRatio)); const targetHeight = typeof options.height === 'number' ? options.height : Math.max(options.height.minHeight, Math.ceil(availableHeight * options.height.ratio)); const width = Math.min(availableWidth, Math.max(availableWidth > 0 ? 1 : 0, targetWidth)); const height = Math.min(availableHeight, Math.max(availableHeight > 0 ? 1 : 0, targetHeight)); const left = Math.min( Math.max(0, availableWidth - width), Math.max(0, Math.floor(availableWidth * options.leftRatio)), ); const top = Math.min( Math.max(0, availableHeight - height), Math.max(0, Math.floor(availableHeight * options.topRatio)), ); const desiredPadding = Math.max(0, Math.floor(options.padding ?? 1)); const padding = Math.min( desiredPadding, Math.floor(Math.max(0, width - 2) / 2), Math.floor(Math.max(0, height - 2) / 2), ); return { left, top, width, height, padding, contentWidth: Math.max(0, width - 2 - padding * 2), contentHeight: Math.max(0, height - 2 - padding * 2), }; } function clampPaneWidth(value: number, minimum: number, maximum: number): number { const safeMaximum = Math.max(0, Math.floor(maximum)); const safeMinimum = Math.min(safeMaximum, Math.max(0, Math.floor(minimum))); const safeValue = Number.isFinite(value) ? Math.floor(value) : safeMinimum; return Math.max(safeMinimum, Math.min(safeMaximum, safeValue)); } /** * Resolve pane widths once for both layout and child rendering. * * Fixed usability minima only apply when the terminal can contain them. On a * tiny terminal each minimum shrinks to the available budget, so explicit * panes never sum to more cells than their parent. */ export function computePanelWidths( terminalWidth: number, layoutMode: LayoutMode, requestedFeedWidth?: number | null, requestedArticleWidth?: number | null, ): PanelWidths { const width = Math.max(0, Math.floor(Number.isFinite(terminalWidth) ? terminalWidth : 0)); // Narrow mode renders one full-width pane at a time. Returning the terminal // width prevents FeedList from formatting itself to a stale sidebar width. if (layoutMode === 'narrow') { return { feedWidth: width, articleWidth: width, minFeedWidth: width, maxFeedWidth: width, minArticleWidth: width, maxArticleWidth: width, }; } const maxFeedWidth = Math.max(0, Math.min(MAX_FEED_WIDTH, Math.floor(width * 0.5))); const minFeedWidth = Math.min(MIN_FEED_WIDTH, maxFeedWidth); const autoFeedWidth = Math.max(24, Math.min(32, Math.floor(width * 0.2))); const feedWidth = clampPaneWidth(requestedFeedWidth ?? autoFeedWidth, minFeedWidth, maxFeedWidth); if (layoutMode === 'medium') { const articleWidth = Math.max(0, width - feedWidth); return { feedWidth, articleWidth, minFeedWidth, maxFeedWidth, minArticleWidth: articleWidth, maxArticleWidth: articleWidth, }; } const readerReserve = Math.min(MIN_READER_WIDTH, Math.floor(width / 3)); const maxArticleWidth = Math.max(0, Math.min(Math.floor(width * 0.6), width - feedWidth - readerReserve)); const minArticleWidth = Math.min(MIN_ARTICLE_WIDTH, maxArticleWidth); const autoArticleWidth = Math.floor(width * 0.35); const articleWidth = clampPaneWidth(requestedArticleWidth ?? autoArticleWidth, minArticleWidth, maxArticleWidth); return { feedWidth, articleWidth, minFeedWidth, maxFeedWidth, minArticleWidth, maxArticleWidth, }; }