169 lines
6.0 KiB
Python
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", "clear the filter / close overlays"),
|
|
(
|
|
"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)
|