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