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:
1 parent
e99e6cf238
commit
b76f50d6f4
4 files changed
+102
-53
No files matched your search
+40
-38
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user