Files
MeshTerm/meshterm/tools/nodes.py
T
Jean-Pierre MartineauandClaude Fable 5 b40caf6891 Nodes screen: the Time Machine picker's node list, now shared
The picker's lane layout - NAME/HEARD/PKTS/HASH under a sort-aware
header, own node pinned, Ctrl+arrow sorting, type-to-filter, a
content-sized name lane flexing the hash lane - moves to a shared
ui/nodelist.py (NodeRow + NodeListScreen). The Time Machine picker
becomes a thin subclass over stored history; the Nodes screen is
rebuilt as the same list over the device's contacts, replacing its
plain-arrow-sorted table (its ring gains the hash column). Enter is
deliberately inert on the Nodes screen for now - per-node actions
come later. The one-shot CLI keeps the static table.

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

91 lines
3.6 KiB
Python

"""The ``nodes`` tool: list this node and the contacts it knows about."""
from __future__ import annotations
from typing import Any
import typer
from ..context import AppContext
from .base import Tool, ToolResult, register
@register
class NodesTool(Tool):
"""List this node and its known contacts, each with its path-hash prefix."""
name = "nodes"
title = "Nodes"
icon = "👥"
help = "List this node and known contacts (last heard, packets, type, key)"
category = "Mesh"
order = 10 # right under the Dashboard: the "who's out there" view
async def run(self, ctx: AppContext, params: dict[str, Any]) -> ToolResult:
"""Query the device and render the node/contact list.
Args:
ctx: Shared application context.
params: Unused.
Returns:
A :class:`ToolResult` summarizing the number of known contacts.
"""
from ..ui.surface import TuiUi
from ..ui.widgets import NodesSort, nodes_table
# Read through the session cache: on a busy node the contacts table is a slow
# round-trip, and re-fetching it (plus self-info) on every menu visit is a chief cause
# of sluggish navigation. The cache holds them for the session and refreshes contacts
# in the background (see :class:`~meshterm.services.device_state.DeviceState`).
info = await ctx.devstate.self_info()
contacts = await ctx.devstate.contacts()
# The path-hash prefix — the leading slice of a key a forced path addresses — is
# ``mode + 1`` bytes wide; highlight it in every key. The mode is an optional read,
# so fall back to no highlighting when the firmware doesn't report it.
try:
mode = await ctx.devstate.path_hash_mode()
except Exception: # noqa: BLE001 - optional read; absence just skips highlighting
mode = None
prefix_bytes = (mode + 1) if isinstance(mode, int) and 0 <= mode <= 3 else 0
# Overheard-packet tallies from background monitoring, keyed by node hash; a
# contact we've never passively overheard simply has no entry.
counts = {n.node: n.count for n in ctx.repo.heard_nodes() if n.node}
self_name = str(info.get("name") or "this node")
self_key = str(info.get("public_key") or "")
sort_name = str(params.get("sort") or "name")
# In the menu, hand the list to the interactive screen so the Ctrl+arrows re-sort it
# live — its ring spans the shared node list's four columns (hash included); on the
# scripted CLI, render the static table once in the requested order.
if isinstance(ctx.ui, TuiUi):
from ..ui.nodelist import SORT_COLUMNS, SORT_OPENS_ASCENDING
from ..ui.nodes_screen import open_nodes
sort = NodesSort.from_name(sort_name, SORT_COLUMNS, SORT_OPENS_ASCENDING)
await open_nodes(ctx, self_name, self_key, contacts, prefix_bytes, counts, sort)
else:
sort = NodesSort.from_name(sort_name)
ctx.ui.show(nodes_table(self_name, self_key, contacts, prefix_bytes, counts, sort))
return ToolResult(summary={"contacts": len(contacts)})
def register_cli(self, app: typer.Typer) -> None:
"""Register the ``nodes`` subcommand.
Args:
app: The Typer application.
"""
from ..cli import run_tool_command
@app.command(name=self.name, help=self.help)
def _nodes(
sort: str = typer.Option(
"name", "--sort", "-s", help="Order contacts by: name, heard, packets"
),
) -> None:
run_tool_command(self, {"sort": sort})