something-x-dev 1.9.0.dev29__tar.gz → 1.9.0.dev30__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.
Files changed (50) hide show
  1. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/PKG-INFO +1 -1
  2. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/ROADMAP.md +1 -1
  3. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/_version.py +2 -2
  4. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/pages/device.py +10 -3
  5. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/pages/theme.py +8 -16
  6. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/profiles.py +1 -1
  7. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/protocol.py +35 -3
  8. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/tray.py +1 -1
  9. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/PKG-INFO +1 -1
  10. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/conftest.py +5 -1
  11. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_notifications.py +94 -0
  12. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_profiles.py +2 -2
  13. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_protocol.py +1 -1
  14. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_theme.py +16 -14
  15. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/CODEOWNERS +0 -0
  16. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/workflows/ci.yml +0 -0
  17. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/workflows/release-dev.yml +0 -0
  18. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.github/workflows/release.yml +0 -0
  19. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/.gitignore +0 -0
  20. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/DEVICES.md +0 -0
  21. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/LICENSE +0 -0
  22. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/PKGBUILD +0 -0
  23. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/README.md +0 -0
  24. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/cliff.toml +0 -0
  25. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/docs/RELEASING.md +0 -0
  26. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/docs/docs.html +0 -0
  27. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/docs/index.html +0 -0
  28. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/flake.nix +0 -0
  29. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/__init__.py +0 -0
  30. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/application.py +0 -0
  31. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/bluetooth.py +0 -0
  32. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/data/__init__.py +0 -0
  33. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/data/com.something.x.omarchy.desktop +0 -0
  34. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/data/style.css +0 -0
  35. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/pages/__init__.py +0 -0
  36. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/pages/home.py +0 -0
  37. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/splash.py +0 -0
  38. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/theme.py +0 -0
  39. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/nothing_app/window.py +0 -0
  40. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/pyproject.toml +0 -0
  41. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/setup.cfg +0 -0
  42. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/SOURCES.txt +0 -0
  43. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/dependency_links.txt +0 -0
  44. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/entry_points.txt +0 -0
  45. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/requires.txt +0 -0
  46. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/something_x_dev.egg-info/top_level.txt +0 -0
  47. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/somethingx +0 -0
  48. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/__init__.py +0 -0
  49. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_bluetooth.py +0 -0
  50. {something_x_dev-1.9.0.dev29 → something_x_dev-1.9.0.dev30}/tests/test_crc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: something-x-dev
3
- Version: 1.9.0.dev29
3
+ Version: 1.9.0.dev30
4
4
  Summary: Something X device manager for Omarchy / Linux
5
5
  Author: Raphael
6
6
  License: MIT
@@ -30,7 +30,7 @@
30
30
  ### Features
31
31
  - [x] **Device nickname** — rename a paired device in the UI; stored in profiles
32
32
  - [x] **Profile import / export** — share `.json` profile files between machines or with other users
33
- - [ ] **Wear-detect MPRIS actions** — pause media when both buds are removed; resume when reinserted (opt-in)
33
+ - [x] **Wear-detect MPRIS actions** — pause media when both buds are removed; resume when reinserted (opt-in)
34
34
  - [x] **Notification preferences** — per-event toggles (battery low, connect, disconnect) in settings
35
35
  - [x] **Theming** — user-selectable accent color and light/dark mode toggle; theme stored in config
36
36
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '1.9.0.dev29'
22
- __version_tuple__ = version_tuple = (1, 9, 0, 'dev29')
21
+ __version__ = version = '1.9.0.dev30'
22
+ __version_tuple__ = version_tuple = (1, 9, 0, 'dev30')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -9,7 +9,7 @@ import gi
9
9
  gi.require_version("Gtk", "4.0")
10
10
  gi.require_version("Pango", "1.0")
11
11
  gi.require_version("PangoCairo", "1.0")
12
- from gi.repository import Gtk, GLib, PangoCairo, Gio
12
+ from gi.repository import Gtk, GLib, PangoCairo
13
13
 
