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.
Files changed (89) hide show
  1. trackerkeeper/__init__.py +55 -0
  2. trackerkeeper/__main__.py +3 -0
  3. trackerkeeper/_version.py +24 -0
  4. trackerkeeper/app.py +445 -0
  5. trackerkeeper/assets/__init__.py +2 -0
  6. trackerkeeper/assets/trackerkeeper.svg +12 -0
  7. trackerkeeper/async_io.py +299 -0
  8. trackerkeeper/autostart/__init__.py +52 -0
  9. trackerkeeper/autostart/_linux.py +129 -0
  10. trackerkeeper/autostart/_macos.py +163 -0
  11. trackerkeeper/autostart/_msix.py +181 -0
  12. trackerkeeper/autostart/_unsupported.py +22 -0
  13. trackerkeeper/autostart/_windows.py +96 -0
  14. trackerkeeper/bake.py +217 -0
  15. trackerkeeper/blur/__init__.py +212 -0
  16. trackerkeeper/blur/_dwm.py +348 -0
  17. trackerkeeper/blur/_faux_frost.py +143 -0
  18. trackerkeeper/blur/_kwin.py +478 -0
  19. trackerkeeper/blur/_macos.py +356 -0
  20. trackerkeeper/blur/_unsupported.py +39 -0
  21. trackerkeeper/boot_timing.py +75 -0
  22. trackerkeeper/breadboard.py +1554 -0
  23. trackerkeeper/bus.py +110 -0
  24. trackerkeeper/catalog.py +176 -0
  25. trackerkeeper/color_tokens.py +697 -0
  26. trackerkeeper/credentials.py +471 -0
  27. trackerkeeper/custom_tooltip.py +254 -0
  28. trackerkeeper/dashboard.py +704 -0
  29. trackerkeeper/deliver.py +442 -0
  30. trackerkeeper/design_tokens.py +415 -0
  31. trackerkeeper/diagnostics.py +168 -0
  32. trackerkeeper/drag_repaint/__init__.py +68 -0
  33. trackerkeeper/drag_repaint/_kwin.py +192 -0
  34. trackerkeeper/drag_repaint/_unsupported.py +41 -0
  35. trackerkeeper/drag_repaint/effect/dragrepaint/contents/code/main.js +86 -0
  36. trackerkeeper/drag_repaint/effect/dragrepaint/metadata.json +17 -0
  37. trackerkeeper/frosted_dialog.py +377 -0
  38. trackerkeeper/i18n/__init__.py +101 -0
  39. trackerkeeper/i18n/fmt.py +137 -0
  40. trackerkeeper/icon_button.py +137 -0
  41. trackerkeeper/icons.py +532 -0
  42. trackerkeeper/identity.py +135 -0
  43. trackerkeeper/item_dialog.py +185 -0
  44. trackerkeeper/kde_titlebar.py +196 -0
  45. trackerkeeper/keyboard_focus.py +353 -0
  46. trackerkeeper/log.py +112 -0
  47. trackerkeeper/macos_menubar.py +282 -0
  48. trackerkeeper/macos_window.py +172 -0
  49. trackerkeeper/metadata.py +121 -0
  50. trackerkeeper/noborder/__init__.py +61 -0
  51. trackerkeeper/noborder/_kwin.py +247 -0
  52. trackerkeeper/noborder/_unsupported.py +42 -0
  53. trackerkeeper/notifications/__init__.py +84 -0
  54. trackerkeeper/notifications/_linux.py +68 -0
  55. trackerkeeper/notifications/_macos.py +159 -0
  56. trackerkeeper/notifications/_unsupported.py +22 -0
  57. trackerkeeper/notifications/_windows.py +115 -0
  58. trackerkeeper/platform_compat.py +149 -0
  59. trackerkeeper/power/__init__.py +84 -0
  60. trackerkeeper/power/_linux.py +79 -0
  61. trackerkeeper/power/_unsupported.py +18 -0
  62. trackerkeeper/power/_windows.py +52 -0
  63. trackerkeeper/rig.py +338 -0
  64. trackerkeeper/scaffold.py +310 -0
  65. trackerkeeper/selector.py +529 -0
  66. trackerkeeper/settings.py +226 -0
  67. trackerkeeper/settings_dialog.py +307 -0
  68. trackerkeeper/settings_migration.py +101 -0
  69. trackerkeeper/single_instance.py +330 -0
  70. trackerkeeper/smooth_scroll.py +155 -0
  71. trackerkeeper/sources.py +318 -0
  72. trackerkeeper/system_accent.py +363 -0
  73. trackerkeeper/terminal.py +423 -0
  74. trackerkeeper/test_bridge.py +514 -0
  75. trackerkeeper/theme.py +725 -0
  76. trackerkeeper/top_bar.py +154 -0
  77. trackerkeeper/tray.py +184 -0
  78. trackerkeeper/ui_helpers.py +2294 -0
  79. trackerkeeper/update_chip.py +119 -0
  80. trackerkeeper/updates.py +216 -0
  81. trackerkeeper/win_frameless.py +364 -0
  82. trackerkeeper/window.py +609 -0
  83. trackerkeeper/windows_shortcut.py +355 -0
  84. trackerkeeper-0.1.0.dist-info/METADATA +144 -0
  85. trackerkeeper-0.1.0.dist-info/RECORD +89 -0
  86. trackerkeeper-0.1.0.dist-info/WHEEL +5 -0
  87. trackerkeeper-0.1.0.dist-info/entry_points.txt +8 -0
  88. trackerkeeper-0.1.0.dist-info/licenses/LICENSE +338 -0
  89. trackerkeeper-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,55 @@
