Every select list now builds its closing furniture through one module, so the conventions hold by construction instead of by copy-paste: * Back is THE exit word — bare, no arrow, no icon — with exactly one blank separator above it (back_rows). Courier and Watchtower lose their "← Back"; the rules popover's "← Close" becomes Back too. * The staged-changes exit pair (✓ Apply … / ✗ Back — discard) moves to exit_rows, shared by the config editor and repeater admin, along with changes_phrase and one confirm_discard dialog (apply/send verb parametrised). * Label + description rows share menu_rows (cell-padded lanes): Device actions, channel detail, repeater actions, and — converted from the inline "label — description" style — Send advert, Identity key, and the channel manager's Add-a-channel section, whose rows now read as two aligned lanes with the trailing … that marks "opens a prompt". * Grouped-list headings share section_heading (── Label ── in accent): Watchtower, Courier, the Time Machine picker, and the admin picker (which also drops the stray 🔑 inside its heading). * Status marks normalize to ✓/✗ (U+2713/17): Watchtower's ✔ and the ✖ on Cancel-this-message / Stop-watching become err-tinted ✗ rows. * fit_cells replaces the four private _fit copies (channels, dashboard, time machine, chat picker) and measures display cells, so a wide glyph in a name can't skew a lane; empty-state notes drop their parentheses ("no channels configured yet", "no matches"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
83 lines
3.1 KiB
Python
83 lines
3.1 KiB
Python
"""The admin-node picker: choose a remote node you hold (or will enter) credentials for.
|
|
|
|
Shared by the tools that administer remote nodes over the mesh — TX optimize and the
|
|
repeater admin screen — so "pick the node to manage" reads identically everywhere.
|
|
Credentialed nodes lead the list in their own 🔑 section (those are the nodes previously
|
|
administered, the likeliest picks), repeaters and room servers follow, then everything
|
|
else; each section orders by how recently the node was heard. Any contact with a public
|
|
key is offerable, since holding a password is a fact about the *user*, not the node.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Optional
|
|
|
|
from rich.text import Text
|
|
|
|
from ..core.models import NODE_TYPE_REPEATER, NODE_TYPE_ROOM, Contact
|
|
|
|
if TYPE_CHECKING:
|
|
from ..context import AppContext
|
|
|
|
|
|
async def pick_admin_node(
|
|
ctx: "AppContext",
|
|
contacts: list[Contact],
|
|
*,
|
|
title: str,
|
|
prompt: str,
|
|
) -> Optional[Contact]:
|
|
"""Pick a remote node to administer, credentialed and infrastructure nodes first.
|
|
|
|
Args:
|
|
ctx: Shared application context (for the UI surface and the admin store).
|
|
contacts: The device's known contacts.
|
|
title: The select screen's heading (names the calling feature).
|
|
prompt: One line above the list saying what the pick is for.
|
|
|
|
Returns:
|
|
The chosen contact, or ``None`` if cancelled (or there is nothing to pick).
|
|
"""
|
|
from .menus import back_rows, section_heading
|
|
from .tui import Choice
|
|
from .widgets import _DEFAULT_GLYPH, _NODE_GLYPHS
|
|
|
|
candidates = [c for c in contacts if (c.public_key or c.key_prefix).strip()]
|
|
if not candidates:
|
|
ctx.ui.note("[err]no contacts with a key — receive an advert first[/err]")
|
|
await ctx.ui.present(title=title)
|
|
return None
|
|
|
|
def row(contact: Contact) -> Choice:
|
|
glyph, style = _NODE_GLYPHS.get(contact.node_type, _DEFAULT_GLYPH)
|
|
label = Text(glyph, style=style)
|
|
label.append(f" {contact.name}")
|
|
return Choice(title=label, value=contact.name)
|
|
|
|
def recency(contact: Contact) -> float:
|
|
return -(contact.last_seen.timestamp() if contact.last_seen else 0.0)
|
|
|
|
remembered = [c for c in candidates if ctx.admin_store.get(c) is not None]
|
|
infrastructure = [
|
|
c for c in candidates
|
|
if c not in remembered and c.node_type in (NODE_TYPE_REPEATER, NODE_TYPE_ROOM)
|
|
]
|
|
others = [c for c in candidates if c not in remembered and c not in infrastructure]
|
|
|
|
items: list = []
|
|
if remembered:
|
|
items.append(section_heading("Remembered admins"))
|
|
items.extend(row(c) for c in sorted(remembered, key=recency))
|
|
if infrastructure:
|
|
items.append(section_heading("Repeaters & rooms"))
|
|
items.extend(row(c) for c in sorted(infrastructure, key=recency))
|
|
if others:
|
|
items.append(section_heading("Other contacts"))
|
|
items.extend(row(c) for c in sorted(others, key=recency))
|
|
items.extend(back_rows())
|
|
|
|
choice = await ctx.ui.select(title, items, prompt=prompt, wrap=False)
|
|
if choice is None:
|
|
return None
|
|
return next((c for c in candidates if c.name == choice), None)
|