From 8fdd7572bd7209b0f834bb3de2ddefb81135e1d7 Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Sun, 26 Jul 2026 18:12:46 -0400 Subject: [PATCH] Message paths: hold the fan's shape still across the selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dialog ranked layers by selection (priority 3 for the picked path, 2 for the rest), so every ↑↓ re-elected the spine and re-laid the whole graph out. Rank by first-heard order instead and move `emphasis`, the way the node detail's Routes tab already does — the geometry is fixed by evidence, only the white lane moves. Co-Authored-By: Claude Opus 5 (1M context) --- meshterm/ui/message_paths_screen.py | 11 +++++++++-- tests/test_message_paths_screen.py | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/meshterm/ui/message_paths_screen.py b/meshterm/ui/message_paths_screen.py index b9fd50f..2cffc13 100644 --- a/meshterm/ui/message_paths_screen.py +++ b/meshterm/ui/message_paths_screen.py @@ -278,15 +278,22 @@ class MessagePathsScreen(Screen): (:func:`~meshterm.ui.widgets.route_graph_style`): endpoints named, relays their own map marker where the type is known, each labelled by its first hash byte in the node's own hue. + + Layout rank is fixed by first-heard order (the first path heard is always the spine), + exactly as the node detail's Routes tab fixes it by evidence order, so the fan's + geometry holds still as ↑↓ walk the arrivals — only the emphasis (which lane draws + white and on top) follows the pick. """ selected = self._arrivals[self._index].hops + paths = self._paths() layers = [ PathLayer( hops=path, color=_EDGE_SELECTED if path == selected else _EDGE_UNUSED, - priority=3 if path == selected else 2, + priority=len(paths) - i, + emphasis=1 if path == selected else 0, ) - for path in self._paths() + for i, path in enumerate(paths) ] glyph_of, label_of, label_rgb_of = route_graph_style( resolve=self._resolve, self_name=self._self_name, source=self._source, diff --git a/tests/test_message_paths_screen.py b/tests/test_message_paths_screen.py index 7421ef4..ed559bb 100644 --- a/tests/test_message_paths_screen.py +++ b/tests/test_message_paths_screen.py @@ -96,6 +96,27 @@ def test_paths_screen_draws_selected_path_white_over_gray() -> None: assert "38;2;255;255;255" in raw2 and "38;2;110;110;110" in raw2 +def test_paths_screen_graph_geometry_holds_still_across_the_selection() -> None: + """↑↓ repaint the fan's colours, never its shape — the Routes tab's rule. + + Layout rank is first-heard order, so only ``emphasis`` follows the pick; the drawn + glyphs/lanes are byte-identical from every selection. + """ + now = utcnow() + arrivals = [ + Arrival(when=now, hops=("3d63",), snr=4.0), + Arrival(when=now + timedelta(seconds=2), hops=("a1b2", "77aa"), snr=-2.0), + Arrival(when=now + timedelta(seconds=4), hops=("a1b2", "5c5c", "77aa"), snr=1.0), + ] + screen = _screen(arrivals) + shapes = [] + for _ in range(len(arrivals)): + shapes.append(_plain(screen._graph_lines(76))) + screen.handle("down") + assert len(set(shapes)) == 1, "the graph's shape moved when the selection did" + assert shapes[0].count("\n") > 1 # a genuine multi-lane fan, not one flat line + + def test_paths_screen_marks_a_repeater_relay_with_its_triangle() -> None: """A relay whose type resolves to a repeater draws ▲, not the generic dot.""" screen = _screen(_arrivals(), type_of=lambda h: 2 if h == "3d63" else None)