Trace actions: the icon column is measured, not assumed

Every action row led with its mark and a hard space, which only aligns
while every mark is one cell wide. ⚡ is an emoji — two — so Explore's
label sat a column right of Compose's, Trace's, and the rest.

The marks now go through a measured icon column: each pads out to the
widest of them, so the labels share one column whatever they lead with.
Measured rather than fixed at two because the marks change width with the
platform — on PicoCalc every icon funnels through theme.glyph to a single
console-font character, so the column tightens to one cell and the labels
move left with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5 committed 2026-08-10 17:01:42 -04:00
1 parent 4c7fa8f812
commit ab63a83bbc
2 files changed
+53 -7

No files matched your search

+36 -7
View File
@@ -97,7 +97,7 @@ from ..services.records import first_repeated_edge
from ..services.topology import render_forced_spec
from .braillechart import meter
from .menus import back_rows, section_heading
from .theme import snr_style
from .theme import glyph, snr_style
from .tui.render import render_lines, render_to_ansi
from .tui.screen import ListWindow, Screen
from .tui.spinner import Spinner, spinner_interval
@@ -149,6 +149,35 @@ _ROUTE_LANE = "route "
#: spec starts, and hangs, in the same column as the numbers above it.
_PATH_LANE = "path "
#: Every mark the action rows lead with, in row order. The list exists to *measure*
#: the icon column: ``⚡`` is an emoji, two cells where the rest are one, so a hard
#: ``"icon "`` prefix would start Explore's label a column right of every other
#: action's. See :func:`_icon_lane`.
_ACTION_ICONS = ("✎", "⇄", "⚡", "⚙", "#", "▶")
def _icon_lane() -> int:
"""The action rows' icon column, in cells: the widest mark on this platform.
Measured rather than hard-coded because the marks themselves change width with
the platform: on PicoCalc every icon funnels through :func:`~meshterm.ui.theme.glyph`
to a single console-font character (``⚡`` → ``!``), so the column tightens to one
cell and the labels move left with it — the same alignment rule at the console's
own resolution.
"""
return max(cell_len(glyph(icon)) for icon in _ACTION_ICONS)
def _icon(text: Text, icon: str, style: str) -> None:
"""Append an action row's mark in the icon column, its trailing space included.
A one-cell mark pads out to :func:`_icon_lane` so every label — narrow mark or
wide emoji — starts in the same column.
"""
mark = glyph(icon)
text.append(mark, style=style)
text.append(" " * (_icon_lane() - cell_len(mark) + 1))
def _lane_lines(label: str, value: list[Text], width: int) -> list[str]:
"""Render a labelled lane whose value hangs, already broken, under its own column.
@@ -842,27 +871,27 @@ class TraceScreen(Screen):
"""One action row: pointer, glyph, and label (current value inlined)."""
text = Text("❯ " if selected else " ", style="brand" if selected else "")
if key == "compose":
text.append("✎ ", style="brand")
_icon(text, "✎", "brand")
text.append("Compose path")
elif key == "reverse":
text.append("⇄ ", style="accent")
_icon(text, "⇄", "accent")
if self._effective_spec()[0]:
text.append("Reverse path — trace it the other way")
else:
text.append("Reverse path — compose a path first", style="muted")
elif key == "explore":
text.append("⚡ ", style="warn")
_icon(text, "⚡", "warn")
text.append("Explore paths")
elif key == "width":
w = self._width_bytes()
text.append("⚙ ", style="accent")
_icon(text, "⚙", "accent")
text.append(f"Path width — {w} byte{'s' if w != 1 else ''} per hop")
elif key == "samples":
n = self._sample_count()
text.append("# ", style="accent")
_icon(text, "#", "accent")
text.append(f"Sample count — {n} trace{'s' if n != 1 else ''}")
elif key == "trace":
text.append("▶ ", style="ok")
_icon(text, "▶", "ok")
if self._mode == "path" and not self._effective_spec()[0]:
text.append("Trace — compose a path first", style="muted")
else:
+17
View File
@@ -11,6 +11,7 @@ import asyncio
from datetime import timedelta
import pytest
from rich.cells import cell_len
from meshterm.core.models import Contact, Hop, TraceResult, TraceStats, TxLevelResult, \
TxOptResult, utcnow
@@ -419,6 +420,22 @@ async def test_trace_screen_explore_adopts_a_scenario_path() -> None:
assert screen._path_spec == "3d63,f2c2" # None leaves the spec untouched
async def test_trace_screen_action_labels_share_one_column() -> None:
"""Every action's label starts in the same column, wide mark or narrow.
``⚡`` is an emoji — two cells where ``✎``/``⚙``/``#``/``▶`` are one — so a fixed
``"icon "`` prefix would start Explore's label a column right of the rest.
"""
screen, _ = _trace_screen()
labels = ["Compose path", "Explore paths", "Path width", "Sample count", "Trace —"]
columns = set()
for row in _plain(screen.render_body(100)).splitlines():
for label in labels:
if label in row:
columns.add(cell_len(row[: row.index(label)]))
assert len(columns) == 1, columns
async def test_trace_screen_action_cursor_commits_the_selected_row() -> None:
"""↑↓ move over the action rows; Enter commits the one under the cursor."""
opened: list[str] = []