Files
beefy-player/beefy_player/widgets/header.py
T

133 lines
5.2 KiB
Python

"""Header: the catalogue's hero, the 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
def _hero_line(markup: str, index: int, width: int, gradient_fill: bool) -> Text:
"""One row of the hero: spectrum-painted glyphs, or the art's own colours."""
if not gradient_fill:
line = Text.from_markup(markup)
line.no_wrap = True
return line
line = Text(no_wrap=True)
for column, char in enumerate(markup):
position = (column / max(1, width - 1) + index * 0.055) % 1.0
line.append(char, style=f"bold {gradient(SPECTRUM_STOPS, position)}")
return line
class HeaderBar(Widget):
can_focus = False
def render(self) -> RenderableType:
app = cast("BeefyPlayerApp", self.app)
provider = getattr(app, "provider", None)
hero = tuple(getattr(provider, "hero", ()) or ())
hero_lines = tuple(getattr(provider, "hero_lines", ()) or ())
gradient_fill = bool(getattr(provider, "hero_gradient", False))
width = max(24, self.size.width)
height = max(1, self.size.height)
state = app.playing_state
glyph, colour = state_glyph(state)
# Measure what the hero *shows*: the rows are rich markup, so their
# raw length says nothing about the columns they occupy.
hero_width = max(
(cell_len(Text.from_markup(row).plain) for row in hero), default=0
)
middle_width = max((cell_len(row) for row in hero_lines), default=0)
right = (clock_time(), date_label(), f"vol {app.volume}%")
right_width = max(cell_len(item) for item in right)
show_middle = bool(hero_lines) and width >= hero_width + middle_width + 8
show_right = width >= hero_width + right_width + 8
if show_middle and show_right:
show_middle = (
width >= hero_width + 4 + middle_width + 2 + right_width
)
# Centre the short text block against however tall the hero is: a
# three-line caption beside a seven-row picture sits in the middle.
text_offset = max(0, (len(hero) - len(hero_lines)) // 2)
lines: list[Text] = []
for index, row in enumerate(hero):
line = _hero_line(row, index, hero_width, gradient_fill)
if show_middle and text_offset <= index < text_offset + len(hero_lines):
middle = hero_lines[index - text_offset]
line.append(" " * 4)
line.append(middle, style="dim")
# Keep the right block on one column even though the middle
# lines differ in length ("TILDERADIO" is shorter than
# "STREAMED BY / FOR USERS OF THE").
line.append(" " * max(0, middle_width - cell_len(middle)))
if show_right and text_offset <= index < text_offset + len(right):
used = hero_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 - text_offset]
line.append(" " * pad)
line.append(" " * max(0, right_width - cell_len(item)))
style = "bold" if index == text_offset else "dim"
line.append(item, style=style)
lines.append(line)
ticker: Text | None = None
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} · "
f"{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
)
# A short terminal loses the bottom of the hero before it loses the
# ticker: the ticker carries the state, the art can be cropped.
if ticker is not None:
if len(lines) >= height:
lines = lines[: height - 1]
lines.append(ticker)
return Group(*lines[:height])