pyobs-gui 2.7.0__tar.gz → 2.8.0__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 (216) hide show
  1. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/CHANGELOG.md +12 -0
  2. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/PKG-INFO +2 -2
  3. pyobs_gui-2.8.0/pyobs_gui/appicon.py +19 -0
  4. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/base.py +4 -9
  5. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/gui.py +2 -0
  6. pyobs_gui-2.8.0/pyobs_gui/icons/README.md +13 -0
  7. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui-128.png +0 -0
  8. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui-16.png +0 -0
  9. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui-24.png +0 -0
  10. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui-256.png +0 -0
  11. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui-32.png +0 -0
  12. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui-48.png +0 -0
  13. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui-512.png +0 -0
  14. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui-64.png +0 -0
  15. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui.ico +0 -0
  16. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui.png +0 -0
  17. pyobs_gui-2.8.0/pyobs_gui/icons/pyobs-gui.svg +13 -0
  18. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/loginwindow.py +2 -0
  19. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/mainwindow.py +2 -0
  20. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/notifier_backend.py +3 -2
  21. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/settingsdialog.py +2 -0
  22. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/windows_identity.py +26 -1
  23. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyproject.toml +5 -2
  24. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pysidedeploy.spec +2 -2
  25. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/plans/2026-10-04-settings-dialog.md +1 -1
  26. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_missing_vfs_root.py +5 -4
  27. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_notifier_backend.py +11 -0
  28. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_windows_identity.py +53 -1
  29. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/uv.lock +159 -118
  30. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.claude/skills/verify/SKILL.md +0 -0
  31. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.github/dependabot.yml +0 -0
  32. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.github/workflows/dependabot-automerge.yml +0 -0
  33. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.github/workflows/pypi.yml +0 -0
  34. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.github/workflows/pyrefly.yml +0 -0
  35. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.github/workflows/ruff.yml +0 -0
  36. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.gitignore +0 -0
  37. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.pre-commit-config.yaml +0 -0
  38. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/.readthedocs.yml +0 -0
  39. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/CLAUDE.md +0 -0
  40. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/CONTRIBUTING.md +0 -0
  41. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/LICENSE +0 -0
  42. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/README.md +0 -0
  43. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/bin/mypyuic5 +0 -0
  44. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/docs/Makefile +0 -0
  45. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/docs/make.bat +0 -0
  46. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/docs/requirements.txt +0 -0
  47. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/docs/source/_static/pyobs.gif +0 -0
  48. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/docs/source/conf.py +0 -0
  49. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/docs/source/index.rst +0 -0
  50. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/mypy.ini +0 -0
  51. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/__init__.py +0 -0
  52. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/__main__.py +0 -0
  53. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/_nuitka_astropy_patch.py +0 -0
  54. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/accounts.py +0 -0
  55. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/acquisitionwidget.py +0 -0
  56. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/autofocuswidget.py +0 -0
  57. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/autoguidingwidget.py +0 -0
  58. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/camerawidget.py +0 -0
  59. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/commandinputwidget.py +0 -0
  60. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/compassmovewidget.py +0 -0
  61. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/coolingwidget.py +0 -0
  62. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/datadisplaywidget.py +0 -0
  63. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/eventswidget.py +0 -0
  64. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/filterwidget.py +0 -0
  65. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/fitsheaderswidget.py +0 -0
  66. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/focuswidget.py +0 -0
  67. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/guisignals.py +0 -0
  68. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/livestream.py +0 -0
  69. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/login.py +0 -0
  70. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/logmodel.py +0 -0
  71. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/modewidget.py +0 -0
  72. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/modifiedmixin.py +0 -0
  73. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/modulegui.py +0 -0
  74. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/movestack.py +0 -0
  75. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/notificationmanager.py +0 -0
  76. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/notifications.py +0 -0
  77. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/notifier.py +0 -0
  78. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/nowheelfilter.py +0 -0
  79. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/__init__.py +0 -0
  80. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/acquisitionwidget.ui +0 -0
  81. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/acquisitionwidget_ui.py +0 -0
  82. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/autofocuswidget.ui +0 -0
  83. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/autofocuswidget_ui.py +0 -0
  84. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/autoguidingwidget.ui +0 -0
  85. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/autoguidingwidget_ui.py +0 -0
  86. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/camerawidget.ui +0 -0
  87. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/camerawidget_ui.py +0 -0
  88. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/compassmoveplugin.py +0 -0
  89. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/compassmovewidget.py +0 -0
  90. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/compassmovewidget.ui +0 -0
  91. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/compassmovewidget_ui.py +0 -0
  92. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/compile.sh +0 -0
  93. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/coolingwidget.ui +0 -0
  94. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/coolingwidget_ui.py +0 -0
  95. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/datadisplayplugin.py +0 -0
  96. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/datadisplaywidget.ui +0 -0
  97. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/datadisplaywidget_ui.py +0 -0
  98. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/designer.sh +0 -0
  99. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/eventswidget.ui +0 -0
  100. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/eventswidget_ui.py +0 -0
  101. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/filterwidget.ui +0 -0
  102. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/filterwidget_ui.py +0 -0
  103. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/fitsheaderswidget.ui +0 -0
  104. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/fitsheaderswidget_ui.py +0 -0
  105. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/focuswidget.ui +0 -0
  106. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/focuswidget_ui.py +0 -0
  107. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/mainwindow.ui +0 -0
  108. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/mainwindow_ui.py +0 -0
  109. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/modewidget.ui +0 -0
  110. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/modewidget_ui.py +0 -0
  111. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/arrow-alt-circle-down-solid.svg +0 -0
  112. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/arrow-alt-circle-left-solid.svg +0 -0
  113. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/arrow-alt-circle-right-solid.svg +0 -0
  114. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/arrow-alt-circle-up-solid.svg +0 -0
  115. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/arrow-up-right-from-square-solid.svg +0 -0
  116. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/arrows-to-eye-solid.svg +0 -0
  117. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/chart-line-solid.svg +0 -0
  118. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/circle-question-solid.svg +0 -0
  119. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/cloud-sun-solid.svg +0 -0
  120. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/edit-solid.svg +0 -0
  121. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/search-solid.svg +0 -0
  122. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources/undo-solid.svg +0 -0
  123. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources.qrc +0 -0
  124. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/resources_rc.py +0 -0
  125. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/roboticwidget.ui +0 -0
  126. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/roboticwidget_ui.py +0 -0
  127. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/roofwidget.ui +0 -0
  128. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/roofwidget_ui.py +0 -0
  129. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/schedulewidget.ui +0 -0
  130. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/schedulewidget_ui.py +0 -0
  131. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/shellwidget.ui +0 -0
  132. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/shellwidget_ui.py +0 -0
  133. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/spectrographwidget.ui +0 -0
  134. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/spectrographwidget_ui.py +0 -0
  135. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/telescopewidget.ui +0 -0
  136. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/telescopewidget_ui.py +0 -0
  137. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/temperaturesplotwidget.ui +0 -0
  138. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/temperaturesplotwidget_ui.py +0 -0
  139. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/temperatureswidget.ui +0 -0
  140. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/temperatureswidget_ui.py +0 -0
  141. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/videograbwidget.ui +0 -0
  142. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/videograbwidget_ui.py +0 -0
  143. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/videowidget.ui +0 -0
  144. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/videowidget_ui.py +0 -0
  145. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/weatherwidget.ui +0 -0
  146. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/qt/weatherwidget_ui.py +0 -0
  147. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/roboticwidget.py +0 -0
  148. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/roofwidget.py +0 -0
  149. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/schedulewidget.py +0 -0
  150. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/settings.py +0 -0
  151. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/shellwidget.py +0 -0
  152. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/spectrographwidget.py +0 -0
  153. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/statuswidget.py +0 -0
  154. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/structuredconfigwidget.py +0 -0
  155. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/telescopewidget.py +0 -0
  156. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/temperaturesplotwidget.py +0 -0
  157. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/temperatureswidget.py +0 -0
  158. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/utils.py +0 -0
  159. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/videograbwidget.py +0 -0
  160. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/videowidget.py +0 -0
  161. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/watchedlabel.py +0 -0
  162. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/pyobs_gui/weatherwidget.py +0 -0
  163. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-08-20-gui-remote-call-error-handling.md +0 -0
  164. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-08-21-gui-widget-startup-responsiveness.md +0 -0
  165. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-08-28-gui-main-vs-sidebar-widgets.md +0 -0
  166. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-08-28-structuredconfig-widget.md +0 -0
  167. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-08-31-irobotic-widgets.md +0 -0
  168. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-09-01-gui-video-widget-split.md +0 -0
  169. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-09-14-fitswidget-toolbar-overflow.md +0 -0
  170. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-09-14-stacked-widget-scroll-fallback.md +0 -0
  171. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-09-29-live-view-mjpeg-raw.md +0 -0
  172. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-10-04-desktop-notifications.md +0 -0
  173. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-10-04-settings-dialog.md +0 -0
  174. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/2026-10-14-typed-config-choices.md +0 -0
  175. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/index.md +0 -0
  176. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/plans/2026-10-04-desktop-notifications.md +0 -0
  177. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/specs/plans/index.md +0 -0
  178. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/spike_standalone/main.py +0 -0
  179. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/spike_standalone/pysidedeploy.spec +0 -0
  180. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/acquisition.yaml +0 -0
  181. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/autofocus.yaml +0 -0
  182. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/camera.yaml +0 -0
  183. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/full.yaml +0 -0
  184. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/guiding.yaml +0 -0
  185. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/mode.yaml +0 -0
  186. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/robotic.yaml +0 -0
  187. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/roof.yaml +0 -0
  188. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/spectrograph.yaml +0 -0
  189. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/structuredconfig.yaml +0 -0
  190. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/telescope.yaml +0 -0
  191. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/telescope_acl.yaml +0 -0
  192. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/telescope_acl_denied.yaml +0 -0
  193. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/test/video.yaml +0 -0
  194. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/__init__.py +0 -0
  195. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/conftest.py +0 -0
  196. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_accounts.py +0 -0
  197. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_basewidget_discard.py +0 -0
  198. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_camerawidget.py +0 -0
  199. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_gui_notifications.py +0 -0
  200. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_import_order.py +0 -0
  201. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_livestream.py +0 -0
  202. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_loginwindow.py +0 -0
  203. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_mainwindow_startup.py +0 -0
  204. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_modewidget.py +0 -0
  205. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_multiwidget_pages.py +0 -0
  206. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_notificationmanager.py +0 -0
  207. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_notifications.py +0 -0
  208. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_roboticwidget.py +0 -0
  209. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_schedulewidget.py +0 -0
  210. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_settings.py +0 -0
  211. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_settingsdialog.py +0 -0
  212. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_spectrographwidget.py +0 -0
  213. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_structuredconfigwidget.py +0 -0
  214. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_video_refresh.py +0 -0
  215. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_videograbwidget.py +0 -0
  216. {pyobs_gui-2.7.0 → pyobs_gui-2.8.0}/tests/test_videowidget.py +0 -0
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  Entries for releases before this file existed were generated from commit subjects.
9
9
 