1
+ """trackerkeeper — the wolfgang warehaus app base. See docs/PHILOSOPHY.md."""
2
+
3
+ from trackerkeeper.identity import configure
4
+
5
+
6
+ def _resolve_version() -> str:
7
+ """The single version source (docs/BAKING.md §2 principle 4). In a built
8
+ install, setuptools-scm has written ``trackerkeeper/_version.py`` from the git tag.
9
+ In a raw source checkout it hasn't, so fall back to the installed-package
10
+ metadata, then to a sentinel — never hardcode a number that could drift."""
11
+ try:
12
+ from trackerkeeper._version import version # generated at build by setuptools-scm
13
+
14
+ return version
15
+ except ImportError:
16
+ pass
17
+ try:
18
+ from importlib.metadata import PackageNotFoundError, version
19
+
20
+ try:
21
+ return version("trackerkeeper")
22
+ except PackageNotFoundError:
23
+ pass
24
+ except ImportError:
25
+ pass
26
+ return "0.0.0+unknown"
27
+
28
+
29
+ __version__ = _resolve_version()
30
+
31
+ # Curated public API. configure() is eager — it must be callable before anything
32
+ # heavy imports (see trackerkeeper.identity: the font-scale loader reads QSettings at
33
+ # import time). The rest resolve LAZILY via __getattr__ so `import trackerkeeper` stays
34
+ # light: importing the chrome (window / app / design_tokens) would trip that
35
+ # import-time read before a fork's configure() could run. Reach for them
36
+ # (trackerkeeper.run_app, trackerkeeper.AppWindow, …) AFTER configuring.
37
+ _LAZY = {
38
+ "run_app": ("trackerkeeper.app", "run_app"),
39
+ "AppWindow": ("trackerkeeper.window", "AppWindow"),
40
+ "AppBus": ("trackerkeeper.bus", "AppBus"),
41
+ "get_bus": ("trackerkeeper.bus", "get_bus"),
42
+ "Settings": ("trackerkeeper.settings", "Settings"),
43
+ "get_settings": ("trackerkeeper.settings", "get_settings"),
44
+ }
45
+
46
+ __all__ = ["__version__", "configure", *_LAZY]
47
+
48
+
49
+ def __getattr__(name: str):
50
+ target = _LAZY.get(name)
51
+ if target is None:
52
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
53
+ import importlib
54
+
55
+ return getattr(importlib.import_module(target[0]), target[1])
@@ -0,0 +1,3 @@
1
+ from trackerkeeper.app import main
2
+
3
+ main()
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.1.0'
22
+ __version_tuple__ = version_tuple = (0, 1, 0)
23
+
24
+ __commit_id__ = commit_id = 'gedcf50355'
trackerkeeper/app.py ADDED
@@ -0,0 +1,445 @@
1
+ """trackerkeeper app entry point.
2
+
3
+ ``main()`` does the cross-platform Qt setup every warehaus app needs — HiDPI
4
+ rounding, app identity, the theme-matched palette, the app icon — then shows an
5
+ ``AppWindow`` with a placeholder. Fork it: set your identity, swap the
6
+ placeholder for your content, wire your own controllers onto ``AppBus``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import sys
13
+
14
+ from PySide6.QtCore import Qt
15
+ from PySide6.QtGui import QGuiApplication, QIcon
16
+ from PySide6.QtWidgets import (
17
+ QApplication,
18
+ QHBoxLayout,
19
+ QLabel,
20
+ QPushButton,
21
+ QVBoxLayout,
22
+ QWidget,
23
+ )
24
+
25
+ from trackerkeeper import __version__, ui_helpers
26
+ from trackerkeeper.bus import AppBus
27
+ from trackerkeeper.platform_compat import IS_MACOS
28
+
29
+
30
+ def _setup_hidpi() -> None:
31
+ """Resolution independence: pass fractional scale through untouched (Qt 6.7+
32
+ talks wp_fractional_scale_v1 to KWin natively) and let widgets size from the
33
+ scaled tokens. Must run before QApplication is constructed."""
34
+ os.environ.setdefault("QT_SCALE_FACTOR_ROUNDING_POLICY", "PassThrough")
35
+ QGuiApplication.setHighDpiScaleFactorRoundingPolicy(
36
+ Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
37
+ )
38
+
39
+
40
+ def _placeholder() -> QWidget:
41
+ """The blank canvas a fresh fork boots to. Replace with your content via
42
+ ``window.set_content(...)``."""
43
+ from trackerkeeper.design_tokens import (
44
+ BTN_PRIMARY,
45
+ TYPE_BODY,
46
+ TYPE_DISPLAY,
47
+ button_qss,
48
+ type_qss,
49
+ )
50
+
51
+ w = QWidget()
52
+ w.setStyleSheet("background: transparent;")
53
+ lay = QVBoxLayout(w)
54
+ lay.setContentsMargins(40, 40, 40, 56)
55
+ lay.addStretch(1)
56
+
57
+ title = QLabel("trackerkeeper")
58
+ title.setAlignment(Qt.AlignmentFlag.AlignCenter)
59
+ title.setStyleSheet(f"color: {ui_helpers.TEXT}; {type_qss(TYPE_DISPLAY)}")
60
+ lay.addWidget(title)
61
+
62
+ sub = QLabel("your app starts here — edit trackerkeeper/app.py")
63
+ sub.setAlignment(Qt.AlignmentFlag.AlignCenter)
64
+ sub.setStyleSheet(f"color: {ui_helpers.TEXT_DIM}; {type_qss(TYPE_BODY)}")
65
+ lay.addWidget(sub)
66
+
67
+ btn = QPushButton("Open Settings")
68
+ btn.setCursor(Qt.CursorShape.PointingHandCursor)
69
+ btn.setStyleSheet(button_qss(BTN_PRIMARY))
70
+ btn.clicked.connect(lambda: AppBus.get().show_settings.emit())
71
+ row = QHBoxLayout()
72
+ row.addStretch(1)
73
+ row.addWidget(btn)
74
+ row.addStretch(1)
75
+ lay.addSpacing(18)
76
+ lay.addLayout(row)
77
+
78
+ lay.addStretch(2)
79
+ return w
80
+
81
+
82
+ # Held for the process lifetime so faulthandler's file sink isn't garbage-
83
+ # collected / closed out from under it on GUI-subsystem builds.
84
+ _CRASH_LOG_FH = None
85
+
86
+
87
+ def _diag_log_dir():
88
+ """Best-effort writable dir for crash / diagnostic logs, resolved WITHOUT a
89
+ QApplication (this runs before one exists). Windows → ``%LOCALAPPDATA%\\
90
+ {app}``; otherwise ``$XDG_CACHE_HOME`` (or ``~/.cache``) ``/{app}`` — where
91
+ ``{app}`` is the configured identity slug. Returns None if nothing writable."""
92
+ import os as _os
93
+
94
+ from trackerkeeper import identity as _ident
95
+
96
+ try:
97
+ if _os.name == "nt":
98
+ base = _os.environ.get("LOCALAPPDATA") or _os.path.expanduser("~")
99
+ else:
100
+ base = _os.environ.get("XDG_CACHE_HOME") or _os.path.join(
101
+ _os.path.expanduser("~"), ".cache"
102
+ )
103
+ d = _os.path.join(base, _ident.app())
104
+ _os.makedirs(d, exist_ok=True)
105
+ return d
106
+ except Exception:
107
+ return None
108
+
109
+
110
+ def _enable_faulthandler() -> None:
111
+ """Convert a hard native crash (e.g. a cross-thread ``~QObject``) into an
112
+ attributable Python + C stack instead of silent process death.
113
+
114
+ On a normal interpreter this writes to stderr. Under a GUI-subsystem
115
+ interpreter (a pipx ``.exe`` gui-script on Windows / ``pythonw``)
116
+ ``sys.stderr`` is ``None`` and a bare ``enable()`` raises ``RuntimeError`` —
117
+ which would both kill the app before ``app.exec()`` AND leave a windowed
118
+ build's crash with zero trace. So there we instead point faulthandler at a
119
+ ``crash.log`` file and attach a file log (``{app}.log``, level from
120
+ ``TRACKERKEEPER_LOG_LEVEL``, default INFO) the user can hand back."""
121
+ import faulthandler
122
+
123
+ if sys.stderr is not None:
124
+ try:
125
+ faulthandler.enable()
126
+ except Exception:
127
+ # e.g. a stderr replaced by a fileno-less stream (test capture,
128
+ # embedded hosts) — losing the crash hook must never be fatal.
129
+ pass
130
+ return
131
+
132
+ # stderr is None (GUI-subsystem build) — route to files instead.
133
+ d = _diag_log_dir()
134
+ if not d:
135
+ return
136
+ import os as _os
137
+
138
+ global _CRASH_LOG_FH
139
+ try:
140
+ _CRASH_LOG_FH = open(_os.path.join(d, "crash.log"), "a", buffering=1)
141
+ faulthandler.enable(file=_CRASH_LOG_FH, all_threads=True)
142
+ except Exception:
143
+ pass
144
+ try:
145
+ import logging as _logging
146
+
147
+ from trackerkeeper import identity as _ident
148
+
149
+ level = (_os.environ.get("TRACKERKEEPER_LOG_LEVEL") or "INFO").upper()
150
+ fh = _logging.FileHandler(_os.path.join(d, f"{_ident.app()}.log"))
151
+ fh.setFormatter(
152
+ _logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
153
+ )
154
+ root = _logging.getLogger()
155
+ root.addHandler(fh)
156
+ root.setLevel(getattr(_logging, level, _logging.INFO))
157
+ except Exception:
158
+ pass
159
+
160
+
161
+ def _reconcile_autostart() -> None:
162
+ """Re-assert an ENABLED launch-on-login entry at boot. ``enable()`` rewrites
163
+ the OS entry (XDG .desktop / Run key / LaunchAgent), self-healing a stale
164
+ ``Exec``/command after the executable or venv moved. Strictly opt-in: trackerkeeper
165
+ never turns autostart ON — only the Settings toggle does; if it's off or
166
+ unsupported this is a no-op. Best-effort, never fatal."""
167
+ try:
168
+ from trackerkeeper import autostart
169
+
170
+ if autostart.is_supported() and autostart.is_enabled():
171
+ autostart.enable()
172
+ except Exception:
173
+ pass
174
+
175
+
176
+ def _wire_notifications(bus) -> None:
177
+ """Route ``AppBus.notify`` (title, body) to the desktop-notification
178
+ backend. App code emits the signal from its real events with zero imports;
179
+ ``trackerkeeper.notifications`` no-ops where unsupported and never raises."""
180
+
181
+ def _notify(title: str, body: str = "") -> None:
182
+ try:
183
+ from trackerkeeper import notifications
184
+
185
+ notifications.notify(title, body)
186
+ except Exception:
187
+ pass
188
+
189
+ bus.notify.connect(_notify)
190
+
191
+
192
+ def run_app(content_factory, *, identity=None, single_instance=True) -> int:
193
+ """Boot a trackerkeeper app and run it to exit. Does the cross-platform Qt setup
194
+ every warehaus app needs — HiDPI rounding, app identity, persisted theme
195
+ overrides, the theme-matched palette + icon — then shows an ``AppWindow``
196
+ whose content is ``content_factory(window)``. Returns the process exit code.
197
+
198
+ Wires the chrome an app shouldn't re-solve, all unconditionally:
199
+ * **single instance** — a second launch raises the running window instead
200
+ of opening a duplicate (pass ``single_instance=False`` to opt out);
201
+ * **persisted theme** — accent / colour overrides load BEFORE the first
202
+ widget so every surface stamps from the saved palette;
203
+ * **the settings dialog** — wired to ``AppBus.show_settings``;
204
+ * **window geometry** — restored on launch, saved on quit;
205
+ * **launch on login** — re-asserts a user-enabled autostart entry (the
206
+ Settings toggle turns it on; trackerkeeper never does);
207
+ * **desktop notifications** — ``AppBus.notify.emit(title, body)`` reaches
208
+ the OS notification backend (silent no-op where unsupported);
209
+ * **translations** — ``trackerkeeper.i18n`` installs the catalog for the user's
210
+ language (Settings override → system locale) before any widget exists.
211
+
212
+ ``identity`` (optional) is a mapping forwarded to :func:`trackerkeeper.configure`
213
+ (``org`` / ``app`` / ``display_name``). NOTE: for the import-time font scale
214
+ to honour a custom identity, set it in ``trackerkeeper.identity`` or call
215
+ ``configure()`` BEFORE importing the app; Qt names, the AUMID, and QSettings
216
+ all honour it here regardless.
217
+ """
218
+ from trackerkeeper import identity as ident
219
+
220
+ if identity:
221
+ ident.configure(**identity)
222
+
223
+ # Crash/diagnostic logging first, so a native SIGSEGV during boot is still
224
+ # attributable (writes to files under a GUI-subsystem interpreter where
225
+ # stderr is None). Best-effort; never fatal.
226
+ _enable_faulthandler()
227
+
228
+ _setup_hidpi()
229
+ try:
230
+ from trackerkeeper.windows_shortcut import set_process_app_user_model_id
231
+
232
+ set_process_app_user_model_id()
233
+ except Exception:
234
+ pass
235
+
236
+ # macOS: set the application-menu name BEFORE QApplication builds the
237
+ # native menu, so a from-source run reads the app name rather than "Python"
238
+ # (a frozen .app already gets it from CFBundleName). Only ordering
239
+ # constraint here. No-op off macOS.
240
+ if IS_MACOS:
241
+ from trackerkeeper import macos_menubar
242
+
243
+ macos_menubar.set_app_name()
244
+
245
+ app = QApplication.instance() or QApplication(sys.argv)
246
+ app.setApplicationName(ident.app())
247
+ app.setApplicationDisplayName(ident.display_name())
248
+ app.setOrganizationName(ident.org())
249
+ app.setApplicationVersion(__version__)
250
+ # The installed .desktop is named by the reverse-DNS app-id
251
+ # (io.github.{owner}.{app}), so the desktop file name must carry
252
+ # desktop_id() for the taskbar to associate the window with its installed
253
+ # icon. Verified live (KDE, 2026-07-03): on Wayland this value becomes the
254
+ # window's app_id; on X11 Qt exports it as _KDE_NET_WM_DESKTOP_FILE (KDE's
255
+ # association path) while WM_CLASS stays the bare applicationName slug —
256
+ # which is why the .desktop's StartupWMClass keeps the slug (non-KDE X11
257
+ # matches on WM_CLASS). The KWin machinery survives the app_id change by
258
+ # design: the noborder rule and the drag_repaint effect both match the
259
+ # bare slug as a SUBSTRING (proven: noBorder=true under the new app_id).
260
+ app.setDesktopFileName(ident.desktop_id())
261
+
262
+ # Structured file logging (trackerkeeper.log): rotating file under the app state
263
+ # dir + a WARNING console handler, level from TRACKERKEEPER_LOG. Installed HERE —
264
+ # after the Qt identity names, so AppDataLocation resolves per-app; before
265
+ # everything else, so the whole boot is captured. Best-effort, never fatal.
266
+ try:
267
+ from trackerkeeper import log as _log
268
+
269
+ _log.install()
270
+ except Exception:
271
+ pass
272
+
273
+ # Versioned settings migrations — run BEFORE Settings (or anything that
274
+ # reads the store: i18n's language override, the persisted theme) is first
275
+ # read, so every accessor sees the post-migration key layout.
276
+ try:
277
+ from trackerkeeper import settings_migration
278
+
279
+ settings_migration.migrate()
280
+ except Exception:
281
+ pass
282
+
283
+ # Install translation catalogs (Settings override → system locale) before
284
+ # ANY widget is built — Qt translates at construction time. Never raises;
285
+ # a missing catalog just leaves the English source strings.
286
+ from trackerkeeper import i18n as _i18n
287
+
288
+ _i18n.install(app)
289
+
290
+ # Single instance: hand off to the already-running copy rather than opening
291
+ # a second window. Keep the lock object alive for the process lifetime.
292
+ si = None
293
+ if single_instance:
294
+ from trackerkeeper.single_instance import SingleInstance
295
+
296
+ si = SingleInstance(ident.app())
297
+ # Forward this launch's argv (file paths / URLs) so the running copy
298
+ # opens them — the "double-click a document while the app is open" path.
299
+ if not si.acquire(sys.argv[1:]):
300
+ return 0 # another instance was found and signalled to come forward
301
+ app._dough_single_instance = si
302
+ # Re-emit forwarded files on the bus so app code binds with zero refs.
303
+ si.files_received.connect(
304
+ lambda paths: AppBus.get().files_received.emit(list(paths))
305
+ )
306
+
307
+ # Persisted accent / colour overrides must load BEFORE the first widget, so
308
+ # every surface stamps from the saved palette rather than the defaults.
309
+ from trackerkeeper.color_tokens import load_persisted_overrides
310
+
311
+ load_persisted_overrides()
312
+
313
+ from trackerkeeper.ui_helpers import apply_app_palette, make_app_icon
314
+
315
+ app.setWindowIcon(QIcon(make_app_icon(64)))
316
+ apply_app_palette()
317
+
318
+ from trackerkeeper.settings import get_settings
319
+ from trackerkeeper.window import AppWindow
320
+
321
+ win = AppWindow(title=ident.display_name())
322
+ # Record whether a saved size was restored: content that wants its own
323
+ # default size (a utility window) checks this so it never overrides a size
324
+ # the user chose.
325
+ win._geometry_restored = get_settings().restore_geometry(win)
326
+ win.set_content(content_factory(win))
327
+
328
+ def _open_settings():
329
+ from trackerkeeper.settings_dialog import SettingsDialog
330
+
331
+ SettingsDialog(win).exec()
332
+
333
+ AppBus.get().show_settings.connect(_open_settings)
334
+
335
+ from trackerkeeper.single_instance import force_foreground
336
+
337
+ if si is not None:
338
+ si.raise_requested.connect(lambda: force_foreground(win))
339
+ AppBus.get().open_main_window.connect(lambda: force_foreground(win))
340
+
341
+ # Persist any pending QSettings writes (window geometry, theme overrides, …)
342
+ # before the process exits. A hard quit that skips the window's closeEvent
343
+ # (e.g. app.quit() from a tray/menu) still reaches aboutToQuit — without
344
+ # this the most recent toggles are silently lost on the next launch.
345
+ def _flush_settings():
346
+ try:
347
+ get_settings().flush()
348
+ except Exception:
349
+ pass
350
+
351
+ app.aboutToQuit.connect(_flush_settings)
352
+
353
+ # Launch-on-login + desktop notifications — the shipped subsystems, wired:
354
+ # autostart re-asserts an entry the user enabled (Settings toggle) so it
355
+ # survives a moved executable; AppBus.notify(title, body) reaches the OS
356
+ # notification backend. Both opt-in and best-effort.
357
+ _reconcile_autostart()
358
+ _wire_notifications(AppBus.get())
359
+
360
+ # macOS: the global menu bar (App/File/Edit/View/Window/Help) + native
361
+ # window chrome (transparent titlebar / full-size content view) — native
362
+ # conventions a Qt app otherwise lacks. Pure PySide6; only built on macOS,
363
+ # best-effort/no-op elsewhere.
364
+ if IS_MACOS:
365
+ from trackerkeeper import macos_menubar, macos_window
366
+
367
+ macos_menubar.install(win)
368
+ macos_window.apply(win)
369
+
370
+ # KDE Wayland borderless chrome: the main window stays server-side-decorated
371
+ # (so compositor blur survives a drag — a Qt-frameless window loses it) and a
372
+ # KWin `noborder` Force rule strips the visible decoration. Reconcile it
373
+ # BEFORE show so a fresh launch never flashes a titlebar; idempotent +
374
+ # self-healing, a no-op off KDE Wayland. `native_window_border` opts back into
375
+ # the real server-side titlebar.
376
+ from trackerkeeper import noborder
377
+
378
+ if get_settings().native_window_border:
379
+ noborder.remove_main_window_noborder()
380
+ else:
381
+ noborder.install_main_window_noborder()
382
+
383
+ # Follow the desktop accent colour if the user enabled it: apply it now
384
+ # (launch re-read) and watch the OS for live changes (XDG portal / DWM
385
+ # message / AppKit notification). Best-effort — a DE without the portal
386
+ # simply never fires. Pinned on `win` so the watcher lives with the window.
387
+ try:
388
+ from trackerkeeper.system_accent import SystemAccentFollower
389
+
390
+ win._accent_follower = SystemAccentFollower(win)
391
+ win._accent_follower.start()
392
+ except Exception:
393
+ pass
394
+
395
+ win.show()
396
+
397
+ # KWin drag-repaint effect — installed AFTER show (off the first-paint path;
398
+ # it only matters once a drag begins). Forces KWin's full-repaint render path
399
+ # while one of the app's windows is dragged, killing the NVIDIA-EGL stale-blur
400
+ # "line artifact" (KWin bug 455526/457727). Idempotent, best-effort, a no-op
401
+ # off KDE Wayland; TRACKERKEEPER_NO_DRAG_REPAINT=1 removes it instead.
402
+ from trackerkeeper import drag_repaint
403
+
404
+ drag_repaint.sync()
405
+
406
+ # Agent test bridge — dev-only remote control for live end-to-end testing.
407
+ # OFF unless TRACKERKEEPER_TEST_BRIDGE=1 is set at launch. Stands up a per-user
408
+ # local socket whose JSON commands (click / tree / screenshot / settings /
409
+ # eval …) run on the GUI thread, so an agent can drive the real app
410
+ # deterministically — including on Wayland, where synthetic OS input is
411
+ # unreliable. Never enabled in a shipped build. See trackerkeeper/test_bridge.py
412
+ # + docs/TEST_BRIDGE.md.
413
+ if os.environ.get("TRACKERKEEPER_TEST_BRIDGE") == "1":
414
+ from trackerkeeper.test_bridge import TestBridge
415
+
416
+ app._dough_test_bridge = TestBridge(app, win)
417
+ app._dough_test_bridge.start()
418
+
419
+ # Daily update check — deferred a few seconds off the first-paint path.
420
+ # Gated inside maybe_check(): the Settings toggle, the once-a-day throttle,
421
+ # and the channel rule (Store / MAS / AUR builds never nag). Best-effort.
422
+ def _check_updates():
423
+ try:
424
+ from trackerkeeper import updates
425
+
426
+ updates.maybe_check()
427
+ except Exception:
428
+ pass
429
+
430
+ from PySide6.QtCore import QTimer
431
+
432
+ QTimer.singleShot(3000, _check_updates)
433
+
434
+ return app.exec()
435
+
436
+
437
+ def main() -> None:
438
+ """The default entry: boot tracker keeper with the update dashboard."""
439
+ from trackerkeeper.dashboard import build_content
440
+
441
+ sys.exit(run_app(build_content))
442
+
443
+
444
+ if __name__ == "__main__":
445
+ main()
@@ -0,0 +1,2 @@
1
+ # Package marker so importlib.resources.files("trackerkeeper.assets") resolves the
2
+ # bundled brand SVG in a built/installed wheel.
@@ -0,0 +1,12 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
+ <defs>
3
+ <linearGradient id="dg" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0" stop-color="#9D8AE8"/>
5
+ <stop offset="1" stop-color="#7C66D0"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <!-- a soft trackerkeeper blob -->
9
+ <path d="M32 7c15 0 25 8.5 25 22.5C57 46 46 57 32 57S7 46 7 29.5C7 15.5 17 7 32 7z"
10
+ fill="url(#dg)"/>
11
+ <ellipse cx="23" cy="23" rx="7" ry="4.5" fill="#ffffff" opacity="0.22"/>
12
+ </svg>