#!/usr/bin/env python3 import subprocess from typing import Optional import gi gi.require_version("Gtk", "3.0") from gi.repository import GLib, Gtk METADATA_FORMAT = "{{{{status}}}} — {{{{artist}}}} — {{{{title}}}}" class PlayerctlApp(Gtk.Application): def __init__(self) -> None: super().__init__(application_id="dev.codex.PlayerctlGUI") self._label: Optional[Gtk.Label] = None def do_activate(self, *args) -> None: # type: ignore[override] if self.props.active_window: self.props.active_window.present() return window = Gtk.ApplicationWindow(application=self) window.set_title("Playerctl Controls") window.set_border_width(12) window.set_default_size(320, 120) controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) controls.set_homogeneous(True) controls.set_hexpand(True) controls.set_vexpand(True) controls.set_valign(Gtk.Align.FILL) prev_button = Gtk.Button.new_from_icon_name( "media-skip-backward", Gtk.IconSize.LARGE_TOOLBAR, ) prev_button.connect("clicked", self._on_prev_clicked) prev_button.set_hexpand(True) prev_button.set_vexpand(True) prev_button.set_valign(Gtk.Align.FILL) controls.pack_start(prev_button, True, True, 0) toggle_button = Gtk.Button.new_from_icon_name( "media-playback-start", Gtk.IconSize.LARGE_TOOLBAR, ) toggle_button.connect("clicked", self._on_toggle_clicked) toggle_button.set_hexpand(True) toggle_button.set_vexpand(True) toggle_button.set_valign(Gtk.Align.FILL) controls.pack_start(toggle_button, True, True, 0) next_button = Gtk.Button.new_from_icon_name( "media-skip-forward", Gtk.IconSize.LARGE_TOOLBAR, ) next_button.connect("clicked", self._on_next_clicked) next_button.set_hexpand(True) next_button.set_vexpand(True) next_button.set_valign(Gtk.Align.FILL) controls.pack_start(next_button, True, True, 0) layout = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) layout.pack_start(controls, True, True, 0) window.add(layout) window.show_all() GLib.timeout_add_seconds(2, self._refresh_metadata) self._refresh_metadata() def _on_toggle_clicked(self, _button: Gtk.Button) -> None: self._run_playerctl(["play-pause"]) GLib.idle_add(self._refresh_metadata) def _on_next_clicked(self, _button: Gtk.Button) -> None: self._run_playerctl(["next"]) GLib.idle_add(self._refresh_metadata) def _on_prev_clicked(self, _button: Gtk.Button) -> None: self._run_playerctl(["previous"]) GLib.idle_add(self._refresh_metadata) def _refresh_metadata(self) -> bool: if not self._label: return False metadata = self._run_playerctl(["metadata", "--format", METADATA_FORMAT]) if metadata is None or not metadata.strip(): status = self._run_playerctl(["status"]) if status is None: self._label.set_text("No active media player detected") else: self._label.set_text(status.strip()) else: self._label.set_text(metadata.strip()) return True def _run_playerctl(self, args: list[str]) -> Optional[str]: cmd = ["playerctl", *args] try: completed = subprocess.run( cmd, check=False, capture_output=True, text=True, timeout=5, ) except (FileNotFoundError, subprocess.SubprocessError): if self._label: self._label.set_text("playerctl command not available") return None if completed.returncode != 0: return None return completed.stdout def main() -> None: app = PlayerctlApp() app.run() if __name__ == "__main__": main()