Skip to content

[BUG] Variation Selector U+FE0F not accounted for in cell width calculation #3897

@patrick91

Description

@patrick91

I was working on a library and noticed additional lines, in the output, like this:

Image

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)

Image

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"

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions