something-x-dev 1.6.0.dev13__py3-none-any.whl → 1.7.0.dev14__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nothing_app/bluetooth.py CHANGED
@@ -1,6 +1,4 @@
1
- import dbus
2
- import dbus.mainloop.glib
3
- from gi.repository import GLib, GObject
1
+ from gi.repository import Gio, GLib, GObject
4
2
 
5
3
  BLUEZ_SERVICE = "org.bluez"
6
4
  ADAPTER_IFACE = "org.bluez.Adapter1"
@@ -20,6 +18,10 @@ NOTHING_PATTERNS = (
20
18
  "nothing phone",
21
19
  )
22
20
 
21
+ # Lightweight proxy flags: no property caching, no auto-signal wiring.
22
+ # Used for proxies that only need to call methods.
23
+ _FLAGS_METHOD = Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES | Gio.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS
24
+
23
25
 
24
26
  class BluetoothDevice:
25
27
  def __init__(self, path: str, props: dict):
@@ -88,96 +90,136 @@ class BluetoothManager(GObject.Object):
88
90
  def __init__(self):
89
91
  super().__init__()
90
92
  self.devices: dict[str, BluetoothDevice] = {}
91
- self._bus: dbus.SystemBus | None = None
93
+ self._connection: Gio.DBusConnection | None = None
94
+ self._adapter_path: str | None = None
95
+ self._subs: list[int] = []
92
96
  self._available = False
93
97
  self._init_dbus()
94
98
 
95
99
  def _init_dbus(self):
96
100
  try:
97
- dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
98
- self._bus = dbus.SystemBus()
99
- self._refresh()
101
+ self._connection = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
102
+ mgr = Gio.DBusProxy.new_sync(
103
+ self._connection,
104
+ _FLAGS_METHOD,
105
+ None,
106
+ BLUEZ_SERVICE,
107
+ "/",
108
+ OBJ_MANAGER_IFACE,
109
+ None,
110
+ )
111
+ # Non-blocking: populates devices and emits devices-changed when ready
112
+ mgr.call(
113
+ "GetManagedObjects",
114
+ None,
115
+ Gio.DBusCallFlags.NONE,
116
+ -1,
117
+ None,
118
+ self._on_managed_objects,
119
+ None,
120
+ )
100
121
  self._subscribe()
101
- self._available = True
102
122
  except Exception as exc:
103
123
  print(f"[bluetooth] D-Bus init failed: {exc}")
104
124
 
105
- def _refresh(self):
106
- if not self._bus:
107
- return
125
+ def _on_managed_objects(self, proxy, result, _user_data):
108
126
  try:
109
- mgr = dbus.Interface(
110
- self._bus.get_object(BLUEZ_SERVICE, "/"),
111
- OBJ_MANAGER_IFACE,
112
- )
113
- objects = mgr.GetManagedObjects()
114
- self.devices = {}
115
- for path, ifaces in objects.items():
116
- if DEVICE_IFACE not in ifaces:
117
- continue
118
- props = {str(k): v for k, v in ifaces[DEVICE_IFACE].items()}
119
- dev = BluetoothDevice(str(path), props)
120
- if BATTERY_IFACE in ifaces:
121
- dev.battery = int(ifaces[BATTERY_IFACE].get("Percentage", 0))
122
- self.devices[str(path)] = dev
127
+ variant = proxy.call_finish(result)
128
+ objects = variant.unpack()[0]
129
+ self._populate(objects)
130
+ self._available = True
131
+ GLib.idle_add(self.emit, "devices-changed")
123
132
  except Exception as exc:
124
- print(f"[bluetooth] Refresh failed: {exc}")
133
+ print(f"[bluetooth] GetManagedObjects failed: {exc}")
134
+
135
+ def _populate(self, objects: dict):
136
+ self.devices = {}
137
+ self._adapter_path = None
138
+ for path, ifaces in objects.items():
139
+ if ADAPTER_IFACE in ifaces and self._adapter_path is None:
140
+ self._adapter_path = path
141
+ if DEVICE_IFACE not in ifaces:
142
+ continue
143
+ props = dict(ifaces[DEVICE_IFACE])
144
+ dev = BluetoothDevice(path, props)
145
+ if BATTERY_IFACE in ifaces:
146
+ dev.battery = int(ifaces[BATTERY_IFACE].get("Percentage", 0))
147
+ self.devices[path] = dev
125
148
 
