synodic-client 0.0.1.dev76__tar.gz → 0.0.1.dev78__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 (110) hide show
  1. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/PKG-INFO +3 -3
  2. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/pyproject.toml +4 -4
  3. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/bootstrap.py +1 -0
  4. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/config_store.py +4 -3
  5. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/debug.py +1 -1
  6. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/package_state.py +23 -0
  7. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/schema.py +0 -33
  8. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/action_card.py +47 -10
  9. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/install.py +218 -97
  10. synodic_client-0.0.1.dev78/synodic_client/application/screen/install_workers.py +161 -0
  11. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/plugin_row.py +84 -28
  12. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/projects.py +28 -13
  13. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/schema.py +41 -28
  14. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/screen.py +105 -26
  15. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/tool_update_controller.py +76 -54
  16. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/theme.py +19 -0
  17. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/update_controller.py +3 -2
  18. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/update_model.py +15 -8
  19. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/cli/__init__.py +2 -0
  20. synodic_client-0.0.1.dev78/synodic_client/cli/install.py +210 -0
  21. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/cli/output.py +5 -3
  22. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/cli/tool.py +4 -0
  23. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/operations/__init__.py +14 -1
  24. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/operations/config.py +31 -8
  25. synodic_client-0.0.1.dev78/synodic_client/operations/install.py +529 -0
  26. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/operations/schema.py +207 -2
  27. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/operations/tool.py +107 -24
  28. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/resolution.py +184 -211
  29. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_config.py +37 -1
  30. synodic_client-0.0.1.dev78/tests/unit/operations/test_install.py +424 -0
  31. synodic_client-0.0.1.dev78/tests/unit/operations/test_install_plan.py +207 -0
  32. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_tool.py +23 -60
  33. synodic_client-0.0.1.dev78/tests/unit/qt/conftest.py +125 -0
  34. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_action_card.py +112 -173
  35. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_gather_packages.py +46 -79
  36. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_install_preview.py +32 -25
  37. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_log_panel.py +47 -91
  38. synodic_client-0.0.1.dev78/tests/unit/qt/test_package_state.py +63 -0
  39. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_preview_model.py +75 -62
  40. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_settings.py +30 -58
  41. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_sidebar.py +17 -32
  42. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_tray_window_show.py +3 -20
  43. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_update_controller.py +11 -40
  44. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_update_feedback.py +78 -11
  45. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_cli.py +107 -0
  46. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_resolution.py +26 -7
  47. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_updater.py +49 -150
  48. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_workers.py +72 -1
  49. synodic_client-0.0.1.dev78/tests/unit/windows/conftest.py +19 -0
  50. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/windows/test_protocol.py +5 -7
  51. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/windows/test_startup.py +17 -43
  52. synodic_client-0.0.1.dev76/synodic_client/application/screen/install_workers.py +0 -152
  53. synodic_client-0.0.1.dev76/synodic_client/application/workers.py +0 -20
  54. synodic_client-0.0.1.dev76/synodic_client/operations/install.py +0 -272
  55. synodic_client-0.0.1.dev76/tests/unit/operations/test_install.py +0 -221
  56. synodic_client-0.0.1.dev76/tests/unit/qt/conftest.py +0 -24
  57. synodic_client-0.0.1.dev76/tests/unit/windows/conftest.py +0 -9
  58. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/LICENSE.md +0 -0
  59. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/README.md +0 -0
  60. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/__init__.py +0 -0
  61. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/__main__.py +0 -0
  62. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/__init__.py +0 -0
  63. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/data.py +0 -0
  64. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/icon.py +0 -0
  65. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/init.py +0 -0
  66. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/instance.py +0 -0
  67. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/qt.py +0 -0
  68. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/__init__.py +0 -0
  69. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/card.py +0 -0
  70. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/log_panel.py +0 -0
  71. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/settings.py +0 -0
  72. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/sidebar.py +0 -0
  73. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/spinner.py +0 -0
  74. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/tray.py +0 -0
  75. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/screen/update_banner.py +0 -0
  76. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/application/uri.py +0 -0
  77. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/cli/config.py +0 -0
  78. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/cli/context.py +0 -0
  79. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/cli/debug.py +0 -0
  80. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/cli/project.py +0 -0
  81. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/cli/update.py +0 -0
  82. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/client.py +0 -0
  83. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/config.py +0 -0
  84. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/logging.py +0 -0
  85. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/operations/bootstrap.py +0 -0
  86. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/operations/project.py +0 -0
  87. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/operations/update.py +0 -0
  88. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/protocol.py +0 -0
  89. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/py.typed +0 -0
  90. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/schema.py +0 -0
  91. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/startup.py +0 -0
  92. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/subprocess_patch.py +0 -0
  93. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/synodic_client/updater.py +0 -0
  94. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/__init__.py +0 -0
  95. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/conftest.py +0 -0
  96. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/__init__.py +0 -0
  97. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/operations/__init__.py +0 -0
  98. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_project.py +0 -0
  99. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/operations/test_update.py +0 -0
  100. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/__init__.py +0 -0
  101. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_logging.py +0 -0
  102. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/qt/test_update_banner.py +0 -0
  103. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_client_updater.py +0 -0
  104. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_client_version.py +0 -0
  105. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_config.py +0 -0
  106. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_examples.py +0 -0
  107. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_init.py +0 -0
  108. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_install.py +0 -0
  109. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/test_uri.py +0 -0
  110. {synodic_client-0.0.1.dev76 → synodic_client-0.0.1.dev78}/tests/unit/windows/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synodic_client
