refactor(providers): paint every hero with the palette gradient

This commit is contained in:
beefcarver committed 2026-11-14 09:34:52 -05:00
1 parent e6684ddbbd
commit 6aac4f4085
9 files changed
+28 -55

No files matched your search

+4 -3
View File
@@ -107,9 +107,10 @@ class RadioProvider(ABC):
#: Rows of header art drawn at the top left (rich markup is allowed).
hero: tuple[str, ...] = ()
#: Paint :attr:`hero` with the app's spectrum gradient (a wordmark) instead
#: of the colours the art carries itself (a picture).
hero_gradient: bool = False
#: Paint :attr:`hero` with the app's active palette gradient instead of the
#: colours the art carries itself. On by default so every catalogue's
#: wordmark follows the terminal's palette; a picture hero can turn it off.
hero_gradient: bool = True
#: Short lines printed beside the hero, top to bottom.
hero_lines: tuple[str, ...] = ()
+2 -2
View File
@@ -29,8 +29,8 @@ class BassdriveProvider(RadioProvider):
station_source = "bassdrive.com + icecast status"
history_source = "icecast status (rolling)"
# The wordmark in the station's own two blues, in the shared half-block
# font; the caption is the station's own description.
# The wordmark in the shared half-block font, painted with the app's
# palette gradient; the caption is the station's own description.
hero = hero_art.HERO
hero_lines = (
"BASSDRIVE",
+4 -7
View File
@@ -1,14 +1,11 @@
"""Bassdrive's header hero: its wordmark in the station's two blues.
"""Bassdrive's header hero: its wordmark in the shared half-block font.
The logo paints "BASS" in the pale blue and "DRIVE" in the deeper one; those
map onto the stock palette's bright cyan and cyan, so the wordmark still reads
as Bassdrive without hard-coded RGB.
The header paints it with the app's active palette gradient, like every other
catalogue's wordmark, so the logo follows the terminal's colours.
"""
from __future__ import annotations
from ..wordmark import render
HERO: tuple[str, ...] = render(
"BASSDRIVE", ["bright_cyan"] * 4 + ["cyan"] * 5
)
HERO: tuple[str, ...] = render("BASSDRIVE")
@@ -30,7 +30,6 @@ class SomaFMProvider(RadioProvider):
history_source = "songs/<station>.json"
hero = hero_art.LOGO
hero_gradient = True
hero_lines = hero_art.TAGLINES
@property
+3 -3
View File
@@ -1,9 +1,9 @@
"""SomaFM's header hero: the block-letter wordmark and the three lines beside it.
The glyphs come from the shared half-block font
(:mod:`beefy_player.providers.wordmark`); the header paints them with the app's
spectrum gradient (``hero_gradient``), so the logo takes on the terminal's own
colours instead of carrying RGB values around.
(:mod:`beefy_player.providers.wordmark`); the header paints every catalogue's
wordmark with the app's active palette gradient, so the logo takes on the
terminal's own colours instead of carrying RGB values around.
"""
from __future__ import annotations
@@ -29,8 +29,8 @@ class TilderadioProvider(RadioProvider):
station_source = "tilderadio.org/api/now/"
history_source = "now_playing + history[]"
# The station's own radio icon, converted to half blocks by
# ``tools/ansi_art.py``; the caption is the tildeverse's motto.
# The station's wordmark in the shared half-block font, painted with the
# app's palette gradient; the caption is the tildeverse's motto.
hero = hero_art.HERO
hero_lines = (
"TILDERADIO",
+3 -2
View File
@@ -1,7 +1,8 @@
"""tilderadio's header hero: its name in the shared half-block font.
No colours of its own — the wordmark inherits the terminal's default
foreground, so it looks like part of the chrome rather than a logo pasted in.
The shared half-block font; the header paints it with the app's active
palette gradient, so the wordmark follows the terminal's colours like the rest
of the chrome.
"""
from __future__ import annotations
+6 -34
View File
@@ -2,9 +2,9 @@
Every shipped catalogue draws its brand the same way: four-column capitals,
six pixels tall, emitted as three rows of half blocks (``▀``/``▄``/``█``) — the
same shape as the original SomaFM logo. Catalogues differ only in the word
they spell and the ANSI colours they paint it with, so the font lives here and
each provider's ``hero.py`` is one line of data.
same shape as the original SomaFM logo. The header paints every wordmark with
the app's active palette gradient, so catalogues differ only in the word they
spell; the font lives here and each provider's ``hero.py`` is one line of data.
Glyphs are 6x4 bitmaps (``M`` is 6x5). Two pixel rows become one text row, so
a wordmark is always three rows tall and the header keeps a fixed height.
@@ -12,8 +12,6 @@ a wordmark is always three rows tall and the header keeps a fixed height.
from __future__ import annotations
from collections.abc import Sequence
#: 6-row bitmap per capital; all rows of one glyph are the same width.
GLYPHS: dict[str, tuple[str, ...]] = {
"A": (".##.", "#..#", "####", "#..#", "#..#", "#..#"),
@@ -65,36 +63,10 @@ def _glyph_rows(char: str) -> tuple[str, ...]:
)
def render(text: str, styles: Sequence[str | None] | None = None) -> tuple[str, ...]:
"""Spell ``text`` as three rows of half blocks.
``styles`` gives one rich-markup style per character (``None`` leaves it in
the terminal's default colour). Contiguous runs of the same style share a
markup span, so ``render("BASSDRIVE", ...)`` paints the first four letters
and the last five separately.
"""
if styles is not None and len(styles) != len(text):
raise ValueError("styles must provide one entry per character")
def render(text: str) -> tuple[str, ...]:
"""Spell ``text`` as three rows of half blocks."""
glyphs = [_glyph_rows(char.upper()) for char in text]
rows: list[str] = []
for row in range(ROWS):
cells: list[tuple[str | None, str]] = []
for index, glyph in enumerate(glyphs):
style = styles[index] if styles is not None else None
if index:
cells.append((style, _GAP))
cells.extend((style, char) for char in glyph[row])
while cells and cells[-1][1] == " ":
cells.pop()
segments: list[tuple[str | None, str]] = []
for style, char in cells:
if segments and segments[-1][0] == style:
segments[-1] = (style, segments[-1][1] + char)
else:
segments.append((style, char))
rows.append(
"".join(f"[{style}]{body}[/]" if style else body for style, body in segments)
)
rows.append(_GAP.join(glyph[row] for glyph in glyphs).rstrip())
return tuple(rows)
+4 -1
View File
@@ -35,8 +35,11 @@ def _hero_line(
line.no_wrap = True
return line
# Iterate the *text* of the row, not its markup: a hero is allowed to
# carry styles of its own, and those brackets are not cells to paint.
plain = Text.from_markup(markup).plain
line = Text(no_wrap=True)
for column, char in enumerate(markup):
for column, char in enumerate(plain):
position = (column / max(1, width - 1) + index * 0.055) % 1.0
line.append(char, style=f"bold {gradient(stops, position)}")
return line