Show key popup: labelled blocks that wrap, not a table

Tables inside popups are border noise and steal the columns the values
need. Each field is now a muted header label with its value beneath, and
the hash, key, and link — strings that exist to be copied out whole —
fold across lines at the dialog width instead of dying at an ellipsis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Fable 5 committed 2026-07-11 15:05:10 -04:00
1 parent c7afe7998b
commit 14caab0dff
2 files changed
+57 -19

No files matched your search

+27 -19
View File
@@ -34,7 +34,6 @@ from typing import TYPE_CHECKING, Awaitable, Callable, Optional
from rich.cells import cell_len
from rich.console import Group
from rich.table import Table
from rich.text import Text
from ..core.channels import (
@@ -749,31 +748,40 @@ async def _show_share(
await ctx.ui.view(body, title=f"Share {name}", footer_hint="Esc back")
def _hash_text(full_hash: str) -> Text:
"""Render a full channel hash with its first byte (the MeshCore fingerprint) highlighted."""
text = Text(no_wrap=True)
text.append(full_hash[:2], style="brand") # the leading byte the companion app reports
text.append(full_hash[2:], style="muted")
return text
async def _show_key(ctx: "AppContext", slot: ChannelSlot) -> None:
"""Show a channel's name, key, hash, and share URL in a small table."""
table = Table(border_style="muted", expand=False, show_header=False)
table.add_column("", style="muted")
table.add_column("")
table.add_row("Name", Text(slot.name, style="brand"))
"""Show a channel's name, type, hash, key, and share link as labelled blocks.
Deliberately not a table: a bordered grid inside a popup is visual noise and wastes
the very columns the values need. Each field is instead a muted column-header-style
label with its value on the line beneath, and the long values — the hash, key, and
link exist to be copied out whole — *wrap* to the popup's width (``overflow="fold"``,
since they are single unbreakable words) rather than being chopped at an ellipsis.
The hash's first byte keeps the brand highlight: it is the two-character fingerprint
the MeshCore companion app reports.
"""
if slot.is_name_derived:
kind = "public (key from name)"
elif slot.is_public:
kind = "public (default channel)"
else:
kind = "private"
table.add_row("Type", kind)
table.add_row("Hash", _hash_text(slot.full_hash))
table.add_row("Key", Text(slot.secret.hex(), style="warn"))
table.add_row("Link", Text(share_url(slot.name, slot.secret), style="accent"))
await ctx.ui.view(table, title=f"Key — {slot.name}", footer_hint="Esc back")
hash_text = Text(overflow="fold")
hash_text.append(slot.full_hash[:2], style="brand")
hash_text.append(slot.full_hash[2:], style="muted")
fields: list[tuple[str, Text]] = [
("NAME", Text(slot.name, style="brand", overflow="fold")),
("TYPE", Text(kind)),
("HASH", hash_text),
("KEY", Text(slot.secret.hex(), style="warn", overflow="fold")),
("LINK", Text(share_url(slot.name, slot.secret), style="accent", overflow="fold")),
]
blocks: list[Text] = []
for label, value in fields:
if blocks:
blocks.append(Text())
blocks.append(Text(label, style="muted"))
blocks.append(value)
await ctx.ui.view(Group(*blocks), title=f"Key — {slot.name}", footer_hint="Esc back")
async def _open_chat(ctx: "AppContext", slot: ChannelSlot) -> None:
+30
View File
@@ -425,6 +425,36 @@ async def test_channel_rows_carry_stats_unread_and_lanes(ctx: AppContext) -> Non
assert label.spans[-1].style == "ok" # …and its newest cell reads as live traffic
async def test_show_key_popup_wraps_values_under_header_labels() -> None:
"""The key popup is labelled blocks, not a table, and long values wrap whole."""
from types import SimpleNamespace
from meshterm.ui.channels import _show_key
from meshterm.ui.tui.render import render_lines
captured: dict = {}
async def view(renderable, *, title="", footer_hint=""): # noqa: ANN001, ANN202
captured["renderable"] = renderable
captured["title"] = title
slot = ChannelSlot(idx=0, name="Ops", secret=bytes(range(16)))
await _show_key(SimpleNamespace(ui=SimpleNamespace(view=view)), slot)
assert captured["title"] == "Key — Ops"
# Render at a deliberately narrow dialog width: every value must survive whole,
# wrapped across lines, with no ellipsis truncation anywhere.
lines = [_ANSI.sub("", line) for line in render_lines(captured["renderable"], 40)]
text = "\n".join(lines)
for label in ("NAME", "TYPE", "HASH", "KEY", "LINK"):
assert label in text
assert "…" not in text
joined = text.replace("\n", "").replace(" ", "")
assert slot.secret.hex() in joined # the 32-hex key, reassembled across wraps
assert full_channel_hash(slot.secret) in joined # the 64-hex hash likewise
assert share_url("Ops", slot.secret).replace(" ", "") in joined
async def test_detail_summary_reads_slot_totals_and_unread(ctx: AppContext) -> None:
"""The detail screen's vital-signs line covers slot, totals, unread, and recency."""
from meshterm.core.models import ChatMessage