Atlas breadcrumbs: key-coloured, loop-truncated, tail-anchored
Three changes to the walk trail: - Each name now carries its node's own key-derived hue (ours the white you-style, a nameless node muted), the focus bold — instead of a flat muted/bold trail. - Walking to a node already on the trail truncates the stack to that node's first appearance, dropping the loop we walked to get back there rather than recording the round trip (this also subsumes the old step-back-through-the-west-node special case). - When the trail outgrows the line the head is dropped behind a leading "…" rather than the tail being cut, so the focus and the steps that led to it stay visible. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
223baee3ec
commit
0f97c0d752
2 files changed
+106
-14
No files matched your search
+52
-14
@@ -28,8 +28,12 @@ only its immediate neighbourhood:
|
||||
PgUp/PgDn a windowful.
|
||||
|
||||
**Enter walks**: the highlighted neighbour becomes the new focus, the breadcrumb trail
|
||||
across the top grows (``you › YUL-Cartierville › …``), and **⌫ steps back** along it.
|
||||
**Home** refocuses our own node. **Typing finds** — a global filter over every node in
|
||||
across the top grows (``you › YUL-Cartierville › …``, each name in its node's own hue),
|
||||
and **⌫ steps back** along it. Walking to a node already on the trail truncates the stack
|
||||
to its first appearance — the loop you walked to get back there is dropped rather than
|
||||
recorded — and when the trail outgrows the line its *head* is dropped behind a leading
|
||||
``…`` so the focus stays visible. **Home** refocuses our own node. **Typing finds** — a
|
||||
global filter over every node in
|
||||
the graph, islands included; Enter teleports the focus to the highlighted match (the
|
||||
trail restarts there, since the walk didn't cross the gap). ``^R`` rebuilds the graph
|
||||
from storage, and Esc peels find first, the screen second.
|
||||
@@ -42,6 +46,7 @@ from collections import deque
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
from rich.cells import cell_len
|
||||
from rich.text import Text
|
||||
|
||||
from ..core.models import (
|
||||
@@ -337,8 +342,12 @@ class AtlasScreen(Screen):
|
||||
# gap, so pretending it did would make ⌫ retrace a path never taken.
|
||||
self._trail = [target]
|
||||
self._filter = ""
|
||||
elif target == self._came_from:
|
||||
self._trail.pop() # walking back through the west node = one step back
|
||||
elif target in self._trail:
|
||||
# Revisiting a node already on the trail — stepping back through the west
|
||||
# node, or looping round to an earlier one — truncates the stack to that
|
||||
# node's first appearance. We drop the circular stretch we walked to get
|
||||
# back here rather than recording the round trip; losing the loop is the point.
|
||||
self._trail = self._trail[: self._trail.index(target) + 1]
|
||||
else:
|
||||
self._trail.append(target)
|
||||
self._index = 0
|
||||
@@ -425,19 +434,48 @@ class AtlasScreen(Screen):
|
||||
"""The breadcrumb trail (when walking) and the focus node's identity line."""
|
||||
out: list[str] = []
|
||||
if len(self._trail) > 1:
|
||||
trail = Text()
|
||||
for i, node in enumerate(self._trail):
|
||||
if i:
|
||||
trail.append(" › ", style="muted")
|
||||
last = i == len(self._trail) - 1
|
||||
trail.append(
|
||||
self._label(node), style="bold" if last else "muted"
|
||||
)
|
||||
trail.truncate(width, overflow="ellipsis")
|
||||
out.append(render_to_ansi(trail, width, no_wrap=True))
|
||||
out.append(render_to_ansi(self._trail_text(width), width, no_wrap=True))
|
||||
out.append(render_to_ansi(self._focus_line(depths), width, no_wrap=True))
|
||||
return out
|
||||
|
||||
def _trail_text(self, width: int) -> Text:
|
||||
"""The breadcrumb trail, each name in its own key-derived hue, tail-anchored.
|
||||
|
||||
Names carry the app-wide per-node hue (ours the white ``you``, a nameless node
|
||||
muted), the focus bold. When the whole trail won't fit on the line the *head* is
|
||||
dropped behind a leading ``…`` — never the tail — so the focus and the steps that
|
||||
led to it are always the ones kept in view.
|
||||
"""
|
||||
nodes = self._trail
|
||||
sep = " › "
|
||||
|
||||
def width_of(start: int) -> int:
|
||||
total = cell_len("…") + cell_len(sep) if start else 0 # the leading "… › "
|
||||
for k in range(start, len(nodes)):
|
||||
total += cell_len(sep) if k > start else 0
|
||||
total += cell_len(self._label(nodes[k]))
|
||||
return total
|
||||
|
||||
start = 0
|
||||
while start < len(nodes) - 1 and width_of(start) > width:
|
||||
start += 1
|
||||
|
||||
trail = Text()
|
||||
if start:
|
||||
trail.append("…" + sep, style="muted")
|
||||
for k in range(start, len(nodes)):
|
||||
if k > start:
|
||||
trail.append(sep, style="muted")
|
||||
last = k == len(nodes) - 1
|
||||
trail.append(self._label(nodes[k]), style=self._trail_style(nodes[k], last))
|
||||
trail.truncate(width, overflow="ellipsis") # guard a lone label wider than the line
|
||||
return trail
|
||||
|
||||
def _trail_style(self, node: str, last: bool) -> str:
|
||||
"""A trail name's style: its key hue (ours white, a nameless node muted), focus bold."""
|
||||
base = self._list_name_style(node)
|
||||
return f"bold {base}" if last else base
|
||||
|
||||
def _focus_line(self, depths: dict[str, int]) -> Text:
|
||||
"""Who is in focus: glyph, name, hash, kind, distance, and recency."""
|
||||
node = self._focus
|
||||
|
||||
@@ -134,6 +134,60 @@ def test_atlas_walking_into_the_back_node_pops_instead_of_growing() -> None:
|
||||
assert screen._trail == [topo.self_id] # popped, not [us, yul, us]
|
||||
|
||||
|
||||
def test_atlas_walking_to_an_earlier_node_drops_the_loop() -> None:
|
||||
"""Revisiting a node already on the trail truncates the stack to its first appearance,
|
||||
dropping the circular stretch walked to get back there."""
|
||||
topo = MeshTopology(US, contacts=[YUL, ALICE])
|
||||
yul = topo.canonical(YUL.public_key)
|
||||
alice = topo.canonical(ALICE.public_key)
|
||||
when = utcnow()
|
||||
topo.add_walk([topo.self_id, yul], snrs=[6.0], when=when, source="trace")
|
||||
topo.add_walk([yul, alice], snrs=[-2.0], when=when, source="packet")
|
||||
topo.add_walk([topo.self_id, alice], snrs=[3.0], when=when, source="trace") # closes the loop
|
||||
screen = _screen(topo)
|
||||
|
||||
def walk_to(node: str) -> None:
|
||||
screen.render_body(80)
|
||||
screen._index = screen._rows().index(node)
|
||||
screen.handle("enter")
|
||||
|
||||
walk_to(yul) # us → YUL
|
||||
walk_to(alice) # YUL → Alice
|
||||
assert screen._trail == [topo.self_id, yul, alice]
|
||||
walk_to(yul) # Alice → YUL: loops back, so Alice is dropped, not re-appended
|
||||
assert screen._trail == [topo.self_id, yul]
|
||||
|
||||
|
||||
def test_atlas_trail_drops_the_head_not_the_tail_when_narrow() -> None:
|
||||
"""A trail too long for the line loses its head behind a leading …, keeping the focus."""
|
||||
screen = _screen(_topo())
|
||||
us = screen._topo.self_id
|
||||
yul = screen._topo.canonical(YUL.public_key)
|
||||
alice = screen._topo.canonical(ALICE.public_key)
|
||||
screen._trail = [us, yul, alice]
|
||||
text = screen._trail_text(20).plain # too narrow for the whole "Homestead › … › Alice"
|
||||
assert text.startswith("…")
|
||||
assert text.endswith("Alice") # the focus is always kept
|
||||
assert "Homestead" not in text # the head was dropped, not the tail
|
||||
|
||||
|
||||
def test_atlas_trail_names_carry_their_node_hues() -> None:
|
||||
"""Trail names take the per-node key hue (ours the white you-style), the focus bold."""
|
||||
from meshterm.ui.theme import node_style
|
||||
|
||||
screen = _screen(_topo())
|
||||
us = screen._topo.self_id
|
||||
yul = screen._topo.canonical(YUL.public_key)
|
||||
screen._trail = [us, yul]
|
||||
text = screen._trail_text(80)
|
||||
plain = text.plain
|
||||
yul_at = plain.index("YUL-Cartierville")
|
||||
assert any(
|
||||
s.start <= yul_at < s.end and "bold" in str(s.style) and node_style(yul) in str(s.style)
|
||||
for s in text.spans
|
||||
)
|
||||
|
||||
|
||||
def test_atlas_home_refocuses_us() -> None:
|
||||
"""Home resets the walk to our own node from anywhere."""
|
||||
topo = _topo()
|
||||
|
||||
Reference in new issue
Block a user