Files
MeshTerm/tests/test_selection_colour.py
T
Jean-Pierre MartineauandClaude Opus 5 c1db8e5a31 Cursor row: a route's hues are content, so the highlight spares them
A picked row folds its node hues to the cursor white so the row reads as one
thing rather than as a name arguing with its own selection. On a path line that
is wrong: there the hue is not decoration on a name, it is what tells one hop
from the next — and what a route graph above the row is cross-referenced by,
its labels being a marker and one byte of hash. Chips were never folded (their
fills are outside the fold's vocabulary), so the bug was the arrow form: every
path line on the PicoCalc, and on any terminal without the powerline glyphs.

PathLine now stamps every shape it mints with PATH_INK, a style that draws
nothing; Text.append_text carries the stamp into the row as a span over the
hops, and the fold skips identity spans lying inside one. So the Message paths
row keeps each hop in the colour its graph label wears, and so do the TX
screen's "Route — via", the trophy board's walk and the feed's sender → recipient
lane. The node detail's Routes tab, which bought this by hand (no cursor base at
all), is untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 00:42:24 -04:00

367 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""White is what "you picked this" looks like — and nothing else says it.
Three surfaces used to answer the question in the wordmark's teal: the cursor row (fixed
earlier), the chip a dialog's committing button is drawn as, and the heading of the column
a list is sorted by. Teal is a *node* hue as well as the app's identity ink, so every one
of them could collide with the very thing it was highlighting.
What holds now: the cursor row and the active sort column are the ``cursor`` white; a
reverse-video chip is grey, because a chip is chrome and marks where a press lands rather
than claiming a colour; and on the cursor row a node's key-derived hue folds to that same
white, so the highlighted row reads as one thing instead of as a name arguing with its own
selection. Only a *keyed* hue folds — the grey of a node no key could place stays grey.
"""
from __future__ import annotations
import re
import pytest
from rich.text import Text
from meshterm.platforms import PICOCALC, REGULAR, set_platform
from meshterm.ui.contactlist import _SORT_ACTIVE
from meshterm.ui.pathline import PathHop, PathLine
from meshterm.ui.theme import (
MESH_THEME,
MESH_THEME_16,
is_identity_style,
name_style,
node_style,
)
from meshterm.ui.tui.render import render_to_ansi
#: A key whose first byte lands well away from white in every direction, so "the hue is
#: gone" is a real observation rather than a coincidence of the palette.
_KEY = "a1b2c3d4e5f6"
@pytest.fixture(autouse=True)
def _regular_platform():
"""Every test states its own platform; default to the desktop and restore after."""
set_platform(REGULAR)
yield
set_platform(REGULAR)
def _row(*, selected: bool) -> Text:
"""A list row in the shape every screen builds: pointer, name, age, key lane."""
row = Text()
row.append("❯ " if selected else " ", style="cursor" if selected else "")
row.append("YUL-Poly", style=name_style("YUL-Poly", _KEY))
row.append(" ")
row.append("5m", style="heat.minutes")
row.append(" ")
row.append("a1", style=node_style(_KEY)) # the lit half of a key lane
row.append("b2c3d4e5", style="muted")
if selected:
row.style = "cursor"
return row
def _hues(ansi: str) -> set[str]:
"""The truecolor foregrounds the rendered row actually emitted, as ``#rrggbb``."""
return {
"#%02x%02x%02x" % tuple(int(part) for part in match.groups())
for match in re.finditer(r"\x1b\[[\d;]*?38;2;(\d+);(\d+);(\d+)m", ansi)
}
# -- the cursor row's names ---------------------------------------------------------
def test_a_selected_row_draws_its_node_name_in_the_cursor_white() -> None:
"""THE ask: the row you are on says "this one", not "this one is mauve"."""
hue = name_style("YUL-Poly", _KEY).removeprefix("bold ")
hues = _hues(render_to_ansi(_row(selected=True), 60))
assert hue not in hues, f"the identity hue {hue} survived the highlight"
assert "#ffffff" in hues
def test_an_unselected_row_keeps_every_name_in_its_own_hue() -> None:
"""The fold is the *highlight's* doing; the column below it still reads as identities."""
hue = name_style("YUL-Poly", _KEY).removeprefix("bold ")
assert hue in _hues(render_to_ansi(_row(selected=False), 60))
def test_a_key_lane_s_lit_hash_folds_with_the_name_it_belongs_to() -> None:
"""``highlighted_hash`` lights the hash in the same key-derived hue, so it folds too.
Otherwise the highlighted row would go white everywhere except one two-character lane
still shouting its hue — the exact incoherence the fold exists to remove.
"""
row = Text(" ")
row.append("a1", style=node_style(_KEY))
row.append("b2c3d4e5", style="muted")
row.style = "cursor"
hues = _hues(render_to_ansi(row, 30))
assert node_style(_KEY).removeprefix("bold ") not in hues
assert "#94a3b8" in hues, "the unlit tail of the key is muted and stays muted"
def test_the_highlight_does_not_claim_to_know_an_unidentified_node() -> None:
"""A node no key could place keeps its grey: the selection knows *where* you are, not who.
``node.unknown`` is not a hue — it is the absence of one — so folding it to white would
have the highlighted row assert an identity the app does not have.
"""
row = Text(" ")
row.append("3d", style="node.unknown")
row.style = "cursor"
assert "#94a3b8" in _hues(render_to_ansi(row, 30))
def test_the_highlight_leaves_every_other_lane_its_own_colour() -> None:
"""Heat, SNR and badges are not identities — the row is highlighted, not repainted."""
row = Text(" ")
row.append("5m", style="heat.minutes")
row.append(" ")
row.append("+7.5", style="snr.good")
row.append(" ")
row.append("stale", style="err")
row.style = "cursor"
hues = _hues(render_to_ansi(row, 40))
assert {"#facc15", "#4ade80", "#f87171"} <= hues
def test_a_path_line_s_chips_keep_their_fills_on_the_cursor_row() -> None:
"""A hue used as a chip's *fill* is the segment, not a name — it is not folded.
The composed style (``ink on fill``) is deliberately not in the identity vocabulary, so
a route drawn in chips survives being highlighted intact.
"""
fill = node_style(_KEY).removeprefix("bold ")
row = Text(" ")
row.append(" YUL-Poly ", style=f"#0f172a on {fill}")
row.style = "cursor"
assert f"48;2;{int(fill[1:3], 16)};" in render_to_ansi(row, 40)
def test_a_path_line_keeps_every_hop_s_hue_on_the_cursor_row() -> None:
"""A route's hues are content: they are how one hop is told from the next.
The chip form was never folded (its fills are outside the vocabulary), so an
arrow-drawn route — every path line on the PicoCalc, and on any terminal without the
powerline glyphs — has to survive the same way, or the same picked row says two
different things on the two platforms. The widget stamps its own extent and the fold
spares what lies inside it.
"""
hops = [PathHop("YUL-Poly", key=_KEY), PathHop("Waymarker", key="77" * 6)]
row = Text("❯ ", style="cursor")
row.append_text(PathLine(hops, mode="plain").text())
row.style = "cursor"
hues = _hues(render_to_ansi(row, 60))
assert node_style(_KEY).removeprefix("bold ") in hues
assert node_style("77").removeprefix("bold ") in hues
def test_a_name_beside_a_path_line_still_folds() -> None:
"""The exception is the route, not the row: a name lane next to one is still a name."""
row = Text(" ")
row.append("YUL-Poly", style=name_style("YUL-Poly", _KEY))
row.append(" ")
row.append_text(PathLine([PathHop("Waymarker", key="77" * 6)], mode="plain").text())
row.style = "cursor"
hues = _hues(render_to_ansi(row, 60))
assert name_style("YUL-Poly", _KEY).removeprefix("bold ") not in hues # the lane folded
assert node_style("77").removeprefix("bold ") in hues # …the route did not
@pytest.mark.parametrize("platform", [REGULAR, PICOCALC])
def test_the_message_paths_dialog_colours_its_picked_route_like_its_graph(platform) -> None: # noqa: ANN001
"""The end JP asked for: the picked row and the graph above it agree on each node.
The graph labels a relay with a marker and one byte of hash — the colour is what says
*which node* — so a row whitened hop by hop left the reader nothing to match them by.
"""
set_platform(platform)
from datetime import timedelta
from meshterm.core.models import ChatMessage, utcnow
from meshterm.services.message_paths import Arrival
from meshterm.ui.message_paths_screen import MessagePathsScreen
now = utcnow()
arrivals = [
Arrival(when=now, hops=("3d63", "a1b2"), snr=4.0),
Arrival(when=now + timedelta(seconds=2), hops=("a1b2",), snr=-2.0),
]
screen = MessagePathsScreen(
ChatMessage(text="on my way", is_channel=True, created_at=now),
arrivals,
matched=True,
resolve=lambda hop: {"3d63": "YUL-Cartierville", "a1b2": "Waymarker"}.get(hop, hop),
prefix_bytes=1, self_name="Homestead", summary="heard twice", source="Alice",
)
picked = next(line for line in screen.render_body(72) if "❯" in line)
for hop in ("3d63", "a1b2"):
assert _ink(node_style(hop)) in picked, f"the picked route lost {hop}'s hue"
def _ink(style: str) -> str:
"""The escape a style emits before its text, as it appears on the bound platform."""
return render_to_ansi(Text("x", style=style), 3).split("x")[0]
def test_the_fold_never_touches_the_row_it_was_handed() -> None:
"""Rendered Texts are cache keys, so the rewrite has to happen on a copy.
A mutated input would poison the ANSI cache: the next lookup would hash the *folded*
row and miss, and any caller re-rendering the same object unselected would get white.
"""
row = _row(selected=True)
before = [(span.start, span.end, str(span.style)) for span in row.spans]
render_to_ansi(row, 60)
assert [(s.start, s.end, str(s.style)) for s in row.spans] == before
def test_the_ansi_cache_tells_a_highlighted_row_from_the_same_row_unhighlighted() -> None:
"""Same words, same spans, different base style — and the cache keys on the base."""
plain_ansi = render_to_ansi(_row(selected=False), 60)
lit_ansi = render_to_ansi(_row(selected=True), 60)
assert plain_ansi != lit_ansi
assert render_to_ansi(_row(selected=False), 60) == plain_ansi # not clobbered
@pytest.mark.parametrize("platform", [REGULAR, PICOCALC])
def test_the_fold_speaks_whichever_hue_vocabulary_is_bound(platform) -> None: # noqa: ANN001
"""PicoCalc quantizes the hue to a palette slot; the rule above it is the same rule."""
set_platform(platform)
row = Text(" ")
row.append("YUL-Poly", style=name_style("YUL-Poly", _KEY))
row.style = "cursor"
lit = render_to_ansi(row, 30)
unlit = render_to_ansi(Text(" YUL-Poly"), 30)
assert lit != unlit
assert "YUL-Poly" in re.sub(r"\x1b\[[\d;]*m", "", lit)
if platform is PICOCALC:
assert "\x1b[1;97mYUL-Poly" in lit, lit # slot 15, the console's white
else:
assert "38;2;255;255;255mYUL-Poly" in lit, lit
# -- the vocabulary itself ------------------------------------------------------------
@pytest.mark.parametrize("platform", [REGULAR, PICOCALC])
def test_every_hue_a_key_can_mint_is_recognised_as_one(platform) -> None: # noqa: ANN001
""":func:`node_style` read backwards has to answer for all 256 first bytes.
A hue the vocabulary missed would be a name that quietly refused to fold on exactly
the nodes whose keys start with that byte.
"""
set_platform(platform)
assert all(is_identity_style(node_style(f"{byte:02x}")) for byte in range(256))
@pytest.mark.parametrize(
"style", ["you", "cursor", "muted", "node.unknown", "brand", "heat.now", "err",
"bold #ffffff", "#94a3b8", "", "none"],
)
def test_nothing_but_a_key_derived_hue_counts_as_an_identity(style: str) -> None:
"""Everything else on a row means something the highlight has no claim on."""
assert not is_identity_style(style)
def test_the_two_platforms_hue_vocabularies_cannot_be_confused() -> None:
"""Both are in the set at once, so they must not overlap or a switch would misfold.
The spectrum tops out at ``0xf2`` (value 0.95) and every console slot spells ``0xff``,
which is what keeps the answer independent of the platform bound at the time.
"""
set_platform(REGULAR)
spectrum = {node_style(f"{byte:02x}") for byte in range(256)}
set_platform(PICOCALC)
slots = {node_style(f"{byte:02x}") for byte in range(256)}
assert not (spectrum & slots)
# -- the reverse-video chip -----------------------------------------------------------
def test_the_selection_chip_is_grey_on_both_platforms() -> None:
"""A dialog button, an Abort, the arrow-mode composer slot: chrome, not a hue."""
for theme in (MESH_THEME, MESH_THEME_16):
chip = theme.styles["selected"]
assert chip.reverse, "the chip is reverse video — the page's own ink shows through"
assert chip.color is not None
red, green, blue = chip.color.get_truecolor()
saturation = (max(red, green, blue) - min(red, green, blue)) / max(red, green, blue)
assert saturation <= 0.25, (
f"the chip's fill {chip.color!r} carries a hue; it should be grey "
"(the palette's greys are slate, so a little blue cast is the family)"
)
def test_the_chip_is_light_enough_for_the_page_s_ink_to_read_on_it() -> None:
"""Reverse video puts the terminal's background *in* the chip, so the fill carries it."""
red, green, blue = MESH_THEME.styles["selected"].color.get_truecolor()
assert min(red, green, blue) >= 0x80
def test_the_console_chip_uses_a_background_the_vt_can_actually_address() -> None:
"""The VT has no bright backgrounds, and slot 8's dark grey reverses to black."""
chip = MESH_THEME_16.styles["selected"]
assert chip.color.number == 7, "slot 7 is the only grey a reverse can put behind text"
assert chip.bold is False, "a dim-slot style states its intent about bold"
def test_no_theme_style_still_paints_a_selection_in_the_wordmark_s_teal() -> None:
"""The regression this whole file is named for, pinned as a property of the theme."""
for theme in (MESH_THEME, MESH_THEME_16):
for name in ("selected", "cursor"):
assert theme.styles[name].color.get_truecolor() != (
MESH_THEME.styles["brand"].color.get_truecolor()
), name
# -- the active sort column -----------------------------------------------------------
def test_the_active_sort_column_is_lit_in_the_cursor_white() -> None:
"""The same claim as the cursor row, one axis over — so the same ink.
It used to be a bare ``#22d3ee``, which put a *selection* cue back on the node
spectrum and, on the console, straight onto the wordmark's own slot.
"""
from meshterm.ui.contactlist import ContactsSort
from meshterm.ui.widgets import _sort_header
lit = _sort_header("NAME", "name", ContactsSort("name", True))
idle = _sort_header("HEARD", "heard", ContactsSort("name", True))
assert lit == "[cursor]NAME ▲[/]"
assert idle == "HEARD"
assert _SORT_ACTIVE == "cursor", "the live list and the static table light it the same"
def test_the_sort_cue_survives_the_console_s_sixteen_slots() -> None:
"""A bare hex would have been quantized at the fold; a theme name is chosen deliberately."""
set_platform(PICOCALC)
header = Text("NAME ▲", style=_SORT_ACTIVE)
assert "\x1b[1;97m" in render_to_ansi(header, 20) # slot 15, not the brand's 14