tuix
Version:
A performant TUI framework for Bun with JSX and reactive state management
103 lines (86 loc) • 2.66 kB
text/typescript
/**
* String width calculation utility using Bun's native stringWidth
*
* This wrapper provides compatibility with the string-width package
* while leveraging Bun's native performance (26x faster).
*
* Handles edge cases where Bun.stringWidth differs from string-width package.
*/
// Known emoji sequences where Bun.stringWidth returns different values
const EMOJI_OVERRIDES: Record<string, number> = {
"🏳️🌈": 2, // Rainbow flag - Bun returns 1, should be 2
}
/**
* Calculate the visual width of a string in terminal columns
*
* @param str - The string to measure
* @returns The width in terminal columns
*/
export const stringWidth = (str: string): number => {
// Check for exact matches in overrides
if (str in EMOJI_OVERRIDES) {
return EMOJI_OVERRIDES[str]
}
// Use Bun's native implementation
return Bun.stringWidth(str)
}
/**
* Truncate a string to fit within a given width
*
* @param str - The string to truncate
* @param maxWidth - Maximum width in columns
* @param suffix - Suffix to append when truncated (default: "…")
* @returns Truncated string
*/
export const truncateString = (str: string, maxWidth: number, suffix = "…"): string => {
const width = stringWidth(str)
if (width <= maxWidth) {
return str
}
const suffixWidth = stringWidth(suffix)
const targetWidth = maxWidth - suffixWidth
if (targetWidth <= 0) {
return suffix.slice(0, maxWidth)
}
let result = ""
let currentWidth = 0
// Use grapheme segmenter to avoid splitting characters
const segmenter = new Intl.Segmenter()
const segments = [...segmenter.segment(str)]
for (const { segment } of segments) {
const segmentWidth = stringWidth(segment)
if (currentWidth + segmentWidth > targetWidth) {
break
}
result += segment
currentWidth += segmentWidth
}
return result + suffix
}
/**
* Pad a string to a specific width
*
* @param str - The string to pad
* @param width - Target width
* @param align - Alignment (left, center, right)
* @returns Padded string
*/
export const padString = (str: string, width: number, align: 'left' | 'center' | 'right' = 'left'): string => {
const strWidth = stringWidth(str)
if (strWidth >= width) {
return str
}
const padding = width - strWidth
switch (align) {
case 'center': {
const leftPad = Math.floor(padding / 2)
const rightPad = padding - leftPad
return ' '.repeat(leftPad) + str + ' '.repeat(rightPad)
}
case 'right':
return ' '.repeat(padding) + str
case 'left':
default:
return str + ' '.repeat(padding)
}
}