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.
Files changed (213) hide show
  1. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/release-checklist-template.md +1 -1
  2. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/pypi-publish.yml +2 -2
  3. {trackerkeeper-0.1.0/trackerkeeper.egg-info → trackerkeeper-0.1.2}/PKG-INFO +1 -1
  4. trackerkeeper-0.1.2/docs/CHANGELOG.md +202 -0
  5. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/RELEASING.md +20 -4
  6. trackerkeeper-0.1.2/docs/TODO.md +155 -0
  7. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/dough-sync.toml +9 -1
  8. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/pyproject.toml +6 -4
  9. trackerkeeper-0.1.2/tests/baselines/settings.png +0 -0
  10. trackerkeeper-0.1.2/tests/baselines/window.png +0 -0
  11. trackerkeeper-0.1.2/tests/conftest.py +121 -0
  12. trackerkeeper-0.1.2/tests/test_dashboard.py +601 -0
  13. trackerkeeper-0.1.2/tests/test_settings_isolation.py +116 -0
  14. trackerkeeper-0.1.2/tests/test_sources.py +674 -0
  15. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_tray.py +27 -0
  16. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/_version.py +3 -3
  17. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/app.py +27 -4
  18. trackerkeeper-0.1.2/trackerkeeper/assets/trackerkeeper.svg +23 -0
  19. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/breadboard.py +749 -168
  20. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/bus.py +5 -0
  21. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/catalog.py +12 -1
  22. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/color_tokens.py +2 -2
  23. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/dashboard.py +446 -66
  24. trackerkeeper-0.1.2/trackerkeeper/detail_dialog.py +157 -0
  25. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/icons.py +1 -1
  26. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/item_dialog.py +10 -0
  27. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/noborder/_kwin.py +46 -8
  28. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/rig.py +11 -1
  29. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/scaffold.py +43 -6
  30. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/settings.py +16 -2
  31. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/settings_dialog.py +79 -0
  32. trackerkeeper-0.1.2/trackerkeeper/sources.py +621 -0
  33. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/terminal.py +157 -13
  34. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/theme.py +10 -7
  35. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/top_bar.py +33 -5
  36. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/tray.py +32 -6
  37. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/ui_helpers.py +91 -6
  38. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper-breadboard.toml +91 -15
  39. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2/trackerkeeper.egg-info}/PKG-INFO +1 -1
  40. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/SOURCES.txt +2 -0
  41. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/scm_file_list.json +2 -0
  42. trackerkeeper-0.1.2/trackerkeeper.egg-info/scm_version.json +8 -0
  43. trackerkeeper-0.1.0/docs/CHANGELOG.md +0 -55
  44. trackerkeeper-0.1.0/docs/TODO.md +0 -102
  45. trackerkeeper-0.1.0/tests/baselines/settings.png +0 -0
  46. trackerkeeper-0.1.0/tests/baselines/window.png +0 -0
  47. trackerkeeper-0.1.0/tests/conftest.py +0 -61
  48. trackerkeeper-0.1.0/tests/test_dashboard.py +0 -199
  49. trackerkeeper-0.1.0/tests/test_sources.py +0 -230
  50. trackerkeeper-0.1.0/trackerkeeper/assets/trackerkeeper.svg +0 -12
  51. trackerkeeper-0.1.0/trackerkeeper/sources.py +0 -318
  52. trackerkeeper-0.1.0/trackerkeeper.egg-info/scm_version.json +0 -8
  53. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.claude/settings.local.json +0 -0
  54. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.gitattributes +0 -0
  55. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/aur.yml +0 -0
  56. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/ci.yml +0 -0
  57. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/macos.yml +0 -0
  58. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/release-checklist.yml +0 -0
  59. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/release.yml +0 -0
  60. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.github/workflows/winget.yml +0 -0
  61. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/.gitignore +0 -0
  62. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/AGENTS.md +0 -0
  63. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/CLAUDE.md +0 -0
  64. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/LICENSE +0 -0
  65. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/README.md +0 -0
  66. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/ACCESSIBILITY.md +0 -0
  67. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/BACKPORT.md +0 -0
  68. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/BAKING.md +0 -0
  69. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/DELIVERY.md +0 -0
  70. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/DESIGN.md +0 -0
  71. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/MACOS.md +0 -0
  72. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/PHILOSOPHY.md +0 -0
  73. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/TEST_BRIDGE.md +0 -0
  74. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/TRANSLATING.md +0 -0
  75. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/docs/WIND-DOWN.md +0 -0
  76. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/appimage/build_appimage.sh +0 -0
  77. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/appimage/smoke_test_appimage.sh +0 -0
  78. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/aur/PKGBUILD +0 -0
  79. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/deb/build_deb.sh +0 -0
  80. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/deb/smoke_test_deb.sh +0 -0
  81. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/io.github.wolfgangwarehaus.trackerkeeper.desktop +0 -0
  82. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/io.github.wolfgangwarehaus.trackerkeeper.metainfo.xml +0 -0
  83. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/entitlements.plist +0 -0
  84. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/mas/entitlements.child.plist +0 -0
  85. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/mas/entitlements.mas.plist +0 -0
  86. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/mas/scan_symbols.sh +0 -0
  87. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/mas/sign_mas.sh +0 -0
  88. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/macos/trackerkeeper.rb +0 -0
  89. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/msix/AppxManifest.xml +0 -0
  90. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/msix/STORE-SUBMISSION.md +0 -0
  91. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/msix/make-assets.sh +0 -0
  92. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/pyinstaller/launch.py +0 -0
  93. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/pyinstaller/trackerkeeper.spec +0 -0
  94. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/appimage/build_appimage.sh.j2 +0 -0
  95. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/appimage/smoke_test_appimage.sh.j2 +0 -0
  96. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/aur/PKGBUILD.j2 +0 -0
  97. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/deb/build_deb.sh.j2 +0 -0
  98. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/deb/smoke_test_deb.sh.j2 +0 -0
  99. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/entitlements.plist.j2 +0 -0
  100. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/entitlements.child.plist.j2 +0 -0
  101. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/entitlements.mas.plist.j2 +0 -0
  102. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/scan_symbols.sh.j2 +0 -0
  103. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/sign_mas.sh.j2 +0 -0
  104. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/macos/{{app_slug}}.rb.j2 +0 -0
  105. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/msix/AppxManifest.xml.j2 +0 -0
  106. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/msix/STORE-SUBMISSION.md.j2 +0 -0
  107. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/msix/make-assets.sh.j2 +0 -0
  108. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/pyinstaller/launch.py.j2 +0 -0
  109. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/pyinstaller/{{app_slug}}.spec.j2 +0 -0
  110. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/windows/version_info.txt.j2 +0 -0
  111. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/windows/{{app_slug}}.iss.j2 +0 -0
  112. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/{{app_id_base}}.desktop.j2 +0 -0
  113. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/templates/{{app_id_base}}.metainfo.xml.j2 +0 -0
  114. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/windows/trackerkeeper.iss +0 -0
  115. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/packaging/windows/version_info.txt +0 -0
  116. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/setup.cfg +0 -0
  117. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_a11y.py +0 -0
  118. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_app_wiring.py +0 -0
  119. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_bake.py +0 -0
  120. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_blur_kwin.py +0 -0
  121. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_boot.py +0 -0
  122. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_breadboard.py +0 -0
  123. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_bridge.py +0 -0
  124. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_bus.py +0 -0
  125. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_catalog.py +0 -0
  126. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_credentials.py +0 -0
  127. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_deliver.py +0 -0
  128. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_diagnostics.py +0 -0
  129. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_drag_repaint.py +0 -0
  130. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_i18n.py +0 -0
  131. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_i18n_fmt.py +0 -0
  132. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_i18n_plurals.py +0 -0
  133. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_identity.py +0 -0
  134. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_imports.py +0 -0
  135. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_log.py +0 -0
  136. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_metadata.py +0 -0
  137. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_noborder.py +0 -0
  138. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_power_runtime.py +0 -0
  139. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_public_api.py +0 -0
  140. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_relative_time.py +0 -0
  141. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_rig.py +0 -0
  142. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_rtl_smoke.py +0 -0
  143. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_settings_migration.py +0 -0
  144. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_single_instance_forwarding.py +0 -0
  145. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_ui_helpers_widgets.py +0 -0
  146. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_updates.py +0 -0
  147. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/tests/test_window_seams.py +0 -0
  148. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/__init__.py +0 -0
  149. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/__main__.py +0 -0
  150. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/assets/__init__.py +0 -0
  151. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/async_io.py +0 -0
  152. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/__init__.py +0 -0
  153. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_linux.py +0 -0
  154. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_macos.py +0 -0
  155. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_msix.py +0 -0
  156. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_unsupported.py +0 -0
  157. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_windows.py +0 -0
  158. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/bake.py +0 -0
  159. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/__init__.py +0 -0
  160. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_dwm.py +0 -0
  161. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_faux_frost.py +0 -0
  162. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_kwin.py +0 -0
  163. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_macos.py +0 -0
  164. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/blur/_unsupported.py +0 -0
  165. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/boot_timing.py +0 -0
  166. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/credentials.py +0 -0
  167. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/custom_tooltip.py +0 -0
  168. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/deliver.py +0 -0
  169. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/design_tokens.py +0 -0
  170. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/diagnostics.py +0 -0
  171. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/__init__.py +0 -0
  172. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/_kwin.py +0 -0
  173. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/_unsupported.py +0 -0
  174. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/effect/dragrepaint/contents/code/main.js +0 -0
  175. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/effect/dragrepaint/metadata.json +0 -0
  176. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/frosted_dialog.py +0 -0
  177. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/i18n/__init__.py +0 -0
  178. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/i18n/fmt.py +0 -0
  179. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/icon_button.py +0 -0
  180. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/identity.py +0 -0
  181. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/kde_titlebar.py +0 -0
  182. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/keyboard_focus.py +0 -0
  183. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/log.py +0 -0
  184. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/macos_menubar.py +0 -0
  185. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/macos_window.py +0 -0
  186. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/metadata.py +0 -0
  187. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/noborder/__init__.py +0 -0
  188. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/noborder/_unsupported.py +0 -0
  189. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/__init__.py +0 -0
  190. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_linux.py +0 -0
  191. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_macos.py +0 -0
  192. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_unsupported.py +0 -0
  193. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_windows.py +0 -0
  194. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/platform_compat.py +0 -0
  195. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/power/__init__.py +0 -0
  196. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/power/_linux.py +0 -0
  197. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/power/_unsupported.py +0 -0
  198. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/power/_windows.py +0 -0
  199. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/selector.py +0 -0
  200. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/settings_migration.py +0 -0
  201. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/single_instance.py +0 -0
  202. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/smooth_scroll.py +0 -0
  203. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/system_accent.py +0 -0
  204. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/test_bridge.py +0 -0
  205. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/update_chip.py +0 -0
  206. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/updates.py +0 -0
  207. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/win_frameless.py +0 -0
  208. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/window.py +0 -0
  209. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper/windows_shortcut.py +0 -0
  210. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/dependency_links.txt +0 -0
  211. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/entry_points.txt +0 -0
  212. {trackerkeeper-0.1.0 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/requires.txt +0 -0
  213. {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-base/${V}/
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-base
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-base/
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trackerkeeper
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: One dashboard for software & firmware updates across everything you own
5
5
  Author-email: august <augustvontrips@gmail.com>
6
6
  License: GPL-2.0-or-later
@@ -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** — add a pending publisher at
36
- <https://pypi.org/manage/account/publishing/> matching `pypi-publish.yml`
37
- (project `trackerkeeper-base`, owner `wolfgangwarehaus`, repo `trackerkeeper`, workflow
38
- `pypi-publish.yml`, environment `pypi`). See the header of that workflow.
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 = "36b09a3"
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 "trackerkeeper" is squatted on PyPI (a 2015 third-party
15
- # package), so pip's name is trackerkeeper-base while the import package stays `trackerkeeper`
16
- # (distribution name import name, like beautifulsoup4/bs4). `trackerkeeper new` pins
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
@@ -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)