synodic-client 0.0.1.dev75__tar.gz → 0.0.1.dev77__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.
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/PKG-INFO +3 -3
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/pyproject.toml +4 -4
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/bootstrap.py +1 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/config_store.py +4 -3
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/debug.py +1 -1
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/package_state.py +23 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/action_card.py +57 -1
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/install.py +195 -80
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/install_workers.py +71 -16
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/projects.py +30 -12
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/schema.py +21 -10
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/screen.py +71 -26
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/tool_update_controller.py +8 -7
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/theme.py +16 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/cli/__init__.py +2 -0
- synodic_client-0.0.1.dev77/synodic_client/cli/install.py +213 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/cli/output.py +5 -3
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/cli/tool.py +4 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/operations/__init__.py +14 -1
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/operations/config.py +23 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/operations/install.py +180 -15
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/operations/schema.py +229 -10
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/operations/tool.py +31 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/operations/test_config.py +37 -1
- synodic_client-0.0.1.dev77/tests/unit/operations/test_install.py +424 -0
- synodic_client-0.0.1.dev77/tests/unit/operations/test_install_plan.py +207 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/operations/test_tool.py +21 -0
- synodic_client-0.0.1.dev77/tests/unit/qt/test_package_state.py +63 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_preview_model.py +61 -15
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_cli.py +107 -0
- synodic_client-0.0.1.dev75/tests/unit/operations/test_install.py +0 -221
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/README.md +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/tray.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/update_controller.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/update_model.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/cli/config.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/cli/context.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/cli/debug.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/cli/project.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/cli/update.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/operations/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/operations/project.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/operations/update.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/operations/test_project.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/operations/test_update.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_settings.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_update_controller.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/tests/unit/windows/test_startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: synodic_client
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev77
|
|
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.
|
|
11
|
+
Requires-Dist: porringer>=0.2.1.dev86
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
|
-
Requires-Dist: velopack>=0.0.
|
|
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.
|
|
13
|
+
"porringer>=0.2.1.dev86",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
|
-
"velopack>=0.0.
|
|
15
|
+
"velopack>=0.0.1521.dev61717",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev77"
|
|
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.
|
|
39
|
+
"pyrefly>=0.57.0",
|
|
40
40
|
]
|
|
41
41
|
test = [
|
|
42
42
|
"pytest>=9.0.2",
|
{synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/bootstrap.py
RENAMED
|
@@ -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)
|
{synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/config_store.py
RENAMED
|
@@ -10,7 +10,8 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from PySide6.QtCore import QObject, Signal
|
|
12
12
|
|
|
13
|
-
from synodic_client.
|
|
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
|
|
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 =
|
|
56
|
+
self._config = update_config(**changes)
|
|
56
57
|
self.changed.emit(self._config)
|
|
57
58
|
return self._config
|
|
58
59
|
|
{synodic_client-0.0.1.dev75 → synodic_client-0.0.1.dev77}/synodic_client/application/debug.py
RENAMED
|
@@ -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 =
|
|
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()
|
|
@@ -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,
|
|
@@ -119,6 +121,10 @@ class ActionCard(QFrame):
|
|
|
119
121
|
"""Emitted with ``(package_name, checked)`` when the user toggles the
|
|
120
122
|
per-row pre-release checkbox."""
|
|
121
123
|
|
|
124
|
+
navigate_to_tool = Signal(str, str)
|
|
125
|
+
"""Emitted with ``(installer, package_name)`` when the user clicks an
|
|
126
|
+
'Update available' card to navigate to the Tools view."""
|
|
127
|
+
|
|
122
128
|
def __init__(
|
|
123
129
|
self,
|
|
124
130
|
parent: QWidget | None = None,
|
|
@@ -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)
|
|
@@ -632,6 +668,7 @@ class ActionCardList(QWidget):
|
|
|
632
668
|
|
|
633
669
|
self._cards: list[ActionCard] = []
|
|
634
670
|
self._action_map: dict[SetupAction, ActionCard] = {}
|
|
671
|
+
self._index_map: dict[int, ActionCard] = {}
|
|
635
672
|
|
|
636
673
|
# ------------------------------------------------------------------
|
|
637
674
|
# Skeleton loading
|
|
@@ -679,10 +716,19 @@ class ActionCardList(QWidget):
|
|
|
679
716
|
prerelease_overrides=prerelease_overrides,
|
|
680
717
|
)
|
|
681
718
|
card.prerelease_toggled.connect(self.prerelease_toggled.emit)
|
|
719
|
+
card.navigate_to_tool.connect(self.navigate_to_tool.emit)
|
|
682
720
|
self._layout.insertWidget(self._layout.count() - 1, card)
|
|
683
721
|
self._cards.append(card)
|
|
684
722
|
self._action_map[act] = card
|
|
685
723
|
|
|
724
|
+
# Build original-index → card mapping so callers can look up by
|
|
725
|
+
# the action index porringer emits, which is independent of the
|
|
726
|
+
# display sort order.
|
|
727
|
+
for original_index, act in enumerate(actions):
|
|
728
|
+
card = self._action_map.get(act)
|
|
729
|
+
if card is not None:
|
|
730
|
+
self._index_map[original_index] = card
|
|
731
|
+
|
|
686
732
|
# ------------------------------------------------------------------
|
|
687
733
|
# Card lookup
|
|
688
734
|
# ------------------------------------------------------------------
|
|
@@ -697,6 +743,15 @@ class ActionCardList(QWidget):
|
|
|
697
743
|
"""Return the number of cards (including skeletons)."""
|
|
698
744
|
return len(self._cards)
|
|
699
745
|
|
|
746
|
+
def card_for_action_index(self, action_index: int) -> ActionCard | None:
|
|
747
|
+
"""Return the card for the given original action index.
|
|
748
|
+
|
|
749
|
+
The index corresponds to the action's position in the unsorted
|
|
750
|
+
list passed to :meth:`populate`, matching the indices emitted
|
|
751
|
+
by porringer's ``ActionCompletedEvent``.
|
|
752
|
+
"""
|
|
753
|
+
return self._index_map.get(action_index)
|
|
754
|
+
|
|
700
755
|
def get_card(self, action: SetupAction) -> ActionCard | None:
|
|
701
756
|
"""Look up the card for a given action.
|
|
702
757
|
|
|
@@ -727,3 +782,4 @@ class ActionCardList(QWidget):
|
|
|
727
782
|
card.deleteLater()
|
|
728
783
|
self._cards.clear()
|
|
729
784
|
self._action_map.clear()
|
|
785
|
+
self._index_map.clear()
|
|
@@ -23,9 +23,7 @@ from porringer.schema import (
|
|
|
23
23
|
SetupAction,
|
|
24
24
|
SetupActionResult,
|
|
25
25
|
SetupResults,
|
|
26
|
-
SkipReason,
|
|
27
26
|
SubActionProgress,
|
|
28
|
-
SyncStrategy,
|
|
29
27
|
)
|
|
30
28
|
from PySide6.QtCore import Qt, QTimer, Signal
|
|
31
29
|
from PySide6.QtGui import QShowEvent
|
|
@@ -46,7 +44,7 @@ from PySide6.QtWidgets import (
|
|
|
46
44
|
from synodic_client.application.package_state import PackageStateStore
|
|
47
45
|
from synodic_client.application.screen.action_card import ActionCardList
|
|
48
46
|
from synodic_client.application.screen.card import CardFrame
|
|
49
|
-
from synodic_client.application.screen.install_workers import run_install, run_preview
|
|
47
|
+
from synodic_client.application.screen.install_workers import run_install, run_post_sync, run_preview
|
|
50
48
|
from synodic_client.application.screen.log_panel import ExecutionLogPanel
|
|
51
49
|
from synodic_client.application.screen.schema import (
|
|
52
50
|
ActionState,
|
|
@@ -106,6 +104,10 @@ class SetupPreviewWidget(QWidget):
|
|
|
106
104
|
#: Emitted whenever the lifecycle phase changes.
|
|
107
105
|
phase_changed = Signal(object) # PreviewPhase
|
|
108
106
|
|
|
107
|
+
#: Emitted with ``(installer, package_name)`` when the user clicks an
|
|
108
|
+
#: 'Update available' card to navigate to the Tools view.
|
|
109
|
+
navigate_to_tool_requested = Signal(str, str)
|
|
110
|
+
|
|
109
111
|
def __init__(
|
|
110
112
|
self,
|
|
111
113
|
porringer: API,
|
|
@@ -134,6 +136,7 @@ class SetupPreviewWidget(QWidget):
|
|
|
134
136
|
|
|
135
137
|
self._model = PreviewModel()
|
|
136
138
|
self._task: asyncio.Task[None] | None = None
|
|
139
|
+
self._install_results: SetupResults | None = None
|
|
137
140
|
|
|
138
141
|
# Debounce timer for per-row pre-release checkbox changes
|
|
139
142
|
self._prerelease_debounce = QTimer(self)
|
|
@@ -183,6 +186,7 @@ class SetupPreviewWidget(QWidget):
|
|
|
183
186
|
|
|
184
187
|
self._card_list = ActionCardList()
|
|
185
188
|
self._card_list.prerelease_toggled.connect(self._on_prerelease_row_toggled)
|
|
189
|
+
self._card_list.navigate_to_tool.connect(self.navigate_to_tool_requested.emit)
|
|
186
190
|
scroll_layout.addWidget(self._card_list)
|
|
187
191
|
|
|
188
192
|
self._log_panel = ExecutionLogPanel()
|
|
@@ -249,6 +253,13 @@ class SetupPreviewWidget(QWidget):
|
|
|
249
253
|
button_bar = QHBoxLayout()
|
|
250
254
|
button_bar.addStretch()
|
|
251
255
|
|
|
256
|
+
self._run_commands_btn = QPushButton('Run Commands')
|
|
257
|
+
self._run_commands_btn.setToolTip('Execute post-sync commands from the manifest')
|
|
258
|
+
self._run_commands_btn.setEnabled(False)
|
|
259
|
+
self._run_commands_btn.hide()
|
|
260
|
+
self._run_commands_btn.clicked.connect(self._on_run_commands)
|
|
261
|
+
button_bar.addWidget(self._run_commands_btn)
|
|
262
|
+
|
|
252
263
|
self._install_btn = QPushButton('Install')
|
|
253
264
|
self._install_btn.setEnabled(False)
|
|
254
265
|
self._install_btn.clicked.connect(self._on_install)
|
|
@@ -370,6 +381,9 @@ class SetupPreviewWidget(QWidget):
|
|
|
370
381
|
self._status_label.setText('')
|
|
371
382
|
self._status_label.setStyleSheet('')
|
|
372
383
|
self._install_btn.setEnabled(False)
|
|
384
|
+
self._run_commands_btn.setEnabled(False)
|
|
385
|
+
self._run_commands_btn.hide()
|
|
386
|
+
self._install_results = None
|
|
373
387
|
self._log_panel.clear()
|
|
374
388
|
self._log_panel.hide()
|
|
375
389
|
|
|
@@ -517,6 +531,7 @@ class SetupPreviewWidget(QWidget):
|
|
|
517
531
|
on_progress=self._on_action_progress,
|
|
518
532
|
),
|
|
519
533
|
plugins=self._discovered_plugins,
|
|
534
|
+
exclude_post_sync=self._model.has_post_sync,
|
|
520
535
|
)
|
|
521
536
|
self._on_install_finished(results)
|
|
522
537
|
except asyncio.CancelledError:
|
|
@@ -614,20 +629,24 @@ class SetupPreviewWidget(QWidget):
|
|
|
614
629
|
self.metadata_ready.emit(preview)
|
|
615
630
|
|
|
616
631
|
def _on_action_checked(self, row: int, result: SetupActionResult, status: str) -> None:
|
|
617
|
-
"""Update the model and action card with a dry-run result.
|
|
618
|
-
|
|
619
|
-
|
|
632
|
+
"""Update the model and action card with a dry-run result.
|
|
633
|
+
|
|
634
|
+
This callback performs only two things:
|
|
635
|
+
1. Update the ``ActionState.status`` in the model.
|
|
636
|
+
2. Update the ``ActionCard`` widget visually.
|
|
620
637
|
|
|
621
|
-
|
|
622
|
-
|
|
638
|
+
Cross-component side effects (PackageStateStore writes) are
|
|
639
|
+
deferred to :meth:`_on_preview_finished` for one-way data flow.
|
|
640
|
+
"""
|
|
641
|
+
m = self._model
|
|
623
642
|
|
|
624
643
|
if 0 <= row < len(m.action_states):
|
|
625
|
-
m.action_states[row].status =
|
|
644
|
+
m.action_states[row].status = status
|
|
626
645
|
|
|
627
646
|
logger.debug(
|
|
628
647
|
'Action checked [%d]: status=%s success=%s skipped=%s skip_reason=%s installed=%s available=%s',
|
|
629
648
|
row,
|
|
630
|
-
|
|
649
|
+
status,
|
|
631
650
|
result.success,
|
|
632
651
|
result.skipped,
|
|
633
652
|
result.skip_reason,
|
|
@@ -637,21 +656,10 @@ class SetupPreviewWidget(QWidget):
|
|
|
637
656
|
|
|
638
657
|
# Update the card widget
|
|
639
658
|
if m.preview and 0 <= row < len(m.preview.actions):
|
|
640
|
-
|
|
641
|
-
card = self._card_list.get_card(action)
|
|
659
|
+
card = self._card_list.card_for_action_index(row)
|
|
642
660
|
if card is not None:
|
|
643
661
|
card.set_check_result(result, status)
|
|
644
662
|
|
|
645
|
-
# Record in shared store so ToolsView can reflect the update
|
|
646
|
-
if self._package_store is not None and action.installer and action.package:
|
|
647
|
-
self._package_store.record_action_result(
|
|
648
|
-
action.installer,
|
|
649
|
-
str(action.package.name),
|
|
650
|
-
installed_version=result.installed_version or '',
|
|
651
|
-
available_version=result.available_version or '',
|
|
652
|
-
has_update=result.skip_reason == SkipReason.UPDATE_AVAILABLE,
|
|
653
|
-
)
|
|
654
|
-
|
|
655
663
|
# Update phase text
|
|
656
664
|
m.checked_count += 1
|
|
657
665
|
total = len(m.action_states)
|
|
@@ -660,7 +668,15 @@ class SetupPreviewWidget(QWidget):
|
|
|
660
668
|
)
|
|
661
669
|
|
|
662
670
|
def _on_preview_finished(self) -> None:
|
|
663
|
-
"""Finalize the preview after the dry-run check completes.
|
|
671
|
+
"""Finalize the preview after the dry-run check completes.
|
|
672
|
+
|
|
673
|
+
Computes the :class:`InstallPlan` via the operations layer,
|
|
674
|
+
batch-writes to :class:`PackageStateStore`, and updates all
|
|
675
|
+
button states. This is the single point where preview results
|
|
676
|
+
are materialised into actionable decisions.
|
|
677
|
+
"""
|
|
678
|
+
from synodic_client.operations.schema import ActionCheckResult, compute_install_plan
|
|
679
|
+
|
|
664
680
|
m = self._model
|
|
665
681
|
if not m.action_states:
|
|
666
682
|
return
|
|
@@ -679,50 +695,60 @@ class SetupPreviewWidget(QWidget):
|
|
|
679
695
|
finalized,
|
|
680
696
|
)
|
|
681
697
|
|
|
682
|
-
#
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
698
|
+
# Build check results for the plan computation
|
|
699
|
+
check_results: list[ActionCheckResult] = []
|
|
700
|
+
for i, state in enumerate(m.action_states):
|
|
701
|
+
# We need the dry-run result — reconstruct a minimal one from the status
|
|
702
|
+
# The actual result was already applied to the card; here we use the
|
|
703
|
+
# status string which is the canonical output of resolve_action_status.
|
|
704
|
+
check_results.append(
|
|
705
|
+
ActionCheckResult(
|
|
706
|
+
index=i,
|
|
707
|
+
action=state.action,
|
|
708
|
+
result=SetupActionResult(action=state.action, success=True),
|
|
709
|
+
status=state.status,
|
|
710
|
+
),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
plan = compute_install_plan(check_results)
|
|
714
|
+
m.install_plan = plan
|
|
715
|
+
|
|
716
|
+
# Batch-write to PackageStateStore (one-way, after plan is computed)
|
|
717
|
+
if self._package_store is not None and m.preview is not None:
|
|
718
|
+
for state in m.action_states:
|
|
719
|
+
action = state.action
|
|
720
|
+
if action.installer and action.package:
|
|
721
|
+
self._package_store.record_action_result(
|
|
722
|
+
action.installer,
|
|
723
|
+
str(action.package.name),
|
|
724
|
+
installed_version='',
|
|
725
|
+
available_version='',
|
|
726
|
+
has_update=state.status == 'Update available',
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
# Update UI from the plan
|
|
730
|
+
self._status_label.setText(plan.summary)
|
|
731
|
+
self._install_btn.setEnabled(plan.install_enabled)
|
|
732
|
+
if not plan.install_enabled:
|
|
733
|
+
self._install_btn.setToolTip('No packages to install')
|
|
710
734
|
else:
|
|
711
|
-
self.
|
|
735
|
+
self._install_btn.setToolTip('')
|
|
736
|
+
|
|
737
|
+
# Show/enable the Run Commands button if post-sync exists
|
|
738
|
+
if plan.has_post_sync:
|
|
739
|
+
self._run_commands_btn.show()
|
|
740
|
+
self._run_commands_btn.setEnabled(True)
|
|
712
741
|
|
|
713
742
|
self._set_phase(PreviewPhase.READY)
|
|
714
743
|
|
|
715
744
|
logger.info(
|
|
716
|
-
'Preview complete: %d total, %d
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
pending,
|
|
724
|
-
unavailable,
|
|
725
|
-
failed,
|
|
745
|
+
'Preview complete: %d total, %d to install, %d satisfied, %d upgradable, %d post-sync, install_enabled=%s',
|
|
746
|
+
len(m.action_states),
|
|
747
|
+
len(plan.install_indices),
|
|
748
|
+
len(plan.satisfied_indices),
|
|
749
|
+
len(plan.upgradable_indices),
|
|
750
|
+
len(plan.post_sync_indices),
|
|
751
|
+
plan.install_enabled,
|
|
726
752
|
)
|
|
727
753
|
|
|
728
754
|
def _on_preview_error(self, message: str) -> None:
|
|
@@ -769,14 +795,20 @@ class SetupPreviewWidget(QWidget):
|
|
|
769
795
|
# --- Install execution ---
|
|
770
796
|
|
|
771
797
|
def _on_install(self) -> None:
|
|
772
|
-
"""Handle the Install button click.
|
|
798
|
+
"""Handle the Install button click.
|
|
799
|
+
|
|
800
|
+
Uses the pre-computed :class:`InstallPlan` to determine the
|
|
801
|
+
sync strategy. Post-sync commands are excluded from this
|
|
802
|
+
execution — they are handled by :meth:`_on_run_commands`.
|
|
803
|
+
"""
|
|
773
804
|
m = self._model
|
|
774
|
-
if m.manifest_path is None:
|
|
805
|
+
if m.manifest_path is None or m.install_plan is None:
|
|
775
806
|
return
|
|
776
807
|
|
|
777
808
|
self._prerelease_debounce.stop()
|
|
778
809
|
self._set_phase(PreviewPhase.INSTALLING)
|
|
779
810
|
self._install_btn.setEnabled(False)
|
|
811
|
+
self._run_commands_btn.setEnabled(False)
|
|
780
812
|
self._close_btn.setEnabled(False)
|
|
781
813
|
m.completed_count = 0
|
|
782
814
|
|
|
@@ -786,9 +818,8 @@ class SetupPreviewWidget(QWidget):
|
|
|
786
818
|
self._log_panel.clear()
|
|
787
819
|
self._log_panel.show()
|
|
788
820
|
|
|
789
|
-
#
|
|
790
|
-
|
|
791
|
-
strategy = SyncStrategy.LATEST if m.upgradable_keys else SyncStrategy.MINIMAL
|
|
821
|
+
# Strategy and post-sync exclusion come from the plan
|
|
822
|
+
strategy = m.install_plan.strategy
|
|
792
823
|
|
|
793
824
|
self._task = asyncio.create_task(
|
|
794
825
|
self._run_install_task(
|
|
@@ -847,25 +878,39 @@ class SetupPreviewWidget(QWidget):
|
|
|
847
878
|
self._task.cancel()
|
|
848
879
|
|
|
849
880
|
def _on_install_finished(self, results: SetupResults) -> None:
|
|
850
|
-
"""Handle install completion.
|
|
851
|
-
self._set_phase(PreviewPhase.DONE)
|
|
881
|
+
"""Handle install completion.
|
|
852
882
|
|
|
853
|
-
|
|
854
|
-
|
|
883
|
+
If the plan includes post-sync commands and no actions failed,
|
|
884
|
+
automatically triggers :meth:`_on_run_commands`.
|
|
885
|
+
"""
|
|
886
|
+
from synodic_client.operations.schema import format_install_summary
|
|
887
|
+
|
|
888
|
+
m = self._model
|
|
889
|
+
pre_skipped = len(m.install_plan.satisfied_indices) if m.install_plan else 0
|
|
855
890
|
failed = sum(1 for r in results.results if not r.success)
|
|
856
891
|
|
|
857
|
-
|
|
858
|
-
if
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
892
|
+
# Auto-run post-sync if install succeeded and post-sync exists
|
|
893
|
+
if m.has_post_sync and failed == 0:
|
|
894
|
+
summary = format_install_summary(
|
|
895
|
+
install_results=list(results.results),
|
|
896
|
+
pre_skipped_count=pre_skipped,
|
|
897
|
+
)
|
|
898
|
+
self._status_label.setText(f'{summary}. Running post-sync commands\u2026')
|
|
899
|
+
self._run_commands_btn.setEnabled(False)
|
|
900
|
+
self._task = asyncio.create_task(self._run_post_sync_task())
|
|
901
|
+
self._install_results = results # Stash for final summary
|
|
902
|
+
return
|
|
864
903
|
|
|
865
|
-
|
|
866
|
-
|
|
904
|
+
self._set_phase(PreviewPhase.DONE)
|
|
905
|
+
summary = format_install_summary(
|
|
906
|
+
install_results=list(results.results),
|
|
907
|
+
pre_skipped_count=pre_skipped,
|
|
908
|
+
)
|
|
909
|
+
self._status_label.setText(summary)
|
|
867
910
|
self._install_btn.setEnabled(False)
|
|
868
911
|
self._close_btn.setEnabled(True)
|
|
912
|
+
if m.has_post_sync:
|
|
913
|
+
self._run_commands_btn.setEnabled(True)
|
|
869
914
|
self.install_finished.emit(results)
|
|
870
915
|
|
|
871
916
|
def _on_install_error(self, message: str) -> None:
|
|
@@ -875,6 +920,76 @@ class SetupPreviewWidget(QWidget):
|
|
|
875
920
|
self._install_btn.setEnabled(True)
|
|
876
921
|
self._close_btn.setEnabled(True)
|
|
877
922
|
|
|
923
|
+
# --- Post-sync execution ---
|
|
924
|
+
|
|
925
|
+
def _on_run_commands(self) -> None:
|
|
926
|
+
"""Handle the Run Commands button click."""
|
|
927
|
+
m = self._model
|
|
928
|
+
if m.manifest_path is None:
|
|
929
|
+
return
|
|
930
|
+
|
|
931
|
+
self._run_commands_btn.setEnabled(False)
|
|
932
|
+
self._install_btn.setEnabled(False)
|
|
933
|
+
self._close_btn.setEnabled(False)
|
|
934
|
+
|
|
935
|
+
self._status_label.setText('Running post-sync commands\u2026')
|
|
936
|
+
|
|
937
|
+
if not self._log_panel.isVisible():
|
|
938
|
+
self._log_panel.clear()
|
|
939
|
+
self._log_panel.show()
|
|
940
|
+
|
|
941
|
+
self._install_results = None
|
|
942
|
+
self._task = asyncio.create_task(self._run_post_sync_task())
|
|
943
|
+
|
|
944
|
+
async def _run_post_sync_task(self) -> None:
|
|
945
|
+
"""Run the post-sync coroutine and route completion/errors."""
|
|
946
|
+
assert self._model.manifest_path is not None
|
|
947
|
+
try:
|
|
948
|
+
results = await run_post_sync(
|
|
949
|
+
self._porringer,
|
|
950
|
+
self._model.manifest_path,
|
|
951
|
+
project_directory=self._model.project_directory,
|
|
952
|
+
callbacks=InstallCallbacks(
|
|
953
|
+
on_action_started=self._on_action_started,
|
|
954
|
+
on_sub_progress=self._on_sub_progress,
|
|
955
|
+
on_progress=self._on_action_progress,
|
|
956
|
+
),
|
|
957
|
+
plugins=self._discovered_plugins,
|
|
958
|
+
)
|
|
959
|
+
self._on_post_sync_finished(results)
|
|
960
|
+
except asyncio.CancelledError:
|
|
961
|
+
self._on_post_sync_finished(SetupResults(actions=[]))
|
|
962
|
+
except Exception as exc:
|
|
963
|
+
logger.exception('Post-sync execution failed')
|
|
964
|
+
self._on_install_error(f'Post-sync failed: {exc}')
|
|
965
|
+
|
|
966
|
+
def _on_post_sync_finished(self, results: SetupResults) -> None:
|
|
967
|
+
"""Handle post-sync completion."""
|
|
968
|
+
from synodic_client.operations.schema import format_install_summary
|
|
969
|
+
|
|
970
|
+
m = self._model
|
|
971
|
+
m.post_sync_completed = True
|
|
972
|
+
m.post_sync_results = list(results.results)
|
|
973
|
+
|
|
974
|
+
self._set_phase(PreviewPhase.DONE)
|
|
975
|
+
|
|
976
|
+
pre_skipped = len(m.install_plan.satisfied_indices) if m.install_plan else 0
|
|
977
|
+
|
|
978
|
+
# If we have stashed install results (auto-run path), use combined summary
|
|
979
|
+
install_results = (
|
|
980
|
+
list(self._install_results.results) if hasattr(self, '_install_results') and self._install_results else None
|
|
981
|
+
)
|
|
982
|
+
summary = format_install_summary(
|
|
983
|
+
install_results=install_results,
|
|
984
|
+
post_sync_results=m.post_sync_results,
|
|
985
|
+
pre_skipped_count=pre_skipped,
|
|
986
|
+
)
|
|
987
|
+
self._status_label.setText(summary)
|
|
988
|
+
self._install_btn.setEnabled(False)
|
|
989
|
+
self._run_commands_btn.setEnabled(False)
|
|
990
|
+
self._close_btn.setEnabled(True)
|
|
991
|
+
self.install_finished.emit(results)
|
|
992
|
+
|
|
878
993
|
|
|
879
994
|
# ---------------------------------------------------------------------------
|
|
880
995
|
# InstallPreviewWindow — standalone URI-based install window
|