"""Full-screen keyboard help.""" from __future__ import annotations import contextlib from typing import TYPE_CHECKING, ClassVar, cast from rich.console import Group from rich.table import Table from rich.text import Text from textual.app import ComposeResult from textual.containers import VerticalScroll from textual.screen import ModalScreen from textual.widgets import Static from .. import __version__ from ..palettes import PALETTE_ORDER, PALETTES from ..providers import PROVIDERS, available_providers from .common import active_stops, gradient if TYPE_CHECKING: from ..app import BeefyPlayerApp #: Where the project lives (shown at the foot of the help screen). URL = "https://tildeforge.dev/beefcarver/beefy-player" HELP_ROWS: tuple[tuple[str, str], ...] = ( ("↑ / ↓ j / k", "move through the station list"), ("pgup / pgdn", "page through the station list"), ("enter", "tune in to the selected station"), ("space", "play / pause the current station"), ("s", "stop playback"), ("] / [", "next / previous station (tunes immediately)"), ("+ / -", "volume up / down"), ("m", "mute / unmute"), ("f", "favourite — favourites float to the top"), ("/", "filter stations by title, genre or description"), ("esc", "close the search box; esc again clears the filter"), ( "v", "cycle visualiser: mirror → bars → wave → waterfall → radial → braille", ), ("c", "cycle the colour palette (listed below)"), ("r", "refresh stations, schedule and song history"), ("p", "switch station catalogue (SomaFM / tilderadio)"), ("g / G", "jump to the top / bottom of the list"), ("? ", "this help"), ("q", "quit"), ) class HelpScreen(ModalScreen[None]): def compose(self) -> ComposeResult: app = cast("BeefyPlayerApp", self.app) provider = app.provider catalogues = Table.grid(padding=(0, 2), expand=False) catalogues.add_column(style="bold cyan", no_wrap=True, justify="right") catalogues.add_column(style="bold", no_wrap=True) catalogues.add_column(style="dim") for slug in available_providers(): catalogue = PROVIDERS[slug] marker = "●" if slug == provider.id else " " pane = "schedule pane" if catalogue.has_schedule else "station list" catalogues.add_row( marker, slug, f"{catalogue.name} — {pane}" ) # -- palettes, two per row so sixteen still fit ------------------ # names = list(PALETTE_ORDER) name_width = max(len(name) for name in names) def swatch(name: str) -> Text: stops = PALETTES[name] block = Text(no_wrap=True) for step in range(8): block.append("█", style=gradient(stops, step / 7)) return block def entry(name: str, first: bool) -> Text: line = Text(no_wrap=True) if not first: line.append(" ") line.append("● " if name == app.palette else " ", style="bold cyan") line.append(f"{name:<{name_width}}", style="bold") line.append(" ") line.append_text(swatch(name)) return line palette_rows: list[Text] = [] for index in range(0, len(names), 2): row = entry(names[index], True) if index + 1 < len(names): row.append_text(entry(names[index + 1], False)) palette_rows.append(row) palette_title = Text() palette_title.append("PALETTES", style="bold cyan") palette_title.append( " c cycles them · remembered between runs", style="dim" ) catalogue_title = Text() catalogue_title.append("CATALOGUES", style="bold cyan") catalogue_title.append( " switch with p or --provider at start", style="dim" ) table = Table.grid(padding=(0, 2), expand=False) table.add_column(style="bold cyan", no_wrap=True, justify="right") table.add_column(style="") for key, description in HELP_ROWS: table.add_row(key, description) header = Text() header.append("◤ ", style=f"bold {gradient(active_stops(self.app), 0.9)}") header.append("BEEFY PLAYER", style=f"bold {gradient(active_stops(self.app), 0.55)}") header.append(" keyboard shortcuts", style="dim") header.append(" · ↑ ↓ / pgup pgdn scroll", style="dim") # One attribution line: version and where the project lives. footer = Text(no_wrap=True) footer.append(f"beefy player {__version__}", style="bold") footer.append(" · ") footer.append( URL, style=f"dim link {URL}" ) body = Group( header, Text(""), table, Text(""), catalogue_title, catalogues, Text(""), palette_title, *palette_rows, Text(""), footer, ) # The body outgrows a small terminal; the scroll container keeps it # readable (and gives the box its border). with VerticalScroll(id="help"): yield Static(body, id="help-body") #: Keys that scroll the page instead of closing it (help outgrows a 24-row #: terminal, so it has to be readable there). SCROLL_KEYS: ClassVar[dict[str, str]] = { "up": "scroll_up", "k": "scroll_up", "down": "scroll_down", "j": "scroll_down", "pageup": "scroll_page_up", "pagedown": "scroll_page_down", "home": "scroll_home", "g": "scroll_home", "end": "scroll_end", "shift+g": "scroll_end", } def on_key(self, event) -> None: event.stop() method = self.SCROLL_KEYS.get(event.key) if method is not None: body = self.query_one("#help") with contextlib.suppress(Exception): getattr(body, method)(animate=False) return self.dismiss(None)