@mariozechner/pi-tui 0.12.9 → 0.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +36 -19
- package/dist/utils.js.map +1 -1
- package/package.json +1 -1
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGhD;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CActE;AAoID;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,CASzG;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAc,GAAG,MAAM,CA8ChG","sourcesContent":["import stringWidth from \"string-width\";\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoTokensWithAnsi(text: string): string[] {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet inWhitespace = false;\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrent += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = text[i];\n\t\tconst charIsSpace = char === \" \";\n\n\t\tif (charIsSpace !== inWhitespace && current) {\n\t\t\t// Switching between whitespace and non-whitespace, push current token\n\t\t\ttokens.push(current);\n\t\t\tcurrent = \"\";\n\t\t}\n\n\t\tinWhitespace = charIsSpace;\n\t\tcurrent += char;\n\t\ti++;\n\t}\n\n\tif (current) {\n\t\ttokens.push(current);\n\t}\n\n\treturn tokens;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst tokens = splitIntoTokensWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const token of tokens) {\n\t\tconst tokenVisibleLength = visibleWidth(token);\n\t\tconst isWhitespace = token.trim() === \"\";\n\n\t\t// Token itself is too long - break it character by character\n\t\tif (tokenVisibleLength > width && !isWhitespace) {\n\t\t\tif (currentLine) {\n\t\t\t\twrapped.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break long token\n\t\t\tconst broken = breakLongWord(token, width, tracker);\n\t\t\twrapped.push(...broken.slice(0, -1));\n\t\t\tcurrentLine = broken[broken.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if adding this token would exceed width\n\t\tconst totalNeeded = currentVisibleLength + tokenVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line - don't carry trailing whitespace\n\t\t\twrapped.push(currentLine.trimEnd());\n\t\t\tif (isWhitespace) {\n\t\t\t\t// Don't start new line with whitespace\n\t\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t} else {\n\t\t\t\tcurrentLine = tracker.getActiveCodes() + token;\n\t\t\t\tcurrentVisibleLength = tokenVisibleLength;\n\t\t\t}\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tcurrentLine += token;\n\t\t\tcurrentVisibleLength += tokenVisibleLength;\n\t\t}\n\n\t\tupdateTrackerFromText(token, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\n// Grapheme segmenter for proper Unicode iteration (handles emojis, etc.)\nconst segmenter = new Intl.Segmenter();\n\nfunction breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentWidth = 0;\n\n\t// First, separate ANSI codes from visible content\n\t// We need to handle ANSI codes specially since they're not graphemes\n\tlet i = 0;\n\tconst segments: Array<{ type: \"ansi\" | \"grapheme\"; value: string }> = [];\n\n\twhile (i < word.length) {\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tsegments.push({ type: \"ansi\", value: ansiResult.code });\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\t// Find the next ANSI code or end of string\n\t\t\tlet end = i;\n\t\t\twhile (end < word.length) {\n\t\t\t\tconst nextAnsi = extractAnsiCode(word, end);\n\t\t\t\tif (nextAnsi) break;\n\t\t\t\tend++;\n\t\t\t}\n\t\t\t// Segment this non-ANSI portion into graphemes\n\t\t\tconst textPortion = word.slice(i, end);\n\t\t\tfor (const seg of segmenter.segment(textPortion)) {\n\t\t\t\tsegments.push({ type: \"grapheme\", value: seg.segment });\n\t\t\t}\n\t\t\ti = end;\n\t\t}\n\t}\n\n\t// Now process segments\n\tfor (const seg of segments) {\n\t\tif (seg.type === \"ansi\") {\n\t\t\tcurrentLine += seg.value;\n\t\t\ttracker.process(seg.value);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst grapheme = seg.value;\n\t\tconst graphemeWidth = visibleWidth(grapheme);\n\n\t\tif (currentWidth + graphemeWidth > width) {\n\t\t\tlines.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\tcurrentWidth = 0;\n\t\t}\n\n\t\tcurrentLine += grapheme;\n\t\tcurrentWidth += graphemeWidth;\n\t}\n\n\tif (currentLine) {\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgFn - Background color function\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string {\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Apply background to content + padding\n\tconst withPadding = line + padding;\n\treturn bgFn(withPadding);\n}\n\n/**\n * Truncate text to fit within a maximum visible width, adding ellipsis if needed.\n * Properly handles ANSI escape codes (they don't count toward width).\n *\n * @param text - Text to truncate (may contain ANSI codes)\n * @param maxWidth - Maximum visible width\n * @param ellipsis - Ellipsis string to append when truncating (default: \"...\")\n * @returns Truncated text with ellipsis if it exceeded maxWidth\n */\nexport function truncateToWidth(text: string, maxWidth: number, ellipsis: string = \"...\"): string {\n\tconst textVisibleWidth = visibleWidth(text);\n\n\tif (textVisibleWidth <= maxWidth) {\n\t\treturn text;\n\t}\n\n\tconst ellipsisWidth = visibleWidth(ellipsis);\n\tconst targetWidth = maxWidth - ellipsisWidth;\n\n\tif (targetWidth <= 0) {\n\t\treturn ellipsis.substring(0, maxWidth);\n\t}\n\n\tlet currentWidth = 0;\n\tlet truncateAt = 0;\n\tlet i = 0;\n\n\twhile (i < text.length && currentWidth < targetWidth) {\n\t\t// Skip ANSI escape sequences (include them in output but don't count width)\n\t\tif (text[i] === \"\\x1b\" && text[i + 1] === \"[\") {\n\t\t\tlet j = i + 2;\n\t\t\twhile (j < text.length && !/[a-zA-Z]/.test(text[j]!)) {\n\t\t\t\tj++;\n\t\t\t}\n\t\t\t// Include the final letter of the escape sequence\n\t\t\tj++;\n\t\t\ttruncateAt = j;\n\t\t\ti = j;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = text[i]!;\n\t\tconst charWidth = visibleWidth(char);\n\n\t\tif (currentWidth + charWidth > targetWidth) {\n\t\t\tbreak;\n\t\t}\n\n\t\tcurrentWidth += charWidth;\n\t\ttruncateAt = i + 1;\n\t\ti++;\n\t}\n\n\t// Add reset code before ellipsis to prevent styling leaking into it\n\treturn text.substring(0, truncateAt) + \"\\x1b[0m\" + ellipsis;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAGhD;AAwGD;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CActE;AAoID;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,MAAM,CASzG;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAc,GAAG,MAAM,CA+DhG","sourcesContent":["import stringWidth from \"string-width\";\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoTokensWithAnsi(text: string): string[] {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet inWhitespace = false;\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrent += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = text[i];\n\t\tconst charIsSpace = char === \" \";\n\n\t\tif (charIsSpace !== inWhitespace && current) {\n\t\t\t// Switching between whitespace and non-whitespace, push current token\n\t\t\ttokens.push(current);\n\t\t\tcurrent = \"\";\n\t\t}\n\n\t\tinWhitespace = charIsSpace;\n\t\tcurrent += char;\n\t\ti++;\n\t}\n\n\tif (current) {\n\t\ttokens.push(current);\n\t}\n\n\treturn tokens;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst tokens = splitIntoTokensWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const token of tokens) {\n\t\tconst tokenVisibleLength = visibleWidth(token);\n\t\tconst isWhitespace = token.trim() === \"\";\n\n\t\t// Token itself is too long - break it character by character\n\t\tif (tokenVisibleLength > width && !isWhitespace) {\n\t\t\tif (currentLine) {\n\t\t\t\twrapped.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break long token\n\t\t\tconst broken = breakLongWord(token, width, tracker);\n\t\t\twrapped.push(...broken.slice(0, -1));\n\t\t\tcurrentLine = broken[broken.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if adding this token would exceed width\n\t\tconst totalNeeded = currentVisibleLength + tokenVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line - don't carry trailing whitespace\n\t\t\twrapped.push(currentLine.trimEnd());\n\t\t\tif (isWhitespace) {\n\t\t\t\t// Don't start new line with whitespace\n\t\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t} else {\n\t\t\t\tcurrentLine = tracker.getActiveCodes() + token;\n\t\t\t\tcurrentVisibleLength = tokenVisibleLength;\n\t\t\t}\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tcurrentLine += token;\n\t\t\tcurrentVisibleLength += tokenVisibleLength;\n\t\t}\n\n\t\tupdateTrackerFromText(token, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\n// Grapheme segmenter for proper Unicode iteration (handles emojis, etc.)\nconst segmenter = new Intl.Segmenter();\n\nfunction breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentWidth = 0;\n\n\t// First, separate ANSI codes from visible content\n\t// We need to handle ANSI codes specially since they're not graphemes\n\tlet i = 0;\n\tconst segments: Array<{ type: \"ansi\" | \"grapheme\"; value: string }> = [];\n\n\twhile (i < word.length) {\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tsegments.push({ type: \"ansi\", value: ansiResult.code });\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\t// Find the next ANSI code or end of string\n\t\t\tlet end = i;\n\t\t\twhile (end < word.length) {\n\t\t\t\tconst nextAnsi = extractAnsiCode(word, end);\n\t\t\t\tif (nextAnsi) break;\n\t\t\t\tend++;\n\t\t\t}\n\t\t\t// Segment this non-ANSI portion into graphemes\n\t\t\tconst textPortion = word.slice(i, end);\n\t\t\tfor (const seg of segmenter.segment(textPortion)) {\n\t\t\t\tsegments.push({ type: \"grapheme\", value: seg.segment });\n\t\t\t}\n\t\t\ti = end;\n\t\t}\n\t}\n\n\t// Now process segments\n\tfor (const seg of segments) {\n\t\tif (seg.type === \"ansi\") {\n\t\t\tcurrentLine += seg.value;\n\t\t\ttracker.process(seg.value);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst grapheme = seg.value;\n\t\tconst graphemeWidth = visibleWidth(grapheme);\n\n\t\tif (currentWidth + graphemeWidth > width) {\n\t\t\tlines.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\tcurrentWidth = 0;\n\t\t}\n\n\t\tcurrentLine += grapheme;\n\t\tcurrentWidth += graphemeWidth;\n\t}\n\n\tif (currentLine) {\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgFn - Background color function\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string {\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Apply background to content + padding\n\tconst withPadding = line + padding;\n\treturn bgFn(withPadding);\n}\n\n/**\n * Truncate text to fit within a maximum visible width, adding ellipsis if needed.\n * Properly handles ANSI escape codes (they don't count toward width).\n *\n * @param text - Text to truncate (may contain ANSI codes)\n * @param maxWidth - Maximum visible width\n * @param ellipsis - Ellipsis string to append when truncating (default: \"...\")\n * @returns Truncated text with ellipsis if it exceeded maxWidth\n */\nexport function truncateToWidth(text: string, maxWidth: number, ellipsis: string = \"...\"): string {\n\tconst textVisibleWidth = visibleWidth(text);\n\n\tif (textVisibleWidth <= maxWidth) {\n\t\treturn text;\n\t}\n\n\tconst ellipsisWidth = visibleWidth(ellipsis);\n\tconst targetWidth = maxWidth - ellipsisWidth;\n\n\tif (targetWidth <= 0) {\n\t\treturn ellipsis.substring(0, maxWidth);\n\t}\n\n\t// Separate ANSI codes from visible content using grapheme segmentation\n\tlet i = 0;\n\tconst segments: Array<{ type: \"ansi\" | \"grapheme\"; value: string }> = [];\n\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tsegments.push({ type: \"ansi\", value: ansiResult.code });\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\t// Find the next ANSI code or end of string\n\t\t\tlet end = i;\n\t\t\twhile (end < text.length) {\n\t\t\t\tconst nextAnsi = extractAnsiCode(text, end);\n\t\t\t\tif (nextAnsi) break;\n\t\t\t\tend++;\n\t\t\t}\n\t\t\t// Segment this non-ANSI portion into graphemes\n\t\t\tconst textPortion = text.slice(i, end);\n\t\t\tfor (const seg of segmenter.segment(textPortion)) {\n\t\t\t\tsegments.push({ type: \"grapheme\", value: seg.segment });\n\t\t\t}\n\t\t\ti = end;\n\t\t}\n\t}\n\n\t// Build truncated string from segments\n\tlet result = \"\";\n\tlet currentWidth = 0;\n\n\tfor (const seg of segments) {\n\t\tif (seg.type === \"ansi\") {\n\t\t\tresult += seg.value;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst grapheme = seg.value;\n\t\tconst graphemeWidth = visibleWidth(grapheme);\n\n\t\tif (currentWidth + graphemeWidth > targetWidth) {\n\t\t\tbreak;\n\t\t}\n\n\t\tresult += grapheme;\n\t\tcurrentWidth += graphemeWidth;\n\t}\n\n\t// Add reset code before ellipsis to prevent styling leaking into it\n\treturn result + \"\\x1b[0m\" + ellipsis;\n}\n"]}
|
package/dist/utils.js
CHANGED
|
@@ -264,32 +264,49 @@ export function truncateToWidth(text, maxWidth, ellipsis = "...") {
|
|
|
264
264
|
if (targetWidth <= 0) {
|
|
265
265
|
return ellipsis.substring(0, maxWidth);
|
|
266
266
|
}
|
|
267
|
-
|
|
268
|
-
let truncateAt = 0;
|
|
267
|
+
// Separate ANSI codes from visible content using grapheme segmentation
|
|
269
268
|
let i = 0;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
269
|
+
const segments = [];
|
|
270
|
+
while (i < text.length) {
|
|
271
|
+
const ansiResult = extractAnsiCode(text, i);
|
|
272
|
+
if (ansiResult) {
|
|
273
|
+
segments.push({ type: "ansi", value: ansiResult.code });
|
|
274
|
+
i += ansiResult.length;
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// Find the next ANSI code or end of string
|
|
278
|
+
let end = i;
|
|
279
|
+
while (end < text.length) {
|
|
280
|
+
const nextAnsi = extractAnsiCode(text, end);
|
|
281
|
+
if (nextAnsi)
|
|
282
|
+
break;
|
|
283
|
+
end++;
|
|
276
284
|
}
|
|
277
|
-
//
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
285
|
+
// Segment this non-ANSI portion into graphemes
|
|
286
|
+
const textPortion = text.slice(i, end);
|
|
287
|
+
for (const seg of segmenter.segment(textPortion)) {
|
|
288
|
+
segments.push({ type: "grapheme", value: seg.segment });
|
|
289
|
+
}
|
|
290
|
+
i = end;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
// Build truncated string from segments
|
|
294
|
+
let result = "";
|
|
295
|
+
let currentWidth = 0;
|
|
296
|
+
for (const seg of segments) {
|
|
297
|
+
if (seg.type === "ansi") {
|
|
298
|
+
result += seg.value;
|
|
281
299
|
continue;
|
|
282
300
|
}
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
if (currentWidth +
|
|
301
|
+
const grapheme = seg.value;
|
|
302
|
+
const graphemeWidth = visibleWidth(grapheme);
|
|
303
|
+
if (currentWidth + graphemeWidth > targetWidth) {
|
|
286
304
|
break;
|
|
287
305
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
i++;
|
|
306
|
+
result += grapheme;
|
|
307
|
+
currentWidth += graphemeWidth;
|
|
291
308
|
}
|
|
292
309
|
// Add reset code before ellipsis to prevent styling leaking into it
|
|
293
|
-
return
|
|
310
|
+
return result + "\x1b[0m" + ellipsis;
|
|
294
311
|
}
|
|
295
312
|
//# sourceMappingURL=utils.js.map
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"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","sourcesContent":["import stringWidth from \"string-width\";\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoTokensWithAnsi(text: string): string[] {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet inWhitespace = false;\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrent += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = text[i];\n\t\tconst charIsSpace = char === \" \";\n\n\t\tif (charIsSpace !== inWhitespace && current) {\n\t\t\t// Switching between whitespace and non-whitespace, push current token\n\t\t\ttokens.push(current);\n\t\t\tcurrent = \"\";\n\t\t}\n\n\t\tinWhitespace = charIsSpace;\n\t\tcurrent += char;\n\t\ti++;\n\t}\n\n\tif (current) {\n\t\ttokens.push(current);\n\t}\n\n\treturn tokens;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst tokens = splitIntoTokensWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const token of tokens) {\n\t\tconst tokenVisibleLength = visibleWidth(token);\n\t\tconst isWhitespace = token.trim() === \"\";\n\n\t\t// Token itself is too long - break it character by character\n\t\tif (tokenVisibleLength > width && !isWhitespace) {\n\t\t\tif (currentLine) {\n\t\t\t\twrapped.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break long token\n\t\t\tconst broken = breakLongWord(token, width, tracker);\n\t\t\twrapped.push(...broken.slice(0, -1));\n\t\t\tcurrentLine = broken[broken.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if adding this token would exceed width\n\t\tconst totalNeeded = currentVisibleLength + tokenVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line - don't carry trailing whitespace\n\t\t\twrapped.push(currentLine.trimEnd());\n\t\t\tif (isWhitespace) {\n\t\t\t\t// Don't start new line with whitespace\n\t\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t} else {\n\t\t\t\tcurrentLine = tracker.getActiveCodes() + token;\n\t\t\t\tcurrentVisibleLength = tokenVisibleLength;\n\t\t\t}\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tcurrentLine += token;\n\t\t\tcurrentVisibleLength += tokenVisibleLength;\n\t\t}\n\n\t\tupdateTrackerFromText(token, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\n// Grapheme segmenter for proper Unicode iteration (handles emojis, etc.)\nconst segmenter = new Intl.Segmenter();\n\nfunction breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentWidth = 0;\n\n\t// First, separate ANSI codes from visible content\n\t// We need to handle ANSI codes specially since they're not graphemes\n\tlet i = 0;\n\tconst segments: Array<{ type: \"ansi\" | \"grapheme\"; value: string }> = [];\n\n\twhile (i < word.length) {\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tsegments.push({ type: \"ansi\", value: ansiResult.code });\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\t// Find the next ANSI code or end of string\n\t\t\tlet end = i;\n\t\t\twhile (end < word.length) {\n\t\t\t\tconst nextAnsi = extractAnsiCode(word, end);\n\t\t\t\tif (nextAnsi) break;\n\t\t\t\tend++;\n\t\t\t}\n\t\t\t// Segment this non-ANSI portion into graphemes\n\t\t\tconst textPortion = word.slice(i, end);\n\t\t\tfor (const seg of segmenter.segment(textPortion)) {\n\t\t\t\tsegments.push({ type: \"grapheme\", value: seg.segment });\n\t\t\t}\n\t\t\ti = end;\n\t\t}\n\t}\n\n\t// Now process segments\n\tfor (const seg of segments) {\n\t\tif (seg.type === \"ansi\") {\n\t\t\tcurrentLine += seg.value;\n\t\t\ttracker.process(seg.value);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst grapheme = seg.value;\n\t\tconst graphemeWidth = visibleWidth(grapheme);\n\n\t\tif (currentWidth + graphemeWidth > width) {\n\t\t\tlines.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\tcurrentWidth = 0;\n\t\t}\n\n\t\tcurrentLine += grapheme;\n\t\tcurrentWidth += graphemeWidth;\n\t}\n\n\tif (currentLine) {\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgFn - Background color function\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string {\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Apply background to content + padding\n\tconst withPadding = line + padding;\n\treturn bgFn(withPadding);\n}\n\n/**\n * Truncate text to fit within a maximum visible width, adding ellipsis if needed.\n * Properly handles ANSI escape codes (they don't count toward width).\n *\n * @param text - Text to truncate (may contain ANSI codes)\n * @param maxWidth - Maximum visible width\n * @param ellipsis - Ellipsis string to append when truncating (default: \"...\")\n * @returns Truncated text with ellipsis if it exceeded maxWidth\n */\nexport function truncateToWidth(text: string, maxWidth: number, ellipsis: string = \"...\"): string {\n\tconst textVisibleWidth = visibleWidth(text);\n\n\tif (textVisibleWidth <= maxWidth) {\n\t\treturn text;\n\t}\n\n\tconst ellipsisWidth = visibleWidth(ellipsis);\n\tconst targetWidth = maxWidth - ellipsisWidth;\n\n\tif (targetWidth <= 0) {\n\t\treturn ellipsis.substring(0, maxWidth);\n\t}\n\n\tlet currentWidth = 0;\n\tlet truncateAt = 0;\n\tlet i = 0;\n\n\twhile (i < text.length && currentWidth < targetWidth) {\n\t\t// Skip ANSI escape sequences (include them in output but don't count width)\n\t\tif (text[i] === \"\\x1b\" && text[i + 1] === \"[\") {\n\t\t\tlet j = i + 2;\n\t\t\twhile (j < text.length && !/[a-zA-Z]/.test(text[j]!)) {\n\t\t\t\tj++;\n\t\t\t}\n\t\t\t// Include the final letter of the escape sequence\n\t\t\tj++;\n\t\t\ttruncateAt = j;\n\t\t\ti = j;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = text[i]!;\n\t\tconst charWidth = visibleWidth(char);\n\n\t\tif (currentWidth + charWidth > targetWidth) {\n\t\t\tbreak;\n\t\t}\n\n\t\tcurrentWidth += charWidth;\n\t\ttruncateAt = i + 1;\n\t\ti++;\n\t}\n\n\t// Add reset code before ellipsis to prevent styling leaking into it\n\treturn text.substring(0, truncateAt) + \"\\x1b[0m\" + ellipsis;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,MAAM,cAAc,CAAC;AAEvC;;GAEG;AACH,MAAM,UAAU,YAAY,CAAC,GAAW,EAAU;IACjD,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAC7C,OAAO,WAAW,CAAC,UAAU,CAAC,CAAC;AAAA,CAC/B;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,GAAW,EAAE,GAAW,EAA2C;IAC3F,IAAI,GAAG,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,MAAM,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;QACtE,OAAO,IAAI,CAAC;IACb,CAAC;IAED,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAE,CAAC,EAAE,CAAC;QAC7D,CAAC,EAAE,CAAC;IACL,CAAC;IAED,IAAI,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC;QACpB,OAAO;YACN,IAAI,EAAE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;YAC/B,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,GAAG;SACnB,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AAAA,CACZ;AAED;;GAEG;AACH,MAAM,eAAe;IACZ,eAAe,GAAa,EAAE,CAAC;IAEvC,OAAO,CAAC,QAAgB,EAAQ;QAC/B,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC7B,OAAO;QACR,CAAC;QAED,+BAA+B;QAC/B,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACrD,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC;QACjC,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrC,CAAC;IAAA,CACD;IAED,cAAc,GAAW;QACxB,OAAO,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAAA,CACrC;IAED,cAAc,GAAY;QACzB,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC;IAAA,CACvC;CACD;AAED,SAAS,qBAAqB,CAAC,IAAY,EAAE,OAAwB,EAAQ;IAC5E,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC5C,IAAI,UAAU,EAAE,CAAC;YAChB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YACjC,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC;QACxB,CAAC;aAAM,CAAC;YACP,CAAC,EAAE,CAAC;QACL,CAAC;IACF,CAAC;AAAA,CACD;AAED;;GAEG;AACH,SAAS,uBAAuB,CAAC,IAAY,EAAY;IACxD,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,YAAY,GAAG,KAAK,CAAC;IACzB,IAAI,CAAC,GAAG,CAAC,CAAC;IAEV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC5C,IAAI,UAAU,EAAE,CAAC;YAChB,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC;YAC3B,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC;YACvB,SAAS;QACV,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,MAAM,WAAW,GAAG,IAAI,KAAK,GAAG,CAAC;QAEjC,IAAI,WAAW,KAAK,YAAY,IAAI,OAAO,EAAE,CAAC;YAC7C,sEAAsE;YACtE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACrB,OAAO,GAAG,EAAE,CAAC;QACd,CAAC;QAED,YAAY,GAAG,WAAW,CAAC;QAC3B,OAAO,IAAI,IAAI,CAAC;QAChB,CAAC,EAAE,CAAC;IACL,CAAC;IAED,IAAI,OAAO,EAAE,CAAC;QACb,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,KAAa,EAAY;IACvE,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,OAAO,CAAC,EAAE,CAAC,CAAC;IACb,CAAC;IAED,qDAAqD;IACrD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACpC,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACpC,MAAM,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAAA,CACzC;AAED,SAAS,cAAc,CAAC,IAAY,EAAE,KAAa,EAAY;IAC9D,IAAI,CAAC,IAAI,EAAE,CAAC;QACX,OAAO,CAAC,EAAE,CAAC,CAAC;IACb,CAAC;IAED,MAAM,aAAa,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,aAAa,IAAI,KAAK,EAAE,CAAC;QAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;IACf,CAAC;IAED,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAG,IAAI,eAAe,EAAE,CAAC;IACtC,MAAM,MAAM,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;IAE7C,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,IAAI,oBAAoB,GAAG,CAAC,CAAC;IAE7B,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,MAAM,kBAAkB,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;QAC/C,MAAM,YAAY,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QAEzC,6DAA6D;QAC7D,IAAI,kBAAkB,GAAG,KAAK,IAAI,CAAC,YAAY,EAAE,CAAC;YACjD,IAAI,WAAW,EAAE,CAAC;gBACjB,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;gBAC1B,WAAW,GAAG,EAAE,CAAC;gBACjB,oBAAoB,GAAG,CAAC,CAAC;YAC1B,CAAC;YAED,mBAAmB;YACnB,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACpD,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACrC,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACxC,oBAAoB,GAAG,YAAY,CAAC,WAAW,CAAC,CAAC;YACjD,SAAS;QACV,CAAC;QAED,gDAAgD;QAChD,MAAM,WAAW,GAAG,oBAAoB,GAAG,kBAAkB,CAAC;QAE9D,IAAI,WAAW,GAAG,KAAK,IAAI,oBAAoB,GAAG,CAAC,EAAE,CAAC;YACrD,sDAAsD;YACtD,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;YACpC,IAAI,YAAY,EAAE,CAAC;gBAClB,uCAAuC;gBACvC,WAAW,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;gBACvC,oBAAoB,GAAG,CAAC,CAAC;YAC1B,CAAC;iBAAM,CAAC;gBACP,WAAW,GAAG,OAAO,CAAC,cAAc,EAAE,GAAG,KAAK,CAAC;gBAC/C,oBAAoB,GAAG,kBAAkB,CAAC;YAC3C,CAAC;QACF,CAAC;aAAM,CAAC;YACP,sBAAsB;YACtB,WAAW,IAAI,KAAK,CAAC;YACrB,oBAAoB,IAAI,kBAAkB,CAAC;QAC5C,CAAC;QAED,qBAAqB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACvC,CAAC;IAED,IAAI,WAAW,EAAE,CAAC;QACjB,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC3B,CAAC;IAED,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAAA,CAC3C;AAED,yEAAyE;AACzE,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;AAEvC,SAAS,aAAa,CAAC,IAAY,EAAE,KAAa,EAAE,OAAwB,EAAY;IACvF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,WAAW,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAC3C,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,kDAAkD;IAClD,qEAAqE;IACrE,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,MAAM,QAAQ,GAAwD,EAAE,CAAC;IAEzE,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC5C,IAAI,UAAU,EAAE,CAAC;YAChB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;YACxD,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC;QACxB,CAAC;aAAM,CAAC;YACP,2CAA2C;YAC3C,IAAI,GAAG,GAAG,CAAC,CAAC;YACZ,OAAO,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC1B,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBAC5C,IAAI,QAAQ;oBAAE,MAAM;gBACpB,GAAG,EAAE,CAAC;YACP,CAAC;YACD,+CAA+C;YAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACvC,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;gBAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACzD,CAAC;YACD,CAAC,GAAG,GAAG,CAAC;QACT,CAAC;IACF,CAAC;IAED,uBAAuB;IACvB,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,WAAW,IAAI,GAAG,CAAC,KAAK,CAAC;YACzB,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAC3B,SAAS;QACV,CAAC;QAED,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC;QAC3B,MAAM,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;QAE7C,IAAI,YAAY,GAAG,aAAa,GAAG,KAAK,EAAE,CAAC;YAC1C,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;YACxB,WAAW,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;YACvC,YAAY,GAAG,CAAC,CAAC;QAClB,CAAC;QAED,WAAW,IAAI,QAAQ,CAAC;QACxB,YAAY,IAAI,aAAa,CAAC;IAC/B,CAAC;IAED,IAAI,WAAW,EAAE,CAAC;QACjB,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAAA,CACvC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY,EAAE,KAAa,EAAE,IAA8B,EAAU;IAC1G,2BAA2B;IAC3B,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,UAAU,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;IAE1C,wCAAwC;IACxC,MAAM,WAAW,GAAG,IAAI,GAAG,OAAO,CAAC;IACnC,OAAO,IAAI,CAAC,WAAW,CAAC,CAAC;AAAA,CACzB;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,QAAgB,EAAE,QAAQ,GAAW,KAAK,EAAU;IACjG,MAAM,gBAAgB,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IAE5C,IAAI,gBAAgB,IAAI,QAAQ,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;IAE7C,IAAI,WAAW,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,uEAAuE;IACvE,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,MAAM,QAAQ,GAAwD,EAAE,CAAC;IAEzE,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACxB,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAC5C,IAAI,UAAU,EAAE,CAAC;YAChB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;YACxD,CAAC,IAAI,UAAU,CAAC,MAAM,CAAC;QACxB,CAAC;aAAM,CAAC;YACP,2CAA2C;YAC3C,IAAI,GAAG,GAAG,CAAC,CAAC;YACZ,OAAO,GAAG,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;gBAC1B,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;gBAC5C,IAAI,QAAQ;oBAAE,MAAM;gBACpB,GAAG,EAAE,CAAC;YACP,CAAC;YACD,+CAA+C;YAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACvC,KAAK,MAAM,GAAG,IAAI,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;gBAClD,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACzD,CAAC;YACD,CAAC,GAAG,GAAG,CAAC;QACT,CAAC;IACF,CAAC;IAED,uCAAuC;IACvC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC;YACpB,SAAS;QACV,CAAC;QAED,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC;QAC3B,MAAM,aAAa,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC;QAE7C,IAAI,YAAY,GAAG,aAAa,GAAG,WAAW,EAAE,CAAC;YAChD,MAAM;QACP,CAAC;QAED,MAAM,IAAI,QAAQ,CAAC;QACnB,YAAY,IAAI,aAAa,CAAC;IAC/B,CAAC;IAED,oEAAoE;IACpE,OAAO,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAAA,CACrC","sourcesContent":["import stringWidth from \"string-width\";\n\n/**\n * Calculate the visible width of a string in terminal columns.\n */\nexport function visibleWidth(str: string): number {\n\tconst normalized = str.replace(/\\t/g, \" \");\n\treturn stringWidth(normalized);\n}\n\n/**\n * Extract ANSI escape sequences from a string at the given position.\n */\nfunction extractAnsiCode(str: string, pos: number): { code: string; length: number } | null {\n\tif (pos >= str.length || str[pos] !== \"\\x1b\" || str[pos + 1] !== \"[\") {\n\t\treturn null;\n\t}\n\n\tlet j = pos + 2;\n\twhile (j < str.length && str[j] && !/[mGKHJ]/.test(str[j]!)) {\n\t\tj++;\n\t}\n\n\tif (j < str.length) {\n\t\treturn {\n\t\t\tcode: str.substring(pos, j + 1),\n\t\t\tlength: j + 1 - pos,\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Track active ANSI SGR codes to preserve styling across line breaks.\n */\nclass AnsiCodeTracker {\n\tprivate activeAnsiCodes: string[] = [];\n\n\tprocess(ansiCode: string): void {\n\t\tif (!ansiCode.endsWith(\"m\")) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Full reset clears everything\n\t\tif (ansiCode === \"\\x1b[0m\" || ansiCode === \"\\x1b[m\") {\n\t\t\tthis.activeAnsiCodes.length = 0;\n\t\t} else {\n\t\t\tthis.activeAnsiCodes.push(ansiCode);\n\t\t}\n\t}\n\n\tgetActiveCodes(): string {\n\t\treturn this.activeAnsiCodes.join(\"\");\n\t}\n\n\thasActiveCodes(): boolean {\n\t\treturn this.activeAnsiCodes.length > 0;\n\t}\n}\n\nfunction updateTrackerFromText(text: string, tracker: AnsiCodeTracker): void {\n\tlet i = 0;\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\ttracker.process(ansiResult.code);\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\ti++;\n\t\t}\n\t}\n}\n\n/**\n * Split text into words while keeping ANSI codes attached.\n */\nfunction splitIntoTokensWithAnsi(text: string): string[] {\n\tconst tokens: string[] = [];\n\tlet current = \"\";\n\tlet inWhitespace = false;\n\tlet i = 0;\n\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tcurrent += ansiResult.code;\n\t\t\ti += ansiResult.length;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst char = text[i];\n\t\tconst charIsSpace = char === \" \";\n\n\t\tif (charIsSpace !== inWhitespace && current) {\n\t\t\t// Switching between whitespace and non-whitespace, push current token\n\t\t\ttokens.push(current);\n\t\t\tcurrent = \"\";\n\t\t}\n\n\t\tinWhitespace = charIsSpace;\n\t\tcurrent += char;\n\t\ti++;\n\t}\n\n\tif (current) {\n\t\ttokens.push(current);\n\t}\n\n\treturn tokens;\n}\n\n/**\n * Wrap text with ANSI codes preserved.\n *\n * ONLY does word wrapping - NO padding, NO background colors.\n * Returns lines where each line is <= width visible chars.\n * Active ANSI codes are preserved across line breaks.\n *\n * @param text - Text to wrap (may contain ANSI codes and newlines)\n * @param width - Maximum visible width per line\n * @returns Array of wrapped lines (NOT padded to width)\n */\nexport function wrapTextWithAnsi(text: string, width: number): string[] {\n\tif (!text) {\n\t\treturn [\"\"];\n\t}\n\n\t// Handle newlines by processing each line separately\n\tconst inputLines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const inputLine of inputLines) {\n\t\tresult.push(...wrapSingleLine(inputLine, width));\n\t}\n\n\treturn result.length > 0 ? result : [\"\"];\n}\n\nfunction wrapSingleLine(line: string, width: number): string[] {\n\tif (!line) {\n\t\treturn [\"\"];\n\t}\n\n\tconst visibleLength = visibleWidth(line);\n\tif (visibleLength <= width) {\n\t\treturn [line];\n\t}\n\n\tconst wrapped: string[] = [];\n\tconst tracker = new AnsiCodeTracker();\n\tconst tokens = splitIntoTokensWithAnsi(line);\n\n\tlet currentLine = \"\";\n\tlet currentVisibleLength = 0;\n\n\tfor (const token of tokens) {\n\t\tconst tokenVisibleLength = visibleWidth(token);\n\t\tconst isWhitespace = token.trim() === \"\";\n\n\t\t// Token itself is too long - break it character by character\n\t\tif (tokenVisibleLength > width && !isWhitespace) {\n\t\t\tif (currentLine) {\n\t\t\t\twrapped.push(currentLine);\n\t\t\t\tcurrentLine = \"\";\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t}\n\n\t\t\t// Break long token\n\t\t\tconst broken = breakLongWord(token, width, tracker);\n\t\t\twrapped.push(...broken.slice(0, -1));\n\t\t\tcurrentLine = broken[broken.length - 1];\n\t\t\tcurrentVisibleLength = visibleWidth(currentLine);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if adding this token would exceed width\n\t\tconst totalNeeded = currentVisibleLength + tokenVisibleLength;\n\n\t\tif (totalNeeded > width && currentVisibleLength > 0) {\n\t\t\t// Wrap to next line - don't carry trailing whitespace\n\t\t\twrapped.push(currentLine.trimEnd());\n\t\t\tif (isWhitespace) {\n\t\t\t\t// Don't start new line with whitespace\n\t\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\t\tcurrentVisibleLength = 0;\n\t\t\t} else {\n\t\t\t\tcurrentLine = tracker.getActiveCodes() + token;\n\t\t\t\tcurrentVisibleLength = tokenVisibleLength;\n\t\t\t}\n\t\t} else {\n\t\t\t// Add to current line\n\t\t\tcurrentLine += token;\n\t\t\tcurrentVisibleLength += tokenVisibleLength;\n\t\t}\n\n\t\tupdateTrackerFromText(token, tracker);\n\t}\n\n\tif (currentLine) {\n\t\twrapped.push(currentLine);\n\t}\n\n\treturn wrapped.length > 0 ? wrapped : [\"\"];\n}\n\n// Grapheme segmenter for proper Unicode iteration (handles emojis, etc.)\nconst segmenter = new Intl.Segmenter();\n\nfunction breakLongWord(word: string, width: number, tracker: AnsiCodeTracker): string[] {\n\tconst lines: string[] = [];\n\tlet currentLine = tracker.getActiveCodes();\n\tlet currentWidth = 0;\n\n\t// First, separate ANSI codes from visible content\n\t// We need to handle ANSI codes specially since they're not graphemes\n\tlet i = 0;\n\tconst segments: Array<{ type: \"ansi\" | \"grapheme\"; value: string }> = [];\n\n\twhile (i < word.length) {\n\t\tconst ansiResult = extractAnsiCode(word, i);\n\t\tif (ansiResult) {\n\t\t\tsegments.push({ type: \"ansi\", value: ansiResult.code });\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\t// Find the next ANSI code or end of string\n\t\t\tlet end = i;\n\t\t\twhile (end < word.length) {\n\t\t\t\tconst nextAnsi = extractAnsiCode(word, end);\n\t\t\t\tif (nextAnsi) break;\n\t\t\t\tend++;\n\t\t\t}\n\t\t\t// Segment this non-ANSI portion into graphemes\n\t\t\tconst textPortion = word.slice(i, end);\n\t\t\tfor (const seg of segmenter.segment(textPortion)) {\n\t\t\t\tsegments.push({ type: \"grapheme\", value: seg.segment });\n\t\t\t}\n\t\t\ti = end;\n\t\t}\n\t}\n\n\t// Now process segments\n\tfor (const seg of segments) {\n\t\tif (seg.type === \"ansi\") {\n\t\t\tcurrentLine += seg.value;\n\t\t\ttracker.process(seg.value);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst grapheme = seg.value;\n\t\tconst graphemeWidth = visibleWidth(grapheme);\n\n\t\tif (currentWidth + graphemeWidth > width) {\n\t\t\tlines.push(currentLine);\n\t\t\tcurrentLine = tracker.getActiveCodes();\n\t\t\tcurrentWidth = 0;\n\t\t}\n\n\t\tcurrentLine += grapheme;\n\t\tcurrentWidth += graphemeWidth;\n\t}\n\n\tif (currentLine) {\n\t\tlines.push(currentLine);\n\t}\n\n\treturn lines.length > 0 ? lines : [\"\"];\n}\n\n/**\n * Apply background color to a line, padding to full width.\n *\n * @param line - Line of text (may contain ANSI codes)\n * @param width - Total width to pad to\n * @param bgFn - Background color function\n * @returns Line with background applied and padded to width\n */\nexport function applyBackgroundToLine(line: string, width: number, bgFn: (text: string) => string): string {\n\t// Calculate padding needed\n\tconst visibleLen = visibleWidth(line);\n\tconst paddingNeeded = Math.max(0, width - visibleLen);\n\tconst padding = \" \".repeat(paddingNeeded);\n\n\t// Apply background to content + padding\n\tconst withPadding = line + padding;\n\treturn bgFn(withPadding);\n}\n\n/**\n * Truncate text to fit within a maximum visible width, adding ellipsis if needed.\n * Properly handles ANSI escape codes (they don't count toward width).\n *\n * @param text - Text to truncate (may contain ANSI codes)\n * @param maxWidth - Maximum visible width\n * @param ellipsis - Ellipsis string to append when truncating (default: \"...\")\n * @returns Truncated text with ellipsis if it exceeded maxWidth\n */\nexport function truncateToWidth(text: string, maxWidth: number, ellipsis: string = \"...\"): string {\n\tconst textVisibleWidth = visibleWidth(text);\n\n\tif (textVisibleWidth <= maxWidth) {\n\t\treturn text;\n\t}\n\n\tconst ellipsisWidth = visibleWidth(ellipsis);\n\tconst targetWidth = maxWidth - ellipsisWidth;\n\n\tif (targetWidth <= 0) {\n\t\treturn ellipsis.substring(0, maxWidth);\n\t}\n\n\t// Separate ANSI codes from visible content using grapheme segmentation\n\tlet i = 0;\n\tconst segments: Array<{ type: \"ansi\" | \"grapheme\"; value: string }> = [];\n\n\twhile (i < text.length) {\n\t\tconst ansiResult = extractAnsiCode(text, i);\n\t\tif (ansiResult) {\n\t\t\tsegments.push({ type: \"ansi\", value: ansiResult.code });\n\t\t\ti += ansiResult.length;\n\t\t} else {\n\t\t\t// Find the next ANSI code or end of string\n\t\t\tlet end = i;\n\t\t\twhile (end < text.length) {\n\t\t\t\tconst nextAnsi = extractAnsiCode(text, end);\n\t\t\t\tif (nextAnsi) break;\n\t\t\t\tend++;\n\t\t\t}\n\t\t\t// Segment this non-ANSI portion into graphemes\n\t\t\tconst textPortion = text.slice(i, end);\n\t\t\tfor (const seg of segmenter.segment(textPortion)) {\n\t\t\t\tsegments.push({ type: \"grapheme\", value: seg.segment });\n\t\t\t}\n\t\t\ti = end;\n\t\t}\n\t}\n\n\t// Build truncated string from segments\n\tlet result = \"\";\n\tlet currentWidth = 0;\n\n\tfor (const seg of segments) {\n\t\tif (seg.type === \"ansi\") {\n\t\t\tresult += seg.value;\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst grapheme = seg.value;\n\t\tconst graphemeWidth = visibleWidth(grapheme);\n\n\t\tif (currentWidth + graphemeWidth > targetWidth) {\n\t\t\tbreak;\n\t\t}\n\n\t\tresult += grapheme;\n\t\tcurrentWidth += graphemeWidth;\n\t}\n\n\t// Add reset code before ellipsis to prevent styling leaking into it\n\treturn result + \"\\x1b[0m\" + ellipsis;\n}\n"]}
|
package/package.json
CHANGED