tuix
Version:
A performant TUI framework for Bun with JSX and reactive state management
206 lines (174 loc) • 5.42 kB
text/typescript
/**
* Optimized String Width Calculation
*
* Fast string width calculation with caching for terminal rendering
*/
// Cache for width calculations
const WIDTH_CACHE = new Map<string, number>()
const MAX_CACHE_SIZE = 10000
// Pre-computed width tables for common characters
const ASCII_WIDTHS = new Array(128).fill(1)
// Control characters have width 0
for (let i = 0; i < 32; i++) ASCII_WIDTHS[i] = 0
ASCII_WIDTHS[127] = 0 // DEL
// Common emoji and unicode character widths (simplified)
const WIDE_CHAR_RANGES = [
[], // Hangul Jamo
[], // CJK Radicals Supplement
[], // Kangxi Radicals
[], // CJK Symbols and Punctuation
[], // Hiragana
[], // Katakana
[], // Bopomofo
[], // Hangul Compatibility Jamo
[], // Kanbun
[], // CJK Strokes
[], // Katakana Phonetic Extensions
[], // Enclosed CJK Letters and Months
[], // Enclosed CJK Letters and Months
[], // CJK Compatibility
[], // CJK Unified Ideographs
[], // Hangul Jamo Extended-A
[], // Hangul Syllables
[], // CJK Compatibility Ideographs
[], // Vertical Forms
[], // CJK Compatibility Forms
[], // Fullwidth Forms
[], // Fullwidth Forms
[], // Miscellaneous Symbols and Pictographs
[], // Emoticons
[], // Transport and Map Symbols
[], // Alchemical Symbols
[], // Geometric Shapes Extended
[], // Supplemental Arrows-C
[], // Supplemental Symbols and Pictographs
]
/**
* Check if a character code is in wide character ranges
*/
function isWideCharCode(code: number): boolean {
for (const [start, end] of WIDE_CHAR_RANGES) {
if (code >= start && code <= end) {
return true
}
}
return false
}
/**
* Fast character width calculation
*/
function getCharWidth(char: string): number {
const code = char.codePointAt(0) || 0
// Fast path for ASCII
if (code < 128) {
return ASCII_WIDTHS[code]
}
// Check for zero-width characters
if (
(code >= 0x0300 && code <= 0x036F) || // Combining Diacritical Marks
(code >= 0x1AB0 && code <= 0x1AFF) || // Combining Diacritical Marks Extended
(code >= 0x1DC0 && code <= 0x1DFF) || // Combining Diacritical Marks Supplement
(code >= 0x20D0 && code <= 0x20FF) || // Combining Diacritical Marks for Symbols
(code >= 0xFE20 && code <= 0xFE2F) // Combining Half Marks
) {
return 0
}
// Check for wide characters
if (isWideCharCode(code)) {
return 2
}
// Default to width 1
return 1
}
/**
* Calculate string width with caching
*/
export function stringWidthOptimized(str: string): number {
if (!str) return 0
// Check cache first
const cached = WIDTH_CACHE.get(str)
if (cached !== undefined) {
return cached
}
let width = 0
// Fast path for ASCII-only strings
if (/^[\x00-\x7F]*$/.test(str)) {
for (let i = 0; i < str.length; i++) {
width += ASCII_WIDTHS[str.charCodeAt(i)]
}
} else {
// Handle Unicode strings
for (const char of str) {
width += getCharWidth(char)
}
}
// Cache the result
if (WIDTH_CACHE.size < MAX_CACHE_SIZE) {
WIDTH_CACHE.set(str, width)
}
return width
}
/**
* Calculate width without ANSI escape sequences
*/
export function stringWidthNoAnsi(str: string): number {
if (!str) return 0
// Remove ANSI escape sequences
const cleaned = str.replace(/\x1b\[[0-9;]*m/g, '')
return stringWidthOptimized(cleaned)
}
/**
* Pad string to specific width (optimized)
*/
export function padStringOptimized(str: string, width: number, char = ' '): string {
const currentWidth = stringWidthOptimized(str)
const padding = width - currentWidth
if (padding <= 0) return str
return str + char.repeat(padding)
}
/**
* Truncate string to specific width (optimized)
*/
export function truncateStringOptimized(str: string, maxWidth: number, suffix = '...'): string {
if (!str) return ''
const currentWidth = stringWidthOptimized(str)
if (currentWidth <= maxWidth) return str
const suffixWidth = stringWidthOptimized(suffix)
const targetWidth = maxWidth - suffixWidth
if (targetWidth <= 0) return maxWidth <= 0 ? '' : suffix.slice(0, maxWidth)
let width = 0
let result = ''
for (const char of str) {
const charWidth = getCharWidth(char)
if (width + charWidth > targetWidth) break
result += char
width += charWidth
}
return result + suffix
}
/**
* Clear the width cache
*/
export function clearWidthCache(): void {
WIDTH_CACHE.clear()
}
/**
* Get cache statistics
*/
export function getWidthCacheStats() {
return {
size: WIDTH_CACHE.size,
maxSize: MAX_CACHE_SIZE,
hitRate: WIDTH_CACHE.size > 0 ? 'N/A' : 'No data'
}
}
/**
* Alias for stringWidthOptimized to match test expectations
* This version strips ANSI and only measures the first line
*/
export const stringWidth = (str: string): number => {
// Take only the first line
const firstLine = str.split('\n')[0] || ''
// Use the no-ANSI version
return stringWidthNoAnsi(firstLine)
}