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:
1 parent
56cc220ee3
commit
24c70c487f
3 files changed
+82
-12
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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())))
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user