chore: initialize recovered claude workspace
This commit is contained in:
179
src/utils/truncate.ts
Normal file
179
src/utils/truncate.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
// Width-aware truncation/wrapping — needs ink/stringWidth (not leaf-safe).
|
||||
|
||||
import { stringWidth } from '../ink/stringWidth.js'
|
||||
import { getGraphemeSegmenter } from './intl.js'
|
||||
|
||||
/**
|
||||
* Truncates a file path in the middle to preserve both directory context and filename.
|
||||
* Width-aware: uses stringWidth() for correct CJK/emoji measurement.
|
||||
* For example: "src/components/deeply/nested/folder/MyComponent.tsx" becomes
|
||||
* "src/components/…/MyComponent.tsx" when maxLength is 30.
|
||||
*
|
||||
* @param path The file path to truncate
|
||||
* @param maxLength Maximum display width of the result in terminal columns (must be > 0)
|
||||
* @returns The truncated path, or original if it fits within maxLength
|
||||
*/
|
||||
export function truncatePathMiddle(path: string, maxLength: number): string {
|
||||
// No truncation needed
|
||||
if (stringWidth(path) <= maxLength) {
|
||||
return path
|
||||
}
|
||||
|
||||
// Handle edge case of very small or non-positive maxLength
|
||||
if (maxLength <= 0) {
|
||||
return '…'
|
||||
}
|
||||
|
||||
// Need at least room for "…" + something meaningful
|
||||
if (maxLength < 5) {
|
||||
return truncateToWidth(path, maxLength)
|
||||
}
|
||||
|
||||
// Find the filename (last path segment)
|
||||
const lastSlash = path.lastIndexOf('/')
|
||||
// Include the leading slash in filename for display
|
||||
const filename = lastSlash >= 0 ? path.slice(lastSlash) : path
|
||||
const directory = lastSlash >= 0 ? path.slice(0, lastSlash) : ''
|
||||
const filenameWidth = stringWidth(filename)
|
||||
|
||||
// If filename alone is too long, truncate from start
|
||||
if (filenameWidth >= maxLength - 1) {
|
||||
return truncateStartToWidth(path, maxLength)
|
||||
}
|
||||
|
||||
// Calculate space available for directory prefix
|
||||
// Result format: directory + "…" + filename
|
||||
const availableForDir = maxLength - 1 - filenameWidth // -1 for ellipsis
|
||||
|
||||
if (availableForDir <= 0) {
|
||||
// No room for directory, just show filename (truncated if needed)
|
||||
return truncateStartToWidth(filename, maxLength)
|
||||
}
|
||||
|
||||
// Truncate directory and combine
|
||||
const truncatedDir = truncateToWidthNoEllipsis(directory, availableForDir)
|
||||
return truncatedDir + '…' + filename
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a string to fit within a maximum display width, measured in terminal columns.
|
||||
* Splits on grapheme boundaries to avoid breaking emoji or surrogate pairs.
|
||||
* Appends '…' when truncation occurs.
|
||||
*/
|
||||
export function truncateToWidth(text: string, maxWidth: number): string {
|
||||
if (stringWidth(text) <= maxWidth) return text
|
||||
if (maxWidth <= 1) return '…'
|
||||
let width = 0
|
||||
let result = ''
|
||||
for (const { segment } of getGraphemeSegmenter().segment(text)) {
|
||||
const segWidth = stringWidth(segment)
|
||||
if (width + segWidth > maxWidth - 1) break
|
||||
result += segment
|
||||
width += segWidth
|
||||
}
|
||||
return result + '…'
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates from the start of a string, keeping the tail end.
|
||||
* Prepends '…' when truncation occurs.
|
||||
* Width-aware and grapheme-safe.
|
||||
*/
|
||||
export function truncateStartToWidth(text: string, maxWidth: number): string {
|
||||
if (stringWidth(text) <= maxWidth) return text
|
||||
if (maxWidth <= 1) return '…'
|
||||
const segments = [...getGraphemeSegmenter().segment(text)]
|
||||
let width = 0
|
||||
let startIdx = segments.length
|
||||
for (let i = segments.length - 1; i >= 0; i--) {
|
||||
const segWidth = stringWidth(segments[i]!.segment)
|
||||
if (width + segWidth > maxWidth - 1) break // -1 for '…'
|
||||
width += segWidth
|
||||
startIdx = i
|
||||
}
|
||||
return (
|
||||
'…' +
|
||||
segments
|
||||
.slice(startIdx)
|
||||
.map(s => s.segment)
|
||||
.join('')
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a string to fit within a maximum display width, without appending an ellipsis.
|
||||
* Useful when the caller adds its own separator (e.g. middle-truncation with '…' between parts).
|
||||
* Width-aware and grapheme-safe.
|
||||
*/
|
||||
export function truncateToWidthNoEllipsis(
|
||||
text: string,
|
||||
maxWidth: number,
|
||||
): string {
|
||||
if (stringWidth(text) <= maxWidth) return text
|
||||
if (maxWidth <= 0) return ''
|
||||
let width = 0
|
||||
let result = ''
|
||||
for (const { segment } of getGraphemeSegmenter().segment(text)) {
|
||||
const segWidth = stringWidth(segment)
|
||||
if (width + segWidth > maxWidth) break
|
||||
result += segment
|
||||
width += segWidth
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates a string to fit within a maximum display width (terminal columns),
|
||||
* splitting on grapheme boundaries to avoid breaking emoji, CJK, or surrogate pairs.
|
||||
* Appends '…' when truncation occurs.
|
||||
* @param str The string to truncate
|
||||
* @param maxWidth Maximum display width in terminal columns
|
||||
* @param singleLine If true, also truncates at the first newline
|
||||
* @returns The truncated string with ellipsis if needed
|
||||
*/
|
||||
export function truncate(
|
||||
str: string,
|
||||
maxWidth: number,
|
||||
singleLine: boolean = false,
|
||||
): string {
|
||||
let result = str
|
||||
|
||||
// If singleLine is true, truncate at first newline
|
||||
if (singleLine) {
|
||||
const firstNewline = str.indexOf('\n')
|
||||
if (firstNewline !== -1) {
|
||||
result = str.substring(0, firstNewline)
|
||||
// Ensure total width including ellipsis doesn't exceed maxWidth
|
||||
if (stringWidth(result) + 1 > maxWidth) {
|
||||
return truncateToWidth(result, maxWidth)
|
||||
}
|
||||
return `${result}…`
|
||||
}
|
||||
}
|
||||
|
||||
if (stringWidth(result) <= maxWidth) {
|
||||
return result
|
||||
}
|
||||
return truncateToWidth(result, maxWidth)
|
||||
}
|
||||
|
||||
export function wrapText(text: string, width: number): string[] {
|
||||
const lines: string[] = []
|
||||
let currentLine = ''
|
||||
let currentWidth = 0
|
||||
|
||||
for (const { segment } of getGraphemeSegmenter().segment(text)) {
|
||||
const segWidth = stringWidth(segment)
|
||||
if (currentWidth + segWidth <= width) {
|
||||
currentLine += segment
|
||||
currentWidth += segWidth
|
||||
} else {
|
||||
if (currentLine) lines.push(currentLine)
|
||||
currentLine = segment
|
||||
currentWidth = segWidth
|
||||
}
|
||||
}
|
||||
|
||||
if (currentLine) lines.push(currentLine)
|
||||
return lines
|
||||
}
|
||||
Reference in New Issue
Block a user