Mesh atlas: the trail is a path line, snapped to where you are

The breadcrumb across the top was hand-rolling its own hop sequence — its own
separator loop, its own width arithmetic, its own head-dropping. A walk *is* a
path, so it now renders through PathLine like every other hop sequence in the
app: powerline chips wherever the terminal draws them, the trail's own › arrows
where it can't, each hop in its node's key-derived hue.

It stays one line — no wrap, no scroll. When the walk outgrows the width the fit
eats into its head rather than a route's usual tail, and what survives snaps
flush to the right edge, so the focus and the steps that just led to it are the
ones always in view.

PathLine.ellipsized grew the parameter that makes that possible: `elide` names
the side the ⋯ eats into. ELIDE_TAIL (the default, unchanged behaviour) anchors
on the head and rescues the destination off the far end, so both endpoints of a
route survive; ELIDE_HEAD anchors on the tail and spares the origin nothing.

Along the way the trail stops losing its colour: the old focus emphasis built
"bold you" / "bold muted" style strings, which Rich can't parse and silently
renders as no style at all — so our own node at the root of the walk was drawing
plain instead of white. Identity styling now comes from the hops themselves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-07-28 19:50:01 -04:00
1 parent e99e6cf238
commit b76f50d6f4
4 files changed
+102 -53

No files matched your search

