Revert "Live feed: one trailing lane, for whatever the row is about"
This reverts commit 5a6bb3c8f4bba295dc78c288963bd19a893d8802.
This commit is contained in:
1 parent
f2330e5372
commit
c2c10aef33
2 files changed
+46
-107
No files matched your search
@@ -3,9 +3,8 @@
|
||||
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),
|
||||
SNR/RSSI, and then whatever the row is *about* — a node that named itself followed by
|
||||
its detail, or, for the relayed flood traffic that names nobody at all, its ``via``
|
||||
chain given the whole lane. Seeded from stored history so the screen opens full, then streamed
|
||||
node, SNR/RSSI, and the trailing detail (a relayed frame's ``via`` chain, a message's
|
||||
conversation). Seeded from stored history so the screen opens full, then streamed
|
||||
live off the event hub. ``↑``/``↓`` walk the rows, PgUp/PgDn/Home/End page and jump
|
||||
them, ``←``/``→`` scroll the highlighted row sideways when it runs past the right edge
|
||||
(a long relay path read to its end without opening anything), and Enter opens the
|
||||
@@ -26,7 +25,6 @@ import asyncio
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING, Any, Optional, Sequence
|
||||
|
||||
from rich.cells import cell_len
|
||||
from rich.text import Text
|
||||
|
||||
from ..core.channels import split_channel_sender
|
||||
@@ -58,25 +56,16 @@ _REFRESH_S = 1.0
|
||||
#: history depth, not layout).
|
||||
_FEED_CAP = 100
|
||||
|
||||
#: The widest a name may grow in the trailing subject lane before it ellipsizes. Not a
|
||||
#: column — the lane is shared with the row's detail, so a name only takes the cells it
|
||||
#: needs, and this is the ceiling that stops one long name from swallowing the route
|
||||
#: behind it.
|
||||
_FEED_NAME_MAX = 18
|
||||
#: The feed's fixed node-name lane width; longer names ellipsize so the columns hold.
|
||||
_FEED_NAME_WIDTH = 18
|
||||
|
||||
#: The class lane's fixed width — wide enough for the longest class the app files a
|
||||
#: packet under (``direct message``), so every label reads whole and the lanes hold.
|
||||
_FEED_CLASS_WIDTH = 14
|
||||
|
||||
#: Cells between the fixed lanes and the trailing subject, and between a name and the
|
||||
#: detail behind it — the feed's one lane gutter.
|
||||
_SUBJECT_GAP = 2
|
||||
|
||||
#: Terminal width below which the feed drops the textual class label and keeps only the
|
||||
#: two-cell icon. The label is the row's most informative fixed field now that it names
|
||||
#: the real class, so it is held onto down to the width where the subject lane would stop
|
||||
#: being able to say anything (the fixed lanes plus a readable route).
|
||||
_FEED_LABEL_MIN_WIDTH = 68
|
||||
#: Terminal width below which the feed drops the textual class label and keeps only
|
||||
#: the two-cell icon, buying the name and reception lanes room (≤72-col care).
|
||||
_FEED_LABEL_MIN_WIDTH = 80
|
||||
|
||||
#: Cells one ←/→ press shifts the highlighted row by — the app-wide select list's own
|
||||
#: step (:attr:`~meshterm.ui.tui.select.SelectScreen._HSCROLL_STEP`), so a row here
|
||||
@@ -346,25 +335,17 @@ class LiveFeedScreen(Screen):
|
||||
def _feed_row(
|
||||
self, entry: PacketEntry, selected: bool, show_label: bool, width: int
|
||||
) -> Text:
|
||||
"""Lay one feed row out: time, class, reception — then whatever it is *about*.
|
||||
"""Lay one feed row out in fixed lanes: time, class, node, reception, detail.
|
||||
|
||||
Four fixed lanes carry the facts every packet has. 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
|
||||
(``show_label``), keeping the lanes aligned either way.
|
||||
|
||||
Everything after the reception lanes is *one* lane, not two, because a packet
|
||||
does not always have a node. Most of what a real mesh carries is relayed flood
|
||||
traffic that names no origin at all, and a fixed node column spent those cells on
|
||||
a dash for row after row while the route — the one thing such a frame does say —
|
||||
was crushed against the right edge. So the trailing lane holds whatever the row
|
||||
is about: a node that named itself (in the app-wide palette hue, our own node
|
||||
white) followed by its detail, or, when nobody did, the ``via`` chain alone. It
|
||||
still opens at a fixed column, so names stay scannable down the screen; what
|
||||
changes is that a nameless row gives those cells to its route instead of to a
|
||||
placeholder.
|
||||
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
|
||||
(``show_label``), keeping the lanes aligned either way. The node lane is then
|
||||
free to be about the node: its name in the app-wide palette hue (our own node
|
||||
white, a bare hash muted), and a dash where nobody identified themselves. Never
|
||||
wraps — the detail lane gets whatever width the fixed lanes leave (``width``).
|
||||
|
||||
A row that runs past the right edge is read by scrolling it, not by growing it.
|
||||
An unhighlighted row simply elides its relay path's middle hops, keeping both
|
||||
@@ -388,6 +369,11 @@ class LiveFeedScreen(Screen):
|
||||
style=KIND_STYLES.get(entry.kind, "brand"),
|
||||
)
|
||||
body.append(" ") # the lane's own gutter — the longest class fills it exactly
|
||||
label, style = node_label(entry, self._resolve, self._self_name)
|
||||
if label == "?":
|
||||
label, style = self._feed_subject(entry) # no node identity: name what we can
|
||||
body.append(fit_cells(label, _FEED_NAME_WIDTH), style=style)
|
||||
body.append(" ")
|
||||
body.append(
|
||||
f"{entry.snr:+5.1f} dB" if entry.snr is not None else " " * 8,
|
||||
style=snr_style(entry.snr) if entry.snr is not None else "muted",
|
||||
@@ -396,29 +382,12 @@ class LiveFeedScreen(Screen):
|
||||
f" {entry.rssi:5.0f} dBm" if entry.rssi is not None else " " * 10,
|
||||
style="muted",
|
||||
)
|
||||
|
||||
subject = Text(no_wrap=True, overflow="ellipsis")
|
||||
named = self._feed_name(entry)
|
||||
if named is not None:
|
||||
# Capped, not padded: a long name ellipsizes rather than starving the route
|
||||
# behind it, but a short one hands its spare cells straight to that route.
|
||||
name, style = named
|
||||
if cell_len(name) > _FEED_NAME_MAX:
|
||||
name = fit_cells(name, _FEED_NAME_MAX)
|
||||
subject.append(name, style=style)
|
||||
used = body.cell_len + _SUBJECT_GAP + subject.cell_len
|
||||
note = self._feed_note(
|
||||
entry,
|
||||
None if selected
|
||||
else max(1, avail - used - (_SUBJECT_GAP if subject.cell_len else 0)),
|
||||
entry, None if selected else max(1, avail - body.cell_len - 2)
|
||||
)
|
||||
if note is not None:
|
||||
if subject.cell_len:
|
||||
subject.append(" " * _SUBJECT_GAP)
|
||||
subject.append_text(note)
|
||||
if subject.cell_len:
|
||||
body.append(" " * _SUBJECT_GAP)
|
||||
body.append_text(subject)
|
||||
body.append(" ")
|
||||
body.append_text(note)
|
||||
|
||||
if selected:
|
||||
self._hmax = max(0, body.cell_len - avail)
|
||||
@@ -428,32 +397,29 @@ class LiveFeedScreen(Screen):
|
||||
row.append_text(body)
|
||||
return row
|
||||
|
||||
def _feed_name(self, entry: PacketEntry) -> Optional[tuple[str, str]]:
|
||||
"""Who the row is about, as ``(label, style)`` — or ``None`` when nobody is.
|
||||
def _feed_subject(self, entry: PacketEntry) -> tuple[str, str]:
|
||||
"""Name the node lane when an entry carries no resolvable node identity.
|
||||
|
||||
The node the packet identified itself as, if any; failing that, the most
|
||||
identifying thing it still carries: the sender a channel message named itself
|
||||
with, then an ack's code. Two things it deliberately does *not* do. It does not
|
||||
stand in the packet's class — the class lane already says that, straight from
|
||||
Rather than a useless ``?``, the lane reads the most identifying thing the entry
|
||||
still carries: the sender a channel message named itself with, then an ack's
|
||||
code. What it deliberately does *not* do is stand in the packet's class — the
|
||||
class lane two columns left already says that, straight from
|
||||
:func:`~meshterm.ui.packet_viewer.class_marks`, and a row that spelled it twice
|
||||
read ``packet`` beside ``channel text`` as though they were two facts. And it
|
||||
does not reach for a placeholder: a relayed flood names no origin, so it returns
|
||||
``None`` and the trailing lane goes to its route, which does name nodes.
|
||||
was reading ``packet`` beside ``channel text`` as though they were two facts.
|
||||
A relayed flood names no origin, so its lane says so with a dash and lets the
|
||||
eye travel on to the ``via`` chain, which does name nodes.
|
||||
"""
|
||||
label, style = node_label(entry, self._resolve, self._self_name)
|
||||
if label != "?":
|
||||
return label, style
|
||||
if entry.kind == "message":
|
||||
sender = _channel_sender(entry.text)
|
||||
if not sender:
|
||||
return None # naming the conversation is the note's job, not a name's
|
||||
ours = self._self_name and sender == self._self_name
|
||||
# A resolvable sender takes its key-derived hue; a stranger stays muted —
|
||||
# colour is reserved for keyed identities.
|
||||
return sender, ("you" if ours else name_style(sender, self._key_of(sender)))
|
||||
if sender:
|
||||
ours = self._self_name and sender == self._self_name
|
||||
# A resolvable sender takes its key-derived hue; a stranger stays
|
||||
# muted — colour is reserved for keyed identities.
|
||||
return sender, ("you" if ours else name_style(sender, self._key_of(sender)))
|
||||
return "channel", "muted"
|
||||
if entry.where:
|
||||
return entry.where, "muted" # an ack's code, or any other stray context
|
||||
return None
|
||||
return "—", "muted"
|
||||
|
||||
def _feed_note(self, entry: PacketEntry, budget: Optional[int]) -> Optional[Text]:
|
||||
"""The row's trailing detail: a packet's relay path, a message's conversation.
|
||||
|
||||
+5
-32
@@ -92,8 +92,8 @@ def test_livefeed_class_lane_names_the_payload_class_once() -> None:
|
||||
|
||||
``packet`` names only the event family the frame arrived in, so the class lane
|
||||
reads its parsed payload class — the very label the viewer's card headlines. The
|
||||
trailing lane then goes straight to the route, because an origin-less flood has no
|
||||
node to name and a placeholder would only spend the cells the route needs.
|
||||
node lane is then free to be about the node, and an origin-less flood says so with
|
||||
a dash rather than standing the class in a second time.
|
||||
"""
|
||||
screen = _screen()
|
||||
raw = {"payload_typename": "TRACE", "route_typename": "FLOOD"}
|
||||
@@ -106,8 +106,8 @@ def test_livefeed_class_lane_names_the_payload_class_once() -> None:
|
||||
assert "🎯 trace" in row # the class lane, under the payload class's own icon
|
||||
assert row.count("trace") == 1 # said once, not once per lane
|
||||
assert "packet" not in row # …and never as the generic event family
|
||||
assert row.rstrip().endswith("via YUL → Alice") # the lane is all route
|
||||
assert "—" not in row and "?" not in row # no placeholder for a node that isn't there
|
||||
assert "—" in row # the node lane: nobody identified themselves
|
||||
assert "?" not in row # …but never the useless placeholder
|
||||
|
||||
|
||||
def test_livefeed_names_a_channel_message_by_its_sender() -> None:
|
||||
@@ -121,33 +121,6 @@ def test_livefeed_names_a_channel_message_by_its_sender() -> None:
|
||||
assert "ch 3" in body # …with the channel kept as the trailing context
|
||||
|
||||
|
||||
def test_livefeed_subject_lane_opens_at_one_column_for_every_row() -> None:
|
||||
"""Named or not, the trailing lane starts in the same place — names stay scannable.
|
||||
|
||||
A named row spends its opening cells on the name; a nameless flood hands the whole
|
||||
lane to its route. What must not move is where that lane begins, or a screenful of
|
||||
rows stops reading as a column.
|
||||
"""
|
||||
screen = _screen(seed=[
|
||||
_obs(kind="advert", age_s=1),
|
||||
_obs(node="", kind="packet", path="3d63,a1b2", age_s=0,
|
||||
raw={"payload_typename": "GRP_TXT"}),
|
||||
])
|
||||
flood, named = _stripped(screen.render_body(90))[1:3] # newest (the flood) first
|
||||
assert named.index("Alice") == flood.index("via") # one lane, one opening column
|
||||
assert "channel text" in flood and "advert" in named
|
||||
|
||||
|
||||
def test_livefeed_keeps_the_class_label_at_72_columns() -> None:
|
||||
"""The class label is the row's most informative field, so it survives a 72-col screen."""
|
||||
screen = _screen(seed=[
|
||||
_obs(node="", kind="packet", path="3d63", raw={"payload_typename": "GRP_TXT"}),
|
||||
])
|
||||
row = _stripped(screen.render_body(72))[1]
|
||||
assert "📻 channel text" in row # the words, not just the icon
|
||||
assert "YUL" in row # …and the route still has room to name a node
|
||||
|
||||
|
||||
def test_livefeed_page_keys_move_the_feed_selection() -> None:
|
||||
"""With a feed row highlighted, PgUp/PgDn walk the selection a windowful at a time."""
|
||||
seed = [_obs(node=f"n{i}", age_s=i) for i in range(20)]
|
||||
@@ -195,7 +168,7 @@ def test_livefeed_highlighted_row_scrolls_sideways_to_its_tail() -> None:
|
||||
assert screen._hshift == screen._hmax
|
||||
assert scrolled.startswith("▸ ") # the pointer lane stays pinned while the row slides
|
||||
assert scrolled.rstrip().endswith("11aa") # the last hop is now readable
|
||||
assert opening[2:10] not in scrolled # …at the cost of the time lane, slid off left
|
||||
assert "Alice" not in scrolled # …at the cost of the lanes that slid off the left
|
||||
|
||||
for _ in range(40):
|
||||
screen.handle("left")
|
||||
|
||||
Reference in new issue
Block a user