The packet viewer states a flood's scope on a row of its own
The region rode the route row as a trailing atom (flood · scope harbour), easy to miss on a card opened from the feed's SCOPE lane to see it. Route now says how the packet was routed (flood, direct) and a labelled scope row underneath says where: the region, 'unknown region · code 3fa1', or 'unscoped'. A direct packet has no scope row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7f59bfa5a0
commit
fcd70c2e2e
3 files changed
+61
-42
No files matched your search
+2
-2
@@ -38,8 +38,8 @@ one caveat SemVer makes for a leading zero: while the major version is still `0`
|
||||
`channels scope INDEX [REGION]` reads or sets it, `channels list` gains a `SCOPE` column,
|
||||
and `chat send --channel N --scope REGION` (or `--scope '*'` for unscoped) overrides it
|
||||
for one message. Scoped sending needs firmware 1.10; `*` needs 1.16.
|
||||
- **Packets say which region a flood was scoped to.** The packet viewer's route row, the
|
||||
message paths title, and a new `SCOPE` lane in the live feed name the region a scoped
|
||||
- **Packets say which region a flood was scoped to.** A `scope` row in the packet viewer,
|
||||
the message paths title, and a new `SCOPE` lane in the live feed name the region a scoped
|
||||
flood was sent into, or its code when no region known here matches it. `monitor` gains
|
||||
the same `SCOPE` column and a `scope` field in `--json`. A scoped packet is kept with
|
||||
what is needed to name its region later, so one heard today is named once a repeater or
|
||||
|
||||
@@ -940,7 +940,10 @@ class PacketViewer(Screen):
|
||||
rows.extend(self._addressing_rows(raw))
|
||||
route = raw.get("route_typename")
|
||||
if route:
|
||||
rows.append(("route", self._route_text(route, self._entry_scope(entry))))
|
||||
scope = self._entry_scope(entry)
|
||||
rows.append(("route", self._route_text(route, scope)))
|
||||
if scope is not None:
|
||||
rows.append(("scope", self._scope_text(scope)))
|
||||
rows.append(("via", self._via_path(entry)))
|
||||
# The figure the chain above encodes but never states, hung under it as the app's
|
||||
# own hop atom — the same one the node page's routes and the trace scenarios carry
|
||||
@@ -995,27 +998,39 @@ class PacketViewer(Screen):
|
||||
|
||||
@staticmethod
|
||||
def _route_text(route: str, scope: Scope | None) -> Text:
|
||||
"""The ``route`` row: how the frame was routed, and for a flood, where to.
|
||||
"""The ``route`` row: how the frame was routed — ``flood`` or ``direct``.
|
||||
|
||||
A flood reads ``flood`` and then its scope as a chained atom
|
||||
(:func:`~meshterm.ui.widgets.scope_text`) — ``flood · scope harbour``,
|
||||
``flood · unknown scope 3fa1``, ``flood · unscoped`` — rather than the library's
|
||||
``tc flood``, which named the wire mechanism (transport codes) and left the
|
||||
reader to know that it meant *scoped*, and to which region nobody could say.
|
||||
A scoped and an unscoped flood are one routing; the scope is what differs, so it
|
||||
is the scope that is spelled out.
|
||||
A scoped flood reads ``flood``, not the library's ``tc flood``: that named the wire
|
||||
mechanism (transport codes) and left the reader to know it meant *scoped*. A scoped
|
||||
and an unscoped flood are one routing; where they differ is the region, and that
|
||||
has a row of its own underneath (:meth:`_scope_text`).
|
||||
|
||||
A direct frame keeps its route word alone. A repeater never region-filters a
|
||||
direct packet, so it has no scope, and a ``TC_DIRECT`` frame's codes are the
|
||||
"to nowhere" pair a contact share uses, not a region — a word there would claim a
|
||||
meaning the frame does not carry.
|
||||
A direct frame keeps its route word alone. A ``TC_DIRECT`` frame's codes are the
|
||||
"to nowhere" pair a contact share uses, not a region, so it has no scope row.
|
||||
"""
|
||||
if scope is None:
|
||||
return Text(route.replace("_", " ").lower())
|
||||
text = Text("flood")
|
||||
text.append(" · ", style="muted")
|
||||
text.append_text(scope_text(scope))
|
||||
return text
|
||||
return Text("flood")
|
||||
|
||||
@staticmethod
|
||||
def _scope_text(scope: Scope) -> Text:
|
||||
"""The ``scope`` row: the region a flood was sent into, stated on its own line.
|
||||
|
||||
Its own row, labelled, rather than an atom trailing the route: it is what a reader
|
||||
opened the card from the feed's SCOPE lane to see, and a card has the room to say
|
||||
it in full where the lane had ten cells. The label says *scope*, so the value is
|
||||
the region alone (``harbour``, in the ``scope`` style); a region no name known here
|
||||
reproduces reads ``unknown region · code 3fa1``, the code kept so two packets can be
|
||||
seen to share it; a plain flood reads ``unscoped``.
|
||||
"""
|
||||
if scope.state == "scoped" and scope.region:
|
||||
return scope_text(scope, bare=True)
|
||||
if scope.scoped:
|
||||
text = Text("unknown region", style="muted")
|
||||
if scope.code:
|
||||
text.append(f" · code {scope.code}", style="muted")
|
||||
return text
|
||||
return Text("unscoped", style="muted")
|
||||
|
||||
def _addressing_rows(self, raw: dict) -> list[tuple[str, RenderableType]]:
|
||||
"""Who a frame was for, who it says it was from, and the token it carries.
|
||||
|
||||
+27
-23
@@ -86,44 +86,48 @@ def _col(line: str, needle: str) -> int:
|
||||
# -- the packet viewer -------------------------------------------------------------------
|
||||
|
||||
|
||||
def _route_row(raw: dict, scope_of=None) -> str: # noqa: ANN001
|
||||
"""The viewer's ``route`` row for one frame, as plain text."""
|
||||
def _card_row(raw: dict, label: str, scope_of=None) -> str | None: # noqa: ANN001
|
||||
"""The value of the viewer's ``label`` row for one frame, as plain text (``None``: absent)."""
|
||||
entry = PacketEntry(when=utcnow(), kind="packet", path="3d", raw=raw)
|
||||
body = _stripped(
|
||||
PacketViewer([entry], 0, resolve=lambda h: "", scope_of=scope_of).render_body(80)
|
||||
)
|
||||
return next(line for line in body if line.startswith("route")).rstrip()
|
||||
row = next((line for line in body if line.split(None, 1)[:1] == [label]), None)
|
||||
return None if row is None else row.split(None, 1)[1].rstrip()
|
||||
|
||||
|
||||
def _has_scope_row(text: str, value: str) -> bool:
|
||||
"""Whether a rendered card carries a ``scope`` row reading ``value``."""
|
||||
return re.search(rf"^scope\s+{re.escape(value)}\s*$", text, re.M) is not None
|
||||
|
||||
|
||||
def test_the_viewer_names_the_region_a_flood_was_scoped_to() -> None:
|
||||
"""``tc flood`` said *how* and never *where*; the row now says flood, then the scope."""
|
||||
assert _route_row(_scoped("harbour"), _knows("harbour")).split(None, 1)[1] == (
|
||||
"flood · scope harbour"
|
||||
)
|
||||
"""``tc flood`` said *how* and never *where*: route says flood, its own row the region."""
|
||||
assert _card_row(_scoped("harbour"), "route", _knows("harbour")) == "flood"
|
||||
assert _card_row(_scoped("harbour"), "scope", _knows("harbour")) == "harbour"
|
||||
|
||||
|
||||
def test_the_viewer_shows_an_unnamed_scopes_code_and_a_plain_flood_as_unscoped() -> None:
|
||||
"""A scope no known name reproduces keeps its code; a plain flood reads unscoped."""
|
||||
assert _route_row(_scoped("elsewhere"), _knows("harbour")).endswith(
|
||||
f"flood · unknown scope {_code('elsewhere')}"
|
||||
assert _card_row(_scoped("elsewhere"), "scope", _knows("harbour")) == (
|
||||
f"unknown region · code {_code('elsewhere')}"
|
||||
)
|
||||
assert _route_row(_FLOOD, _knows("harbour")).endswith("flood · unscoped")
|
||||
assert _card_row(_FLOOD, "scope", _knows("harbour")) == "unscoped"
|
||||
|
||||
|
||||
def test_the_viewer_without_a_store_still_tells_scoped_from_unscoped() -> None:
|
||||
"""No names at hand is less of the truth, never a wrong one: the code stands in."""
|
||||
assert _route_row(_scoped("harbour")).endswith(f"flood · unknown scope {_code('harbour')}")
|
||||
assert _route_row(_FLOOD).endswith("flood · unscoped")
|
||||
assert _card_row(_scoped("harbour"), "scope") == f"unknown region · code {_code('harbour')}"
|
||||
assert _card_row(_FLOOD, "scope") == "unscoped"
|
||||
|
||||
|
||||
def test_the_viewer_never_gives_a_direct_frame_a_scope() -> None:
|
||||
"""A repeater never region-filters a direct packet, so its route row is the word alone."""
|
||||
assert _route_row(_DIRECT, _knows("harbour")).split() == ["route", "direct"]
|
||||
assert _route_row({**_DIRECT, "route_typename": "TC_DIRECT"}).split() == [
|
||||
"route",
|
||||
"tc",
|
||||
"direct",
|
||||
]
|
||||
"""A repeater never region-filters a direct packet: its route word alone, no scope row."""
|
||||
assert _card_row(_DIRECT, "route", _knows("harbour")) == "direct"
|
||||
assert _card_row(_DIRECT, "scope", _knows("harbour")) is None
|
||||
tc_direct = {**_DIRECT, "route_typename": "TC_DIRECT"}
|
||||
assert _card_row(tc_direct, "route") == "tc direct"
|
||||
assert _card_row(tc_direct, "scope") is None
|
||||
|
||||
|
||||
def test_the_viewer_keeps_the_scope_body_out_of_the_raw_dump(tmp_path: Path) -> None:
|
||||
@@ -139,7 +143,7 @@ def test_the_viewer_keeps_the_scope_body_out_of_the_raw_dump(tmp_path: Path) ->
|
||||
scope_of=_knows("harbour"),
|
||||
).render_body(80)
|
||||
)
|
||||
assert "scope harbour" in body # a replayed frame resolves exactly as a live one
|
||||
assert _has_scope_row(body, "harbour") # a replayed frame resolves exactly as a live one
|
||||
assert "scope_body" not in body
|
||||
|
||||
|
||||
@@ -148,9 +152,9 @@ def test_the_viewer_renames_a_scope_the_moment_its_region_is_learned(tmp_path: P
|
||||
store = RegionStore(tmp_path / "regions.json")
|
||||
entry = PacketEntry(when=utcnow(), kind="packet", path="", raw=_scoped("harbour"))
|
||||
viewer = PacketViewer([entry], 0, resolve=lambda h: "", scope_of=store.scope_of)
|
||||
assert "scope harbour" not in _plain(viewer.render_body(80))
|
||||
assert not _has_scope_row(_plain(viewer.render_body(80)), "harbour")
|
||||
store.learn("harbour", "typed")
|
||||
assert "scope harbour" in _plain(viewer.render_body(80))
|
||||
assert _has_scope_row(_plain(viewer.render_body(80)), "harbour")
|
||||
|
||||
|
||||
# -- the live feed -----------------------------------------------------------------------
|
||||
@@ -220,7 +224,7 @@ def test_the_feed_hands_its_scope_reader_to_the_viewer() -> None:
|
||||
screen._session.run_screen = lambda viewer: opened.append(viewer) # noqa: SLF001
|
||||
screen._session.run_detached = lambda coro: None # noqa: SLF001
|
||||
screen.handle("enter")
|
||||
assert "flood · scope harbour" in _plain(opened[0].render_body(80))
|
||||
assert _has_scope_row(_plain(opened[0].render_body(80)), "harbour")
|
||||
|
||||
|
||||
# -- message paths -----------------------------------------------------------------------
|
||||
|
||||
Reference in new issue
Block a user