diff --git a/meshterm/ui/dashboard_screen.py b/meshterm/ui/dashboard_screen.py index f5ade4f..03f3bf4 100644 --- a/meshterm/ui/dashboard_screen.py +++ b/meshterm/ui/dashboard_screen.py @@ -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, diff --git a/meshterm/ui/tui/screen.py b/meshterm/ui/tui/screen.py index 183816d..bb3782e 100644 --- a/meshterm/ui/tui/screen.py +++ b/meshterm/ui/tui/screen.py @@ -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) diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index dd84b23..64c9efa 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -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.