14
14
  from ..bluetooth import BluetoothDevice, BluetoothManager
15
15
  from ..protocol import NothingDevice, ANCMode, EQ_PRESETS
@@ -477,12 +477,15 @@ class DevicePage(Gtk.Box):
477
477
  )
478
478
 
479
479
  self._auto_pause_switch = Gtk.Switch()
480
- self._auto_pause_switch.set_active(True)
480
+ self._auto_pause_switch.set_active(
481
+ profiles.get_notify_prefs(self._bt_device.address).get("wear_mpris", False)
482
+ )
481
483
  self._auto_pause_switch.set_valign(Gtk.Align.CENTER)
484
+ self._auto_pause_switch.connect("state-set", self._on_auto_pause_toggled)
482
485
  settings_group.append(
483
486
  _settings_row(
484
487
  "Auto-Pause",
485
- "Pause media on removal",
488
+ "Pause/resume media with wear detection",
486
489
  self._auto_pause_switch,
487
490
  )
488
491
  )
@@ -712,6 +715,10 @@ class DevicePage(Gtk.Box):
712
715
  if not entry.has_focus():
713
716
  self._save_nickname()
714
717
 
718
+ def _on_auto_pause_toggled(self, _switch, state: bool):
719
+ profiles.set_notify_prefs(self._bt_device.address, {"wear_mpris": state})
720
+ return False
721
+
715
722
  def _on_notif_battery_toggled(self, _switch, state: bool):
716
723
  profiles.set_notify_prefs(self._bt_device.address, {"battery_low": state})
717
724
  return False
@@ -1,10 +1,10 @@
1
1
  import math
2
- from typing import Callable
2
+ from collections.abc import Callable
3
3
 
4
4
  import gi
5
5
 
6
6
  gi.require_version("Gtk", "4.0")
7
- from gi.repository import Gdk, GLib, Gtk
7
+ from gi.repository import Gdk, Gtk
8
8
 
9
9
  from ..theme import ACCENT_PRESETS, BG_PRESETS, FONT_PRESETS, TEXTURES, Theme, hex_to_rgb
10
10
 
@@ -345,11 +345,8 @@ class ThemePage(Gtk.Box):
345
345
 
346
346
  def _on_accent_color_set(self, btn: Gtk.ColorButton):
347
347
  rgba = btn.get_rgba()
348
- self._theme.accent = "#{:02x}{:02x}{:02x}".format(
349
- int(rgba.red * 255),
350
- int(rgba.green * 255),
351
- int(rgba.blue * 255),
352
- )
348
+ r, g, b = int(rgba.red * 255), int(rgba.green * 255), int(rgba.blue * 255)
349
+ self._theme.accent = f"#{r:02x}{g:02x}{b:02x}"
353
350
  self._update_accent_swatches()
354
351
  self._emit()
355
352
 
@@ -368,11 +365,8 @@ class ThemePage(Gtk.Box):
368
365
 
369
366
  def _on_bg_color_set(self, btn: Gtk.ColorButton):
370
367
  rgba = btn.get_rgba()
371
- self._theme.bg_color = "#{:02x}{:02x}{:02x}".format(
372
- int(rgba.red * 255),
373
- int(rgba.green * 255),
374
- int(rgba.blue * 255),
375
- )
368
+ r, g, b = int(rgba.red * 255), int(rgba.green * 255), int(rgba.blue * 255)
369
+ self._theme.bg_color = f"#{r:02x}{g:02x}{b:02x}"
376
370
  self._update_bg_swatches()
377
371
  self._emit()
378
372
 
@@ -419,8 +413,6 @@ class ThemePage(Gtk.Box):
419
413
  self._emit()
420
414
 
421
415
  def _on_reset(self, _btn):
422
- import dataclasses
423
-
424
416
  self._theme = Theme()
425
417
  self._reload_controls()
426
418
  self._emit()
