Files
beefy-player/beefy_player/widgets/header.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

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])