Describe the bug
Rich wraps text by tokenizing it into "words" with re_word = re.compile(r"\s*\S+\s*")
(rich/_wrap.py). In Python's default (Unicode) regex mode, \s matches U+00A0
NO-BREAK SPACE, so Rich treats NBSP as a word boundary and a line-break opportunity.
This violates the Unicode line-breaking semantics of NBSP, which is explicitly a
non-breaking space: text must not break at an NBSP. Any content that uses NBSP
to keep a run together (e.g. a kaomoji like ( ´・ω・` ), or manually joined
tokens) gets split across lines anyway.
Minimal example
from rich.console import Console
# Kaomoji whose internal spaces are NBSP (U+00A0) so it should NOT be split.
kaomoji = "( ´・ω・` )".replace(" ", "\u00a0")
# A long passage that exceeds the console width and must wrap onto several lines,
# so we can observe where the line breaks happen.
long_text = "this is a long passage that exceeds the console width and must wrap"
console = Console(width=10)
console.print(kaomoji + " " + long_text)
- Expected: the kaomoji stays intact on a single line as one unit; wrapping happens
only at the regular spaces in the following long passage.
- Actual: Rich breaks inside the kaomoji at the NBSP positions, scattering the
kaomoji across multiple lines (the long passage after it is irrelevant to the bug —
it only forces wrapping so the break inside the kaomoji becomes visible).
Root cause
rich/_wrap.py:
re_word = re.compile(r"\s*\S+\s*")
\s (Unicode mode) includes U+00A0; \S excludes it. So a "word" is a run of \S,
which stops at NBSP — making NBSP a break point. A NBSP should be part of the
unbreakable word, not a separator.
Expected behavior / minimal fix
Treat NBSP as a non-breaking space: include it in the word token, never a break
point.
# separator = whitespace except NBSP; word = (non-ws or NBSP)+
# the two branches \S and \xa0 are mutually exclusive -> linear time, no backtracking
re_word = re.compile(r"[^\S\xa0]*(?:\S|\xa0)+[^\S\xa0]*")
Platform
- Rich: 15.0.0
- Python: 3.14.6 (CPython, MSC v.1944 64 bit AMD64)
- OS / terminal: Windows 11 (10.0.26200) / Windows Terminal
python -m rich.diagnose
╭───────────────────────── <class 'rich.console.Console'> ─────────────────────────╮
│ A high level console interface. │
│ │
│ ╭──────────────────────────────────────────────────────────────────────────────╮ │
│ │ <console width=119 ColorSystem.TRUECOLOR> │ │
│ ╰──────────────────────────────────────────────────────────────────────────────╯ │
│ │
│ color_system = 'truecolor' │
│ encoding = 'utf-8' │
│ file = <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'> │
│ height = 40 │
│ 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=119, height=40), │
│ legacy_windows=False, │
│ min_width=1, │
│ max_width=119, │
│ is_terminal=True, │
│ encoding='utf-8', │
│ max_height=40, │
│ justify=None, │
│ overflow=None, │
│ no_wrap=False, │
│ highlight=None, │
│ markup=None, │
│ height=None │
│ ) │
│ quiet = False │
│ record = False │
│ safe_box = True │
│ size = ConsoleDimensions(width=119, height=40) │
│ soft_wrap = False │
│ stderr = False │
│ style = None │
│ tab_size = 8 │
│ width = 119 │
╰──────────────────────────────────────────────────────────────────────────────────╯
╭── <class 'rich._windows.WindowsConsoleFeatures'> ───╮
│ Windows features available. │
│ │
│ ╭─────────────────────────────────────────────────╮ │
│ │ WindowsConsoleFeatures(vt=True, truecolor=True) │ │
│ ╰─────────────────────────────────────────────────╯ │
│ │
│ truecolor = True │
│ vt = True │
╰─────────────────────────────────────────────────────╯
╭────── Environment Variables ───────╮
│ { │
│ 'CLICOLOR': None, │
│ 'COLORTERM': None, │
│ 'COLUMNS': None, │
│ 'JPY_PARENT_PID': None, │
│ 'JUPYTER_COLUMNS': None, │
│ 'JUPYTER_LINES': None, │
│ 'LINES': None, │
│ 'NO_COLOR': None, │
│ 'TERM_PROGRAM': None, │
│ 'TERM': None, │
│ 'TTY_COMPATIBLE': None, │
│ 'TTY_INTERACTIVE': None, │
│ 'VSCODE_VERBOSE_LOGGING': None │
│ } │
╰────────────────────────────────────╯
platform="Windows"
Describe the bug
Rich wraps text by tokenizing it into "words" with
re_word = re.compile(r"\s*\S+\s*")(rich/_wrap.py). In Python's default (Unicode) regex mode,
\smatches U+00A0NO-BREAK SPACE, so Rich treats NBSP as a word boundary and a line-break opportunity.
This violates the Unicode line-breaking semantics of NBSP, which is explicitly a
non-breaking space: text must not break at an NBSP. Any content that uses NBSP
to keep a run together (e.g. a kaomoji like
( ´・ω・` ), or manually joinedtokens) gets split across lines anyway.
Minimal example
only at the regular spaces in the following long passage.
kaomoji across multiple lines (the long passage after it is irrelevant to the bug —
it only forces wrapping so the break inside the kaomoji becomes visible).
Root cause
rich/_wrap.py:\s(Unicode mode) includes U+00A0;\Sexcludes it. So a "word" is a run of\S,which stops at NBSP — making NBSP a break point. A NBSP should be part of the
unbreakable word, not a separator.
Expected behavior / minimal fix
Treat NBSP as a non-breaking space: include it in the word token, never a break
point.
Platform
python -m rich.diagnose