diff --git a/meshterm/ui/node_detail_screen.py b/meshterm/ui/node_detail_screen.py index 0bd2c74..2a4d711 100644 --- a/meshterm/ui/node_detail_screen.py +++ b/meshterm/ui/node_detail_screen.py @@ -1538,10 +1538,30 @@ async def _archive_contact(ctx: AppContext, contact: Contact, self_key: str, lab assert isinstance(ctx.ui, TuiUi) # guaranteed by open_node_detail session = ctx.ui.session + # The contact is named as the Contacts list names it: the type mark in its own colour, + # then the name in its key-derived hue (a bare id standing in for a nameless one is + # ``node.unknown``). The prose carries the amber span by span rather than as the + # prompt's base style, whose bold Rich would merge into the mark. + glyph, glyph_style = NODE_GLYPHS.get(contact.node_type, DEFAULT_GLYPH) + prompt = Text() + prompt.append("Archive ", style="warn") + prompt.append(f"{glyph} ", style=glyph_style) + prompt.append( + label, + style=( + name_style(label, contact.public_key or contact.key_prefix) + if contact.name + else "node.unknown" + ), + ) + prompt.append( + "? It comes off this device's contact list, freeing a slot for a new one. MeshTerm " + "keeps it — with its key, reception history and messages — and you can restore it " + "at any time.", + style="warn", + ) if not await ctx.ui.dialog( - f"Archive {label}? It comes off this device's contact list, freeing a slot for a " - "new one. MeshTerm keeps it — with its key, reception history and messages — and " - "you can restore it at any time.", + prompt, [("Cancel", False), ("Archive", True)], title="Archive contact", default=1, diff --git a/meshterm/ui/surface.py b/meshterm/ui/surface.py index 670dfe2..743f733 100644 --- a/meshterm/ui/surface.py +++ b/meshterm/ui/surface.py @@ -330,7 +330,7 @@ class Ui: async def dialog( self, - prompt: str, + prompt: str | Text, buttons: list[tuple[str, Any]], *, title: str = "", @@ -348,6 +348,10 @@ class Ui: sibling. Buttons follow the platform-dialog convention: the safe way out sits on the left and the committing action on the right, which is also the sensible ``default`` so Enter commits it while Esc always backs out. + + A pre-styled :class:`~rich.text.Text` prompt is drawn as it is, so it can name a + node in its own colours; the tier then themes only the border, and the prompt's + prose carries the tier's colour itself. """ raise NotImplementedError @@ -634,7 +638,7 @@ class PlainUi(Ui): async def dialog( self, - prompt: str, + prompt: str | Text, buttons: list[tuple[str, Any]], *, title: str = "", @@ -905,7 +909,7 @@ class TuiUi(Ui): async def dialog( self, - prompt: str, + prompt: str | Text, buttons: list[tuple[str, Any]], *, title: str = "", diff --git a/tests/test_device_config.py b/tests/test_device_config.py index 2b2de6c..1c04b67 100644 --- a/tests/test_device_config.py +++ b/tests/test_device_config.py @@ -1035,12 +1035,15 @@ async def test_archiving_one_contact_takes_it_off_the_device_and_keeps_it() -> N import logging from types import SimpleNamespace - from meshterm.core.models import Contact + from meshterm.core.models import NODE_TYPE_REPEATER, Contact from meshterm.ui.node_detail_screen import _archive_contact from meshterm.ui.surface import TuiUi + from meshterm.ui.theme import name_style from meshterm.ui.tui.session import TuiSession - hub = Contact(name="Hub", public_key="3d" * 32, key_prefix="3d" * 6) + hub = Contact( + name="Hub", public_key="3d" * 32, key_prefix="3d" * 6, node_type=NODE_TYPE_REPEATER + ) removed: list[str] = [] archived: list[tuple[str, str]] = [] invalidated: list[bool] = [] @@ -1073,7 +1076,19 @@ async def test_archiving_one_contact_takes_it_off_the_device_and_keeps_it() -> N assert asked[0]["danger"] is True and "destructive" not in asked[0] assert [label for label, _v in asked[0]["buttons"]] == ["Cancel", "Archive"] assert asked[0]["default"] == 1 - assert "restore it at any time" in asked[0]["prompt"] + prompt = asked[0]["prompt"] + assert "restore it at any time" in prompt + # The contact is named as the Contacts list names it — its type mark to the left of the + # name, each in its own colour — while the sentence around them keeps the amber. + assert prompt.plain.startswith("Archive ▲ Hub? ") + + def styles_at(index: int) -> list[str]: + return [str(span.style) for span in prompt.spans if span.start <= index < span.end] + + assert styles_at(prompt.plain.index("▲")) == ["type.repeater"] + assert styles_at(prompt.plain.index("Hub")) == [name_style("Hub", "3d" * 32)] + assert styles_at(0) == styles_at(prompt.plain.index("restore")) == ["warn"] + assert not prompt.style, "a base style would merge the amber's bold into the mark" # Off the radio, kept here, and the cached contact list dropped so the list re-reads. assert removed == ["Hub"] assert archived == [("cc" * 32, "3d" * 32)]