The twwidth package provides display width calculation for strings, with automatic handling of ANSI escape sequences and East Asian character widths. It includes a thread-safe LRU cache for high-performance width calculations and text truncation with ANSI preservation.
go get github.com/olekukonko/tablewriter/pkg/twwidthimport "github.com/olekukonko/tablewriter/pkg/twwidth"import "github.com/olekukonko/tablewriter/pkg/twwidth"
// Calculate display width of plain text
width := twwidth.Width("Hello") // Returns: 5
// Width calculation automatically strips ANSI codes
colorized := "\x1b[31mHello\x1b[0m"
width = twwidth.Width(colorized) // Returns: 5 (color codes ignored)
// Truncate string to fit within display width
truncated := twwidth.Truncate("Hello World", 8, "...") // Returns: "Hello..."
// Enable East Asian width handling (treats ambiguous characters as wide)
twwidth.SetEastAsian(true)
// Get cache statistics
size, capacity, hitRate := twwidth.GetCacheStats()Calculate the visual display width of a string, automatically stripping ANSI escape sequences.
func Width(str string) intUses the global cache and East Asian width settings. Thread-safe.
Parameters:
str - Input string (may contain ANSI escape sequences)Returns:
Example:
// Plain text
width := twwidth.Width("Hello") // Returns: 5
// Text with ANSI color codes
colored := "\x1b[31mRed Text\x1b[0m"
width = twwidth.Width(colored) // Returns: 8 (color codes stripped)
// East Asian characters (with default settings)
width = twwidth.Width("你好") // Returns: 4 (2 wide characters)Calculate width with specific options, bypassing global settings and cache.
func WidthWithOptions(str string, opts Options) intParameters:
str - Input stringopts - Width calculation options (including EastAsianWidth setting)Returns:
Example:
// Calculate width with East Asian handling enabled
opts := twwidth.Options{EastAsianWidth: true}
width := twwidth.WidthWithOptions("Hello 你好", opts)
// Calculate width with East Asian handling disabled
opts = twwidth.Options{EastAsianWidth: false}
width = twwidth.WidthWithOptions("Hello 你好", opts)Calculate width without using the cache (uses current global settings).
func WidthNoCache(str string) intParameters:
str - Input stringReturns:
Example:
// Bypass cache for one-off calculation
width := twwidth.WidthNoCache("Temporary text")Deprecated: Calculate width using a runewidth.Condition (backward compatibility).
func Display(cond *runewidth.Condition, str string) intUse WidthWithOptions instead with the new Options struct.
Truncate a string to fit within a specified visual display width.
func Truncate(s string, maxWidth int, suffix ...string) stringPreserves ANSI escape sequences and adds reset sequences (\x1b[0m) when needed to prevent formatting bleed. Respects global East Asian width settings. Thread-safe.
Parameters:
s - Input string (may contain ANSI codes)maxWidth - Maximum visual display widthsuffix - Optional suffix to append (e.g., "..." for ellipsis). Multiple suffixes are joinedReturns:
maxWidth, including any suffixBehavior:
maxWidth < 0: Returns empty stringmaxWidth == 0 and suffix provided: Returns suffix if it fitsmaxWidth: Returns original string (with ANSI reset if needed)Example:
// Basic truncation
result := twwidth.Truncate("Hello World", 8) // Returns: "Hello Wo"
// Truncation with suffix
result = twwidth.Truncate("Hello World", 8, "...") // Returns: "Hello..."
// Truncation with ANSI codes preserved
colored := "\x1b[31mRed Text Here\x1b[0m"
result = twwidth.Truncate(colored, 8, "...")
// Returns: "\x1b[31mRed Text\x1b[0m..." (preserves color, adds reset)
// Custom suffix
result = twwidth.Truncate("Long text here", 10, " [more]")
// Returns: "Long [more]"
// Empty result when maxWidth is negative
result = twwidth.Truncate("Hello", -1) // Returns: ""
// String shorter than maxWidth
result = twwidth.Truncate("Hello", 10) // Returns: "Hello" (or "Hello\x1b[0m" if ANSI present)Configure global East Asian width handling and options.
func SetOptions(opts Options)Sets global width calculation options. Changes to EastAsianWidth purge the cache. Thread-safe.
Parameters:
opts - Global options to applyExample:
// Enable East Asian width handling globally
opts := twwidth.Options{EastAsianWidth: true}
twwidth.SetOptions(opts)
// Disable East Asian width handling
opts = twwidth.Options{EastAsianWidth: false}
twwidth.SetOptions(opts)Enable or disable East Asian width handling globally.
func SetEastAsian(enable bool)Thread-safe. Purges cache when setting changes.
Parameters:
enable - true to enable East Asian width (treat ambiguous characters as wide), false to disableExample:
// Enable East Asian width handling
twwidth.SetEastAsian(true)
// Characters like "│" are now treated as wide
width := twwidth.Width("│") // Returns: 2 (with EastAsian enabled)
// Disable East Asian width handling
twwidth.SetEastAsian(false)
width = twwidth.Width("│") // Returns: 1 (with EastAsian disabled)Get current East Asian width setting.
func IsEastAsian() boolThread-safe.
Returns:
true if East Asian width handling is enabled, false otherwiseExample:
if twwidth.IsEastAsian() {
fmt.Println("East Asian width handling is enabled")
} else {
fmt.Println("East Asian width handling is disabled")
}Deprecated: Set runewidth condition for backward compatibility.
func SetCondition(cond *runewidth.Condition)Use SetOptions with the new Options struct instead.
Manage the internal LRU cache for width calculations.
func SetCacheCapacity(capacity int)Changes the cache size dynamically. Thread-safe.
Parameters:
capacity - New cache capacity. If capacity <= 0, caching is disabled entirelyExample:
// Increase cache size
twwidth.SetCacheCapacity(16384)
// Disable caching
twwidth.SetCacheCapacity(0)
// Re-enable with smaller cache
twwidth.SetCacheCapacity(4096)Get current cache statistics.
func GetCacheStats() (size, capacity int, hitRate float64)Thread-safe.
Returns:
size - Current number of entries in cachecapacity - Maximum cache capacityhitRate - Cache hit rate (0.0 to 1.0), where 1.0 means 100% cache hitsExample:
size, capacity, hitRate := twwidth.GetCacheStats()
fmt.Printf("Cache: %d/%d entries, %.2f%% hit rate\n",
size, capacity, hitRate*100)
// Example output: "Cache: 1234/8192 entries, 87.50% hit rate"Get a compiled regular expression for matching ANSI escape sequences.
func Filter() *regexp.RegexpReturns a regex that matches both CSI (Control Sequence Introducer) and OSC (Operating System Command) ANSI sequences.
Returns:
Example:
ansi := twwidth.Filter()
// Strip ANSI codes from a string
text := "\x1b[31mRed\x1b[0m \x1b[32mGreen\x1b[0m"
clean := ansi.ReplaceAllLiteralString(text, "")
fmt.Println(clean) // Output: "Red Green"
// Check if string contains ANSI codes
hasANSI := ansi.MatchString(text)
fmt.Println(hasANSI) // Output: true// Options configures width calculation on a per-call basis
type Options struct {
// EastAsianWidth enables East Asian ambiguous character width handling.
// When true, ambiguous-width characters (like box drawing characters)
// are treated as wide (width=2) instead of narrow (width=1).
EastAsianWidth bool
}ANSI escape sequences are special character sequences used for terminal formatting (colors, cursor movement, etc.). They are not visible when rendered but occupy bytes in the string. The twwidth package automatically detects and excludes these sequences when calculating display width.
ANSI Sequence Types Handled:
\x1b[ followed by parameters and a command byte (e.g., \x1b[31m for red text)\x1b] followed by content and terminated by \x1b\ or \x07 (e.g., \x1b]0;Title\x07 for window title)Example:
// String with ANSI color codes
colored := "\x1b[31mHello\x1b[0m World"
// Byte length includes ANSI codes
fmt.Println(len(colored)) // Output: 21 bytes
// Display width excludes ANSI codes
width := twwidth.Width(colored)
fmt.Println(width) // Output: 11 (just "Hello World")The Unicode standard defines some characters as "East Asian Ambiguous Width" (like box-drawing characters │, ─, etc.). In East Asian locales, these are typically displayed as wide (2 columns), while in other locales they are narrow (1 column).
When to Enable:
EastAsianWidth when rendering for East Asian terminals or when working with East Asian textExample:
// Box drawing character with different settings
boxChar := "│"
// With East Asian width disabled (default)
twwidth.SetEastAsian(false)
fmt.Println(twwidth.Width(boxChar)) // Output: 1
// With East Asian width enabled
twwidth.SetEastAsian(true)
fmt.Println(twwidth.Width(boxChar)) // Output: 2Display width differs from byte length because:
Example:
// ASCII: 1 byte = 1 display column
ascii := "A"
fmt.Println(len(ascii)) // 1 byte
fmt.Println(twwidth.Width(ascii)) // 1 column
// CJK: 3 bytes = 2 display columns
cjk := "你"
fmt.Println(len(cjk)) // 3 bytes (UTF-8)
fmt.Println(twwidth.Width(cjk)) // 2 columns (wide character)
// ANSI codes: N bytes = 0 display columns
ansi := "\x1b[31m"
fmt.Println(len(ansi)) // 5 bytes
fmt.Println(twwidth.Width(ansi)) // 0 columns (control sequence)The package uses a thread-safe LRU (Least Recently Used) cache to optimize repeated width calculations. The cache is particularly beneficial when:
Cache Behavior:
"0:" prefix for false, "1:" prefix for true)EastAsianWidth setting purges the cacheFunctions Using Cache:
Width() - Uses cache with global settingsTruncate() - Uses cache internally for width calculationsFunctions Bypassing Cache:
WidthWithOptions() - Direct calculation without cacheWidthNoCache() - Direct calculation with global settingsExample:
// Initial calculation (cache miss)
width1 := twwidth.Width("Hello")
// Subsequent calculation (cache hit)
width2 := twwidth.Width("Hello")
// Check cache statistics
size, capacity, hitRate := twwidth.GetCacheStats()
fmt.Printf("Hit rate: %.2f%%\n", hitRate*100)
// Disable caching for memory-constrained environments
twwidth.SetCacheCapacity(0)
// Re-enable with custom capacity
twwidth.SetCacheCapacity(4096)All exported functions in the twwidth package are thread-safe:
Example:
import (
"sync"
"github.com/olekukonko/tablewriter/pkg/twwidth"
)
// Safe to use from multiple goroutines
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(text string) {
defer wg.Done()
width := twwidth.Width(text)
fmt.Println(width)
}(fmt.Sprintf("Text %d", i))
}
wg.Wait()
// Safe to change settings concurrently
go twwidth.SetEastAsian(true)
go twwidth.SetCacheCapacity(16384)Calculate column widths for aligned table output:
headers := []string{"Name", "Age", "City"}
rows := [][]string{
{"Alice", "30", "New York"},
{"Bob", "25", "Los Angeles"},
{"Charlie", "35", "Chicago"},
}
// Calculate maximum width for each column
colWidths := make([]int, len(headers))
for i, header := range headers {
colWidths[i] = twwidth.Width(header)
}
for _, row := range rows {
for i, cell := range row {
width := twwidth.Width(cell)
if width > colWidths[i] {
colWidths[i] = width
}
}
}
// Use colWidths for formattingHandle colored text while maintaining alignment:
// Colored strings with different visual widths
items := []string{
"\x1b[31mError:\x1b[0m Connection failed",
"\x1b[33mWarning:\x1b[0m Low memory",
"\x1b[32mSuccess:\x1b[0m Complete",
}
// Truncate to consistent display width
maxWidth := 30
for _, item := range items {
truncated := twwidth.Truncate(item, maxWidth, "...")
fmt.Println(truncated)
}Handle mixed ASCII and Unicode text:
// Mixed content
texts := []string{
"Hello World", // ASCII
"你好世界", // Chinese (wide characters)
"Hello 世界", // Mixed
"\x1b[31m你好\x1b[0m", // Colored Chinese
}
// Calculate widths correctly for all cases
for _, text := range texts {
width := twwidth.Width(text)
fmt.Printf("Text: %s, Width: %d\n", text, width)
}Truncate long text to fit in limited space:
// Truncate file paths
longPath := "/very/long/path/to/some/file/with/a/long/name.txt"
shortPath := twwidth.Truncate(longPath, 30, "...")
// Truncate descriptions
desc := "This is a very long description that needs to be shortened"
shortDesc := twwidth.Truncate(desc, 40, "…")
// Truncate with custom suffix
code := "function veryLongFunctionName(param1, param2, param3)"
shortCode := twwidth.Truncate(code, 35, "...)")