Emoji width: the motorway is two cells wide, whoever says otherwise

🛣 (U+1F6E3) heads the Trophy case's Longest distance board, and it pushed that
heading's row a column past the border. Both width authorities call it one cell —
it sits outside Emoji_Presentation and its East-Asian width is Neutral — while the
font draws it as a two-cell colour glyph.

That is a new shape for the wide set, which until now held VS16 bases the narrow
verdict got wrong (🛩, whose selector-stripped base would otherwise collapse to
one). Nothing narrowed the motorway; it was never measured right in the first
place, so it overran its row on any screen that pads to a width. The fix is the
same either way — force the base to two in Rich and in prompt_toolkit — so the set
is now `_DEFAULT_WIDE_BASE`, seeded with both confirmed glyphs and still overridden
outright by MESHTERM_WIDE_EMOJI.

Curated, as always, because which glyph a terminal draws wide is the font's
business and a cursor probe reads the PTY, not the renderer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-07-29 04:13:44 -04:00
1 parent 65a47af037
commit f17f7b95a1
2 files changed
+58 -32

No files matched your search

+42 -26
View File
@@ -52,16 +52,20 @@ as *four* (a two-column notch, worse than a lone emoji's one). Since the indicat
shared across flags, they can only be handled as a whole category, never one country at a
time; narrowing every indicator to one cell makes any flag sum to two in both authorities.
A handful of VS16 sequences break the "width 1" verdict the other way. This terminal is not
uniform: the probe ``☀️`` paints in one cell, but ``🛩️`` (a small airplane, U+1F6E9 U+FE0F)
paints in *two*. Narrowing it along with the rest pulls its chat-row border a column short and
smears the line, so those glyphs are carved back out into a curated *wide* set
(:data:`_DEFAULT_WIDE_VS16`, extended without a code change via ``MESHTERM_WIDE_EMOJI``) that
counts them two in **both** authorities — the mirror image of the lone-narrow allowlist, curated
for the same reason: which VS16 a terminal draws wide is the font's business, not the codepoint's,
and a cursor probe reads the PTY, not the renderer. Keyed on the *base* codepoint (the trailing
``U+FE0F`` is skipped regardless), so a bare base airplane rides along too — rare, and the same
trade the lone allowlist already makes.
Some glyphs run the other way — this terminal draws them **two** cells where the authorities say
one — and they are carved back out into a curated *wide* set (:data:`_DEFAULT_WIDE_BASE`, extended
without a code change via ``MESHTERM_WIDE_EMOJI``) counted two in **both** authorities. A VS16
sequence lands here when the "width 1" verdict is wrong for it: the renderer is not uniform — the
probe ``☀️`` paints in one cell, but ``🛩️`` (a small airplane, U+1F6E9 U+FE0F) paints in two, and
narrowing it along with the rest pulls its chat-row border a column short and smears the line. A
*lone* codepoint lands here for a plainer reason: an emoji outside Emoji_Presentation whose
East-Asian width is Neutral (``🛣``, U+1F6E3) is measured as one cell by Rich and wcwidth alike,
yet the font draws it as a two-cell colour glyph — never narrowed, just never measured right, so
it overruns its row by a column wherever the row pads to a width. Either way it is the mirror image
of the lone-narrow allowlist, curated for the same reason: which glyph a terminal draws wide is the
font's business, not the codepoint's, and a cursor probe reads the PTY, not the renderer. Keyed on
the *base* codepoint (the trailing ``U+FE0F`` is skipped regardless), so a bare base airplane rides
along with its VS16 form.
"""
from __future__ import annotations
@@ -89,16 +93,28 @@ _PROBE = "☀️"
#: the confirmed glyph and extended — no code change — through ``MESHTERM_NARROW_EMOJI``.
_DEFAULT_NARROW_LONE = "👋"
#: Base codepoints of VS16 emoji-presentation sequences this terminal draws *two* cells wide
#: despite the narrow-VS16 verdict that width-1 calibration applies to every such sequence. The
#: renderer is not uniform (see the module docstring): ``☀️`` — the calibration probe — paints in
#: one cell, but ``🛩️`` (U+1F6E9 U+FE0F) paints in two, so blanket-narrowing it pulls its chat-row
#: border a column short and smears the row. There is no rule separating the wide VS16 from the
#: narrow — it is the font's glyph, not the codepoint, and a probe reads the PTY, not the renderer —
#: so, the mirror of :data:`_DEFAULT_NARROW_LONE`, this is a curated set, seeded with the confirmed
#: glyph and extended, no code change, through ``MESHTERM_WIDE_EMOJI``. Keyed on the base codepoint
#: Base codepoints this terminal draws *two* cells wide where the width authorities say one.
#: Two sources feed it, and both are the same font story:
#:
#: * a **VS16 sequence** the narrow-VS16 verdict gets wrong — the renderer is not uniform (see the
#: module docstring): ``☀️``, the calibration probe, paints in one cell, but ``🛩️``
#: (U+1F6E9 U+FE0F) paints in two, so blanket-narrowing it pulls its chat-row border a column
#: short and smears the row;
#: * a **lone codepoint** Rich and wcwidth both already call one — the emoji outside
#: Emoji_Presentation, whose East-Asian width is Neutral (``🛣`` U+1F6E3, the Trophy case's
#: Longest-distance icon) — that the font nonetheless draws as a two-cell colour glyph. Nothing
#: narrowed these; they were never measured right in the first place, so they overrun their row
#: by a column on every screen that pads to a width.
#:
#: There is no rule separating either from its correctly-measured neighbours — it is the font's
#: glyph, not the codepoint, and a probe reads the PTY, not the renderer — so, the mirror of
#: :data:`_DEFAULT_NARROW_LONE`, this is a curated set, seeded with the confirmed glyphs and
#: extended, no code change, through ``MESHTERM_WIDE_EMOJI``. Keyed on the base codepoint,
#: because the trailing selector is skipped by both authorities either way.
_DEFAULT_WIDE_VS16 = "\U0001f6e9" # 🛩 airplane (U+1F6E9), drawn two-wide with its VS16 selector
_DEFAULT_WIDE_BASE = (
"\U0001f6e9" # 🛩 airplane (U+1F6E9), drawn two-wide with its VS16 selector
"\U0001f6e3" # 🛣 motorway (U+1F6E3), drawn two-wide though both authorities measure one
)
#: Set once :func:`calibrate` has run so repeated calls are cheap no-ops.
_CALIBRATED = False
@@ -130,18 +146,18 @@ def _narrow_lone_set() -> frozenset[str]:
return frozenset(os.environ.get("MESHTERM_NARROW_EMOJI", _DEFAULT_NARROW_LONE))
def _wide_vs16_set() -> frozenset[str]:
"""The VS16 base codepoints to keep two cells wide (see :data:`_DEFAULT_WIDE_VS16`).
def _wide_base_set() -> frozenset[str]:
"""The base codepoints to measure two cells wide (see :data:`_DEFAULT_WIDE_BASE`).
``MESHTERM_WIDE_EMOJI`` overrides the default outright: set it to the base glyphs you have
confirmed this terminal draws two wide (e.g. ``MESHTERM_WIDE_EMOJI=🛩``), or to the empty
confirmed this terminal draws two wide (e.g. ``MESHTERM_WIDE_EMOJI=🛩🛣``), or to the empty
string to trust the narrow-VS16 verdict for every sequence. A trailing variation selector is
dropped, so pasting the whole rendered glyph (``🛩️``) still keys on the base and never counts
the zero-width selector as a wide cell itself. Only consulted on the width-1 path (see
:func:`calibrate`); a terminal that already draws emoji two wide never narrowed them, so there
is nothing to carve back out.
:func:`calibrate`); a terminal that already draws emoji two wide measures these right on its
own, and nothing here would have narrowed them.
"""
listed = frozenset(os.environ.get("MESHTERM_WIDE_EMOJI", _DEFAULT_WIDE_VS16))
listed = frozenset(os.environ.get("MESHTERM_WIDE_EMOJI", _DEFAULT_WIDE_BASE))
return listed - frozenset(_ZERO_WIDTH)
@@ -167,7 +183,7 @@ def calibrate(*, force_width: int | None = None) -> None:
# width 2 (or unknown/None) -> Rich's default already matches; leave everything alone
# (narrowing here would instead pull a correctly-wide emoji's border a column short).
if width == 1:
_install_terminal_widths(_narrow_lone_set(), _wide_vs16_set())
_install_terminal_widths(_narrow_lone_set(), _wide_base_set())
def _decide_vs16_width() -> int | None:
+16 -6
View File
@@ -25,6 +25,7 @@ _CA = "🇨🇦" # a flag: two Regional Indicators; prompt_toolkit miscounts it
_CN = "🇨🇳" # a second flag sharing the "C" indicator — the whole category must be handled
_PLANE = "🛩️" # a VS16 sequence this terminal draws two wide despite the narrow-VS16 verdict
_PLANE_BASE = "\U0001f6e9" # its base codepoint (no selector) — what the wide set is keyed on
_ROAD = "\U0001f6e3" # 🛣 a lone codepoint both authorities measure one, drawn two wide here
def test_narrow_lone_set_defaults_extends_and_disables(monkeypatch) -> None:
@@ -40,24 +41,26 @@ def test_narrow_lone_set_defaults_extends_and_disables(monkeypatch) -> None:
assert ew._narrow_lone_set() == frozenset()
def test_wide_vs16_set_defaults_extends_and_disables(monkeypatch) -> None:
"""The wide set seeds to the confirmed airplane base; the env var overrides it outright."""
def test_wide_base_set_defaults_extends_and_disables(monkeypatch) -> None:
"""The wide set seeds to the confirmed bases; the env var overrides it outright."""
monkeypatch.delenv("MESHTERM_WIDE_EMOJI", raising=False)
assert _PLANE_BASE in ew._wide_vs16_set()
# Both sources of a wide base: a VS16 sequence the narrow verdict gets wrong (the airplane)
# and a lone codepoint both authorities under-measure at one cell (the motorway).
assert {_PLANE_BASE, _ROAD} <= ew._wide_base_set()
# ✈ (U+2708) is a second VS16 airplane base; both list cleanly.
monkeypatch.setenv("MESHTERM_WIDE_EMOJI", "\U0001f6e9✈")
assert {"\U0001f6e9", "✈"} <= ew._wide_vs16_set()
assert {"\U0001f6e9", "✈"} <= ew._wide_base_set()
# Pasting the whole rendered glyph keeps the base but strips the zero-width selector, so
# the selector is never itself counted as a wide cell.
monkeypatch.setenv("MESHTERM_WIDE_EMOJI", _PLANE)
wide = ew._wide_vs16_set()
wide = ew._wide_base_set()
assert _PLANE_BASE in wide and "️" not in wide
# Empty string trusts the narrow-VS16 verdict for every sequence.
monkeypatch.setenv("MESHTERM_WIDE_EMOJI", "")
assert ew._wide_vs16_set() == frozenset()
assert ew._wide_base_set() == frozenset()
def test_rich_cell_len_narrows_only_allowlisted_lone_emoji() -> None:
@@ -74,6 +77,9 @@ def test_rich_cell_len_narrows_only_allowlisted_lone_emoji() -> None:
# frames flush instead of collapsing to one and smearing the row.
assert cell_len(_PLANE) == 2
assert cell_len("hi 🛩️") == len("hi ") + 2
# A listed *lone* base is the same fix for a glyph nothing narrowed: Rich reads U+1F6E3 as
# one cell on its own, so a heading carrying it would overrun its row by a column.
assert ew._make_cell_len(frozenset(), frozenset(_ROAD))(_ROAD) == 2
def test_pt_cache_narrows_only_allowlisted_lone_emoji() -> None:
@@ -89,6 +95,8 @@ def test_pt_cache_narrows_only_allowlisted_lone_emoji() -> None:
# zero, so the whole glyph (and any line holding it) keeps the terminal's two cells.
assert cache[_PLANE] == 2
assert cache["🛩️ hi"] == 2 + len(" hi")
# And a listed lone base, which wcwidth also under-measures at one, lands on two as well.
assert ew._make_pt_cache(frozenset(), frozenset(_ROAD))[_ROAD] == 2
def test_flags_measure_two_cells_in_both_authorities() -> None:
@@ -134,10 +142,12 @@ def test_calibrate_width1_narrows_the_wave_in_both_authorities(monkeypatch) -> N
assert cells.cell_len(_DISH) == 2 # unlisted icon stays wide
assert cells.cell_len(_CA) == 2 # flag handled by category, no allowlist entry
assert cells.cell_len(_PLANE) == 2 # wide-VS16 exception carved back out of the narrowing
assert cells.cell_len(_ROAD) == 2 # a lone base the authorities under-measured at one
assert get_cwidth(_WAVE) == 1 # pt now places the border a cell earlier
assert get_cwidth(_DISH) == 2
assert get_cwidth(_CA) == 2 # was 4 unpatched
assert get_cwidth(_PLANE) == 2 # was 1 unpatched: the airplane smeared a cell short
assert get_cwidth(_ROAD) == 2 # likewise for the motorway, in the border authority
finally:
_restore(snap)