@@ -428,11 +420,11 @@ class ThemePage(Gtk.Box):
428
420
  # ── Internal helpers ──────────────────────────────────────────────────────
429
421
 
430
422
  def _update_accent_swatches(self):
431
- for sw, (color, _) in zip(self._accent_swatches, ACCENT_PRESETS):
423
+ for sw, (color, _) in zip(self._accent_swatches, ACCENT_PRESETS, strict=False):
432
424
  sw.set_active(color.lower() == self._theme.accent.lower())
433
425
 
434
426
  def _update_bg_swatches(self):
435
- for sw, (color, _) in zip(self._bg_swatches, BG_PRESETS):
427
+ for sw, (color, _) in zip(self._bg_swatches, BG_PRESETS, strict=False):
436
428
  sw.set_active(color.lower() == self._theme.bg_color.lower())
437
429
 
438
430
  def _reload_controls(self):
@@ -5,7 +5,7 @@ _DIR = os.path.expanduser("~/.config/something-x")
5
5
  _PROFILES_FILE = os.path.join(_DIR, "profiles.json")
6
6
  _LAST_DEV_FILE = os.path.join(_DIR, "last_device")
7
7
 
8
- _NOTIFY_DEFAULTS: dict = {"battery_low": True, "connect": True, "disconnect": True}
8
+ _NOTIFY_DEFAULTS: dict = {"battery_low": True, "connect": True, "disconnect": True, "wear_mpris": False}
9
9
  _ALLOWED_IMPORT_KEYS = frozenset({"anc", "eq", "nickname", "notify"})
10
10
 
11
11
 
@@ -152,6 +152,8 @@ class NothingDevice(GObject.Object):
152
152
  self._thread: threading.Thread | None = None
153
153
  self._low_bat_notified: dict[str, set[int]] = {}
154
154
  self._low_bat_seen: set[str] = set()
155
+ self._wear_both_removed: bool = False
156
+ self._wear_paused: bool = False
155
157
 
156
158
  # ── Public API ────────────────────────────────────────────────────────────
157
159
 
@@ -545,9 +547,8 @@ class NothingDevice(GObject.Object):
545
547
  else:
546
548
  modes = frozenset([ANCMode.OFF, ANCMode.TRANSPARENCY])
547
549
  self.state.supported_anc_modes = modes
548
- _log(
549
- f"[protocol] supported ANC modes detected: {[ANCMode.LABELS.get(m, m) for m in sorted(modes)]}"
550
- )
550
+ labels = [ANCMode.LABELS.get(m, m) for m in sorted(modes)]
551
+ _log(f"[protocol] supported ANC modes detected: {labels}")
551
552
  changed = True
552
553
 
553
554
  return changed
@@ -579,6 +580,7 @@ class NothingDevice(GObject.Object):
579
580
  changed = True
580
581
  if changed:
581
582
  _log(f"[protocol] wearing L={self.state.left_wearing} R={self.state.right_wearing}")
583
+ self._check_wear_mpris()
582
584
  return changed
583
585
 
584
586
  # ── Legacy 0x03 frame handling (status-only fallback) ────────────────────
@@ -681,6 +683,36 @@ class NothingDevice(GObject.Object):
681
683
  ).start()
682
684
  break
683
685
 
686
+ def _check_wear_mpris(self):
687
+ from . import profiles
688
+
689
+ if not profiles.get_notify_prefs(self.address).get("wear_mpris", False):
690
+ self._wear_both_removed = False
691
+ self._wear_paused = False
692
+ return
693
+
694
+ both_removed = not self.state.left_wearing and not self.state.right_wearing
695
+
696
+ if both_removed and not self._wear_both_removed:
697
+ self._wear_both_removed = True
698
+ self._wear_paused = True
699
+ threading.Thread(
700
+ target=subprocess.run,
701
+ args=(["playerctl", "pause"],),
702
+ kwargs={"capture_output": True},
703
+ daemon=True,
704
+ ).start()
705
+ elif not both_removed and self._wear_both_removed:
706
+ self._wear_both_removed = False
707
+ if self._wear_paused:
708
+ self._wear_paused = False
709
+ threading.Thread(
710
+ target=subprocess.run,
711
+ args=(["playerctl", "play"],),
712
+ kwargs={"capture_output": True},
713
+ daemon=True,
714
+ ).start()
715
+
684
716
  def _poll_earphone_status(self):
