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,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,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,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>
|