Atlas: focus-left canvas that earns its area, list that scrolls in place
The graph now takes the majority of the screen (tapering a little on tall terminals), draws only as many neighbours as its area can carry — strongest links first, the weaker rest collapsed into one … marker that lights up as whichever hidden row the list highlights — and lays the walk out left-to-right: focus toward the left with its label west of the marker, trail-back ducked under it at the far edge, the fan and its labels east. The link list beneath scrolls inside the fixed screen: canvas, legend, and heading hold still, faint ↑/↓ n more markers bracket the window, and PgUp/PgDn stride by a windowful. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8330c5a5db
commit
2dca52fa56
2 files changed
+345
-120
No files matched your search
+270
-115
@@ -10,23 +10,28 @@ 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** draws the focus at the centre with its direct neighbours fanned around
|
||||
it, 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 to the **west**, so walking always reads as moving right and backing
|
||||
up as moving left. A dozen markers at most — never a tangle.
|
||||
* the **link list** beneath repeats those neighbours as selectable rows, strongest
|
||||
* the **canvas** — the majority of the screen, so the shape stays legible — draws the
|
||||
focus toward the left (its label to the left of its marker) with its strongest
|
||||
neighbours fanned to the right, labels rightward, 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; the weaker rest 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
|
||||
that node. The highlighted row's marker and label light white on the canvas.
|
||||
that node. The highlighted row's marker and label light white on the canvas. The
|
||||
list scrolls *within* the screen — the canvas, legend, and heading hold still, and
|
||||
faint ``↑/↓ n more`` markers bracket the window — with ↑↓ moving one row and
|
||||
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
|
||||
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. PgUp/PgDn scroll when a hub
|
||||
node's list outgrows the viewport.
|
||||
from storage, and Esc peels find first, the screen second.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -62,22 +67,30 @@ if TYPE_CHECKING:
|
||||
_PAD_X_DOTS = 18
|
||||
_PAD_Y_DOTS = 6
|
||||
|
||||
#: The canvas's height in character rows: enough to read the fan's shape, never so
|
||||
#: tall it starves the link list (the body scrolls, but the list should open visible).
|
||||
#: The canvas's floor in character rows: below this the fan's shape stops reading.
|
||||
_CANVAS_MIN_H = 6
|
||||
_CANVAS_MAX_H = 12
|
||||
|
||||
#: Rows the link list always keeps for itself under the canvas, however tall the
|
||||
#: graph would like to be — a windowed list needs at least a few rows to scroll in.
|
||||
_LIST_MIN_ROWS = 3
|
||||
|
||||
#: The widest a node label may render on the canvas before it is ellipsized.
|
||||
_LABEL_W = 16
|
||||
|
||||
#: How many canvas labels are offered beyond the always-on ones (focus, selection,
|
||||
#: trail-back). A busy hub keeps its markers but drops the excess labels — the list
|
||||
#: below names every row anyway.
|
||||
_LABEL_BUDGET = 8
|
||||
#: The fan's angular reach on each side of due east, in radians. The whole fan stays
|
||||
#: east of the focus — neighbours to the right, labels rightward — and a smaller
|
||||
#: neighbourhood uses proportionally less of the arc so two nodes never sit at its
|
||||
#: extremes with nothing between them.
|
||||
_FAN_HALF_ANGLE = math.radians(72)
|
||||
|
||||
#: The fan's angular reach on each side of due east, in radians. The west wedge is
|
||||
#: reserved for the trail-back node, so the fan never overprints it.
|
||||
_FAN_HALF_ANGLE = math.radians(130)
|
||||
#: Vertical dot spacing one fan marker (and its possible label row) needs; the canvas
|
||||
#: area divided by this is how many neighbours the graph can carry before the weaker
|
||||
#: rest collapse into the one ``…`` marker.
|
||||
_FAN_SLOT_DOTS = 6
|
||||
|
||||
#: Sentinel key for the collapsed weaker-links marker in the placed-node map. NUL can
|
||||
#: never collide with a canonical id (those are hex).
|
||||
_MORE = "\x00more"
|
||||
|
||||
#: How many find matches the list shows at most (the filter narrows it fast).
|
||||
_MAX_MATCHES = 10
|
||||
@@ -181,7 +194,11 @@ class AtlasScreen(Screen):
|
||||
#: The live find-as-you-type filter ("" = off; matches every node known).
|
||||
self._filter = ""
|
||||
self._needs_scrub = True # braille smear scrub, exactly like the map
|
||||
self._cursor: Optional[int] = None
|
||||
#: First list row the internal window shows (the list scrolls, the screen
|
||||
#: doesn't), and how many rows that window carried on the last paint — the
|
||||
#: stride a PgUp/PgDn moves the highlight by.
|
||||
self._list_top = 0
|
||||
self._list_page = 6
|
||||
|
||||
# --- state -------------------------------------------------------------------
|
||||
|
||||
@@ -277,12 +294,11 @@ class AtlasScreen(Screen):
|
||||
elif action == "down" and rows:
|
||||
self._index = (self._index + 1) % len(rows)
|
||||
elif action == "pageup" and rows:
|
||||
# The highlight pages (the dashboard feed's behaviour): a row is always
|
||||
# selected here, and the cursor pin keeps it in view, so paging the view
|
||||
# without the highlight would just snap straight back.
|
||||
self._index = max(0, self._index - self._page_step)
|
||||
# The highlight pages by one list windowful: the window follows the
|
||||
# highlight, so paging the view without it would just snap straight back.
|
||||
self._index = max(0, self._index - self._list_page)
|
||||
elif action == "pagedown" and rows:
|
||||
self._index = min(len(rows) - 1, self._index + self._page_step)
|
||||
self._index = min(len(rows) - 1, self._index + self._list_page)
|
||||
elif action == "enter":
|
||||
self._walk(rows)
|
||||
elif action == "backspace":
|
||||
@@ -344,30 +360,52 @@ class AtlasScreen(Screen):
|
||||
# --- rendering -----------------------------------------------------------------
|
||||
|
||||
def render_body(self, width: int) -> list[str]:
|
||||
"""Render the trail, the focus line, the canvas, and the link list."""
|
||||
"""Render the trail, the focus line, the canvas, and the windowed link list.
|
||||
|
||||
The body is laid out to fit the frame's viewport exactly: the canvas takes
|
||||
the majority of the rows (a touch less of the share on tall terminals), the
|
||||
chrome around it holds still, and whatever remains is the link list's
|
||||
window — only its rows scroll, inside :meth:`_list_lines`.
|
||||
"""
|
||||
links = self._topo.links()
|
||||
rows = self._rows()
|
||||
self._index = max(0, min(self._index, len(rows) - 1)) if rows else 0
|
||||
self.title = self._compose_title(links)
|
||||
self._cursor = None
|
||||
if not links:
|
||||
return self._empty_state(width)
|
||||
|
||||
depths = self._hops_out()
|
||||
selected = rows[self._index] if rows else None
|
||||
_, viewport = self._session.base_body_size()
|
||||
|
||||
lines: list[str] = []
|
||||
lines.extend(self._header_lines(width, depths))
|
||||
lines.extend(self._canvas_lines(width, selected))
|
||||
header = self._header_lines(width, depths)
|
||||
chrome = len(header) + 3 # legend, blank, list heading
|
||||
canvas_h = self._canvas_height(viewport, chrome)
|
||||
list_win = max(1, viewport - chrome - canvas_h)
|
||||
|
||||
lines: list[str] = list(header)
|
||||
lines.extend(self._canvas_lines(width, canvas_h, selected))
|
||||
lines.append(render_to_ansi(self._legend(), width, no_wrap=True))
|
||||
lines.append("")
|
||||
lines.extend(self._list_lines(width, rows, depths))
|
||||
lines.extend(self._list_lines(width, rows, depths, list_win))
|
||||
self._scroll_total = max(1, len(lines))
|
||||
return lines
|
||||
|
||||
def cursor_line(self) -> Optional[int]:
|
||||
"""The highlighted list row, so the session keeps it in view."""
|
||||
return self._cursor
|
||||
def _canvas_height(self, viewport: int, chrome: int) -> int:
|
||||
"""Rows the canvas takes: the majority of the screen, ceded where pointless.
|
||||
|
||||
The share starts at ~62% of the viewport and tapers toward half on tall
|
||||
terminals (a huge graph area buys little once the fan is legible, while the
|
||||
list keeps earning rows). A sparse neighbourhood caps it lower — a two-node
|
||||
link needs no half-screen void — and the link list always keeps its
|
||||
:data:`_LIST_MIN_ROWS` under the fixed chrome.
|
||||
"""
|
||||
crowd = len(self._links_of(self._focus))
|
||||
share = 0.62 - 0.12 * min(max(viewport - 20, 0) / 24.0, 1.0)
|
||||
height = round(viewport * share)
|
||||
height = min(height, max(_CANVAS_MIN_H, 5 + 2 * crowd))
|
||||
height = min(height, viewport - chrome - _LIST_MIN_ROWS)
|
||||
return max(4, height)
|
||||
|
||||
def _compose_title(self, links: list[Link]) -> str:
|
||||
"""``Mesh atlas — focus`` plus the graph's status atoms."""
|
||||
@@ -427,94 +465,169 @@ class AtlasScreen(Screen):
|
||||
|
||||
# -- the canvas --
|
||||
|
||||
def _canvas_lines(self, width: int, selected: Optional[str]) -> list[str]:
|
||||
"""Draw the focus neighbourhood: centre marker, west trail-back, eastern fan."""
|
||||
_, cell_h = self._session.base_body_size()
|
||||
# Sized to the neighbourhood: a two-node link needs no twelve-row void, while
|
||||
# a hub earns the full fan height — always capped so the list opens visible.
|
||||
crowd = len(self._links_of(self._focus))
|
||||
canvas_h = max(_CANVAS_MIN_H, min(_CANVAS_MAX_H, cell_h - 10, 5 + crowd))
|
||||
canvas = MapCanvas(width, canvas_h)
|
||||
dot_w, dot_h = width * 2, canvas_h * 4
|
||||
cx, cy = dot_w // 2, dot_h // 2
|
||||
rx = max(10.0, dot_w / 2.0 - _PAD_X_DOTS)
|
||||
ry = max(6.0, dot_h / 2.0 - _PAD_Y_DOTS)
|
||||
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.
|
||||
|
||||
now = utcnow()
|
||||
placed = self._place_neighbours(cx, cy, rx, ry)
|
||||
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.
|
||||
"""
|
||||
canvas = MapCanvas(width, canvas_h)
|
||||
fx, fy = self._focus_pos(width, canvas_h)
|
||||
|
||||
pairs = self._links_of(self._focus)
|
||||
by_other = dict(pairs)
|
||||
back = self._came_from if self._came_from in by_other else None
|
||||
fan = [other for other, _link in pairs if other != back]
|
||||
capacity = self._fan_capacity(canvas_h)
|
||||
if len(fan) > capacity + 1: # collapsing exactly one node would save nothing
|
||||
shown, hidden = fan[:capacity], fan[capacity:]
|
||||
else:
|
||||
shown, hidden = fan, []
|
||||
placed = self._place_neighbours(width, canvas_h, shown, back, bool(hidden))
|
||||
|
||||
# Edges first (markers and labels overprint them), coloured by SNR and faded
|
||||
# by evidence age; the highlighted neighbour's edge wins its cells.
|
||||
by_other = {other: link for other, link in self._links_of(self._focus)}
|
||||
# by evidence age; the highlighted neighbour's edge wins its cells. The
|
||||
# collapsed marker's edge is slate — unless the selection hides in it, when
|
||||
# it takes the selected link's colour instead.
|
||||
now = utcnow()
|
||||
for other, (x, y) in placed.items():
|
||||
link = by_other.get(other)
|
||||
if link is None: # pragma: no cover - placed comes from the same list
|
||||
continue
|
||||
color = _scaled(_snr_rgb(link.median_snr), _freshness(link.last_seen, now))
|
||||
priority = 3 if other == selected else 2
|
||||
canvas.draw_line([(cx, cy), (x, y)], color, priority)
|
||||
if other == _MORE:
|
||||
if selected in hidden:
|
||||
link = by_other[selected]
|
||||
color = _scaled(
|
||||
_snr_rgb(link.median_snr), _freshness(link.last_seen, now)
|
||||
)
|
||||
priority = 3
|
||||
else:
|
||||
color = _scaled(_NO_READING, 0.6)
|
||||
priority = 1
|
||||
else:
|
||||
link = by_other[other]
|
||||
color = _scaled(_snr_rgb(link.median_snr), _freshness(link.last_seen, now))
|
||||
priority = 3 if other == selected else 2
|
||||
canvas.draw_line([(fx, fy), (x, y)], color, priority)
|
||||
|
||||
# The focus marker and its label, always on and always white-labelled.
|
||||
# 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(cx, cy, glyph, parse_hex(color_hex))
|
||||
self._place_label(canvas, cx, cy, self._label(self._focus), (255, 255, 255))
|
||||
canvas.marker(fx, fy, glyph, parse_hex(color_hex))
|
||||
self._label_left(canvas, fx, fy, self._label(self._focus), (255, 255, 255))
|
||||
|
||||
# Neighbour markers, then labels under a budget (selection and trail-back
|
||||
# always labelled; the rest strongest-first until the budget runs out).
|
||||
# Markers, then labels in priority order (selection first, trail-back next,
|
||||
# then strongest-first) so the collision check drops the least important.
|
||||
white = (255, 255, 255)
|
||||
for other, (x, y) in placed.items():
|
||||
if other == _MORE:
|
||||
rgb = white if selected in hidden else parse_hex(_UNKNOWN[1])
|
||||
canvas.marker(x, y, "…", rgb)
|
||||
continue
|
||||
glyph, color_hex = self._glyph(other)
|
||||
rgb = (255, 255, 255) if other == selected else parse_hex(color_hex)
|
||||
canvas.marker(x, y, glyph, rgb)
|
||||
budget = _LABEL_BUDGET
|
||||
for other, (x, y) in placed.items():
|
||||
always = other in (selected, self._came_from)
|
||||
if not always:
|
||||
if budget <= 0:
|
||||
continue
|
||||
budget -= 1
|
||||
rgb = (255, 255, 255) if other == selected else parse_hex(self._glyph(other)[1])
|
||||
canvas.marker(x, y, glyph, white if other == selected else parse_hex(color_hex))
|
||||
ordered = [n for n in (selected, back) if n is not None and n in placed]
|
||||
ordered += [n for n in shown if n not in ordered]
|
||||
for other in ordered:
|
||||
x, y = placed[other]
|
||||
rgb = white if other == selected else parse_hex(self._glyph(other)[1])
|
||||
self._place_label(canvas, x, y, self._label(other), rgb)
|
||||
if _MORE in placed:
|
||||
x, y = placed[_MORE]
|
||||
if selected in hidden:
|
||||
self._place_label(canvas, x, y, self._label(selected), white)
|
||||
else:
|
||||
self._place_label(canvas, x, y, f"+{len(hidden)} weaker", parse_hex(_UNKNOWN[1]))
|
||||
|
||||
return canvas.to_ansi_lines()
|
||||
|
||||
def _place_neighbours(
|
||||
self, cx: int, cy: int, rx: float, ry: float
|
||||
) -> dict[str, tuple[int, int]]:
|
||||
"""Dot-space positions for the focus's neighbours.
|
||||
def _fan_capacity(self, canvas_h: int) -> int:
|
||||
"""How many fan markers the canvas area carries before the rest collapse.
|
||||
|
||||
The trail-back node (when among them) anchors due west; everyone else fans
|
||||
across the eastern arc, strongest link at the top, weakest at the bottom —
|
||||
the same order as the list below, so the picture and the rows correspond.
|
||||
One marker (plus the row its label may need) wants :data:`_FAN_SLOT_DOTS`
|
||||
of the fan's vertical span; the area's height decides the count — a taller
|
||||
graph area simply shows more of the mesh.
|
||||
"""
|
||||
neighbours = [other for other, _link in self._links_of(self._focus)]
|
||||
span = canvas_h * 4 - 2 * _PAD_Y_DOTS
|
||||
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)
|
||||
dot_w = width * 2
|
||||
x = max((label_cells + 3) * 2, dot_w // 5)
|
||||
return min(x, dot_w // 3), (canvas_h * 4) // 2
|
||||
|
||||
def _place_neighbours(
|
||||
self,
|
||||
width: int,
|
||||
canvas_h: int,
|
||||
shown: list[str],
|
||||
back: Optional[str],
|
||||
more: bool,
|
||||
) -> 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.
|
||||
"""
|
||||
dot_w, dot_h = width * 2, canvas_h * 4
|
||||
fx, fy = self._focus_pos(width, canvas_h)
|
||||
placed: dict[str, tuple[int, int]] = {}
|
||||
back = self._came_from
|
||||
if back in neighbours:
|
||||
placed[back] = (round(cx - rx), cy)
|
||||
fan = [n for n in neighbours if n != back]
|
||||
else:
|
||||
fan = neighbours
|
||||
n = len(fan)
|
||||
for i, node in enumerate(fan):
|
||||
if n == 1:
|
||||
angle = 0.0
|
||||
else:
|
||||
angle = -_FAN_HALF_ANGLE + (2 * _FAN_HALF_ANGLE) * i / (n - 1)
|
||||
x = cx + rx * math.cos(angle)
|
||||
y = cy + ry * math.sin(angle)
|
||||
if back is not None:
|
||||
placed[back] = (4, min(fy + 8, 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)
|
||||
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)
|
||||
placed[node] = (round(x), round(y))
|
||||
return placed
|
||||
|
||||
def _place_label(
|
||||
self, canvas: MapCanvas, x: int, y: int, label: str, rgb: RGB
|
||||
) -> None:
|
||||
"""Place one marker label, retrying a row below then above on collision."""
|
||||
"""Place one marker label (right of the marker when it fits, else left),
|
||||
retrying a row below then above on collision."""
|
||||
if len(label) > _LABEL_W:
|
||||
label = label[: _LABEL_W - 1] + "…"
|
||||
for dy in (0, 4, -4):
|
||||
if canvas.marker_label(x, y + dy, label, rgb):
|
||||
return
|
||||
|
||||
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.
|
||||
"""
|
||||
if len(label) > _LABEL_W:
|
||||
label = label[: _LABEL_W - 1] + "…"
|
||||
cx, cy = x >> 1, y >> 2
|
||||
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."""
|
||||
legend = Text()
|
||||
@@ -530,9 +643,15 @@ class AtlasScreen(Screen):
|
||||
# -- the list --
|
||||
|
||||
def _list_lines(
|
||||
self, width: int, rows: list[str], depths: dict[str, int]
|
||||
self, width: int, rows: list[str], depths: dict[str, int], win: int
|
||||
) -> list[str]:
|
||||
"""The selectable rows: find matches, or the focus's links strongest-first."""
|
||||
"""The selectable rows, windowed to ``win`` lines under a pinned heading.
|
||||
|
||||
Only the rows scroll — the heading (and everything above it) holds still.
|
||||
When the list outgrows the window, faint ``↑/↓ n more`` markers take the
|
||||
window's edge rows and the highlight is kept inside what remains; the
|
||||
window's row count becomes the PgUp/PgDn stride.
|
||||
"""
|
||||
out: list[str] = []
|
||||
if self._filter:
|
||||
heading = Text("Matches", style="accent")
|
||||
@@ -540,28 +659,64 @@ class AtlasScreen(Screen):
|
||||
out.append(render_to_ansi(heading, width, no_wrap=True))
|
||||
if not rows:
|
||||
out.append(render_to_ansi(Text("no matches", style="muted"), width))
|
||||
for i, node in enumerate(rows):
|
||||
text = self._match_row(node, i == self._index, depths)
|
||||
if i == self._index:
|
||||
self._cursor = len(out)
|
||||
out.append(render_to_ansi(text, width, no_wrap=True))
|
||||
return out
|
||||
return out
|
||||
|
||||
heading = Text("Links", style="accent")
|
||||
heading.append(" · strongest observed first · Enter walks", style="muted")
|
||||
out.append(render_to_ansi(heading, width, no_wrap=True))
|
||||
pairs = self._links_of(self._focus)
|
||||
if not pairs:
|
||||
note = Text("no observed links from here — type to find another node", style="muted")
|
||||
out.append(render_to_ansi(note, width))
|
||||
onward = self._onward_counts(pairs)
|
||||
for i, (other, link) in enumerate(pairs):
|
||||
text = self._link_row(other, link, i == self._index, onward.get(other, 0))
|
||||
if i == self._index:
|
||||
self._cursor = len(out)
|
||||
out.append(render_to_ansi(text, width, no_wrap=True))
|
||||
def render(i: int) -> Text:
|
||||
return self._match_row(rows[i], i == self._index, depths)
|
||||
else:
|
||||
heading = Text("Links", style="accent")
|
||||
heading.append(" · strongest observed first · Enter walks", style="muted")
|
||||
out.append(render_to_ansi(heading, width, no_wrap=True))
|
||||
pairs = self._links_of(self._focus)
|
||||
if not pairs:
|
||||
note = Text(
|
||||
"no observed links from here — type to find another node", style="muted"
|
||||
)
|
||||
out.append(render_to_ansi(note, width))
|
||||
return out
|
||||
onward = self._onward_counts(pairs)
|
||||
|
||||
def render(i: int) -> Text:
|
||||
other, link = pairs[i]
|
||||
return self._link_row(other, link, i == self._index, onward.get(other, 0))
|
||||
|
||||
top, count = self._window(len(rows), win)
|
||||
if top > 0:
|
||||
out.append(render_to_ansi(Text(f" ↑ {top} more", style="faint"), width))
|
||||
for i in range(top, top + count):
|
||||
out.append(render_to_ansi(render(i), width, no_wrap=True))
|
||||
below = len(rows) - top - count
|
||||
if below > 0:
|
||||
out.append(render_to_ansi(Text(f" ↓ {below} more", style="faint"), width))
|
||||
return out
|
||||
|
||||
def _window(self, n: int, win: int) -> tuple[int, int]:
|
||||
"""The list window: ``(first row, row count)`` keeping the highlight inside.
|
||||
|
||||
The ``↑/↓ n more`` markers eat the window's edge rows exactly when there are
|
||||
hidden rows on that side, so the content capacity shifts as the window slides;
|
||||
a couple of passes settles top, capacity, and the highlight clamp together.
|
||||
The settled capacity is remembered as the PgUp/PgDn stride.
|
||||
"""
|
||||
if n <= win:
|
||||
self._list_top = 0
|
||||
self._list_page = max(1, win)
|
||||
return 0, n
|
||||
top = max(0, min(self._list_top, n - 1))
|
||||
count = 1
|
||||
for _ in range(4):
|
||||
above = 1 if top > 0 else 0
|
||||
below = 1 if n - top > win - above else 0
|
||||
count = max(1, win - above - below)
|
||||
if self._index < top:
|
||||
top = self._index
|
||||
elif self._index >= top + count:
|
||||
top = self._index - count + 1
|
||||
top = max(0, min(top, n - count))
|
||||
self._list_top = top
|
||||
self._list_page = count
|
||||
return top, count
|
||||
|
||||
def _onward_counts(self, pairs: list[tuple[str, Link]]) -> dict[str, int]:
|
||||
"""How many links continue from each neighbour, the one back here excluded."""
|
||||
counts: dict[str, int] = {}
|
||||
|
||||
+75
-5
@@ -146,16 +146,86 @@ def test_atlas_home_refocuses_us() -> None:
|
||||
assert screen._trail == [topo.self_id]
|
||||
|
||||
|
||||
def test_atlas_came_from_anchors_west_on_the_canvas() -> None:
|
||||
"""The node the trail arrived from sits due west of the focus."""
|
||||
def test_atlas_came_from_anchors_west_and_the_fan_stays_east() -> None:
|
||||
"""The trail-back node sits at the far west; every fan node east of the focus."""
|
||||
topo = _topo()
|
||||
screen = _screen(topo)
|
||||
screen.render_body(80)
|
||||
screen.handle("enter") # focus YUL; we came from us
|
||||
placed = screen._place_neighbours(100, 40, 60.0, 30.0)
|
||||
assert placed[topo.self_id] == (40, 40) # cx - rx, level with the centre
|
||||
alice = topo.canonical(ALICE.public_key)
|
||||
assert placed[alice][0] > 100 # everyone else fans east
|
||||
fx, fy = screen._focus_pos(80, 12)
|
||||
placed = screen._place_neighbours(80, 12, [alice], topo.self_id, False)
|
||||
bx, by = placed[topo.self_id]
|
||||
assert bx < fx and by > fy # back home: far left, ducked under the focus label
|
||||
assert placed[alice][0] > fx # the fan is east of the focus
|
||||
assert fx <= (80 * 2) // 3 # and the focus itself leans left
|
||||
|
||||
|
||||
def _hub_topo(spokes: int) -> MeshTopology:
|
||||
"""Us at the centre of a ``spokes``-neighbour hub, strengths descending."""
|
||||
topo = MeshTopology(US, contacts=[])
|
||||
when = utcnow()
|
||||
for i in range(spokes):
|
||||
node = f"{i:02x}" * 6
|
||||
for _ in range(spokes - i): # more samples = stronger, so the order is fixed
|
||||
topo.add_walk([topo.self_id, node], snrs=[5.0], when=when, source="trace")
|
||||
return topo
|
||||
|
||||
|
||||
def test_atlas_collapses_the_weak_links_into_one_ellipsis_marker() -> None:
|
||||
"""Beyond the area's capacity, weaker neighbours fold into a single ``…`` node."""
|
||||
screen = AtlasScreen(
|
||||
session=_FakeSession(cell_h=20), topo=_hub_topo(14), contacts={}, self_label="us"
|
||||
)
|
||||
body = _plain(screen.render_body(80))
|
||||
assert "weaker" in body # the collapsed marker is labelled "+n weaker"
|
||||
canvas_part = body.split("Links")[0]
|
||||
assert "…" in canvas_part
|
||||
# The list still names every neighbour — selection is the list's job.
|
||||
assert len(screen._rows()) == 14
|
||||
|
||||
|
||||
def test_atlas_selecting_a_collapsed_row_lights_the_ellipsis_with_its_name() -> None:
|
||||
"""Highlighting a weak (collapsed) row surfaces its name at the ``…`` marker."""
|
||||
screen = AtlasScreen(
|
||||
session=_FakeSession(cell_h=20), topo=_hub_topo(14), contacts={}, self_label="us"
|
||||
)
|
||||
screen.render_body(80)
|
||||
screen._index = len(screen._rows()) - 1 # the weakest row, surely collapsed
|
||||
body = _plain(screen.render_body(80))
|
||||
weakest = screen._rows()[-1][:8]
|
||||
canvas_part = body.split("Links")[0]
|
||||
assert weakest in canvas_part # the ellipsis marker took the selection's label
|
||||
assert "weaker" not in canvas_part # ...replacing the "+n weaker" count
|
||||
|
||||
|
||||
def test_atlas_body_fits_the_viewport_and_windows_the_list() -> None:
|
||||
"""The screen never outgrows the frame; only the link list scrolls, marked."""
|
||||
screen = AtlasScreen(
|
||||
session=_FakeSession(cell_h=22), topo=_hub_topo(16), contacts={}, self_label="us"
|
||||
)
|
||||
lines = screen.render_body(80)
|
||||
assert len(lines) <= 22 # canvas + chrome + list window == the viewport
|
||||
body = _plain(lines)
|
||||
assert "↓" in body and "more" in body # the window marks the rows below
|
||||
assert "Links" in body
|
||||
|
||||
|
||||
def test_atlas_pgdn_pages_the_highlight_by_the_list_window() -> None:
|
||||
"""PgUp/PgDn stride by the list window, and the window follows the highlight."""
|
||||
screen = AtlasScreen(
|
||||
session=_FakeSession(cell_h=22), topo=_hub_topo(16), contacts={}, self_label="us"
|
||||
)
|
||||
screen.render_body(80)
|
||||
stride = screen._list_page
|
||||
assert stride >= 1
|
||||
screen.handle("pagedown")
|
||||
assert screen._index == min(15, stride)
|
||||
for _ in range(6):
|
||||
screen.handle("pagedown")
|
||||
assert screen._index == 15 # clamped at the last row
|
||||
body = _plain(screen.render_body(80))
|
||||
assert "↑" in body and "more" in body # rows scrolled off above are counted
|
||||
|
||||
|
||||
# --- find ---------------------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user