685
717
  # The firmware only computes a fresh per-bud snapshot when asked; the
686
718
  # pushed EVT frames carry stale placeholder entries for the bud that
@@ -4,7 +4,7 @@ import dbus.service
4
4
  import dbus.mainloop.glib
5
5
  from gi.repository import GLib, GObject
6
6
 
7
- from .bluetooth import BluetoothManager, BluetoothDevice, device_icon_name
7
+ from .bluetooth import BluetoothManager, device_icon_name
8
8
 
9
9
  _ITEM_IFACE = "org.kde.StatusNotifierItem"
10
10
  _WATCHER_IFACE = "org.kde.StatusNotifierWatcher"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: something-x-dev
3
- Version: 1.9.0.dev29
3
+ Version: 1.9.0.dev30
4
4
  Summary: Something X device manager for Omarchy / Linux
5
5
  Author: Raphael
6
6
  License: MIT
@@ -22,7 +22,11 @@ def mock_profiles(monkeypatch):
22
22
  monkeypatch.setattr(profiles, "save", MagicMock())
23
23
  monkeypatch.setattr(profiles, "load", MagicMock(return_value={}))
24
24
  monkeypatch.setattr(profiles, "set_last_device", MagicMock())
25
- monkeypatch.setattr(profiles, "get_notify_prefs", MagicMock(return_value={"battery_low": True, "connect": True, "disconnect": True}))
25
+ monkeypatch.setattr(
26
+ profiles,
27
+ "get_notify_prefs",
28
+ MagicMock(return_value={"battery_low": True, "connect": True, "disconnect": True}),
29
+ )
26
30
  monkeypatch.setattr(profiles, "get_nickname", MagicMock(return_value=None))
27
31
 
28
32
 
@@ -131,3 +131,97 @@ def test_battery_low_sent_with_default_prefs():
131
131
  fired.wait(timeout=2)
132
132
 
133
133
  assert calls
