Files
beefy-player/somafm_tui/widgets/status.py
T
beefcarver 9c722c5f61 feat(macos): drive the system Now Playing session through mpv
macOS has no D-Bus/MPRIS: the audio engine is what the OS media UI talks
to. mpv's Cocoa backend registers with MPRemoteCommandCenter and
MPNowPlayingInfoCenter, so it already publishes the live ICY title and
receives the media keys.

Add a platform-neutral media session (mediasession.py) so the app keeps a
single interface: MprisService on D-Bus platforms, NowPlayingSession on
macOS. Map mpv's NEXT/PREV/PLAY/PLAYONLY/PAUSEONLY/STOP keys onto
script-messages and read them back as client-message IPC events, so the
hardware media keys and Control Center drive the TUI (station changes,
play/pause/stop) instead of fighting it.

--no-mpris (now also --no-media) disables the media keys too. macOS-only
mpv options are gated on sys.platform so the Linux command line is
unchanged; mpv aborts on unknown flags.
2026-10-30 18:43:03 -04: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 SomaFMApp
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("SomaFMApp", 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