126
149
  def _subscribe(self):
127
- if not self._bus:
150
+ if not self._connection:
128
151
  return
129
152
  try:
130
- self._bus.add_signal_receiver(
131
- self._on_props_changed,
132
- signal_name="PropertiesChanged",
133
- dbus_interface=PROPERTIES_IFACE,
134
- path_keyword="path",
153
+ self._subs.append(
154
+ self._connection.signal_subscribe(
155
+ BLUEZ_SERVICE,
156
+ PROPERTIES_IFACE,
157
+ "PropertiesChanged",
158
+ None,
159
+ None,
160
+ Gio.DBusSignalFlags.NONE,
161
+ self._on_props_changed,
162
+ None,
163
+ )
135
164
  )
136
- self._bus.add_signal_receiver(
137
- self._on_ifaces_added,
138
- signal_name="InterfacesAdded",
139
- dbus_interface=OBJ_MANAGER_IFACE,
140
- bus_name=BLUEZ_SERVICE,
165
+ self._subs.append(
166
+ self._connection.signal_subscribe(
167
+ BLUEZ_SERVICE,
168
+ OBJ_MANAGER_IFACE,
169
+ "InterfacesAdded",
170
+ None,
171
+ None,
172
+ Gio.DBusSignalFlags.NONE,
173
+ self._on_ifaces_added,
174
+ None,
175
+ )
141
176
  )
142
- self._bus.add_signal_receiver(
143
- self._on_ifaces_removed,
144
- signal_name="InterfacesRemoved",
145
- dbus_interface=OBJ_MANAGER_IFACE,
146
- bus_name=BLUEZ_SERVICE,
177
+ self._subs.append(
178
+ self._connection.signal_subscribe(
179
+ BLUEZ_SERVICE,
180
+ OBJ_MANAGER_IFACE,
181
+ "InterfacesRemoved",
182
+ None,
183
+ None,
184
+ Gio.DBusSignalFlags.NONE,
185
+ self._on_ifaces_removed,
186
+ None,
187
+ )
147
188
  )
148
189
  except Exception as exc:
149
190
  print(f"[bluetooth] Signal subscribe failed: {exc}")
150
191
 
151
- def _on_props_changed(self, interface, changed, _invalidated=None, path=None):
152
- if interface != DEVICE_IFACE:
153
- return
154
- if not path:
192
+ def _on_props_changed(self, _conn, _sender, path, _iface, _signal, params, _user_data):
193
+ iface_name, changed, _invalidated = params.unpack()
194
+ if iface_name != DEVICE_IFACE:
155
195
  return
156
- path = str(path)
157
196
  if path not in self.devices:
158
197
  return
159
198
  dev = self.devices[path]
160
199
  old_connected = dev.connected
161
- dev.update({str(k): v for k, v in changed.items()})
200
+ dev.update(dict(changed))
162
201
  if dev.connected != old_connected:
163
202
  sig = "device-connected" if dev.connected else "device-disconnected"
164
203
  GLib.idle_add(self.emit, sig, path)
165
204
  GLib.idle_add(self.emit, "devices-changed")
166
205
 
167
- def _on_ifaces_added(self, path, ifaces):
206
+ def _on_ifaces_added(self, _conn, _sender, _path, _iface, _signal, params, _user_data):
207
+ obj_path, ifaces = params.unpack()
208
+ if ADAPTER_IFACE in ifaces and self._adapter_path is None:
209
+ self._adapter_path = obj_path
168
210
  if DEVICE_IFACE not in ifaces:
169
211
  return
170
- props = {str(k): v for k, v in ifaces[DEVICE_IFACE].items()}
171
- dev = BluetoothDevice(str(path), props)
212
+ props = dict(ifaces[DEVICE_IFACE])
213
+ dev = BluetoothDevice(obj_path, props)
172
214
  if BATTERY_IFACE in ifaces:
173
215
  dev.battery = int(ifaces[BATTERY_IFACE].get("Percentage", 0))
174
- self.devices[str(path)] = dev
216
+ self.devices[obj_path] = dev
175
217
  GLib.idle_add(self.emit, "devices-changed")
176
218
 
177
- def _on_ifaces_removed(self, path, ifaces):
178
- path = str(path)
179
- if DEVICE_IFACE in ifaces and path in self.devices:
180
- del self.devices[path]
219
+ def _on_ifaces_removed(self, _conn, _sender, _path, _iface, _signal, params, _user_data):
220
+ obj_path, ifaces = params.unpack()
221
+ if DEVICE_IFACE in ifaces and obj_path in self.devices:
222
+ del self.devices[obj_path]
181
223
  GLib.idle_add(self.emit, "devices-changed")
182
224
 
183
225
  @property
@@ -191,56 +233,152 @@ class BluetoothManager(GObject.Object):
191
233
  return [d for d in self.get_all() if d.is_nothing]
192
234
 
193
235
  def refresh(self):
194
- self._refresh()
195
- self.emit("devices-changed")
236
+ if not self._connection:
237
+ return
238
+ try:
239
+ mgr = Gio.DBusProxy.new_sync(
240
+ self._connection,
241
+ _FLAGS_METHOD,
242
+ None,
243
+ BLUEZ_SERVICE,
244
+ "/",
245
+ OBJ_MANAGER_IFACE,
246
+ None,
247
+ )
248
+ mgr.call(
249
+ "GetManagedObjects",
250
+ None,
251
+ Gio.DBusCallFlags.NONE,
252
+ -1,
253
+ None,
254
+ self._on_managed_objects,
255
+ None,
256
+ )
257
+ except Exception as exc:
258
+ print(f"[bluetooth] refresh: {exc}")
196
259
 
197
260
  def connect_device(self, path: str, on_error=None):
198
- if not self._bus:
261
+ if not self._connection:
199
262
  return
263
+ Gio.DBusProxy.new(
264
+ self._connection,
265
+ _FLAGS_METHOD,
266
+ None,
267
+ BLUEZ_SERVICE,
268
+ path,
269
+ DEVICE_IFACE,
270
+ None,
271
+ self._on_connect_proxy_ready,
272
+ on_error,
273
+ )
200
274
 
201
- def _err(e):
202
- print(f"[BT] connect error: {e}")
275
+ def _on_connect_proxy_ready(self, _source, result, on_error):
276
+ try:
277
+ proxy = Gio.DBusProxy.new_finish(result)
278
+ proxy.call(
279
+ "Connect",
280
+ None,
281
+ Gio.DBusCallFlags.NONE,
282
+ -1,
283
+ None,
284
+ self._on_connect_done,
285
+ on_error,
286
+ )
287
+ except Exception as exc:
288
+ print(f"[bluetooth] connect proxy failed: {exc}")
203
289
  if on_error:
204
290
  GLib.idle_add(on_error)
205
291
 
292
+ def _on_connect_done(self, proxy, result, on_error):
206
293
  try:
207
- iface = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, path), DEVICE_IFACE)
208
- iface.Connect(reply_handler=lambda: None, error_handler=_err)
294
+ proxy.call_finish(result)
209
295
  except Exception as exc:
210
- print(f"[bluetooth] connect {path}: {exc}")
296
+ print(f"[BT] connect error: {exc}")
211
297
  if on_error:
212
298
  GLib.idle_add(on_error)
213
299
 
214
300
  def disconnect_device(self, path: str):
215
- if not self._bus:
301
+ if not self._connection:
216
302
  return
303
+ Gio.DBusProxy.new(
304
+ self._connection,
305
+ _FLAGS_METHOD,
306
+ None,
307
+ BLUEZ_SERVICE,
308
+ path,
309
+ DEVICE_IFACE,
310
+ None,
311
+ self._on_disconnect_proxy_ready,
312
+ None,
313
+ )
314
+
315
+ def _on_disconnect_proxy_ready(self, _source, result, _user_data):
217
316
  try:
218
- iface = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, path), DEVICE_IFACE)
219
- iface.Disconnect(
220
- reply_handler=lambda: None,
221
- error_handler=lambda e: print(f"[BT] disconnect error: {e}"),
317
+ proxy = Gio.DBusProxy.new_finish(result)
318
+ proxy.call(
319
+ "Disconnect",
320
+ None,
321
+ Gio.DBusCallFlags.NONE,
322
+ -1,
323
+ None,
324
+ self._on_disconnect_done,
325
+ None,
222
326
  )
223
327
  except Exception as exc:
224
- print(f"[bluetooth] disconnect {path}: {exc}")
328
+ print(f"[bluetooth] disconnect proxy failed: {exc}")
329
+
330
+ def _on_disconnect_done(self, proxy, result, _user_data):
331
+ try:
332
+ proxy.call_finish(result)
333
+ except Exception as exc:
334
+ print(f"[BT] disconnect error: {exc}")
225
335
 
226
336
  def start_discovery(self):
227
- if not self._bus:
337
+ if not self._connection or not self._adapter_path:
228
338
  return
229
339
  try:
230
- mgr = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, "/"), OBJ_MANAGER_IFACE)
231
- for path, ifaces in mgr.GetManagedObjects().items():
232
- if ADAPTER_IFACE in ifaces:
233
- adapter = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, path), ADAPTER_IFACE)
234
- adapter.StartDiscovery()
235
- GLib.timeout_add_seconds(30, self._stop_discovery_on_path, str(path))
236
- break
340
+ proxy = Gio.DBusProxy.new_sync(
341
+ self._connection,
342
+ _FLAGS_METHOD,
343
+ None,
344
+ BLUEZ_SERVICE,
345
+ self._adapter_path,
346
+ ADAPTER_IFACE,
347
+ None,
348
+ )
349
+ proxy.call(
350
+ "StartDiscovery",
351
+ None,
352
+ Gio.DBusCallFlags.NONE,
353
+ -1,
354
+ None,
355
+ self._on_start_discovery_done,
356
+ None,
357
+ )
237
358
  except Exception as exc:
