import type { ModuleSpec, RenderedModule } from "./types"; // eslint-disable-next-line no-control-regex const ANSI_RE = new RegExp("\\u001b\\[[0-9;]*m", "g"); const ANSI_PREFIX_RE = /^\u001b\[[0-9;]*m/; export function visibleWidth(text: string): number { return text.replace(ANSI_RE, "").length; } export function truncate(text: string, width: number): string { if (width <= 0) return ""; if (visibleWidth(text) <= width) return text; // ANSI-Aware Truncation let output = ""; let count = 0; for (let index = 0; index < text.length && count < width; index++) { if (text[index] === "\x1b") { const match = text.slice(index).match(ANSI_PREFIX_RE); if (match) { output += match[0]; index += match[0].length - 1; continue; } } output += text[index]; count++; } return `${output}\x1b[0m`; } export function joinText(parts: string[], separator: string): string { return parts.filter(Boolean).join(` ${separator} `); } // ANSI-Aware Slice - Extract a substring by visible character positions, // preserving ANSI escape codes that precede visible characters. function ansiSlice(text: string, start: number, end: number): string { let output = ""; let visible = 0; for (let i = 0; i < text.length; i++) { if (text[i] === "\x1b") { const match = text.slice(i).match(ANSI_PREFIX_RE); if (match) { if (visible >= start && visible < end) output += match[0]; i += match[0].length - 1; continue; } } if (visible >= start && visible < end) output += text[i]; visible++; if (visible >= end) break; } return output; } export function overlay(base: string, text: string): string { const baseWidth = visibleWidth(base); const textWidth = visibleWidth(text); if (textWidth >= baseWidth) return truncate(text, baseWidth); const start = Math.max(0, Math.floor((baseWidth - textWidth) / 2)); return `${ansiSlice(base, 0, start)}${text}${ansiSlice(base, start + textWidth, baseWidth)}`; } function dimText(text: string, theme?: any): string { return text && theme?.fg ? theme.fg("dim", text) : text; } function renderGroup(modules: RenderedModule[], width: number, separator: string, theme?: any): string { const fixed = modules.map(module => module.text ?? ""); const growModules = modules.filter(module => module.render); const groupSeparator = modules.every(module => module.render) ? " " : ` ${separator} `; const renderedSeparator = modules.every(module => module.render) ? " " : dimText(groupSeparator, theme); const fixedText = fixed.filter(Boolean).map(text => dimText(text, theme)).join(dimText(` ${separator} `, theme)); if (growModules.length === 0) return truncate(fixedText, width); const separatorWidth = Math.max(0, modules.length - 1) * groupSeparator.length; const fixedWidth = fixed.reduce((total, text) => total + visibleWidth(text), 0); const remaining = Math.max(0, width - fixedWidth - separatorWidth); const totalGrow = growModules.reduce((total, module) => total + (module.grow ?? 1), 0) || 1; let used = 0; const rendered = modules.map(module => { if (!module.render) return dimText(module.text ?? "", theme); const isLastGrow = growModules[growModules.length - 1] === module; const allocated = isLastGrow ? remaining - used : Math.floor(remaining * ((module.grow ?? 1) / totalGrow)); used += allocated; return module.render(Math.max(0, allocated), theme); }); return truncate(rendered.filter(Boolean).join(renderedSeparator), width); } export function renderRow(rendered: Record<"left" | "center" | "right", RenderedModule[]>, width: number, separator: string, theme?: any): string { const leftText = renderGroup(rendered.left, width, separator, theme); const rightText = renderGroup(rendered.right, width, separator, theme); const gap = leftText && rightText ? 1 : 0; const centerWidth = Math.max(0, width - visibleWidth(leftText) - visibleWidth(rightText) - gap); const centerText = renderGroup(rendered.center, centerWidth, separator, theme); if (centerText) { const leftPad = leftText ? `${leftText} ` : ""; const rightPad = rightText ? ` ${rightText}` : ""; return truncate(`${leftPad}${centerText}${rightPad}`, width); } if (!leftText) return `${" ".repeat(Math.max(0, width - visibleWidth(rightText)))}${truncate(rightText, width)}`; if (!rightText) return truncate(leftText, width); const padding = " ".repeat(Math.max(1, width - visibleWidth(leftText) - visibleWidth(rightText))); return truncate(`${leftText}${padding}${rightText}`, width); } export function moduleType(spec: ModuleSpec): string { return typeof spec === "string" ? spec : spec.type; }