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:
1 parent
e5231687de
commit
10f48dbffc
6 files changed
+231
-60
No files matched your search
@@ -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 ·
|
||||
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in new issue
Block a user