Files

119 lines
4.3 KiB
Python

"""Catalogue picker: switch between SomaFM, tilderadio and whatever comes next."""
from __future__ import annotations
from rich.console import Group
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 ..providers import PROVIDERS, available_providers
from .common import active_stops, gradient, truncate
class ProviderScreen(ModalScreen[str | None]):
"""A list of catalogues; enter selects, esc leaves everything as it was."""
def __init__(self, current_id: str = "") -> None:
super().__init__()
self.slugs: tuple[str, ...] = available_providers()
self.current_id = current_id
self.index = self.slugs.index(current_id) if current_id in self.slugs else 0
def compose(self) -> ComposeResult:
# A scroll container keeps thirteen catalogues navigable on a short
# terminal; the body is given its exact line count so it measures.
with VerticalScroll(id="providers"):
yield Static(self._body(), id="providers-body")
def on_mount(self) -> None:
body = self.query_one("#providers-body", Static)
body.styles.height = len(self.slugs) + 5 # heading, blank, rows, footer
def _body(self) -> Group:
header = Text()
header.append("◤ ", style=f"bold {gradient(active_stops(self.app), 0.9)}")
header.append("CATALOGUES", style=f"bold {gradient(active_stops(self.app), 0.55)}")
header.append(" · pick a station catalogue", style="dim")
name_width = (
max(
(
len(PROVIDERS[slug].name)
for slug in self.slugs
if slug in PROVIDERS
),
default=12,
)
+ 2
)
rows: list[Text] = []
for index, slug in enumerate(self.slugs):
provider = PROVIDERS[slug]
selected = index == self.index
row = Text(no_wrap=True)
row.append(
"▶ " if selected else " ", style="bold magenta" if selected else ""
)
row.append(f"{index + 1:>2} ", style="dim")
row.append(
f"{truncate(provider.name, name_width - 2):<{name_width}}",
style="bold underline" if selected else "bold",
)
row.append(" ")
# marker(2) + number(5) + name + gap(2) + tagline + current dot(3)
room = max(8, 58 - 2 - 5 - name_width - 2 - 3)
row.append(truncate(provider.tagline, room), style="italic dim")
if slug == self.current_id:
row.append(" ●", style="bold green")
rows.append(row)
current = PROVIDERS.get(self.current_id)
footer = Text()
footer.append("\n")
if current is not None:
line = truncate(f"now: {current.name} — {current.station_source}", 58)
footer.append(line + "\n", style="dim")
footer.append(
"↑ ↓ choose · enter switch · esc cancel",
style="dim",
)
return Group(header, Text(""), *rows, footer)
# -- interaction ----------------------------------------------------- #
def _move(self, delta: int) -> None:
self.index = (self.index + delta) % len(self.slugs)
body = self.query_one("#providers-body", Static)
body.update(self._body())
box = self.query_one("#providers", VerticalScroll)
def follow() -> None:
visible = max(1, box.size.height)
target = max(0, 2 + self.index - (visible - 3) // 2)
box.scroll_to(y=target, animate=False)
box.call_after_refresh(follow)
def on_key(self, event) -> None:
key = event.key
if key in ("up", "k", "shift+tab"):
self._move(-1)
elif key in ("down", "j", "tab"):
self._move(1)
elif key in ("enter", "space"):
self.dismiss(self.slugs[self.index])
elif key == "escape" or key == "q":
self.dismiss(None)
elif key.isdigit() and key != "0":
choice = int(key) - 1
if 0 <= choice < len(self.slugs):
self.dismiss(self.slugs[choice])
else:
return
event.stop()