something-x-dev 1.3.0.dev5__tar.gz → 1.4.0.dev7__tar.gz
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.
- {something_x_dev-1.3.0.dev5/something_x_dev.egg-info → something_x_dev-1.4.0.dev7}/PKG-INFO +1 -1
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/application.py +3 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/bluetooth.py +34 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/pages/device.py +32 -6
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/pages/home.py +2 -7
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/protocol.py +32 -10
- something_x_dev-1.4.0.dev7/nothing_app/tray.py +149 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/pyproject.toml +1 -1
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7/something_x_dev.egg-info}/PKG-INFO +1 -1
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/something_x_dev.egg-info/SOURCES.txt +1 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/LICENSE +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/README.md +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/__init__.py +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/data/__init__.py +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/data/com.something.x.omarchy.desktop +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/data/style.css +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/pages/__init__.py +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/profiles.py +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/splash.py +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/nothing_app/window.py +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/setup.cfg +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/something_x_dev.egg-info/dependency_links.txt +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/something_x_dev.egg-info/entry_points.txt +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/something_x_dev.egg-info/requires.txt +0 -0
- {something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/something_x_dev.egg-info/top_level.txt +0 -0
|
@@ -13,6 +13,7 @@ from gi.repository import Gtk, Adw, Gdk, Gio, GLib
|
|
|
13
13
|
from .bluetooth import BluetoothManager
|
|
14
14
|
from .window import SomethingXWindow
|
|
15
15
|
from .splash import SplashScreen
|
|
16
|
+
from .tray import SomethingXTray
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def _install_desktop_file():
|
|
@@ -50,6 +51,7 @@ class SomethingXApplication(Adw.Application):
|
|
|
50
51
|
self._bt: BluetoothManager | None = None
|
|
51
52
|
self._splash: SplashScreen | None = None
|
|
52
53
|
self._window: SomethingXWindow | None = None
|
|
54
|
+
self._tray: SomethingXTray | None = None
|
|
53
55
|
self.connect("activate", self._on_activate)
|
|
54
56
|
|
|
55
57
|
def _on_activate(self, _app):
|
|
@@ -72,6 +74,7 @@ class SomethingXApplication(Adw.Application):
|
|
|
72
74
|
win = SomethingXWindow(bt_manager=self._bt, application=self)
|
|
73
75
|
win.connect("close-request", self._on_window_close)
|
|
74
76
|
self._window = win
|
|
77
|
+
self._tray = SomethingXTray(self._bt, on_show_window=win.present)
|
|
75
78
|
win.present()
|
|
76
79
|
if self._splash:
|
|
77
80
|
self._splash.destroy()
|
|
@@ -44,6 +44,40 @@ class BluetoothDevice:
|
|
|
44
44
|
return f"<BTDevice {self.name!r} {'●' if self.connected else '○'}>"
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
# BlueZ Device1.Icon → GTK symbolic icon name
|
|
48
|
+
_BLUEZ_ICON_MAP: dict[str, str] = {
|
|
49
|
+
"audio-headphones": "audio-headphones-symbolic",
|
|
50
|
+
"audio-headset": "audio-headset-symbolic",
|
|
51
|
+
"audio-card": "audio-card-symbolic",
|
|
52
|
+
"input-mouse": "input-mouse-symbolic",
|
|
53
|
+
"input-keyboard": "input-keyboard-symbolic",
|
|
54
|
+
"input-gaming": "input-gaming-symbolic",
|
|
55
|
+
"input-tablet": "input-tablet-symbolic",
|
|
56
|
+
"phone": "phone-symbolic",
|
|
57
|
+
"computer": "computer-symbolic",
|
|
58
|
+
"printer": "printer-symbolic",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_WATCH_NAME_PATTERNS = ("watch", "band", "gear", "amazfit", "fenix", "vivoactive", "galaxy fit")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def device_icon_name(device: "BluetoothDevice | None") -> str:
|
|
65
|
+
"""Return a GTK symbolic icon name for a Bluetooth device."""
|
|
66
|
+
if device is None:
|
|
67
|
+
return "audio-headphones-symbolic"
|
|
68
|
+
name_lower = device.name.lower()
|
|
69
|
+
if any(p in name_lower for p in _WATCH_NAME_PATTERNS):
|
|
70
|
+
return "alarm-symbolic"
|
|
71
|
+
if "ear (stick)" in name_lower:
|
|
72
|
+
return "audio-input-microphone-symbolic"
|
|
73
|
+
mapped = _BLUEZ_ICON_MAP.get(device.icon)
|
|
74
|
+
if mapped:
|
|
75
|
+
return mapped
|
|
76
|
+
if "phone" in name_lower:
|
|
77
|
+
return "phone-symbolic"
|
|
78
|
+
return "audio-headphones-symbolic"
|
|
79
|
+
|
|
80
|
+
|
|
47
81
|
class BluetoothManager(GObject.Object):
|
|
48
82
|
__gsignals__ = {
|
|
49
83
|
"devices-changed": (GObject.SignalFlags.RUN_FIRST, None, ()),
|
|
@@ -93,19 +93,24 @@ class EarbudVisual(Gtk.DrawingArea):
|
|
|
93
93
|
self._left = -1
|
|
94
94
|
self._right = -1
|
|
95
95
|
self._case = -1
|
|
96
|
+
self._left_wearing = False
|
|
97
|
+
self._right_wearing = False
|
|
96
98
|
|
|
97
|
-
def update(
|
|
99
|
+
def update(
|
|
100
|
+
self, left: int, right: int, case: int, left_wearing: bool = False, right_wearing: bool = False
|
|
101
|
+
):
|
|
98
102
|
self._left, self._right, self._case = left, right, case
|
|
103
|
+
self._left_wearing, self._right_wearing = left_wearing, right_wearing
|
|
99
104
|
self.queue_draw()
|
|
100
105
|
|
|
101
106
|
def _draw(self, _area, cr, width, height):
|
|
102
107
|
cx = width / 2
|
|
103
108
|
cy = height / 2 - 8
|
|
104
|
-
self._draw_bud(cr, cx - 92, cy, self._left, "L")
|
|
105
|
-
self._draw_bud(cr, cx + 92, cy, self._right, "R")
|
|
109
|
+
self._draw_bud(cr, cx - 92, cy, self._left, "L", self._left_wearing)
|
|
110
|
+
self._draw_bud(cr, cx + 92, cy, self._right, "R", self._right_wearing)
|
|
106
111
|
self._draw_case(cr, cx, cy + 54, self._case)
|
|
107
112
|
|
|
108
|
-
def _draw_bud(self, cr, cx, cy, pct, label):
|
|
113
|
+
def _draw_bud(self, cr, cx, cy, pct, label, wearing: bool = False):
|
|
109
114
|
R = 42
|
|
110
115
|
r = 29
|
|
111
116
|
bc = _battery_color(pct) if pct >= 0 else (0.18, 0.18, 0.18)
|
|
@@ -166,12 +171,27 @@ class EarbudVisual(Gtk.DrawingArea):
|
|
|
166
171
|
cr.move_to(cx - te.width / 2 - te.x_bearing, cy - te.height / 2 - te.y_bearing)
|
|
167
172
|
cr.show_text(text)
|
|
168
173
|
|
|
174
|
+
# in-ear indicator dot (always rendered; glows red when wearing)
|
|
175
|
+
dot_y = cy + R + 8
|
|
176
|
+
if wearing:
|
|
177
|
+
rg = cairo.RadialGradient(cx, dot_y, 0, cx, dot_y, 9)
|
|
178
|
+
rg.add_color_stop_rgba(0, 0.87, 0.18, 0.18, 0.30)
|
|
179
|
+
rg.add_color_stop_rgba(1, 0.87, 0.18, 0.18, 0.0)
|
|
180
|
+
cr.set_source(rg)
|
|
181
|
+
cr.arc(cx, dot_y, 9, 0, 2 * math.pi)
|
|
182
|
+
cr.fill()
|
|
183
|
+
cr.set_source_rgba(0.87, 0.18, 0.18, 0.9)
|
|
184
|
+
else:
|
|
185
|
+
cr.set_source_rgba(1.0, 1.0, 1.0, 0.07)
|
|
186
|
+
cr.arc(cx, dot_y, 3, 0, 2 * math.pi)
|
|
187
|
+
cr.fill()
|
|
188
|
+
|
|
169
189
|
# L / R label below
|
|
170
190
|
cr.set_source_rgba(1.0, 1.0, 1.0, 0.20)
|
|
171
191
|
cr.select_font_face(_MONO, cairo.FontSlant.NORMAL, cairo.FontWeight.BOLD)
|
|
172
192
|
cr.set_font_size(9)
|
|
173
193
|
te = cr.text_extents(label)
|
|
174
|
-
cr.move_to(cx - te.width / 2 - te.x_bearing, cy + R +
|
|
194
|
+
cr.move_to(cx - te.width / 2 - te.x_bearing, cy + R + 20)
|
|
175
195
|
cr.show_text(label)
|
|
176
196
|
|
|
177
197
|
def _draw_case(self, cr, cx, cy, pct):
|
|
@@ -508,7 +528,13 @@ class DevicePage(Gtk.Box):
|
|
|
508
528
|
|
|
509
529
|
def _on_state_changed(self, dev: NothingDevice):
|
|
510
530
|
state = dev.state
|
|
511
|
-
self._visual.update(
|
|
531
|
+
self._visual.update(
|
|
532
|
+
state.left_battery,
|
|
533
|
+
state.right_battery,
|
|
534
|
+
state.case_battery,
|
|
535
|
+
state.left_wearing,
|
|
536
|
+
state.right_wearing,
|
|
537
|
+
)
|
|
512
538
|
self._sync_anc_ui(state.anc_mode)
|
|
513
539
|
self._sync_eq_ui(state.eq_preset)
|
|
514
540
|
self._updating_ui = True
|
|
@@ -3,7 +3,7 @@ import gi
|
|
|
3
3
|
gi.require_version("Gtk", "4.0")
|
|
4
4
|
from gi.repository import Gtk, GLib, GObject
|
|
5
5
|
|
|
6
|
-
from ..bluetooth import BluetoothDevice, BluetoothManager
|
|
6
|
+
from ..bluetooth import BluetoothDevice, BluetoothManager, device_icon_name
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class DeviceRow(Gtk.Box):
|
|
@@ -14,12 +14,7 @@ class DeviceRow(Gtk.Box):
|
|
|
14
14
|
self._build()
|
|
15
15
|
|
|
16
16
|
def _build(self):
|
|
17
|
-
|
|
18
|
-
if "phone" in self.device.name.lower():
|
|
19
|
-
icon_name = "phone-symbolic"
|
|
20
|
-
elif "ear (stick)" in self.device.name.lower():
|
|
21
|
-
icon_name = "audio-input-microphone-symbolic"
|
|
22
|
-
icon = Gtk.Image.new_from_icon_name(icon_name)
|
|
17
|
+
icon = Gtk.Image.new_from_icon_name(device_icon_name(self.device))
|
|
23
18
|
icon.set_pixel_size(28)
|
|
24
19
|
icon.set_opacity(0.6)
|
|
25
20
|
self.append(icon)
|
|
@@ -417,6 +417,8 @@ class NothingDevice(GObject.Object):
|
|
|
417
417
|
GLib.timeout_add(3000, self._activation_fallback)
|
|
418
418
|
elif cmd_id == _CMD_SET_ACTIVATED:
|
|
419
419
|
_log(f"[RX INFO] activation ACK payload={payload.hex()}")
|
|
420
|
+
if not self._activated:
|
|
421
|
+
GLib.timeout_add(2000, self._poll_earphone_status)
|
|
420
422
|
self._activated = True
|
|
421
423
|
from . import profiles
|
|
422
424
|
|
|
@@ -433,8 +435,15 @@ class NothingDevice(GObject.Object):
|
|
|
433
435
|
changed = self._parse_battery(payload)
|
|
434
436
|
elif cmd_id in (_CMD_NOISE_RED, _EVT_NOISE_RED):
|
|
435
437
|
changed = self._parse_anc(payload)
|
|
436
|
-
elif cmd_id
|
|
438
|
+
elif cmd_id == _CMD_EARPHONE:
|
|
437
439
|
changed = self._parse_earphone_status(payload)
|
|
440
|
+
elif cmd_id == _EVT_STATUS:
|
|
441
|
+
# The pushed event only carries accurate data for the bud that
|
|
442
|
+
# changed; the other entries are stale placeholders. Use it purely
|
|
443
|
+
# as a trigger and re-query for a fresh full snapshot.
|
|
444
|
+
if _DEBUG:
|
|
445
|
+
_log(f"[protocol] EVT_STATUS {payload.hex()} → re-query GET_EARPHONE")
|
|
446
|
+
self._x55_send(_CMD_EARPHONE)
|
|
438
447
|
elif cmd_id == _CMD_HOST_VERSION:
|
|
439
448
|
ver = payload.decode(errors="replace").strip("\x00").strip()
|
|
440
449
|
if ver and ver != self.state.firmware_version:
|
|
@@ -512,26 +521,29 @@ class NothingDevice(GObject.Object):
|
|
|
512
521
|
return changed
|
|
513
522
|
|
|
514
523
|
def _parse_earphone_status(self, payload: bytes) -> bool:
|
|
515
|
-
# payload: [count:1][type:1][val:1]...
|
|
516
|
-
#
|
|
517
|
-
#
|
|
524
|
+
# payload: [count:1][type:1][val:1]... (only GET responses reach here;
|
|
525
|
+
# they are a fresh full snapshot, unlike the EVT push frames)
|
|
526
|
+
# EarphoneStatus.java: bit0=inCase, bit2=inEar, bit7=isConnect
|
|
527
|
+
# type: 2=left, 3=right, 4=case, 5=tws, 6=stereo
|
|
518
528
|
if len(payload) < 3:
|
|
519
529
|
return False
|
|
520
530
|
count = payload[0]
|
|
521
531
|
changed = False
|
|
532
|
+
if _DEBUG:
|
|
533
|
+
_log(f"[protocol] earphone raw={payload.hex()}")
|
|
522
534
|
for i in range(1, 1 + count * 2, 2):
|
|
523
535
|
if i + 1 >= len(payload):
|
|
524
536
|
break
|
|
525
537
|
etype = payload[i]
|
|
526
538
|
val = payload[i + 1]
|
|
539
|
+
if etype not in (2, 3):
|
|
540
|
+
continue
|
|
527
541
|
in_ear = bool(val & 0x04)
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
if etype == 2 and wearing != self.state.left_wearing:
|
|
531
|
-
self.state.left_wearing = wearing
|
|
542
|
+
if etype == 2 and in_ear != self.state.left_wearing:
|
|
543
|
+
self.state.left_wearing = in_ear
|
|
532
544
|
changed = True
|
|
533
|
-
elif etype == 3 and
|
|
534
|
-
self.state.right_wearing =
|
|
545
|
+
elif etype == 3 and in_ear != self.state.right_wearing:
|
|
546
|
+
self.state.right_wearing = in_ear
|
|
535
547
|
changed = True
|
|
536
548
|
if changed:
|
|
537
549
|
_log(f"[protocol] wearing L={self.state.left_wearing} R={self.state.right_wearing}")
|
|
@@ -632,10 +644,20 @@ class NothingDevice(GObject.Object):
|
|
|
632
644
|
).start()
|
|
633
645
|
break
|
|
634
646
|
|
|
647
|
+
def _poll_earphone_status(self):
|
|
648
|
+
# The firmware only computes a fresh per-bud snapshot when asked; the
|
|
649
|
+
# pushed EVT frames carry stale placeholder entries for the bud that
|
|
650
|
+
# didn't change. Polling keeps both buds' wearing state accurate.
|
|
651
|
+
if not self._rfcomm_connected:
|
|
652
|
+
return False
|
|
653
|
+
self._x55_send(_CMD_EARPHONE)
|
|
654
|
+
return True
|
|
655
|
+
|
|
635
656
|
def _activation_fallback(self):
|
|
636
657
|
if not self._activated and self._rfcomm_connected:
|
|
637
658
|
_log("[protocol] activation ACK not received within 3s — sending GET queries")
|
|
638
659
|
self._activated = True
|
|
660
|
+
GLib.timeout_add(2000, self._poll_earphone_status)
|
|
639
661
|
self._x55_send(_CMD_BATTERY)
|
|
640
662
|
self._x55_send(_CMD_NOISE_RED, bytes([0x03]))
|
|
641
663
|
self._x55_send(_CMD_EARPHONE)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import dbus
|
|
3
|
+
import dbus.service
|
|
4
|
+
from gi.repository import GLib, GObject
|
|
5
|
+
|
|
6
|
+
from .bluetooth import BluetoothManager, BluetoothDevice, device_icon_name
|
|
7
|
+
|
|
8
|
+
_ITEM_IFACE = "org.kde.StatusNotifierItem"
|
|
9
|
+
_WATCHER_IFACE = "org.kde.StatusNotifierWatcher"
|
|
10
|
+
_WATCHER_SERVICE = "org.kde.StatusNotifierWatcher"
|
|
11
|
+
_ITEM_PATH = "/StatusNotifierItem"
|
|
12
|
+
|
|
13
|
+
_EMPTY_PIXMAPS = dbus.Array([], signature="(iiay)")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _SNIItem(dbus.service.Object):
|
|
17
|
+
def __init__(self, bus, service_name, on_activate):
|
|
18
|
+
bus_name = dbus.service.BusName(service_name, bus)
|
|
19
|
+
super().__init__(bus_name, _ITEM_PATH)
|
|
20
|
+
self._on_activate = on_activate
|
|
21
|
+
self._icon_name = "audio-headphones"
|
|
22
|
+
self._tooltip_title = "Something X"
|
|
23
|
+
self._tooltip_body = ""
|
|
24
|
+
|
|
25
|
+
# ── SNI methods ────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
@dbus.service.method(_ITEM_IFACE, in_signature="ii")
|
|
28
|
+
def Activate(self, x, y):
|
|
29
|
+
GLib.idle_add(self._on_activate)
|
|
30
|
+
|
|
31
|
+
@dbus.service.method(_ITEM_IFACE, in_signature="ii")
|
|
32
|
+
def SecondaryActivate(self, x, y):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@dbus.service.method(_ITEM_IFACE, in_signature="ii")
|
|
36
|
+
def ContextMenu(self, x, y):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@dbus.service.method(_ITEM_IFACE, in_signature="is")
|
|
40
|
+
def Scroll(self, delta, orientation):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
# ── SNI signals ───────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
@dbus.service.signal(_ITEM_IFACE)
|
|
46
|
+
def NewIcon(self):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
@dbus.service.signal(_ITEM_IFACE)
|
|
50
|
+
def NewToolTip(self):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
@dbus.service.signal(_ITEM_IFACE, signature="s")
|
|
54
|
+
def NewStatus(self, status):
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
# ── D-Bus Properties ──────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="ss", out_signature="v")
|
|
60
|
+
def Get(self, interface, prop):
|
|
61
|
+
return self._props()[prop]
|
|
62
|
+
|
|
63
|
+
@dbus.service.method(dbus.PROPERTIES_IFACE, in_signature="s", out_signature="a{sv}")
|
|
64
|
+
def GetAll(self, interface):
|
|
65
|
+
return self._props()
|
|
66
|
+
|
|
67
|
+
def _props(self):
|
|
68
|
+
tooltip = dbus.Struct(
|
|
69
|
+
(
|
|
70
|
+
dbus.String(""),
|
|
71
|
+
_EMPTY_PIXMAPS,
|
|
72
|
+
dbus.String(self._tooltip_title),
|
|
73
|
+
dbus.String(self._tooltip_body),
|
|
74
|
+
),
|
|
75
|
+
signature="sa(iiay)ss",
|
|
76
|
+
)
|
|
77
|
+
return {
|
|
78
|
+
"Id": dbus.String("something-x"),
|
|
79
|
+
"Category": dbus.String("Hardware"),
|
|
80
|
+
"Title": dbus.String("Something X"),
|
|
81
|
+
"Status": dbus.String("Active"),
|
|
82
|
+
"WindowId": dbus.UInt32(0),
|
|
83
|
+
"IconName": dbus.String(self._icon_name),
|
|
84
|
+
"IconPixmap": _EMPTY_PIXMAPS,
|
|
85
|
+
"OverlayIconName": dbus.String(""),
|
|
86
|
+
"OverlayIconPixmap": _EMPTY_PIXMAPS,
|
|
87
|
+
"AttentionIconName": dbus.String(""),
|
|
88
|
+
"AttentionIconPixmap": _EMPTY_PIXMAPS,
|
|
89
|
+
"AttentionMovieName": dbus.String(""),
|
|
90
|
+
"ToolTip": tooltip,
|
|
91
|
+
"ItemIsMenu": dbus.Boolean(False),
|
|
92
|
+
"Menu": dbus.ObjectPath(_ITEM_PATH),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# ── update helpers ────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
def set_icon(self, icon_name: str):
|
|
98
|
+
if icon_name != self._icon_name:
|
|
99
|
+
self._icon_name = icon_name
|
|
100
|
+
self.NewIcon()
|
|
101
|
+
|
|
102
|
+
def set_tooltip(self, title: str, body: str):
|
|
103
|
+
self._tooltip_title = title
|
|
104
|
+
self._tooltip_body = body
|
|
105
|
+
self.NewToolTip()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SomethingXTray(GObject.Object):
|
|
109
|
+
"""StatusNotifierItem tray icon. Shows battery on hover; icon adapts to device type."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, bt_manager: BluetoothManager, on_show_window):
|
|
112
|
+
super().__init__()
|
|
113
|
+
self._bt = bt_manager
|
|
114
|
+
self._on_show = on_show_window
|
|
115
|
+
self._item: _SNIItem | None = None
|
|
116
|
+
self._setup()
|
|
117
|
+
bt_manager.connect("devices-changed", self._on_devices_changed)
|
|
118
|
+
|
|
119
|
+
def _setup(self):
|
|
120
|
+
try:
|
|
121
|
+
bus = dbus.SessionBus()
|
|
122
|
+
service_name = f"org.kde.StatusNotifierItem-{os.getpid()}-1"
|
|
123
|
+
self._item = _SNIItem(bus, service_name, self._on_show)
|
|
124
|
+
try:
|
|
125
|
+
watcher = bus.get_object(_WATCHER_SERVICE, "/StatusNotifierWatcher")
|
|
126
|
+
dbus.Interface(watcher, _WATCHER_IFACE).RegisterStatusNotifierItem(service_name)
|
|
127
|
+
except dbus.exceptions.DBusException:
|
|
128
|
+
pass # watcher not running; item is still exported on the bus
|
|
129
|
+
except Exception as exc:
|
|
130
|
+
print(f"[tray] SNI setup failed: {exc}")
|
|
131
|
+
|
|
132
|
+
def _on_devices_changed(self, _manager):
|
|
133
|
+
if not self._item:
|
|
134
|
+
return
|
|
135
|
+
nothing_devs = self._bt.get_nothing_devices()
|
|
136
|
+
connected = [d for d in nothing_devs if d.connected]
|
|
137
|
+
if connected:
|
|
138
|
+
dev = connected[0]
|
|
139
|
+
parts = []
|
|
140
|
+
if dev.battery is not None:
|
|
141
|
+
parts.append(f"{dev.name}: {dev.battery}%")
|
|
142
|
+
self._item.set_tooltip("Something X", "\n".join(parts) if parts else "Connected")
|
|
143
|
+
self._item.set_icon(device_icon_name(dev))
|
|
144
|
+
else:
|
|
145
|
+
# fall back to first paired Nothing device, or generic icon
|
|
146
|
+
paired = nothing_devs[0] if nothing_devs else None
|
|
147
|
+
icon = device_icon_name(paired) if paired else "audio-headphones"
|
|
148
|
+
self._item.set_tooltip("Something X", "No devices connected")
|
|
149
|
+
self._item.set_icon(icon)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/something_x_dev.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/something_x_dev.egg-info/requires.txt
RENAMED
|
File without changes
|
{something_x_dev-1.3.0.dev5 → something_x_dev-1.4.0.dev7}/something_x_dev.egg-info/top_level.txt
RENAMED
|
File without changes
|