Tray, fullscreen-only hiding, peeks, Ethernet details, wider maps

- 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
This commit is contained in:
server2
2026-10-13 04:58:08 +02:00
parent b14df3a435
commit 1e99d7264c
13 changed files with 1963 additions and 126 deletions
+134
View File
@@ -0,0 +1,134 @@
#!/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()