119 lines
4.3 KiB
Python
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()
|