Files
beefy-player/beefy_player/widgets/station_list.py
T
beefcarver e30ca09627 refactor!: rename somafm-tui to beefy-player
The app is now a general internet-radio player rather than a SomaFM client, so the package, console script, MPRIS identity, mpv namespaces, macOS tap names, XDG directories and docs all move to beefy-player.

Environment variables are now BEEFY_* (the old SOMAFM_* spellings are still honoured) and an existing ~/.config/somafm-tui/config.json is migrated on first run, so favourites and volume survive the rename.

The visible interface is untouched: same layout, same logo, same keys.
2026-11-03 16:10:25 -05:00

107 lines
3.7 KiB
Python

"""Station list with favourites, live markers and a filter."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from rich.cells import cell_len
from rich.console import Group, RenderableType
from rich.text import Text
from textual.widget import Widget
from .common import compact_count, truncate
if TYPE_CHECKING:
from ..app import BeefyPlayerApp
class StationList(Widget):
can_focus = False
def __init__(self) -> None:
super().__init__()
self.border_title = "STATIONS"
def set_filter(self, query: str) -> None:
safe_query = query.replace("[", "(").replace("]", ")") if query else ""
title = f"STATIONS · /{safe_query}" if safe_query else "STATIONS"
if self.border_title != title:
self.border_title = title
def on_mouse_scroll_down(self, event) -> None:
cast("BeefyPlayerApp", self.app).action_cursor_down()
def on_mouse_scroll_up(self, event) -> None:
cast("BeefyPlayerApp", self.app).action_cursor_up()
def render(self) -> RenderableType:
app = cast("BeefyPlayerApp", self.app)
width = max(12, self.size.width)
height = max(1, self.size.height)
view = app.view
if not view:
message = "no stations match" if app.query else "loading stations…"
return Group(Text(truncate(message, width), style="dim italic"))
first = 0
if len(view) > height:
first = max(0, min(app.index - height // 2, len(view) - height))
lines: list[Text] = []
for offset in range(min(height, len(view))):
index = first + offset
station = view[index]
selected = index == app.index
current = app.current is not None and app.current.id == station.id
playing = current and app.playing_state not in ("stopped", "error")
marker = "▌" if selected else " "
star = "★" if station.favorite else " "
play = (
("‖" if app.playing_state == "paused" else "▶") if current else " "
)
count_text = ""
count_width = 0
if width >= 26 and station.listeners > 0:
count_text = f"{compact_count(station.listeners)} ♪"
count_width = cell_len(count_text) + 1
prefix = marker + star + play + " "
available = width - cell_len(prefix) - count_width
title = truncate(station.title, max(4, available))
gap = max(0, width - cell_len(prefix) - cell_len(title) - count_width)
if selected:
base = ""
marker_style = "magenta"
title_style = "bold underline"
count_style = "bold"
elif current:
base = ""
marker_style = "cyan"
title_style = "cyan"
count_style = "dim"
else:
base = ""
marker_style = ""
title_style = ""
count_style = "dim"
row = Text(no_wrap=True, style=base)
row.append(marker, style=marker_style)
row.append(star, style="bold yellow" if station.favorite else "")
row.append(play, style="cyan" if playing else "")
row.append(" ")
row.append(title, style=title_style)
row.append(" " * gap)
if count_text:
row.append(count_text, style=count_style)
used = cell_len(prefix) + cell_len(title) + gap + cell_len(count_text)
if used < width:
row.append(" " * (width - used))
lines.append(row)
return Group(*lines)