Files
MeshTerm/meshterm/ui/nodes_screen.py
T
Jean-Pierre MartineauandClaude Fable 5 56cc220ee3 Lexicon: a lane shows the key, the hash is the lit slice
The node lists' HASH column header becomes KEY: the lane renders as
full a key as the caller resolved, and the hash is only the short
path-hash-mode prefix highlighted_hash lights inside it. CLAUDE.md's
key/hash lexicon rows now carry the sharper definitions so UX text
never again calls a truncated key a hash. The "hash" sort-ring id
stays, for saved-sort compatibility.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 00:43:07 -04:00

117 lines
4.5 KiB
Python

"""The interactive Nodes screen: the device's contacts in the shared sortable node list.
The Time Machine picker's presentation pointed at the companion's contact table: the same
aligned ``NAME · HEARD · PKTS · KEY`` lanes, our own node pinned first, the same Ctrl+arrow
sort (now including the key column) and type-to-filter — one node list app-wide, whatever
the data source (see :mod:`~meshterm.ui.nodelist`). Enter is deliberately inert for now: the
highlight is a cursor, not yet a selection — a per-node action will land on it later. The
one-shot CLI (``meshterm nodes --sort …``) still renders the static
:func:`~meshterm.ui.widgets.nodes_table`; only the menu gets the live list.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from .nodelist import NodeListScreen, NodeRow
from .widgets import NodesSort, _contact_pkts
if TYPE_CHECKING:
from ..context import AppContext
from ..core.models import Contact
#: A stable identity for the own-node row (its lane data has no key to stand on when no
#: device answered), so a re-sort can keep the highlight on it.
YOU = ("you",)
#: The Nodes list's footer: the shared list's grammar minus the Enter atom — pressing it
#: does nothing yet, so the hint doesn't advertise it.
_HINT = "↑↓ move · ^←→↑↓ sort · type filter · Esc back"
class NodesScreen(NodeListScreen):
"""A full-screen, Ctrl+arrow-sortable list of this node and its known contacts."""
def __init__(
self,
self_name: str,
self_key: str,
contacts: "list[Contact]",
prefix_bytes: int,
counts: dict[str, int],
sort: NodesSort,
) -> None:
"""Create the nodes screen over already-fetched contact data.
Args:
self_name: This node's advertised name.
self_key: This node's full public key (hex); blank renders a muted ``?``.
contacts: Known contacts to list under our own node.
prefix_bytes: Path-hash width in bytes to highlight in every key.
counts: Overheard-packet counts keyed by lowercased 12-hex node id; a contact
with no entry shows a faint ``—``.
sort: The initial sort; mutated in place by the Ctrl+arrows. Its ring should
span :data:`~meshterm.ui.nodelist.SORT_COLUMNS` so the hash sort is
reachable.
"""
rows = [NodeRow(value=YOU, name=self_name, key=self_key, you=True)]
for c in contacts:
rows.append(
NodeRow(
value=c.public_key or c.name,
name=c.name,
key=c.public_key,
node_type=c.node_type,
last_seen=c.last_seen,
count=_contact_pkts(c, counts),
)
)
super().__init__(
f"Nodes · {len(contacts)} known",
rows=rows,
prefix_bytes=prefix_bytes,
sort=sort,
footer_hint=_HINT,
)
def handle(self, action: str, data: str = "") -> None:
"""Swallow Enter; every other key is the shared list's.
The highlight is only a cursor until per-node actions land, so committing it
must neither act nor dismiss the screen — only Esc leaves.
"""
if action == "enter":
return
super().handle(action, data)
async def open_nodes(
ctx: "AppContext",
self_name: str,
self_key: str,
contacts: "list[Contact]",
prefix_bytes: int,
counts: dict[str, int],
sort: NodesSort,
) -> None:
"""Open the interactive, sortable nodes list and run until dismissed with Esc.
Args:
ctx: Shared application context (must be in the interactive menu).
self_name: This node's advertised name.
self_key: This node's full public key (hex).
contacts: Known contacts to list under our own node.
prefix_bytes: Path-hash width in bytes to highlight in every key.
counts: Overheard-packet counts keyed by lowercased 12-hex node id.
sort: The initial sort (column + direction) over the shared list's ring.
Raises:
RuntimeError: If called outside the interactive menu (no full-screen session).
"""
from .surface import TuiUi
if not isinstance(ctx.ui, TuiUi): # pragma: no cover - guarded by the menu-only caller
raise RuntimeError("the interactive nodes list is only available in the menu")
screen = NodesScreen(self_name, self_key, contacts, prefix_bytes, counts, sort)
await ctx.ui.session.run_screen(screen)