main.py
Python script, ASCII text executable
1
"""
2
Implements the org.freedesktop.StatusNotifierWatcher and nothing else.
3
Copyright 2025, roundabout-host.com <vlad@roundabout-host.com>
4
5
This program is free software: you can redistribute it and/or modify
6
it under the terms of the GNU General Public Licence as published by
7
the Free Software Foundation, either version 3 of the Licence, or
8
(at your option) any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public Licence for more details.
14
15
You should have received a copy of the GNU General Public Licence
16
along with this program. If not, see <https://www.gnu.org/licenses/>.
17
"""
18
19
import gi
20
gi.require_version("Gtk", "4.0")
21
from gi.repository import Gtk, Gio, GLib
22
23
class StatusNotifierWatcher:
24
def __init__(self, connection: Gio.DBusConnection, interface_name: str):
25
self.connection = connection
26
self.interface_name = interface_name
27
self.items = {}
28
self.hosts = set()
29
30
self.node_info = Gio.DBusNodeInfo.new_for_xml(f"""
31
<node>
32
<interface name="{interface_name}">
33
<method name="RegisterStatusNotifierItem">
34
<arg type="s" name="service" direction="in"/>
35
</method>
36
<method name="RegisterStatusNotifierHost">
37
<arg type="s" name="service" direction="in"/>
38
</method>
39
<property name="RegisteredStatusNotifierItems" type="as" access="read"/>
40
<property name="IsStatusNotifierHostRegistered" type="b" access="read"/>
41
<property name="ProtocolVersion" type="i" access="read"/>
42
<signal name="StatusNotifierItemRegistered">
43
<arg type="s" name="service" direction="out"/>
44
</signal>
45
<signal name="StatusNotifierItemUnregistered">
46
<arg type="s" name="service" direction="out"/>
47
</signal>
48
<signal name="StatusNotifierHostRegistered">
49
<arg type="s" name="service" direction="out"/>
50
</signal>
51
</interface>
52
</node>
53
"""
54
)
55
self.interface_info = self.node_info.interfaces[0]
56
57
self.connection.register_object(
58
"/StatusNotifierWatcher",
59
self.interface_info,
60
self.handle_method_call,
61
self.get_property,
62
None
63
)
64
65
self.connection.signal_subscribe(
66
"org.freedesktop.DBus",
67
"org.freedesktop.DBus",
68
"NameOwnerChanged",
69
"/org/freedesktop/DBus",
70
None,
71
Gio.DBusSignalFlags.NONE,
72
self.on_name_owner_changed,
73
)
74
75
def get_property(self, connection, sender, object_path, interface_name, property_name):
76
if property_name == "IsStatusNotifierHostRegistered":
77
return GLib.Variant("b", bool(self.hosts))
78
elif property_name == "RegisteredStatusNotifierItems":
79
return GLib.Variant("as", list(self.items))
80
elif property_name == "ProtocolVersion":
81
return GLib.Variant("i", 0)
82
else:
83
raise Exception(f"Unknown property {property_name}")
84
85
def on_name_owner_changed(self, connection, sender_name, object_path, interface_name, signal_name, params):
86
name, old_owner, new_owner = params.unpack()
87
if new_owner == "":
88
for lookup in (name, old_owner):
89
if lookup in self.items:
90
path = self.items[lookup]
91
del self.items[lookup]
92
print(f"Item {lookup} vanished")
93
self.emit_signal("StatusNotifierItemUnregistered", lookup + ":" + path)
94
if lookup in self.hosts:
95
self.hosts.discard(lookup)
96
print(f"Host {lookup} vanished")
97
98
def handle_method_call(self, conn, sender, obj_path, iface_name, method, params, invocation):
99
if method == "RegisterStatusNotifierItem":
100
(arg_path,) = params.unpack()
101
self._register_item(sender, arg_path)
102
invocation.return_value(None)
103
elif method == "RegisterStatusNotifierHost":
104
(service,) = params.unpack()
105
self._register_host(sender, service)
106
invocation.return_value(None)
107
else:
108
invocation.return_dbus_error("org.freedesktop.DBus.Error.UnknownMethod", "Unknown method")
109
110
def _register_item(self, sender, path):
111
if sender not in self.items:
112
if not path.startswith("/"):
113
path = "/StatusNotifierItem"
114
self.items[sender] = path
115
print(f"Registered item {sender}")
116
self.emit_signal("StatusNotifierItemRegistered", sender + ":" + path)
117
118
def _register_host(self, sender, service):
119
if sender not in self.hosts:
120
self.hosts.add(sender)
121
print(f"Registered host {sender}")
122
self.emit_signal("StatusNotifierHostRegistered", sender)
123
124
def emit_signal(self, name, arg=None):
125
args = GLib.Variant("(s)", (arg,)) if arg else None
126
self.connection.emit_signal(
127
None,
128
"/StatusNotifierWatcher",
129
self.interface_name,
130
name,
131
args
132
)
133
134
135
class WatcherApplication(Gtk.Application):
136
def __init__(self):
137
super().__init__(
138
application_id="com.roundabout_host.roundabout.PanoramaIndicatorService",
139
flags=Gio.ApplicationFlags.FLAGS_NONE,
140
)
141
self.fd_watcher = None
142
self.kde_watcher = None
143
144
def do_startup(self):
145
Gtk.Application.do_startup(self)
146
147
def do_activate(self):
148
print("StatusNotifierWatcher starting...")
149
150
Gio.bus_own_name(
151
Gio.BusType.SESSION,
152
"org.freedesktop.StatusNotifierWatcher",
153
Gio.BusNameOwnerFlags.NONE,
154
self.on_bus_acquired_fd,
155
None,
156
None,
157
)
158
Gio.bus_own_name(
159
Gio.BusType.SESSION,
160
"org.kde.StatusNotifierWatcher",
161
Gio.BusNameOwnerFlags.NONE,
162
self.on_bus_acquired_kde,
163
None,
164
None,
165
)
166
167
self.hold()
168
169
def on_bus_acquired_fd(self, connection, name):
170
self.fd_watcher = StatusNotifierWatcher(connection, "org.freedesktop.StatusNotifierWatcher")
171
172
def on_bus_acquired_kde(self, connection, name):
173
self.kde_watcher = StatusNotifierWatcher(connection, "org.kde.StatusNotifierWatcher")
174
175
def do_shutdown(self):
176
Gtk.Application.do_shutdown(self)
177
178
179
if __name__ == "__main__":
180
app = WatcherApplication()
181
app.run(None)
182