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