import stringWidth from "string-width";
import sliceAnsi from "slice-ansi";
const MAX_LINES = 3;
const PADDING = 10;
* ANSI-aware truncation to at most MAX_LINES visual lines.
* ANSI-aware multi-line truncation for TUI display.
*
* 4-line special case: if the content has exactly MAX_LINES+1 visual lines,
* show all 4 rather than truncating to 3 (avoids the absurd "3 lines shown,
* 1 more" pattern).
*/
export function truncateForDisplay(content: string, terminalWidth: number): string {
if (!content) return "";
const trimmed = content.trimEnd();
if (!trimmed) return "";
const wrapWidth = Math.max(terminalWidth - PADDING, 10);
const charBudget = MAX_LINES * wrapWidth * 4;
const source = trimmed.length > charBudget ? trimmed.slice(0, charBudget) : trimmed;
const visualLines = wrapText(source, wrapWidth);
const totalVisual = visualLines.length;
if (totalVisual <= MAX_LINES) {
return trimmed;
}
if (totalVisual === MAX_LINES + 1) {
return trimmed;
}
return visualLines.slice(0, MAX_LINES).join("\n");
}
* Count how many visual lines `content` would occupy at `terminalWidth`.
*/
export function countVisualLines(content: string, terminalWidth: number): number {
if (!content) return 0;
const wrapWidth = Math.max(terminalWidth - PADDING, 10);
return wrapText(content, wrapWidth).length;
}
* Split content into visual lines, wrapping long lines based on string-width
* (ANSI-aware). Each element in the returned array is one visual line.
*/
function wrapText(text: string, width: number): string[] {
const result: string[] = [];
const rawLines = text.split("\n");
for (const rawLine of rawLines) {
const lineWidth = stringWidth(rawLine);
if (lineWidth <= width) {
result.push(rawLine);
continue;
}
let remaining = rawLine;
while (remaining.length > 0) {
const w = stringWidth(remaining);
if (w <= width) {
result.push(remaining);
break;
}
let lo = 1;
let hi = remaining.length;
let best = 1;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
const slice = sliceAnsi(remaining, 0, mid);
const sw = stringWidth(slice);
if (sw <= width) {
best = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
result.push(sliceAnsi(remaining, 0, best));
remaining = sliceAnsi(remaining, best);
}
}
return result;
}