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>
117 lines
4.5 KiB
Python
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)
|