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.
94 lines
3.2 KiB
Python
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
|