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:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-08-09 18:36:43 -04:00
1 parent 9e262d157d
commit 6a12104cf7
15 files changed
+206 -49

No files matched your search

+9 -4
View File
@@ -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 ·
+9 -2
View File
@@ -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
+49 -11
View File
@@ -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.
+15 -4
View File
@@ -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] = []
+5 -2
View File
@@ -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):
+2 -2
View File
@@ -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
+6 -2
View File
@@ -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
+15 -7
View File
@@ -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:
+11 -4
View File
@@ -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:
+6 -1
View File
@@ -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
+3 -1
View File
@@ -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.
+51 -2
View File
@@ -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
+4 -4
View File
@@ -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) -----------------------------------
+17
View File
@@ -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
View File
@@ -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