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'")