Packet viewer: the class headlines the card, icon and all

Every packet now leads with its class — icon + UPPERCASE label — as the
first row of the viewer, whatever the kind: a raw frame shows its parsed
payload class, everything else its own kind. The new PAYLOAD_ICONS table
gives each raw payload class a concept glyph (📻 channel text, 🎯 trace,
…, matching the tools that own those concepts) and joins the CLAUDE.md
icon lexicon; the dialog title drops its emoji, per the title rule the
icon was violating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5 committed 2026-07-19 00:45:06 -04:00
1 parent 56cc220ee3
commit 24c70c487f
3 files changed
+82 -12

No files matched your search

+4 -1
View File
@@ -102,7 +102,10 @@ Relative ages: `format_ago` for prose ("now", "5m ago", "never" — never "now a
📤 send now · 📨 courier/queue · 💬 chat · 🔔 notify · 🔕 mute (notifications off) ·
📱 QR · 🔗 link · ↻ re-read · ↕ reorder · ⇄ reverse (flip a path's direction) ·
🏆 trophy case/record · ⌨ command line · 🚪 quit. Packet-class icons (feed/viewer
lane): 📢 advert · 📊 telemetry · 📦 packet · 💬 message · ✅ ack.
lane): 📢 advert · 📊 telemetry · 📦 packet · 💬 message · ✅ ack. Raw payload
classes (`PAYLOAD_ICONS`): 📻 channel text · 💽 channel data · 📩 direct message
(overheard) · 📥 request · 📮 response · 🎭 anon request · 🧭 path · 🎯 trace ·
🧩 multipart · 🧰 control · ❔ unknown — raw advert/ack reuse 📢/✅.
### Colour
+51 -9
View File
@@ -12,10 +12,10 @@ Message paths dialog and the Trophy case draw — and, for an overheard channel-
naming a channel we hold the key for, the decrypted text too (see
:func:`~meshterm.core.channels.decrypt_channel_text`), even though the radio itself never
decoded it for us; a message shows the sender, the
conversation, and the text; an ack shows its code. Common to all: the timestamp, the
node (name coloured by the app-wide palette, hash in the hash widget), reception
quality on the shared SNR bar, and any leftover raw field the flavoured layout doesn't
already show.
conversation, and the text; an ack shows its code. Common to all: the class headline
(icon + UPPERCASE class, first row of the card), the timestamp, the node (name coloured
by the app-wide palette, key in the key widget), reception quality on the shared SNR
bar, and any leftover raw field the flavoured layout doesn't already show.
When opened over a list the viewer pages through it in place — ``↑``/``↓`` step to
the newer/older packet, Home/End jump to the newest/oldest, mirroring the keys the
@@ -105,6 +105,25 @@ KIND_ICONS = {
}
DEFAULT_ICON = "❔"
#: The two-cell icon per parsed payload class of a raw ``packet`` frame, shared by the
#: viewer's leading class row and the dashboard's traffic meters. One glyph per concept:
#: 📻 rhymes with the Channels tool, 🎯 with Trace, and a raw ADVERT/ACK frame reuses
#: its kind's icon; :data:`DEFAULT_ICON` marks a typename this table has never heard of.
PAYLOAD_ICONS = {
"REQ": "📥",
"RESPONSE": "📮",
"TEXT_MSG": "📩",
"ACK": "✅",
"ADVERT": "📢",
"GRP_TXT": "📻",
"GRP_DATA": "💽",
"ANON_REQ": "🎭",
"PATH": "🧭",
"TRACE": "🎯",
"MULTIPART": "🧩",
"CONTROL": "🧰",
}
#: The colour a relayed packet's route draws in on THE route graph. It is the only path
#: on the canvas, so white just reads as "the route the packet rode" (the Trophy case's
#: single-walk convention).
@@ -190,6 +209,26 @@ def payload_class(raw: Optional[dict]) -> Optional[str]:
return _PAYLOAD_GLOSS.get(typename, typename.lower())
def class_chrome(entry: PacketEntry) -> tuple[str, str]:
"""The class headline every packet leads with: a two-cell icon and an UPPERCASE label.
A raw ``packet`` frame's class is its parsed payload class — ``CHANNEL TEXT``,
``TRACE``, … — under its :data:`PAYLOAD_ICONS` glyph; a typename this table has
never heard of keeps the fallback mark and shouts the raw typename; a class-less
frame stays a plain ``PACKET``. Every other kind is its own class (``ADVERT``,
``MESSAGE``, …) under the shared :data:`KIND_ICONS` glyph.
"""
if entry.kind == "packet":
raw = entry.raw if isinstance(entry.raw, dict) else {}
typename = raw.get("payload_typename")
if typename:
icon = PAYLOAD_ICONS.get(typename, DEFAULT_ICON)
label = _PAYLOAD_GLOSS.get(typename, typename.lower()).upper()
return icon, label
return KIND_ICONS["packet"], "PACKET"
return kind_icon(entry.kind), entry.kind.upper()
def node_label(
entry: PacketEntry, resolve: NodeResolver, self_name: Optional[str] = None
) -> tuple[str, str]:
@@ -334,8 +373,9 @@ class PacketViewer(Screen):
self._set_title()
def _set_title(self) -> None:
# No emoji in a dialog title (the standards' rule); the class row carries the icon.
entry = self._entries[self._index]
title = f"{kind_icon(entry.kind)} {entry.kind}"
title = entry.kind
if len(self._entries) > 1:
title += f" · {self._index + 1}/{len(self._entries)}"
self.title = title
@@ -436,9 +476,13 @@ class PacketViewer(Screen):
return render_lines(grid, width)
def _head_rows(self, entry: PacketEntry) -> list[tuple[str, RenderableType]]:
"""The rows above the route graph: the common core plus a packet's parsed frame."""
"""The rows above the route graph: the class headline, the common core, then a
packet's parsed frame."""
rows: list[tuple[str, RenderableType]] = []
icon, class_label = class_chrome(entry)
rows.append(("class", Text(f"{icon} {class_label}")))
secs = _age_seconds(entry.when)
heard = Text(entry.when.astimezone().strftime("%b %d %H:%M:%S"))
heard.append(f" ({format_ago(secs)})", style="muted")
@@ -543,9 +587,7 @@ class PacketViewer(Screen):
"""
raw = entry.raw if isinstance(entry.raw, dict) else {}
rows: list[tuple[str, RenderableType]] = []
typename = raw.get("payload_typename")
if typename:
rows.append(("class", Text(payload_class(raw) or typename.lower())))
typename = raw.get("payload_typename") # class itself leads the card (class_chrome)
route = raw.get("route_typename")
if route:
rows.append(("route", Text(route.replace("_", " ").lower())))
+27 -2
View File
@@ -44,13 +44,38 @@ def _grp_txt_frame(secret: bytes, text: str) -> tuple[str, str, str]:
def test_packet_viewer_shows_class_and_route() -> None:
"""A raw packet's parsed payload class and route surface as labelled rows."""
"""A raw packet's parsed payload class headlines the card; the route is a row."""
entry = _grp_txt_entry({"chan_hash": "ff", "cipher_mac": "0000", "crypted": "00" * 16})
body = _plain(_viewer(entry).render_body(80))
assert "channel text" in body
assert "📻 CHANNEL TEXT" in body
assert "flood" in body
def test_packet_viewer_class_row_leads_the_card() -> None:
"""The class headline is the first row — above heard/from — for every kind."""
packet = _grp_txt_entry({"chan_hash": "ff", "cipher_mac": "0000", "crypted": "00" * 16})
body = _plain(_viewer(packet).render_body(80))
assert body.index("CHANNEL TEXT") < body.index("heard")
advert = PacketEntry(when=utcnow(), kind="advert", node="aa")
body = _plain(_viewer(advert).render_body(80))
assert "📢 ADVERT" in body
assert body.index("ADVERT") < body.index("heard")
def test_packet_viewer_class_marks_an_unknown_typename() -> None:
"""A payload class this build has never heard of keeps the ❔ mark and its raw name."""
entry = PacketEntry(when=utcnow(), kind="packet", raw={"payload_typename": "XYZZY"})
body = _plain(_viewer(entry).render_body(80))
assert "❔ XYZZY" in body
def test_packet_viewer_title_carries_no_emoji() -> None:
"""Dialog titles stay emoji-free (the standards' rule); the class row has the icon."""
entry = PacketEntry(when=utcnow(), kind="advert", node="aa")
assert _viewer(entry).title == "advert"
def test_packet_viewer_decrypts_a_known_channel() -> None:
"""A channel-text frame we hold the key for decrypts to its plaintext."""
name, secret = "#general", derive_secret("#general")