141 lines
5.4 KiB
Python
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)
|