something-x-dev 1.2.3.dev1__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/__init__.py +2 -0
- nothing_app/application.py +244 -0
- nothing_app/bluetooth.py +212 -0
- nothing_app/data/__init__.py +0 -0
- nothing_app/data/com.something.x.omarchy.desktop +13 -0
- nothing_app/data/style.css +530 -0
- nothing_app/pages/__init__.py +0 -0
- nothing_app/pages/device.py +599 -0
- nothing_app/pages/home.py +210 -0
- nothing_app/profiles.py +41 -0
- nothing_app/protocol.py +650 -0
- nothing_app/splash.py +181 -0
- nothing_app/window.py +89 -0
- something_x_dev-1.2.3.dev1.dist-info/METADATA +201 -0
- something_x_dev-1.2.3.dev1.dist-info/RECORD +19 -0
- something_x_dev-1.2.3.dev1.dist-info/WHEEL +5 -0
- something_x_dev-1.2.3.dev1.dist-info/entry_points.txt +2 -0
- something_x_dev-1.2.3.dev1.dist-info/licenses/LICENSE +21 -0
- something_x_dev-1.2.3.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import gi
|
|
2
|
+
|
|
3
|
+
gi.require_version("Gtk", "4.0")
|
|
4
|
+
from gi.repository import Gtk, GLib, GObject
|
|
5
|
+
|
|
6
|
+
from ..bluetooth import BluetoothDevice, BluetoothManager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeviceRow(Gtk.Box):
|
|
10
|
+
def __init__(self, device: BluetoothDevice):
|
|
11
|
+
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=14)
|
|
12
|
+
self.add_css_class("device-card")
|
|
13
|
+
self.device = device
|
|
14
|
+
self._build()
|
|
15
|
+
|
|
16
|
+
def _build(self):
|
|
17
|
+
icon_name = "audio-headphones-symbolic"
|
|
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)
|
|
23
|
+
icon.set_pixel_size(28)
|
|
24
|
+
icon.set_opacity(0.6)
|
|
25
|
+
self.append(icon)
|
|
26
|
+
|
|
27
|
+
text_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
|
|
28
|
+
text_box.set_hexpand(True)
|
|
29
|
+
|
|
30
|
+
name_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
|
31
|
+
name_lbl = Gtk.Label(label=self.device.name)
|
|
32
|
+
name_lbl.add_css_class("device-card-name")
|
|
33
|
+
name_lbl.set_xalign(0)
|
|
34
|
+
name_row.append(name_lbl)
|
|
35
|
+
|
|
36
|
+
if self.device.is_nothing:
|
|
37
|
+
badge = Gtk.Label(label="NOTHING")
|
|
38
|
+
badge.add_css_class("status-nothing")
|
|
39
|
+
name_row.append(badge)
|
|
40
|
+
|
|
41
|
+
text_box.append(name_row)
|
|
42
|
+
|
|
43
|
+
addr_lbl = Gtk.Label(label=self.device.address)
|
|
44
|
+
addr_lbl.add_css_class("device-card-address")
|
|
45
|
+
addr_lbl.set_xalign(0)
|
|
46
|
+
text_box.append(addr_lbl)
|
|
47
|
+
|
|
48
|
+
bottom_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=8)
|
|
49
|
+
status_lbl = Gtk.Label(label="● Connected" if self.device.connected else "○ Disconnected")
|
|
50
|
+
status_lbl.add_css_class("status-connected" if self.device.connected else "status-disconnected")
|
|
51
|
+
status_lbl.set_xalign(0)
|
|
52
|
+
bottom_row.append(status_lbl)
|
|
53
|
+
|
|
54
|
+
if self.device.battery is not None:
|
|
55
|
+
bat_lbl = Gtk.Label(label=f" {self.device.battery}%")
|
|
56
|
+
bat_lbl.add_css_class("battery-pct")
|
|
57
|
+
bat_lbl.set_opacity(0.6)
|
|
58
|
+
bottom_row.append(bat_lbl)
|
|
59
|
+
|
|
60
|
+
text_box.append(bottom_row)
|
|
61
|
+
self.append(text_box)
|
|
62
|
+
|
|
63
|
+
arrow = Gtk.Image.new_from_icon_name("go-next-symbolic")
|
|
64
|
+
arrow.set_opacity(0.3)
|
|
65
|
+
self.append(arrow)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class HomePage(Gtk.Box):
|
|
69
|
+
__gsignals__ = {
|
|
70
|
+
"device-selected": (GObject.SignalFlags.RUN_FIRST, None, (GObject.TYPE_PYOBJECT,)),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
def __init__(self, bt_manager: BluetoothManager):
|
|
74
|
+
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
|
75
|
+
self._bt = bt_manager
|
|
76
|
+
self._scanning = False
|
|
77
|
+
self._build()
|
|
78
|
+
bt_manager.connect("devices-changed", self._on_devices_changed)
|
|
79
|
+
self._refresh_list()
|
|
80
|
+
|
|
81
|
+
def _build(self):
|
|
82
|
+
scroll = Gtk.ScrolledWindow()
|
|
83
|
+
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
|
|
84
|
+
scroll.set_vexpand(True)
|
|
85
|
+
|
|
86
|
+
self._content = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
87
|
+
self._content.add_css_class("nothing-page")
|
|
88
|
+
scroll.set_child(self._content)
|
|
89
|
+
self.append(scroll)
|
|
90
|
+
|
|
91
|
+
header_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
|
92
|
+
header_box.set_margin_top(20)
|
|
93
|
+
header_box.set_margin_bottom(4)
|
|
94
|
+
|
|
95
|
+
dot = Gtk.Box()
|
|
96
|
+
dot.add_css_class("nothing-dot")
|
|
97
|
+
dot.set_valign(Gtk.Align.CENTER)
|
|
98
|
+
header_box.append(dot)
|
|
99
|
+
|
|
100
|
+
brand = Gtk.Label(label="Something X")
|
|
101
|
+
brand.add_css_class("section-label")
|
|
102
|
+
brand.set_margin_top(0)
|
|
103
|
+
brand.set_margin_bottom(0)
|
|
104
|
+
header_box.append(brand)
|
|
105
|
+
self._content.append(header_box)
|
|
106
|
+
|
|
107
|
+
self._nothing_label = Gtk.Label(label="MY DEVICES")
|
|
108
|
+
self._nothing_label.add_css_class("section-label")
|
|
109
|
+
self._nothing_label.set_xalign(0)
|
|
110
|
+
self._content.append(self._nothing_label)
|
|
111
|
+
|
|
112
|
+
self._nothing_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
|
113
|
+
self._content.append(self._nothing_list)
|
|
114
|
+
|
|
115
|
+
self._other_label = Gtk.Label(label="OTHER DEVICES")
|
|
116
|
+
self._other_label.add_css_class("section-label")
|
|
117
|
+
self._other_label.set_xalign(0)
|
|
118
|
+
self._content.append(self._other_label)
|
|
119
|
+
|
|
120
|
+
self._other_list = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0)
|
|
121
|
+
self._content.append(self._other_list)
|
|
122
|
+
|
|
123
|
+
self._empty_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
|
|
124
|
+
self._empty_box.set_margin_top(60)
|
|
125
|
+
self._empty_box.set_halign(Gtk.Align.CENTER)
|
|
126
|
+
|
|
127
|
+
empty_icon = Gtk.Image.new_from_icon_name("bluetooth-symbolic")
|
|
128
|
+
empty_icon.set_pixel_size(48)
|
|
129
|
+
empty_icon.set_opacity(0.15)
|
|
130
|
+
self._empty_box.append(empty_icon)
|
|
131
|
+
|
|
132
|
+
empty_title = Gtk.Label(label="NO DEVICES")
|
|
133
|
+
empty_title.add_css_class("empty-title")
|
|
134
|
+
self._empty_box.append(empty_title)
|
|
135
|
+
|
|
136
|
+
empty_sub = Gtk.Label(label="Pair a device via Bluetooth settings")
|
|
137
|
+
empty_sub.add_css_class("empty-subtitle")
|
|
138
|
+
self._empty_box.append(empty_sub)
|
|
139
|
+
|
|
140
|
+
self._content.append(self._empty_box)
|
|
141
|
+
|
|
142
|
+
scan_row = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
|
|
143
|
+
scan_row.set_margin_top(28)
|
|
144
|
+
scan_row.set_halign(Gtk.Align.CENTER)
|
|
145
|
+
|
|
146
|
+
self._scan_btn = Gtk.Button(label="SCAN FOR DEVICES")
|
|
147
|
+
self._scan_btn.add_css_class("scan-button")
|
|
148
|
+
self._scan_btn.connect("clicked", self._on_scan_clicked)
|
|
149
|
+
scan_row.append(self._scan_btn)
|
|
150
|
+
self._content.append(scan_row)
|
|
151
|
+
|
|
152
|
+
self._bt_warning = Gtk.Label(label="⚠ Bluetooth unavailable — is bluetoothd running?")
|
|
153
|
+
self._bt_warning.add_css_class("empty-subtitle")
|
|
154
|
+
self._bt_warning.set_margin_top(8)
|
|
155
|
+
self._bt_warning.set_halign(Gtk.Align.CENTER)
|
|
156
|
+
self._content.append(self._bt_warning)
|
|
157
|
+
self._bt_warning.set_visible(not self._bt.available)
|
|
158
|
+
|
|
159
|
+
def _clear_list(self, box: Gtk.Box):
|
|
160
|
+
while True:
|
|
161
|
+
child = box.get_first_child()
|
|
162
|
+
if child is None:
|
|
163
|
+
break
|
|
164
|
+
box.remove(child)
|
|
165
|
+
|
|
166
|
+
def _refresh_list(self):
|
|
167
|
+
devices = self._bt.get_all()
|
|
168
|
+
nothing = [d for d in devices if d.is_nothing]
|
|
169
|
+
others = [d for d in devices if not d.is_nothing]
|
|
170
|
+
|
|
171
|
+
self._clear_list(self._nothing_list)
|
|
172
|
+
self._clear_list(self._other_list)
|
|
173
|
+
|
|
174
|
+
for dev in nothing:
|
|
175
|
+
self._nothing_list.append(self._make_row(dev))
|
|
176
|
+
|
|
177
|
+
for dev in others:
|
|
178
|
+
self._other_list.append(self._make_row(dev))
|
|
179
|
+
|
|
180
|
+
self._nothing_label.set_visible(bool(nothing))
|
|
181
|
+
self._other_label.set_visible(bool(others))
|
|
182
|
+
self._empty_box.set_visible(not devices)
|
|
183
|
+
|
|
184
|
+
def _make_row(self, device: BluetoothDevice) -> Gtk.Button:
|
|
185
|
+
btn = Gtk.Button()
|
|
186
|
+
btn.set_has_frame(False)
|
|
187
|
+
btn.add_css_class("device-row-btn")
|
|
188
|
+
row = DeviceRow(device)
|
|
189
|
+
btn.set_child(row)
|
|
190
|
+
btn.connect("clicked", lambda _b, d=device: self.emit("device-selected", d))
|
|
191
|
+
return btn
|
|
192
|
+
|
|
193
|
+
def _on_devices_changed(self, _manager):
|
|
194
|
+
self._refresh_list()
|
|
195
|
+
|
|
196
|
+
def _on_scan_clicked(self, _btn):
|
|
197
|
+
if self._scanning:
|
|
198
|
+
return
|
|
199
|
+
self._scanning = True
|
|
200
|
+
self._scan_btn.set_label("SCANNING…")
|
|
201
|
+
self._scan_btn.set_sensitive(False)
|
|
202
|
+
self._bt.start_discovery()
|
|
203
|
+
GLib.timeout_add_seconds(30, self._scan_done)
|
|
204
|
+
|
|
205
|
+
def _scan_done(self):
|
|
206
|
+
self._scanning = False
|
|
207
|
+
self._scan_btn.set_label("SCAN FOR DEVICES")
|
|
208
|
+
self._scan_btn.set_sensitive(True)
|
|
209
|
+
self._refresh_list()
|
|
210
|
+
return False
|
nothing_app/profiles.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
_DIR = os.path.expanduser("~/.config/something-x")
|
|
5
|
+
_PROFILES_FILE = os.path.join(_DIR, "profiles.json")
|
|
6
|
+
_LAST_DEV_FILE = os.path.join(_DIR, "last_device")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load(address: str) -> dict:
|
|
10
|
+
try:
|
|
11
|
+
with open(_PROFILES_FILE) as f:
|
|
12
|
+
return json.load(f).get(address, {})
|
|
13
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
14
|
+
return {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def save(address: str, anc_mode: int, eq_preset: str):
|
|
18
|
+
os.makedirs(_DIR, exist_ok=True)
|
|
19
|
+
data = {}
|
|
20
|
+
try:
|
|
21
|
+
with open(_PROFILES_FILE) as f:
|
|
22
|
+
data = json.load(f)
|
|
23
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
24
|
+
pass
|
|
25
|
+
data[address] = {"anc": anc_mode, "eq": eq_preset}
|
|
26
|
+
with open(_PROFILES_FILE, "w") as f:
|
|
27
|
+
json.dump(data, f, indent=2)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_last_device() -> str | None:
|
|
31
|
+
try:
|
|
32
|
+
addr = open(_LAST_DEV_FILE).read().strip()
|
|
33
|
+
return addr or None
|
|
34
|
+
except FileNotFoundError:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_last_device(address: str):
|
|
39
|
+
os.makedirs(_DIR, exist_ok=True)
|
|
40
|
+
with open(_LAST_DEV_FILE, "w") as f:
|
|
41
|
+
f.write(address)
|