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