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.
@@ -0,0 +1,2 @@
1
+ __version__ = "1.0.0"
2
+ APP_ID = "com.something.x.omarchy"
@@ -0,0 +1,244 @@
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ import sys
5
+ import importlib.resources
6
+ import gi
7
+
8
+ gi.require_version("Gtk", "4.0")
9
+ gi.require_version("Adw", "1")
10
+ gi.require_version("Gdk", "4.0")
11
+ from gi.repository import Gtk, Adw, Gdk, Gio, GLib
12
+
13
+ from .bluetooth import BluetoothManager
14
+ from .window import SomethingXWindow
15
+ from .splash import SplashScreen
16
+
17
+
18
+ def _install_desktop_file():
19
+ dest_dir = os.path.expanduser("~/.local/share/applications")
20
+ dest = os.path.join(dest_dir, "com.something.x.omarchy.desktop")
21
+ if os.path.exists(dest):
22
+ return
23
+ try:
24
+ ref = importlib.resources.files("nothing_app.data").joinpath("com.something.x.omarchy.desktop")
25
+ os.makedirs(dest_dir, exist_ok=True)
26
+ with importlib.resources.as_file(ref) as src:
27
+ shutil.copy2(src, dest)
28
+ subprocess.run(["update-desktop-database", dest_dir], capture_output=True)
29
+ print("[app] desktop file installed to ~/.local/share/applications/")
30
+ except Exception as exc:
31
+ print(f"[app] desktop file install skipped: {exc}")
32
+
33
+
34
+ def _css_path() -> str:
35
+ try:
36
+ ref = importlib.resources.files("nothing_app.data").joinpath("style.css")
37
+ return str(ref)
38
+ except Exception:
39
+ import os
40
+
41
+ return os.path.join(os.path.dirname(__file__), "data", "style.css")
42
+
43
+
44
+ class SomethingXApplication(Adw.Application):
45
+ def __init__(self):
46
+ super().__init__(
47
+ application_id="com.something.x.omarchy",
48
+ flags=Gio.ApplicationFlags.DEFAULT_FLAGS,
49
+ )
50
+ self._bt: BluetoothManager | None = None
51
+ self._splash: SplashScreen | None = None
52
+ self._window: SomethingXWindow | None = None
53
+ self.connect("activate", self._on_activate)
54
+
55
+ def _on_activate(self, _app):
56
+ # Second launch while already running: just show the existing window
57
+ if self._window is not None:
58
+ self._window.present()
59
+ return
60
+
61
+ _install_desktop_file()
62
+ Adw.StyleManager.get_default().set_color_scheme(Adw.ColorScheme.FORCE_DARK)
63
+ self._load_css()
64
+ self._bt = BluetoothManager()
65
+ splash = SplashScreen(on_done=self._on_splash_done)
66
+ splash.set_application(self)
67
+ self._splash = splash
68
+ splash.present()
69
+ splash.start()
70
+
71
+ def _on_splash_done(self):
72
+ win = SomethingXWindow(bt_manager=self._bt, application=self)
73
+ win.connect("close-request", self._on_window_close)
74
+ self._window = win
75
+ win.present()
76
+ if self._splash:
77
+ self._splash.destroy()
78
+ self._splash = None
79
+
80
+ def _on_window_close(self, _win):
81
+ # Hide instead of destroy so the app keeps running in background
82
+ self._window.hide()
83
+ subprocess.Popen(
84
+ [
85
+ "notify-send",
86
+ "-i",
87
+ "audio-headphones",
88
+ "Something X",
89
+ "Running in background. Launch again to reopen.",
90
+ ],
91
+ start_new_session=True,
92
+ )
93
+ return True # prevent default close/destroy
94
+
95
+ def _load_css(self):
96
+ provider = Gtk.CssProvider()
97
+ css = _css_path()
98
+ try:
99
+ provider.load_from_path(css)
100
+ except Exception as exc:
101
+ print(f"[app] CSS load failed ({css}): {exc}")
102
+
103
+ display = Gdk.Display.get_default()
104
+ if display:
105
+ Gtk.StyleContext.add_provider_for_display(
106
+ display,
107
+ provider,
108
+ Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
109
+ )
110
+
111
+
112
+ # ── CLI quick-toggle mode ─────────────────────────────────────────────────────
113
+
114
+ _ANC_ALIASES = {
115
+ "off": 0,
116
+ "0": 0,
117
+ "on": 1,
118
+ "anc": 1,
119
+ "noise": 1,
120
+ "transparency": 2,
121
+ "trans": 2,
122
+ "passthrough": 2,
123
+ }
124
+
125
+ _EQ_ALIASES = {
126
+ "balanced": "Balanced",
127
+ "bass": "More Bass",
128
+ "treble": "More Treble",
129
+ "voice": "Voice",
130
+ }
131
+
132
+
133
+ def _run_cli(argv: list[str]) -> int:
134
+ from . import protocol as _proto
135
+
136
+ _proto._QUIET = True
137
+ from .protocol import NothingDevice, ANCMode
138
+ from . import profiles
139
+
140
+ address = None
141
+ if "--device" in argv:
142
+ idx = argv.index("--device")
143
+ if idx + 1 < len(argv):
144
+ address = argv[idx + 1]
145
+
146
+ if address is None:
147
+ address = profiles.get_last_device()
148
+
149
+ if address is None:
150
+ print(
151
+ "No known device. Open the GUI and connect to a device first,\n"
152
+ "or pass --device AA:BB:CC:DD:EE:FF.",
153
+ file=sys.stderr,
154
+ )
155
+ return 1
156
+
157
+ loop = GLib.MainLoop()
158
+ dev = NothingDevice(address)
159
+ exit_code = [0]
160
+ _acted = [False]
161
+
162
+ def _act():
163
+ if _acted[0]:
164
+ return False
165
+ _acted[0] = True
166
+
167
+ if "--battery" in argv:
168
+ s = dev.state
169
+ parts = []
170
+ if s.left_battery >= 0:
171
+ parts.append(f"Left: {s.left_battery}%")
172
+ if s.right_battery >= 0:
173
+ parts.append(f"Right: {s.right_battery}%")
174
+ if s.case_battery >= 0:
175
+ parts.append(f"Case: {s.case_battery}%")
176
+ print(" ".join(parts) if parts else "No battery data received.")
177
+
178
+ if "--anc" in argv:
179
+ idx = argv.index("--anc")
180
+ val = argv[idx + 1] if idx + 1 < len(argv) else ""
181
+ mode = _ANC_ALIASES.get(val.lower())
182
+ if mode is None:
183
+ print(f"Unknown ANC value '{val}'. Use: off, on, transparency", file=sys.stderr)
184
+ exit_code[0] = 1
185
+ else:
186
+ dev.set_anc_mode(mode)
187
+ print(f"ANC → {ANCMode.LABELS.get(mode)}")
188
+
189
+ if "--eq" in argv:
190
+ idx = argv.index("--eq")
191
+ val = argv[idx + 1] if idx + 1 < len(argv) else ""
192
+ preset = _EQ_ALIASES.get(val.lower())
193
+ if preset is None:
194
+ print(f"Unknown EQ preset '{val}'. Use: balanced, bass, treble, voice", file=sys.stderr)
195
+ exit_code[0] = 1
196
+ else:
197
+ dev.set_eq_preset(preset)
198
+ print(f"EQ → {preset}")
199
+
200
+ GLib.timeout_add(600, loop.quit)
201
+ return False
202
+
203
+ def _on_state_changed(_d):
204
+ if dev.state.left_battery >= 0 or dev.state.right_battery >= 0:
205
+ _act()
206
+
207
+ def _on_timeout():
208
+ print("Timeout: device did not respond in time.", file=sys.stderr)
209
+ exit_code[0] = 1
210
+ loop.quit()
211
+ return False
212
+
213
+ dev.connect("state-changed", _on_state_changed)
214
+ dev.connect_rfcomm()
215
+ GLib.timeout_add(12000, _on_timeout)
216
+ loop.run()
217
+ dev.disconnect_rfcomm()
218
+ return exit_code[0]
219
+
220
+
221
+ def _print_help():
222
+ print(
223
+ "Usage:\n"
224
+ " something-x launch GUI\n"
225
+ " something-x --battery print battery levels\n"
226
+ " something-x --anc off|on|transparency set ANC mode\n"
227
+ " something-x --eq balanced|bass|treble|voice set EQ preset\n"
228
+ " something-x --device AA:BB:CC:DD:EE:FF target specific device\n"
229
+ )
230
+
231
+
232
+ def main():
233
+ argv = sys.argv[1:]
234
+ cli_flags = {"--battery", "--anc", "--eq"}
235
+
236
+ if "--help" in argv or "-h" in argv:
237
+ _print_help()
238
+ sys.exit(0)
239
+
240
+ if any(f in argv for f in cli_flags):
241
+ sys.exit(_run_cli(argv))
242
+
243
+ app = SomethingXApplication()
244
+ sys.exit(app.run(sys.argv))
@@ -0,0 +1,212 @@
1
+ import dbus
2
+ import dbus.mainloop.glib
3
+ from gi.repository import GLib, GObject
4
+
5
+ BLUEZ_SERVICE = "org.bluez"
6
+ ADAPTER_IFACE = "org.bluez.Adapter1"
7
+ DEVICE_IFACE = "org.bluez.Device1"
8
+ BATTERY_IFACE = "org.bluez.Battery1"
9
+ OBJ_MANAGER_IFACE = "org.freedesktop.DBus.ObjectManager"
10
+ PROPERTIES_IFACE = "org.freedesktop.DBus.Properties"
11
+
12
+ NOTHING_PATTERNS = (
13
+ "nothing ear",
14
+ "ear (1)",
15
+ "ear (2)",
16
+ "ear (a)",
17
+ "ear (stick)",
18
+ "cmf buds",
19
+ "cmf earphone",
20
+ "nothing phone",
21
+ )
22
+
23
+
24
+ class BluetoothDevice:
25
+ def __init__(self, path: str, props: dict):
26
+ self.path = path
27
+ self.address: str = str(props.get("Address", ""))
28
+ self.name: str = str(props.get("Name", props.get("Alias", "Unknown Device")))
29
+ self.connected: bool = bool(props.get("Connected", False))
30
+ self.paired: bool = bool(props.get("Paired", False))
31
+ self.battery: int | None = None
32
+ self.icon: str = str(props.get("Icon", "audio-headphones"))
33
+ self.is_nothing: bool = any(p in self.name.lower() for p in NOTHING_PATTERNS)
34
+
35
+ def update(self, changed: dict):
36
+ if "Connected" in changed:
37
+ self.connected = bool(changed["Connected"])
38
+ if "Name" in changed:
39
+ self.name = str(changed["Name"])
40
+ if "Alias" in changed and not self.name:
41
+ self.name = str(changed["Alias"])
42
+
43
+ def __repr__(self):
44
+ return f"<BTDevice {self.name!r} {'●' if self.connected else '○'}>"
45
+
46
+
47
+ class BluetoothManager(GObject.Object):
48
+ __gsignals__ = {
49
+ "devices-changed": (GObject.SignalFlags.RUN_FIRST, None, ()),
50
+ "device-connected": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
51
+ "device-disconnected": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
52
+ }
53
+
54
+ def __init__(self):
55
+ super().__init__()
56
+ self.devices: dict[str, BluetoothDevice] = {}
57
+ self._bus: dbus.SystemBus | None = None
58
+ self._available = False
59
+ self._init_dbus()
60
+
61
+ def _init_dbus(self):
62
+ try:
63
+ dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
64
+ self._bus = dbus.SystemBus()
65
+ self._refresh()
66
+ self._subscribe()
67
+ self._available = True
68
+ except Exception as exc:
69
+ print(f"[bluetooth] D-Bus init failed: {exc}")
70
+
71
+ def _refresh(self):
72
+ if not self._bus:
73
+ return
74
+ try:
75
+ mgr = dbus.Interface(
76
+ self._bus.get_object(BLUEZ_SERVICE, "/"),
77
+ OBJ_MANAGER_IFACE,
78
+ )
79
+ objects = mgr.GetManagedObjects()
80
+ self.devices = {}
81
+ for path, ifaces in objects.items():
82
+ if DEVICE_IFACE not in ifaces:
83
+ continue
84
+ props = {str(k): v for k, v in ifaces[DEVICE_IFACE].items()}
85
+ dev = BluetoothDevice(str(path), props)
86
+ if BATTERY_IFACE in ifaces:
87
+ dev.battery = int(ifaces[BATTERY_IFACE].get("Percentage", 0))
88
+ self.devices[str(path)] = dev
89
+ except Exception as exc:
90
+ print(f"[bluetooth] Refresh failed: {exc}")
91
+
92
+ def _subscribe(self):
93
+ if not self._bus:
94
+ return
95
+ try:
96
+ self._bus.add_signal_receiver(
97
+ self._on_props_changed,
98
+ signal_name="PropertiesChanged",
99
+ dbus_interface=PROPERTIES_IFACE,
100
+ path_keyword="path",
101
+ )
102
+ self._bus.add_signal_receiver(
103
+ self._on_ifaces_added,
104
+ signal_name="InterfacesAdded",
105
+ dbus_interface=OBJ_MANAGER_IFACE,
106
+ bus_name=BLUEZ_SERVICE,
107
+ )
108
+ self._bus.add_signal_receiver(
109
+ self._on_ifaces_removed,
110
+ signal_name="InterfacesRemoved",
111
+ dbus_interface=OBJ_MANAGER_IFACE,
112
+ bus_name=BLUEZ_SERVICE,
113
+ )
114
+ except Exception as exc:
115
+ print(f"[bluetooth] Signal subscribe failed: {exc}")
116
+
117
+ def _on_props_changed(self, interface, changed, _invalidated=None, path=None):
118
+ if interface != DEVICE_IFACE:
119
+ return
120
+ if not path:
121
+ return
122
+ path = str(path)
123
+ if path not in self.devices:
124
+ return
125
+ dev = self.devices[path]
126
+ old_connected = dev.connected
127
+ dev.update({str(k): v for k, v in changed.items()})
128
+ if dev.connected != old_connected:
129
+ sig = "device-connected" if dev.connected else "device-disconnected"
130
+ GLib.idle_add(self.emit, sig, path)
131
+ GLib.idle_add(self.emit, "devices-changed")
132
+
133
+ def _on_ifaces_added(self, path, ifaces):
134
+ if DEVICE_IFACE not in ifaces:
135
+ return
136
+ props = {str(k): v for k, v in ifaces[DEVICE_IFACE].items()}
137
+ dev = BluetoothDevice(str(path), props)
138
+ if BATTERY_IFACE in ifaces:
139
+ dev.battery = int(ifaces[BATTERY_IFACE].get("Percentage", 0))
140
+ self.devices[str(path)] = dev
141
+ GLib.idle_add(self.emit, "devices-changed")
142
+
143
+ def _on_ifaces_removed(self, path, ifaces):
144
+ path = str(path)
145
+ if DEVICE_IFACE in ifaces and path in self.devices:
146
+ del self.devices[path]
147
+ GLib.idle_add(self.emit, "devices-changed")
148
+
149
+ @property
150
+ def available(self) -> bool:
151
+ return self._available
152
+
153
+ def get_all(self) -> list[BluetoothDevice]:
154
+ return sorted(self.devices.values(), key=lambda d: (not d.connected, d.name))
155
+
156
+ def get_nothing_devices(self) -> list[BluetoothDevice]:
157
+ return [d for d in self.get_all() if d.is_nothing]
158
+
159
+ def refresh(self):
160
+ self._refresh()
161
+ self.emit("devices-changed")
162
+
163
+ def connect_device(self, path: str, on_error=None):
164
+ if not self._bus:
165
+ return
166
+
167
+ def _err(e):
168
+ print(f"[BT] connect error: {e}")
169
+ if on_error:
170
+ GLib.idle_add(on_error)
171
+
172
+ try:
173
+ iface = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, path), DEVICE_IFACE)
174
+ iface.Connect(reply_handler=lambda: None, error_handler=_err)
175
+ except Exception as exc:
176
+ print(f"[bluetooth] connect {path}: {exc}")
177
+ if on_error:
178
+ GLib.idle_add(on_error)
179
+
180
+ def disconnect_device(self, path: str):
181
+ if not self._bus:
182
+ return
183
+ try:
184
+ iface = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, path), DEVICE_IFACE)
185
+ iface.Disconnect(
186
+ reply_handler=lambda: None,
187
+ error_handler=lambda e: print(f"[BT] disconnect error: {e}"),
188
+ )
189
+ except Exception as exc:
190
+ print(f"[bluetooth] disconnect {path}: {exc}")
191
+
192
+ def start_discovery(self):
193
+ if not self._bus:
194
+ return
195
+ try:
196
+ mgr = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, "/"), OBJ_MANAGER_IFACE)
197
+ for path, ifaces in mgr.GetManagedObjects().items():
198
+ if ADAPTER_IFACE in ifaces:
199
+ adapter = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, path), ADAPTER_IFACE)
200
+ adapter.StartDiscovery()
201
+ GLib.timeout_add_seconds(30, self._stop_discovery_on_path, str(path))
202
+ break
203
+ except Exception as exc:
204
+ print(f"[bluetooth] discovery start: {exc}")
205
+
206
+ def _stop_discovery_on_path(self, path: str):
207
+ try:
208
+ adapter = dbus.Interface(self._bus.get_object(BLUEZ_SERVICE, path), ADAPTER_IFACE)
209
+ adapter.StopDiscovery()
210
+ except Exception:
211
+ pass
212
+ return False
File without changes
@@ -0,0 +1,13 @@
1
+ [Desktop Entry]
2
+ Version=1.0
3
+ Type=Application
4
+ Name=Something X
5
+ GenericName=Bluetooth Device Manager
6
+ Comment=Manage Nothing and CMF Bluetooth devices
7
+ Exec=something-x
8
+ Icon=audio-headphones
9
+ Categories=Utility;GTK;HardwareSettings;
10
+ Keywords=bluetooth;nothing;ear;cmf;audio;headphones;earbuds;
11
+ Terminal=false
12
+ StartupNotify=true
13
+ StartupWMClass=com.something.x.omarchy