3
- Version: 0.0.1.dev76
3
+ Version: 0.0.1.dev78
4
4
  Author-Email: Synodic Software <contact@synodic.software>
5
5
  License: LGPL-3.0-or-later
6
6
  Project-URL: homepage, https://github.com/synodic/synodic-client
@@ -8,9 +8,9 @@ Project-URL: repository, https://github.com/synodic/synodic-client
8
8
  Requires-Python: <3.15,>=3.14
9
9
  Requires-Dist: pyside6>=6.10.2
10
10
  Requires-Dist: packaging>=26.0
11
- Requires-Dist: porringer>=0.2.1.dev85
11
+ Requires-Dist: porringer>=0.2.1.dev86
12
12
  Requires-Dist: qasync>=0.28.0
13
- Requires-Dist: velopack>=0.0.1444.dev49733
13
+ Requires-Dist: velopack>=0.0.1521.dev61717
14
14
  Requires-Dist: typer>=0.24.1
15
15
  Description-Content-Type: text/markdown
16
16
 
@@ -10,12 +10,12 @@ requires-python = ">=3.14, <3.15"
10
10
  dependencies = [
11
11
  "pyside6>=6.10.2",
12
12
  "packaging>=26.0",
13
- "porringer>=0.2.1.dev85",
13
+ "porringer>=0.2.1.dev86",
14
14
  "qasync>=0.28.0",
15
- "velopack>=0.0.1444.dev49733",
15
+ "velopack>=0.0.1521.dev61717",
16
16
  "typer>=0.24.1",
17
17
  ]
18
- version = "0.0.1.dev76"
18
+ version = "0.0.1.dev78"
19
19
 
20
20
  [project.license]
21
21
  text = "LGPL-3.0-or-later"
@@ -36,7 +36,7 @@ build = [
36
36
  ]
37
37
  lint = [
38
38
  "ruff>=0.15.6",
39
- "pyrefly>=0.56.0",
39
+ "pyrefly>=0.57.0",
40
40
  ]
