interface ColumnDef { key: string; header: string; minWidth?: number; maxWidth?: number; width?: number; } function visualWidth(str: string): number { let width = 0; for (const ch of str) { const code = ch.codePointAt(0) ?? 0; // CJK Unified, Compatibility, Extension ranges, plus fullwidth forms if ( (code >= 0x4e00 && code <= 0x9fff) || (code >= 0x3400 && code <= 0x4dbf) || (code >= 0x20000 && code <= 0x2a6df) || (code >= 0xf900 && code <= 0xfaff) || (code >= 0xff01 && code <= 0xff60) || (code >= 0xffe0 && code <= 0xffe6) ) { width += 2; } else { width += 1; } } return width; } function padToWidth(str: string, targetWidth: number): string { let padded = str; let current = visualWidth(str); while (current < targetWidth) { padded += " "; current += 1; } return padded; } function truncateToWidth(str: string, maxWidth: number): string { let result = ""; let vw = 0; for (const ch of str) { const charW = visualWidth(ch); if (vw + charW > maxWidth - 3) { result += "..."; break; } result += ch; vw += charW; } if (visualWidth(result) <= maxWidth && result.length < str.length) { result += "..."; } return result; } export function formatTable(columns: ColumnDef[], rows: Record[]): string { const minW = 8; const maxW = 60; const resolved = columns.map((col) => { const headerW = visualWidth(col.header); let dataW = 0; for (const row of rows) { const val = row[col.key] ?? "-"; dataW = Math.max(dataW, visualWidth(val)); } const natural = Math.max(headerW, dataW); const lo = col.minWidth ?? minW; const hi = col.maxWidth ?? maxW; const width = Math.max(lo, Math.min(hi, natural)); return { ...col, width }; }); const padCell = (col: ColumnDef, text: string): string => { if (visualWidth(text) > col.width!) { return truncateToWidth(text, col.width!); } return padToWidth(text, col.width!); }; const lines: string[] = []; const headerLine = resolved .map((col) => padCell(col, col.header)) .join(" "); lines.push(headerLine); const sepLine = resolved .map((col) => "-".repeat(col.width!)) .join(" "); lines.push(sepLine); for (const row of rows) { const line = resolved .map((col) => padCell(col, row[col.key] ?? "-")) .join(" "); lines.push(line); } return lines.join("\n"); }