The live feed never collapses a packet's class name, and the PicoCalc drops its icon

The class name was dropped below the width the whole row needed, leaving only its icon. It is now drawn at every width, like the scope; a row wider than the screen is read by scrolling the highlighted one with ←→. The class icon beside the name is a family cue once the name is always there, so it follows Platform.menu_icons: on the PicoCalc, where it could only be a one-glyph stand-in, the lane is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-09-25 02:38:41 -04:00
1 parent 01a8f711d6
commit d019c840a8
3 files changed
+76 -78

No files matched your search

+40 -51
View File
@@ -3,7 +3,7 @@
The interactive face of the ``livefeed`` tool — the dashboard's old feed panel,
promoted to a first-class screen. One always-repainting list streams the latest
packets, newest first: time, class (icon + label, icon alone on a narrow terminal),
packets, newest first: time, class (its name, beside an icon where the platform draws one),
subject, SNR/RSSI, and a message's conversation. What a row deliberately does *not* carry
is the relay path a frame rode in on: a route is a shape, not a lane, and squeezing one
into the cells left at the right edge only ever produced a stub — so the route belongs
@@ -68,6 +68,7 @@ from ..core.channels import identify_channel, split_channel_sender
from ..core.events import EventKind, MeshEvent
from ..core.frames import CHANNEL_CLASSES, ENDPOINT_HASH_BYTES
from ..core.models import Observation, utcnow
from ..platforms import Platform, on_platform
from .menus import fit_cells
from .packet_viewer import (
KIND_STYLES,
@@ -136,8 +137,25 @@ _FEED_CLASS_WIDTH = 14
_LANE_GAP = 2
#: ``HH:MM:SS`` plus the gap that follows it.
_TIME_LANE = 8 + _LANE_GAP
#: The class icon and its trailing space (all the class lane keeps on a narrow terminal).
#: The class icon and its trailing space — or nothing, where the platform draws no icons
#: (bound by :func:`_bind_class_icon`).
_ICON_LANE = 3
@on_platform
def _bind_class_icon(platform: Platform) -> None:
"""Bind the class icon lane to the platform (runs now and on every switch).
The class *name* is always drawn (it never collapses — JP, 2026-09-25), so the icon
beside it is a family cue rather than the information, which is exactly what
:attr:`~meshterm.platforms.Platform.menu_icons` governs: on the PicoCalc, where a class
icon can only be a one-glyph stand-in, the lane is dropped and its three cells go to
the row.
"""
global _ICON_LANE
_ICON_LANE = 3 if platform.menu_icons else 0
#: Cells a reception reading right-aligns its number in — the ``%+5.1f``/``%5.0f`` field
#: both lanes are built on, and the slot their column headers sit over.
_READING_W = 5
@@ -146,21 +164,6 @@ _SNR_LANE = _READING_W + len(" dB")
#: `` -61 dBm`` — the lane gap, the number field, then its unit.
_RSSI_LANE = _LANE_GAP + _READING_W + len(" dBm")
#: The full row's width without its scope lane: every lane the row always carries plus the
#: class label — see :data:`_FEED_LABEL_MIN_WIDTH` for the width the label needs once the
#: scope is added.
_FEED_ROW_WIDTH = (
2
+ _TIME_LANE
+ _ICON_LANE
+ _FEED_CLASS_WIDTH
+ _LANE_GAP
+ _FEED_SUBJECT_WIDTH
+ _LANE_GAP
+ _SNR_LANE
+ _RSSI_LANE
)
#: The scope lane's width. Room for an unnamed scope's ``? 3fa1`` and a short region name
#: whole; a longer name ellipsizes — its whole name is on the viewer's ``scope`` row, one
#: keypress away, like everything else a lane cuts.
@@ -169,13 +172,6 @@ _FEED_SCOPE_WIDTH = 10
#: The scope lane with the gap that sets it off from the readings after it.
_SCOPE_LANE = _FEED_SCOPE_WIDTH + _LANE_GAP
#: Terminal width below which the feed drops the textual class label and keeps only the
#: two-cell icon: the whole row, scope lane included. The scope never collapses (JP,
#: 2026-09-25) — a lane that came and went with the terminal's width hid the one reading
#: that says where a flood was allowed to go — so on a narrower screen it is the label that
#: gives way, the icon beside it still saying what the packet is.
_FEED_LABEL_MIN_WIDTH = _FEED_ROW_WIDTH + _SCOPE_LANE
def _feed_scope(scope: Scope | None) -> Text:
"""One row's scope cell: the region, ``? 3fa1`` unnamed, a muted dash for unscoped.
@@ -189,28 +185,25 @@ def _feed_scope(scope: Scope | None) -> Text:
class _Lanes(NamedTuple):
"""Which of the optional lanes a paint at one width carries, and the subject's width.
"""The lane widths one paint lays its rows out in.
Attributes:
label: The class lane's text label beside its icon.
subject: The subject lane's width (:data:`_FEED_SUBJECT_WIDTH`).
"""
label: bool
subject: int
def _lanes_for(width: int) -> _Lanes:
"""The lanes a row can carry at ``width``.
"""The lanes a row carries at ``width`` — all of them, at every width.
Every lane but one is always drawn, the scope lane included. The class label is the
one that gives way: it needs the whole row (:data:`_FEED_LABEL_MIN_WIDTH`), and below
that the icon alone says what the packet is. On a 72-column terminal (a 68-cell body)
that is the icon row with its scope, 66 cells; on the PicoCalc's 53 the icon row is
wider than the screen before any scope, so there the row reads by scrolling the
highlighted one sideways (``←→``), as it already did.
No lane collapses (JP, 2026-09-25): the class name and the scope were each dropped on
a narrow terminal in turn, and each time the lane that went was one a reader came to
the feed for. A row wider than the screen is read the app-wide way instead — the
highlighted row slides sideways under ``←→`` — which on a 72-column terminal reaches
the readings at the row's end, and on the PicoCalc's 53 everything past the subject.
"""
return _Lanes(width >= _FEED_LABEL_MIN_WIDTH, _FEED_SUBJECT_WIDTH)
return _Lanes(_FEED_SUBJECT_WIDTH)
#: Cells one ←/→ press shifts the highlighted row by — the app-wide select list's own
@@ -345,7 +338,7 @@ class LiveFeedScreen(Screen):
self._message_scope = message_scope
#: The lanes the last paint carried (see :func:`_lanes_for`) — read by the subject
#: builders, whose crowded-lane fallbacks measure against the lane actually drawn.
self._lanes = _lanes_for(_FEED_LABEL_MIN_WIDTH)
self._lanes = _lanes_for(0)
#: The feed: latest events of every class as data, newest first — rendered
#: fresh each paint (rows adapt to width) and handed whole to the viewer.
self._feed: deque[PacketEntry] = deque(maxlen=_FEED_CAP)
@@ -603,11 +596,7 @@ class LiveFeedScreen(Screen):
"""
header = Text(" ", style="muted") # the pointer lane
header.append(fit_cells("TIME", _TIME_LANE))
header.append(
fit_cells("CLASS", _ICON_LANE + _FEED_CLASS_WIDTH + _LANE_GAP)
if lanes.label
else " " * _ICON_LANE
)
header.append(fit_cells("CLASS", _ICON_LANE + _FEED_CLASS_WIDTH + _LANE_GAP))
header.append(fit_cells("SUBJECT", lanes.subject + _LANE_GAP))
header.append(fit_cells("SCOPE", _SCOPE_LANE))
# The two readings right-align their number, so their labels do too — each sits
@@ -642,9 +631,9 @@ class LiveFeedScreen(Screen):
The class lane says what the packet *is*, straight from
:func:`~meshterm.ui.packet_viewer.class_marks` — so a raw frame reads
``📻 channel text``, the class the viewer's card headlines, rather than the
``📦 packet`` event family it merely arrived in. Its icon always shows; the
textual label beside it is dropped wholesale on a narrow terminal
(``lanes.label``), keeping the lanes aligned either way. The subject lane beside it
``📦 packet`` event family it merely arrived in. The name always shows; the icon
beside it only where the platform draws icons (:func:`_bind_class_icon`). The
subject lane beside it
then answers what the packet is *about*, in whatever terms its class deals in
(:meth:`_feed_subject`). No relay path rides here — the whole row fits a
72-column screen precisely because it doesn't try to, and the route is drawn
@@ -678,13 +667,13 @@ class LiveFeedScreen(Screen):
style="muted",
)
icon, class_label = class_marks(entry)
body.append(fit_cells(icon, _ICON_LANE))
if lanes.label:
body.append(
fit_cells(class_label, _FEED_CLASS_WIDTH),
style=KIND_STYLES.get(entry.kind, "brand"),
)
body.append(" " * _LANE_GAP) # the lane's gutter — the longest class fills it
if _ICON_LANE:
body.append(fit_cells(icon, _ICON_LANE))
body.append(
fit_cells(class_label, _FEED_CLASS_WIDTH),
style=KIND_STYLES.get(entry.kind, "brand"),
)
body.append(" " * _LANE_GAP) # the lane's gutter — the longest class fills it
subject = self._feed_subject(entry)
# The lane fits like every other: cells, not characters, ellipsis on overflow —
# but as a Text, so a subject built of several styled pieces keeps them.
+9 -10
View File
@@ -123,16 +123,15 @@ def test_livefeed_rows_leave_the_relay_path_to_the_viewer() -> None:
"""A row says what arrived and how well it was heard; the route is Enter's job.
Whatever the fixed lanes left a route was never enough to draw one in — it arrived
elided to a stub — so the feed carries none: the whole row, scope lane and all, fits a
72-column screen, and a wide one keeps its class label in words too.
elided to a stub — so the feed carries none: the row is its fixed lanes, class name
and all, and a wide terminal holds the whole of it.
"""
screen = _screen()
screen.on_event(MeshEvent.observation_event(_obs(kind="packet", path="3d63,a1b2", snr=1.0)))
row = _rows(screen, 68)[0] # a 72-column terminal's framed body
row = _rows(screen, 100)[0]
assert "via" not in row and "Hub" not in row # no route, not even a stub of one
assert "+1.0 dB" in row and "-90 dBm" in row # …the reception it was heard at reads
assert len(row.rstrip()) <= 68
assert "📦 packet" in _rows(screen, 90)[0] # the class label, where there is room
assert "📦 packet" in row # the class, in words
assert "+1.0 dB" in row and "-90 dBm" in row # …and the reception it was heard at
def test_livefeed_class_lane_names_the_payload_class_once() -> None:
@@ -483,11 +482,11 @@ def test_livefeed_windows_inside_the_fixed_screen() -> None:
_CRAMPED = 44
def test_livefeed_the_whole_row_fits_a_72_column_screen() -> None:
"""No row overflows at 72 — which is why ←→ has nothing to advertise there."""
def test_livefeed_the_whole_row_fits_a_wide_screen() -> None:
"""No row overflows where the lanes fit — which is when ←→ has nothing to advertise."""
screen = _screen(seed=[_obs(node="n0", kind="telemetry")])
for line in _stripped(screen.render_body(72)):
assert len(line.rstrip()) <= 72
for line in _stripped(screen.render_body(100)):
assert len(line.rstrip()) <= 100
assert screen._hmax == 0
assert "←→" not in screen.footer_hint
+27 -17
View File
@@ -180,18 +180,12 @@ def _feed(*raws: dict) -> LiveFeedScreen:
return screen
def test_the_feed_carries_a_scope_lane_on_a_72_column_terminal() -> None:
"""The framed body there is 68 cells: the icon-only row holds the lane, name and all.
The lane sits between the subject and the readings; a plain flood is a muted dash and
a direct frame, which has no scope, leaves the lane blank.
"""
def test_the_feed_scope_lane_sits_between_subject_and_readings() -> None:
"""A plain flood is a muted dash; a direct frame, which has no scope, leaves it blank."""
screen = _feed(_DIRECT, _FLOOD, _scoped("elsewhere"), _scoped("harbour"))
header, *rows = _stripped(screen.render_body(68))
header, *rows = _stripped(screen.render_body(100))
lanes = header.split()
assert lanes.index("SCOPE") == lanes.index("SNR") - 1
for line in (header, *rows):
assert cell_len(line.rstrip()) <= 68
assert lanes.index("SUBJECT") + 1 == lanes.index("SCOPE") == lanes.index("SNR") - 1
col = _col(header, "SCOPE")
assert _col(rows[0], "harbour") == col
assert _col(rows[1], f"? {_code('elsewhere')}") == col
@@ -200,18 +194,34 @@ def test_the_feed_carries_a_scope_lane_on_a_72_column_terminal() -> None:
assert all(row.rstrip().endswith("dBm") for row in rows)
def test_the_feed_never_collapses_the_scope_lane() -> None:
"""The scope is drawn at every width; the class label is what gives way when narrow.
def test_the_feed_never_collapses_a_lane() -> None:
"""The class name and the scope are drawn at every width; a narrow row scrolls instead.
Beside the subject, the lane is on screen even at the PicoCalc's 53, where it is the
readings after it that run off the edge.
Nothing gives way: at 72 columns (a 68-cell body) the readings run past the edge, and
the highlighted row's ←→ is what reaches them.
"""
screen = _feed(_scoped("harbour"))
for width in (53, 68, 72, 80, 100):
for width in (68, 72, 80, 100):
header, row = _stripped(screen.render_body(width))[:2]
assert "CLASS" in header and "channel text" in row, width
assert _col(header, "SCOPE") == _col(row, "harbour"), width
assert "CLASS" not in _stripped(screen.render_body(80))[0]
assert "CLASS" in _stripped(screen.render_body(100))[0]
screen._selected = 0 # noqa: SLF001
screen.render_body(68)
assert screen._hmax > 0 # noqa: SLF001 - the row is wider than the body: ←→ reaches it
def test_the_picocalc_feed_names_the_class_without_an_icon() -> None:
"""On the console the class icon is a one-glyph stand-in; the name alone says it."""
from meshterm.platforms import PICOCALC, REGULAR, set_platform
set_platform(PICOCALC)
try:
screen = _feed(_scoped("harbour"))
header, row = _stripped(screen.render_body(53))[:2]
assert header.split()[:2] == ["TIME", "CLASS"]
assert _col(header, "CLASS") == _col(row, "channel text")
finally:
set_platform(REGULAR)
def test_the_feed_hands_its_scope_reader_to_the_viewer() -> None: