Enter on the live feed's pin opens the packet viewer on the pin
The viewer now opens on the stop the feed has. On the pin (^), the card follows the stream and the feed stays pinned; on a selected packet, the viewer holds that packet as before. Enter no longer drops the pin. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8f7e7ec48
commit
db62ddf701
4 files changed
+87
-27
No files matched your search
@@ -57,9 +57,11 @@ itself with the same ``↑``/``↓``. If an overheard channel-text packet names
|
||||
which we have the key, the viewer decrypts it. **The viewer has the two stops too.** It
|
||||
holds the same pin above the newest packet, so ``↑`` from the top follows the stream
|
||||
there too (each packet that arrives becomes the card that the user reads). The viewer
|
||||
also moves the cursor of the feed with it, the pin included. Thus, when the dialog
|
||||
closes, the feed is on the stop that the user had in the dialog, not on the stop that
|
||||
the user left when the dialog opened.
|
||||
opens on the stop that the feed has. Enter on the pin opens the viewer on the pin, so
|
||||
the card follows the stream immediately, and Enter on a selected packet opens that
|
||||
packet and holds it. The viewer also moves the cursor of the feed with it, the pin
|
||||
included. Thus, when the dialog closes, the feed is on the stop that the user had in the
|
||||
dialog, not on the stop that the user left when the dialog opened.
|
||||
|
||||
The screen has no subscriptions of its own. The opener (:func:`open_livefeed`) connects
|
||||
the hub subscription and the paint that occurs one time each second, and removes them
|
||||
@@ -556,14 +558,17 @@ class LiveFeedScreen(Screen):
|
||||
|
||||
When the user pages in the viewer, the feed highlight moves with it (through
|
||||
``on_navigate``). Thus, when the viewer closes, the feed is on the packet that the
|
||||
user saw last. For the same reason, this method removes the pin when it opens the
|
||||
viewer. Enter names this packet. If the pin stays, the packets that arrive during a
|
||||
long read move the cursor off this packet. Then, when the viewer closes, the feed
|
||||
is on a row that the user did not select.
|
||||
user saw last.
|
||||
|
||||
The viewer opens on the stop that the feed has, and Enter does not change it. On
|
||||
the pin, the viewer opens on the pin too, and the card follows the stream. On a
|
||||
selected packet, the viewer holds that packet. JP decided this on 2026-10-10.
|
||||
Before, Enter removed the pin, because Enter named the packet under it. But a user
|
||||
who watches the feed and opens the card continues to watch, and ``↓`` in the viewer
|
||||
holds the visible packet with one key press.
|
||||
"""
|
||||
if self._selected is None or not self._feed:
|
||||
return
|
||||
self._select_row(self._selected)
|
||||
|
||||
def follow(entry: PacketEntry) -> None:
|
||||
# The feed may have more rows after the snapshot. Thus find the entry itself.
|
||||
@@ -575,6 +580,7 @@ class LiveFeedScreen(Screen):
|
||||
viewer = PacketViewer(
|
||||
list(self._feed),
|
||||
self._selected,
|
||||
pinned=self._pinned,
|
||||
resolve=self._resolve,
|
||||
prefix_bytes=self._prefix_bytes,
|
||||
self_name=self._self_name,
|
||||
|
||||
@@ -42,9 +42,9 @@ packet, and Home from anywhere, go to the **pin**. On the pin, the viewer holds
|
||||
position instead of a packet. Thus each new packet becomes the card that the user reads,
|
||||
and the title says ``following`` instead of ``n/total``. ``↓`` steps off the pin onto
|
||||
the packet that shows at that moment. That packet then moves down the list as newer
|
||||
packets arrive. Thus a user who opened a packet to read it keeps it, and a user who
|
||||
walked up to the top gets the stream. These are the same two stops, with the same keys,
|
||||
as the feed under the dialog.
|
||||
packets arrive. The viewer opens on the stop that the list had (``pinned``). Thus a user
|
||||
who opened a packet to read it keeps it, and a user who watched the stream continues to
|
||||
watch it. These are the same two stops, with the same keys, as the feed under the dialog.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -465,6 +465,7 @@ class PacketViewer(Screen):
|
||||
self_name: str | None = None,
|
||||
on_navigate: Callable[[PacketEntry], None] | None = None,
|
||||
on_pin: Callable[[], None] | None = None,
|
||||
pinned: bool = False,
|
||||
channels: Sequence[tuple[str, bytes]] = (),
|
||||
source: Callable[[], Sequence[PacketEntry]] | None = None,
|
||||
type_of: TypeOf | None = None,
|
||||
@@ -486,6 +487,10 @@ class PacketViewer(Screen):
|
||||
same stop (the live feed) can also follow the stream, and still follows it
|
||||
when the dialog closes. It is never called without a ``source``, because
|
||||
the pin is a stop only where the list is live.
|
||||
pinned: Whether to open on the pin instead of on the packet at ``index``.
|
||||
The live feed gives its own stop here, so the viewer follows the stream
|
||||
when the feed did. The open does not call ``on_pin``, because the list is
|
||||
already on the pin. Without a ``source``, the viewer ignores this value.
|
||||
channels: The configured channels of the device, as ``(name, secret)``
|
||||
pairs. The viewer tries them on the channel-text packet of an overheard
|
||||
``packet`` entry (refer to
|
||||
@@ -532,18 +537,19 @@ class PacketViewer(Screen):
|
||||
self._key_of = key_of
|
||||
self._scope_of: ScopeOf = scope_of or unnamed_scope
|
||||
self._message_scope = message_scope
|
||||
#: The packet that shows now, followed by identity. Thus a live prepend to the
|
||||
#: source (which moves each index) never moves the viewer onto another packet.
|
||||
self._current: PacketEntry | None = self._entries[self._index] if self._entries else None
|
||||
#: Whether the viewer is on the pin: the stop above the newest packet, which
|
||||
#: holds the top position instead of a packet. It means that ``_index == 0``. The
|
||||
#: pin is a mode on the newest entry, so all the code that reads the viewer (the
|
||||
#: card, the scroll, the cache) needs no special case. Only the title and the
|
||||
#: moves are different. An open never goes to the pin, because Enter selected
|
||||
#: this packet (refer to
|
||||
#: :meth:`~meshterm.ui.livefeed_screen.LiveFeedScreen._open_packet`). Thus the
|
||||
#: user walks up to the stream on purpose, or not at all.
|
||||
self._pinned = False
|
||||
#: moves are different. An open goes to the pin only when the list was on its own
|
||||
#: pin (refer to
|
||||
#: :meth:`~meshterm.ui.livefeed_screen.LiveFeedScreen._open_packet`).
|
||||
self._pinned = pinned and self._can_pin and bool(self._entries)
|
||||
if self._pinned:
|
||||
self._index = 0
|
||||
#: The packet that shows now, followed by identity. Thus a live prepend to the
|
||||
#: source (which moves each index) never moves the viewer onto another packet.
|
||||
self._current: PacketEntry | None = self._entries[self._index] if self._entries else None
|
||||
self._update_footer()
|
||||
self._set_title()
|
||||
|
||||
|
||||
+26
-7
@@ -366,12 +366,30 @@ def test_livefeed_home_resumes_following_from_deep_in_the_history() -> None:
|
||||
assert screen._selected == 0 # the feed follows again, and does not stay on the old newest
|
||||
|
||||
|
||||
def test_livefeed_opening_a_packet_drops_the_pin() -> None:
|
||||
"""Enter names this packet, so an arrival during a long read cannot move the highlight."""
|
||||
def test_livefeed_opening_a_packet_keeps_the_pin() -> None:
|
||||
"""Enter on the pin opens the viewer on the pin, so the feed and the card both follow.
|
||||
|
||||
JP decided this on 2026-10-10. Before, Enter removed the pin.
|
||||
"""
|
||||
screen = _screen(seed=[_obs(node="n0", age_s=1)])
|
||||
screen.future = _Fut()
|
||||
assert screen._pinned
|
||||
screen._select_row(0) # this is what _open_packet does before it floats the viewer
|
||||
screen.handle("enter")
|
||||
viewer = screen._session.opened
|
||||
assert viewer is not None and viewer._pinned and "following" in viewer.title
|
||||
assert screen._pinned, "Enter knocked the feed out of the stream"
|
||||
|
||||
screen.on_event(MeshEvent.observation_event(_obs(node="a1b2")))
|
||||
assert screen._selected == 0, "the pin let go of the top"
|
||||
assert "Alice" in _plain(viewer.render_body(72)), "the card did not follow the stream"
|
||||
|
||||
|
||||
def test_livefeed_opening_a_selected_packet_holds_it() -> None:
|
||||
"""Enter on a selected packet holds it, so an arrival during a long read cannot move it."""
|
||||
screen = _screen(seed=[_obs(node="n0", age_s=1)])
|
||||
screen.handle("down") # off the pin, onto the newest packet
|
||||
screen.handle("enter")
|
||||
viewer = screen._session.opened
|
||||
assert viewer is not None and not viewer._pinned
|
||||
assert not screen._pinned and screen._selected == 0
|
||||
screen.on_event(MeshEvent.observation_event(_obs(node="a1b2")))
|
||||
assert screen._selected == 1, "the cursor left the packet being viewed"
|
||||
@@ -380,11 +398,12 @@ def test_livefeed_opening_a_packet_drops_the_pin() -> None:
|
||||
def test_livefeed_viewer_that_walks_up_to_the_stream_re_pins_the_feed() -> None:
|
||||
"""The viewer has the second stop of this screen, and it returns the pin through it.
|
||||
|
||||
Enter still names this packet, so the dialog opens with one packet. The user can press
|
||||
up at the top of the viewer. This is the same move as up at the top of the feed. Thus
|
||||
the feed follows the stream again, and it still follows when the dialog closes.
|
||||
Enter on a selected packet opens the dialog on that packet. The user can press up at
|
||||
the top of the viewer. This is the same move as up at the top of the feed. Thus the
|
||||
feed follows the stream again, and it still follows when the dialog closes.
|
||||
"""
|
||||
screen = _screen(seed=[_obs(node="n0", age_s=2), _obs(node="n1", age_s=1)])
|
||||
screen.handle("down") # off the pin, onto the newest packet
|
||||
screen.handle("enter")
|
||||
viewer = screen._session.opened
|
||||
assert viewer is not None and not screen._pinned
|
||||
|
||||
@@ -158,7 +158,7 @@ def test_packet_viewer_follows_the_stream_off_the_top_of_a_live_list() -> None:
|
||||
source=lambda: list(feed),
|
||||
on_pin=lambda: pinned_by_viewer.append(True),
|
||||
)
|
||||
assert not viewer._pinned, "opening a packet named that packet, not the stream"
|
||||
assert not viewer._pinned, "an open without pinned holds the packet, not the stream"
|
||||
assert "↑ follow" in viewer.footer_hint
|
||||
|
||||
viewer.handle("up")
|
||||
@@ -176,6 +176,35 @@ def test_packet_viewer_follows_the_stream_off_the_top_of_a_live_list() -> None:
|
||||
assert "1/2" in viewer.title # the viewer holds that packet, which now moves down the list
|
||||
|
||||
|
||||
def test_packet_viewer_opens_on_the_pin_of_a_live_list() -> None:
|
||||
"""``pinned`` opens on the pin: the card follows the stream from the first paint.
|
||||
|
||||
The open does not call ``on_pin``, because the list is already on its own pin. Over a
|
||||
snapshot, ``pinned`` does nothing, because there is no stream to follow.
|
||||
"""
|
||||
old = PacketEntry(when=utcnow(), kind="advert", node="aa")
|
||||
feed = [old]
|
||||
pinned_by_viewer = []
|
||||
viewer = PacketViewer(
|
||||
list(feed),
|
||||
0,
|
||||
pinned=True,
|
||||
resolve=lambda h: "",
|
||||
source=lambda: list(feed),
|
||||
on_pin=lambda: pinned_by_viewer.append(True),
|
||||
)
|
||||
assert viewer._pinned and viewer.title.endswith("· following")
|
||||
assert "↓ hold packet" in viewer.footer_hint
|
||||
assert pinned_by_viewer == []
|
||||
|
||||
newer = PacketEntry(when=utcnow(), kind="message", node="bb", text="new!")
|
||||
feed.insert(0, newer)
|
||||
assert "new!" in _plain(viewer.render_body(80))
|
||||
|
||||
snapshot = PacketViewer([newer, old], 0, pinned=True, resolve=lambda h: "")
|
||||
assert not snapshot._pinned and "following" not in snapshot.title
|
||||
|
||||
|
||||
def test_packet_viewer_over_a_snapshot_has_nothing_to_follow() -> None:
|
||||
"""With no live source there is no pin. ``↑`` at the newest packet clamps, as before."""
|
||||
newer = PacketEntry(when=utcnow(), kind="message", node="bb", text="new!")
|
||||
|
||||
Reference in new issue
Block a user