/** * Text display width utilities * * Pure functions for calculating and truncating text based on * terminal display width, with full-width (CJK) character support. */ /** * Check if a Unicode code point is full-width (occupies 2 columns). * Covers CJK unified ideographs, Hangul, fullwidth forms, etc. */ export function isFullWidth(code: number): boolean { return ( (code >= 0x1100 && code <= 0x115F) || // Hangul Jamo (code >= 0x2E80 && code <= 0x9FFF) || // CJK radicals, symbols, ideographs (code >= 0xAC00 && code <= 0xD7AF) || // Hangul syllables (code >= 0xF900 && code <= 0xFAFF) || // CJK compatibility ideographs (code >= 0xFE10 && code <= 0xFE6F) || // CJK compatibility forms (code >= 0xFF01 && code <= 0xFF60) || // Fullwidth ASCII variants (code >= 0xFFE0 && code <= 0xFFE6) || // Fullwidth symbols (code >= 0x20000 && code <= 0x2FA1F) // CJK extension B+ ); } /** * Calculate the display width of a plain text string. * Full-width characters (CJK etc.) count as 2, others as 1. */ export function getDisplayWidth(text: string): number { let width = 0; for (const char of text) { const code = char.codePointAt(0) ?? 0; width += isFullWidth(code) ? 2 : 1; } return width; } /** * Truncate plain text to fit within maxWidth display columns. * Appends '…' if truncated. The ellipsis itself counts as 1 column. */ export function truncateText(text: string, maxWidth: number): string { if (maxWidth <= 0) return ''; let width = 0; let i = 0; for (const char of text) { const charWidth = isFullWidth(char.codePointAt(0) ?? 0) ? 2 : 1; if (width + charWidth > maxWidth - 1) { // Not enough room; truncate and add ellipsis return text.slice(0, i) + '…'; } width += charWidth; i += char.length; } return text; }