1e99d7264c
- tray.rs: the shell serves org.kde.StatusNotifierWatcher (only when no other watcher runs) and is its host; icons from the theme or from IconPixmap (png.rs, a stdlib png writer); left/middle/right click and scroll; dbusmenu menus drawn in the island's look; tray IPC verbs for testing - edge.rs: the bar hides only on monitors showing a fullscreen window, driven by Hyprland's event socket; peeks of the workspaces (with the disc sliding) and the island (song change, new player), with a pop on the island - prompt.rs: Ethernet details popup (addresses, gateway, DNS, MAC copyable) - map.rs: 1100x560 maps, every other node a ring further in - tools/fake_tray.py: a fake tray app, by bus name or by path
135 lines
6.0 KiB
Python
135 lines
6.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Fake StatusNotifierItem for tray tests.
|
|
|
|
usage: fake_tray.py name|path [ICON_NAME]
|
|
name: owns org.kde.StatusNotifierItem-<pid>-1, item at /StatusNotifierItem, IconName
|
|
path: registers its object path only (libappindicator style), IconPixmap pixels
|
|
Every call it receives is printed (Activate, ContextMenu, Scroll, menu Event...).
|
|
A menu with a checkmark, a separator, a submenu and a disabled entry.
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
from gi.repository import Gio, GLib
|
|
|
|
MODE = sys.argv[1] if len(sys.argv) > 1 else "name"
|
|
ICON = sys.argv[2] if len(sys.argv) > 2 else "audio-volume-high"
|
|
PATH = "/StatusNotifierItem" if MODE == "name" else "/org/ayatana/NotificationItem/fake_tray"
|
|
NAME = f"org.kde.StatusNotifierItem-{os.getpid()}-1"
|
|
|
|
ITEM_XML = """<node><interface name="org.kde.StatusNotifierItem">
|
|
<property name="Category" type="s" access="read"/><property name="Id" type="s" access="read"/>
|
|
<property name="Title" type="s" access="read"/><property name="Status" type="s" access="read"/>
|
|
<property name="IconName" type="s" access="read"/><property name="IconPixmap" type="a(iiay)" access="read"/>
|
|
<property name="IconThemePath" type="s" access="read"/><property name="ItemIsMenu" type="b" access="read"/>
|
|
<property name="Menu" type="o" access="read"/>
|
|
<method name="Activate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
|
<method name="SecondaryActivate"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
|
<method name="ContextMenu"><arg type="i" direction="in"/><arg type="i" direction="in"/></method>
|
|
<method name="Scroll"><arg type="i" direction="in"/><arg type="s" direction="in"/></method>
|
|
<signal name="NewIcon"/><signal name="NewStatus"><arg type="s"/></signal>
|
|
</interface></node>"""
|
|
|
|
MENU_XML = """<node><interface name="com.canonical.dbusmenu">
|
|
<method name="GetLayout"><arg type="i" direction="in"/><arg type="i" direction="in"/><arg type="as" direction="in"/>
|
|
<arg type="u" direction="out"/><arg type="(ia{sv}av)" direction="out"/></method>
|
|
<method name="AboutToShow"><arg type="i" direction="in"/><arg type="b" direction="out"/></method>
|
|
<method name="Event"><arg type="i" direction="in"/><arg type="s" direction="in"/><arg type="v" direction="in"/><arg type="u" direction="in"/></method>
|
|
</interface></node>"""
|
|
|
|
TITLE = "Fake Tray " + MODE
|
|
muted = [True]
|
|
|
|
|
|
def entry(id, props, children=()):
|
|
return GLib.Variant("(ia{sv}av)", (id, props, [GLib.Variant("v", child) for child in children]))
|
|
|
|
|
|
def menu(parent):
|
|
sub = [
|
|
entry(31, {"label": GLib.Variant("s", "Sub _A")}),
|
|
entry(32, {"label": GLib.Variant("s", "Sub B")}),
|
|
]
|
|
top = [
|
|
entry(1, {"label": GLib.Variant("s", "_Open Window")}),
|
|
entry(2, {"type": GLib.Variant("s", "separator")}),
|
|
entry(3, {"label": GLib.Variant("s", "Mute"), "toggle-type": GLib.Variant("s", "checkmark"), "toggle-state": GLib.Variant("i", 1 if muted[0] else 0)}),
|
|
entry(4, {"label": GLib.Variant("s", "More"), "children-display": GLib.Variant("s", "submenu")}, sub),
|
|
entry(5, {"label": GLib.Variant("s", "Disabled thing"), "enabled": GLib.Variant("b", False)}),
|
|
entry(6, {"label": GLib.Variant("s", "Hidden"), "visible": GLib.Variant("b", False)}),
|
|
entry(7, {"label": GLib.Variant("s", "Quit")}),
|
|
]
|
|
children = sub if parent == 4 else top
|
|
return GLib.Variant("(u(ia{sv}av))", (1, (parent, {"children-display": GLib.Variant("s", "submenu")}, [GLib.Variant("v", child) for child in children])))
|
|
|
|
|
|
def pixmap():
|
|
size = 24
|
|
data = bytearray()
|
|
for y in range(size):
|
|
for x in range(size):
|
|
inside = (x - 11.5) ** 2 + (y - 11.5) ** 2 < 100
|
|
data += bytes([255 if inside else 0, 60, 200, 120]) if inside else bytes([0, 0, 0, 0])
|
|
return [(size, size, bytes(data))]
|
|
|
|
|
|
def item_property(conn, sender, path, iface, name):
|
|
print("get", name, flush=True)
|
|
values = {
|
|
"Category": GLib.Variant("s", "ApplicationStatus"),
|
|
"Id": GLib.Variant("s", "fake_tray_" + MODE),
|
|
"Title": GLib.Variant("s", TITLE),
|
|
"Status": GLib.Variant("s", "Active"),
|
|
"IconName": GLib.Variant("s", ICON if MODE == "name" else ""),
|
|
"IconPixmap": GLib.Variant("a(iiay)", pixmap() if MODE == "path" else []),
|
|
"IconThemePath": GLib.Variant("s", ""),
|
|
"ItemIsMenu": GLib.Variant("b", False),
|
|
"Menu": GLib.Variant("o", "/MenuBar"),
|
|
}
|
|
return values.get(name)
|
|
|
|
|
|
def item_call(conn, sender, path, iface, method, params, invocation):
|
|
print("item", method, params.unpack(), flush=True)
|
|
invocation.return_value(None)
|
|
|
|
|
|
def menu_call(conn, sender, path, iface, method, params, invocation):
|
|
args = params.unpack()
|
|
print("menu", method, args, flush=True)
|
|
if method == "GetLayout":
|
|
invocation.return_value(menu(args[0]))
|
|
elif method == "AboutToShow":
|
|
invocation.return_value(GLib.Variant("(b)", (False,)))
|
|
else:
|
|
if method == "Event" and args[0] == 3:
|
|
muted[0] = not muted[0]
|
|
if method == "Event" and args[0] == 7:
|
|
GLib.idle_add(loop.quit)
|
|
invocation.return_value(None)
|
|
|
|
|
|
bus = Gio.bus_get_sync(Gio.BusType.SESSION)
|
|
bus.register_object(PATH, Gio.DBusNodeInfo.new_for_xml(ITEM_XML).interfaces[0], item_call, item_property, None)
|
|
bus.register_object("/MenuBar", Gio.DBusNodeInfo.new_for_xml(MENU_XML).interfaces[0], menu_call, None, None)
|
|
|
|
|
|
def register(*_):
|
|
service = NAME if MODE == "name" else PATH
|
|
try:
|
|
bus.call_sync("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", "org.kde.StatusNotifierWatcher",
|
|
"RegisterStatusNotifierItem", GLib.Variant("(s)", (service,)), None, 0, 3000, None)
|
|
print("registered", service, flush=True)
|
|
except GLib.Error as error:
|
|
print("no watcher:", error.message, flush=True)
|
|
|
|
|
|
if MODE == "name":
|
|
Gio.bus_own_name_on_connection(bus, NAME, 0, None, None)
|
|
|
|
# like the real libraries: register again whenever a watcher appears
|
|
Gio.bus_watch_name_on_connection(bus, "org.kde.StatusNotifierWatcher", 0, register, None)
|
|
|
|
loop = GLib.MainLoop()
|
|
loop.run()
|