41
41
  test = [
42
42
  "pytest>=9.0.2",
@@ -55,6 +55,7 @@ def bootstrap() -> None:
55
55
  run_startup_preamble(sys.executable)
56
56
 
57
57
  # Heavy imports happen here — PySide6, porringer, etc.
58
+
58
59
  from synodic_client.application.qt import application
59
60
 
60
61
  application(uri=extract_uri_from_args(), dev_mode=dev_mode, debug=debug)
@@ -10,7 +10,8 @@ from __future__ import annotations
10
10
 
11
11
  from PySide6.QtCore import QObject, Signal
12
12
 
13
- from synodic_client.resolution import ResolvedConfig, resolve_config, update_user_config
13
+ from synodic_client.operations.config import get_config, update_config
14
+ from synodic_client.schema import ResolvedConfig
14
15
 
15
16
 
16
17
  class ConfigStore(QObject):
@@ -33,7 +34,7 @@ class ConfigStore(QObject):
33
34
  def __init__(self, config: ResolvedConfig | None = None, parent: QObject | None = None) -> None:
34
35
  """Create a new store, optionally seeded with *config*."""
35
36
  super().__init__(parent)
36
- self._config = config if config is not None else resolve_config()
37
+ self._config = config if config is not None else get_config()
37
38
 
38
39
  @property
39
40
  def config(self) -> ResolvedConfig:
@@ -52,7 +53,7 @@ class ConfigStore(QObject):
52
53
  Returns:
53
54
  The fresh :class:`ResolvedConfig`.
54
55
  """
55
- self._config = update_user_config(**changes)
56
+ self._config = update_config(**changes)
56
57
  self.changed.emit(self._config)
57
58
  return self._config
58
59
 
@@ -227,7 +227,7 @@ class DebugHandler:
227
227
  needed = sum(1 for s in model.action_states if classify_status(s.status) == 'needed')
228
228
  satisfied = sum(1 for s in model.action_states if classify_status(s.status) == 'satisfied')
229
229
  pending = sum(1 for s in model.action_states if classify_status(s.status) == 'pending')
230
- upgradable = len(model.upgradable_keys)
230
+ upgradable = sum(1 for s in model.action_states if s.status == 'Update available')
231
231
 
232
232
  return json.dumps({
233
233
  'path': str(target),
@@ -123,3 +123,26 @@ class PackageStateStore(QObject):
123
123
  def clear(self) -> None:
124
124
  """Remove all recorded state."""
125
125
  self._data.clear()
126
+
127
+ def record_updates_completed(
128
+ self,
129
+ signal_key: str,
130
+ version_map: dict[str, tuple[str, str]],
131
+ ) -> None:
132
+ """Mark packages as updated, clearing stale ``has_update`` flags.
133
+
134
+ Called after a successful tool update run. For each entry in
135
+ *version_map* (``{package_name: (old_version, new_version)}``),
136
+ the corresponding :class:`PackageState` is updated to reflect
137
+ the new installed version and ``has_update`` is cleared.
138
+ """
139
+ changed = False
140
+ bucket = self._data.get(signal_key, {})
141
+ for pkg_name, (_, new_ver) in version_map.items():
142
+ existing = bucket.get(pkg_name)
143
+ if existing is not None:
144
+ existing.installed_version = new_ver
145
+ existing.has_update = False
146
+ changed = True
147
+ if changed:
148
+ self.state_changed.emit()
@@ -43,36 +43,3 @@ class Snapshot:
43
43
 
44
44
  plugin_capabilities: dict[str, frozenset[PluginCapability]] = field(default_factory=dict)
45
45
  """Protocol capabilities reported for each discovered plugin."""
46
-
47
-
48
- @dataclass(slots=True)
49
- class ToolUpdateResult:
50
- """Summary of a tool-update run across cached manifests."""
51
-
52
- manifests_processed: int = 0
53
- updated: int = 0
54
- already_latest: int = 0
55
- failed: int = 0
56
- updated_packages: set[str] = field(default_factory=set)
57
- """Package names that were successfully upgraded."""
58
-
59
-
60
- @dataclass(frozen=True, slots=True)
61
- class UpdateTarget:
62
- """Identifies the scope of a manual tool update.
63
-
64
- Passed to the shared completion handler so it can clear the correct
65
- updating state and derive timestamp keys. ``None`` (the default in
66
- the handler) means the update was periodic / automatic.
67
-
68
- When *package* is empty the update targeted an entire plugin;
69
- otherwise it targeted one specific package within the plugin.
70
- *plugin* always carries the signal key (possibly composite
71
- ``"plugin:tag"``).
72
- """
73
-
74
- plugin: str
75
- """Signal key for the plugin (may be composite ``"name:tag"``)."""
76
-
77
- package: str = ''
78
- """Package name, or empty when the whole plugin was updated."""
@@ -17,11 +17,12 @@ from porringer.backend.command.core.action_builder import PHASE_ORDER
17
17
  from porringer.schema import SetupAction, SetupActionResult
18
18
  from porringer.schema.plugin import PluginKind
19
19
  from PySide6.QtCore import Qt, QTimer, Signal
20
- from PySide6.QtGui import QColor
20
+ from PySide6.QtGui import QColor, QCursor, QMouseEvent
21
21
  from PySide6.QtWidgets import (
22
22
  QApplication,
23
23
  QCheckBox,
24
24
  QFrame,
25
+ QGraphicsOpacityEffect,
25
26
  QHBoxLayout,
26
27
  QLabel,
27
28
  QToolButton,
@@ -57,6 +58,7 @@ from synodic_client.application.theme import (
57
58
  ACTION_CARD_STATUS_UPDATE,
58
59
  ACTION_CARD_STYLE,
59
60
  ACTION_CARD_TYPE_BADGE_STYLE,
61
+ ACTION_CARD_UPDATE_AVAILABLE_STYLE,
60
62
  ACTION_CARD_VERSION_STYLE,
61
63
  COPY_BTN_SIZE,
62
64
  COPY_BTN_STYLE,
@@ -77,6 +79,15 @@ _SPINNER_INTERVAL = 50
77
79
  #: display order always matches the order actions actually execute.
78
80
  _KIND_ORDER: dict[PluginKind | None, int] = {kind: i for i, kind in enumerate(PHASE_ORDER)}
79
81
 
82
+ #: Mapping of resolved status label → stylesheet for dry-run badge styling.
83
+ _STATUS_STYLES: dict[str, str] = {
84
+ 'Update available': ACTION_CARD_STATUS_UPDATE,
85
+ 'Failed': ACTION_CARD_STATUS_FAILED,
86
+ 'Pending': ACTION_CARD_STATUS_PENDING,
87
+ 'Ready': ACTION_CARD_STATUS_SATISFIED,
88
+ 'Needed': ACTION_CARD_STATUS_NEEDED,
89
+ }
90
+
80
91
 
81
92
  def action_sort_key(action: SetupAction) -> int:
82
93
  """Return a sort key that groups cards by execution phase.
@@ -119,6 +130,10 @@ class ActionCard(QFrame):
119
130
  """Emitted with ``(package_name, checked)`` when the user toggles the
120
131
  per-row pre-release checkbox."""
121
132
 
133
+ navigate_to_tool = Signal(str, str)
134
+ """Emitted with ``(installer, package_name)`` when the user clicks an
135
+ 'Update available' card to navigate to the Tools view."""
136
+
122
137
  def __init__(
123
138
  self,
124
139
  parent: QWidget | None = None,
@@ -459,15 +474,6 @@ class ActionCard(QFrame):
459
474
 
460
475
  self._stop_spinner()
461
476
 
462
- # Status-to-style mapping
463
- _STATUS_STYLES: dict[str, str] = {
464
- 'Update available': ACTION_CARD_STATUS_UPDATE,
465
- 'Failed': ACTION_CARD_STATUS_FAILED,
466
- 'Pending': ACTION_CARD_STATUS_PENDING,
467
- 'Ready': ACTION_CARD_STATUS_SATISFIED,
468
- 'Needed': ACTION_CARD_STATUS_NEEDED,
469
- }
470
-
471
477
  style = _STATUS_STYLES.get(status, ACTION_CARD_STATUS_SATISFIED)
472
478
  display = status
473
479
 
@@ -491,6 +497,10 @@ class ActionCard(QFrame):
491
497
  else:
492
498
  self._status_label.setToolTip('')
493
499
 
500
+ # "Update available" — fade the card and make it clickable
501
+ if status == 'Update available':
502
+ self._apply_update_available_style()
503
+
494
504
  # CLI command — update with resolved cli_command from result
495
505
  assert self._action is not None
496
506
  cmd_text = format_cli_command(self._action, result=result, suppress_description=True)
@@ -524,6 +534,15 @@ class ActionCard(QFrame):
524
534
  self._version_label.setText(f'\u2192 {result.available_version}')
525
535
  self._version_label.setStyleSheet(ACTION_CARD_VERSION_STYLE + ' color: grey;')
526
536
 
537
+ def _apply_update_available_style(self) -> None:
538
+ """Fade the card and make it clickable for 'Update available' status."""
539
+ self.setStyleSheet(ACTION_CARD_UPDATE_AVAILABLE_STYLE)
540
+ opacity = QGraphicsOpacityEffect(self)
541
+ opacity.setOpacity(0.55)
542
+ self.setGraphicsEffect(opacity)
543
+ self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
544
+ self.setToolTip('Manage this update in the Tools view')
545
+
527
546
  def finalize_checking(self) -> None:
528
547
  """Resolve a still-pending 'Checking\u2026' status to 'Needed'.
529
548
 
@@ -605,6 +624,16 @@ class ActionCard(QFrame):
605
624
  """Return whether the card shows an 'Update available' status."""
606
625
  return self.status_text() == 'Update available'
607
626
 
627
+ def mousePressEvent(self, event: QMouseEvent) -> None:
628
+ """Navigate to Tools view when clicking an 'Update available' card."""
629
+ if self.is_update_available() and self._action is not None:
630
+ installer = self._action.installer or ''
631
+ package = str(self._action.package.name) if self._action.package else ''
632
+ if installer and package:
633
+ self.navigate_to_tool.emit(installer, package)
634
+ return
635
+ super().mousePressEvent(event)
636
+
608
637
 
609
638
  # ---------------------------------------------------------------------------
610
639
  # ActionCardList — card container
@@ -621,6 +650,13 @@ class ActionCardList(QWidget):
621
650
  prerelease_toggled = Signal(str, bool)
622
651
  """Forwarded from child :class:`ActionCard` widgets."""
623
652
 
653
+ navigate_to_tool = Signal(str, str)
654
+ """Forwarded from child :class:`ActionCard` widgets.
655
+
656
+ Emitted with ``(installer, package_name)`` when the user clicks an
657
+ 'Update available' card.
658
+ """
659
+
624
660
  def __init__(self, parent: QWidget | None = None) -> None:
625
661
  """Initialise the card list."""
626
662
  super().__init__(parent)
@@ -680,6 +716,7 @@ class ActionCardList(QWidget):
680
716
  prerelease_overrides=prerelease_overrides,
681
717
  )
682
718
  card.prerelease_toggled.connect(self.prerelease_toggled.emit)
719
+ card.navigate_to_tool.connect(self.navigate_to_tool.emit)
683
720
  self._layout.insertWidget(self._layout.count() - 1, card)
684
721
  self._cards.append(card)
685
722
  self._action_map[act] = card