Path lines: cut them all the same way, and end them where they end
Two fixes to what a path line shows at its edges and at its ends. The edges: the crack only reached the two surfaces that drew their own marks. Everywhere else a path still met render_to_ansi(no_wrap=True) or a plain truncate, both of which ellipsize — so the Message paths rows you were not on trailed off in dots while the one you were on cracked. Those cuts now go through cut_to, which reads the line and decides: a chip breaks off on the crack, prose keeps its ellipsis. That covers the unselected arrival rows, every select-list row (the trophy walks and probe candidates), the TX sweep action rows, and the composer's suggestion rows. The generic surfaces are safe to convert precisely because the fallback is the old behaviour. The ends: a path's ends are the nodes it went between, so a line that opens on its first *relay* is naming a node that only passed the message on. The Message paths rows now run origin → relays → us — the same two endpoints the graph one row up already drew between, so the row and the picture finally cross-read. A hopless arrival stops collapsing to the word "direct": Alice ▶ ★ is what direct delivery looks like, on the same rails as every relayed row. And our own end is the ★ everywhere, not our name — it was already so on the trace lane, the trophy card and the composer, and now also on the node detail Routes rows, the TX sweep route, the mesh walk trail and the live feed's subject lane. It is the one node the reader never has to be told, and in an 18-cell feed lane or a width-starved trail the cells it gives back are hops that stay in view. Left alone deliberately: the packet viewer's `via` row (the dialog has a `from` row naming the origin, so `via` really is just the relay chain), the verbatim wire spec, and widgets._link_text — a table of FROM → TO *links*, still on the legacy path_text builder. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9e262d157d
commit
6a12104cf7
15 files changed
+206
-49
No files matched your search
@@ -125,10 +125,15 @@ Relative ages: `format_ago` for prose ("now", "5m ago", "never" — never "now a
|
||||
`■` room · `◉` sensor · `○` unknown.
|
||||
- Path cuts: a path line drawn as **chips** that gets cut — a lane that ran out, a row
|
||||
scrolled past its edge — breaks the chip off on a half block in that chip's own fill
|
||||
(`▌` end of line, `▐` start; `pathline.cut_mark`/`cut_to`), so half the cell is segment
|
||||
and half is bare page. An ellipsis would say a *word* was shortened; the crack says the
|
||||
segment continues. Arrow-drawn paths keep the `…` — nothing to shear. Distinct from
|
||||
`⋯`, which marks whole hops *elided* out of the middle (`PathLine.ellipsized`).
|
||||
(`▐` ends a line, `▌` opens one; `pathline.cut_mark`/`cut_to`), so half the cell is
|
||||
segment and half is bare page. An ellipsis would say a *word* was shortened; the crack
|
||||
says the segment continues. Arrow-drawn paths keep the `…` — nothing to shear. Distinct
|
||||
from `⋯`, which marks whole hops *elided* out of the middle (`PathLine.ellipsized`).
|
||||
- Path endpoints: a path line runs between the nodes it actually went between — the true
|
||||
origin and destination, never the first and last *relay*. Our own end is always the
|
||||
app-wide `★` (`pathline.SELF_GLYPH`, or `path_line(bare_self=True)`), never our name:
|
||||
the one node the reader never has to be told, and the cells belong to the hops that
|
||||
differ from row to row.
|
||||
- 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 ·
|
||||
|
||||
@@ -56,7 +56,7 @@ from .packet_viewer import (
|
||||
class_marks,
|
||||
node_label,
|
||||
)
|
||||
from .pathline import PathHop, PathLine
|
||||
from .pathline import SELF_GLYPH, PathHop, PathLine
|
||||
from .theme import name_style, snr_style
|
||||
from .tui.render import crop_cells, render_to_ansi
|
||||
from .tui.screen import ListWindow, Screen
|
||||
@@ -667,6 +667,11 @@ class LiveFeedScreen(Screen):
|
||||
def _hop(self, value: str, *, named: bool = True) -> PathHop:
|
||||
"""One endpoint as a path hop: its contact name, or its one-byte hash in that hue.
|
||||
|
||||
Our own node resolves to the app-wide :data:`~meshterm.ui.pathline.SELF_GLYPH`
|
||||
rather than its name — the one endpoint the reader never has to be told, and in an
|
||||
18-cell lane the cells it gives back are the *other* end's name reading whole
|
||||
instead of being cut.
|
||||
|
||||
Args:
|
||||
value: The endpoint's key hash (or, for an anonymous request's sender, its
|
||||
whole key — shown cut to the lane's one-byte width when it can't be named).
|
||||
@@ -675,8 +680,10 @@ class LiveFeedScreen(Screen):
|
||||
"""
|
||||
short = value[: 2 * ENDPOINT_HASH_BYTES]
|
||||
name = self._resolve(value) if named else None
|
||||
if name and name == self._self_name:
|
||||
return PathHop(SELF_GLYPH, you=True)
|
||||
if name and name != value:
|
||||
return PathHop(name, key=value, you=bool(self._self_name and name == self._self_name))
|
||||
return PathHop(name, key=value)
|
||||
return PathHop(short, key=value, lit_bytes=ENDPOINT_HASH_BYTES)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -15,17 +15,19 @@ detail read together:
|
||||
so the graph's labels stay two cells wide and a many-path graph stays readable.
|
||||
* the **arrival list** beneath is two lines per logged copy. On top, alone on its lane
|
||||
and introduced by nothing, the **route** as a path line (:mod:`~meshterm.ui.pathline`):
|
||||
each relay a chip in its own node hue, named where we know the node (``YUL-Poly``) and
|
||||
standing in its own hash at the device's path-hash width where we don't (``e839f2``,
|
||||
keyless grey). No hash is repeated after a name — the graph above is where the hash
|
||||
the origin, every relay, and us — the same two endpoints the graph draws between, so
|
||||
the row and the picture start and finish in the same places. Each node is a chip in its
|
||||
own hue, named where we know it (``YUL-Poly``) and standing in its own hash at the
|
||||
device's path-hash width where we don't (``e839f2``, keyless grey); our own end is the
|
||||
app-wide ``★``. No hash is repeated after a name — the graph above is where the hash
|
||||
bytes live, and a chip and its label share the node's colour, so the two halves
|
||||
cross-reference by hue instead of by spelling the hex twice. Under it, hanging muted,
|
||||
the frame's own facts: time heard, reception SNR, and which resend it was.
|
||||
The route is the line you pick — it is what one arrival differs from another by, and
|
||||
what the graph highlights. ↑↓ move the selection (the graph's highlight follows); a
|
||||
route longer than the dialog **scrolls horizontally with ←→**, the whole line shifting
|
||||
under a ``…`` at whichever edge continues, and snaps back the moment the selection
|
||||
moves on.
|
||||
under a cracked chip at whichever edge continues, and snaps back the moment the
|
||||
selection moves on. A row you are *not* on is cut the same way — it just cannot slide.
|
||||
|
||||
Nothing here transmits; like the service beneath it, this is a read-model over what
|
||||
the radio already heard.
|
||||
@@ -40,7 +42,16 @@ from rich.text import Text
|
||||
from ..core.models import ChatMessage
|
||||
from ..services.message_paths import Arrival
|
||||
from .pathgraph import PathLayer, render_path_graph, revisited_hops
|
||||
from .pathline import ELIDE_HEAD, ELIDE_TAIL, cut_mark, path_line
|
||||
from .pathline import (
|
||||
ELIDE_HEAD,
|
||||
ELIDE_TAIL,
|
||||
SELF_GLYPH,
|
||||
PathHop,
|
||||
PathLine,
|
||||
cut_mark,
|
||||
cut_to,
|
||||
path_line,
|
||||
)
|
||||
from .theme import snr_style
|
||||
from .tui.render import crop_cells, render_to_ansi
|
||||
from .tui.screen import Screen
|
||||
@@ -209,7 +220,10 @@ class MessagePathsScreen(Screen):
|
||||
else:
|
||||
row = Text(" ")
|
||||
row.append_text(self._path_text(arrival))
|
||||
lines.append(render_to_ansi(row, width, no_wrap=True))
|
||||
# Cut here rather than letting the render boundary ellipsize it: an
|
||||
# unselected route runs off the lane exactly as the selected one does,
|
||||
# and it owes the reader the same cracked chip rather than three dots.
|
||||
lines.append(render_to_ansi(cut_to(row, width), width, no_wrap=True))
|
||||
detail = Text(" " * _DETAIL_INDENT)
|
||||
detail.append_text(self._detail_text(arrival))
|
||||
lines.append(render_to_ansi(detail, width, no_wrap=True))
|
||||
@@ -219,7 +233,7 @@ class MessagePathsScreen(Screen):
|
||||
# -- the rows --
|
||||
|
||||
def _path_text(self, arrival: Arrival) -> Text:
|
||||
"""One arrival's relay chain — the row's first line, and the one you pick.
|
||||
"""One arrival's whole route — the row's first line, and the one you pick.
|
||||
|
||||
The route gets the row's full width to itself, so no ``via`` introduces it: on a
|
||||
lane that holds nothing else there is nothing to tell it apart from. It is THE
|
||||
@@ -230,12 +244,36 @@ class MessagePathsScreen(Screen):
|
||||
"this is a name" signal), and no hop repeats its hash after its name. The hash
|
||||
bytes are the graph's job, one row up; what ties the two together is the shared
|
||||
node hue, not a second spelling of the hex.
|
||||
|
||||
The line runs **origin to us**, not relay to relay (JP, 2026-08-09): a path's
|
||||
ends are the nodes it went between, and a chain that opens on its first *relay*
|
||||
reads as a route from a node that only passed the message on. So the sender leads
|
||||
(:meth:`_origin_hop`) and we close it on the app-wide ``★``, which is exactly what
|
||||
the graph one row up draws between — the two now name the same two endpoints
|
||||
instead of the row starting a hop later than the picture above it. A hopless
|
||||
arrival is no longer a lone word: ``Alice ▶ ★`` *is* what direct delivery looks
|
||||
like, and it reads on the same rails as every other row.
|
||||
"""
|
||||
return path_line(
|
||||
relays = path_line(
|
||||
arrival.hops, self._resolve,
|
||||
prefix_bytes=self._prefix_bytes, self_name=self._self_name,
|
||||
empty="direct", hash_as_name=True,
|
||||
).text()
|
||||
hash_as_name=True,
|
||||
).hops
|
||||
return PathLine([self._origin_hop(), *relays, PathHop(SELF_GLYPH, you=True)]).text()
|
||||
|
||||
def _origin_hop(self) -> PathHop:
|
||||
"""The node the message set out from — the route's true head.
|
||||
|
||||
The same origin the graph's left endpoint draws (:func:`~meshterm.ui.widgets.
|
||||
route_graph_style`), named and hued the same way: the sender's display name in its
|
||||
key-derived hue, our own ``★`` for a message we sent, and a bare ``?`` where the
|
||||
frame named nobody — never a hue guessed from a name we can't place.
|
||||
"""
|
||||
if not self._source:
|
||||
return PathHop("?")
|
||||
if self._self_name and self._source == self._self_name:
|
||||
return PathHop(SELF_GLYPH, you=True)
|
||||
return PathHop(self._source, key=self._key_of(self._source) if self._key_of else None)
|
||||
|
||||
def _detail_text(self, arrival: Arrival) -> Text:
|
||||
"""One arrival's reception facts — when it landed, how loudly, which copy.
|
||||
|
||||
@@ -78,7 +78,7 @@ from rich.cells import cell_len
|
||||
from rich.text import Text
|
||||
|
||||
from ..core.geo import EARTH_RADIUS_KM, usable_fix
|
||||
from ..core.models import LOCAL_DEVICE_LABEL, NODE_TYPE_LABELS, Contact, utcnow
|
||||
from ..core.models import NODE_TYPE_LABELS, Contact, utcnow
|
||||
from ..platforms import Platform, on_platform
|
||||
from .mapcanvas import RGB
|
||||
from .minimap import MiniMap
|
||||
@@ -95,7 +95,15 @@ from .pathgraph import (
|
||||
)
|
||||
from .theme import mark_rgb, name_style, snr_style
|
||||
from .tui.render import crop_cells, render_hanging, render_lines, render_to_ansi
|
||||
from .pathline import ELIDE_HEAD, ELIDE_TAIL, PathHop, PathLine, cut_mark, cut_to
|
||||
from .pathline import (
|
||||
ELIDE_HEAD,
|
||||
ELIDE_TAIL,
|
||||
SELF_GLYPH,
|
||||
PathHop,
|
||||
PathLine,
|
||||
cut_mark,
|
||||
cut_to,
|
||||
)
|
||||
from .tui.screen import CANCEL, ListWindow, Screen
|
||||
from .widgets import (
|
||||
_DEFAULT_GLYPH,
|
||||
@@ -1431,7 +1439,10 @@ def _route_line(
|
||||
:class:`~meshterm.ui.pathline.PathLine` drawn exactly as the Message paths dialog draws
|
||||
its arrivals (JP, 2026-08-09): **hops read as names** — every hop the resolver can place
|
||||
wears its contact name in that node's key-derived hue, as powerline chips where the
|
||||
terminal can draw them, and our own end takes the ``you`` white. A hop nobody can name has
|
||||
terminal can draw them, and our own end stands on the app-wide ``★`` — every route on this
|
||||
screen ends on us, so the star says it in one cell and leaves the rest of the lane to the
|
||||
hops that differ from row to row (the same trade the trace route lane and the trophy card
|
||||
make, and the same mark the graph above plants on our end). A hop nobody can name has
|
||||
no name to show, so it stands in its own hash at the device's path-hash-mode width (the
|
||||
width the radio itself carries per hop) in the app-wide unknown-node grey — colour being
|
||||
the "this is a name" signal, exactly as in the graph above. No hop repeats its hash after
|
||||
@@ -1457,7 +1468,7 @@ def _route_line(
|
||||
PathHop(node_label, key=name_key) if node_known and name_key
|
||||
else PathHop(_hop_hash(name_key, node_label, hash_bytes)),
|
||||
*(hop_of(hop) for hop in reversed(hops_out)),
|
||||
PathHop(self_name or LOCAL_DEVICE_LABEL, you=True),
|
||||
PathHop(SELF_GLYPH, you=True),
|
||||
]).text()
|
||||
|
||||
atoms: list[Text] = []
|
||||
|
||||
@@ -73,7 +73,7 @@ from ..services.topology import (
|
||||
from .theme import snr_style
|
||||
from .tui.render import query_line, render_lines, render_to_ansi
|
||||
from .tui.screen import ListWindow, Screen
|
||||
from .pathline import PathLine, path_line
|
||||
from .pathline import PathLine, cut_to, path_line
|
||||
from .widgets import NodeResolver, _age_seconds, _format_age, _identity, path_text
|
||||
|
||||
#: Sentinel spec meaning "no forced path — let the device route" (the trace screen's
|
||||
@@ -612,7 +612,10 @@ class PathComposerScreen(Screen):
|
||||
if is_sel:
|
||||
text.style = "brand"
|
||||
text.no_wrap = True
|
||||
text.truncate(width, overflow="ellipsis")
|
||||
# A suggestion row names a node in the route's own vocabulary, so it is cut
|
||||
# like one: a chip that runs off the dialog cracks, prose keeps the ellipsis.
|
||||
text = cut_to(text, width)
|
||||
text.no_wrap = True
|
||||
entries.append((i, render_to_ansi(text, width)))
|
||||
if not any(kind == "hop" for kind, _ in rows):
|
||||
if any(kind == "fetch" for kind, _ in rows):
|
||||
|
||||
@@ -128,8 +128,8 @@ _ELLIPSIS = "…"
|
||||
#: the page takes the right); :data:`CRACK_HEAD` is its mirror, opening a line whose
|
||||
#: start is off-screen. Only chips crack — :func:`cut_mark` finds a fill or falls back to
|
||||
#: :data:`_ELLIPSIS`, so arrow lines are untouched by the whole idea.
|
||||
CRACK_TAIL = "▌"
|
||||
CRACK_HEAD = "▐"
|
||||
CRACK_TAIL = "▐"
|
||||
CRACK_HEAD = "▌"
|
||||
|
||||
#: 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
|
||||
|
||||
@@ -15,6 +15,7 @@ from typing import Any, Callable, Optional, Union
|
||||
from rich.cells import cell_len
|
||||
from rich.text import Text
|
||||
|
||||
from ..pathline import cut_to
|
||||
from .render import crop_cells, query_line, render_lines, render_to_ansi
|
||||
from .screen import LazyLines, Screen
|
||||
|
||||
@@ -611,8 +612,11 @@ class SelectScreen(Screen):
|
||||
text.append_text(label_text)
|
||||
text.style = style
|
||||
text.no_wrap = True
|
||||
text.overflow = "ellipsis"
|
||||
text.truncate(width)
|
||||
# Cut rather than truncate, so a row carrying a path line (a trophy walk, a
|
||||
# probe candidate) breaks its chip off on the crack while every ordinary row
|
||||
# still ends in the ellipsis — ``cut_to`` decides that from the row itself.
|
||||
text = cut_to(text, width)
|
||||
text.no_wrap = True
|
||||
return render_to_ansi(text, width)
|
||||
|
||||
return draw
|
||||
|
||||
@@ -41,7 +41,7 @@ from ..core.connection import REMOTE_TX_MAX, REMOTE_TX_MIN
|
||||
from ..core.models import Contact, TraceResult, TxLevelResult, TxOptResult
|
||||
from ..services import trace_runner, tx_optimizer
|
||||
from ..services.topology import build_topology, render_custom_spec
|
||||
from .pathline import PathHop, PathLine, path_line
|
||||
from .pathline import SELF_GLYPH, PathHop, PathLine, cut_to, path_line
|
||||
from .theme import name_style, snr_style
|
||||
from .trace_screen import TracingDialog, _collapse_trace_width, snr_bar
|
||||
from .tui.render import render_lines, render_to_ansi
|
||||
@@ -352,7 +352,10 @@ class TxSweepScreen(Screen):
|
||||
selected = i == self._index
|
||||
text = self._action_text(key, selected)
|
||||
text.no_wrap = True
|
||||
text.truncate(width, overflow="ellipsis")
|
||||
# The route row carries a path line, so it is cut rather than truncated: a
|
||||
# chip that runs off the row cracks, every other row keeps the ellipsis.
|
||||
text = cut_to(text, width)
|
||||
text.no_wrap = True
|
||||
if selected:
|
||||
self._cursor = len(lines)
|
||||
lines.append(render_to_ansi(text, width))
|
||||
@@ -446,11 +449,16 @@ class TxSweepScreen(Screen):
|
||||
"""The walk one measurement makes: out through the tuned link, mirrored home.
|
||||
|
||||
The outbound leg — us, any composed hops, the tuned node, the target — draws
|
||||
in full colour (each name in its own key hue, us the white ``you``); the
|
||||
return (the outbound mirrored back, the trace boomerang the optimizer
|
||||
actually flies) is dimmed, reading as "not yours to compose".
|
||||
in full colour (each name in its own key hue); the return (the outbound
|
||||
mirrored back, the trace boomerang the optimizer actually flies) is dimmed,
|
||||
reading as "not yours to compose".
|
||||
|
||||
Both our ends stand on the app-wide ``★`` rather than our name: every
|
||||
measurement this screen makes leaves us and comes home to us, so spelling
|
||||
ourselves out twice per line would cost the lane the very hops the sweep is
|
||||
tuning — the same trade the trace route lane and the trophy card make.
|
||||
"""
|
||||
hops: list[PathHop] = [PathHop(self._device_label, you=True)]
|
||||
hops: list[PathHop] = [PathHop(SELF_GLYPH, you=True)]
|
||||
for hop in self.route_hops:
|
||||
hops.append(PathHop(self._hop_name(hop), key=hop))
|
||||
hops.append(PathHop(self._admin_label, key=self._admin_key))
|
||||
@@ -458,7 +466,7 @@ class TxSweepScreen(Screen):
|
||||
hops.append(PathHop(self._admin_label, key=self._admin_key, dim=True))
|
||||
for hop in reversed(self.route_hops):
|
||||
hops.append(PathHop(self._hop_name(hop), key=hop, dim=True))
|
||||
hops.append(PathHop(self._device_label, you=True, dim=True))
|
||||
hops.append(PathHop(SELF_GLYPH, you=True, dim=True))
|
||||
return PathLine(hops)
|
||||
|
||||
def _hop_name(self, hop: str) -> str:
|
||||
|
||||
@@ -66,7 +66,7 @@ from ..services.topology import Link, MeshTopology
|
||||
from .map_render import _SELF, _UNKNOWN
|
||||
from .mapcanvas import RGB, MapCanvas, parse_hex
|
||||
from .menus import fit_cells
|
||||
from .pathline import ELIDE_HEAD, PathHop, PathLine
|
||||
from .pathline import ELIDE_HEAD, SELF_GLYPH, PathHop, PathLine
|
||||
from .theme import mark_rgb, name_style, snr_style
|
||||
from .trace_screen import snr_bar
|
||||
from .tui.render import query_line, render_to_ansi
|
||||
@@ -667,12 +667,19 @@ class WalkScreen(Screen):
|
||||
return limit
|
||||
|
||||
def _trail_hop(self, node: str) -> PathHop:
|
||||
"""One walked step as a path hop: its display name in its own identity colour."""
|
||||
"""One walked step as a path hop: its display name in its own identity colour.
|
||||
|
||||
Our own node takes the app-wide :data:`~meshterm.ui.pathline.SELF_GLYPH` instead
|
||||
of its name: every walk sets out from us, so the star says in one cell what the
|
||||
trail's most width-starved line would otherwise spend a whole name on — and the
|
||||
cells it frees are steps that stay in view before the head has to be elided.
|
||||
"""
|
||||
style = self._list_name_style(node)
|
||||
if style == "you":
|
||||
return PathHop(SELF_GLYPH, you=True)
|
||||
return PathHop(
|
||||
self._label(node),
|
||||
key=None if style in ("you", "node.unknown") else node,
|
||||
you=style == "you",
|
||||
key=None if style == "node.unknown" else node,
|
||||
)
|
||||
|
||||
def _focus_line(self, depths: dict[str, int]) -> Text:
|
||||
|
||||
@@ -1020,7 +1020,12 @@ def test_sweep_screen_stars_the_running_best() -> None:
|
||||
screen.on_level(1, 7, _level(12, -2.0))
|
||||
screen.on_level(2, 7, _level(18, 7.5))
|
||||
assert screen.phase_label() == "coarse sweep · level 2/7"
|
||||
starred = next(line for line in _plain(screen.render_body(100)).splitlines() if "★" in line)
|
||||
# The route lane wears ★ for our own ends, so the star that marks the winner is the
|
||||
# one in the levels table — a row that also carries the level's reading.
|
||||
starred = next(
|
||||
line for line in _plain(screen.render_body(100)).splitlines()
|
||||
if "★" in line and "dB" in line
|
||||
)
|
||||
assert "18" in starred
|
||||
|
||||
|
||||
|
||||
@@ -206,7 +206,9 @@ def test_livefeed_addressed_frame_is_about_its_two_ends() -> None:
|
||||
screen.on_event(MeshEvent.observation_event(_obs(node="", kind="packet", raw=raw)))
|
||||
row = _rows(screen, 100)[0]
|
||||
assert "📩 direct message" in row
|
||||
assert "Alice → Us" in row # …including us, when a frame is addressed to us
|
||||
# …including us, when a frame is addressed to us — as the app-wide ★, which is the one
|
||||
# endpoint the reader never has to be told and the cells the other end's name needs.
|
||||
assert "Alice → ★" in row
|
||||
|
||||
# A pair too wide for the lane spends its cells on the addressee: the sender drops to
|
||||
# the hash it was named by rather than the recipient being the half cut off.
|
||||
|
||||
@@ -208,12 +208,61 @@ def test_paths_screen_cracks_the_chips_the_scroll_cuts(monkeypatch) -> None: #
|
||||
assert row.rstrip().endswith(CRACK_TAIL) and "…" not in row
|
||||
|
||||
|
||||
def test_paths_screen_route_runs_origin_to_us_not_relay_to_relay() -> None:
|
||||
"""A row is the whole route, not the relay chain it rode.
|
||||
|
||||
A path's ends are the nodes it went *between*, so the sender leads the line and we
|
||||
close it on the ★ — the same two endpoints the graph one row up draws between, which
|
||||
is what lets the row and the picture cross-read. A chain that opened on its first
|
||||
relay read as a route from a node that had only passed the message on.
|
||||
"""
|
||||
screen = _screen(_arrivals())
|
||||
rows = _plain(screen.render_body(76)).split("sensor\n\n")[1].split("\n")
|
||||
assert rows[0] == "❯ Alice → YUL-Cartierville → ★"
|
||||
assert rows[2] == " Alice → Waymarker → 77 → ★" # an unnamed relay still stands in its hash
|
||||
|
||||
|
||||
def test_paths_screen_origin_is_a_star_for_us_and_a_question_for_nobody() -> None:
|
||||
"""The head is named exactly as the graph's left endpoint is: our own ``★`` on a
|
||||
message we sent, a bare ``?`` where the frame named nobody — never a guessed name."""
|
||||
now = utcnow()
|
||||
one = [Arrival(when=now, hops=("3d63",), snr=1.0)]
|
||||
ours = _plain(_screen(one, source="Homestead").render_body(76))
|
||||
assert "❯ ★ → YUL-Cartierville → ★" in ours
|
||||
nameless = _plain(_screen(one, source=None).render_body(76))
|
||||
assert "❯ ? → YUL-Cartierville → ★" in nameless
|
||||
|
||||
|
||||
def test_paths_screen_cuts_unselected_rows_the_same_way(monkeypatch) -> None: # noqa: ANN001
|
||||
"""A row you are not on runs off the lane exactly as the selected one does, so it owes
|
||||
the reader the same cracked chip — it just cannot slide to read the rest."""
|
||||
monkeypatch.setattr(pathline, "powerline_enabled", lambda: True)
|
||||
now = utcnow()
|
||||
long = tuple(f"{i:02x}{i:02x}" for i in range(12))
|
||||
screen = _screen([
|
||||
Arrival(when=now, hops=long, snr=1.0),
|
||||
Arrival(when=now + timedelta(seconds=2), hops=long[::-1], snr=2.0),
|
||||
])
|
||||
lines = _plain(screen.render_body(40)).split("\n")
|
||||
rows = lines[next(i for i, ln in enumerate(lines) if ln.startswith("❯")):]
|
||||
assert rows[0].rstrip().endswith(CRACK_TAIL) # the selected row
|
||||
unselected = rows[2] # row 1 is the selected row's hanging reception facts
|
||||
assert unselected.rstrip().endswith(CRACK_TAIL) and "…" not in unselected
|
||||
|
||||
|
||||
def test_paths_screen_direct_arrival_and_empty_state() -> None:
|
||||
"""A hop-less arrival reads direct; no arrivals at all explain themselves."""
|
||||
"""A hop-less arrival draws its two endpoints and nothing between; no arrivals at all
|
||||
explain themselves.
|
||||
|
||||
The route no longer collapses to the word *direct*: with both ends on the line,
|
||||
``Alice → ★`` **is** what a direct delivery looks like, and it reads on the same rails
|
||||
as every relayed row instead of swapping the picture for a caption.
|
||||
"""
|
||||
now = utcnow()
|
||||
screen = _screen([Arrival(when=now, hops=(), snr=2.5)])
|
||||
body = _plain(screen.render_body(76))
|
||||
assert "direct" in body
|
||||
assert "❯ Alice → ★" in body
|
||||
assert "direct" not in body
|
||||
empty = _screen([], matched=False)
|
||||
body = _plain(empty.render_body(76))
|
||||
assert "No direct-message frames logged in the window." in body
|
||||
|
||||
@@ -36,7 +36,7 @@ from meshterm.ui.node_detail_screen import (
|
||||
_signal_row,
|
||||
)
|
||||
from meshterm.ui.pathgraph import DST_NODE, SRC_NODE
|
||||
from meshterm.ui.pathline import CRACK_HEAD, CRACK_TAIL, PathHop, PathLine
|
||||
from meshterm.ui.pathline import CRACK_HEAD, CRACK_TAIL, SELF_GLYPH, PathHop, PathLine
|
||||
from meshterm.ui.tui.screen import CANCEL
|
||||
from meshterm.ui.widgets import highlighted_hash, route_graph_style, tab_strip
|
||||
|
||||
@@ -203,7 +203,8 @@ def test_route_line_names_its_hops() -> None:
|
||||
assert line.startswith("Far") # the contact anchors the left, by name
|
||||
assert "Hub" in line # the relay reads as the contact it is, not as ``3d``
|
||||
assert "3d" not in line and "f2" not in line # no hop repeats its hash after its name
|
||||
assert line.rstrip().endswith("Us") # our own node anchors the right
|
||||
assert line.rstrip().endswith(SELF_GLYPH) # our own node anchors the right, as the ★
|
||||
assert "Us" not in line # …in one cell, not a name every row would repeat
|
||||
assert context.plain == "device route" # the firmware-route tag, off the pathline itself
|
||||
|
||||
|
||||
@@ -257,8 +258,7 @@ def test_route_line_direct_route_has_no_relay() -> None:
|
||||
resolve=make_node_resolver([HUB]), node_known=True,
|
||||
self_name="Us", hash_bytes=1,
|
||||
)
|
||||
line = path.plain
|
||||
assert "Far" in line and "Us" in line and "→" in line
|
||||
assert path.plain == f"Far → {SELF_GLYPH}"
|
||||
|
||||
|
||||
# --- the routes view (list + graph callbacks) -----------------------------------
|
||||
|
||||
@@ -21,6 +21,7 @@ from rich.text import Text
|
||||
from meshterm import copyright_notice
|
||||
from tests.conftest import plain as _plain
|
||||
from meshterm.ui.menus import section_heading
|
||||
from meshterm.ui.pathline import CRACK_TAIL, PathHop, PathLine
|
||||
from meshterm.ui.tui import frame, glow
|
||||
from meshterm.ui.tui.glow import apply_corner_glow
|
||||
from meshterm.ui.tui.progress import ProgressScreen
|
||||
@@ -208,6 +209,22 @@ def test_select_width_aware_title_fits_itself_to_the_row() -> None:
|
||||
assert screen._rows() == screen._items
|
||||
|
||||
|
||||
def test_select_row_cracks_a_chip_path_and_ellipsizes_everything_else() -> None:
|
||||
"""A row too wide for the list is *cut*, not truncated: a row carrying a path line
|
||||
(a trophy walk, a probe candidate) breaks its chip off on the crack, while an
|
||||
ordinary prose row keeps the ellipsis. The row itself decides which — the list has
|
||||
no idea it is ever holding a route."""
|
||||
route = PathLine(
|
||||
[PathHop(f"NODE{i:02d}", key=f"{i:02x}aa") for i in range(8)], mode="powerline"
|
||||
).text()
|
||||
screen = SelectScreen("pick", [Choice(route, 1), Choice("a plainly worded row", 2)])
|
||||
rows = [ln for ln in _plain(screen.render_body(16)).split("\n") if ln.strip()]
|
||||
route_row = next(ln for ln in rows if "NODE" in ln)
|
||||
prose_row = next(ln for ln in rows if "plainly" in ln)
|
||||
assert route_row.rstrip().endswith(CRACK_TAIL) and "…" not in route_row
|
||||
assert prose_row.rstrip().endswith("…") # prose was shortened; that is what happened
|
||||
|
||||
|
||||
def test_select_hscroll_highlight_keeps_the_natural_row() -> None:
|
||||
"""In an hscroll list the highlighted row skips self-fitting — ←→ slide the full
|
||||
line — while every other row still elides itself to the width."""
|
||||
|
||||
+4
-3
@@ -11,6 +11,7 @@ from datetime import timedelta
|
||||
|
||||
from meshterm.core.models import Contact, utcnow
|
||||
from meshterm.services.topology import MeshTopology
|
||||
from meshterm.ui.pathline import SELF_GLYPH
|
||||
from meshterm.ui.tui.render import render_to_ansi
|
||||
from meshterm.ui.walk_screen import WalkScreen
|
||||
|
||||
@@ -210,7 +211,7 @@ def test_walk_enter_walks_and_grows_the_trail() -> None:
|
||||
screen.handle("enter") # walk to YUL (our only neighbour)
|
||||
assert screen._focus == topo.canonical(YUL.public_key)
|
||||
body = _plain(screen.render_body(80))
|
||||
assert "Homestead › YUL-Cartierville" in body # the trail
|
||||
assert "★ › YUL-Cartierville" in body # the trail, our end on the app-wide star
|
||||
# The focus line reads name (hash) — the glyph carries the type, not a spelled-out kind.
|
||||
assert "YUL-Cartierville (3d)" in body and "1 hop out" in body
|
||||
assert "Alice" in body # YUL's onward neighbour is now a row
|
||||
@@ -303,7 +304,7 @@ def test_walk_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"
|
||||
assert screen._trail_text(40).plain == SELF_GLYPH # us, in one cell
|
||||
|
||||
|
||||
def test_walk_trail_names_carry_their_node_hues() -> None:
|
||||
@@ -669,7 +670,7 @@ def test_walk_trail_scroll_stops_at_the_head_and_resets_with_the_trail() -> None
|
||||
screen.handle("left")
|
||||
assert screen._trail_scroll == limit
|
||||
head = _plain([render_to_ansi(screen._trail_text(width), width, no_wrap=True)])
|
||||
assert "Homestead" in head # scrolled far enough to read where the walk set out from
|
||||
assert head.lstrip().startswith(SELF_GLYPH) # scrolled far enough to see where it set out
|
||||
|
||||
screen.handle("backspace") # stepping back is a change to the trail
|
||||
assert screen._trail_scroll == 0
|
||||
|
||||
Reference in new issue
Block a user