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.
Files changed (207) hide show
  1. {trackerkeeper-0.1.1/trackerkeeper.egg-info → trackerkeeper-0.1.2}/PKG-INFO +1 -1
  2. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/CHANGELOG.md +73 -0
  3. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/dough-sync.toml +9 -1
  4. trackerkeeper-0.1.2/tests/baselines/settings.png +0 -0
  5. trackerkeeper-0.1.2/tests/baselines/window.png +0 -0
  6. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_dashboard.py +261 -0
  7. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_sources.py +81 -0
  8. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/_version.py +3 -3
  9. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/app.py +15 -3
  10. trackerkeeper-0.1.2/trackerkeeper/assets/trackerkeeper.svg +23 -0
  11. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/breadboard.py +749 -168
  12. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/catalog.py +1 -0
  13. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/color_tokens.py +2 -2
  14. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/dashboard.py +292 -53
  15. trackerkeeper-0.1.2/trackerkeeper/detail_dialog.py +157 -0
  16. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/icons.py +1 -1
  17. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/noborder/_kwin.py +46 -8
  18. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/rig.py +11 -1
  19. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/scaffold.py +43 -6
  20. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/settings.py +1 -1
  21. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/sources.py +69 -1
  22. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/terminal.py +157 -13
  23. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/theme.py +10 -7
  24. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/top_bar.py +33 -5
  25. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/ui_helpers.py +91 -6
  26. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper-breadboard.toml +27 -7
  27. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2/trackerkeeper.egg-info}/PKG-INFO +1 -1
  28. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/SOURCES.txt +1 -0
  29. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/scm_file_list.json +1 -0
  30. trackerkeeper-0.1.2/trackerkeeper.egg-info/scm_version.json +8 -0
  31. trackerkeeper-0.1.1/tests/baselines/settings.png +0 -0
  32. trackerkeeper-0.1.1/tests/baselines/window.png +0 -0
  33. trackerkeeper-0.1.1/trackerkeeper/assets/trackerkeeper.svg +0 -12
  34. trackerkeeper-0.1.1/trackerkeeper.egg-info/scm_version.json +0 -8
  35. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.claude/settings.local.json +0 -0
  36. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.gitattributes +0 -0
  37. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/release-checklist-template.md +0 -0
  38. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/aur.yml +0 -0
  39. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/ci.yml +0 -0
  40. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/macos.yml +0 -0
  41. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/pypi-publish.yml +0 -0
  42. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/release-checklist.yml +0 -0
  43. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/release.yml +0 -0
  44. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.github/workflows/winget.yml +0 -0
  45. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/.gitignore +0 -0
  46. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/AGENTS.md +0 -0
  47. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/CLAUDE.md +0 -0
  48. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/LICENSE +0 -0
  49. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/README.md +0 -0
  50. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/ACCESSIBILITY.md +0 -0
  51. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/BACKPORT.md +0 -0
  52. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/BAKING.md +0 -0
  53. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/DELIVERY.md +0 -0
  54. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/DESIGN.md +0 -0
  55. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/MACOS.md +0 -0
  56. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/PHILOSOPHY.md +0 -0
  57. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/RELEASING.md +0 -0
  58. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/TEST_BRIDGE.md +0 -0
  59. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/TODO.md +0 -0
  60. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/TRANSLATING.md +0 -0
  61. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/docs/WIND-DOWN.md +0 -0
  62. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/appimage/build_appimage.sh +0 -0
  63. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/appimage/smoke_test_appimage.sh +0 -0
  64. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/aur/PKGBUILD +0 -0
  65. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/deb/build_deb.sh +0 -0
  66. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/deb/smoke_test_deb.sh +0 -0
  67. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/io.github.wolfgangwarehaus.trackerkeeper.desktop +0 -0
  68. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/io.github.wolfgangwarehaus.trackerkeeper.metainfo.xml +0 -0
  69. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/entitlements.plist +0 -0
  70. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/mas/entitlements.child.plist +0 -0
  71. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/mas/entitlements.mas.plist +0 -0
  72. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/mas/scan_symbols.sh +0 -0
  73. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/mas/sign_mas.sh +0 -0
  74. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/macos/trackerkeeper.rb +0 -0
  75. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/msix/AppxManifest.xml +0 -0
  76. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/msix/STORE-SUBMISSION.md +0 -0
  77. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/msix/make-assets.sh +0 -0
  78. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/pyinstaller/launch.py +0 -0
  79. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/pyinstaller/trackerkeeper.spec +0 -0
  80. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/appimage/build_appimage.sh.j2 +0 -0
  81. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/appimage/smoke_test_appimage.sh.j2 +0 -0
  82. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/aur/PKGBUILD.j2 +0 -0
  83. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/deb/build_deb.sh.j2 +0 -0
  84. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/deb/smoke_test_deb.sh.j2 +0 -0
  85. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/entitlements.plist.j2 +0 -0
  86. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/entitlements.child.plist.j2 +0 -0
  87. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/entitlements.mas.plist.j2 +0 -0
  88. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/scan_symbols.sh.j2 +0 -0
  89. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/mas/sign_mas.sh.j2 +0 -0
  90. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/macos/{{app_slug}}.rb.j2 +0 -0
  91. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/msix/AppxManifest.xml.j2 +0 -0
  92. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/msix/STORE-SUBMISSION.md.j2 +0 -0
  93. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/msix/make-assets.sh.j2 +0 -0
  94. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/pyinstaller/launch.py.j2 +0 -0
  95. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/pyinstaller/{{app_slug}}.spec.j2 +0 -0
  96. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/windows/version_info.txt.j2 +0 -0
  97. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/windows/{{app_slug}}.iss.j2 +0 -0
  98. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/{{app_id_base}}.desktop.j2 +0 -0
  99. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/templates/{{app_id_base}}.metainfo.xml.j2 +0 -0
  100. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/windows/trackerkeeper.iss +0 -0
  101. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/packaging/windows/version_info.txt +0 -0
  102. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/pyproject.toml +0 -0
  103. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/setup.cfg +0 -0
  104. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/conftest.py +0 -0
  105. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_a11y.py +0 -0
  106. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_app_wiring.py +0 -0
  107. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_bake.py +0 -0
  108. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_blur_kwin.py +0 -0
  109. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_boot.py +0 -0
  110. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_breadboard.py +0 -0
  111. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_bridge.py +0 -0
  112. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_bus.py +0 -0
  113. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_catalog.py +0 -0
  114. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_credentials.py +0 -0
  115. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_deliver.py +0 -0
  116. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_diagnostics.py +0 -0
  117. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_drag_repaint.py +0 -0
  118. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_i18n.py +0 -0
  119. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_i18n_fmt.py +0 -0
  120. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_i18n_plurals.py +0 -0
  121. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_identity.py +0 -0
  122. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_imports.py +0 -0
  123. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_log.py +0 -0
  124. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_metadata.py +0 -0
  125. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_noborder.py +0 -0
  126. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_power_runtime.py +0 -0
  127. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_public_api.py +0 -0
  128. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_relative_time.py +0 -0
  129. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_rig.py +0 -0
  130. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_rtl_smoke.py +0 -0
  131. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_settings_isolation.py +0 -0
  132. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_settings_migration.py +0 -0
  133. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_single_instance_forwarding.py +0 -0
  134. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_tray.py +0 -0
  135. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_ui_helpers_widgets.py +0 -0
  136. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_updates.py +0 -0
  137. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/tests/test_window_seams.py +0 -0
  138. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/__init__.py +0 -0
  139. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/__main__.py +0 -0
  140. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/assets/__init__.py +0 -0
  141. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/async_io.py +0 -0
  142. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/__init__.py +0 -0
  143. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_linux.py +0 -0
  144. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_macos.py +0 -0
  145. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_msix.py +0 -0
  146. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_unsupported.py +0 -0
  147. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/autostart/_windows.py +0 -0
  148. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/bake.py +0 -0
  149. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/__init__.py +0 -0
  150. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_dwm.py +0 -0
  151. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_faux_frost.py +0 -0
  152. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_kwin.py +0 -0
  153. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_macos.py +0 -0
  154. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/blur/_unsupported.py +0 -0
  155. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/boot_timing.py +0 -0
  156. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/bus.py +0 -0
  157. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/credentials.py +0 -0
  158. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/custom_tooltip.py +0 -0
  159. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/deliver.py +0 -0
  160. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/design_tokens.py +0 -0
  161. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/diagnostics.py +0 -0
  162. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/__init__.py +0 -0
  163. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/_kwin.py +0 -0
  164. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/_unsupported.py +0 -0
  165. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/effect/dragrepaint/contents/code/main.js +0 -0
  166. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/drag_repaint/effect/dragrepaint/metadata.json +0 -0
  167. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/frosted_dialog.py +0 -0
  168. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/i18n/__init__.py +0 -0
  169. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/i18n/fmt.py +0 -0
  170. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/icon_button.py +0 -0
  171. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/identity.py +0 -0
  172. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/item_dialog.py +0 -0
  173. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/kde_titlebar.py +0 -0
  174. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/keyboard_focus.py +0 -0
  175. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/log.py +0 -0
  176. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/macos_menubar.py +0 -0
  177. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/macos_window.py +0 -0
  178. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/metadata.py +0 -0
  179. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/noborder/__init__.py +0 -0
  180. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/noborder/_unsupported.py +0 -0
  181. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/__init__.py +0 -0
  182. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_linux.py +0 -0
  183. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_macos.py +0 -0
  184. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_unsupported.py +0 -0
  185. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/notifications/_windows.py +0 -0
  186. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/platform_compat.py +0 -0
  187. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/power/__init__.py +0 -0
  188. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/power/_linux.py +0 -0
  189. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/power/_unsupported.py +0 -0
  190. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/power/_windows.py +0 -0
  191. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/selector.py +0 -0
  192. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/settings_dialog.py +0 -0
  193. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/settings_migration.py +0 -0
  194. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/single_instance.py +0 -0
  195. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/smooth_scroll.py +0 -0
  196. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/system_accent.py +0 -0
  197. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/test_bridge.py +0 -0
  198. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/tray.py +0 -0
  199. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/update_chip.py +0 -0
  200. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/updates.py +0 -0
  201. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/win_frameless.py +0 -0
  202. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/window.py +0 -0
  203. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper/windows_shortcut.py +0 -0
  204. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/dependency_links.txt +0 -0
  205. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/entry_points.txt +0 -0
  206. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/requires.txt +0 -0
  207. {trackerkeeper-0.1.1 → trackerkeeper-0.1.2}/trackerkeeper.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trackerkeeper
3
- Version: 0.1.1
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
@@ -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 = "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
  ]
@@ -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 &amp; 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 &lt;b&gt;full&lt;/b&gt; 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.1'
22
- __version_tuple__ = version_tuple = (0, 1, 1)
21
+ __version__ = version = '0.1.2'
22
+ __version_tuple__ = version_tuple = (0, 1, 2)
23
23
 
24
- __commit_id__ = commit_id = 'g49c3293d8'
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 main() -> None:
449
- """The default entry: boot tracker keeper with the update dashboard."""
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
- sys.exit(run_app(build_content))
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>