Files
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

94 lines
3.2 KiB
Python

"""One-line status / key hint bar."""
from __future__ import annotations
from typing import TYPE_CHECKING, cast
from rich.cells import cell_len
from rich.console import RenderableType
from rich.text import Text
from textual.widget import Widget
from .common import compact_count, state_glyph, truncate
if TYPE_CHECKING:
from ..app import BeefyPlayerApp
HINTS_LONG = (
"SPACE pause ↑↓ station ENTER tune +/- vol f ★ / filter "
"v visual r refresh ? help q quit"
)
HINTS_MEDIUM = (
"space pause · ↑↓ station · enter tune · +/- vol · f ★ · / filter · ? help · q quit"
)
HINTS_SHORT = "↑↓ station · space play · ? help · q quit"
class StatusBar(Widget):
can_focus = False
def render(self) -> RenderableType:
app = cast("BeefyPlayerApp", self.app)
width = max(20, self.size.width)
glyph, colour = state_glyph(app.playing_state)
frame = app.spectrum.snapshot()
session = app.media.label
status = app.media.status_label
backend = app.player.backend or "no audio"
analyser = frame.label
listeners = ""
if app.current is not None and app.current.listeners:
listeners = f"♪ {compact_count(app.current.listeners)} "
state = app.playing_state
volume = f"vol {app.volume:>3}%"
# From most to least informative; pick the first combination that fits.
right_variants = (
f"{state} {listeners}{volume} {backend} {session} {status} {analyser}",
f"{state} {volume} {session} {status} {analyser}",
f"{state} {volume} {analyser}",
f"{state} {volume}",
volume,
)
options = (
(HINTS_LONG, right_variants[0]),
(HINTS_MEDIUM, right_variants[0]),
(HINTS_LONG, right_variants[1]),
(HINTS_MEDIUM, right_variants[1]),
(HINTS_SHORT, right_variants[1]),
(HINTS_SHORT, right_variants[2]),
(HINTS_SHORT, right_variants[3]),
(HINTS_SHORT, right_variants[4]),
)
chosen: tuple[int, str, str] | None = None
for hint, right_text in options:
right_width = cell_len(glyph) + 1 + cell_len(right_text)
if cell_len(hint) + right_width + 2 <= width:
chosen = (right_width, right_text, hint)
break
if chosen is None:
right_text = right_variants[-1]
right_width = cell_len(glyph) + 1 + cell_len(right_text)
if width <= right_width + 2:
line = Text(no_wrap=True)
line.append(
truncate(f"{glyph} {right_text}", width), style="dim"
)
return line
chosen = (right_width, right_text, "")
right_width, right_text, hint = chosen
left_width = max(0, width - right_width - 2)
left = app.notice or hint
left_style = "italic yellow" if app.notice else "dim"
line = Text(no_wrap=True)
line.append(truncate(left, left_width), style=left_style)
line.append(" " * max(0, width - left_width - right_width))
line.append(f"{glyph} ", style=f"bold {colour}")
line.append(right_text, style="dim")
return line