134
+
135
+
136
+ # ── wear_mpris pref ───────────────────────────────────────────────────────────
137
+
138
+
139
+ def _set_wearing(dev: NothingDevice, left: bool, right: bool):
140
+ """Directly update wearing state and call the MPRIS check."""
141
+ dev.state.left_wearing = left
142
+ dev.state.right_wearing = right
143
+ dev._check_wear_mpris()
144
+
145
+
146
+ def test_wear_mpris_pauses_when_both_removed(tmp_path, monkeypatch):
147
+ monkeypatch.setattr(profiles, "_PROFILES_FILE", str(tmp_path / "profiles.json"))
148
+ profiles.set_notify_prefs(_ADDR, {"wear_mpris": True})
149
+
150
+ calls = []
151
+
152
+ def fake_run(cmd, **kw):
153
+ calls.append(cmd)
154
+
155
+ dev = _make_device()
156
+ with patch("nothing_app.protocol.subprocess.run", fake_run):
157
+ _set_wearing(dev, True, True) # both wearing
158
+ _set_wearing(dev, False, False) # both removed → pause
159
+ import time
160
+
161
+ time.sleep(0.05)
162
+
163
+ assert any("pause" in c for c in calls)
164
+
165
+
166
+ def test_wear_mpris_resumes_when_reinserted(tmp_path, monkeypatch):
167
+ monkeypatch.setattr(profiles, "_PROFILES_FILE", str(tmp_path / "profiles.json"))
168
+ profiles.set_notify_prefs(_ADDR, {"wear_mpris": True})
169
+
170
+ calls = []
171
+
172
+ def fake_run(cmd, **kw):
173
+ calls.append(cmd)
174
+
175
+ dev = _make_device()
176
+ with patch("nothing_app.protocol.subprocess.run", fake_run):
177
+ _set_wearing(dev, True, True)
178
+ _set_wearing(dev, False, False) # pause
179
+ _set_wearing(dev, True, False) # one bud back in → play
180
+ import time
181
+
182
+ time.sleep(0.05)
183
+
184
+ cmds = [c[-1] for c in calls]
185
+ assert "pause" in cmds
186
+ assert "play" in cmds
187
+
188
+
189
+ def test_wear_mpris_disabled_by_default():
190
+ calls = []
191
+
192
+ def fake_run(cmd, **kw):
193
+ calls.append(cmd)
194
+
195
+ dev = _make_device()
196
+ with patch("nothing_app.protocol.subprocess.run", fake_run):
197
+ _set_wearing(dev, True, True)
198
+ _set_wearing(dev, False, False)
199
+ import time
200
+
201
+ time.sleep(0.05)
202
+
203
+ assert not calls, "wear_mpris is opt-in — should not fire with default prefs"
204
+
205
+
206
+ def test_wear_mpris_no_double_play(tmp_path, monkeypatch):
207
+ """Wearing state bouncing while already in-ear should not trigger repeated plays."""
208
+ monkeypatch.setattr(profiles, "_PROFILES_FILE", str(tmp_path / "profiles.json"))
209
+ profiles.set_notify_prefs(_ADDR, {"wear_mpris": True})
210
+
211
+ calls = []
212
+
213
+ def fake_run(cmd, **kw):
214
+ calls.append(cmd)
215
+
216
+ dev = _make_device()
217
+ with patch("nothing_app.protocol.subprocess.run", fake_run):
218
+ _set_wearing(dev, True, True)
219
+ _set_wearing(dev, False, False) # pause
220
+ _set_wearing(dev, True, False) # play
221
+ _set_wearing(dev, True, True) # still wearing — no extra play
222
+ import time
223
+
224
+ time.sleep(0.05)
225
+
226
+ play_calls = [c for c in calls if "play" in c]
227
+ assert len(play_calls) == 1
@@ -142,9 +142,9 @@ def test_nickname_does_not_affect_other_device():
142
142
  # ── notify_prefs ──────────────────────────────────────────────────────────────
143
143
 
144
144
 
145
- def test_get_notify_prefs_defaults_all_true():
145
+ def test_get_notify_prefs_defaults():
146
146
  prefs = profiles.get_notify_prefs("AA:BB:CC:DD:EE:FF")
147
- assert prefs == {"battery_low": True, "connect": True, "disconnect": True}
147
+ assert prefs == {"battery_low": True, "connect": True, "disconnect": True, "wear_mpris": False}
148
148
 
149
149
 
150
150
  def test_set_notify_prefs_roundtrip():
@@ -90,7 +90,7 @@ def test_parse_battery_updates_only_present_types():
90
90
  payload = bytes([0x01, 0x03, 60])
91
91
  dev._parse_battery(payload)
92
92
  assert dev.state.right_battery == 60
93
- assert dev.state.left_battery == -1 # untouched default
93
+ assert dev.state.left_battery == -1 # untouched default
94
94
  assert dev.state.case_battery == -1
95
95
 
96
96
 
@@ -66,7 +66,9 @@ def test_load_bad_json_returns_defaults(tmp_path):
66
66
 
67
67
 
68
68
  def test_save_and_load_roundtrip():
69
- t = Theme(accent="#3b82f6", bg_color="#1a1b26", window_opacity=0.8, card_opacity=0.5, blur=6, texture="dots")
69
+ t = Theme(
70
+ accent="#3b82f6", bg_color="#1a1b26", window_opacity=0.8, card_opacity=0.5, blur=6, texture="dots"
71
+ )
70
72
  save(t)
