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.
108 lines
3.9 KiB
Python
108 lines
3.9 KiB
Python
"""Header: logo, clock and a live ticker."""
|
|
|
|
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 (
|
|
SPECTRUM_STOPS,
|
|
clock_time,
|
|
date_label,
|
|
gradient,
|
|
marquee,
|
|
state_glyph,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from ..app import BeefyPlayerApp
|
|
|
|
LOGO = (
|
|
"▄▀▀▀ ▄▀▀▄ █▄ ▄█ ▄▀▀▄",
|
|
"▀▀▀▄ █ █ █ █ █ █▀▀█",
|
|
"▄▄▄▀ ▀▄▄▀ █ █ █ █",
|
|
)
|
|
|
|
MIDDLE = ("SOMA FM RADIO", "LISTENER SUPPORTED", "SAN FRANCISCO")
|
|
|
|
|
|
class HeaderBar(Widget):
|
|
can_focus = False
|
|
|
|
def render(self) -> RenderableType:
|
|
app = cast("BeefyPlayerApp", self.app)
|
|
width = max(24, self.size.width)
|
|
height = max(1, self.size.height)
|
|
state = app.playing_state
|
|
glyph, colour = state_glyph(state)
|
|
|
|
logo_width = max(cell_len(row) for row in LOGO)
|
|
middle_width = max(cell_len(row) for row in MIDDLE)
|
|
right = (clock_time(), date_label(), f"vol {app.volume}%")
|
|
right_width = max(cell_len(item) for item in right)
|
|
|
|
show_middle = width >= logo_width + middle_width + 8
|
|
show_right = width >= logo_width + right_width + 8
|
|
if show_middle and show_right:
|
|
show_middle = width >= logo_width + 4 + middle_width + 2 + right_width
|
|
|
|
lines: list[Text] = []
|
|
for index, row in enumerate(LOGO):
|
|
line = Text(no_wrap=True)
|
|
for column, char in enumerate(row):
|
|
position = (
|
|
column / max(1, logo_width - 1) + index * 0.055
|
|
) % 1.0
|
|
line.append(
|
|
char, style=f"bold {gradient(SPECTRUM_STOPS, position)}"
|
|
)
|
|
if show_middle:
|
|
line.append(" " * 4)
|
|
middle = MIDDLE[index]
|
|
line.append(middle, style="dim")
|
|
# Keep the right block on one column even though the middle
|
|
# lines differ in length ("SOMA FM RADIO" is shorter than
|
|
# "LISTENER SUPPORTED").
|
|
line.append(" " * max(0, middle_width - cell_len(middle)))
|
|
if show_right:
|
|
used = logo_width
|
|
if show_middle:
|
|
used += 4 + middle_width
|
|
# Right-align the block *and* every item inside it: the clock,
|
|
# the date and the volume each keep their own length, so pad
|
|
# the shortest ones instead of left-aligning the whole block.
|
|
pad = max(1, width - used - right_width)
|
|
item = right[index]
|
|
line.append(" " * pad)
|
|
line.append(" " * max(0, right_width - cell_len(item)))
|
|
style = "bold" if index == 0 else "dim"
|
|
line.append(item, style=style)
|
|
lines.append(line)
|
|
|
|
if height >= 4:
|
|
ticker = Text(no_wrap=True)
|
|
prefix = f"{glyph} {state.upper():<8} "
|
|
ticker.append(prefix, style=f"bold {colour}")
|
|
station = app.current
|
|
if app.notice:
|
|
text = app.notice
|
|
text_style = "italic yellow"
|
|
elif station is not None:
|
|
song = station.now_playing
|
|
if song is not None:
|
|
text = f"{station.title} · {song.display_artist} — {song.display_title}"
|
|
else:
|
|
text = f"{station.title} · {station.description}"
|
|
text_style = ""
|
|
else:
|
|
text = "Pick a station — press enter to tune in"
|
|
text_style = "italic dim"
|
|
ticker.append(marquee(text, max(4, width - cell_len(prefix))), style=text_style)
|
|
lines.append(ticker)
|
|
|
|
return Group(*lines[:height])
|