trackerkeeper 0.1.0__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.
- trackerkeeper/__init__.py +55 -0
- trackerkeeper/__main__.py +3 -0
- trackerkeeper/_version.py +24 -0
- trackerkeeper/app.py +445 -0
- trackerkeeper/assets/__init__.py +2 -0
- trackerkeeper/assets/trackerkeeper.svg +12 -0
- trackerkeeper/async_io.py +299 -0
- trackerkeeper/autostart/__init__.py +52 -0
- trackerkeeper/autostart/_linux.py +129 -0
- trackerkeeper/autostart/_macos.py +163 -0
- trackerkeeper/autostart/_msix.py +181 -0
- trackerkeeper/autostart/_unsupported.py +22 -0
- trackerkeeper/autostart/_windows.py +96 -0
- trackerkeeper/bake.py +217 -0
- trackerkeeper/blur/__init__.py +212 -0
- trackerkeeper/blur/_dwm.py +348 -0
- trackerkeeper/blur/_faux_frost.py +143 -0
- trackerkeeper/blur/_kwin.py +478 -0
- trackerkeeper/blur/_macos.py +356 -0
- trackerkeeper/blur/_unsupported.py +39 -0
- trackerkeeper/boot_timing.py +75 -0
- trackerkeeper/breadboard.py +1554 -0
- trackerkeeper/bus.py +110 -0
- trackerkeeper/catalog.py +176 -0
- trackerkeeper/color_tokens.py +697 -0
- trackerkeeper/credentials.py +471 -0
- trackerkeeper/custom_tooltip.py +254 -0
- trackerkeeper/dashboard.py +704 -0
- trackerkeeper/deliver.py +442 -0
- trackerkeeper/design_tokens.py +415 -0
- trackerkeeper/diagnostics.py +168 -0
- trackerkeeper/drag_repaint/__init__.py +68 -0
- trackerkeeper/drag_repaint/_kwin.py +192 -0
- trackerkeeper/drag_repaint/_unsupported.py +41 -0
- trackerkeeper/drag_repaint/effect/dragrepaint/contents/code/main.js +86 -0
- trackerkeeper/drag_repaint/effect/dragrepaint/metadata.json +17 -0
- trackerkeeper/frosted_dialog.py +377 -0
- trackerkeeper/i18n/__init__.py +101 -0
- trackerkeeper/i18n/fmt.py +137 -0
- trackerkeeper/icon_button.py +137 -0
- trackerkeeper/icons.py +532 -0
- trackerkeeper/identity.py +135 -0
- trackerkeeper/item_dialog.py +185 -0
- trackerkeeper/kde_titlebar.py +196 -0
- trackerkeeper/keyboard_focus.py +353 -0
- trackerkeeper/log.py +112 -0
- trackerkeeper/macos_menubar.py +282 -0
- trackerkeeper/macos_window.py +172 -0
- trackerkeeper/metadata.py +121 -0
- trackerkeeper/noborder/__init__.py +61 -0
- trackerkeeper/noborder/_kwin.py +247 -0
- trackerkeeper/noborder/_unsupported.py +42 -0
- trackerkeeper/notifications/__init__.py +84 -0
- trackerkeeper/notifications/_linux.py +68 -0
- trackerkeeper/notifications/_macos.py +159 -0
- trackerkeeper/notifications/_unsupported.py +22 -0
- trackerkeeper/notifications/_windows.py +115 -0
- trackerkeeper/platform_compat.py +149 -0
- trackerkeeper/power/__init__.py +84 -0
- trackerkeeper/power/_linux.py +79 -0
- trackerkeeper/power/_unsupported.py +18 -0
- trackerkeeper/power/_windows.py +52 -0
- trackerkeeper/rig.py +338 -0
- trackerkeeper/scaffold.py +310 -0
- trackerkeeper/selector.py +529 -0
- trackerkeeper/settings.py +226 -0
- trackerkeeper/settings_dialog.py +307 -0
- trackerkeeper/settings_migration.py +101 -0
- trackerkeeper/single_instance.py +330 -0
- trackerkeeper/smooth_scroll.py +155 -0
- trackerkeeper/sources.py +318 -0
- trackerkeeper/system_accent.py +363 -0
- trackerkeeper/terminal.py +423 -0
- trackerkeeper/test_bridge.py +514 -0
- trackerkeeper/theme.py +725 -0
- trackerkeeper/top_bar.py +154 -0
- trackerkeeper/tray.py +184 -0
- trackerkeeper/ui_helpers.py +2294 -0
- trackerkeeper/update_chip.py +119 -0
- trackerkeeper/updates.py +216 -0
- trackerkeeper/win_frameless.py +364 -0
- trackerkeeper/window.py +609 -0
- trackerkeeper/windows_shortcut.py +355 -0
- trackerkeeper-0.1.0.dist-info/METADATA +144 -0
- trackerkeeper-0.1.0.dist-info/RECORD +89 -0
- trackerkeeper-0.1.0.dist-info/WHEEL +5 -0
- trackerkeeper-0.1.0.dist-info/entry_points.txt +8 -0
- trackerkeeper-0.1.0.dist-info/licenses/LICENSE +338 -0
- trackerkeeper-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Window "blur behind" — the frosted-glass effect for translucent
|
|
2
|
+
surfaces. This is what visually separates Frosted mode (blurred glass)
|
|
3
|
+
from Transparent mode (clear glass).
|
|
4
|
+
|
|
5
|
+
Public API:
|
|
6
|
+
is_supported() -> bool # backend can *request* blur
|
|
7
|
+
apply(widget, enabled) -> bool # enable/disable blur behind widget
|
|
8
|
+
status() -> BlurStatus # is a real backdrop VERIFIED behind us?
|
|
9
|
+
|
|
10
|
+
The status() call is the one that fixes the "frosted renders see-through"
|
|
11
|
+
class of bug: `apply()` is fire-and-forget (KWin gives no success signal),
|
|
12
|
+
so a frosted body painted at ~67% opacity goes transparent-broken wherever
|
|
13
|
+
blur silently no-ops (Blur effect off, missing plugin, no compositing, a
|
|
14
|
+
non-KDE desktop, Windows < 11). `status()` probes whether a real backdrop
|
|
15
|
+
is actually present so the theme layer can pick the body alpha — full glass
|
|
16
|
+
when blur landed, a near-opaque "frosted panel" fallback when it didn't.
|
|
17
|
+
See trackerkeeper/theme.body_color_for() and docs/research/portable_blur.md.
|
|
18
|
+
|
|
19
|
+
Backend (Linux): KDE's KWindowSystem — `KWindowEffects::enableBlurBehind`
|
|
20
|
+
to request blur and `KWindowEffects::isEffectAvailable(BlurBehind)` to
|
|
21
|
+
verify it, both via ctypes (no PySide6 binding exists). KWindowSystem
|
|
22
|
+
speaks `ext-background-effect-v1` where the compositor offers it and falls
|
|
23
|
+
back to the legacy `org_kde_kwin_blur` — so this covers KWin, and also niri
|
|
24
|
+
/ COSMIC where KWindowSystem is installed.
|
|
25
|
+
|
|
26
|
+
Everywhere else (Windows, macOS, or a Linux box without KWindowSystem, or a
|
|
27
|
+
compositor with no blur protocol — Hyprland/Wayfire/sway/GNOME): the
|
|
28
|
+
backend reports UNSUPPORTED and the window paints a near-opaque body. On
|
|
29
|
+
compositors that blur via user config rather than a protocol (Hyprland,
|
|
30
|
+
Wayfire, SwayFX), the user can target trackerkeeper in their own window rules:
|
|
31
|
+
our Wayland app_id is the stable string "trackerkeeper" (set via
|
|
32
|
+
setDesktopFileName).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import enum
|
|
38
|
+
import os
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class BlurStatus(enum.Enum):
|
|
42
|
+
"""Whether a real compositor/OS backdrop is verified to sit behind the
|
|
43
|
+
window — the signal the theme layer maps to a body opacity.
|
|
44
|
+
|
|
45
|
+
ACTIVE — blur issued AND positive evidence it landed;
|
|
46
|
+
frosted surfaces ride it at full glass alpha.
|
|
47
|
+
REQUESTED_UNVERIFIABLE — blur was issued but we can't confirm it took
|
|
48
|
+
(no success feedback, or a known-flaky path
|
|
49
|
+
like KDE X11); paint a conservative near-opaque
|
|
50
|
+
body so we never gamble on a see-through window.
|
|
51
|
+
UNSUPPORTED — the backend can't request blur here at all;
|
|
52
|
+
paint the near-opaque fallback body.
|
|
53
|
+
DISABLED — the active theme doesn't ask for blur (Solid /
|
|
54
|
+
Transparent); not an error. Bodies keep their
|
|
55
|
+
own opaque/translucent alpha unchanged.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
ACTIVE = "active"
|
|
59
|
+
REQUESTED_UNVERIFIABLE = "unverifiable"
|
|
60
|
+
UNSUPPORTED = "unsupported"
|
|
61
|
+
DISABLED = "disabled"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
from trackerkeeper.platform_compat import IS_LINUX, IS_MACOS, IS_WINDOWS
|
|
65
|
+
|
|
66
|
+
if IS_LINUX:
|
|
67
|
+
from trackerkeeper.blur import _kwin as _backend
|
|
68
|
+
elif IS_WINDOWS: # pragma: no cover - exercised on Windows
|
|
69
|
+
from trackerkeeper.blur import _dwm as _backend
|
|
70
|
+
elif IS_MACOS: # pragma: no cover - exercised on macOS
|
|
71
|
+
from trackerkeeper.blur import _macos as _backend
|
|
72
|
+
else: # pragma: no cover
|
|
73
|
+
from trackerkeeper.blur import _unsupported as _backend
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# Process-wide cache. Blur availability is a per-session compositor/OS fact,
|
|
77
|
+
# not a per-window one (KWindowEffects::isEffectAvailable takes no window),
|
|
78
|
+
# so we probe once and reuse. force=True re-probes after a compositing /
|
|
79
|
+
# Blur-effect toggle.
|
|
80
|
+
_status_cache: BlurStatus | None = None
|
|
81
|
+
|
|
82
|
+
# Debug override: TRACKERKEEPER_BLUR_FORCE=active|unverifiable|unsupported pins the
|
|
83
|
+
# reported status, bypassing the probe. Lets you eyeball the near-opaque
|
|
84
|
+
# fallback body (TRACKERKEEPER_BLUR_FORCE=unsupported) without disabling the
|
|
85
|
+
# compositor's Blur effect, and the reverse on a box where blur is off.
|
|
86
|
+
# Same JT_* debug-switch family as TRACKERKEEPER_OPAQUE.
|
|
87
|
+
_FORCE = os.environ.get("TRACKERKEEPER_BLUR_FORCE", "").strip().lower()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def opaque_mode_active() -> bool:
|
|
91
|
+
"""Dev diagnostic: fully-opaque chrome — no translucency, no blur — via the
|
|
92
|
+
``TRACKERKEEPER_OPAQUE=1`` env switch. When on, :func:`status` reports UNSUPPORTED (so
|
|
93
|
+
frosted bodies + popups use their near-opaque fallback) and :func:`apply`
|
|
94
|
+
skips requesting compositor blur.
|
|
95
|
+
|
|
96
|
+
Env-only on purpose — there is NO user-facing setting. A frosted theme that
|
|
97
|
+
can't get real blur already falls back to a near-opaque body automatically
|
|
98
|
+
(status() → UNSUPPORTED/REQUESTED_UNVERIFIABLE), which covers the real user
|
|
99
|
+
need; the old Settings toggle additionally dropped WA_TranslucentBackground
|
|
100
|
+
and broke the window's rounded corners, so it was removed. TRACKERKEEPER_OPAQUE stays
|
|
101
|
+
for the screencast / streaming-flicker repro it was born for. Never raises."""
|
|
102
|
+
return os.environ.get("TRACKERKEEPER_OPAQUE") == "1"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def is_supported() -> bool:
|
|
106
|
+
"""True if the backend can *request* blur (KWindowSystem present /
|
|
107
|
+
Windows 11 build). A True here doesn't guarantee the compositor will
|
|
108
|
+
actually blur — that's what status() checks."""
|
|
109
|
+
return _backend.is_supported()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def apply(
|
|
113
|
+
widget,
|
|
114
|
+
enabled: bool,
|
|
115
|
+
corner_radius: int = 0,
|
|
116
|
+
dark: bool | None = None,
|
|
117
|
+
elevated: bool = False,
|
|
118
|
+
) -> bool:
|
|
119
|
+
"""Enable (``enabled=True``) or remove (``False``) compositor blur
|
|
120
|
+
behind ``widget``'s window. ``widget`` is a QWidget; its QWindow must
|
|
121
|
+
already exist (call after ``show()``).
|
|
122
|
+
|
|
123
|
+
``corner_radius``: when > 0 the blur region is shaped to a rounded
|
|
124
|
+
rectangle of that radius matching the widget's current size — pass this
|
|
125
|
+
for frameless rounded windows (mini player, settings dialog) so the blur
|
|
126
|
+
doesn't bleed into the transparent corners. ``0`` blurs the whole window
|
|
127
|
+
rectangle — correct for server-side-decorated windows.
|
|
128
|
+
|
|
129
|
+
``dark``: dark vs light variant for backends whose backdrop has one
|
|
130
|
+
(Windows Mica's immersive dark/light tint). ``None`` (the default)
|
|
131
|
+
resolves it from the active theme, so every call site gets the right
|
|
132
|
+
variant for free; the KWin / macOS / unsupported backends ignore it.
|
|
133
|
+
|
|
134
|
+
``elevated``: True for elevated popups (menus / dropdowns / volume
|
|
135
|
+
popups / tooltips) that paint their own status-aware QSS frost fill.
|
|
136
|
+
Backends whose blur material carries a built-in tint (Windows
|
|
137
|
+
Acrylic) drop it to near-zero so the popup isn't double-veiled —
|
|
138
|
+
KWin's blur is untinted, so on Linux this is a no-op and the QSS
|
|
139
|
+
fill stays the single tint source everywhere.
|
|
140
|
+
|
|
141
|
+
Returns True if the request was issued, False on any unsupported /
|
|
142
|
+
not-yet-shown case. The return is best-effort ("issued", not "blurred")
|
|
143
|
+
— use status() to learn whether blur actually landed. Never raises."""
|
|
144
|
+
if enabled and opaque_mode_active():
|
|
145
|
+
# User forced opaque chrome — never request blur (and remove any
|
|
146
|
+
# already-applied blur on this widget).
|
|
147
|
+
enabled = False
|
|
148
|
+
# "Never raises" is part of the contract (blur is progressive enhancement)
|
|
149
|
+
# — theme resolution + the backend call are best-effort, so swallow errors.
|
|
150
|
+
try:
|
|
151
|
+
if dark is None:
|
|
152
|
+
from trackerkeeper.theme import get_active_theme
|
|
153
|
+
|
|
154
|
+
dark = get_active_theme().dark
|
|
155
|
+
# On macOS this dispatches to the live NSVisualEffectView vibrancy
|
|
156
|
+
# backend (trackerkeeper/blur/_macos.py): it installs a "behind window" effect
|
|
157
|
+
# view as a SIBLING ordered strictly below Qt's content view, so the
|
|
158
|
+
# system frost shows through the translucent body — the mac-native
|
|
159
|
+
# equivalent of KWin's blur-behind. corner_radius/dark/elevated all
|
|
160
|
+
# forward through, same as the KWin/DWM backends.
|
|
161
|
+
return _backend.apply(widget, enabled, corner_radius, dark, elevated)
|
|
162
|
+
except Exception:
|
|
163
|
+
return False
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def status(*, force: bool = False) -> BlurStatus:
|
|
167
|
+
"""Whether a real backdrop is VERIFIED behind our windows on this
|
|
168
|
+
machine/session — the value the theme layer uses to choose body opacity.
|
|
169
|
+
|
|
170
|
+
Computed once and cached (the answer is a per-session compositor/OS
|
|
171
|
+
fact). Pass ``force=True`` to re-probe, e.g. after the window is mapped
|
|
172
|
+
or a compositing/Blur-effect toggle. Returns ACTIVE /
|
|
173
|
+
REQUESTED_UNVERIFIABLE / UNSUPPORTED — never DISABLED (that's the theme's
|
|
174
|
+
call, not the machine's). Never raises; any failure resolves to the
|
|
175
|
+
conservative REQUESTED_UNVERIFIABLE so a frosted body stays near-opaque
|
|
176
|
+
rather than risking see-through."""
|
|
177
|
+
global _status_cache
|
|
178
|
+
if opaque_mode_active():
|
|
179
|
+
# User forced opaque chrome — report no backdrop so frosted bodies +
|
|
180
|
+
# popups fall back to their near-opaque alpha.
|
|
181
|
+
return BlurStatus.UNSUPPORTED
|
|
182
|
+
if _FORCE:
|
|
183
|
+
try:
|
|
184
|
+
forced = BlurStatus(_FORCE)
|
|
185
|
+
except ValueError:
|
|
186
|
+
forced = None # unrecognised value → ignore, probe normally
|
|
187
|
+
# status() reports a machine capability and never returns DISABLED
|
|
188
|
+
# (that's the theme's call) — so TRACKERKEEPER_BLUR_FORCE=disabled is ignored.
|
|
189
|
+
if forced is not None and forced is not BlurStatus.DISABLED:
|
|
190
|
+
return forced
|
|
191
|
+
if _status_cache is not None and not force:
|
|
192
|
+
return _status_cache
|
|
193
|
+
try:
|
|
194
|
+
_status_cache = _backend.probe()
|
|
195
|
+
except Exception:
|
|
196
|
+
_status_cache = BlurStatus.REQUESTED_UNVERIFIABLE
|
|
197
|
+
return _status_cache
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def reason() -> str:
|
|
201
|
+
"""A short, human-readable explanation of the current blur status — for
|
|
202
|
+
the boot log and the Settings hint when a frosted theme can't get real
|
|
203
|
+
blur (e.g. "GNOME has no app-controllable window blur", "KWin's Blur
|
|
204
|
+
effect is off", "Windows 10 has no Mica backdrop"). Reads status()
|
|
205
|
+
(cached) + the environment via the active backend. Never raises."""
|
|
206
|
+
st = status()
|
|
207
|
+
try:
|
|
208
|
+
return _backend.reason(st)
|
|
209
|
+
except Exception:
|
|
210
|
+
if st is BlurStatus.ACTIVE:
|
|
211
|
+
return "compositor blur active"
|
|
212
|
+
return "compositor blur unavailable — using a near-opaque body"
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Windows blur backend for the Frosted theme.
|
|
2
|
+
|
|
3
|
+
The DEFAULT is real **Acrylic** blur-behind — live frosted glass — driven by
|
|
4
|
+
the legacy ``SetWindowCompositionAttribute`` accent policy
|
|
5
|
+
(``ACCENT_ENABLE_ACRYLICBLURBEHIND``); see ``apply_acrylic``. ``TRACKERKEEPER_NO_WIN_BLUR``
|
|
6
|
+
opts out to the DWM **Mica** system backdrop instead (an opaque, once-sampled
|
|
7
|
+
wallpaper tint — NOT a live blur). Both composite BEHIND the window, visible
|
|
8
|
+
through transparent Qt pixels — the Windows analog of KWin's blur-behind. The
|
|
9
|
+
main window pairs the Acrylic path with a NON-layered window (trackerkeeper/app.py's
|
|
10
|
+
``_win_blur`` drops ``WA_TranslucentBackground``); the layered mini player /
|
|
11
|
+
dialogs keep it.
|
|
12
|
+
|
|
13
|
+
``DwmSetWindowAttribute`` returns an HRESULT, so the Mica fallback has real
|
|
14
|
+
success feedback. Build gates for that fallback (verified against
|
|
15
|
+
learn.microsoft.com):
|
|
16
|
+
|
|
17
|
+
* Windows 11 22H2+ (build >= 22621): the documented
|
|
18
|
+
``DWMWA_SYSTEMBACKDROP_TYPE`` (38) = ``DWMSBT_MAINWINDOW`` (2 = Mica).
|
|
19
|
+
* Windows 11 21H2 (22000..22620): the undocumented ``DWMWA_MICA_EFFECT``
|
|
20
|
+
(1029) = 1.
|
|
21
|
+
* Windows 10 / older (< 22000): no backdrop — UNSUPPORTED → near-opaque body.
|
|
22
|
+
|
|
23
|
+
Mica only renders when Windows' "Transparency effects" toggle is on; when it's
|
|
24
|
+
off we'd paint a translucent body over nothing (see-through), so ``probe()``
|
|
25
|
+
reads that setting from the registry and reports UNSUPPORTED when it's off —
|
|
26
|
+
the Windows analog of KDE's ``kwinrc blurEnabled`` demotion.
|
|
27
|
+
|
|
28
|
+
See docs/research/portable_blur.md §5. The DWM calls are exercised on Windows
|
|
29
|
+
only; the build/transparency gating is unit-tested cross-platform.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import ctypes
|
|
35
|
+
import os
|
|
36
|
+
import sys
|
|
37
|
+
|
|
38
|
+
from trackerkeeper.platform_compat import IS_WINDOWS
|
|
39
|
+
|
|
40
|
+
# ── DWM attribute ids + backdrop enum (learn.microsoft.com) ──────────────
|
|
41
|
+
_DWMWA_USE_IMMERSIVE_DARK_MODE = 20 # dark native titlebar
|
|
42
|
+
_DWMWA_SYSTEMBACKDROP_TYPE = 38 # documented; build >= 22621
|
|
43
|
+
_DWMWA_MICA_EFFECT = 1029 # legacy undocumented; build 22000..22620
|
|
44
|
+
_DWMWA_WINDOW_CORNER_PREFERENCE = 33 # round a frameless window; build >= 22000
|
|
45
|
+
_DWMWCP_ROUND = 2 # DWMWCP_ROUND — round the corners
|
|
46
|
+
_DWMSBT_NONE = 1 # remove the backdrop
|
|
47
|
+
_DWMSBT_MAINWINDOW = 2 # Mica
|
|
48
|
+
|
|
49
|
+
_MIN_BUILD_MICA = 22000 # Windows 11 21H2
|
|
50
|
+
_MIN_BUILD_DOCUMENTED = 22621 # Windows 11 22H2 (documented attr 38)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _build() -> int:
|
|
54
|
+
"""Windows build number, or 0 where unavailable (non-Windows / error)."""
|
|
55
|
+
try:
|
|
56
|
+
return int(sys.getwindowsversion().build)
|
|
57
|
+
except Exception:
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _transparency_enabled() -> bool:
|
|
62
|
+
"""Windows "Transparency effects" toggle (Settings → Personalization →
|
|
63
|
+
Colors). Mica does not render when it's off, so we'd be painting a
|
|
64
|
+
translucent body over nothing — read it to demote to the near-opaque
|
|
65
|
+
fallback instead. HKCU\\…\\Themes\\Personalize\\EnableTransparency;
|
|
66
|
+
defaults True when unreadable (apply stays best-effort). The Windows
|
|
67
|
+
analog of KDE's ``kwinrc [Plugins] blurEnabled`` check."""
|
|
68
|
+
try:
|
|
69
|
+
import winreg
|
|
70
|
+
|
|
71
|
+
with winreg.OpenKey(
|
|
72
|
+
winreg.HKEY_CURRENT_USER,
|
|
73
|
+
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
|
|
74
|
+
) as key:
|
|
75
|
+
val, _ = winreg.QueryValueEx(key, "EnableTransparency")
|
|
76
|
+
return bool(val)
|
|
77
|
+
except Exception:
|
|
78
|
+
return True
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def is_supported() -> bool:
|
|
82
|
+
"""True on a Windows 11 build that can show Mica. A True here doesn't
|
|
83
|
+
guarantee it renders (transparency could be off) — that's probe()'s job."""
|
|
84
|
+
return IS_WINDOWS and _build() >= _MIN_BUILD_MICA
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _MARGINS(ctypes.Structure):
|
|
88
|
+
_fields_ = [
|
|
89
|
+
("cxLeftWidth", ctypes.c_int),
|
|
90
|
+
("cxRightWidth", ctypes.c_int),
|
|
91
|
+
("cyTopHeight", ctypes.c_int),
|
|
92
|
+
("cyBottomHeight", ctypes.c_int),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _set_attr(hwnd: int, attr: int, value: int) -> int:
|
|
97
|
+
"""DwmSetWindowAttribute(hwnd, attr, &value, 4) → HRESULT.
|
|
98
|
+
|
|
99
|
+
``restype`` is ``c_long`` (signed) so ``E_INVALIDARG`` (0x80070057, high
|
|
100
|
+
bit set) reads back as a negative failure rather than a huge positive."""
|
|
101
|
+
fn = ctypes.windll.dwmapi.DwmSetWindowAttribute
|
|
102
|
+
fn.restype = ctypes.c_long
|
|
103
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_uint]
|
|
104
|
+
val = ctypes.c_int(value)
|
|
105
|
+
return fn(
|
|
106
|
+
ctypes.c_void_p(hwnd),
|
|
107
|
+
ctypes.c_uint(attr),
|
|
108
|
+
ctypes.byref(val),
|
|
109
|
+
ctypes.c_uint(ctypes.sizeof(val)),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _extend_frame(hwnd: int) -> None:
|
|
114
|
+
"""Extend the window frame across the whole client area (margins all -1)
|
|
115
|
+
so the Mica backdrop fills it. Required on the legacy 1029 path and
|
|
116
|
+
harmless-recommended on 22621+."""
|
|
117
|
+
fn = ctypes.windll.dwmapi.DwmExtendFrameIntoClientArea
|
|
118
|
+
fn.restype = ctypes.c_long
|
|
119
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
120
|
+
margins = _MARGINS(-1, -1, -1, -1)
|
|
121
|
+
fn(ctypes.c_void_p(hwnd), ctypes.byref(margins))
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ── SetWindowCompositionAttribute accent policy (undocumented user32) ──────
|
|
125
|
+
# The legacy accent-policy API drives the real Acrylic blur-behind (see
|
|
126
|
+
# apply_acrylic). Undocumented user32, so every call is best-effort.
|
|
127
|
+
_WCA_ACCENT_POLICY = 19
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class _ACCENT_POLICY(ctypes.Structure):
|
|
131
|
+
_fields_ = [
|
|
132
|
+
("AccentState", ctypes.c_uint),
|
|
133
|
+
("AccentFlags", ctypes.c_uint),
|
|
134
|
+
("GradientColor", ctypes.c_uint),
|
|
135
|
+
("AnimationId", ctypes.c_uint),
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class _WINCOMPATTRDATA(ctypes.Structure):
|
|
140
|
+
_fields_ = [
|
|
141
|
+
("Attribute", ctypes.c_int),
|
|
142
|
+
("Data", ctypes.c_void_p),
|
|
143
|
+
("SizeOfData", ctypes.c_size_t),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _set_wca(hwnd: int, attribute: int, payload) -> bool:
|
|
148
|
+
"""SetWindowCompositionAttribute(hwnd, &WINCOMPATTRDATA) — best-effort.
|
|
149
|
+
``payload`` is any ctypes object (ACCENT_POLICY struct or a c_int).
|
|
150
|
+
|
|
151
|
+
Returns whether the call was *issued* successfully: the function returns a
|
|
152
|
+
BOOL (nonzero = accepted), so we propagate it (rather than always claiming
|
|
153
|
+
success) to give the Acrylic path the same honest 'issued' signal the Mica
|
|
154
|
+
branch gets from its HRESULT. The undocumented API is best-effort either
|
|
155
|
+
way — the visible blur is identical whatever this returns; only apply()'s
|
|
156
|
+
return becomes truthful. False on a zero return, a missing export, or any
|
|
157
|
+
error (including off-Windows, where ``ctypes.windll`` is absent)."""
|
|
158
|
+
try:
|
|
159
|
+
data = _WINCOMPATTRDATA()
|
|
160
|
+
data.Attribute = attribute
|
|
161
|
+
data.Data = ctypes.cast(ctypes.byref(payload), ctypes.c_void_p)
|
|
162
|
+
data.SizeOfData = ctypes.sizeof(payload)
|
|
163
|
+
fn = ctypes.windll.user32.SetWindowCompositionAttribute
|
|
164
|
+
fn.restype = ctypes.c_int
|
|
165
|
+
fn.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
|
|
166
|
+
return bool(fn(ctypes.c_void_p(hwnd), ctypes.byref(data)))
|
|
167
|
+
except Exception:
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ── Acrylic blur-behind (real frosted glass) ──────────────────────────────
|
|
172
|
+
# Unlike Mica (opaque, wallpaper-sampled-once tint), Acrylic is a live
|
|
173
|
+
# frosted-glass blur. The maintained qframelesswindow drives it through the
|
|
174
|
+
# LEGACY accent-policy API (ACCENT_ENABLE_ACRYLICBLURBEHIND), NOT the modern
|
|
175
|
+
# DWMWA_SYSTEMBACKDROP_TYPE — the system-backdrop Acrylic (DWMSBT_TRANSIENT)
|
|
176
|
+
# is for transient surfaces. The GradientColor is the tint over the blur,
|
|
177
|
+
# packed AABBGGRR; alpha governs how much wallpaper-blur reads through.
|
|
178
|
+
_ACCENT_DISABLED = 0
|
|
179
|
+
_ACCENT_ENABLE_ACRYLICBLURBEHIND = 4
|
|
180
|
+
# Border/shadow flags qframelesswindow passes (draw the 4 edges).
|
|
181
|
+
_ACCENT_DRAW_ALL_BORDERS = 0x20 | 0x40 | 0x80 | 0x100
|
|
182
|
+
# Dark carries a heavier veil than light: at the shared 0x99 the dark
|
|
183
|
+
# theme read "too transparent — missing the weight of a dark themed
|
|
184
|
+
# app" over a bright wallpaper (eyeball-calibrated to 190/0xBE on the
|
|
185
|
+
# Windows 11 laptop, 2026-06-10; light stays at qframelesswindow's
|
|
186
|
+
# default, which calibrated as already right).
|
|
187
|
+
_ACRYLIC_TINT_DARK = 0xBE202020 # A=0xBE (190) over (32,32,32)
|
|
188
|
+
_ACRYLIC_TINT_LIGHT = 0x99F2F2F2 # qframelesswindow's default light tint
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _acrylic_tint(dark: bool, elevated: bool = False) -> int:
|
|
192
|
+
"""Acrylic tint (AABBGGRR). TRACKERKEEPER_WIN_BLUR_ALPHA overrides just the alpha
|
|
193
|
+
(0–255): lower = more blur reads through, higher = more solid tint.
|
|
194
|
+
|
|
195
|
+
``elevated`` (menus / dropdowns / volume popups / tooltips): these
|
|
196
|
+
surfaces carry their own status-aware QSS frost fill — the same veil
|
|
197
|
+
that KDE's UNTINTED KWin blur composites under on Linux. Acrylic's
|
|
198
|
+
default tint stacked a second warm veil on top, so Windows popups
|
|
199
|
+
read warmer + more opaque than the same popup on Linux (2026-06-10
|
|
200
|
+
Windows round). Elevated surfaces therefore request a near-zero
|
|
201
|
+
tint alpha — 0x01, not 0x00, because a fully transparent gradient
|
|
202
|
+
disables the Acrylic material on some builds — leaving the QSS fill
|
|
203
|
+
as the single tint source on every platform. TRACKERKEEPER_WIN_POPUP_BLUR_ALPHA
|
|
204
|
+
tunes it live for eyeball calibration."""
|
|
205
|
+
base = _ACRYLIC_TINT_DARK if dark else _ACRYLIC_TINT_LIGHT
|
|
206
|
+
if elevated:
|
|
207
|
+
try:
|
|
208
|
+
a = int(os.environ.get("TRACKERKEEPER_WIN_POPUP_BLUR_ALPHA", "1"))
|
|
209
|
+
except ValueError:
|
|
210
|
+
a = 1
|
|
211
|
+
return (max(1, min(255, a)) << 24) | (base & 0x00FFFFFF)
|
|
212
|
+
try:
|
|
213
|
+
a = int(os.environ.get("TRACKERKEEPER_WIN_BLUR_ALPHA", ""))
|
|
214
|
+
except ValueError:
|
|
215
|
+
return base
|
|
216
|
+
return (max(0, min(255, a)) << 24) | (base & 0x00FFFFFF)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def apply_acrylic(hwnd: int, dark: bool, enabled: bool = True, elevated: bool = False) -> bool:
|
|
220
|
+
"""Apply (or remove) the legacy Acrylic blur-behind accent policy — the
|
|
221
|
+
qframelesswindow recipe for genuine frosted glass. Best-effort; returns
|
|
222
|
+
whether the accent-policy call was issued successfully (propagated from
|
|
223
|
+
``_set_wca``), mirroring the Mica branch's HRESULT check."""
|
|
224
|
+
accent = _ACCENT_POLICY()
|
|
225
|
+
if enabled:
|
|
226
|
+
accent.AccentState = _ACCENT_ENABLE_ACRYLICBLURBEHIND
|
|
227
|
+
accent.AccentFlags = _ACCENT_DRAW_ALL_BORDERS
|
|
228
|
+
accent.GradientColor = _acrylic_tint(dark, elevated=elevated)
|
|
229
|
+
else:
|
|
230
|
+
accent.AccentState = _ACCENT_DISABLED
|
|
231
|
+
return _set_wca(hwnd, _WCA_ACCENT_POLICY, accent)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def apply(
|
|
235
|
+
widget,
|
|
236
|
+
enabled: bool,
|
|
237
|
+
corner_radius: int = 0,
|
|
238
|
+
dark: bool = True,
|
|
239
|
+
elevated: bool = False,
|
|
240
|
+
) -> bool:
|
|
241
|
+
"""Apply (``enabled``) or remove (``not enabled``) the Windows backdrop
|
|
242
|
+
behind ``widget`` — real Acrylic blur by default, the Mica system backdrop
|
|
243
|
+
when ``TRACKERKEEPER_NO_WIN_BLUR`` is set. ``corner_radius > 0`` additionally asks DWM to round
|
|
244
|
+
the window's corners — needed for the frameless, self-painted surfaces
|
|
245
|
+
(mini player + dialogs, and the main window once it goes frameless on
|
|
246
|
+
Windows), because Windows does NOT clip a frameless translucent HWND to
|
|
247
|
+
the painted rounded body, so without this the corners read square. The
|
|
248
|
+
pixel radius itself is DWM's choice (Win11's standard ~8 px), so the value
|
|
249
|
+
only acts as a "round me" flag; it's harmless on the native-framed window
|
|
250
|
+
(already rounded).
|
|
251
|
+
|
|
252
|
+
Must be called AFTER ``show()`` (``winId()`` needs a real HWND, and Qt
|
|
253
|
+
6.8+ re-runs native window setup that would clobber a constructor-time
|
|
254
|
+
call). Returns True if the backdrop request was accepted (HRESULT S_OK),
|
|
255
|
+
False on any non-Windows / pre-22000 / not-yet-shown / error case. Never
|
|
256
|
+
raises — blur is progressive enhancement."""
|
|
257
|
+
if not IS_WINDOWS:
|
|
258
|
+
return False
|
|
259
|
+
build = _build()
|
|
260
|
+
if build < _MIN_BUILD_MICA:
|
|
261
|
+
return False
|
|
262
|
+
try:
|
|
263
|
+
# windowHandle() check BEFORE winId(): winId() force-creates the
|
|
264
|
+
# native window on a never-shown widget, so the bare int() call both
|
|
265
|
+
# violated the "returns False when not-yet-shown" contract above and
|
|
266
|
+
# triggered exactly the premature native-window setup the Qt 6.8 note
|
|
267
|
+
# warns about. Mirrors the KWin backend's guard.
|
|
268
|
+
if widget.windowHandle() is None:
|
|
269
|
+
return False # never shown — no platform window to blur
|
|
270
|
+
hwnd = int(widget.winId())
|
|
271
|
+
if not hwnd:
|
|
272
|
+
return False # no native window yet
|
|
273
|
+
# Frameless surfaces self-paint a rounded body but Windows leaves the
|
|
274
|
+
# HWND square; ask DWM to round it (Win11 22000+, the build we already
|
|
275
|
+
# gated on). Runs whether or not Mica is enabled so a Solid-theme
|
|
276
|
+
# frameless dialog still gets rounded corners.
|
|
277
|
+
if corner_radius > 0:
|
|
278
|
+
_set_attr(hwnd, _DWMWA_WINDOW_CORNER_PREFERENCE, _DWMWCP_ROUND)
|
|
279
|
+
# Match the titlebar AND the Mica backdrop variant to the theme:
|
|
280
|
+
# immersive-dark on for dark themes (dark Mica), off for light
|
|
281
|
+
# themes (light, wallpaper-tinted Mica). Follows the OS live when
|
|
282
|
+
# the theme_mode is "auto".
|
|
283
|
+
_set_attr(hwnd, _DWMWA_USE_IMMERSIVE_DARK_MODE, 1 if dark else 0)
|
|
284
|
+
# Real frosted-glass blur — the DEFAULT on Windows (TRACKERKEEPER_NO_WIN_BLUR
|
|
285
|
+
# opts out to the Mica system-backdrop below). Drive the legacy
|
|
286
|
+
# Acrylic accent policy instead of Mica (Mica is an opaque
|
|
287
|
+
# once-sampled tint, not a live blur). The main window pairs this with
|
|
288
|
+
# a NON-layered window (trackerkeeper/app.py `_win_blur` drops
|
|
289
|
+
# WA_TranslucentBackground); the accent path also blurs the layered
|
|
290
|
+
# mini player / dialogs. enabled=False (a Solid theme) removes it.
|
|
291
|
+
if not os.environ.get("TRACKERKEEPER_NO_WIN_BLUR"):
|
|
292
|
+
# Propagate the accent-policy result instead of an unconditional
|
|
293
|
+
# True — symmetric with the Mica branch below (`_set_attr(...) == 0`)
|
|
294
|
+
# so apply()'s "issued" return is honest on BOTH paths. Safe to
|
|
295
|
+
# change: no caller reads this return (it's best-effort by contract,
|
|
296
|
+
# see blur/__init__.py), and the visible blur is unaffected.
|
|
297
|
+
return apply_acrylic(hwnd, dark, enabled, elevated=elevated)
|
|
298
|
+
_extend_frame(hwnd)
|
|
299
|
+
if build >= _MIN_BUILD_DOCUMENTED:
|
|
300
|
+
attr = _DWMWA_SYSTEMBACKDROP_TYPE
|
|
301
|
+
value = _DWMSBT_MAINWINDOW if enabled else _DWMSBT_NONE
|
|
302
|
+
else:
|
|
303
|
+
attr = _DWMWA_MICA_EFFECT # legacy: 1 = Mica, 0 = off
|
|
304
|
+
value = 1 if enabled else 0
|
|
305
|
+
return _set_attr(hwnd, attr, value) == 0
|
|
306
|
+
except Exception:
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def probe():
|
|
311
|
+
"""Verified BlurStatus for Windows. Mica availability is a build-version
|
|
312
|
+
fact (no window needed): Windows 11 22000+ with Transparency effects on
|
|
313
|
+
gets a real backdrop → ACTIVE (translucent body rides Mica). Pre-22000,
|
|
314
|
+
or transparency disabled, → UNSUPPORTED (near-opaque body, never
|
|
315
|
+
see-through). See trackerkeeper/blur/__init__.py."""
|
|
316
|
+
from trackerkeeper.blur import BlurStatus
|
|
317
|
+
|
|
318
|
+
if not IS_WINDOWS or _build() < _MIN_BUILD_MICA:
|
|
319
|
+
return BlurStatus.UNSUPPORTED
|
|
320
|
+
if not _transparency_enabled():
|
|
321
|
+
return BlurStatus.UNSUPPORTED
|
|
322
|
+
return BlurStatus.ACTIVE
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def reason(status) -> str:
|
|
326
|
+
"""Human-readable explanation of the Windows blur status — for the boot
|
|
327
|
+
log + Settings hint. Mirrors the other backends' ``reason(status)``; reads
|
|
328
|
+
the build + transparency facts so the message is actionable. Never raises."""
|
|
329
|
+
from trackerkeeper.blur import BlurStatus
|
|
330
|
+
|
|
331
|
+
if not IS_WINDOWS:
|
|
332
|
+
return "not running on Windows"
|
|
333
|
+
if _build() < _MIN_BUILD_MICA:
|
|
334
|
+
return "Windows 10 has no Mica backdrop — using a near-opaque body"
|
|
335
|
+
if not _transparency_enabled():
|
|
336
|
+
return (
|
|
337
|
+
"Windows 'Transparency effects' is off (Settings → Personalization "
|
|
338
|
+
"→ Colors) — using a near-opaque body"
|
|
339
|
+
)
|
|
340
|
+
if status == BlurStatus.ACTIVE:
|
|
341
|
+
# Default is the real Acrylic accent blur; TRACKERKEEPER_NO_WIN_BLUR falls back
|
|
342
|
+
# to the (flat) Mica system-backdrop.
|
|
343
|
+
return (
|
|
344
|
+
"Windows 11 Mica backdrop active"
|
|
345
|
+
if os.environ.get("TRACKERKEEPER_NO_WIN_BLUR")
|
|
346
|
+
else "Windows 11 Acrylic blur active"
|
|
347
|
+
)
|
|
348
|
+
return "blur unavailable — using a near-opaque body"
|