Dashboard: advertise scroll keys only when the overview overflows

The footer named "↑↓ PgUp/PgDn scroll" unconditionally, even on a tall
terminal where the whole overview fits and those keys do nothing. Gate the
scroll atom on the body actually being taller than its viewport, so Esc is
the only key advertised when there is nothing to scroll.

Add a reusable Screen.content_overflows predicate (from the metrics the
frame already records each paint) for the pattern, so any scrolling screen
can gate its footer the same way.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 4.8 committed 2026-07-19 14:12:51 -04:00
1 parent 79fb014c52
commit 8daf3f1294
3 files changed
+37 -1

No files matched your search

+14 -1
View File
@@ -121,7 +121,20 @@ class DashboardScreen(Screen):
"""The live mesh overview. Renders state and scrolls; the opener feeds it."""
floating = False
footer_hint = "↑↓ PgUp/PgDn scroll · Esc back"
@property
def footer_hint(self) -> str: # type: ignore[override]
"""The footer keys — advertising scroll only when the overview actually overflows.
The dashboard is one scrolling body, but on a tall terminal it often fits whole; a
static ``↑↓ PgUp/PgDn scroll`` then names keys that do nothing. Show that atom only
while the content is taller than the viewport (see
:attr:`~meshterm.ui.tui.screen.Screen.content_overflows`); otherwise Esc is the only
key that acts.
"""
if self.content_overflows:
return "↑↓ PgUp/PgDn scroll · Esc back"
return "Esc back"
def __init__(
self,
+12
View File
@@ -208,6 +208,18 @@ class Screen:
"""Lines a PageUp/PageDown moves: a screenful, bar one row kept for continuity."""
return max(1, self._scroll_viewport - 1)
@property
def content_overflows(self) -> bool:
"""Whether the last-rendered body is taller than its viewport — i.e. it can scroll.
The reusable gate for the app-wide rule that a footer only advertises a key that
would do something: a screen whose body fits has nothing to scroll, so it drops its
``↑↓ PgUp/PgDn scroll`` atom (see :attr:`DashboardScreen.footer_hint`). Reads the
metrics the frame recorded on the *previous* paint (:meth:`note_metrics`), so it is
honest one frame after a resize — the frame repaints again immediately.
"""
return self._scroll_total > self._scroll_viewport
def scroll_by(self, delta: int, total: int, viewport: int) -> None:
"""Adjust :attr:`scroll` by ``delta`` lines, clamped to the given content bounds."""
self.scroll = _clamp_scroll(self.scroll + delta, total, viewport)
+11
View File
@@ -58,6 +58,17 @@ def _stripped(lines: list[str]) -> list[str]:
# --- the screen ----------------------------------------------------------------------
def test_footer_advertises_scroll_only_when_the_overview_overflows() -> None:
"""The scroll keys join the footer only when the body is taller than the viewport."""
screen = _screen()
# Fits (the frame recorded a body no taller than the viewport): Esc alone acts.
screen.note_metrics(total=10, viewport=20)
assert screen.footer_hint == "Esc back"
# Overflows: the scroll atom appears, ahead of Esc.
screen.note_metrics(total=40, viewport=20)
assert screen.footer_hint == "↑↓ PgUp/PgDn scroll · Esc back"
def test_dashboard_renders_all_three_sections() -> None:
"""Activity, Traffic, and RF health all render from a seeded window.