-
-
Notifications
You must be signed in to change notification settings - Fork 2k
Description
- I've checked docs and closed issues for possible solutions.
- I can't find my issue in the FAQ.
I was working on a library and noticed additional lines, in the output, like this:
I've debugged this with Claude, and looks like rich's cell_len function does not account for the variation selector U+FE0F (VS-16), which requests emoji presentation. When a narrow character (East Asian Width = Narrow) is followed by VS-16, it should be rendered as a 2-cell-wide emoji, but Rich reports it as 1 cell.
By the way, this seems to ok in Mac's default terminal, I'm assuming because they don't really care about the variation selector? ๐ค
See (ghostty top and mac os terminal at the bottom)
Reproduction
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "rich",
# "wcwidth",
# ]
# ///
from rich.cells import cell_len
from rich.console import Console
from rich.text import Text
from rich.live_render import LiveRender
import wcwidth
# Characters with variation selector (VS-16 requests emoji presentation)
test_cases = [
("โฌ", "arrow without VS"),
("โฌ๏ธ", "arrow with VS (U+2B07 U+FE0F)"),
("โป", "recycle without VS"),
("โป๏ธ", "recycle with VS (U+267B U+FE0F)"),
("๐", "pushpin (already wide)"),
]
print("=" * 60)
print("Cell width comparison")
print("=" * 60)
print()
print("Character | wcwidth | Rich cell_len | Expected")
print("----------|---------|---------------|----------")
for char, desc in test_cases:
wc = wcwidth.wcswidth(char)
rc = cell_len(char)
expected = 2 if "with VS" in desc or "wide" in desc else 1
match = "โ" if rc == expected else "โ"
print(f"{char} {desc:30} | {wc:7} | {rc:13} | {expected} {match}")
print()
print("=" * 60)
print("Visual bug: LiveRender pads lines to terminal width")
print("=" * 60)
print()
print("Lines with VS-16 emojis wrap incorrectly because Rich thinks")
print("the emoji is 1 cell but the terminal renders it as 2 cells.")
print()
console = Console()
menu = Text(justify="left")
options = [
("๐", "Fix CI Build"),
("โฌ๏ธ", "Downgrade dependencies"), # BUG: will wrap
("โฌ๏ธ", "Upgrade dependencies"), # BUG: will wrap
("๐", "Pin dependencies"),
]
for i, (emoji, desc) in enumerate(options):
is_last = i == len(options) - 1
menu.append(Text.assemble("โ ", emoji, " ", desc, "\n" if not is_last else ""))
live_render = LiveRender(menu)
console.print(live_render)
print()
print("If lines with โฌ๏ธ and โฌ๏ธ wrapped to a new line, that's the bug.")Table output:
| Character | wcwidth | Rich cell_len | Expected |
|---|---|---|---|
| โฌ arrow without VS | 1 | 1 | 1 โ |
| โฌ๏ธ arrow with VS (U+2B07 U+FE0F) | 2 | 1 | 2 โ |
| โป recycle without VS | 1 | 1 | 1 โ |
| โป๏ธ recycle with VS (U+267B U+FE0F) | 2 | 1 | 2 โ |
| ๐ pushpin (already wide) | 2 | 2 | 2 โ |
Expected Behavior
cell_len("โฌ๏ธ") should return 2, matching wcwidth.wcswidth("โฌ๏ธ").
Environment
- Rich version: (tested with latest)
- Python: 3.11+
- Terminal: Ghostty, but affects any terminal that correctly renders VS-16 emojis (not Mac os terminal, at least on mac os 15.6.1)
ature, consider posting a screenshot.
Platform
Click to expand
What platform (Win/Linux/Mac) are you running on? What terminal software are you using?
I may ask you to copy and paste the output of the following commands. It may save some time if you do it now.
If you're using Rich in a terminal:
โญโโโโโโโโโโโโโโโโโโโโโโโโโ <class 'rich.console.Console'> โโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ A high level console interface. โ
โ โ
โ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ โ
โ โ <console width=110 ColorSystem.TRUECOLOR> โ โ
โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ โ
โ โ
โ color_system = 'truecolor' โ
โ encoding = 'utf-8' โ
โ file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> โ
โ height = 29 โ
โ is_alt_screen = False โ
โ is_dumb_terminal = False โ
โ is_interactive = True โ
โ is_jupyter = False โ
โ is_terminal = True โ
โ legacy_windows = False โ
โ no_color = False โ
โ options = ConsoleOptions( โ
โ size=ConsoleDimensions(width=110, height=29), โ
โ legacy_windows=False, โ
โ min_width=1, โ
โ max_width=110, โ
โ is_terminal=True, โ
โ encoding='utf-8', โ
โ max_height=29, โ
โ justify=None, โ
โ overflow=None, โ
โ no_wrap=False, โ
โ highlight=None, โ
โ markup=None, โ
โ height=None โ
โ ) โ
โ quiet = False โ
โ record = False โ
โ safe_box = True โ
โ size = ConsoleDimensions(width=110, height=29) โ
โ soft_wrap = False โ
โ stderr = False โ
โ style = None โ
โ tab_size = 8 โ
โ width = 110 โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโ <class 'rich._windows.WindowsConsoleFeatures'> โโโโโฎ
โ Windows features available. โ
โ โ
โ โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ โ
โ โ WindowsConsoleFeatures(vt=False, truecolor=False) โ โ
โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ โ
โ โ
โ truecolor = False โ
โ vt = False โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโ Environment Variables โโโโโโโโฎ
โ { โ
โ 'CLICOLOR': None, โ
โ 'COLORTERM': 'truecolor', โ
โ 'COLUMNS': None, โ
โ 'JPY_PARENT_PID': None, โ
โ 'JUPYTER_COLUMNS': None, โ
โ 'JUPYTER_LINES': None, โ
โ 'LINES': None, โ
โ 'NO_COLOR': None, โ
โ 'TERM_PROGRAM': 'ghostty', โ
โ 'TERM': 'xterm-ghostty', โ
โ 'TTY_COMPATIBLE': None, โ
โ 'TTY_INTERACTIVE': None, โ
โ 'VSCODE_VERBOSE_LOGGING': None โ
โ } โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
platform="Darwin"