A relay chain opens and closes on the chevron, since its ends are not the route's

A packet's via field names the repeaters that forwarded it and neither the
node that sent it nor the one that heard it, yet its ribbon opened and closed
square, the mark that says the route began and ended there. PathLine takes
from_origin / to_destination: a cleared end opens on the notch or closes on
the point, and arrow mode spells it with a bare leading or trailing arrow.
The packet viewer's via chain and the TX sweep's composed relays draw both
ends open. Under the chain, the "reception describes..." note gives way to
the hop count atom the node page and trace scenarios already carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-09-11 04:29:19 -04:00
1 parent e5231687de
commit 10f48dbffc
6 files changed
+231 -60

No files matched your search

+12 -6
View File
@@ -264,12 +264,18 @@ as a grouped list does. Filling a page in is editing its `.md`; no Python follow
same fill are the exception the interlock can't draw: the point drops its background so
the page cuts the wedge. An elision breaks the ribbon rather than joining it — bare `⋯`
on the page between a closing point and the next chip's notch, no fill, no padding.
- The ribbon's **outer ends**: the chevron means *the route continues*, so a path that has
finished never wears one. It ends **square** — nothing appended, the last chip's own pad
is the edge — and opens square the same way; a full Nerd Font rounds both ends into a
lozenge instead (`POWERLINE_ROUND_*`, drawn only where `termfont.powerline_full`). A
*wrapped* line the path outruns still closes on the point: there the route really does
go on, which is the cue arrow mode spells with a trailing `→`.
- The ribbon's **outer ends**: the chevron means *the route continues*, so an end that is
the route's own never wears one. It ends **square** — nothing appended, the last chip's
own pad is the edge — and opens square the same way; a full Nerd Font rounds such ends
into a lozenge instead (`POWERLINE_ROUND_*`, drawn only where `termfont.powerline_full`).
Two things put the chevron back, and either is enough. A *wrapped* line the path outruns
closes on the point: there the route really does go on. And a line drawing only the
**middle** of a route — a packet's `via` chain, the TX sweep's composed relays: hops that
are neither the node the frame came from nor the one it reached — opens on the notch and
closes on the point, because a square end is the promise that the chip beside it is
where the route began or ended (`PathLine(from_origin=…, to_destination=…)`, defaulting
to a whole route). Arrow mode spells the same claim with the separator alone, a leading
or trailing `→` with no hop on the far side of it.
- Concept icons: 📡 advert · 🕒 clock/sync · 🔄 reboot · 💾 backup · 📂 restore ·
🔑 channel/credential key · 🔐 identity/auth secret · 🗑 clear/delete · ✎ compose/edit ·
⚙ parameter · `#` count · ▶ run · ⚡ explore/probe · ★ best/winner · ⭐ watch ·
+18 -21
View File
@@ -54,7 +54,7 @@ from ..services.trace_runner import NodeResolver
from .map_render import _SELF, _UNKNOWN
from .menus import SEP_COMPACT, SEP_ROOMY
from .pathgraph import PathLayer, render_path_graph, revisited_hops
from .pathline import PathLine, path_line
from .pathline import PathLine, hops_atom, path_line
from .theme import glyph, name_style, snr_style
from .trace_screen import snr_bar
from .tui.render import render_lines, render_to_ansi
@@ -869,6 +869,12 @@ class PacketViewer(Screen):
with an empty path and a full set of readings, and "direct — no relays" would be
a flat contradiction of the ``links`` row two lines above. It says how far the
packet got instead, which is the one thing those readings do establish.
The chain is the route's *middle* — the ``path`` field names the relays that
forwarded the frame and neither the node that sent it nor the one that received
it — so both ends are drawn open (``from_origin``/``to_destination``): the
chevron says the route carries on past the chip, where a flat end would claim
the first relay was the origin.
"""
return path_line(
(entry.path or "").split(","),
@@ -876,6 +882,8 @@ class PacketViewer(Screen):
prefix_bytes=self._prefix_bytes,
self_name=self._self_name,
empty=self._empty_via(entry),
from_origin=False,
to_destination=False,
)
@staticmethod
@@ -904,26 +912,15 @@ class PacketViewer(Screen):
if route:
rows.append(("route", Text(route.replace("_", " ").lower())))
rows.append(("via", self._via_path(entry)))
# Which node the SNR beside it actually measured. On a relayed frame that is the
# last repeater, and saying so stops the figure being read as the origin's signal;
# on one that crossed nothing it *is* the origin's, which is worth saying outright
# — a frame heard straight off its sender is the best evidence a link ever gives.
# A trace that walked counts as relayed even with no hops to name: its readings
# are the proof something forwarded it (see :meth:`_via_path`).
relayed = bool(
[hop for hop in (entry.path or "").split(",") if hop] or raw.get("trace_snrs")
)
rows.append(
(
"",
Text(
"reception describes the last relay, not the origin"
if relayed
else "reception describes the sender itself — nothing relayed it",
style="faint",
),
)
)
# The figure the chain above encodes but never states, hung under it as the app's
# own hop atom — the same one the node page's routes and the trace scenarios carry
# (:func:`~meshterm.ui.pathline.hops_atom`), so a count reads the same wherever a
# path has one. Only where there is a chain to count: an empty ``path`` has already
# been spelt out in words on the row above (:meth:`_empty_via`), and a second line
# reading ``direct`` under ``direct — no relays`` would be the same answer twice.
relays = [hop for hop in (entry.path or "").split(",") if hop]
if relays:
rows.append(("", hops_atom(len(relays))))
# A chain that names one hop twice reads as a mistake until it is explained; the graph
# below draws that hop twice too (see ``_graph_lines``), so the note covers both.
revisits = revisit_note(
+85 -21
View File
@@ -22,7 +22,13 @@ drawn*, so every surface the survey found can eventually route through it:
that name. A chip caught by a *cut* — a lane that ran out, a line scrolled past its
edge — breaks off on a half block in its own fill
(:func:`cut_mark`), and that crack is what says the segment continues; only chips
crack, an arrow line still ellipsizes. A row that also carries the app-wide
crack, an arrow line still ellipsizes. A line's two *outer* ends read the same way:
a path that begins at its origin and ends at its destination opens and closes flat (a
rounded cap where the font has one), while one drawing only the *middle* of a route —
a packet's ``via`` chain, which names relays and neither of the nodes the packet went
between — wears the chevron at that end instead, the same mark a wrapped line uses for
"there is more of this out there" (``from_origin`` / ``to_destination``).
A row that also carries the app-wide
opens-further-prompts ``…`` hangs it *outside* the budget
(:func:`with_action_mark`) — chrome may not cost the route cells. ``auto`` (the default)
picks powerline exactly when the terminal can draw it
@@ -405,6 +411,12 @@ class PathLine:
surface with its own convention (the mesh walk trail's ``" › "``) may pass
it. Ignored by powerline mode.
empty: The muted text a hopless path reads as (``"direct"``).
from_origin: The first hop *is* the route's origin. Clear it where the line
draws only the middle of a route — a packet's ``via`` chain names its
relays and neither the node that sent it nor the one that received it — and
the head is drawn as a continuation instead of as a beginning.
to_destination: The last hop *is* the route's destination. Clear it under the
same reading at the other end.
"""
def __init__(
@@ -414,6 +426,8 @@ class PathLine:
mode: str = "auto",
separator: str = _ARROW,
empty: str = "direct",
from_origin: bool = True,
to_destination: bool = True,
) -> None:
"""Hold the hops and the drawing choices; nothing is measured or drawn yet.
@@ -423,6 +437,8 @@ class PathLine:
self._mode = mode
self._separator = separator
self._empty = empty
self._from_origin = from_origin
self._to_destination = to_destination
@property
def hops(self) -> list[PathHop]:
@@ -616,14 +632,22 @@ class PathLine:
"""
if plain:
plain_cells = [self._plain_hop(hop).cell_len for hop in hops]
return plain_cells, cell_len(self._separator), 0, 0, 0, 0
head = 0 if self._from_origin else cell_len(self._separator.lstrip())
foot = 0 if self._to_destination else cell_len(self._separator.rstrip())
return plain_cells, cell_len(self._separator), 0, 0, head, foot
widths = [
cell_len(hop.label) if hop.gap else self._chip(hop, self._chip_fill(hop)).cell_len
for hop in hops
]
sep = cell_len(POWERLINE_SEP)
cap = cell_len(POWERLINE_ROUND_OPEN) if powerline_full() else 0
return widths, sep, sep, sep, cap, cap
# An end the route runs on past costs its chevron whatever the font can draw:
# the point and the notch are core glyphs, and it is the *cap* that is optional.
# So a relays-only line is measured a cell wider at that end than a whole route
# is on a terminal with no rounded caps to spend.
head = cap if self._from_origin else sep
foot = cap if self._to_destination else sep
return widths, sep, sep, sep, head, foot
@staticmethod
def _fill(
@@ -797,17 +821,33 @@ class PathLine:
if not force_plain and self._resolved_mode() == "powerline":
line = self._render_chips(hops, carry_in=carry_in, carry_on=carry_on)
else:
line = self._render_plain(hops)
line = self._render_plain(hops, carry_in=carry_in, carry_on=carry_on)
line.style = PATH_INK
return line
def _render_plain(self, hops: list[PathHop]) -> Text:
"""Arrow-joined hops — ``path_text``'s presentation, hop by hop."""
def _render_plain(
self, hops: list[PathHop], *, carry_in: bool = False, carry_on: bool = False
) -> Text:
"""Arrow-joined hops — ``path_text``'s presentation, hop by hop.
An end the route runs on past wears the separator with nothing on the far side
of it: a leading ``→`` where the origin isn't drawn, a trailing one where the
destination isn't. Arrow mode already spells "the path goes on" with that
trailing mark when a line wraps, so a chain of relays says it the same way —
which is what keeps the two modes telling the reader the same thing on a console
with no chevrons to shear (see :meth:`_render_chips`). The wrap flags win where
they overlap: a continuation already opens under its predecessor, and
:meth:`wrapped` appends the cue itself on a line the path outruns.
"""
text = Text()
if hops and not carry_in and not self._from_origin and not hops[0].gap:
text.append(self._separator.lstrip(), style="muted")
for i, hop in enumerate(hops):
if i:
text.append(self._separator, style="faint" if hop.dim else "muted")
text.append_text(self._plain_hop(hop))
if hops and not carry_on and not self._to_destination and not hops[-1].gap:
text.append(self._separator.rstrip(), style="muted")
return text
def _plain_hop(self, hop: PathHop) -> Text:
@@ -845,19 +885,27 @@ class PathLine:
) -> Text:
"""Powerline chips: each hop filled with its hue, a two-cell gap at every seam.
The two outer ends say whether this line *is* the path or only part of it. A
line that opens the path opens rounded; one that continues a wrapped path opens
on the break's other half — which is exactly the seam's own second cell, so a
fold looks like the seam it interrupted. A line that ends the path closes
rounded; one the path outruns closes on the point, the same "goes on" cue arrow
mode spells with a trailing ``→``.
The two outer ends say whether the reader is looking at the whole route, and
two separate things can shorten it. **This line** may be one of several a wrapped
path folds into (``carry_in``/``carry_on``), and **the path itself** may only ever
have been the middle of a route — a packet's ``via`` chain names its relays and
neither of the nodes it actually went between (``from_origin``/``to_destination``
on the line). Either one draws the same mark at that end, because the reader is
being told the same thing: *the route goes on out there, past what is drawn*. A
head like that opens on the break's other half — the seam's own second cell, so a
fold looks like the seam it interrupted, and a relays-only chain looks like a
route caught mid-stride. A tail like that closes on the point, the same "goes on"
cue arrow mode spells with a trailing ``→``.
Where the font has no rounded caps both outer ends are drawn **square** — the
chip's own pad is the edge, and nothing is appended. Square is not a degraded
cap, it is the one shape left that says *stop*: the point is spoken for as the
continuation cue, so closing a finished path on one claimed a hop had been cut
off (JP, 2026-09-09). Only the *opening* used to square itself off, which left
every route on a core-only terminal ending on the mark for "there is more".
An end that really is the route's own opens or closes **rounded**, and, where the
font has no rounded caps, **square** — the chip's own pad is the edge, and nothing
is appended. Square is not a degraded cap, it is the one shape left that says
*stop*: the point is spoken for as the continuation cue, so closing a finished
path on one claimed a hop had been cut off (JP, 2026-09-09). Only the *opening*
used to square itself off, which left every route on a core-only terminal ending
on the mark for "there is more". The pair now reads both ways round — a square end
is a promise that the node beside it is where the route began or ended, which is
why a chain that starts and finishes on relays must not wear one (JP, 2026-09-09).
An elision (:attr:`PathHop.gap`) interrupts the ribbon rather than joining it: the
chip before it closes on the page, the mark sits on the page bare, and the chip
@@ -871,8 +919,12 @@ class PathLine:
"""
fills = [self._chip_fill(hop) for hop in hops]
rounded = powerline_full()
# A fold and a half-drawn route are two reasons for the same mark; either is
# enough. An elision already breaks the ribbon on that side, and a mark cut out
# of a bare ``⋯`` would have no fill to cut into.
opens_mid = (carry_in or not self._from_origin) and not hops[0].gap
text = Text()
if carry_in:
if opens_mid:
text.append_text(self._notch(fills[0])) # the break's other half
elif rounded and not hops[0].gap:
text.append(POWERLINE_ROUND_OPEN, style=fills[0])
@@ -890,7 +942,7 @@ class PathLine:
text.append_text(self._chip(hop, fills[i]))
if hops[-1].gap:
return text # the line ended on the page; there is no chip left to close
if carry_on:
if carry_on or not self._to_destination:
text.append(POWERLINE_SEP, style=fills[-1]) # the point: the path goes on
elif rounded:
text.append(POWERLINE_ROUND_CLOSE, style=fills[-1]) # the lozenge's far end
@@ -1019,6 +1071,8 @@ def path_line(
dim_self: bool = True,
cursor: int | None = None,
mode: str = "auto",
from_origin: bool = True,
to_destination: bool = True,
) -> PathLine:
"""Build a :class:`PathLine` from raw hop hashes — ``path_text``'s vocabulary.
@@ -1067,6 +1121,10 @@ def path_line(
as ``dim_from`` is, and applied after them, so a slot never shifts what a
hop shows or fades. ``None`` draws no cursor.
mode: The :class:`PathLine` mode (``"auto"``/``"powerline"``/``"plain"``).
from_origin: The first hop is the route's origin (see :class:`PathLine`). Clear
it where ``hops`` is a chain of *relays* — the raw ``path`` field a packet
carries names neither the node it came from nor the one it reached.
to_destination: The last hop is the route's destination, same reading.
Returns:
The assembled :class:`PathLine`.
@@ -1116,4 +1174,10 @@ def path_line(
built.append(PathHop(compact))
if cursor is not None:
built.insert(max(0, min(cursor, len(built))), PathHop(CURSOR_GLYPH, cursor=True))
return PathLine(built, mode=mode, empty=empty)
return PathLine(
built,
mode=mode,
empty=empty,
from_origin=from_origin,
to_destination=to_destination,
)
+12 -1
View File
@@ -391,8 +391,19 @@ class TxSweepScreen(Screen):
if key == "route":
_icon(text, "✎", "brand")
if self.route_hops:
# The composed relays alone — the sweep always sets out from us and always
# lands on the repeater being tuned, and neither is a hop anyone picked
# here. So both ends are drawn open: the chevrons say the route runs on
# past them, which is exactly what ``via`` claims in the word before it.
text.append("Route — via ")
text.append_text(path_line(list(self.route_hops), self._resolve).text())
text.append_text(
path_line(
list(self.route_hops),
self._resolve,
from_origin=False,
to_destination=False,
).text()
)
else:
text.append(f"Route — direct to {self._admin_label}")
elif key == "range":
+25 -11
View File
@@ -309,7 +309,7 @@ def test_packet_viewer_via_wraps_at_hop_boundaries_under_its_own_lane() -> None:
entry = PacketEntry(when=utcnow(), kind="packet", path=",".join(names))
lines = _stripped(PacketViewer([entry], 0, resolve=lambda h: names.get(h, "")).render_body(72))
via_at = next(i for i, line in enumerate(lines) if line.startswith("via"))
end = next(i for i, line in enumerate(lines) if "reception describes" in line)
end = next(i for i, line in enumerate(lines) if line.strip() == "8 hops")
folded = lines[via_at:end]
assert len(folded) > 1 # a chain this long does not fit one lane
assert all(line.startswith(" " * 11) for line in folded[1:]) # hangs past the label lane
@@ -319,6 +319,20 @@ def test_packet_viewer_via_wraps_at_hop_boundaries_under_its_own_lane() -> None:
assert all(len(line.rstrip()) <= 72 for line in folded)
def test_packet_viewer_via_chain_is_drawn_as_the_middle_of_a_route() -> None:
"""``via`` names relays, so neither end of it is where the frame set out or arrived.
The chain opens and closes on the "there is more out there" mark — the chevron in
chips, the bare separator in arrows — rather than on the flat end that would claim
the first relay was the origin.
"""
entry = PacketEntry(when=utcnow(), kind="packet", node="c0ffee", path="a1b2c3,d4e5f6")
via = next(ln for ln in _stripped(_viewer(entry).render_body(80)) if ln.startswith("via"))
chain = via.split(None, 1)[1].strip()
assert chain.startswith("→ ") and chain.endswith(" →")
assert chain.count("→") == 3 # both open ends, and the one join between the two relays
def test_packet_viewer_from_row_leads_with_the_node_type_mark() -> None:
"""The sender wears its shared node glyph — typed by the packet, by the contacts, or ``○``."""
from meshterm.core.models import NODE_TYPE_REPEATER
@@ -491,12 +505,13 @@ def test_packet_viewer_omits_links_for_a_trace_nobody_relayed() -> None:
assert "links" not in _plain(_viewer(entry).render_body(80))
def test_packet_viewer_says_whose_signal_the_reading_is() -> None:
"""The SNR beside a frame measures its last transmitter — which is not always a relay.
def test_packet_viewer_counts_the_relays_under_the_chain() -> None:
"""The hop count hangs under the ``via`` chain — the figure the chips never state.
A relayed frame's reading belongs to the repeater that passed it on, and reading it as
the origin's would be wrong. One that crossed nothing was heard straight off its
sender, so the reading is exactly that link — the strongest evidence there is.
It is the app's own hop atom, so a count here reads as it does on the node page's
routes and under a trace scenario. An empty chain has no count: the ``via`` row has
already said "direct — no relays" in words, and ``direct`` under it would be the same
answer twice.
"""
def note(path: str) -> str:
@@ -505,9 +520,10 @@ def test_packet_viewer_says_whose_signal_the_reading_is() -> None:
)
return _plain(_viewer(entry).render_body(80))
assert "last relay, not the origin" in note("a1b2c3,d4e5f6")
assert "the sender itself" in note("")
assert "last relay" not in note("")
assert "2 hops" in note("a1b2c3,d4e5f6")
assert "1 hop" in note("a1b2c3") and "1 hops" not in note("a1b2c3")
assert "direct" not in note("a1b2c3")
assert note("").count("direct") == 1 # the via row's own words, and nothing under them
def test_packet_viewer_does_not_call_a_walked_trace_direct() -> None:
@@ -526,7 +542,6 @@ def test_packet_viewer_does_not_call_a_walked_trace_direct() -> None:
body = _plain(_viewer(entry).render_body(80))
assert "3 hops walked" in body
assert "direct — no relays" not in body
assert "last relay, not the origin" in body # something *did* relay it
def test_packet_viewer_still_calls_an_unwalked_frame_direct() -> None:
@@ -538,7 +553,6 @@ def test_packet_viewer_still_calls_an_unwalked_frame_direct() -> None:
).render_body(80)
)
assert "direct — no relays" in body, raw
assert "the sender itself" in body, raw
def test_packet_viewer_counts_one_walked_hop_in_the_singular() -> None:
+79
View File
@@ -559,6 +559,85 @@ def test_rounded_caps_finish_a_path_only_where_the_font_has_them(monkeypatch) ->
assert all(text.cell_len <= 20 for text in wrapped)
def test_a_line_drawing_only_a_route_middle_wears_the_chevron_at_that_end(monkeypatch) -> None: # noqa: ANN001
"""A chain of relays has no endpoints of its own, so neither end may claim to be one.
A packet's ``via`` field names the nodes that forwarded it and neither the node it
came from nor the one that received it. A flat end there would say the first relay
*is* where the route began — so the line opens on the notch and closes on the point
instead, the same "there is more of this out there" mark a wrapped line uses. The cap
is what the flags trade away: a full Nerd Font rounds only the ends that are real.
"""
hops = [PathHop(label, key=key) for label, key in (("AAAA", "aa"), ("BBBB", "77"))]
middle = PathLine(hops, mode="powerline", from_origin=False, to_destination=False)
monkeypatch.setattr(pathline, "powerline_full", lambda: True)
text = middle.text()
assert text.plain.startswith(POWERLINE_SEP + " AAAA") # the notch, not the lozenge
assert text.plain.endswith("BBBB " + POWERLINE_SEP) # the point: the route goes on
opening = next(s for s in text.spans if s.start == 0 and str(s.style) != PATH_INK)
assert str(opening.style) == f"{_style_hex(node_style('aa'))} reverse" # cut out of AAAA
# One end at a time: the half that *is* an endpoint keeps its cap either way.
head = PathLine(hops, mode="powerline", to_destination=False).text().plain
assert head.startswith(POWERLINE_ROUND_OPEN) and head.endswith(POWERLINE_SEP)
tail = PathLine(hops, mode="powerline", from_origin=False).text().plain
assert tail.startswith(POWERLINE_SEP) and tail.endswith(POWERLINE_ROUND_CLOSE)
monkeypatch.setattr(pathline, "powerline_full", lambda: False)
squared = PathLine(hops, mode="powerline").text().plain # the whole route, for contrast
assert squared.startswith(" AAAA") and squared.endswith("BBBB ")
assert middle.text().plain.startswith(POWERLINE_SEP) # …the chevron is not the cap
assert middle.text().plain.endswith(POWERLINE_SEP)
def test_arrow_mode_says_a_route_middle_with_the_separator_it_joins_with() -> None:
"""No chevrons to shear on a console, so the arrow itself carries the same claim.
A leading ``→`` where the origin is not drawn, a trailing one where the destination
is not — the mark arrow mode already spells "the path goes on" with when a line
wraps. Both modes tell the reader the same thing.
"""
hops = [PathHop(label, key=key) for label, key in (("AAAA", "aa"), ("BBBB", "77"))]
assert PathLine(hops, mode="plain").text().plain == "AAAA → BBBB"
middle = PathLine(hops, mode="plain", from_origin=False, to_destination=False)
assert middle.text().plain == "→ AAAA → BBBB →"
assert PathLine(hops, mode="plain", from_origin=False).text().plain == "→ AAAA → BBBB"
assert PathLine(hops, mode="plain", to_destination=False).text().plain == "AAAA → BBBB →"
def test_a_wrapped_route_middle_pays_for_both_its_chevrons() -> None:
"""The marks are measured, not appended past the budget — on either mode.
A relays-only line spends a cell at each outer end that a whole route spends only
where the font has caps, so the fit has to know about them; the wrap flags and the
endpoint flags must not both draw at the same break either.
"""
hops = [
PathHop(label, key=key)
for label, key in (("AAAA", "aa"), ("BBBB", "77"), ("CCCC", "3d"), ("DDDD", "f2"))
]
lines = PathLine(hops, mode="powerline", from_origin=False, to_destination=False).wrapped(
20, indent=2
)
assert len(lines) == 2
assert all(line.cell_len <= 20 for line in lines)
assert lines[0].plain.startswith(POWERLINE_SEP) # opens mid-route…
assert lines[0].plain.endswith(POWERLINE_SEP) # …and the fold goes on
assert lines[-1].plain.endswith(POWERLINE_SEP) # …as does the route past the last chip
# Never both marks at one break: a fold that also happens to be the head or the
# tail of the drawn route still draws one chevron there, not two.
assert POWERLINE_SEP * 2 not in "".join(line.plain for line in lines)
folded = PathLine(hops, mode="plain", from_origin=False, to_destination=False).wrapped(
22, indent=2
)
assert len(folded) > 1 and all(line.cell_len <= 22 for line in folded)
assert folded[0].plain.startswith("→ AAAA") # the head mark, once
assert folded[-1].plain.rstrip().endswith("→")
assert not folded[-1].plain.strip().startswith("→") # a continuation opens on its hop
def test_a_seam_between_two_of_one_colour_falls_back_to_the_page() -> None:
"""Where two chips land on the same fill, the seam falls back to the bare page.