trackerkeeper 0.1.0__tar.gz → 0.1.2__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.
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/release-checklist-template.md +1 -1
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/pypi-publish.yml +2 -2
- {trackerkeeper-0.1.0/trackerkeeper.egg-info → trackerkeeper-0.1.2}/PKG-INFO +1 -1
- trackerkeeper-0.1.2/docs/CHANGELOG.md +202 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/RELEASING.md +20 -4
- trackerkeeper-0.1.2/docs/TODO.md +155 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/dough-sync.toml +9 -1
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/pyproject.toml +6 -4
- trackerkeeper-0.1.2/tests/baselines/settings.png +0 -0
- trackerkeeper-0.1.2/tests/baselines/window.png +0 -0
- trackerkeeper-0.1.2/tests/conftest.py +121 -0
- trackerkeeper-0.1.2/tests/test_dashboard.py +601 -0
- trackerkeeper-0.1.2/tests/test_settings_isolation.py +116 -0
- trackerkeeper-0.1.2/tests/test_sources.py +674 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_tray.py +27 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/_version.py +3 -3
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/app.py +27 -4
- trackerkeeper-0.1.2/trackerkeeper/assets/trackerkeeper.svg +23 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/breadboard.py +749 -168
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/bus.py +5 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/catalog.py +12 -1
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/color_tokens.py +2 -2
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/dashboard.py +446 -66
- trackerkeeper-0.1.2/trackerkeeper/detail_dialog.py +157 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/icons.py +1 -1
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/item_dialog.py +10 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/noborder/_kwin.py +46 -8
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/rig.py +11 -1
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/scaffold.py +43 -6
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/settings.py +16 -2
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/settings_dialog.py +79 -0
- trackerkeeper-0.1.2/trackerkeeper/sources.py +621 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/terminal.py +157 -13
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/theme.py +10 -7
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/top_bar.py +33 -5
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/tray.py +32 -6
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/ui_helpers.py +91 -6
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper-breadboard.toml +91 -15
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2/trackerkeeper.egg-info}/PKG-INFO +1 -1
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/SOURCES.txt +2 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/scm_file_list.json +2 -0
- trackerkeeper-0.1.2/trackerkeeper.egg-info/scm_version.json +8 -0
- trackerkeeper-0.1.0/docs/CHANGELOG.md +0 -55
- trackerkeeper-0.1.0/docs/TODO.md +0 -102
- trackerkeeper-0.1.0/tests/baselines/settings.png +0 -0
- trackerkeeper-0.1.0/tests/baselines/window.png +0 -0
- trackerkeeper-0.1.0/tests/conftest.py +0 -61
- trackerkeeper-0.1.0/tests/test_dashboard.py +0 -199
- trackerkeeper-0.1.0/tests/test_sources.py +0 -230
- trackerkeeper-0.1.0/trackerkeeper/assets/trackerkeeper.svg +0 -12
- trackerkeeper-0.1.0/trackerkeeper/sources.py +0 -318
- trackerkeeper-0.1.0/trackerkeeper.egg-info/scm_version.json +0 -8
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.claude/settings.local.json +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.gitattributes +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/aur.yml +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/ci.yml +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/macos.yml +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/release-checklist.yml +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/release.yml +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/winget.yml +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.gitignore +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/AGENTS.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/CLAUDE.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/LICENSE +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/README.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/ACCESSIBILITY.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/BACKPORT.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/BAKING.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/DELIVERY.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/DESIGN.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/MACOS.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/PHILOSOPHY.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/TEST_BRIDGE.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/TRANSLATING.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/WIND-DOWN.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/appimage/build_appimage.sh +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/appimage/smoke_test_appimage.sh +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/aur/PKGBUILD +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/deb/build_deb.sh +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/deb/smoke_test_deb.sh +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/io.github.wolfgangwarehaus.trackerkeeper.desktop +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/io.github.wolfgangwarehaus.trackerkeeper.metainfo.xml +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/entitlements.plist +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/mas/entitlements.child.plist +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/mas/entitlements.mas.plist +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/mas/scan_symbols.sh +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/mas/sign_mas.sh +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/trackerkeeper.rb +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/msix/AppxManifest.xml +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/msix/STORE-SUBMISSION.md +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/msix/make-assets.sh +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/pyinstaller/launch.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/pyinstaller/trackerkeeper.spec +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/appimage/build_appimage.sh.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/appimage/smoke_test_appimage.sh.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/aur/PKGBUILD.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/deb/build_deb.sh.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/deb/smoke_test_deb.sh.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/entitlements.plist.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/entitlements.child.plist.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/entitlements.mas.plist.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/scan_symbols.sh.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/sign_mas.sh.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/{{app_slug}}.rb.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/msix/AppxManifest.xml.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/msix/STORE-SUBMISSION.md.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/msix/make-assets.sh.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/pyinstaller/launch.py.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/pyinstaller/{{app_slug}}.spec.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/windows/version_info.txt.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/windows/{{app_slug}}.iss.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/{{app_id_base}}.desktop.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/{{app_id_base}}.metainfo.xml.j2 +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/windows/trackerkeeper.iss +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/windows/version_info.txt +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/setup.cfg +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_a11y.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_app_wiring.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_bake.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_blur_kwin.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_boot.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_breadboard.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_bridge.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_bus.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_catalog.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_credentials.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_deliver.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_diagnostics.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_drag_repaint.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_i18n.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_i18n_fmt.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_i18n_plurals.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_identity.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_imports.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_log.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_metadata.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_noborder.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_power_runtime.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_public_api.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_relative_time.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_rig.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_rtl_smoke.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_settings_migration.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_single_instance_forwarding.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_ui_helpers_widgets.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_updates.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_window_seams.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/__main__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/assets/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/async_io.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_linux.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_macos.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_msix.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_unsupported.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_windows.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/bake.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_dwm.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_faux_frost.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_kwin.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_macos.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_unsupported.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/boot_timing.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/credentials.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/custom_tooltip.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/deliver.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/design_tokens.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/diagnostics.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/_kwin.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/_unsupported.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/effect/dragrepaint/contents/code/main.js +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/effect/dragrepaint/metadata.json +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/frosted_dialog.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/i18n/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/i18n/fmt.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/icon_button.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/identity.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/kde_titlebar.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/keyboard_focus.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/log.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/macos_menubar.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/macos_window.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/metadata.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/noborder/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/noborder/_unsupported.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_linux.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_macos.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_unsupported.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_windows.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/platform_compat.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/power/__init__.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/power/_linux.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/power/_unsupported.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/power/_windows.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/selector.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/settings_migration.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/single_instance.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/smooth_scroll.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/system_accent.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/test_bridge.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/update_chip.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/updates.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/win_frameless.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/window.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/windows_shortcut.py +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/dependency_links.txt +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/entry_points.txt +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/requires.txt +0 -0
- {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ it propagates. See `docs/RELEASING.md`.
|
|
|
7
7
|
- [ ] **GitHub release** drafted by `release.yml` — `.deb`, AppImage + `.zsync`, sdist/wheel, `SHA256SUMS`, Sigstore attestations
|
|
8
8
|
- [ ] Cross-distro **smoke tests** green (deb + AppImage self-containment)
|
|
9
9
|
- [ ] **Review the draft** and click **Publish** ← the one human gate
|
|
10
|
-
- [ ] **PyPI** — `pypi-publish.yml` uploaded `${V}`: https://pypi.org/project/trackerkeeper
|
|
10
|
+
- [ ] **PyPI** — `pypi-publish.yml` uploaded `${V}`: https://pypi.org/project/trackerkeeper/${V}/
|
|
11
11
|
- [ ] Provenance verifies: `gh attestation verify <asset> --repo wolfgangwarehaus/trackerkeeper`
|
|
12
12
|
|
|
13
13
|
## Dormant / deferred — light up when configured (docs/TODO.md)
|
|
@@ -10,7 +10,7 @@ name: Publish to PyPI
|
|
|
10
10
|
# One-time PyPI setup — create a pending publisher (the project doesn't exist on
|
|
11
11
|
# PyPI yet, so use the "pending publisher" form):
|
|
12
12
|
# https://pypi.org/manage/account/publishing/ → Add a new pending publisher:
|
|
13
|
-
# PyPI Project Name: trackerkeeper
|
|
13
|
+
# PyPI Project Name: trackerkeeper # == [project] name in pyproject.toml
|
|
14
14
|
# Owner: wolfgangwarehaus
|
|
15
15
|
# Repository name: trackerkeeper
|
|
16
16
|
# Workflow name: pypi-publish.yml
|
|
@@ -34,7 +34,7 @@ jobs:
|
|
|
34
34
|
runs-on: ubuntu-latest
|
|
35
35
|
environment:
|
|
36
36
|
name: pypi
|
|
37
|
-
url: https://pypi.org/project/trackerkeeper
|
|
37
|
+
url: https://pypi.org/project/trackerkeeper/
|
|
38
38
|
permissions:
|
|
39
39
|
contents: read # gh release download
|
|
40
40
|
id-token: write # mint the short-lived OIDC token Trusted Publishing needs
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to trackerkeeper are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning is
|
|
5
|
+
[SemVer](https://semver.org/) and **the git tag is the version** (setuptools-scm).
|
|
6
|
+
|
|
7
|
+
Cutting a release moves the `[Unreleased]` section below into a dated
|
|
8
|
+
`[X.Y.Z]` heading, then `git tag vX.Y.Z`. `release.yml` lifts that section verbatim
|
|
9
|
+
into the GitHub release notes (falling back to auto-generated notes if the tag has
|
|
10
|
+
no matching section). See `docs/RELEASING.md`.
|
|
11
|
+
|
|
12
|
+
## [Unreleased]
|
|
13
|
+
|
|
14
|
+
## [0.1.2] — 2026-07-26
|
|
15
|
+
|
|
16
|
+
The looks pass. tracker keeper stops wearing dough's placeholder, learns to tell
|
|
17
|
+
you *what* changed rather than only *that* something did, and becomes usable at
|
|
18
|
+
the 300px width it always advertised.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **A real logo, and a brand accent to match.** The first two releases shipped
|
|
22
|
+
wearing dough's placeholder blob. The mark is a refresh wheel closing on a
|
|
23
|
+
check — the ring is the update cycle, the check is "you're current" — with the
|
|
24
|
+
gap and arrowhead in the bottom-right, the one quadrant the check's diagonal
|
|
25
|
+
never crosses. One accent throughout, because a two-tone draft put a
|
|
26
|
+
near-white check on the ring that vanished on light backgrounds. The default
|
|
27
|
+
accent moves from dough's violet to the brand green `#2fbe8a` (already the
|
|
28
|
+
"Green" preset), so a fresh profile's chrome matches its own icon.
|
|
29
|
+
- **Per-item detail.** Click any card to see what actually changed — the release
|
|
30
|
+
notes the checkers were already fetching and throwing away. GitHub's release
|
|
31
|
+
body, Steam's announcement, the App Store's "What's New", Flathub's release
|
|
32
|
+
description and any RSS entry's body all land here, normalised to plain text.
|
|
33
|
+
A source that structurally can't carry notes (a package index, an ISO mirror)
|
|
34
|
+
says so rather than showing an empty box.
|
|
35
|
+
|
|
36
|
+
Notes render as **plain text, never rich text**: the body is written by a
|
|
37
|
+
third party, and handing arbitrary remote HTML to a Qt rich-text widget would
|
|
38
|
+
pull in remote images and let a stranger control the markup. The changelog
|
|
39
|
+
link still goes to the fully formatted original.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- **The fleet list has a foreground.** A card had two states — pending or not —
|
|
43
|
+
so a month-old update looked exactly like one from an hour ago, and every row
|
|
44
|
+
wore the same green. Now there are three: **fresh** (an update inside a week)
|
|
45
|
+
leads with a full-white name and the accent on the dot, the new build and the
|
|
46
|
+
changelog link; **pending but older** keeps its dot and its *mark updated*
|
|
47
|
+
button — it's still yours to install — but eases back to neutral; **current**
|
|
48
|
+
sits quietest of all.
|
|
49
|
+
- **One accent, not two near-identical greens.** The "new" colour (`#56c48d`)
|
|
50
|
+
sat a few points off the accent (`#2fbe8a`) while meaning something different,
|
|
51
|
+
which read as a printing error rather than a distinction. The accent is now
|
|
52
|
+
the single colour meaning *live news*, and card tints derive from whatever
|
|
53
|
+
accent is active rather than a frozen hex — so picking another accent
|
|
54
|
+
recolours the list properly.
|
|
55
|
+
- **Category sections read as sections.** A dim caps label over a flat run of
|
|
56
|
+
cards left the groups blending into one list. The header is brighter and
|
|
57
|
+
carries a hairline rule, the cards step in beneath it, and sections are
|
|
58
|
+
separated by a gap.
|
|
59
|
+
- **The app name reads in full at every width.** The title steps down the type
|
|
60
|
+
ladder as the bar tightens (subhead → body → caption) instead of eliding, and
|
|
61
|
+
the update badge shortens with it — `· 3 updates available`, then `· 3 new`,
|
|
62
|
+
then just `· 3`. At 300px the app's own name is worth more of that row than
|
|
63
|
+
two extra words.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
- **The app is legible at its minimum width.** At 300px the fleet list was
|
|
67
|
+
unusable: names chopped to `Stea` / `Game` / `Slay`, versions to `202607`, the
|
|
68
|
+
age column showing `hours ago` with the number cut off the front, and the title
|
|
69
|
+
reading `tracker keepeı`. Four causes: `QLabel` clips instead of eliding (there
|
|
70
|
+
is now a shared `ui_helpers.ElidedLabel`); the name shared one rich-text label
|
|
71
|
+
with the platform tag, so the name was what disappeared; the full-text
|
|
72
|
+
*changelog →* and *mark updated* buttons cost ~175px of a 430px row and are now
|
|
73
|
+
a `→` and a `✓` below the widest tier; and the age column gets compact forms
|
|
74
|
+
(`13h`, `4d`, `2w`, `3mo`) that fit whole, with the full phrase on hover.
|
|
75
|
+
- **An empty update badge no longer reserves space.** Its width floor was
|
|
76
|
+
computed once per layout tier from whatever text happened to be there, so a
|
|
77
|
+
badge that later emptied out kept holding ~59px — and the title paid for it,
|
|
78
|
+
eliding beside an invisible label occupying a sixth of the bar.
|
|
79
|
+
- The Steam checker asked for `maxlength=1`, truncating every announcement body
|
|
80
|
+
to a single character — invisible while only the title was read, and exactly
|
|
81
|
+
what the detail view needed.
|
|
82
|
+
- **The visual baseline was guarding a screen users never see.** The rig grabbed
|
|
83
|
+
dough's placeholder canvas rather than the app's own first screen, so the
|
|
84
|
+
golden would have passed through any dashboard regression. Fixed upstream in
|
|
85
|
+
dough, so every app built from it gets a baseline that tracks its real UI.
|
|
86
|
+
|
|
87
|
+
## [0.1.1] — 2026-07-25
|
|
88
|
+
|
|
89
|
+
The watchtower actually watches now. v0.1.0 only checked at launch, so a
|
|
90
|
+
tray-resident app showed you whatever the world looked like when you booted it;
|
|
91
|
+
this release adds the heartbeat, tells you what's new *since you last looked*,
|
|
92
|
+
and grows the checker count from six to eight.
|
|
93
|
+
|
|
94
|
+
### Added
|
|
95
|
+
- **A Flatpak checker** (`flatpak`) — Flathub app id (`org.videolan.VLC`) via
|
|
96
|
+
Flathub's appstream API. With `arch` beside it that's most of a Linux desktop
|
|
97
|
+
covered. Prefers the newest **stable** release: the feed carries development
|
|
98
|
+
builds too, and quietly promoting you onto a beta would misrepresent "latest".
|
|
99
|
+
- **An optional GitHub token** — set `TRACKERKEEPER_GITHUB_TOKEN` (or
|
|
100
|
+
`GITHUB_TOKEN` / `GH_TOKEN`) to raise GitHub's unauthenticated 60 requests/hour
|
|
101
|
+
to 5000. Read from the environment, never stored by the app, and sent **only**
|
|
102
|
+
to `api.github.com` — a token in your environment is never handed to a distro
|
|
103
|
+
mirror or a changelog feed.
|
|
104
|
+
- **A generic RSS / Atom checker** (`rss`) — the widest-coverage source yet, and
|
|
105
|
+
the answer for the long tail: most projects publish releases as a feed even
|
|
106
|
+
when they expose no API at all. `ref` is the feed URL, optionally followed by
|
|
107
|
+
a space and a filter phrase (`https://…/index.xml plasma`) so one busy feed
|
|
108
|
+
can serve several tracked items. Handles both feed dialects, and falls back to
|
|
109
|
+
the entry link when a feed ships empty titles — as KDE's own does.
|
|
110
|
+
- **The heartbeat.** tracker keeper now re-checks on a timer (every 2 hours by
|
|
111
|
+
default, 15 minutes minimum, `0` for manual-only) instead of once at launch —
|
|
112
|
+
and the timer runs whether or not the window is open, because a watchtower
|
|
113
|
+
resting in the tray is still watching. Opening the window from the tray also
|
|
114
|
+
re-checks when the data has gone stale, so what you see is current.
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
- **Checks run concurrently** (up to 8 at a time) rather than one after another.
|
|
118
|
+
A refresh used to take as long as the *sum* of its sources, and one
|
|
119
|
+
unreachable mirror stalled every item queued behind it.
|
|
120
|
+
- **Conditional requests.** Every fetch now sends `If-None-Match` /
|
|
121
|
+
`If-Modified-Since` and serves the cached body on a `304`. On GitHub a 304
|
|
122
|
+
costs **no rate-limit quota**, which is what makes a two-hourly heartbeat
|
|
123
|
+
affordable in the first place.
|
|
124
|
+
- **A failed check says which failure it was.** "Couldn't reach the source" and
|
|
125
|
+
"nothing matched this source — check the handle" need different reactions;
|
|
126
|
+
reporting both as "couldn't check" sent you debugging your network when the
|
|
127
|
+
actual problem was a typo'd app id.
|
|
128
|
+
- **New since you last looked.** An update that arrives while you're away is
|
|
129
|
+
now marked `NEW` until you actually open the window, and the state is stored
|
|
130
|
+
— so a restart doesn't re-shout what you already saw, and it doesn't quietly
|
|
131
|
+
drop it either. The tray tooltip reports it too ("3 updates available, 1 new").
|
|
132
|
+
- **Settings gained a Tracking section**: how often to check (15 minutes to 12
|
|
133
|
+
hours, or only when you ask), show the tray icon, close-to-tray, and start in
|
|
134
|
+
the tray. The interval re-arms live — no relaunch to change how often a
|
|
135
|
+
watchtower watches.
|
|
136
|
+
|
|
137
|
+
### Fixed
|
|
138
|
+
- A changelog URL is now escaped before it reaches the card's rich-text label —
|
|
139
|
+
those URLs are user-entered, and an unescaped quote could close the `href` and
|
|
140
|
+
let the rest of the string render as markup.
|
|
141
|
+
- **The release tooling broke its own first release.** The Inno Setup template
|
|
142
|
+
carried a sample `/DAppVersion=0.1.0`, and the gate asserting no committed file
|
|
143
|
+
bakes the current version saw that as live the moment v0.1.0 was tagged. That
|
|
144
|
+
gate also matched substrings, so `1.0.0` would have matched inside the MSIX
|
|
145
|
+
placeholder `1.0.0.0` and failed the same way at v1.0.0. Both fixed upstream in
|
|
146
|
+
dough, so no future release — here or in any other app built from it — trips
|
|
147
|
+
over them.
|
|
148
|
+
|
|
149
|
+
### Internal
|
|
150
|
+
- **The test suite was writing to your real settings.** It had no isolation at
|
|
151
|
+
all, so `pytest` edited `~/.config/<org>/trackerkeeper.conf` — a run could
|
|
152
|
+
silently discard collapsed groups you'd set. Closing it properly took three
|
|
153
|
+
Qt calls plus the shipped `Settings` constructor (which used the bare
|
|
154
|
+
`QSettings(org, app)` form, hardwiring NativeFormat); production behaviour is
|
|
155
|
+
unchanged.
|
|
156
|
+
- **The intermittent CI abort had a root cause**, and it wasn't the test being
|
|
157
|
+
blamed: `processEvents()` never delivers `DeferredDelete`, so the whole suite's
|
|
158
|
+
widgets were destroyed at once by the first test to spin a real nested event
|
|
159
|
+
loop. Fixing it also roughly halved the suite's runtime.
|
|
160
|
+
|
|
161
|
+
## [0.1.0] — 2026-07-25
|
|
162
|
+
|
|
163
|
+
The first release. tracker keeper is a **watchtower, not an updater**: it tells
|
|
164
|
+
you what's new across everything you own and links the changelog — it never
|
|
165
|
+
downloads, installs, or applies anything.
|
|
166
|
+
|
|
167
|
+
### Added
|
|
168
|
+
- **The dashboard** — your fleet in one scrollable column, newest-update-first.
|
|
169
|
+
Each card shows what you have vs what the source found, how long ago it
|
|
170
|
+
dropped, its release channel, and a changelog link. Sort by recency or
|
|
171
|
+
channel (click the active chip to flip direction), group into your own
|
|
172
|
+
categories, and collapse the sections you're not watching today.
|
|
173
|
+
- **Six source checkers**, each a small function behind two injected network
|
|
174
|
+
seams so no test ever touches the network:
|
|
175
|
+
|
|
176
|
+
| kind | source | handle (`ref`) |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `github` | GitHub releases API | `owner/repo` |
|
|
179
|
+
| `arch` | archlinux.org package search | the pkgname |
|
|
180
|
+
| `appstore` | Apple iTunes Lookup | track id **or** bundle id |
|
|
181
|
+
| `appledev` | Apple developer-releases feed | an OS filter — "iOS 27" |
|
|
182
|
+
| `steam` | Steam news API | the numeric appid |
|
|
183
|
+
| `cachyos` | the mirror's dated ISO index | the edition |
|
|
184
|
+
|
|
185
|
+
Plus `manual` — the universal fallback for a world no checker reaches yet.
|
|
186
|
+
- **A tray presence.** The app rests in the system tray with the update count
|
|
187
|
+
in its tooltip, a menu (Show / Check / Settings / Quit), click-to-toggle, and
|
|
188
|
+
close-to-tray. It self-disables where the desktop has no tray, so the window
|
|
189
|
+
can never be trapped invisible.
|
|
190
|
+
- **Utility sizing** — a 480×620 default that stays readable down to a 300px
|
|
191
|
+
strip, dropping columns and labels as it narrows rather than squeezing them.
|
|
192
|
+
- **Desktop notifications** when a check finds something genuinely new.
|
|
193
|
+
- The baking phase: a single metadata source (`[tool.trackerkeeper.metadata]`), the
|
|
194
|
+
`trackerkeeper bake` renderer, and the first channels — PyPI, a loose `.deb`, and an
|
|
195
|
+
AppImage — generated from one source and verified against drift.
|
|
196
|
+
|
|
197
|
+
### The rule it lives by
|
|
198
|
+
A card only ever shows a version a **real source returned**. A check that can't
|
|
199
|
+
reach a source keeps the last-known value and says "couldn't check" — tracker
|
|
200
|
+
keeper never invents a latest.
|
|
201
|
+
|
|
202
|
+
<!-- release-notes-end -->
|
|
@@ -32,10 +32,26 @@ uploads workflow artifacts but creates no release.
|
|
|
32
32
|
|
|
33
33
|
## One-time setup
|
|
34
34
|
|
|
35
|
-
- **PyPI Trusted Publishing** —
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`
|
|
35
|
+
- **PyPI Trusted Publishing** *(not yet configured — this is what failed on the
|
|
36
|
+
v0.1.0 publish)* — add a pending publisher at
|
|
37
|
+
<https://pypi.org/manage/account/publishing/>. The project name must match
|
|
38
|
+
`[project] name` in `pyproject.toml`, which for this loaf is **`trackerkeeper`**
|
|
39
|
+
— NOT the dough base's own PyPI project. Register exactly these, which are the
|
|
40
|
+
claims GitHub actually presented on the v0.1.0 run:
|
|
41
|
+
|
|
42
|
+
| field | value |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| PyPI project name | `trackerkeeper` |
|
|
45
|
+
| Owner | `wolfgangwarehaus` |
|
|
46
|
+
| Repository name | `trackerkeeper` |
|
|
47
|
+
| Workflow name | `pypi-publish.yml` |
|
|
48
|
+
| Environment name | `pypi` |
|
|
49
|
+
|
|
50
|
+
Until it exists, publishing a release fails that one leg with
|
|
51
|
+
`invalid-publisher: valid token, but no corresponding publisher` — the GitHub
|
|
52
|
+
release itself still publishes with every artifact, so only PyPI is missing.
|
|
53
|
+
After registering, re-run the failed job (`gh run rerun <id>`) or dispatch
|
|
54
|
+
`pypi-publish.yml` with the tag; nothing needs re-tagging.
|
|
39
55
|
- **AUR** *(dormant)* — `aur.yml` publishes the PKGBUILD on `release: released`,
|
|
40
56
|
but skips until you add an `AUR_SSH_PRIVATE_KEY` secret (a dedicated AUR deploy
|
|
41
57
|
keypair; the public half on your AUR account) **and** the AUR reopens
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# TODO — the handoff
|
|
2
|
+
|
|
3
|
+
**Read this first.** Current state in two lines, then exactly what to do next,
|
|
4
|
+
then the standing backlog.
|
|
5
|
+
|
|
6
|
+
## Where things stand (2026-07-25)
|
|
7
|
+
|
|
8
|
+
**v0.1.0 is tagged and built.** The release workflow produced a complete draft —
|
|
9
|
+
`.deb`, AppImage (+`.zsync`), Windows setup `.exe` + portable `.zip`, sdist,
|
|
10
|
+
wheel, `SHA256SUMS`, Sigstore attestations. It is **unpublished**: reviewing and
|
|
11
|
+
clicking Publish on GitHub is the one deliberate human gate, and publishing
|
|
12
|
+
fires `pypi-publish.yml` (PyPI via OIDC).
|
|
13
|
+
|
|
14
|
+
Since the tag, `main` has moved on with the heartbeat, "new since you last
|
|
15
|
+
looked", the Tracking settings section, and a generic RSS/Atom checker — all
|
|
16
|
+
under `[Unreleased]` in the changelog, i.e. the substance of an 0.2.0.
|
|
17
|
+
|
|
18
|
+
The app is a tray-resident watchtower over a 9-item fleet, every item
|
|
19
|
+
auto-checked, that now **keeps checking on a timer whether or not the window is
|
|
20
|
+
open**. CI is green on all five legs.
|
|
21
|
+
|
|
22
|
+
## Pick up here (in order)
|
|
23
|
+
|
|
24
|
+
1. **Publish the v0.1.0 draft** — review it at
|
|
25
|
+
`gh release view v0.1.0 --web`, then Publish. Delivery item `267380`.
|
|
26
|
+
(The tag was force-moved once, onto `edcf503`, so the released commit is
|
|
27
|
+
green; the draft is idempotently rebuilt on any re-push of the tag.)
|
|
28
|
+
2. **Ship Linux-first** (delivery `0a95b7`): AUR + deb/AppImage via
|
|
29
|
+
`trackerkeeper-deliver`. Note `aur.yml` stays dormant until an
|
|
30
|
+
`AUR_SSH_PRIVATE_KEY` secret exists **and** AUR reopens new-package
|
|
31
|
+
registration.
|
|
32
|
+
3. **Then 0.2.0** when the `[Unreleased]` block feels complete — the two
|
|
33
|
+
cheapest wins on the board are conditional requests (`cb0d95`) and
|
|
34
|
+
distinguishing "no match" from "unreachable" (`4e3124`).
|
|
35
|
+
|
|
36
|
+
## Resolved: the CI abort that cost a re-run on most pushes
|
|
37
|
+
|
|
38
|
+
**Root cause found, and it was not in the single-instance code.** conftest's
|
|
39
|
+
`_isolate_qt_windows` closed and `deleteLater()`'d every top-level widget after
|
|
40
|
+
each test, then called `processEvents()` — but **`processEvents()` does not
|
|
41
|
+
deliver `DeferredDelete`**. Qt only reaps those when an event loop unwinds to
|
|
42
|
+
the nesting level that posted them, and that fixture never runs one:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
w = QWidget(); w.deleteLater()
|
|
46
|
+
app.processEvents() # -> still alive
|
|
47
|
+
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) # -> destroyed
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
So every `deleteLater()`'d widget in the suite stayed alive until the first test
|
|
51
|
+
that spun a **real nested `QEventLoop`** — `_spin()` in
|
|
52
|
+
`test_single_instance_forwarding` — which then destroyed hundreds at once, in
|
|
53
|
+
arbitrary order, each carrying native blur / event-filter state. That explains
|
|
54
|
+
every symptom: always the same point, always right after
|
|
55
|
+
`test_settings_migration` (its alphabetical predecessor), roaming across all
|
|
56
|
+
three OSes, passing locally on a single file, and cleared by `rerun --failed`
|
|
57
|
+
(which runs that file alone).
|
|
58
|
+
|
|
59
|
+
Fixed in `tests/conftest.py` with an explicit `sendPostedEvents(DeferredDelete)`.
|
|
60
|
+
Side effect: the suite got **~2× faster** (8–10 s → ~4 s) while running more
|
|
61
|
+
tests, because every `processEvents()` had been walking an ever-growing object
|
|
62
|
+
graph. If an abort ever returns, look for a *new* fixture that defers deletion
|
|
63
|
+
without reaping it.
|
|
64
|
+
|
|
65
|
+
## What exists now (so you don't re-derive it)
|
|
66
|
+
|
|
67
|
+
**Seven checkers**, all in `sources.py`, each a `(item, http, http_text)`
|
|
68
|
+
function behind two injected network seams (so no test touches the network):
|
|
69
|
+
|
|
70
|
+
| kind | source | notes |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `github` | GitHub releases API | latest **stable** (pre-releases skipped by design) |
|
|
73
|
+
| `arch` | archlinux.org JSON search | prefers stable repo over testing |
|
|
74
|
+
| `appstore` | Apple iTunes Lookup | app id **or** bundle id; the whole iOS/Mac store |
|
|
75
|
+
| `appledev` | Apple developer-releases RSS | `ref` is an OS filter — "iOS 27", "macOS 27" |
|
|
76
|
+
| `steam` | Steam news API | filters to `patchnotes`; version parsed from the title |
|
|
77
|
+
| `cachyos` | mirror ISO index | rolling distro — newest `YYMMDD` snapshot folder |
|
|
78
|
+
| `rss` | **any** RSS/Atom feed | `ref` = feed URL + optional title filter |
|
|
79
|
+
| `manual` | — | the universal fallback; never fabricates a version |
|
|
80
|
+
|
|
81
|
+
`rss` is the widest-coverage one and the answer for the long tail. `ref` is the
|
|
82
|
+
feed URL, optionally followed by a space and a filter phrase
|
|
83
|
+
(`https://kde.org/announcements/index.xml plasma`) — URLs can't contain spaces,
|
|
84
|
+
so the split is unambiguous. It reads both dialects, and when a feed ships empty
|
|
85
|
+
`<title/>` elements it takes the version from the entry **link** instead — KDE's
|
|
86
|
+
own announcement feed does exactly that, and a title-only reader sees nothing.
|
|
87
|
+
|
|
88
|
+
**The heartbeat** (`dashboard.py`): a `QTimer` re-checks every 2 h by default
|
|
89
|
+
(15 min floor, `0` = manual only), running whether or not the window is visible;
|
|
90
|
+
showing the window re-checks when stale. The timer stays `None` under offscreen,
|
|
91
|
+
which doubles as the "network is allowed" sentinel — headless runs never reach
|
|
92
|
+
the network. Checks run **concurrently** (pool of 8), so one dead mirror can't
|
|
93
|
+
stall the rest.
|
|
94
|
+
|
|
95
|
+
**"New since you last looked"**: `Item.seen_version` + `is_new()`. `has_update()`
|
|
96
|
+
stays true until you install, so it can't tell *pending* from *news*; `is_new()`
|
|
97
|
+
is an update you haven't laid eyes on, and it's persisted. Banked on
|
|
98
|
+
`hideEvent` — banking on show would clear the badges in the instant they
|
|
99
|
+
appeared. Notifications deliberately still fire only when `latest` **changes**;
|
|
100
|
+
firing on "unseen" would re-notify every cycle until you looked.
|
|
101
|
+
|
|
102
|
+
**The cardinal rule** (holds everywhere): a card only ever shows a version a
|
|
103
|
+
real source returned. Unreachable → "couldn't check", never an invented latest.
|
|
104
|
+
|
|
105
|
+
**UI:** one top bar (hamburger + settings left, title + badge, actions right,
|
|
106
|
+
window controls) — the dashboard folds its header onto it via
|
|
107
|
+
`TopBar.add_action()` / `insert_title_widget()` / `add_menu_action()`. Below it:
|
|
108
|
+
sort chips (Updated / Channel, click the active one to flip direction), a Group
|
|
109
|
+
toggle, then collapsible category sections. Density comes from the design-token
|
|
110
|
+
type ladder (CAPTION/TINY/MICRO), never literal px, so the font-scale setting
|
|
111
|
+
still works. `width_tier()` drops columns as the window narrows.
|
|
112
|
+
|
|
113
|
+
**Tray** (`tray.py`): tooltip carries the update count *and* the unseen count
|
|
114
|
+
("3 updates available, 1 new"), menu (Show / Check / Settings / Quit),
|
|
115
|
+
click-to-toggle, close-to-tray, start-in-tray. Self-disables when the desktop
|
|
116
|
+
has no tray so the window can never be trapped invisible —
|
|
117
|
+
`tray.will_have_tray()` is the single predicate for that, and start-in-tray is
|
|
118
|
+
gated on it (skipping the show without a tray would launch a process with no
|
|
119
|
+
icon *and* no window).
|
|
120
|
+
|
|
121
|
+
**Settings** now has a TRACKING section: check interval, show tray icon,
|
|
122
|
+
close-to-tray, start-in-tray. The interval re-arms the live dashboard through
|
|
123
|
+
`AppBus.tracking_prefs_changed` — no relaunch.
|
|
124
|
+
|
|
125
|
+
## Standing backlog
|
|
126
|
+
|
|
127
|
+
- **More checkers**: Flatpak (baking `f7a5a3`, Arch half already shipped) —
|
|
128
|
+
Flathub's `/api/v2/appstream/<id>` is the obvious way in.
|
|
129
|
+
- **Conditional requests** (`cb0d95`) — ETag/`If-None-Match` + optional GitHub
|
|
130
|
+
token. Matters more now that checks run every 2 h by default.
|
|
131
|
+
- **"No match" vs "unreachable"** (`4e3124`) — a typo'd ref currently reads as a
|
|
132
|
+
network blip.
|
|
133
|
+
- **Per-item detail view** (`ba86e2`) — GitHub hands you `body` and Steam
|
|
134
|
+
`contents` for free; both are currently discarded in favour of a link.
|
|
135
|
+
- **Brand**: replace the placeholder logo SVG + pick an accent (ingredient
|
|
136
|
+
`ac9750`) — currently riding the system accent.
|
|
137
|
+
- **rig baseline goldens** now that the UI has settled (`eb70cb`).
|
|
138
|
+
- **CachyOS `kde` edition** doesn't parse (different mirror layout); `desktop`,
|
|
139
|
+
`handheld`, `cli` work.
|
|
140
|
+
|
|
141
|
+
## Gotchas worth keeping
|
|
142
|
+
|
|
143
|
+
- **Two catalogs.** `default_fleet()` in `catalog.py` is the seed for a *fresh*
|
|
144
|
+
install; the live fleet is `~/.local/share/wolfgangwarehaus/trackerkeeper/catalog.json`
|
|
145
|
+
and **the file wins**. Editing only the seed changes nothing for an existing
|
|
146
|
+
user — that cost a debugging round early on.
|
|
147
|
+
- **Stop the app before hand-editing that JSON**, or the running instance
|
|
148
|
+
overwrites your edit on its next save.
|
|
149
|
+
- **`rig probe` needs no other instance running** — single-instance refuses the
|
|
150
|
+
probe's second launch and the X11 leg reports "(no window)" as a false FAIL.
|
|
151
|
+
- **Re-render must un-parent before `deleteLater`**, or old rows ghost over the
|
|
152
|
+
new layout.
|
|
153
|
+
- `top_bar.py` is now `manual` in `dough-sync.toml` (it was unclassified, i.e.
|
|
154
|
+
AUTO — a sync would have silently overwritten the hamburger work); `tray.py`
|
|
155
|
+
is `authored`.
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# The dough commit this loaf was last reconciled against. `--record` stamps it.
|
|
5
5
|
# Seeded to dough HEAD at init; set it to the commit you actually forked from if
|
|
6
6
|
# you want the first sync to surface everything dough changed since.
|
|
7
|
-
synced_from = "
|
|
7
|
+
synced_from = "f51c08b"
|
|
8
8
|
|
|
9
9
|
# Files (relative to the trackerkeeper/ package dir) the fork OWNS — net-new app code or
|
|
10
10
|
# modules it fully rewrote. Sync never reads or writes these.
|
|
@@ -13,6 +13,7 @@ authored = [
|
|
|
13
13
|
"sources.py", # the update-checker provider engine — net-new
|
|
14
14
|
"dashboard.py", # the fleet dashboard UI — net-new
|
|
15
15
|
"item_dialog.py", # the add/edit-item form — net-new
|
|
16
|
+
"detail_dialog.py",# the per-item release-notes view — net-new
|
|
16
17
|
"tray.py", # the system-tray presence (watchtower resting state) — net-new
|
|
17
18
|
]
|
|
18
19
|
|
|
@@ -22,4 +23,11 @@ manual = [
|
|
|
22
23
|
"app.py", # main() wires the dashboard as run_app content (not the placeholder)
|
|
23
24
|
"identity.py", # display_name "tracker keeper" — the whole-word transform would regress it
|
|
24
25
|
"top_bar.py", # hamburger + left settings + the add_action/title-slot API the dashboard folds into
|
|
26
|
+
# These three were UNCLASSIFIED while carrying local changes, which meant the
|
|
27
|
+
# next `--apply` would have silently reverted them — the drift report read
|
|
28
|
+
# +0/-79, +0/-5, +1/-11, i.e. pure loss. Classify a file the moment you
|
|
29
|
+
# customize it; the sync door assumes anything unlisted is dough's to own.
|
|
30
|
+
"settings_dialog.py", # the TRACKING section (check interval + the tray toggles)
|
|
31
|
+
"bus.py", # AppBus.tracking_prefs_changed — Settings re-arms the live heartbeat
|
|
32
|
+
"rig.py", # baseline grabs the app's real content, not dough's placeholder
|
|
25
33
|
]
|
|
@@ -11,10 +11,12 @@ requires = ["setuptools>=68", "wheel", "setuptools-scm>=8"]
|
|
|
11
11
|
build-backend = "setuptools.build_meta"
|
|
12
12
|
|
|
13
13
|
[project]
|
|
14
|
-
# The PyPI DISTRIBUTION name
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# a fork's name back to its slug.
|
|
14
|
+
# The PyPI DISTRIBUTION name. Inherited lore warning: the BASE (dough) publishes
|
|
15
|
+
# under a -base suffix because its own bare name is squatted, and baking rewrote
|
|
16
|
+
# that comment with this slug — but `trackerkeeper` is free on PyPI, and `new`
|
|
17
|
+
# pins a fork's name back to its slug, which is what happened here. This value is
|
|
18
|
+
# authoritative: it names the wheel/sdist, deliver.py reads it, and it is what the
|
|
19
|
+
# PyPI Trusted Publisher must be registered against (docs/RELEASING.md).
|
|
18
20
|
name = "trackerkeeper"
|
|
19
21
|
# version is DYNAMIC — the tag is the version (docs/BAKING.md §2 principle 4).
|
|
20
22
|
# setuptools-scm derives it from the latest `git tag vX.Y.Z`; there is nothing
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Shared fixtures for the trackerkeeper test suite.
|
|
2
|
+
|
|
3
|
+
trackerkeeper is PySide6 code, so almost every test needs a live ``QApplication``. We
|
|
4
|
+
deliberately don't depend on ``pytest-qt`` (it's not in the dev extras) — a
|
|
5
|
+
hand-rolled, session-scoped ``qapp`` fixture mirrors the offscreen application
|
|
6
|
+
``ci.yml``'s boot-smoke builds, so the suite runs headless and deterministically.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
# Force the offscreen platform BEFORE any PySide6 import so the suite never needs
|
|
14
|
+
# a display server (parity with the boot-smoke CI step). setdefault so a caller
|
|
15
|
+
# can still override (e.g. QT_QPA_PLATFORM=xcb to eyeball a widget).
|
|
16
|
+
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from PySide6.QtCore import (
|
|
20
|
+
QCoreApplication,
|
|
21
|
+
QEvent,
|
|
22
|
+
QSettings,
|
|
23
|
+
QStandardPaths,
|
|
24
|
+
QThread,
|
|
25
|
+
)
|
|
26
|
+
from PySide6.QtWidgets import QApplication
|
|
27
|
+
|
|
28
|
+
# Keep the suite OUT of the maker's real settings — it had been writing straight
|
|
29
|
+
# into them. Tests mutate settings through the shipped `get_settings()` handle
|
|
30
|
+
# (save_collapsed, the tray toggles, the refresh interval), and that handle keys
|
|
31
|
+
# off QSettings(identity.org(), identity.app()) — the SHIPPED path — so a plain
|
|
32
|
+
# `pytest` was editing ~/.config/wolfgangwarehaus/trackerkeeper.conf. It really
|
|
33
|
+
# did: `collapsed_groups` and `start_minimized` turned up there, written by the
|
|
34
|
+
# suite, which means a run could quietly discard the groups you had folded.
|
|
35
|
+
#
|
|
36
|
+
# Two calls are needed, not one. Test mode redirects PATHS, but QSettings'
|
|
37
|
+
# NativeFormat is a different store per platform and only Linux's is a path it
|
|
38
|
+
# can reach — macOS uses a CFPreferences .plist and Windows the registry, and
|
|
39
|
+
# neither is redirected. IniFormat routes all three through QStandardPaths so
|
|
40
|
+
# the redirect is universal. Both must precede any QSettings construction: the
|
|
41
|
+
# (org, app) ctor resolves its backend at construction time.
|
|
42
|
+
# tests/test_settings_isolation.py is the guard that keeps this honest.
|
|
43
|
+
QStandardPaths.setTestModeEnabled(True)
|
|
44
|
+
QSettings.setDefaultFormat(QSettings.Format.IniFormat)
|
|
45
|
+
|
|
46
|
+
# …and pin WHERE the INI lives, because test mode doesn't reach it off Linux.
|
|
47
|
+
# QSettings resolves its own base dir and only consults QStandardPaths on Linux
|
|
48
|
+
# (via XDG_CONFIG_HOME, which test mode sets); macOS uses a hardcoded ~/.config
|
|
49
|
+
# and Windows the raw %APPDATA%, so IniFormat alone still lands on the real
|
|
50
|
+
# store in an INI coat. setPath() is Qt's explicit override and the only thing
|
|
51
|
+
# that binds all three. Anchored to the test-mode config location so everything
|
|
52
|
+
# stays under one qttest tree.
|
|
53
|
+
QSettings.setPath(
|
|
54
|
+
QSettings.Format.IniFormat,
|
|
55
|
+
QSettings.Scope.UserScope,
|
|
56
|
+
QStandardPaths.writableLocation(
|
|
57
|
+
QStandardPaths.StandardLocation.GenericConfigLocation),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.fixture(scope="session")
|
|
62
|
+
def qapp():
|
|
63
|
+
"""The process-wide QApplication, identity-stamped exactly like ``main()``:
|
|
64
|
+
application + organization name ``"trackerkeeper"`` so the ``QSettings("trackerkeeper",
|
|
65
|
+
"trackerkeeper")`` handle resolves identically under test."""
|
|
66
|
+
app = QApplication.instance() or QApplication([])
|
|
67
|
+
app.setApplicationName("trackerkeeper")
|
|
68
|
+
app.setOrganizationName("trackerkeeper")
|
|
69
|
+
yield app
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.fixture(autouse=True)
|
|
73
|
+
def _isolate_identity():
|
|
74
|
+
"""Snapshot and restore the process-global identity (trackerkeeper.identity is module
|
|
75
|
+
state mutated by configure()). Defense-in-depth for the whole identity /
|
|
76
|
+
metadata suite: a test that reidentifies the app and fails — or a future one
|
|
77
|
+
that forgets to restore — would otherwise leak into every later assertion.
|
|
78
|
+
Restores the raw ``_owner`` sentinel too, which configure() cannot reset to
|
|
79
|
+
None. Snapshot via the private globals so the reset is exact."""
|
|
80
|
+
from trackerkeeper import identity
|
|
81
|
+
|
|
82
|
+
saved = (identity._org, identity._app, identity._display_name, identity._owner)
|
|
83
|
+
yield
|
|
84
|
+
identity._org, identity._app, identity._display_name, identity._owner = saved
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@pytest.fixture(autouse=True)
|
|
88
|
+
def _isolate_qt_windows(qapp):
|
|
89
|
+
"""Tear down any top-level windows a test creates, right after it runs, so Qt
|
|
90
|
+
state never accumulates across tests. Without this, lingering windows — each
|
|
91
|
+
carrying native blur / event-filter state — pile up and get destroyed in an
|
|
92
|
+
arbitrary order at process exit, which makes PySide6 segfault; the leak is
|
|
93
|
+
order-dependent, so it only bites under shuffled runs (pytest-randomly).
|
|
94
|
+
Deleting per test, while the QApplication is healthy, keeps each test's Qt
|
|
95
|
+
world isolated and the process exit clean."""
|
|
96
|
+
yield
|
|
97
|
+
# A widget can own a RUNNING QThread — Dashboard parents its _RefreshWorker
|
|
98
|
+
# to itself — and destroying one mid-flight aborts the process ("QThread:
|
|
99
|
+
# Destroyed while thread is still running"). Let them finish before the
|
|
100
|
+
# delete below can take their parent out from under them. Caught in dough,
|
|
101
|
+
# where the breadboard's channel probe hit exactly this once the reap landed.
|
|
102
|
+
for w in qapp.topLevelWidgets():
|
|
103
|
+
for t in w.findChildren(QThread):
|
|
104
|
+
if t.isRunning():
|
|
105
|
+
t.quit() # ends an event loop; a blocking run() ignores it
|
|
106
|
+
t.wait(10_000) # …so this is what actually makes it safe
|
|
107
|
+
for w in qapp.topLevelWidgets():
|
|
108
|
+
w.close()
|
|
109
|
+
w.deleteLater()
|
|
110
|
+
qapp.processEvents()
|
|
111
|
+
# …and actually REAP them. processEvents() does not deliver DeferredDelete:
|
|
112
|
+
# Qt only reaps those when an event loop unwinds to the nesting level that
|
|
113
|
+
# posted them, and this fixture never runs one. Without this line every
|
|
114
|
+
# deleteLater'd widget in the suite stays alive until the first test that
|
|
115
|
+
# spins a real nested QEventLoop — test_single_instance_forwarding's _spin()
|
|
116
|
+
# — which then destroys hundreds of them at once, in arbitrary order, each
|
|
117
|
+
# carrying native blur / event-filter state. That is exactly the pile-up
|
|
118
|
+
# this fixture exists to prevent, and it aborted the run (SIGABRT on
|
|
119
|
+
# ubuntu/macos, a hard exit on windows) on most pushes. Reaping per test
|
|
120
|
+
# keeps destruction ordered and the process exit clean.
|
|
121
|
+
QCoreApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete)
|