Dotfiles/desktopenvs/hyprlua/horizon-dock/tray.py

141 lines
5.4 KiB
Python

"""Minimal StatusNotifierItem (SNI) HOST — connects to whichever
org.kde.StatusNotifierWatcher is already running (the existing eww bars' own
systray widget registers one) and renders the currently registered tray items
as clickable planet buttons. The SNI spec allows multiple hosts, so registering
alongside eww's own host is expected and fine, not a conflict.
Rebuilt fresh each time the tray popover box is built (dock.py calls
build_into() on every dock reveal, since favorites — and therefore the tray
satellite — rebuild then) rather than live-subscribed to Registered/
Unregistered signals — simple and correct for a popover that's only open
briefly; if no watcher is running at all, build_into() shows a small
placeholder instead of raising.
"""
from __future__ import annotations
from typing import Optional
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Gio", "2.0")
from gi.repository import Gio, GLib, Gtk # noqa: E402
_WATCHER_BUS = "org.kde.StatusNotifierWatcher"
_WATCHER_PATH = "/StatusNotifierWatcher"
_WATCHER_IFACE = "org.kde.StatusNotifierWatcher"
_ITEM_IFACE = "org.kde.StatusNotifierItem"
_HOST_NAME = "org.kde.StatusNotifierHost-horizon-dock"
class TrayHost:
def __init__(self) -> None:
self._watcher: Optional[Gio.DBusProxy] = None
self._registered_host = False
self._connect()
def _connect(self) -> None:
try:
proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None,
_WATCHER_BUS, _WATCHER_PATH, _WATCHER_IFACE, None,
)
# A proxy can be constructed even for a name with no owner yet — only
# a real name-owner query tells us whether a watcher is actually up.
self._watcher = proxy if proxy.get_name_owner() is not None else None
except GLib.Error:
self._watcher = None
def _ensure_host_registered(self) -> None:
if self._registered_host or self._watcher is None:
return
try:
self._watcher.call_sync(
"RegisterStatusNotifierHost", GLib.Variant("(s)", (_HOST_NAME,)),
Gio.DBusCallFlags.NONE, 500, None,
)
except GLib.Error:
pass # some watchers don't require/support this explicitly — fine either way
self._registered_host = True
def _registered_items(self) -> list[str]:
if self._watcher is None:
return []
value = self._watcher.get_cached_property("RegisteredStatusNotifierItems")
if value is None:
return []
return list(value.unpack())
@staticmethod
def _parse_item(entry: str) -> tuple[str, str]:
"""Registration strings are either just a bus name (path defaults to
/StatusNotifierItem) or "busname/ObjectPath" — implementations disagree,
so accept both."""
if "/" in entry:
bus, _, path = entry.partition("/")
return bus, "/" + path
return entry, "/StatusNotifierItem"
# -- public: build the popover contents -----------------------------------
def build_into(self, box: Gtk.Box) -> None:
"""Populate `box` with one planet button per currently registered tray
item. Safe to call repeatedly (dock.py does, on every dock reveal)."""
if self._watcher is None:
box.append(Gtk.Label(label="No tray watcher running"))
return
self._ensure_host_registered()
entries = self._registered_items()
if not entries:
box.append(Gtk.Label(label="(empty)"))
return
for entry in entries:
bus, path = self._parse_item(entry)
btn = self._build_item_button(bus, path)
if btn is not None:
box.append(btn)
def _build_item_button(self, bus: str, path: str) -> Optional[Gtk.Button]:
try:
proxy = Gio.DBusProxy.new_for_bus_sync(
Gio.BusType.SESSION, Gio.DBusProxyFlags.NONE, None,
bus, path, _ITEM_IFACE, None,
)
except GLib.Error:
return None
icon_name = self._prop_str(proxy, "IconName") or "application-x-executable"
title = self._prop_str(proxy, "Title") or self._prop_str(proxy, "IconName") or bus
btn = Gtk.Button()
btn.set_has_frame(False)
btn.add_css_class("horizon-planet")
btn.add_css_class("horizon-tray-item")
btn.set_tooltip_text(title)
image = Gtk.Image.new_from_icon_name(icon_name)
image.set_pixel_size(20)
btn.set_child(image)
btn.connect("clicked", lambda *_a, p=proxy: self._activate(p))
right_click = Gtk.GestureClick(button=3)
right_click.connect("pressed", lambda *_a, p=proxy: self._context_menu(p))
btn.add_controller(right_click)
return btn
@staticmethod
def _prop_str(proxy: Gio.DBusProxy, name: str) -> str:
value = proxy.get_cached_property(name)
return value.unpack() if value is not None else ""
@staticmethod
def _activate(proxy: Gio.DBusProxy) -> None:
proxy.call("Activate", GLib.Variant("(ii)", (0, 0)),
Gio.DBusCallFlags.NONE, -1, None, None, None)
@staticmethod
def _context_menu(proxy: Gio.DBusProxy) -> None:
proxy.call("ContextMenu", GLib.Variant("(ii)", (0, 0)),
Gio.DBusCallFlags.NONE, -1, None, None, None)