10
+ ## [Unreleased]
11
+
12
+ ### Changed
13
+
14
+ - Recognize a missing VFS root by the new `pyobs.vfs.UnknownRootError` instead of matching the
15
+ error message. Requires pyobs-core 2.19.0 or newer.
16
+
17
+ ### Added
18
+
19
+ - Magenta observing-monitor app icon for Qt windows, desktop notifications and
20
+ Windows Start Menu shortcuts; update existing shortcuts without changing their targets (#197).
21
+
10
22
  ## [2.7.0] - 2026-10-15
11
23
 
12
24
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyobs-gui
3
- Version: 2.7.0
3
+ Version: 2.8.0
4
4
  Summary: A remote GUI for pyobs
5
5
  Project-URL: Homepage, https://www.pyobs.org
6
6
  Project-URL: Documentation, https://docs.pyobs.org
@@ -19,7 +19,7 @@ Requires-Dist: keyring<26,>=25.7.0
19
19
  Requires-Dist: matplotlib<4,>=3.10.1
20
20
  Requires-Dist: pandas>=2.2.3
21
21
  Requires-Dist: pydantic<3,>=2.12.5
22
- Requires-Dist: pyobs-core<3,>=2.18.0
22
+ Requires-Dist: pyobs-core<3,>=2.19.0
23
23
  Requires-Dist: pyside6-essentials>=6.10.1
24
24
  Requires-Dist: pywin32>=312; sys_platform == 'win32'
25
25
  Requires-Dist: pyyaml<7,>=6.0.2
@@ -0,0 +1,19 @@
1
+ """Shared app artwork as real files for Qt, notifications and Windows shortcuts."""
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING, Literal
5
+
6
+ if TYPE_CHECKING:
7
+ from PySide6.QtGui import QIcon
8
+
9
+
10
+ def icon_path(format: Literal["png", "ico"] = "png") -> Path:
11
+ """Installed package data, also placed beside this module by Nuitka."""
12
+ return Path(__file__).resolve().with_name("icons") / f"pyobs-gui.{format}"
13
+
14
+
15
+ def window_icon() -> "QIcon":
16
+ """Qt reads all resolutions in the ICO, including the small window-title icon."""
17
+ from PySide6.QtGui import QIcon
18
+
19
+ return QIcon(str(icon_path("ico")))
@@ -46,17 +46,12 @@ async def show_remote_error(parent: QtWidgets.QWidget, exception: Exception) ->
46
46
 
47
47
 
48
48
  def missing_root_message(exception: Exception, path: str, module: str) -> str | None:
49
- """User-facing text if `exception` is the VFS's "unknown root" error, else None.
49
+ """User-facing text if `exception` is the VFS's `UnknownRootError`, else None."""
50
+ from pyobs.vfs import UnknownRootError
50
51
 
51
- `VirtualFileSystem.open_file()` raises a plain `ValueError("Could not find root ...")`, so the
52
- message prefix is the only way to tell it from other `ValueError`s.
53
- """
54
- from pyobs.vfs import VirtualFileSystem
55
-
56
- if not isinstance(exception, ValueError) or not str(exception).startswith("Could not find root"):
52
+ if not isinstance(exception, UnknownRootError):
57
53
  return None
58
- root, _ = VirtualFileSystem.split_root(path)
59
- return f"Cannot open '{path}' from module '{module}': the VFS root '{root}' is not configured."
54
+ return f"Cannot open '{path}' from module '{module}': the VFS root '{exception.root}' is not configured."
60
55
 
61
56
 
62
57
  async def cancel_and_drain(task: asyncio.Task[Any]) -> None:
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
12
12
  from pyobs.interfaces import FitsHeaderEntry, IFitsHeaderBefore
13
13
  from pyobs.modules import Module
14
14
 
15
+ from .appicon import window_icon
15
16
  from .nowheelfilter import NoWheelWhenUnfocused
16
17
  from .guisignals import gui_signals
17
18
  from .notificationmanager import NotificationManager
@@ -147,6 +148,7 @@ class GUI(Module, IFitsHeaderBefore):
147
148
  @staticmethod
148
149
  def new_event_loop() -> asyncio.AbstractEventLoop:
149
150
  GUI.app = QtWidgets.QApplication(sys.argv)
151
+ GUI.app.setWindowIcon(window_icon())
150
152
  # standalone/login mode briefly has zero visible top-level windows while the login
151
153
  # dialog closes and the (old or new) MainWindow hasn't opened yet -- Qt's default
152
154
  # "quit when the last window closes" would tear down the whole event loop right there,
@@ -0,0 +1,13 @@
1
+ # pyobs GUI icon
2
+
3
+ White observing monitor and crosshair on magenta (`#d63384`), following the fleet's
4
+ white-symbol-on-colour app icons. Original artwork created for pyobs-gui and
5
+ approved on 2026-10-15; distributed under this repository's MIT licence.
6
+
7
+ `pyobs-gui.svg` is the editable source. PNG exports cover 16, 24, 32, 48, 64,
8
+ 128, 256 and 512 pixels. `pyobs-gui.png` is the 256-pixel notification icon;
9
+ `pyobs-gui.ico` contains 16 through 256-pixel renditions for Qt and Windows.
10
+
11
+ These files must remain ordinary package data: desktop-notifier and the Windows
12
+ shortcut require filesystem paths, rather than Qt resource URLs. Nuitka includes
13
+ them with `--include-package-data=pyobs_gui` in `pysidedeploy.spec`.
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
2
+ <title>pyobs GUI icon</title>
3
+ <desc>A white observing monitor with a crosshair on a magenta rounded square.</desc>
4
+ <rect width="256" height="256" rx="52" fill="#d63384"/>
5
+ <g fill="none" stroke="#fff" stroke-width="14" stroke-linecap="round" stroke-linejoin="round">
6
+ <rect x="42" y="53" width="172" height="119" rx="13"/>
7
+ <path d="M128 174v29M96 204h64"/>
8
+ </g>
9
+ <g fill="none" stroke="#fff" stroke-width="10" stroke-linecap="round">
10
+ <circle cx="128" cy="112" r="20"/>
11
+ <path d="M128 78v16M128 130v16M94 112h16M146 112h16"/>
12
+ </g>
13
+ </svg>
@@ -14,6 +14,7 @@ from dataclasses import dataclass
14
14
  from PySide6 import QtCore, QtGui, QtWidgets
15
15
  from pyobs.comm.xmpp import is_valid_jid
16
16
 
17
+ from .appicon import window_icon
17
18
  from .accounts import SavedAccountsModel
18
19
 
19
20
  log = logging.getLogger(__name__)
@@ -86,6 +87,7 @@ def build_connection_request(
86
87
  class LoginWindow(QtWidgets.QDialog):
87
88
  def __init__(self, accounts: SavedAccountsModel | None = None, parent: QtWidgets.QWidget | None = None) -> None:
88
89
  super().__init__(parent)
90
+ self.setWindowIcon(window_icon())
89
91
  self.setWindowTitle("pyobs - Sign in")
90
92
  self.resize(640, 480)
91
93
 
@@ -37,6 +37,7 @@ from pyobs.interfaces import (
37
37
  IStructuredConfig,
38
38
  )
39
39
 
40
+ from .appicon import window_icon
40
41
  from .base import BaseWindow, BaseWidget, cancel_and_drain
41
42
  from .acquisitionwidget import AcquisitionWidget
42
43
  from .autofocuswidget import AutoFocusWidget
@@ -641,6 +642,7 @@ class MainWindow(QtWidgets.QMainWindow, BaseWindow, Ui_MainWindow): # type: ign
641
642
  QtWidgets.QMainWindow.__init__(self)
642
643
  BaseWindow.__init__(self)
643
644
  self.setupUi(self) # type: ignore
645
+ self.setWindowIcon(window_icon())
644
646
  self.resize(1600, 900)
645
647
 
646
648
  # store stuff
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any
11
11
 
12
12
  from PySide6 import QtCore # type: ignore
13
13
 
14
+ from .appicon import icon_path
14
15
  from .notifier import NotifierError
15
16
 
16
17
  if TYPE_CHECKING:
@@ -77,9 +78,9 @@ class DesktopNotifierBackend:
77
78
  if self._factory is not None:
78
79
  self._notifier = self._factory()
79
80
  else:
80
- from desktop_notifier import DesktopNotifier
81
+ from desktop_notifier import DesktopNotifier, Icon
81
82
 
82
- self._notifier = DesktopNotifier(app_name=APP_NAME)
83
+ self._notifier = DesktopNotifier(app_name=APP_NAME, app_icon=Icon(path=icon_path()))
83
84
  return self._notifier
84
85
 
85
86
  async def ensure_authorisation(self) -> None:
@@ -18,6 +18,7 @@ from pydantic import ValidationError
18
18
  from pyobs.object import get_class_from_string
19
19
  from PySide6 import QtWidgets # type: ignore
20
20
 
21
+ from .appicon import window_icon
21
22
  from .notifier import NotifierError
22
23
  from .settings import GuiSettings, NotificationSettings, VfsSettings
23
24
 
@@ -83,6 +84,7 @@ class SettingsDialog(QtWidgets.QDialog):
83
84
  parent: QtWidgets.QWidget | None = None,
84
85
  ) -> None:
85
86
  super().__init__(parent)
87
+ self.setWindowIcon(window_icon())
86
88
  self.setWindowTitle("Settings")
87
89
  self.resize(640, 480)
88
90
  self._on_apply = on_apply
@@ -19,6 +19,8 @@ from typing import TYPE_CHECKING
19
19
  if TYPE_CHECKING:
20
20
  from collections.abc import Callable
21
21
 
22
+ from .appicon import icon_path
23
+
22
24
  log = logging.getLogger(__name__)
23
25
 
24
26
  SHORTCUT_NAME = "pyobs-gui.lnk"
@@ -52,6 +54,7 @@ def create_link_com(path: Path, target: str, arguments: str, app_id: str) -> Non
52
54
 
53
55
  link = pythoncom.CoCreateInstance(shell.CLSID_ShellLink, None, pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink)
54
56
  link.SetPath(target)
57
+ link.SetIconLocation(str(icon_path("ico")), 0)
55
58
  if arguments:
56
59
  link.SetArguments(arguments)
57
60
  store = link.QueryInterface(propsys.IID_IPropertyStore)
@@ -60,15 +63,36 @@ def create_link_com(path: Path, target: str, arguments: str, app_id: str) -> Non
60
63
  link.QueryInterface(pythoncom.IID_IPersistFile).Save(str(path), 0)
61
64
 
62
65
 
66
+ def update_link_icon_com(path: Path) -> bool:
67
+ """Upgrade an existing shortcut's icon, preserving its target and app identity.
68
+
69
+ Like create_link_com(), this still needs verification on Windows (#192).
70
+ """
71
+ import pythoncom # type: ignore[import-not-found]
72
+ from win32com.shell import shell # type: ignore[import-not-found]
73
+
74
+ link = pythoncom.CoCreateInstance(shell.CLSID_ShellLink, None, pythoncom.CLSCTX_INPROC_SERVER, shell.IID_IShellLink)
75
+ persist = link.QueryInterface(pythoncom.IID_IPersistFile)
76
+ persist.Load(str(path))
77
+ location = (str(icon_path("ico")), 0)
78
+ if link.GetIconLocation() == location:
79
+ return False
80
+ link.SetIconLocation(*location)
81
+ persist.Save(str(path), 0)
82
+ return True
83
+
84
+
63
85
  def ensure_shortcut(
64
86
  app_id: str,
65
87
  path: Path | None = None,
66
88
  command: tuple[str, str] | None = None,
67
89
  create_link: Callable[[Path, str, str, str], None] = create_link_com,
90
+ update_icon: Callable[[Path], bool] = update_link_icon_com,
68
91
  ) -> bool:
69
92
  """Make sure the Start Menu shortcut with the AppUserModelID exists.
70
93
 
71
- An existing shortcut is left alone, whatever it points to.
94
+ An existing shortcut keeps its target and identity; only its icon is updated.
95
+ Repeated starts leave the file alone when the icon already matches.
72
96
 
73
97
  Returns:
74
98
  Whether a shortcut was created.
@@ -78,6 +102,7 @@ def ensure_shortcut(
78
102
  """
79
103
  path = shortcut_path() if path is None else path
80
104
  if path.exists():
105
+ update_icon(path)
81
106
  return False
82
107
  target, arguments = launch_command() if command is None else command
83
108
  path.parent.mkdir(parents=True, exist_ok=True)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyobs-gui"
3
- version = "2.7.0"
3
+ version = "2.8.0"
4
4
  description = "A remote GUI for pyobs"
5
5
  authors = [{ name = "Tim-Oliver Husser", email = "thusser@uni-goettingen.de" }]
6
6
  requires-python = ">=3.11"
@@ -15,7 +15,7 @@ dependencies = [
15
15
  "astroquery>=0.4.10,<0.5",
16
16
  "QtAwesome>=1.4.0,<2",
17
17
  "qfitswidget>=1.2.0",
18
- "pyobs-core>=2.18.0,<3",
18
+ "pyobs-core>=2.19.0,<3",
19
19
  "pyside6-essentials>=6.10.1",
20
20
  "astroplan>=0.10.1",
21
21
  "astropy>=7.0.1,<8",
@@ -58,6 +58,9 @@ dev = [
58
58
  requires = ["hatchling"]
59
59
  build-backend = "hatchling.build"
60
60
 
61
+ [tool.hatch.build.targets.wheel]
62
+ packages = ["pyobs_gui"]
63
+
61
64
  [tool.black]
62
65
  line-length = 120
63
66
  target-version = ['py311']
@@ -16,7 +16,7 @@ exec_directory = dist
16
16
  project_file =
17
17
 
18
18
  # application icon
19
- icon = /home/husser/code/pyobs/pyobs-gui/.venv/lib/python3.13/site-packages/PySide6/scripts/deploy_lib/pyside_icon.jpg
19
+ icon = pyobs_gui/icons/pyobs-gui.png
20
20
 
21
21
  [python]
22
22
 
@@ -68,7 +68,7 @@ macos.permissions =
68
68
  mode = standalone
69
69
 
70
70
  # specify any extra nuitka arguments
71
- extra_args = --quiet --noinclude-qt-translations --python-flag=-m --include-package=astropy.units.format --include-package=astropy.units.format.generic_parsetab --include-package=astropy.units.format.generic_lextab --include-package=astropy.units.format.cds_parsetab --include-package=astropy.units.format.cds_lextab --include-package=astropy.units.format.ogip_parsetab --include-package=astropy.units.format.ogip_lextab --include-package=astropy.coordinates.angles --include-package=astropy.coordinates.angles.angle_parsetab --include-package=astropy.coordinates.angles.angle_lextab --include-package=astropy.extern.ply --include-package-data=astropy --include-package-data=sunpy --include-package-data=matplotlib --include-package-data=qfitswidget --include-package-data=astropy_iers_data --include-package=pyobs.vfs --include-package-data=astroquery --include-package=slixmpp --include-package=pyobs_gui --noinclude-dlls=*webengine* --noinclude-dlls=*WebEngine* --include-distribution-metadata=astropy --include-distribution-metadata=numpy --include-distribution-metadata=PyYAML --include-distribution-metadata=sunpy --include-distribution-metadata=matplotlib --include-distribution-metadata=qfitswidget --include-distribution-metadata=astroquery --include-distribution-metadata=pyobs-core --include-distribution-metadata=pyobs-gui
71
+ extra_args = --quiet --noinclude-qt-translations --python-flag=-m --include-package=astropy.units.format --include-package=astropy.units.format.generic_parsetab --include-package=astropy.units.format.generic_lextab --include-package=astropy.units.format.cds_parsetab --include-package=astropy.units.format.cds_lextab --include-package=astropy.units.format.ogip_parsetab --include-package=astropy.units.format.ogip_lextab --include-package=astropy.coordinates.angles --include-package=astropy.coordinates.angles.angle_parsetab --include-package=astropy.coordinates.angles.angle_lextab --include-package=astropy.extern.ply --include-package-data=astropy --include-package-data=sunpy --include-package-data=matplotlib --include-package-data=qfitswidget --include-package-data=astropy_iers_data --include-package=pyobs.vfs --include-package-data=astroquery --include-package=slixmpp --include-package=pyobs_gui --include-package-data=pyobs_gui --noinclude-dlls=*webengine* --noinclude-dlls=*WebEngine* --include-distribution-metadata=astropy --include-distribution-metadata=numpy --include-distribution-metadata=PyYAML --include-distribution-metadata=sunpy --include-distribution-metadata=matplotlib --include-distribution-metadata=qfitswidget --include-distribution-metadata=astroquery --include-distribution-metadata=pyobs-core --include-distribution-metadata=pyobs-gui
72
72
 
73
73
  [buildozer]
74
74
 
@@ -21,7 +21,7 @@ settings).
21
21
  `VideoWidget` (`_resolve_url` records it, `_init` shows it if no stream resolved).
22
22
  - [x] Tests (`tests/test_missing_vfs_root.py`): message content, once-per-root, other
23
23
  `ValueError`s still propagate, video init shows the box and stays disabled.
24
- - [ ] Follow-up in core: a dedicated exception for an unknown root, so the message-prefix match in
24
+ - [x] Follow-up in core (pyobs-core#941): a dedicated exception for an unknown root, so the message-prefix match in
25
25
  `missing_root_message()` can go.
26
26
 
27
27
  ## 3. Settings schema and store
@@ -2,19 +2,19 @@ from unittest.mock import AsyncMock, MagicMock, patch
2
2
 
3
3
  import pytest
4
4
  from pyobs.events import NewImageEvent
5
- from pyobs.vfs import VirtualFileSystem
5
+ from pyobs.vfs import UnknownRootError, VirtualFileSystem
6
6
 
7
7
  from pyobs_gui.base import missing_root_message
8
8
  from pyobs_gui.datadisplaywidget import DataDisplayWidget
9
9
  from pyobs_gui.videowidget import VideoWidget
10
10
 
11
11
 
12
- def _missing_root_error() -> ValueError:
12
+ def _missing_root_error() -> UnknownRootError:
13
13
  try:
14
14
  VirtualFileSystem().open_file("/cache/img.fits", "rb")
15
- except ValueError as e:
15
+ except UnknownRootError as e:
16
16
  return e
17
- raise AssertionError("expected ValueError")
17
+ raise AssertionError("expected UnknownRootError")
18
18
 
19
19
 
20
20
  def test_missing_root_message_names_root_path_and_module() -> None:
@@ -25,6 +25,7 @@ def test_missing_root_message_names_root_path_and_module() -> None:
25
25
 
26
26
  def test_missing_root_message_ignores_other_errors() -> None:
27
27
  assert missing_root_message(ValueError("something else"), "/cache/img.fits", "cam") is None
28
+ assert missing_root_message(ValueError("Could not find root cache for file."), "/cache/img.fits", "cam") is None
28
29
  assert missing_root_message(OSError("Could not find root"), "/cache/img.fits", "cam") is None
29
30
 
30
31
 
@@ -203,3 +203,14 @@ async def test_guard_passes_everything_on() -> None:
203
203
 
204
204
  def test_app_name_is_what_windows_uses_as_the_id() -> None:
205
205
  assert APP_NAME == "pyobs-gui"
206
+
207
+
208
+ def test_default_notifier_uses_packaged_app_icon(monkeypatch: pytest.MonkeyPatch) -> None:
209
+ from desktop_notifier import Icon
210
+ from pyobs_gui.appicon import icon_path
211
+
212
+ factory = MagicMock()
213
+ monkeypatch.setattr("desktop_notifier.DesktopNotifier", factory)
214
+ backend = DesktopNotifierBackend(ensure_identity=MagicMock())
215
+ backend._get()
216
+ factory.assert_called_once_with(app_name=APP_NAME, app_icon=Icon(path=icon_path()))
@@ -1,6 +1,7 @@
1
1
  import sys
2
2
  from pathlib import Path
3
3
  from typing import Any
4
+ from unittest.mock import MagicMock
4
5
 
5
6
  import pytest
6
7
 
@@ -34,7 +35,9 @@ def test_an_existing_shortcut_is_left_alone(tmp_path: Path) -> None:
34
35
  path = tmp_path / SHORTCUT_NAME
35
36
  path.write_text("mine")
36
37
  create = Recorder()
37
- assert ensure_shortcut("pyobs-gui", path, ("x", ""), create) is False
38
+ update = MagicMock(return_value=True)
39
+ assert ensure_shortcut("pyobs-gui", path, ("x", ""), create, update) is False
40
+ update.assert_called_once_with(path)
38
41
  assert create.calls == [] and path.read_text() == "mine"
39
42
 
40
43
 
@@ -67,3 +70,52 @@ def test_launch_command_for_the_pip_launcher(monkeypatch: pytest.MonkeyPatch, tm
67
70
  def test_launch_command_otherwise_starts_the_module(monkeypatch: pytest.MonkeyPatch, argv0: Any) -> None:
68
71
  monkeypatch.setattr(sys, "argv", [argv0])
69
72
  assert launch_command() == (sys.executable, "-m pyobs_gui")
73
+
74
+
75
+ @pytest.fixture
76
+ def com_link(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
77
+ """Exercise our COM calls without requiring a Windows notification service."""
78
+ from types import ModuleType
79
+
80
+ link = MagicMock()
81
+ pythoncom = MagicMock()
82
+ pythoncom.CoCreateInstance.return_value = link
83
+ monkeypatch.setitem(sys.modules, "pythoncom", pythoncom)
84
+ monkeypatch.setitem(sys.modules, "win32com", ModuleType("win32com"))
85
+ monkeypatch.setitem(sys.modules, "win32com.shell", MagicMock())
86
+ monkeypatch.setitem(sys.modules, "win32com.propsys", MagicMock())
87
+ return link
88
+
89
+
90
+ def test_new_com_shortcut_has_the_packaged_icon(com_link: MagicMock, tmp_path: Path) -> None:
91
+ from pyobs_gui.appicon import icon_path
92
+ from pyobs_gui.windows_identity import create_link_com
93
+
94
+ create_link_com(tmp_path / SHORTCUT_NAME, "python.exe", "-m pyobs_gui", "pyobs-gui")
95
+ com_link.SetIconLocation.assert_called_once_with(str(icon_path("ico")), 0)
96
+ com_link.SetPath.assert_called_once_with("python.exe")
97
+ com_link.SetArguments.assert_called_once_with("-m pyobs_gui")
98
+
99
+
100
+ def test_existing_com_shortcut_gets_icon_without_changing_target(com_link: MagicMock, tmp_path: Path) -> None:
101
+ from pyobs_gui.appicon import icon_path
102
+ from pyobs_gui.windows_identity import update_link_icon_com
103
+
104
+ path = tmp_path / SHORTCUT_NAME
105
+ com_link.GetIconLocation.return_value = ("python.exe", 0)
106
+ assert update_link_icon_com(path) is True
107
+ com_link.QueryInterface.return_value.Load.assert_called_once_with(str(path))
108
+ com_link.SetIconLocation.assert_called_once_with(str(icon_path("ico")), 0)
109
+ com_link.QueryInterface.return_value.Save.assert_called_once_with(str(path), 0)
110
+ com_link.SetPath.assert_not_called()
111
+ com_link.SetArguments.assert_not_called()
112
+
113
+
114
+ def test_matching_com_shortcut_is_not_rewritten(com_link: MagicMock, tmp_path: Path) -> None:
115
+ from pyobs_gui.appicon import icon_path
116
+ from pyobs_gui.windows_identity import update_link_icon_com
117
+
118
+ com_link.GetIconLocation.return_value = (str(icon_path("ico")), 0)
119
+ assert update_link_icon_com(tmp_path / SHORTCUT_NAME) is False
120
+ com_link.SetIconLocation.assert_not_called()
121
+ com_link.QueryInterface.return_value.Save.assert_not_called()