diff --git a/render.ts b/render.ts index 77f1b64..9a62621 100644 --- a/render.ts +++ b/render.ts @@ -34,10 +34,33 @@ 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 { - if (visibleWidth(text) >= visibleWidth(base)) return truncate(text, visibleWidth(base)); - const start = Math.max(0, Math.floor((visibleWidth(base) - visibleWidth(text)) / 2)); - return `${base.slice(0, start)}${text}${base.slice(start + visibleWidth(text))}`; + 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 {