71
73
  t2 = load()
72
74
  assert t2 == t
@@ -319,7 +321,7 @@ def test_generate_css_bg_color_default_is_black():
319
321
 
320
322
  def test_generate_css_custom_bg_used_in_app_background_gradient():
321
323
  css = generate_css(Theme(bg_color="#1e1e2e"))
322
- bg_block = css[css.index(".app-background {"):][:300]
324
+ bg_block = css[css.index(".app-background {") :][:300]
323
325
  assert "1e1e2e" in bg_block or "background" in bg_block
324
326
 
325
327
 
@@ -344,29 +346,29 @@ def test_generate_css_blur_emits_filter():
344
346
 
345
347
  def test_generate_css_blur_appears_in_app_background():
346
348
  css = generate_css(Theme(blur=4))
347
- bg_section = css[css.index(".app-background {"):]
348
- first_block = bg_section[:bg_section.index("}") + 1]
349
+ bg_section = css[css.index(".app-background {") :]
350
+ first_block = bg_section[: bg_section.index("}") + 1]
349
351
  assert "blur(4px)" in first_block
350
352
 
351
353
 
352
354
  def test_generate_css_blur_not_in_nothing_page():
353
355
  css = generate_css(Theme(blur=6))
354
- page_section = css[css.index(".nothing-page {"):]
355
- first_block = page_section[:page_section.index("}") + 1]
356
+ page_section = css[css.index(".nothing-page {") :]
357
+ first_block = page_section[: page_section.index("}") + 1]
356
358
  assert "blur(" not in first_block
357
359
 
358
360
 
359
361
  def test_generate_css_blur_not_in_device_card():
360
362
  css = generate_css(Theme(blur=6))
361
- card_section = css[css.index(".device-card {"):]
362
- first_block = card_section[:card_section.index("}") + 1]
363
+ card_section = css[css.index(".device-card {") :]
364
+ first_block = card_section[: card_section.index("}") + 1]
363
365
  assert "blur(" not in first_block
364
366
 
365
367
 
366
368
  def test_generate_css_blur_not_in_settings_group():
367
369
  css = generate_css(Theme(blur=6))
368
- group_section = css[css.index(".settings-group {"):]
369
- first_block = group_section[:group_section.index("}") + 1]
370
+ group_section = css[css.index(".settings-group {") :]
371
+ first_block = group_section[: group_section.index("}") + 1]
370
372
  assert "blur(" not in first_block
371
373
 
372
374
 
@@ -392,25 +394,25 @@ def test_generate_css_card_opacity_scales_alpha():
392
394
 
393
395
  def test_generate_css_texture_none_sets_no_image():
394
396
  css = generate_css(Theme(texture="none"))
395
- bg_block = css[css.index(".app-background {"):][:300]
397
+ bg_block = css[css.index(".app-background {") :][:300]
396
398
  assert "background-image: none" in bg_block
397
399
 
398
400
 
399
401
  def test_generate_css_texture_dots():
400
402
  css = generate_css(Theme(texture="dots"))
401
- bg_block = css[css.index(".app-background {"):][:400]
403
+ bg_block = css[css.index(".app-background {") :][:400]
402
404
  assert "radial-gradient" in bg_block
403
405
 
404
406
 
405
407
  def test_generate_css_texture_scanlines():
406
408
  css = generate_css(Theme(texture="scanlines"))
407
- bg_block = css[css.index(".app-background {"):][:500]
409
+ bg_block = css[css.index(".app-background {") :][:500]
408
410
  assert "repeating-linear-gradient" in bg_block
409
411
 
410
412
 
411
413
  def test_generate_css_texture_noise():
412
414
  css = generate_css(Theme(texture="noise"))
413
- bg_block = css[css.index(".app-background {"):][:700]
415
+ bg_block = css[css.index(".app-background {") :][:700]
414
416
  assert "data:image/svg+xml;base64," in bg_block
415
417
 
416
418