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