import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui"; export function columnWidth(lines: string[]): number { return Math.max(0, ...lines.map((line) => visibleWidth(line))); } function columnsWidth(columns: string[][], gap = 2): number { if (columns.length === 0) return 0; return columns.reduce((sum, column) => sum + columnWidth(column), 0) + gap * (columns.length - 1); } function joinColumns(columns: string[][], gap = 2): string[] { if (columns.length === 0) return []; const widths = columns.map(columnWidth); const maxLines = Math.max(0, ...columns.map((lines) => lines.length)); return Array.from({ length: maxLines }, (_, rowIndex) => { let lastColumnWithContent = -1; for (let columnIndex = columns.length - 1; columnIndex >= 0; columnIndex--) { const line = columns[columnIndex]?.[rowIndex]; if (line !== undefined && line !== "") { lastColumnWithContent = columnIndex; break; } } if (lastColumnWithContent === -1) return ""; let row = ""; for (let columnIndex = 0; columnIndex <= lastColumnWithContent; columnIndex++) { const line = columns[columnIndex]?.[rowIndex] ?? ""; if (columnIndex === lastColumnWithContent) { row += line; } else { const paddingWidth = widths[columnIndex] - visibleWidth(line) + gap; row += line + " ".repeat(Math.max(0, paddingWidth)); } } return row; }); } function chunkItems(items: T[], size: number): T[][] { const chunks: T[][] = []; for (let index = 0; index < items.length; index += size) { chunks.push(items.slice(index, index + size)); } return chunks; } export function arrangeItemsByWidth(items: string[], maxWidth: number, gap = 2): string[] { if (items.length === 0) return []; for (let columnCount = items.length; columnCount > 1; columnCount--) { const itemsPerColumn = Math.ceil(items.length / columnCount); const columns = chunkItems(items, itemsPerColumn); if (columnsWidth(columns, gap) <= maxWidth) { return joinColumns(columns, gap); } } return items; } export function joinLogoAndContent(logoLines: string[], contentLines: string[], gap = 2): string[] { const logoWidth = columnWidth(logoLines); const maxLines = Math.max(logoLines.length, contentLines.length); return Array.from({ length: maxLines }, (_, index) => { const logoLine = logoLines[index]; const contentLine = contentLines[index] ?? ""; if (logoLine === undefined) return contentLine; if (!contentLine) return logoLine; const padding = " ".repeat(Math.max(0, logoWidth - visibleWidth(logoLine) + gap)); return `${logoLine}${padding}${contentLine}`; }); } function bottomAlignLastLine(lines: string[], targetLineCount: number): string[] { if (lines.length === 0) return lines; const lastLine = lines.at(-1) ?? ""; const leadingLines = lines.slice(0, -1); const compactLeadingLines = leadingLines.length + 1 > targetLineCount ? leadingLines.filter((line) => line !== "") : leadingLines; const alignedLines = [...compactLeadingLines, lastLine]; if (alignedLines.length >= targetLineCount) return alignedLines; return [ ...alignedLines.slice(0, -1), ...Array.from({ length: targetLineCount - alignedLines.length }, () => ""), lastLine, ]; } interface LogoHeaderLayout { showLogo: boolean; compactContentWidth: number; liftedHelpCount: number; } interface LogoHeaderLayoutOptions { gap: number; minContentWidth: number; liftedHelpReservedLines: number; } interface ResponsiveLogoHeaderOptions extends LogoHeaderLayoutOptions { width: number; expanded: boolean; logoLines: string[]; miniLogoLines: string[]; titleLine: string; miniHelpLine: string; onboardingLine: string; compactLines: string[]; expandedHelpLines: string[]; leftPadding?: number; } function computeLogoHeaderLayout( width: number, logoLines: string[], helpLineCount: number, options: LogoHeaderLayoutOptions, ): LogoHeaderLayout { const logoWidth = columnWidth(logoLines); const showLogo = width >= logoWidth + options.gap + options.minContentWidth; const compactContentWidth = Math.max(0, showLogo ? width - logoWidth - options.gap : width); const liftedHelpCount = showLogo ? Math.max(0, Math.min(helpLineCount, logoLines.length - options.liftedHelpReservedLines)) : 0; return { showLogo, compactContentWidth, liftedHelpCount }; } function buildExpandedIntroLines( showLogo: boolean, titleLine: string, onboardingLine: string, liftedHelpLines: string[], ): string[] { if (!showLogo) return [titleLine, onboardingLine]; return [ titleLine, ...liftedHelpLines, "", onboardingLine, ]; } function buildExpandedLines( showLogo: boolean, logoLines: string[], expandedIntroLines: string[], remainingHelpLines: string[], width: number, gap: number, ): string[] { const expandedHelp = arrangeItemsByWidth(remainingHelpLines, width, gap); if (!showLogo) { return [ ...expandedIntroLines, "", ...expandedHelp, ]; } return [ ...joinLogoAndContent(logoLines, expandedIntroLines, gap), "", ...expandedHelp, ]; } export function buildResponsiveLogoHeaderLines(options: ResponsiveLogoHeaderOptions): string[] { const { width, expanded, logoLines, miniLogoLines, titleLine, miniHelpLine, onboardingLine, compactLines, expandedHelpLines, gap, } = options; const leftPaddingWidth = Math.max(0, Math.min(width, options.leftPadding ?? 0)); const leftPadding = " ".repeat(leftPaddingWidth); const availableWidth = Math.max(0, width - leftPaddingWidth); const { showLogo, compactContentWidth, liftedHelpCount } = computeLogoHeaderLayout( availableWidth, logoLines, expandedHelpLines.length, options, ); const liftedHelpLines = expandedHelpLines.slice(0, liftedHelpCount); const remainingHelpLines = expandedHelpLines.slice(liftedHelpCount); const expandedIntroLines = buildExpandedIntroLines( showLogo, titleLine, onboardingLine, liftedHelpLines, ); const expandedLines = buildExpandedLines( showLogo, logoLines, expandedIntroLines, remainingHelpLines, availableWidth, gap, ); const compactLinesForWidth = showLogo ? bottomAlignLastLine( compactLines.map((line) => truncateToWidth(line, compactContentWidth)), logoLines.length, ) : compactLines; const miniLogoWidth = columnWidth(miniLogoLines); const canShowMiniLogo = availableWidth >= miniLogoWidth + gap + visibleWidth(titleLine); const joinedLines = expanded ? expandedLines : showLogo ? joinLogoAndContent(logoLines, compactLinesForWidth, gap) : canShowMiniLogo ? joinLogoAndContent(miniLogoLines, [ titleLine, truncateToWidth(miniHelpLine, Math.max(0, availableWidth - miniLogoWidth - gap)), ], gap) : [titleLine]; return joinedLines.map((line) => leftPadding + truncateToWidth(line, availableWidth)); }