Files
beefy-player/beefy_player/widgets/help.py
T

169 lines
6.0 KiB
Python

"""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)