trackerkeeper 0.1.1__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.1/trackerkeeper.egg-info → trackerkeeper-0.1.2}/PKG-INFO +1 -1
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/CHANGELOG.md +73 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/dough-sync.toml +9 -1
- trackerkeeper-0.1.2/tests/baselines/settings.png +0 -0
- trackerkeeper-0.1.2/tests/baselines/window.png +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_dashboard.py +261 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_sources.py +81 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/_version.py +3 -3
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/app.py +15 -3
- trackerkeeper-0.1.2/trackerkeeper/assets/trackerkeeper.svg +23 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/breadboard.py +749 -168
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/catalog.py +1 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/color_tokens.py +2 -2
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/dashboard.py +292 -53
- trackerkeeper-0.1.2/trackerkeeper/detail_dialog.py +157 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/icons.py +1 -1
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/noborder/_kwin.py +46 -8
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/rig.py +11 -1
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/scaffold.py +43 -6
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/settings.py +1 -1
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/sources.py +69 -1
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/terminal.py +157 -13
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/theme.py +10 -7
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/top_bar.py +33 -5
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/ui_helpers.py +91 -6
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper-breadboard.toml +27 -7
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2/trackerkeeper.egg-info}/PKG-INFO +1 -1
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/SOURCES.txt +1 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/scm_file_list.json +1 -0
- trackerkeeper-0.1.2/trackerkeeper.egg-info/scm_version.json +8 -0
- trackerkeeper-0.1.1/tests/baselines/settings.png +0 -0
- trackerkeeper-0.1.1/tests/baselines/window.png +0 -0
- trackerkeeper-0.1.1/trackerkeeper/assets/trackerkeeper.svg +0 -12
- trackerkeeper-0.1.1/trackerkeeper.egg-info/scm_version.json +0 -8
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.claude/settings.local.json +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.gitattributes +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/release-checklist-template.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/aur.yml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/ci.yml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/macos.yml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/pypi-publish.yml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/release-checklist.yml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/release.yml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/winget.yml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.gitignore +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/AGENTS.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/CLAUDE.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/LICENSE +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/README.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/ACCESSIBILITY.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/BACKPORT.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/BAKING.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/DELIVERY.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/DESIGN.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/MACOS.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/PHILOSOPHY.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/RELEASING.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/TEST_BRIDGE.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/TODO.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/TRANSLATING.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/WIND-DOWN.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/appimage/build_appimage.sh +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/appimage/smoke_test_appimage.sh +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/aur/PKGBUILD +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/deb/build_deb.sh +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/deb/smoke_test_deb.sh +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/io.github.wolfgangwarehaus.trackerkeeper.desktop +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/io.github.wolfgangwarehaus.trackerkeeper.metainfo.xml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/entitlements.plist +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/mas/entitlements.child.plist +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/mas/entitlements.mas.plist +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/mas/scan_symbols.sh +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/mas/sign_mas.sh +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/trackerkeeper.rb +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/msix/AppxManifest.xml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/msix/STORE-SUBMISSION.md +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/msix/make-assets.sh +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/pyinstaller/launch.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/pyinstaller/trackerkeeper.spec +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/appimage/build_appimage.sh.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/appimage/smoke_test_appimage.sh.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/aur/PKGBUILD.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/deb/build_deb.sh.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/deb/smoke_test_deb.sh.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/entitlements.plist.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/entitlements.child.plist.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/entitlements.mas.plist.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/scan_symbols.sh.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/sign_mas.sh.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/{{app_slug}}.rb.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/msix/AppxManifest.xml.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/msix/STORE-SUBMISSION.md.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/msix/make-assets.sh.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/pyinstaller/launch.py.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/pyinstaller/{{app_slug}}.spec.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/windows/version_info.txt.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/windows/{{app_slug}}.iss.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/{{app_id_base}}.desktop.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/{{app_id_base}}.metainfo.xml.j2 +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/windows/trackerkeeper.iss +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/windows/version_info.txt +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/pyproject.toml +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/setup.cfg +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/conftest.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_a11y.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_app_wiring.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_bake.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_blur_kwin.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_boot.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_breadboard.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_bridge.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_bus.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_catalog.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_credentials.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_deliver.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_diagnostics.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_drag_repaint.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_i18n.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_i18n_fmt.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_i18n_plurals.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_identity.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_imports.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_log.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_metadata.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_noborder.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_power_runtime.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_public_api.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_relative_time.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_rig.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_rtl_smoke.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_settings_isolation.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_settings_migration.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_single_instance_forwarding.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_tray.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_ui_helpers_widgets.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_updates.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_window_seams.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/__main__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/assets/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/async_io.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_linux.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_macos.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_msix.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_unsupported.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_windows.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/bake.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_dwm.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_faux_frost.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_kwin.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_macos.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_unsupported.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/boot_timing.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/bus.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/credentials.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/custom_tooltip.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/deliver.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/design_tokens.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/diagnostics.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/_kwin.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/_unsupported.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/effect/dragrepaint/contents/code/main.js +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/effect/dragrepaint/metadata.json +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/frosted_dialog.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/i18n/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/i18n/fmt.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/icon_button.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/identity.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/item_dialog.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/kde_titlebar.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/keyboard_focus.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/log.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/macos_menubar.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/macos_window.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/metadata.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/noborder/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/noborder/_unsupported.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_linux.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_macos.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_unsupported.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_windows.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/platform_compat.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/power/__init__.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/power/_linux.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/power/_unsupported.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/power/_windows.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/selector.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/settings_dialog.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/settings_migration.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/single_instance.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/smooth_scroll.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/system_accent.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/test_bridge.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/tray.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/update_chip.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/updates.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/win_frameless.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/window.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/windows_shortcut.py +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/dependency_links.txt +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/entry_points.txt +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/requires.txt +0 -0
- {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/top_level.txt +0 -0
|
@@ -11,6 +11,79 @@ no matching section). See `docs/RELEASING.md`.
|
|
|
11
11
|
|
|
12
12
|
## [Unreleased]
|
|
13
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
|
+
|
|
14
87
|
## [0.1.1] — 2026-07-25
|
|
15
88
|
|
|
16
89
|
The watchtower actually watches now. v0.1.0 only checked at launch, so a
|
|
@@ -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
|
]
|
|
Binary file
|
|
Binary file
|
|
@@ -338,3 +338,264 @@ def test_error_text_separates_the_two_failures():
|
|
|
338
338
|
assert "check the handle" in error_text(sources.NOT_FOUND)
|
|
339
339
|
assert "couldn't reach" in error_text(sources.UNREACHABLE)
|
|
340
340
|
assert "couldn't reach" in error_text("") # unknown -> the conservative read
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def test_results_store_the_release_notes(qapp):
|
|
344
|
+
dash = _dash(qapp, [catalog.Item(name="G", kind="github", installed="1.0")])
|
|
345
|
+
dash._on_results({"G": CheckResult(latest="1.1", date="2026-07-25",
|
|
346
|
+
notes="• Fixed a thing")})
|
|
347
|
+
assert dash._items[0].latest_notes == "• Fixed a thing"
|
|
348
|
+
assert catalog.load()[0].latest_notes == "• Fixed a thing" # persisted
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_detail_dialog_builds_and_explains_an_empty_body(qapp):
|
|
352
|
+
from trackerkeeper.detail_dialog import DetailDialog
|
|
353
|
+
|
|
354
|
+
# a source that structurally cannot carry notes must SAY so, not show a blank
|
|
355
|
+
d = DetailDialog(item=catalog.Item(name="KDE", kind="arch", ref="plasma-desktop",
|
|
356
|
+
installed="6.7.3-1", latest="6.7.3-1"))
|
|
357
|
+
assert "package index" in d._no_notes_reason()
|
|
358
|
+
d2 = DetailDialog(item=catalog.Item(name="G", kind="github", installed="1.0",
|
|
359
|
+
latest="1.1", latest_notes="• Fixed"))
|
|
360
|
+
assert d2._item.latest_notes == "• Fixed"
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_detail_dialog_mark_updated_mutates_the_item(qapp):
|
|
364
|
+
from trackerkeeper.detail_dialog import DetailDialog
|
|
365
|
+
|
|
366
|
+
item = catalog.Item(name="G", kind="github", installed="1.0", latest="1.1")
|
|
367
|
+
d = DetailDialog(item=item)
|
|
368
|
+
d._on_mark()
|
|
369
|
+
assert item.installed == "1.1" and not item.has_update()
|
|
370
|
+
assert d._marked is True # so prompt() reports "marked" and the caller saves
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ── fresh vs past: the visual hierarchy ──────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def test_is_fresh_tracks_the_release_date_not_just_pending():
|
|
377
|
+
from datetime import datetime, timedelta, timezone
|
|
378
|
+
|
|
379
|
+
from trackerkeeper.dashboard import FRESH_DAYS, is_fresh
|
|
380
|
+
|
|
381
|
+
now = datetime(2026, 7, 26, tzinfo=timezone.utc)
|
|
382
|
+
|
|
383
|
+
def at(days):
|
|
384
|
+
return (now - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
385
|
+
|
|
386
|
+
pending_new = catalog.Item(name="a", installed="1", latest="2", latest_date=at(1))
|
|
387
|
+
pending_old = catalog.Item(name="b", installed="1", latest="2",
|
|
388
|
+
latest_date=at(FRESH_DAYS + 1))
|
|
389
|
+
current = catalog.Item(name="c", installed="2", latest="2", latest_date=at(0))
|
|
390
|
+
|
|
391
|
+
assert is_fresh(pending_new, now)
|
|
392
|
+
assert not is_fresh(pending_old, now) # still yours to install, no longer news
|
|
393
|
+
assert not is_fresh(current, now) # nothing to do at all
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_an_update_with_no_date_counts_as_fresh():
|
|
397
|
+
"""Greying out something actionable on a guess is the worse failure."""
|
|
398
|
+
from trackerkeeper.dashboard import is_fresh
|
|
399
|
+
|
|
400
|
+
assert is_fresh(catalog.Item(name="a", installed="1", latest="2"))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_only_fresh_cards_take_the_accent_wash(qapp):
|
|
404
|
+
from trackerkeeper.dashboard import _card_qss
|
|
405
|
+
|
|
406
|
+
plain, washed = _card_qss(False), _card_qss(True)
|
|
407
|
+
assert plain != washed
|
|
408
|
+
assert "255,255,255" in plain # neutral
|
|
409
|
+
assert "255,255,255" not in washed # accent-tinted
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_card_tint_follows_the_live_accent(qapp):
|
|
413
|
+
"""Hardcoding the brand green would strand anyone who picks another accent."""
|
|
414
|
+
from trackerkeeper import ui_helpers
|
|
415
|
+
from trackerkeeper.dashboard import _card_qss, _rgba
|
|
416
|
+
|
|
417
|
+
assert _rgba("#2fbe8a", 0.08) == "rgba(47,190,138,0.08)"
|
|
418
|
+
assert _rgba("", 0.5).startswith("rgba(255,255,255") # malformed → safe neutral
|
|
419
|
+
r, g, b = (int(ui_helpers.ACCENT.lstrip("#")[i:i + 2], 16) for i in (0, 2, 4))
|
|
420
|
+
assert f"rgba({r},{g},{b}," in _card_qss(True)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# ── legibility at the minimum width ──────────────────────────────────────────
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_elided_label_shortens_with_an_ellipsis_not_a_chop(qapp):
|
|
427
|
+
from trackerkeeper.ui_helpers import ElidedLabel
|
|
428
|
+
|
|
429
|
+
lab = ElidedLabel("SteamOS (Armada)")
|
|
430
|
+
lab.show() # Qt defers resize events for hidden widgets
|
|
431
|
+
lab.resize(40, 20)
|
|
432
|
+
assert lab.text() != "SteamOS (Armada)"
|
|
433
|
+
assert lab.text().endswith("…") # not "Stea"
|
|
434
|
+
assert lab.full_text() == "SteamOS (Armada)"
|
|
435
|
+
lab.resize(400, 20)
|
|
436
|
+
assert lab.text() == "SteamOS (Armada)" # room again → whole string back
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_elided_label_sizehint_reports_the_FULL_text(qapp):
|
|
440
|
+
"""The feedback loop that made a title read "trac…" on a roomy bar: eliding
|
|
441
|
+
shrinks QLabel's own sizeHint, so the layout hands it less, so it elides
|
|
442
|
+
harder. The hint has to describe the full string."""
|
|
443
|
+
from trackerkeeper.ui_helpers import ElidedLabel
|
|
444
|
+
|
|
445
|
+
lab = ElidedLabel("a very long application title indeed")
|
|
446
|
+
lab.show()
|
|
447
|
+
wide = lab.sizeHint().width()
|
|
448
|
+
lab.resize(30, 20) # force a hard elide
|
|
449
|
+
assert lab.text().endswith("…")
|
|
450
|
+
assert lab.sizeHint().width() == wide # unchanged by what's painted
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def test_elided_label_minimum_does_not_grow_with_its_text(qapp):
|
|
454
|
+
"""QLabel's default minimumSizeHint tracks the text and would put a floor
|
|
455
|
+
under the whole row — the reason a long name could block the resize."""
|
|
456
|
+
from trackerkeeper.ui_helpers import ElidedLabel
|
|
457
|
+
|
|
458
|
+
short, long = ElidedLabel("hi"), ElidedLabel("x" * 400)
|
|
459
|
+
assert long.minimumSizeHint().width() == short.minimumSizeHint().width()
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_short_age_keeps_the_number(qapp):
|
|
463
|
+
"""The narrow tier rendered the full phrase into a 62px box, which cut the
|
|
464
|
+
NUMBER off and left "hours ago" — the half carrying no information."""
|
|
465
|
+
from datetime import datetime, timedelta, timezone
|
|
466
|
+
|
|
467
|
+
from trackerkeeper.dashboard import humanize_age_short
|
|
468
|
+
|
|
469
|
+
now = datetime(2026, 7, 26, 12, tzinfo=timezone.utc)
|
|
470
|
+
|
|
471
|
+
def ago(**kw):
|
|
472
|
+
return (now - timedelta(**kw)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
473
|
+
|
|
474
|
+
assert humanize_age_short(ago(minutes=30), now) == "30m"
|
|
475
|
+
assert humanize_age_short(ago(hours=13), now) == "13h"
|
|
476
|
+
assert humanize_age_short(ago(days=4), now) == "4d"
|
|
477
|
+
assert humanize_age_short(ago(days=21), now) == "3w"
|
|
478
|
+
assert humanize_age_short(ago(days=90), now) == "3mo"
|
|
479
|
+
assert humanize_age_short(ago(days=800), now) == "2y"
|
|
480
|
+
assert humanize_age_short("", now) == ""
|
|
481
|
+
# every form stays inside the narrow column's budget
|
|
482
|
+
for v in ("30m", "13h", "4d", "3w", "3mo", "2y"):
|
|
483
|
+
assert len(v) <= 3
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
# ── category sections ────────────────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def test_group_header_carries_a_rule(qapp):
|
|
490
|
+
from trackerkeeper.dashboard import GROUP_RULE, _GroupHeader
|
|
491
|
+
|
|
492
|
+
h = _GroupHeader("GAMING", lambda: None)
|
|
493
|
+
assert "border-bottom" in h.styleSheet()
|
|
494
|
+
assert GROUP_RULE in h.styleSheet()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def test_cards_are_indented_under_their_category_but_not_when_cramped(qapp):
|
|
498
|
+
from trackerkeeper.dashboard import GROUP_INDENT, TIER_NARROW, TIER_WIDE
|
|
499
|
+
|
|
500
|
+
dash = _dash(qapp, _grouped_fleet())
|
|
501
|
+
card = dash._card(dash._items[0])
|
|
502
|
+
|
|
503
|
+
dash._tier = TIER_WIDE
|
|
504
|
+
holder = dash._indent(card)
|
|
505
|
+
assert holder is not card # wrapped
|
|
506
|
+
assert holder.layout().contentsMargins().left() == GROUP_INDENT
|
|
507
|
+
|
|
508
|
+
dash._tier = TIER_NARROW
|
|
509
|
+
# at the minimum width every pixel is already spoken for — no indent
|
|
510
|
+
assert dash._indent(dash._card(dash._items[1])) is not None
|
|
511
|
+
assert dash._indent(card) is card
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def test_sections_are_separated_by_a_gap(qapp):
|
|
515
|
+
from trackerkeeper.dashboard import save_collapsed
|
|
516
|
+
|
|
517
|
+
save_collapsed(set())
|
|
518
|
+
try:
|
|
519
|
+
dash = _dash(qapp, _grouped_fleet()) # two categories: Gaming, PC
|
|
520
|
+
dash._grouped = True
|
|
521
|
+
dash._render()
|
|
522
|
+
# header + cards for both groups, plus ONE spacer between them and the
|
|
523
|
+
# trailing stretch — a gap only ever appears BETWEEN sections.
|
|
524
|
+
spacers = sum(1 for i in range(dash._list.count())
|
|
525
|
+
if dash._list.itemAt(i).spacerItem() is not None)
|
|
526
|
+
assert spacers == 2 # the inter-section gap + the trailing stretch
|
|
527
|
+
finally:
|
|
528
|
+
save_collapsed(set())
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def test_an_empty_badge_reserves_no_width(qapp):
|
|
532
|
+
"""The bug that elided the title to "trac…" on a 360px bar: the count's
|
|
533
|
+
width floor was computed once per tier from whatever text was there, so a
|
|
534
|
+
badge that later emptied out kept holding ~59px of the row."""
|
|
535
|
+
dash = _dash(qapp, [catalog.Item(name="A", kind="github", installed="1", latest="1")])
|
|
536
|
+
dash._render()
|
|
537
|
+
assert dash._count.text().strip() in ("", "· all current")
|
|
538
|
+
assert dash._count.minimumWidth() == 0
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def test_the_badge_gets_shorter_as_the_bar_tightens(qapp):
|
|
542
|
+
from trackerkeeper.dashboard import TIER_MEDIUM, TIER_NARROW, TIER_WIDE
|
|
543
|
+
|
|
544
|
+
dash = _dash(qapp, [catalog.Item(name="A", kind="github", installed="1", latest="2")])
|
|
545
|
+
seen = {}
|
|
546
|
+
for tier in (TIER_NARROW, TIER_MEDIUM, TIER_WIDE):
|
|
547
|
+
dash._tier = tier
|
|
548
|
+
dash._render()
|
|
549
|
+
seen[tier] = dash._count.text()
|
|
550
|
+
assert seen[TIER_NARROW] == "· 1" # just the number
|
|
551
|
+
assert "new" in seen[TIER_MEDIUM]
|
|
552
|
+
assert "available" in seen[TIER_WIDE]
|
|
553
|
+
# strictly shorter as it narrows — the app's name gets the difference
|
|
554
|
+
assert (len(seen[TIER_NARROW]) < len(seen[TIER_MEDIUM])
|
|
555
|
+
< len(seen[TIER_WIDE]))
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def test_the_title_is_not_starved_by_the_bar(qapp):
|
|
559
|
+
"""The bug this guards: an empty badge reserved ~59px and the title elided
|
|
560
|
+
beside it. Asserts the MECHANISM, not a pixel outcome — how many characters
|
|
561
|
+
fit at a given width depends on the system font, and hard-coding this
|
|
562
|
+
machine's metrics fails on any platform with a wider one (it did: Windows
|
|
563
|
+
rendered "tracker…" where Linux fit the lot).
|
|
564
|
+
|
|
565
|
+
At a comfortable width the title must get everything it asks for."""
|
|
566
|
+
from trackerkeeper.dashboard import Dashboard
|
|
567
|
+
from trackerkeeper.window import AppWindow
|
|
568
|
+
|
|
569
|
+
catalog.save([catalog.Item(name=f"item {n}", kind="github", installed="1",
|
|
570
|
+
latest="1", latest_date="2026-07-01") for n in range(6)])
|
|
571
|
+
win = AppWindow(title="tracker keeper")
|
|
572
|
+
win.set_content(Dashboard(win))
|
|
573
|
+
win.resize(900, 400)
|
|
574
|
+
win.show()
|
|
575
|
+
for _ in range(8):
|
|
576
|
+
qapp.processEvents()
|
|
577
|
+
title = win.top_bar.title
|
|
578
|
+
assert title.width() >= title.sizeHint().width() # nothing is starving it
|
|
579
|
+
assert title.text() == title.full_text() # …so nothing is elided
|
|
580
|
+
win.close()
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def test_the_title_shrinks_its_type_as_the_bar_tightens(qapp):
|
|
584
|
+
"""The other half of fitting the name: step down the type ladder rather than
|
|
585
|
+
elide. Checked via the scale API so it holds whatever the system font is."""
|
|
586
|
+
from trackerkeeper.dashboard import TIER_MEDIUM, TIER_NARROW, TIER_WIDE, Dashboard
|
|
587
|
+
from trackerkeeper.top_bar import TopBar
|
|
588
|
+
from trackerkeeper.window import AppWindow
|
|
589
|
+
|
|
590
|
+
catalog.save([catalog.Item(name="A", kind="github", installed="1", latest="1")])
|
|
591
|
+
win = AppWindow(title="tracker keeper")
|
|
592
|
+
dash = Dashboard(win)
|
|
593
|
+
win.set_content(dash)
|
|
594
|
+
steps = {}
|
|
595
|
+
for tier in (TIER_WIDE, TIER_MEDIUM, TIER_NARROW):
|
|
596
|
+
dash._tier = tier
|
|
597
|
+
dash._apply_tier()
|
|
598
|
+
steps[tier] = getattr(win.top_bar, "_title_step", 0)
|
|
599
|
+
assert steps[TIER_WIDE] < steps[TIER_MEDIUM] < steps[TIER_NARROW]
|
|
600
|
+
assert steps[TIER_NARROW] <= len(TopBar.TITLE_STEPS) - 1
|
|
601
|
+
win.close()
|
|
@@ -591,3 +591,84 @@ def test_flatpak_survives_a_junk_timestamp():
|
|
|
591
591
|
def test_flatpak_rejects_a_ref_that_is_not_an_app_id():
|
|
592
592
|
item = catalog.Item(name="X", kind="flatpak", ref="owner/repo")
|
|
593
593
|
assert sources.check(item, _fake({})) is None
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
# ── release notes ────────────────────────────────────────────────────────────
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def test_plain_notes_flattens_steam_bbcode():
|
|
600
|
+
raw = ("[b]BUG FIXES:[/b][list][*]Fixed a crash[*]Fixed plurals[/list]"
|
|
601
|
+
"[url=https://x]see more[/url]")
|
|
602
|
+
out = sources.plain_notes(raw)
|
|
603
|
+
assert "[" not in out and "]" not in out
|
|
604
|
+
assert "BUG FIXES:" in out
|
|
605
|
+
assert out.count("•") == 2
|
|
606
|
+
assert "see more" in out # link TEXT survives, the markup doesn't
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def test_plain_notes_flattens_html_and_unescapes():
|
|
610
|
+
raw = "<p>Fixed & improved</p><ul><li>one</li><li>two</li></ul>"
|
|
611
|
+
out = sources.plain_notes(raw)
|
|
612
|
+
assert "<" not in out
|
|
613
|
+
assert "Fixed & improved" in out
|
|
614
|
+
assert out.count("•") == 2
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def test_plain_notes_drops_steam_image_macros():
|
|
618
|
+
raw = "{STEAM_CLAN_IMAGE}/44971832/abc123.png Real content here"
|
|
619
|
+
assert sources.plain_notes(raw).strip() == "Real content here"
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def test_plain_notes_collapses_blank_lines_and_trims():
|
|
623
|
+
assert sources.plain_notes("a\n\n\n\n\nb \n") == "a\n\nb"
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def test_plain_notes_truncates_with_a_pointer_to_the_changelog():
|
|
627
|
+
out = sources.plain_notes("x" * 9000)
|
|
628
|
+
assert len(out) < 9000
|
|
629
|
+
assert "truncated" in out
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def test_plain_notes_of_nothing_is_empty():
|
|
633
|
+
assert sources.plain_notes("") == ""
|
|
634
|
+
assert sources.plain_notes(None) == ""
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def test_github_captures_the_release_body():
|
|
638
|
+
item = catalog.Item(name="g", kind="github", ref="a/b")
|
|
639
|
+
http = _fake({"releases/latest": {"tag_name": "v2", "body": "## Fixed\r\n- a thing"}})
|
|
640
|
+
assert "Fixed" in sources.check(item, http).notes
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def test_appstore_captures_release_notes():
|
|
644
|
+
item = catalog.Item(name="a", kind="appstore", ref="123")
|
|
645
|
+
http = _fake({"itunes.apple.com": {"results": [
|
|
646
|
+
{"version": "3.5", "releaseNotes": "• Faster startup"}]}})
|
|
647
|
+
assert sources.check(item, http).notes == "• Faster startup"
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def test_flatpak_captures_the_release_description():
|
|
651
|
+
item = catalog.Item(name="f", kind="flatpak", ref="org.x.Y")
|
|
652
|
+
http = _fake({"flathub.org": {"releases": [
|
|
653
|
+
{"version": "1.0", "timestamp": "1767225600", "type": "stable",
|
|
654
|
+
"description": "<p>A hotfix</p>"}]}})
|
|
655
|
+
assert sources.check(item, http).notes == "A hotfix"
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def test_rss_prefers_the_full_body_over_the_teaser():
|
|
659
|
+
"""content:encoded is the whole post; description is usually a blurb."""
|
|
660
|
+
feed = ("<rss><channel><item><title>Release 2.0</title><link>https://x/2</link>"
|
|
661
|
+
"<description>short blurb</description>"
|
|
662
|
+
"<content:encoded>the <b>full</b> post</content:encoded>"
|
|
663
|
+
"</item></channel></rss>")
|
|
664
|
+
res = sources.check(_item(ref="https://x/feed"),
|
|
665
|
+
http=_no_json, http_text=lambda u: feed)
|
|
666
|
+
assert "full" in res.notes and "blurb" not in res.notes
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def test_a_source_with_no_notes_is_simply_empty():
|
|
670
|
+
item = catalog.Item(name="k", kind="arch", ref="plasma-desktop")
|
|
671
|
+
http = _fake({"archlinux.org": {"results": [
|
|
672
|
+
{"pkgname": "plasma-desktop", "pkgver": "6.7.3", "pkgrel": "1",
|
|
673
|
+
"repo": "extra", "arch": "x86_64", "last_update": "2026-07-15T00:00:00Z"}]}})
|
|
674
|
+
assert sources.check(item, http).notes == ""
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.1.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
21
|
+
__version__ = version = '0.1.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g2f4ba77a9'
|
|
@@ -445,11 +445,23 @@ def run_app(content_factory, *, identity=None, single_instance=True) -> int:
|
|
|
445
445
|
return app.exec()
|
|
446
446
|
|
|
447
447
|
|
|
448
|
-
def
|
|
449
|
-
"""The
|
|
448
|
+
def content_factory():
|
|
449
|
+
"""The factory :func:`main` hands to :func:`run_app` — the app's REAL first
|
|
450
|
+
screen, exposed as a seam so tooling can build it without booting.
|
|
451
|
+
|
|
452
|
+
The rig's visual baseline is the caller that matters: it used to grab
|
|
453
|
+
``_placeholder()`` directly, so a loaf's goldens guarded dough's "your app
|
|
454
|
+
starts here" screen — a surface its users never see — and would have gone on
|
|
455
|
+
passing through any dashboard regression. Reading the factory from here means
|
|
456
|
+
the goldens track whatever the app actually shows."""
|
|
450
457
|
from trackerkeeper.dashboard import build_content
|
|
451
458
|
|
|
452
|
-
|
|
459
|
+
return build_content
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def main() -> None:
|
|
463
|
+
"""The default entry: boot tracker keeper with the update dashboard."""
|
|
464
|
+
sys.exit(run_app(content_factory()))
|
|
453
465
|
|
|
454
466
|
|
|
455
467
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<!-- tracker keeper — a refresh wheel closing on a check.
|
|
3
|
+
The ring is the update cycle; the check is "you're current".
|
|
4
|
+
|
|
5
|
+
The gap and arrowhead sit BOTTOM-RIGHT: the check runs lower-left to
|
|
6
|
+
upper-right, so that quadrant is the one area it never crosses, and the
|
|
7
|
+
two elements never crowd each other. Every version that grew the check
|
|
8
|
+
ran its tip into the ring stroke instead.
|
|
9
|
+
|
|
10
|
+
ONE accent, ring and check at the same weight — an earlier two-tone draft
|
|
11
|
+
put a near-white check on the ring, which vanished completely on light
|
|
12
|
+
backgrounds.
|
|
13
|
+
|
|
14
|
+
This file is the single brand source: the .deb, AppImage, MSIX, macOS
|
|
15
|
+
iconset, Windows .ico and ui_helpers.make_app_icon() all derive from it. -->
|
|
16
|
+
<g transform="rotate(90 32 32)">
|
|
17
|
+
<path d="M49.7,14.3 A25,25 0 1,0 57,32" fill="none" stroke="#2fbe8a"
|
|
18
|
+
stroke-width="7" stroke-linecap="round"/>
|
|
19
|
+
<path d="M54,6 L59,25 L40,21 Z" fill="#2fbe8a"/>
|
|
20
|
+
</g>
|
|
21
|
+
<path d="M19,33 l10,10 l19,-21" fill="none" stroke="#2fbe8a" stroke-width="7"
|
|
22
|
+
stroke-linecap="round" stroke-linejoin="round"/>
|
|
23
|
+
</svg>
|