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:
1 parent
c7afe7998b
commit
14caab0dff
2 files changed
+57
-19
No files matched your search
+27
-19
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user