Atlas graph: key-hue every marker, air the fan out west, always breadcrumb

Colour each node's marker (and its body-text glyph) by its own key-derived
hue instead of the node type — the glyph *shape* still carries the type — so
the graph reads by identity. No marker is whited out any more, not even the
selection or our own repeaters/nodes: the selection is shown by its white
label and the lit route. Our own node keeps the white `you` mark; the legend
becomes an honest shape key (role glyphs muted) now that colour means identity.

Anchor the focus at the far west with its name to the right and let the fan
spread across the whole width east of it — the graph aired out rather than
squeezed into the middle. The focus and the came-from node keep their icon on
the left with the name to its right, but every edge now attaches at the *right
end of the name* (not the icon) so a connecting line never crosses a label; the
came-from ducks in just west of the focus and lower.

Render the breadcrumb trail always, even at the root where it is just our own
node, so the header keeps a steady height rather than shifting up a row on the
first walk step.

The fan's promote/collapse ranking already folds SNR, sample count, and recency
together via Link.strength() — documented, not changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 4.8 committed 2026-07-19 22:17:34 -04:00
1 parent 8054d064e8
commit 41c2d3168e
1 file changed
+181 -92
+181 -92
View File
@@ -10,15 +10,21 @@ Rather than plotting the whole mesh at once (which reads as a hairball the momen
graph grows), the atlas keeps one node *in focus* — our own, to begin with — and shows
only its immediate neighbourhood:
* the **canvas** — the majority of the screen, so the shape stays legible — draws the
focus toward the left (its label to its left) with a deliberately sparse fan of its
strongest neighbours to the right, each named just to the right of its marker, edges
as braille lines coloured by the link's median SNR (green → amber → red, slate for
links with no reading) and faded by evidence age. The node the trail came from is
anchored at the far **west**, so walking always reads as moving right and backing up
as moving left. Only as many neighbours as the canvas area can carry are drawn — a
sparse fan reads far better than a crowded one; the weaker rest collapse into one
``…`` marker (which lights up as whichever collapsed row the list highlights).
* the **canvas** — the majority of the screen, so the shape stays legible — anchors the
focus at the far **west** with its name to its right, then fans a deliberately sparse
spread of its strongest neighbours across the width to the east, each named just to the
right of its marker. Every marker is drawn in its own **key-derived hue** (the app-wide
per-node colour, keyed on the node's key), never the node *type* — the glyph *shape*
still carries the type. Edges are braille lines coloured by the link's median SNR
(green → amber → red, slate for links with no reading) and faded by evidence age; they
leave the *right end of the focus name* (and land on the right end of the came-from
name) so a connecting line never crosses a label. The node the trail came from ducks
in just west of the focus and lower, so walking always reads as moving right and backing
up as moving left. Only as many neighbours as the canvas area can carry are drawn — a
sparse fan reads far better than a crowded one; the weaker rest, ranked by observed
strength (SNR, sample count, and recency folded together, so a strong-but-long-stale
link is not promoted over a fresher one), collapse into one ``…`` marker (which lights
up as whichever collapsed row the list highlights).
* the **link list** beneath names every neighbour as a selectable row, strongest
observed link first: type glyph, name, hash, SNR with a quality bar, the evidence
behind the link (samples, sources, age), and how many links continue onward from
@@ -58,7 +64,7 @@ from ..services.topology import Link, MeshTopology
from .map_render import _NODE, _REPEATER, _SELF, _UNKNOWN
from .mapcanvas import RGB, MapCanvas, parse_hex
from .menus import fit_cells
from .theme import name_style, snr_style
from .theme import name_style, node_style, snr_style
from .trace_screen import snr_bar
from .tui.render import render_to_ansi
from .tui.screen import ListWindow, Screen
@@ -86,9 +92,9 @@ _LIST_MIN_ROWS = 3
#: The widest any on-canvas label renders before it ellipsizes — a generous cap that most
#: real node names clear whole. It is only an upper bound: each label is *also* clamped to
#: the cells actually free between its marker and the canvas edge (see :meth:`_label_right`,
#: :meth:`_label_left`, :meth:`_place_label`), so a name loses letters only when it genuinely
#: won't fit, not to a fixed short budget — the fan's placement leaves most markers far more
#: room than the old flat cap allowed.
#: :meth:`_focus_anchor`, :meth:`_place_label`), so a name loses letters only when it
#: genuinely won't fit, not to a fixed short budget — the fan's placement leaves most
#: markers far more room than the old flat cap allowed.
_LABEL_W = 22
#: Kept as the *fan* label cap for symmetry with the focus/selection one; both now defer to
@@ -436,12 +442,17 @@ class AtlasScreen(Screen):
return title
def _header_lines(self, width: int, depths: dict[str, int]) -> list[str]:
"""The breadcrumb trail (when walking) and the focus node's identity line."""
out: list[str] = []
if len(self._trail) > 1:
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
"""The breadcrumb trail and the focus node's identity line.
The trail draws always — even at the root, where it is just our own node — so the
breadcrumb is a constant fixture and the header keeps a steady height whether you
have walked or not, rather than the whole body shifting up a row the moment you take
the first step.
"""
return [
render_to_ansi(self._trail_text(width), width, no_wrap=True),
render_to_ansi(self._focus_line(depths), width, no_wrap=True),
]
def _trail_text(self, width: int) -> Text:
"""The breadcrumb trail, each name in its own key-derived hue, tail-anchored.
@@ -484,9 +495,11 @@ class AtlasScreen(Screen):
def _focus_line(self, depths: dict[str, int]) -> Text:
"""Who is in focus: glyph, name with its parenthesized hash, distance, and recency.
The leading glyph already carries the node type, so the line no longer spells it
out; the name reads ``name (hash)`` — the addressed path-hash in parentheses, its
digits lit in the node's hue — rather than a bare slice of the key.
The leading glyph carries the node type as its *shape* and the node's own key hue
as its *colour* (:meth:`_glyph_style`, matching the canvas marker), so the line no
longer spells the type out; the name reads ``name (hash)`` — the addressed
path-hash in parentheses, its digits lit in the node's hue — rather than a bare
slice of the key.
The ``N hops out`` distance is the *shortest* observed path from us to this node
(a BFS over the whole evidence graph, :meth:`_hops_out`) — the node's ring in the
@@ -498,10 +511,10 @@ class AtlasScreen(Screen):
not a miscount.
"""
node = self._focus
glyph, color = self._glyph(node)
glyph, _type_color = self._glyph(node)
contact = self._contacts.get(node)
line = Text()
line.append(glyph, style=color)
line.append(glyph, style=self._glyph_style(node))
line.append(" ")
line.append(self._label(node), style=self._list_name_style(node))
short = self._short_hash(node)
@@ -539,18 +552,29 @@ class AtlasScreen(Screen):
def _canvas_lines(
self, width: int, canvas_h: int, selected: Optional[str]
) -> list[str]:
"""Draw the focus neighbourhood: focus at the left, the strongest fan east.
"""Draw the focus neighbourhood: focus at the far west, the strongest fan east.
The focus icon sits at the far west with its name to its right; the fan spreads
east across the available width (see :meth:`_place_neighbours`), each neighbour
named to the right of its marker. Every marker takes its own **key-derived hue**
(:meth:`_marker_rgb`) rather than a node-*type* colour — the glyph *shape* carries
the type — and no marker is whited out, not even the selection: the highlighted row
is shown by its *label* going white and by the lit route, so the icons stay
colourful throughout.
Only as many neighbours as the area can carry get their own marker (see
:meth:`_fan_capacity`); the weaker rest collapse into one ``…`` marker at
the fan's foot. Highlighting a collapsed row from the list lights that
marker white and swaps its label for the highlighted node's name, so the
selection is always somewhere on the picture. The edges tracing the *route to
the selected link* — that link plus the approach the walk took into the focus —
draw brightest and undimmed, lighting the whole path that reaches it.
:meth:`_fan_capacity`); the weaker rest collapse into one ``…`` marker at the fan's
foot. Highlighting a collapsed row from the list lights that marker white and swaps
its label for the highlighted node's name, so the selection is always somewhere on
the picture. Every edge leaves the *right end of the focus name* (and lands on the
right end of the came-from name) so a connecting line never crosses a label; the
edges tracing the *route to the selected link* — that link plus the approach the
walk took into the focus — draw brightest and undimmed, lighting the whole path that
reaches it.
"""
canvas = MapCanvas(width, canvas_h)
fx, fy = self._focus_pos(width, canvas_h)
ax, ay, focus_name = self._focus_anchor(width, canvas_h)
pairs = self._links_of(self._focus)
by_other = dict(pairs)
@@ -563,19 +587,31 @@ class AtlasScreen(Screen):
shown, hidden = fan, []
placed = self._place_neighbours(width, canvas_h, shown, back, bool(hidden))
# The came-from node keeps its icon on the west with its name to the right, but its
# edge home attaches at the *right end of that name* — like the focus — so the line
# never crosses the label. Work that attach point out before drawing edges.
back_attach: Optional[tuple[int, int]] = None
if back is not None and back in placed:
bx, by = placed[back]
back_name = self._clip(self._label(back), width - ((bx >> 1) + 2))
back_attach = (((bx >> 1) + 2 + cell_len(back_name) + 1) * 2, by)
# Edges first (markers and labels overprint them), coloured by SNR and faded by
# evidence age. The *route to the selected link* draws brightest and on top: the
# selected neighbour's own edge, plus the approach the walk took into the focus
# (came_from → focus) — so selecting a link lights the whole path that reaches it,
# west through the focus to east, not just the one hop. Route edges shed the age
# fade (drawn full-strength) so they read as one lit thread over the dimmer rest;
# the age is still legible in the row's age column. The collapsed marker's edge is
# slate — unless the selection hides in it, when it joins the route.
# evidence age. Every edge leaves the focus's name-end anchor; a fan edge lands on
# its marker, the came-from edge on its own name-end. The *route to the selected
# link* draws brightest and on top: the selected neighbour's own edge, plus the
# approach the walk took into the focus (came_from → focus) — so selecting a link
# lights the whole path that reaches it, west through the focus to east, not just the
# one hop. Route edges shed the age fade (drawn full-strength) so they read as one
# lit thread over the dimmer rest; the age is still legible in the row's age column.
# The collapsed marker's edge is slate — unless the selection hides in it, when it
# joins the route.
now = utcnow()
route = {selected} if selected is not None else set()
if selected is not None and back is not None and back != selected:
route.add(back) # the last walked step, drawn as part of the lit route
for other, (x, y) in placed.items():
dest = back_attach if (other == back and back_attach is not None) else (x, y)
if other == _MORE:
if selected in hidden:
link = by_other[selected]
@@ -592,19 +628,20 @@ class AtlasScreen(Screen):
else:
color = _scaled(_snr_rgb(link.median_snr), _freshness(link.last_seen, now))
priority = 2
canvas.draw_line([(fx, fy), (x, y)], color, priority)
canvas.draw_line([(ax, ay), dest], color, priority)
# The focus marker, its label to the LEFT — the walker reads left-to-right,
# so the focus name never sits in the fan's way.
glyph, color_hex = self._glyph(self._focus)
canvas.marker(fx, fy, glyph, parse_hex(color_hex))
self._label_left(canvas, fx, fy, self._label(self._focus), (255, 255, 255))
# The focus marker at the far west, its name to the RIGHT — icon left, name right,
# the walk's reading way — both in the node's key hue (ours white). The edges have
# already left the name-end, so they run east clear of the label rather than through
# it.
glyph, _type_color = self._glyph(self._focus)
canvas.marker(fx, fy, glyph, self._marker_rgb(self._focus))
self._label_right(canvas, fx, fy, focus_name, self._label_rgb(self._focus))
# Markers first, then labels. The selection keeps the smart two-sided
# placement (it reads well as-is); every other node is named to the RIGHT
# of its icon, the reading direction of the walk. Labels are laid
# most-important-first (selection, then the trail-back node, then strongest
# links) so the collision check drops the least important where two would
# Markers first, then labels. Every marker keeps its key hue — the selection is
# shown by its white *label* and the lit route, never by whiting out the icon.
# Labels are laid most-important-first (selection, then the trail-back node, then
# strongest links) so the collision check drops the least important where two would
# overprint.
white = (255, 255, 255)
for other, (x, y) in placed.items():
@@ -612,8 +649,8 @@ class AtlasScreen(Screen):
rgb = white if selected in hidden else parse_hex(_UNKNOWN[1])
canvas.marker(x, y, "…", rgb)
continue
glyph, color_hex = self._glyph(other)
canvas.marker(x, y, glyph, white if other == selected else parse_hex(color_hex))
glyph, _type_color = self._glyph(other)
canvas.marker(x, y, glyph, self._marker_rgb(other))
if selected is not None and selected in placed:
x, y = placed[selected]
self._place_label(canvas, x, y, self._label(selected), white)
@@ -621,7 +658,7 @@ class AtlasScreen(Screen):
rightward += [n for n in shown if n != selected and n != back]
for other in rightward:
x, y = placed[other]
self._label_right(canvas, x, y, self._label(other), parse_hex(self._glyph(other)[1]))
self._label_right(canvas, x, y, self._label(other), self._label_rgb(other))
if _MORE in placed:
x, y = placed[_MORE]
if selected in hidden:
@@ -642,11 +679,30 @@ class AtlasScreen(Screen):
return max(3, span // _FAN_SLOT_DOTS + 1)
def _focus_pos(self, width: int, canvas_h: int) -> tuple[int, int]:
"""The focus marker's dot position: left of centre, room for its west label."""
label_cells = min(len(self._label(self._focus)), _LABEL_W)
"""The focus marker's dot position: near the west edge, name and fan to its east.
The icon hugs the left so its name (drawn to the right) and the neighbour fan get
the whole width to spread across — the graph aired out rather than squeezed into
the middle; the came-from node ducks in just west of it and lower (see
:meth:`_place_neighbours`).
"""
dot_w = width * 2
x = max((label_cells + 3) * 2, dot_w // 5)
return min(x, dot_w // 3), (canvas_h * 4) // 2
x = max(4, dot_w // 12)
return x, (canvas_h * 4) // 2
def _focus_anchor(self, width: int, canvas_h: int) -> tuple[int, int, str]:
"""The focus's edge-attach point and its on-canvas name.
The focus icon sits at the far west (:meth:`_focus_pos`) with its name to the
*right*; every edge leaves the **right end of that name** so the connecting lines
never cross the label. Returns the attach point in dot coordinates and the name as
it is drawn (clipped to the room between the icon and the canvas edge).
"""
fx, fy = self._focus_pos(width, canvas_h)
icon_cx = fx >> 1
name = self._clip(self._label(self._focus), width - (icon_cx + 2))
anchor_cx = icon_cx + 2 + cell_len(name) + 1
return anchor_cx * 2, fy, name
def _place_neighbours(
self,
@@ -658,32 +714,33 @@ class AtlasScreen(Screen):
) -> dict[str, tuple[int, int]]:
"""Dot-space positions for the drawn neighbourhood.
The trail-back node (when among the neighbours) anchors at the far west,
a couple of rows below the focus — the focus's own label owns the row to
its left, so the back node ducks under it and keeps its label eastward;
everyone shown fans across the arc east of the focus, strongest link at
the top, weakest at the bottom — the same order as the list below, so the
picture and the rows correspond — with the collapsed ``…`` marker (keyed
:data:`_MORE`) taking the fan's last slot. A small fan uses proportionally
less of the arc, so two neighbours sit near due east rather than at
opposite rims.
The trail-back node (when among the neighbours) ducks in just west of the focus
and lower — its icon stays on the west side, under the focus, with its name to the
right; everyone shown fans across the arc east of the focus's name-end anchor,
strongest link at the top, weakest at the bottom — the same order as the list
below, so the picture and the rows correspond — with the collapsed ``…`` marker
(keyed :data:`_MORE`) taking the fan's last slot. Anchoring the fan at the name-end
(rather than at the icon) hands it the whole width east of the focus label to
breathe in. A small fan uses proportionally less of the arc, so two neighbours sit
near due east rather than at opposite rims.
"""
dot_w, dot_h = width * 2, canvas_h * 4
fx, fy = self._focus_pos(width, canvas_h)
ax, ay, _name = self._focus_anchor(width, canvas_h)
placed: dict[str, tuple[int, int]] = {}
if back is not None:
placed[back] = (4, min(fy + 8, dot_h - 4))
placed[back] = (max(0, fx - 6), min(fy + 14, dot_h - 4))
slots = len(shown) + (1 if more else 0)
if not slots:
return placed
rx = max(10.0, dot_w - _PAD_X_DOTS - fx)
rx = max(10.0, dot_w - _PAD_X_DOTS - ax)
ry = max(4.0, dot_h / 2.0 - _PAD_Y_DOTS)
phi = _FAN_HALF_ANGLE * min(1.0, (slots - 1) / 5.0)
keys = list(shown) + ([_MORE] if more else [])
for i, node in enumerate(keys):
angle = 0.0 if slots == 1 else -phi + (2 * phi) * i / (slots - 1)
x = fx + rx * math.cos(angle)
y = fy + ry * math.sin(angle)
x = ax + rx * math.cos(angle)
y = ay + ry * math.sin(angle)
placed[node] = (round(x), round(y))
return placed
@@ -741,31 +798,17 @@ class AtlasScreen(Screen):
return
canvas._place_run(start, cy, label, rgb, bold=True)
def _label_left(
self, canvas: MapCanvas, x: int, y: int, label: str, rgb: RGB
) -> None:
"""Place the focus label to the *left* of its marker, forced if it must be.
The focus is drawn first, so a collision is rare (a trail-back label at
most); after the dodge rows are exhausted the label is stamped anyway —
the focus must always be named. The name is clamped to the cells free to the
marker's left (column 0 onward).
"""
cx, cy = x >> 1, y >> 2
label = self._clip(label, cx - 1)
if not label:
return
start = cx - 1 - len(label)
for dy in (0, 1, -1):
if canvas._place_run(start, cy + dy, label, rgb, bold=True, checked=True):
return
canvas._place_run(start, cy, label, rgb, bold=True)
def _legend(self) -> Text:
"""The one-line glyph legend and edge key under the canvas."""
"""The one-line glyph legend and edge key under the canvas.
A *shape* key: on the canvas the glyph carries the node type while its colour
carries the node's identity (its key hue), so the role glyphs here are drawn muted
rather than in a type colour that would falsely read as "repeaters are violet". Our
own node keeps the white ``you`` mark it wears on the canvas.
"""
legend = Text()
for glyph, color in (_SELF, _REPEATER, _NODE, _UNKNOWN):
legend.append(glyph, style=color)
for glyph, _type_color in (_SELF, _REPEATER, _NODE, _UNKNOWN):
legend.append(glyph, style="you" if glyph == _SELF[0] else "muted")
legend.append(
{"★": " you ", "▲": " repeater ", "●": " node ", "○": " unknown"}[glyph],
style="muted",
@@ -935,6 +978,52 @@ class AtlasScreen(Screen):
return "muted"
return name_style(label, node)
def _marker_rgb(self, node: str) -> RGB:
"""The marker colour for a node: its key-derived hue, ours white, a keyless id grey.
The atlas colours every marker by *identity* — the app-wide per-node hue
(:func:`~meshterm.ui.theme.node_style`, keyed on the node's own key) — rather than
by node *type*; the glyph *shape* (see :meth:`_glyph`) is what carries the type. Our
own node keeps the pure-white ``you`` convention, and a non-hex placeholder id (our
own ``"local"`` before a key is known) has no hue to derive, so it falls back to the
unknown grey.
"""
if node == self._topo.self_id:
return (255, 255, 255)
raw = node.lower()
if not raw or any(c not in "0123456789abcdef" for c in raw):
return parse_hex(_UNKNOWN[1])
return parse_hex(node_style(node).rsplit("#", 1)[-1])
def _label_rgb(self, node: str) -> RGB:
"""The label colour for a node on the canvas: its name hue, ours white, a bare hash muted.
The RGB counterpart of :meth:`_list_name_style`: a named node's key hue, our own
node white, and a node known only by a bare hash muted (a key standing in as a name
is never itself coloured, so the marker carries the identity and the hash-label
stays grey).
"""
style = self._list_name_style(node)
if style == "you":
return (255, 255, 255)
if style == "muted":
return parse_hex("#94a3b8")
return parse_hex(style.rsplit("#", 1)[-1])
def _glyph_style(self, node: str) -> str:
"""The Rich style a node's type glyph takes in body text: its key hue (ours white).
The text-side companion of :meth:`_marker_rgb`, so the focus line's leading glyph
matches its canvas marker — key-hued for a real node, the white ``you`` for us, and
muted for a keyless placeholder id.
"""
if node == self._topo.self_id:
return "you"
raw = node.lower()
if not raw or any(c not in "0123456789abcdef" for c in raw):
return "muted"
return node_style(node)
def _empty_state(self, width: int) -> list[str]:
"""A friendly explanation while the evidence graph is still empty."""
lines = [