The twwarp package provides text wrapping algorithms for optimal line breaking in terminal tables. It implements a Knuth-Plass-inspired minimal raggedness algorithm for high-quality text layout and supports Unicode-aware word segmentation using UAX#29.
github.com/olekukonko/tablewriter/pkg/twwarpimport "github.com/olekukonko/tablewriter/pkg/twwarp"The twwarp package is designed for text wrapping within table cells, providing:
The package uses display width calculations from the twwidth package to handle ANSI escape sequences, East Asian characters, and combining characters correctly.
package main
import (
"fmt"
"github.com/olekukonko/tablewriter/pkg/twwarp"
)
func main() {
// Simple text wrapping
text := "This is a long sentence that needs to be wrapped to fit within a specific width"
lines, actualWidth := twwarp.WrapString(text, 30)
for _, line := range lines {
fmt.Printf("[%s]\n", line)
}
fmt.Printf("Actual width used: %d\n", actualWidth)
// Output:
// [This is a long sentence that]
// [needs to be wrapped to fit]
// [within a specific width]
// Actual width used: 30
}Splits a string into words using Unicode-aware word boundary detection (UAX#29).
func SplitWords(s string) []stringParameters:
s string - Input string to split into wordsReturns:
[]string - Slice of words (whitespace removed)Description:
SplitWords performs Unicode-aware word segmentation by identifying word boundaries according to Unicode Annex #29 (UAX#29). It splits text at spaces and returns only the non-whitespace word tokens. This is the foundational function used by the higher-level wrapping functions.
The function is optimized for performance with pre-allocated capacity estimates and does not allocate memory for spaces.
Usage Example:
package main
import (
"fmt"
"github.com/olekukonko/tablewriter/pkg/twwarp"
)
func main() {
text := "Hello world! How are you?"
words := twwarp.SplitWords(text)
fmt.Printf("Words: %v\n", words)
// Output: Words: [Hello world! How are you?]
// Handles multiple spaces
text2 := "Multiple spaces between words"
words2 := twwarp.SplitWords(text2)
fmt.Printf("Words: %v\n", words2)
// Output: Words: [Multiple spaces between words]
// Handles Unicode
text3 := "日本語 English 混合"
words3 := twwarp.SplitWords(text3)
fmt.Printf("Words: %v\n", words3)
// Output: Words: [日本語 English 混合]
}Wraps a string into lines with minimal raggedness, optimal for most text wrapping needs.
func WrapString(s string, lim int) ([]string, int)Parameters:
s string - Input string to wraplim int - Maximum display width per line (in terminal columns)Returns:
[]string - Wrapped linesint - Adjusted width limit (may be increased if a word exceeds lim)Description:
WrapString is the primary text wrapping function that breaks text into lines using a minimal raggedness algorithm. It:
SplitWordstwwidth.WidthWrapWords) to minimize raggednessThe function handles edge cases:
[]string{" "}[]string{""}WrapStringWithSpaces for that)Minimal Raggedness Algorithm:
The algorithm minimizes the total "raggedness" across all lines, where raggedness is measured as the squared difference between line length and the target width. This produces visually pleasing text layout by avoiding very short lines followed by very long lines.
Usage Example:
package main
import (
"fmt"
"github.com/olekukonko/tablewriter/pkg/twwarp"
)
func main() {
// Basic wrapping
text := "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
lines, width := twwarp.WrapString(text, 20)
fmt.Printf("Wrapped to width %d:\n", width)
for i, line := range lines {
fmt.Printf("Line %d: [%s]\n", i+1, line)
}
// Output:
// Wrapped to width 20:
// Line 1: [Lorem ipsum dolor]
// Line 2: [sit amet,]
// Line 3: [consectetur]
// Line 4: [adipiscing elit]
// Automatic width adjustment for long words
text2 := "This contains supercalifragilisticexpialidocious word"
lines2, width2 := twwarp.WrapString(text2, 20)
fmt.Printf("\nAdjusted width from 20 to %d:\n", width2)
for i, line := range lines2 {
fmt.Printf("Line %d: [%s]\n", i+1, line)
}
// Output:
// Adjusted width from 20 to 34:
// Line 1: [This contains]
// Line 2: [supercalifragilisticexpialidocious]
// Line 3: [word]
}Wraps text while preserving leading and trailing spaces, essential for formatted output.
func WrapStringWithSpaces(s string, lim int) ([]string, int)Parameters:
s string - Input string to wraplim int - Maximum display width per line (in terminal columns)Returns:
[]string - Wrapped lines with preserved spacingint - Adjusted width limit (may be increased if a word exceeds lim)Description:
WrapStringWithSpaces provides space-aware text wrapping that preserves leading and trailing whitespace in the input string. This is critical for maintaining formatting in logs, code output, and other structured text.
The function:
Special Cases:
[]string{""}This function is used internally by the tablewriter logging library to format log messages with consistent indentation and spacing.
Usage Example:
package main
import (
"fmt"
"github.com/olekukonko/tablewriter/pkg/twwarp"
)
func main() {
// Text with leading spaces (indentation)
text := " This is an indented paragraph that needs wrapping"
lines, width := twwarp.WrapStringWithSpaces(text, 25)
fmt.Printf("Wrapped with preserved leading spaces:\n")
for i, line := range lines {
fmt.Printf("Line %d: [%s]\n", i+1, line)
}
// Output:
// Wrapped with preserved leading spaces:
// Line 1: [ This is an indented]
// Line 2: [paragraph that needs]
// Line 3: [wrapping]
// Text with trailing spaces
text2 := "Some text "
lines2, _ := twwarp.WrapStringWithSpaces(text2, 30)
fmt.Printf("\nWith trailing spaces:\n")
for i, line := range lines2 {
fmt.Printf("Line %d: [%s] (length: %d)\n", i+1, line, len(line))
}
// Output:
// With trailing spaces:
// Line 1: [Some text ] (length: 12)
// All-whitespace string
spaces := " "
lines3, _ := twwarp.WrapStringWithSpaces(spaces, 10)
fmt.Printf("\nAll spaces:\n")
for i, line := range lines3 {
fmt.Printf("Line %d: [%s] (length: %d)\n", i+1, line, len(line))
}
// Output:
// All spaces:
// Line 1: [ ] (length: 8)
// Both leading and trailing spaces
text3 := " Content with spaces "
lines4, _ := twwarp.WrapStringWithSpaces(text3, 15)
fmt.Printf("\nBoth leading and trailing:\n")
for i, line := range lines4 {
fmt.Printf("Line %d: [%s]\n", i+1, line)
}
// Output:
// Both leading and trailing:
// Line 1: [ Content with]
// Line 2: [spaces ]
}Advanced line-breaking algorithm for custom text layout with fine-grained control.
func WrapWords(words []string, spc, lim, pen int) [][]stringParameters:
words []string - Pre-split list of words to wrapspc int - Display width of space between words (typically 1)lim int - Maximum line width in terminal columnspen int - Penalty for lines exceeding the limit (typically 100000)Returns:
[][]string - Groups of words per lineDescription:
WrapWords implements the low-level Knuth-Plass-inspired line-breaking algorithm used by WrapString and WrapStringWithSpaces. It provides direct control over the text wrapping process for advanced use cases.
Algorithm Details:
The algorithm uses dynamic programming to minimize total "raggedness" across all lines:
Cost Function:
cost = (limit - line_length)² + cost_of_remaining_text
if line_length > limit:
cost += penaltyThis produces visually balanced text by avoiding extreme differences in line length.
Usage Example:
package main
import (
"fmt"
"strings"
"github.com/olekukonko/tablewriter/pkg/twwarp"
)
func main() {
// Pre-split words
words := []string{"The", "quick", "brown", "fox", "jumps", "over", "the", "lazy", "dog"}
// Standard wrapping: 1 space, 20 column limit, 100000 penalty
lines := twwarp.WrapWords(words, 1, 20, 100000)
fmt.Println("Standard wrapping:")
for i, line := range lines {
joined := strings.Join(line, " ")
fmt.Printf("Line %d: [%s] (width: %d)\n", i+1, joined, len(joined))
}
// Output:
// Standard wrapping:
// Line 1: [The quick brown fox] (width: 19)
// Line 2: [jumps over the lazy] (width: 19)
// Line 3: [dog] (width: 3)
// Custom spacing: 2 spaces between words
lines2 := twwarp.WrapWords(words, 2, 20, 100000)
fmt.Println("\nWith 2-space separation:")
for i, line := range lines2 {
joined := strings.Join(line, " ") // Join with 2 spaces
fmt.Printf("Line %d: [%s]\n", i+1, joined)
}
// Output:
// With 2-space separation:
// Line 1: [The quick brown]
// Line 2: [fox jumps over]
// Line 3: [the lazy dog]
// Low penalty allows longer lines
lines3 := twwarp.WrapWords(words, 1, 20, 10)
fmt.Println("\nWith low penalty (allows longer lines):")
for i, line := range lines3 {
joined := strings.Join(line, " ")
fmt.Printf("Line %d: [%s] (width: %d)\n", i+1, joined, len(joined))
}
// Output may show different breaking pattern with longer lines
// Very tight limit forces more breaks
lines4 := twwarp.WrapWords(words, 1, 10, 100000)
fmt.Println("\nWith tight 10-column limit:")
for i, line := range lines4 {
joined := strings.Join(line, " ")
fmt.Printf("Line %d: [%s] (width: %d)\n", i+1, joined, len(joined))
}
// Output:
// With tight 10-column limit:
// Line 1: [The quick] (width: 9)
// Line 2: [brown fox] (width: 9)
// Line 3: [jumps over] (width: 10)
// Line 4: [the lazy] (width: 8)
// Line 5: [dog] (width: 3)
}The text wrapping algorithm is inspired by the Knuth-Plass line breaking algorithm used in TeX, adapted for terminal output:
1. Dynamic Programming Approach
The algorithm uses dynamic programming to find optimal line breaks:
For each word position i:
cost[i] = minimum cost to wrap words from i to end
nbrk[i] = index of first word on next line2. Cost Calculation
For each potential line from word i to word j:
phraseLength = sum(word_widths[i:j]) + (j-i-1) * space_width
difference = limit - phraseLength
cost = difference² + cost[j] // Squared difference for raggedness
if phraseLength > limit:
cost += penalty // Penalize lines that exceed limit3. Optimal Path Selection
The algorithm selects the line breaks that minimize total cost:
i = 0
while i < n:
lines = append(lines, words[i:nbrk[i]])
i = nbrk[i]4. Why Squared Difference?
Using squared difference (rather than absolute difference) heavily penalizes very short or very long lines, producing more uniform line lengths. For example:
This creates visually balanced text with consistent line lengths.
5. Automatic Width Adjustment
Before running the algorithm, the implementation checks if any single word exceeds the limit and increases the limit accordingly. This ensures the algorithm always succeeds.
The SplitWords function uses Unicode Annex #29 word boundary detection, which correctly handles:
This is more sophisticated than simple space-splitting and produces better results for international text.
package main
import (
"fmt"
"github.com/olekukonko/tablewriter/pkg/twwarp"
)
func main() {
// Simulate wrapping text in a 30-column table cell
cellContent := "This is a long description that needs to fit in a table cell"
maxCellWidth := 30
lines, _ := twwarp.WrapString(cellContent, maxCellWidth)
fmt.Println("Table cell content:")
fmt.Println("┌" + strings.Repeat("─", maxCellWidth) + "┐")
for _, line := range lines {
fmt.Printf("│%-30s│\n", line)
}
fmt.Println("└" + strings.Repeat("─", maxCellWidth) + "┘")
}package main
import (
"fmt"
"github.com/olekukonko/tablewriter/pkg/twwarp"
)
func main() {
// Wrap log messages with preserved indentation
logMessage := " [INFO] This is a very long log message that needs to be wrapped while preserving the indentation level"
lines, _ := twwarp.WrapStringWithSpaces(logMessage, 60)
for _, line := range lines {
fmt.Println(line)
}
// Output maintains the 4-space indentation on all lines
}package main
import (
"fmt"
"strings"
"github.com/olekukonko/tablewriter/pkg/twwarp"
)
func main() {
// Create a two-column layout
leftText := "This is the left column content that will be wrapped to fit"
rightText := "This is the right column content that will also be wrapped"
colWidth := 25
leftLines, _ := twwarp.WrapString(leftText, colWidth)
rightLines, _ := twwarp.WrapString(rightText, colWidth)
// Pad to equal heights
maxHeight := len(leftLines)
if len(rightLines) > maxHeight {
maxHeight = len(rightLines)
}
for i := 0; i < maxHeight; i++ {
left := ""
right := ""
if i < len(leftLines) {
left = leftLines[i]
}
if i < len(rightLines) {
right = rightLines[i]
}
fmt.Printf("%-25s | %-25s\n", left, right)
}
}The package uses twwidth.Width() which is optimized with an LRU cache. For repeated strings (common in tables), width calculation is O(1) after the first call.
SplitWords pre-allocates capacity based on estimated word countWrapWords are allocated once per callFor typical table cell content (< 100 words), performance is excellent.
// Empty string
lines, _ := twwarp.WrapString("", 20)
// Returns: []string{""}
// Single space
lines, _ := twwarp.WrapString(" ", 20)
// Returns: []string{" "}
// All spaces (short)
lines, _ := twwarp.WrapStringWithSpaces(" ", 20)
// Returns: []string{" "}
// All spaces (long)
lines, _ := twwarp.WrapStringWithSpaces(" ", 10)
// Returns: []string{" "} (truncated to 10 columns)// East Asian characters (2 columns each)
lines, width := twwarp.WrapString("你好世界", 5)
// width = 8 (auto-adjusted), lines = ["你好世界"] (8 columns)
// Emoji (typically 2 columns)
lines, _ := twwarp.WrapString("Hello 😀 World", 10)
// Returns: ["Hello 😀", "World"]
// Combining characters
lines, _ := twwarp.WrapString("café", 5)
// Returns: ["café"] (correctly measures é as 1 column)// Word longer than limit
text := "supercalifragilisticexpialidocious"
lines, adjustedWidth := twwarp.WrapString(text, 10)
// adjustedWidth = 34 (length of word)
// lines = ["supercalifragilisticexpialidocious"]All functions in the twwarp package are thread-safe:
twwidth.Width() function uses a thread-safe LRU cacheMultiple goroutines can safely call wrapping functions concurrently.
The twwarp package depends on:
github.com/clipperhouse/uax29/v2/graphemes - UAX#29 grapheme cluster segmentationgithub.com/olekukonko/tablewriter/pkg/twwidth - Display width calculationThese dependencies are automatically installed when you import tablewriter.