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
(text: str)
| 16 | |
| 17 | |
| 18 | def calculate_display_width(text: str) -> int: |
| 19 | """Calculate the visible width of text in terminal columns. |
| 20 | |
| 21 | This function correctly handles: |
| 22 | - ANSI escape codes (removed from width calculation) |
| 23 | - Emoji characters (counted as 2 columns) |
| 24 | - East Asian Wide/Fullwidth characters (counted as 2 columns) |
| 25 | - Combining characters (counted as 0 columns) |
| 26 | - Regular ASCII characters (counted as 1 column) |
| 27 | |
| 28 | Args: |
| 29 | text: Input text that may contain ANSI codes, emoji, or unicode characters |
| 30 | |
| 31 | Returns: |
| 32 | Number of terminal columns the text will occupy when displayed |
| 33 | |
| 34 | Examples: |
| 35 | >>> calculate_display_width("Hello") |
| 36 | 5 |
| 37 | >>> calculate_display_width("你好") |
| 38 | 4 |
| 39 | >>> calculate_display_width("🤖") |
| 40 | 2 |
| 41 | >>> calculate_display_width("\033[31mRed\033[0m") |
| 42 | 3 |
| 43 | """ |
| 44 | # Remove ANSI escape codes (they don't occupy display space) |
| 45 | clean_text = ANSI_ESCAPE_RE.sub("", text) |
| 46 | |
| 47 | width = 0 |
| 48 | for char in clean_text: |
| 49 | # Skip combining characters (zero width) |
| 50 | if unicodedata.combining(char): |
| 51 | continue |
| 52 | |
| 53 | code_point = ord(char) |
| 54 | |
| 55 | # Emoji range (most common emoji, counted as 2 columns) |
| 56 | if EMOJI_START <= code_point <= EMOJI_END: |
| 57 | width += 2 |
| 58 | continue |
| 59 | |
| 60 | # East Asian Width property |
| 61 | # W = Wide, F = Fullwidth (both occupy 2 columns) |
| 62 | eaw = unicodedata.east_asian_width(char) |
| 63 | if eaw in ("W", "F"): |
| 64 | width += 2 |
| 65 | else: |
| 66 | width += 1 |
| 67 | |
| 68 | return width |
| 69 | |
| 70 | |
| 71 | def truncate_with_ellipsis(text: str, max_width: int, ellipsis: str = "…") -> str: |
no outgoing calls