File size: 4,708 Bytes
dc893fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | """Terminal display utilities for proper text alignment.
This module provides utilities for calculating visible width of text in terminals,
handling ANSI escape codes, emoji, and East Asian characters correctly.
"""
import re
import unicodedata
# Compile regex once at module level for performance
ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
# Unicode ranges for emoji
EMOJI_START = 0x1F300
EMOJI_END = 0x1FAFF
def calculate_display_width(text: str) -> int:
"""Calculate the visible width of text in terminal columns.
This function correctly handles:
- ANSI escape codes (removed from width calculation)
- Emoji characters (counted as 2 columns)
- East Asian Wide/Fullwidth characters (counted as 2 columns)
- Combining characters (counted as 0 columns)
- Regular ASCII characters (counted as 1 column)
Args:
text: Input text that may contain ANSI codes, emoji, or unicode characters
Returns:
Number of terminal columns the text will occupy when displayed
Examples:
>>> calculate_display_width("Hello")
5
>>> calculate_display_width("你好")
4
>>> calculate_display_width("🤖")
2
>>> calculate_display_width("\033[31mRed\033[0m")
3
"""
# Remove ANSI escape codes (they don't occupy display space)
clean_text = ANSI_ESCAPE_RE.sub("", text)
width = 0
for char in clean_text:
# Skip combining characters (zero width)
if unicodedata.combining(char):
continue
code_point = ord(char)
# Emoji range (most common emoji, counted as 2 columns)
if EMOJI_START <= code_point <= EMOJI_END:
width += 2
continue
# East Asian Width property
# W = Wide, F = Fullwidth (both occupy 2 columns)
eaw = unicodedata.east_asian_width(char)
if eaw in ("W", "F"):
width += 2
else:
width += 1
return width
def truncate_with_ellipsis(text: str, max_width: int, ellipsis: str = "…") -> str:
"""Truncate text to fit within max_width, adding ellipsis if needed.
Args:
text: Text to truncate (ANSI codes are preserved but not counted)
max_width: Maximum visible width in terminal columns
ellipsis: Ellipsis character to use (default: "…")
Returns:
Truncated text with ellipsis if needed
Examples:
>>> truncate_with_ellipsis("Hello World", 8)
'Hello W…'
>>> truncate_with_ellipsis("你好世界", 5)
'你好…'
"""
if max_width <= 0:
return ""
current_width = calculate_display_width(text)
# No truncation needed
if current_width <= max_width:
return text
# Remove ANSI codes for truncation (we'll lose color, but that's expected)
plain_text = ANSI_ESCAPE_RE.sub("", text)
# If max_width is too small for ellipsis
ellipsis_width = calculate_display_width(ellipsis)
if max_width <= ellipsis_width:
return plain_text[:max_width]
# Find truncation point
available_width = max_width - ellipsis_width
truncated = ""
current_width = 0
for char in plain_text:
char_width = calculate_display_width(char)
if current_width + char_width > available_width:
break
truncated += char
current_width += char_width
return truncated + ellipsis
def pad_to_width(text: str, target_width: int, align: str = "left", fill_char: str = " ") -> str:
"""Pad text to reach target width with proper alignment.
Args:
text: Text to pad (may contain ANSI codes)
target_width: Target width in terminal columns
align: Alignment mode - "left", "right", or "center"
fill_char: Character to use for padding (default: space)
Returns:
Padded text
Examples:
>>> pad_to_width("Hello", 10)
'Hello '
>>> pad_to_width("你好", 10)
'你好 '
>>> pad_to_width("Test", 10, align="center")
' Test '
"""
current_width = calculate_display_width(text)
if current_width >= target_width:
return text
padding_needed = target_width - current_width
if align == "left":
return text + (fill_char * padding_needed)
elif align == "right":
return (fill_char * padding_needed) + text
elif align == "center":
left_padding = padding_needed // 2
right_padding = padding_needed - left_padding
return (fill_char * left_padding) + text + (fill_char * right_padding)
else:
raise ValueError(f"Invalid align value: {align}. Must be 'left', 'right', or 'center'")
|