The Contacts title counts the archived too, abbreviating where it's narrow
"Contacts · 198 known + 123 archived", falling back to "Contacts · 198 + 123" where the frame's rule can't hold the words. The fallback is a new Screen.short_title the frame picks through fitted_title, on both the bordered panel and the PicoCalc title bar. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
53de3c9fb4
commit
3fccfd7971
4 files changed
+73
-9
No files matched your search
@@ -98,9 +98,10 @@ class ContactsScreen(ContactListScreen):
|
||||
span :data:`~meshterm.ui.contactlist.SORT_COLUMNS` so the hash sort is
|
||||
reachable.
|
||||
archived: How many contacts are archived off this device. Non-zero adds the
|
||||
``View archived contacts`` row and shows the count on it, so the tally is
|
||||
visible without opening the list; zero draws no row, since a screen should
|
||||
not offer a way into an empty list.
|
||||
``View archived contacts`` row and shows the count on it and in the title
|
||||
(``Contacts · 198 known + 123 archived``), so the tally is visible without
|
||||
opening the list; zero draws no row and leaves the title at ``… known``,
|
||||
since a screen should not offer a way into an empty list.
|
||||
locked: The full keys (lowercase hex) of the contacts locked against archiving;
|
||||
each of those rows closes its name lane on a padlock.
|
||||
"""
|
||||
@@ -132,13 +133,22 @@ class ContactsScreen(ContactListScreen):
|
||||
label = marked_label("📂", "View archived contacts", "", lane=lane)
|
||||
label.append(f" · {archived}", style="muted")
|
||||
tail.append(Choice(title=label, value=_ARCHIVED))
|
||||
# The title counts both halves of what this device's contacts amount to: those on it
|
||||
# and those a sweep moved off it. Where the frame is too narrow for the words, the
|
||||
# figures stay and the words go (``Contacts · 198 + 123``).
|
||||
known = len(contacts)
|
||||
title = f"Contacts · {known} known"
|
||||
if archived:
|
||||
title += f" + {archived} archived"
|
||||
super().__init__(
|
||||
f"Contacts · {len(contacts)} known",
|
||||
title,
|
||||
rows=rows,
|
||||
prefix_bytes=prefix_bytes,
|
||||
sort=sort,
|
||||
tail=tail,
|
||||
)
|
||||
if archived:
|
||||
self.short_title = f"Contacts · {known} + {archived}"
|
||||
|
||||
def _contact_rows_for(self, locked: frozenset[str]) -> list[ContactRow]:
|
||||
"""Our own node, then every contact, each marked locked or not by ``locked``."""
|
||||
|
||||
@@ -220,6 +220,23 @@ _ESC_ATOM = re.compile(r"(?:^|·\s*)(Esc\s+\S+)\s*$")
|
||||
_MIN_RULE = 2
|
||||
|
||||
|
||||
def fitted_title(screen: Screen, room: int) -> str:
|
||||
"""The heading to draw in ``room`` cells: the full title, or its short form where it won't fit.
|
||||
|
||||
A title that states figures (``Contacts · 198 known + 123 archived``) would otherwise be
|
||||
cut by the rule it sits in, and the cut takes the end — the last figure. A screen that
|
||||
declares a :attr:`~meshterm.ui.tui.screen.Screen.short_title` gets that instead, the same
|
||||
atoms said tighter; one with none keeps its title and the frame's usual clipping.
|
||||
|
||||
Args:
|
||||
screen: The screen whose heading is drawn.
|
||||
room: Cells the heading may take, not counting the space either side of it.
|
||||
"""
|
||||
if screen.short_title and cell_len(screen.title) > room:
|
||||
return screen.short_title
|
||||
return screen.title
|
||||
|
||||
|
||||
def _esc_hint(hint: str) -> str:
|
||||
"""The ``Esc …`` atom a screen's footer hint ends on, or ``""`` where it has none."""
|
||||
found = _ESC_ATOM.search(hint or "")
|
||||
@@ -264,7 +281,10 @@ def _title_bar(screen: Screen, cols: int, more_above: bool, more_below: bool) ->
|
||||
"""
|
||||
border = "accent"
|
||||
head_span = 3 # the arrow pair, plus the space parting it from the rule
|
||||
label_w = cell_len(screen.title) + 2 if screen.title else 0 # a space either side
|
||||
# The short form is chosen against the bar with no Esc tail at all: the title is what
|
||||
# the reader came for, so the tail gives way to it before it gives way to the tail.
|
||||
title = fitted_title(screen, cols - head_span - 2 - 2 * _MIN_RULE)
|
||||
label_w = cell_len(title) + 2 if title else 0 # a space either side
|
||||
atom = _esc_hint(screen.footer_hint)
|
||||
for esc in (atom, "Esc" if atom else "", ""):
|
||||
tail_span = cell_len(esc) + 1 if esc else 0 # the space in front of the tail
|
||||
@@ -276,12 +296,12 @@ def _title_bar(screen: Screen, cols: int, more_above: bool, more_below: bool) ->
|
||||
bar.append("↑", style=border if more_above else "muted")
|
||||
bar.append("↓", style=border if more_below else "muted")
|
||||
bar.append(" ")
|
||||
if screen.title:
|
||||
if title:
|
||||
left = max(1, (rule_span - label_w) // 2)
|
||||
right = max(1, rule_span - label_w - left)
|
||||
bar.append("─" * left, style=border)
|
||||
bar.append(" ", style=None)
|
||||
bar.append(screen.title, style=title_style(border))
|
||||
bar.append(title, style=title_style(border))
|
||||
bar.append(" ", style=None)
|
||||
bar.append("─" * right, style=border)
|
||||
else:
|
||||
@@ -347,11 +367,13 @@ def compose_base(
|
||||
# a keystroke that need not change a single body line, and a memo without it would
|
||||
# keep serving the rule the reader has already grown out of.
|
||||
caption = base.bottom_caption
|
||||
key = (cols, rows, base.title, caption, footer_hint, more_above, more_below, *visible)
|
||||
# Rich sets the padded title into the rule's inner ``cols - 4`` cells.
|
||||
title = fitted_title(base, cols - 4 - 2)
|
||||
key = (cols, rows, title, caption, footer_hint, more_above, more_below, *visible)
|
||||
if footer_lane is None and _BASE_BOX_CACHE is not None and _BASE_BOX_CACHE[0] == key:
|
||||
body = _BASE_BOX_CACHE[1]
|
||||
else:
|
||||
panel = _panel_box(base.title, visible, more_above, more_below, "accent", caption)
|
||||
panel = _panel_box(title, visible, more_above, more_below, "accent", caption)
|
||||
body = render_lines(Group(panel, footer_row()), cols)
|
||||
if footer_lane is None:
|
||||
_BASE_BOX_CACHE = (key, body)
|
||||
|
||||
@@ -60,6 +60,11 @@ class Screen:
|
||||
|
||||
Attributes:
|
||||
title: Heading shown at the top of the screen/dialog.
|
||||
short_title: The abbreviated heading the frame draws instead of :attr:`title` where
|
||||
the full one would not fit the rule it sits in (see
|
||||
:func:`~meshterm.ui.tui.frame.fitted_title`) — the same atoms said tighter, so a
|
||||
narrow frame still carries every figure rather than a title cut off mid-word.
|
||||
Empty (the default) means the title has no shorter form.
|
||||
footer_hint: One-line key hint shown in the footer.
|
||||
scroll: Current vertical scroll offset into the body's rendered lines.
|
||||
future: Resolved with the screen's result (or :data:`CANCEL`) when it commits.
|
||||
@@ -104,6 +109,7 @@ class Screen:
|
||||
"""
|
||||
|
||||
title: str = ""
|
||||
short_title: str = ""
|
||||
footer_hint: str = "Esc back"
|
||||
bottom_caption: str = ""
|
||||
floating: bool = True
|
||||
|
||||
@@ -1074,6 +1074,32 @@ def test_the_archived_row_appears_only_when_something_is_archived() -> None:
|
||||
assert _ARCHIVE not in values and values[-1] == _ARCHIVED
|
||||
|
||||
|
||||
def test_contacts_title_counts_the_archived_and_abbreviates_where_narrow() -> None:
|
||||
"""The title carries both tallies, and drops the words before it would drop a figure.
|
||||
|
||||
Nothing archived keeps the plain ``… known`` title with no short form; with some, the
|
||||
full ``198 known + 123 archived`` draws wherever it fits and ``198 + 123`` where the
|
||||
rule is too narrow — on both frames, the bordered panel and the one-row title bar.
|
||||
"""
|
||||
from meshterm.core.models import Contact
|
||||
from meshterm.ui.contacts_screen import ContactsScreen
|
||||
from meshterm.ui.tui.frame import _title_bar, fitted_title
|
||||
|
||||
contacts = [Contact(name=f"n{i}", public_key=f"{i:02x}" * 32) for i in range(198)]
|
||||
|
||||
plain = ContactsScreen("Us", "cc" * 32, contacts, 1, {}, _contacts_sort())
|
||||
assert plain.title == "Contacts · 198 known" and not plain.short_title
|
||||
|
||||
both = ContactsScreen("Us", "cc" * 32, contacts, 1, {}, _contacts_sort(), archived=123)
|
||||
assert both.title == "Contacts · 198 known + 123 archived"
|
||||
assert both.short_title == "Contacts · 198 + 123"
|
||||
assert fitted_title(both, 72) == both.title
|
||||
assert fitted_title(both, 30) == both.short_title
|
||||
|
||||
assert "198 known + 123 archived" in _title_bar(both, 53, False, False).plain
|
||||
assert "Contacts · 198 + 123 " in _title_bar(both, 40, False, False).plain
|
||||
|
||||
|
||||
async def test_archiving_one_contact_takes_it_off_the_device_and_keeps_it() -> None:
|
||||
"""The single-contact archive: an amber confirm, off the radio, stamped in the store.
|
||||
|
||||
|
||||
Reference in new issue
Block a user