238
359
  print(f"[bluetooth] discovery start: {exc}")
239
360
 
240
- def _stop_discovery_on_path(self, path: str):
361
+ def _on_start_discovery_done(self, proxy, result, _user_data):
241
362
  try:
242
- adapter = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, path), ADAPTER_IFACE)
243
- adapter.StopDiscovery()
363
+ proxy.call_finish(result)
364
+ GLib.timeout_add_seconds(30, self._stop_discovery)
365
+ except Exception as exc:
366
+ print(f"[bluetooth] start discovery error: {exc}")
367
+
368
+ def _stop_discovery(self):
369
+ if not self._connection or not self._adapter_path:
370
+ return False
371
+ try:
372
+ proxy = Gio.DBusProxy.new_sync(
373
+ self._connection,
374
+ _FLAGS_METHOD,
375
+ None,
376
+ BLUEZ_SERVICE,
377
+ self._adapter_path,
378
+ ADAPTER_IFACE,
379
+ None,
380
+ )
381
+ proxy.call_sync("StopDiscovery", None, Gio.DBusCallFlags.NONE, -1, None)
244
382
  except Exception:
245
383
  pass
246
384
  return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: something-x-dev
3
- Version: 1.6.0.dev13
3
+ Version: 1.7.0.dev14
4
4
  Summary: Something X device manager for Omarchy / Linux
