/** * @fileoverview * Word wrapping utilities for terminal output * * This module provides functions for wrapping text at specified widths * while preserving proper indentation and handling Unicode characters * correctly. */ import { getDisplayWidth, stripAnsi } from "./wcwidth.ts"; /** * Wrap text at specified width with proper indentation for continuation lines. * Automatically detects the message start position from the first line. * * @param text The text to wrap (may contain ANSI escape codes) * @param maxWidth Maximum width in terminal columns * @param messageContent The plain message content (used to find message start) * @returns Wrapped text with proper indentation */ export function wrapText( text: string, maxWidth: number, messageContent: string, ): string { if (maxWidth <= 0) return text; const displayWidth = getDisplayWidth(text); // If text has newlines (multiline interpolated values), always process it // even if it fits within the width if (displayWidth <= maxWidth && !text.includes("\n")) return text; // Find where the message content starts in the first line const firstLineWords = messageContent.split(" "); const firstWord = firstLineWords[0]; const plainText = stripAnsi(text); const messageStartIndex = plainText.indexOf(firstWord); // Calculate the display width of the text up to the message start // This is crucial for proper alignment when emojis are present let indentWidth = 0; if (messageStartIndex >= 0) { const prefixText = plainText.slice(0, messageStartIndex); indentWidth = getDisplayWidth(prefixText); } const indent = " ".repeat(Math.max(0, indentWidth)); // Check if text contains newlines (from interpolated values like Error objects) if (text.includes("\n")) { // Split by existing newlines and process each line const lines = text.split("\n"); const wrappedLines: string[] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const lineDisplayWidth = getDisplayWidth(line); if (lineDisplayWidth <= maxWidth) { // Line doesn't need wrapping, but add indentation if it's not the first line if (i === 0) { wrappedLines.push(line); } else { wrappedLines.push(indent + line); } } else { // Line needs wrapping const wrappedLine = wrapSingleLine(line, maxWidth, indent); if (i === 0) { wrappedLines.push(wrappedLine); } else { // For continuation lines from interpolated values, add proper indentation const subLines = wrappedLine.split("\n"); for (let j = 0; j < subLines.length; j++) { if (j === 0) { wrappedLines.push(indent + subLines[j]); } else { wrappedLines.push(subLines[j]); } } } } } return wrappedLines.join("\n"); } // Process as a single line since log records should not have newlines in the formatted output return wrapSingleLine(text, maxWidth, indent); } /** * Wrap a single line of text (without existing newlines) at word boundaries. * Preserves ANSI escape codes and handles Unicode character widths correctly. * * @param text The text to wrap (single line, may contain ANSI codes) * @param maxWidth Maximum width in terminal columns * @param indent Indentation string for continuation lines * @returns Wrapped text with newlines and proper indentation */ export function wrapSingleLine( text: string, maxWidth: number, indent: string, ): string { // Split text into chunks while preserving ANSI codes const lines: string[] = []; let currentLine = ""; let currentDisplayWidth = 0; let i = 0; while (i < text.length) { // Check for ANSI escape sequence if (text[i] === "\x1b" && text[i + 1] === "[") { // Find the end of the ANSI sequence let j = i + 2; while (j < text.length && text[j] !== "m") { j++; } if (j < text.length) { j++; // Include the 'm' currentLine += text.slice(i, j); i = j; continue; } } const char = text[i]; // Check if adding this character would exceed the width if (currentDisplayWidth >= maxWidth && char !== " ") { // Try to find a good break point (space) before the current position const breakPoint = currentLine.lastIndexOf(" "); if (breakPoint > 0) { // Break at the space lines.push(currentLine.slice(0, breakPoint)); currentLine = indent + currentLine.slice(breakPoint + 1) + char; currentDisplayWidth = getDisplayWidth(currentLine); } else { // No space found, hard break lines.push(currentLine); currentLine = indent + char; currentDisplayWidth = getDisplayWidth(currentLine); } } else { currentLine += char; // Recalculate display width properly for Unicode characters currentDisplayWidth = getDisplayWidth(currentLine); } i++; } if (currentLine.trim()) { lines.push(currentLine); } // Filter out empty lines (lines with only indentation/spaces) const filteredLines = lines.filter((line) => line.trim().length > 0); return filteredLines.join("\n"); }