+40 -38
View File
@@ -34,11 +34,13 @@ only its immediate neighbourhood:
PgUp/PgDn a windowful.
**Enter walks**: the highlighted neighbour becomes the new focus, the breadcrumb trail
across the top grows (``you › YUL-Cartierville › …``, each name in its node's own hue),
and **⌫ steps back** along it. Walking to a node already on the trail truncates the stack
to its first appearance — the loop you walked to get back there is dropped rather than
recorded — and when the trail outgrows the line its *head* is dropped behind a leading
``…`` so the focus stays visible. **Home** refocuses our own node. **Typing finds** — a
across the top grows (a :mod:`~meshterm.ui.pathline` path line — powerline chips where the
terminal draws them, ``you › YUL-Cartierville › …`` where it doesn't, each name in its
node's own hue), and **⌫ steps back** along it. Walking to a node already on the trail
truncates the stack to its first appearance — the loop you walked to get back there is
dropped rather than recorded — and when the trail outgrows the line it neither wraps nor
scrolls: its *head* goes behind a leading ``⋯`` and the rest snaps flush right, so the
focus and the steps just taken stay in view. **Home** refocuses our own node. **Typing finds** — a
global filter over every node in
the graph, islands included; Enter teleports the focus to the highlighted match (the
trail restarts there, since the walk didn't cross the gap). Esc peels find first, the
@@ -64,6 +66,7 @@ from ..services.topology import Link, MeshTopology
from .map_render import _NODE, _REPEATER, _SELF, _UNKNOWN
from .mapcanvas import RGB, MapCanvas, parse_hex
from .menus import fit_cells
from .pathline import ELIDE_HEAD, PathHop, PathLine
from .theme import name_style, node_style, snr_style
from .trace_screen import snr_bar
from .tui.render import render_to_ansi
@@ -466,42 +469,41 @@ class AtlasScreen(Screen):
]
def _trail_text(self, width: int) -> Text:
"""The breadcrumb trail, each name in its own key-derived hue, tail-anchored.
"""The breadcrumb trail as a path line: one line, tail-anchored, never wrapped.
Names carry the app-wide per-node hue (ours the white ``you``, a nameless node
muted), the focus bold. When the whole trail won't fit on the line the *head* is
dropped behind a leading ``…`` — never the tail — so the focus and the steps that
led to it are always the ones kept in view.
The walk *is* a path — us, then every node stepped through, ending on the focus —
so it renders through :class:`~meshterm.ui.pathline.PathLine` like every other hop
sequence in the app: powerline chips wherever the terminal can draw them, the
trail's own ``›`` arrows where it can't. Each hop wears its node's key-derived hue
(ours the white ``you``; a node known only by a bare hash stays muted — colour is
reserved for keyed identities), so the trail and the rows below it agree on who is
who.
The line neither wraps nor scrolls. When the walk outgrows the width the fit eats
into its *head* (:data:`~meshterm.ui.pathline.ELIDE_HEAD`) rather than a route's
usual tail: the oldest steps disappear behind a leading ``⋯`` and what survives
snaps flush against the **right** edge, so the focus and the steps that just led
to it are the ones always in view. A long walk therefore reads as a line that
grows rightward until it meets the margin and then starts shedding its oldest
steps, rather than one that pushes the focus off the end.
"""
nodes = self._trail
sep = " › "
line = PathLine([self._trail_hop(node) for node in self._trail], separator=" › ")
full = line.text()
if full.cell_len <= width:
return full
fitted = line.ellipsized(width, elide=ELIDE_HEAD)
snapped = Text(" " * max(0, width - fitted.cell_len)) # snap the tail to the edge
snapped.append_text(fitted)
return snapped
def width_of(start: int) -> int:
total = cell_len("…") + cell_len(sep) if start else 0 # the leading "… › "
for k in range(start, len(nodes)):
total += cell_len(sep) if k > start else 0
total += cell_len(self._label(nodes[k]))
return total
start = 0
while start < len(nodes) - 1 and width_of(start) > width:
start += 1
trail = Text()
if start:
trail.append("…" + sep, style="muted")
for k in range(start, len(nodes)):
if k > start:
trail.append(sep, style="muted")
last = k == len(nodes) - 1
trail.append(self._label(nodes[k]), style=self._trail_style(nodes[k], last))
trail.truncate(width, overflow="ellipsis") # guard a lone label wider than the line
return trail
def _trail_style(self, node: str, last: bool) -> str:
"""A trail name's style: its key hue (ours white, a nameless node muted), focus bold."""
base = self._list_name_style(node)
return f"bold {base}" if last else base
def _trail_hop(self, node: str) -> PathHop:
"""One walked step as a path hop: its display name in its own identity colour."""
style = self._list_name_style(node)
return PathHop(
self._label(node),
key=None if style in ("you", "muted") else node,
you=style == "you",
)
def _focus_line(self, depths: dict[str, int]) -> Text:
"""Who is in focus: glyph, name with its parenthesized hash, distance, and recency.
+35 -11
View File
@@ -21,8 +21,10 @@ drawn*, so every surface the survey found can eventually route through it:
* **Three overflow answers**, matching the three patterns the surfaces already use:
:meth:`PathLine.text` is the full one-liner (for callers that ellipsize or
h-scroll it themselves), :meth:`PathLine.ellipsized` fits a width by eliding
*middle* hops behind a ``⋯`` mark — both endpoints survive, unlike a tail
truncation that amputates the destination — and :meth:`PathLine.wrapped` breaks
hops behind a ``⋯`` mark, on whichever side it is told to eat into
(:data:`ELIDE_TAIL` by default — both endpoints survive, unlike a plain tail
truncation that amputates the destination; :data:`ELIDE_HEAD` for a breadcrumb
trail, which anchors on where the walk *is*) — and :meth:`PathLine.wrapped` breaks
at hop boundaries under a hanging indent (never mid-name, never mid-chip),
folding where the route *means* something: the fewest lines it can take, evened
out across them rather than greedily crammed, and preferring the seam where the
@@ -104,10 +106,17 @@ _YOU_BG = "#ffffff"
_DIM_BG = "#334155"
_DIM_FG = "#94a3b8"
#: The mark standing in for elided middle hops (see :meth:`PathLine.ellipsized`) —
#: rendered as a dim pseudo-hop so it recedes in both modes.
#: The mark standing in for elided hops (see :meth:`PathLine.ellipsized`) — rendered as
#: a dim pseudo-hop so it recedes in both modes.
_ELISION = "⋯"
#: Which side of a path an :meth:`PathLine.ellipsized` fit eats into. ``ELIDE_TAIL`` is
#: the route reading — the origin anchors the line, the destination is rescued off the
#: far end, and the mark lands in the middle. ``ELIDE_HEAD`` is the trail reading — the
#: last hop anchors the line and everything before it goes, origin included.
ELIDE_TAIL = "tail"
ELIDE_HEAD = "head"
#: Extra columns a wrapped line steps in past the hanging indent. The continuation
#: marks (a trailing ``→``, a notched chip edge) say the path goes on; the step says
#: it at a glance, from the shape of the block alone — a route that folds reads as one
@@ -214,16 +223,28 @@ class PathLine:
return Text(self._empty, style="muted")
return self._render(self._hops)
def ellipsized(self, width: int) -> Text:
"""The line fitted to ``width`` by eliding *middle* hops behind ``⋯``.
def ellipsized(self, width: int, *, elide: str = ELIDE_TAIL) -> Text:
"""The line fitted to ``width`` by eliding hops behind a ``⋯`` mark.
Endpoints matter most — a route reads origin and destination first — so the
fit keeps the first hop and as much of the tail as possible, then drops the
head too if it must, and only as a last resort truncates a single over-long
hop the classic way.
``elide`` names the *side* the mark eats into — which end of the path pays for
the overflow:
* :data:`ELIDE_TAIL` (the default) anchors the line on its head: the origin
holds and hops go from the tail end. The destination is rescued out of that
side along with as much of the run before it as fits, so the ``⋯`` settles in
the *middle* and both endpoints survive — a route reads origin and
destination first, and a plain right-truncation would amputate the second.
The origin is only given up if even that won't fit, and a lone over-long hop
is truncated the classic way as the last resort.
* :data:`ELIDE_HEAD` anchors the line on its tail: the last hop holds and hops
go from the head end, the origin among them, with no endpoint rescued. A
breadcrumb trail's news is where the walk *is*; where it set out from is
exactly the part worth losing.
Args:
width: The cell budget the returned line must fit in.
elide: Which side the ``⋯`` eats into — :data:`ELIDE_TAIL` (the default,
keeping both endpoints) or :data:`ELIDE_HEAD` (keeping the tail alone).
Returns:
A one-line :class:`Text` no wider than ``width``.
@@ -233,7 +254,10 @@ class PathLine:
return full
mark = PathHop(_ELISION, dim=True)
count = len(self._hops)
for head in (1, 0):
# The heads worth trying, most-rescued first: a tail-side elision spares the
# origin while it fits and gives it up only when it must; a head-side one is
# eating the head on purpose, so it never spares it at all.
for head in ((0,) if elide == ELIDE_HEAD else (1, 0)):
for tail in range(count - 1 - head, 0, -1):
kept = self._hops[:head] + [mark] + self._hops[-tail:]
candidate = self._render(kept)
+12 -3
View File
@@ -269,16 +269,25 @@ def test_atlas_walking_to_an_earlier_node_drops_the_loop() -> None:
def test_atlas_trail_drops_the_head_not_the_tail_when_narrow() -> None:
"""A trail too long for the line loses its head behind a leading …, keeping the focus."""
"""A trail too long for the line loses its head behind a leading ⋯, snapped right."""
screen = _screen(_topo())
us = screen._topo.self_id
yul = screen._topo.canonical(YUL.public_key)
alice = screen._topo.canonical(ALICE.public_key)
screen._trail = [us, yul, alice]
text = screen._trail_text(20).plain # too narrow for the whole "Homestead › … › Alice"
assert text.startswith("…")
width = 20 # too narrow for the whole "Homestead › YUL-Cartierville › Alice"
text = screen._trail_text(width).plain
assert text.lstrip().startswith("⋯")
assert text.endswith("Alice") # the focus is always kept
assert "Homestead" not in text # the head was dropped, not the tail
assert len(text) == width # the surviving tail snaps flush to the right edge
def test_atlas_trail_sits_left_until_it_overflows() -> None:
"""A trail that fits is left-aligned; only an elided one snaps to the right edge."""
screen = _screen(_topo())
screen._trail = [screen._topo.self_id]
assert screen._trail_text(40).plain == "Homestead"
def test_atlas_trail_names_carry_their_node_hues() -> None:
+15 -1
View File
@@ -14,7 +14,7 @@ import pytest
import meshterm.ui.pathline as pathline
from meshterm.ui.pathline import (
CURSOR_GLYPH, POWERLINE_ROUND_CLOSE, POWERLINE_ROUND_OPEN, POWERLINE_SEP,
CURSOR_GLYPH, ELIDE_HEAD, POWERLINE_ROUND_CLOSE, POWERLINE_ROUND_OPEN, POWERLINE_SEP,
SELF_GLYPH, WRAP_OFFSET, PathHop, PathLine, _style_hex, path_line,
)
from meshterm.ui.theme import node_style
@@ -153,6 +153,20 @@ def test_ellipsized_elides_the_middle_and_keeps_both_endpoints() -> None:
assert "⋯" in fitted.plain
def test_ellipsized_eats_the_head_when_told_to() -> None:
"""A breadcrumb's news is where the walk *is*, so ``ELIDE_HEAD`` spares the origin
nothing: the fit keeps as much of the tail as the width holds, behind a ``⋯``."""
hops = [PathHop(label) for label in ("AAAA", "BBBB", "CCCC", "DDDD", "EEEE", "FFFF")]
line = PathLine(hops, mode="plain")
fits = line.ellipsized(200, elide=ELIDE_HEAD).plain
assert fits == line.text().plain # fits → untouched, whichever side would give
fitted = line.ellipsized(25, elide=ELIDE_HEAD)
assert fitted.cell_len <= 25
assert fitted.plain.startswith("⋯") # the origin goes too, unlike the tail-side fit
assert fitted.plain.endswith("FFFF")
assert "AAAA" not in fitted.plain
def test_ellipsized_last_resort_truncates_a_single_giant_hop() -> None:
"""When even ``⋯ → last`` overflows, the classic ellipsis truncation steps in."""
line = PathLine([PathHop("AAAAAAAAAA"), PathHop("BBBBBBBBBB")], mode="plain")