5
5
  Author: Raphael
6
6
  License: MIT
@@ -16,9 +16,10 @@ Requires-Python: >=3.11
16
16
  Description-Content-Type: text/markdown
17
17
  License-File: LICENSE
18
18
  Requires-Dist: PyGObject>=3.42
19
- Requires-Dist: dbus-python>=1.3
20
19
  Provides-Extra: dev
21
20
  Requires-Dist: ruff; extra == "dev"
21
+ Requires-Dist: pytest; extra == "dev"
22
+ Requires-Dist: pytest-cov; extra == "dev"
22
23
  Dynamic: license-file
23
24
 
24
25
  <div align="center">
@@ -32,6 +33,8 @@ Built for [Omarchy](https://omarchy.org) · GTK4 · Pure black · JetBrains Mono
32
33
  [![AUR](https://img.shields.io/aur/version/something-x?color=red)](https://aur.archlinux.org/packages/something-x)
33
34
  [![License: MIT](https://img.shields.io/badge/license-MIT-red.svg)](LICENSE)
34
35
  [![Platform](https://img.shields.io/badge/platform-Linux-lightgrey)](https://github.com/SoaOaoS/something-x)
36
+ [![CI](https://github.com/SoaOaoS/something-x/actions/workflows/ci.yml/badge.svg)](https://github.com/SoaOaoS/something-x/actions/workflows/ci.yml)
37
+ [![Coverage](https://img.shields.io/badge/coverage-tracked-red)](https://github.com/SoaOaoS/something-x/actions/workflows/ci.yml)
35
38
 
36
39
  </div>
37
40
 
@@ -1,6 +1,6 @@
1
1
  nothing_app/__init__.py,sha256=Z68l9J3zMyCa6M1dmudJgClgEwEuUyxETe9C5bv24XY,57
2
2
  nothing_app/application.py,sha256=vM53vkQMdu85l18fbC-paNZBgZYIKrmp52pQ3K8ozBw,7600
3
- nothing_app/bluetooth.py,sha256=5T7nYK4pXapHIATtgGRRlUefJmhZ-niL2KztV-R6-rE,8815
3
+ nothing_app/bluetooth.py,sha256=MQeF0u_13SZ1GAY2UeadW4bMvGpu5UbgIR912M81dBs,12824
4
4
  nothing_app/profiles.py,sha256=eop3-VXnjkmvlAmIxPlE7CWiS3OxHiQKruCDGiEgXqk,1088
5
5
  nothing_app/protocol.py,sha256=QPhkafFRHvEzVBrR0E3PhC911IVYbpPdcCWsH2LkFXQ,28683
6
6
  nothing_app/splash.py,sha256=8GhwQ4F2B9tsxu23VpragjLgxdMKVsg3ZQIlzRC2dY8,6811
@@ -12,9 +12,9 @@ nothing_app/data/style.css,sha256=s5AmafmzF3s_VyJFKlI-qJfyBmmZimwGjkSgJ4O25mc,16
12
12
  nothing_app/pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  nothing_app/pages/device.py,sha256=AouM4zl4w9613jff866Va0OSyxWdpEMolDhTZA2v0XI,23008
14
14
  nothing_app/pages/home.py,sha256=n4gj-aaKObFNwrASki2Sway6trIvZvSUCn9iHbhyjqY,8499
15
- something_x_dev-1.6.0.dev13.dist-info/licenses/LICENSE,sha256=f82aGY-Qd4Huw5T9EsynF6CJhVPdcnsTSuuaskxc1ak,1064
16
- something_x_dev-1.6.0.dev13.dist-info/METADATA,sha256=4rqKXuTsHeN-W1OPZ4xZMUUQofV11TcZ7eXPWyZfSVU,7238
17
- something_x_dev-1.6.0.dev13.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
- something_x_dev-1.6.0.dev13.dist-info/entry_points.txt,sha256=HpMQVOiNSmzNKoa2Fb65XtbK7NWTfxMDBrBNlk9WHi8,65
19
- something_x_dev-1.6.0.dev13.dist-info/top_level.txt,sha256=yERYCJXvIBXR40iWxixVVZI3z-B4gNYXEdXFiiWa7KI,12
20
- something_x_dev-1.6.0.dev13.dist-info/RECORD,,
15
+ something_x_dev-1.7.0.dev14.dist-info/licenses/LICENSE,sha256=f82aGY-Qd4Huw5T9EsynF6CJhVPdcnsTSuuaskxc1ak,1064
16
+ something_x_dev-1.7.0.dev14.dist-info/METADATA,sha256=O7JHrx49A3-UZ_bm1naDEYaofZmuVWVeHQdqjZT-Y6o,7564
17
+ something_x_dev-1.7.0.dev14.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
18
+ something_x_dev-1.7.0.dev14.dist-info/entry_points.txt,sha256=HpMQVOiNSmzNKoa2Fb65XtbK7NWTfxMDBrBNlk9WHi8,65
19
+ something_x_dev-1.7.0.dev14.dist-info/top_level.txt,sha256=yERYCJXvIBXR40iWxixVVZI3z-B4gNYXEdXFiiWa7KI,12
20
+ something_x_dev-1.7.0.dev14.dist-info/RECORD,,