synodic-client 0.0.1.dev80__tar.gz → 0.0.1.dev81__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.dev80 → synodic_client-0.0.1.dev81}/PKG-INFO +3 -3
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/pyproject.toml +3 -3
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/action_card.py +92 -15
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/screen.py +20 -0
- synodic_client-0.0.1.dev81/synodic_client/application/screen/wsl.py +419 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/theme.py +16 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/install.py +1 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/schema.py +2 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/updater.py +3 -4
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/README.md +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/debug.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/install.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/install_workers.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/schema.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/tray.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/update_banner.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/update_controller.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/update_model.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/config.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/context.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/debug.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/install.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/output.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/project.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/tool.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/cli/update.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/config.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/project.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/tool.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/update.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_config.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_install.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_install_plan.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_project.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_tool.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_update.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_install_preview.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_log_panel.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_package_state.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_preview_model.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_settings.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_banner.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_controller.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_bootstrap.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/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.dev81
|
|
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.dev88
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
|
-
Requires-Dist: velopack>=0.0.
|
|
13
|
+
Requires-Dist: velopack>=0.0.1535.dev45597
|
|
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.dev88",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
|
-
"velopack>=0.0.
|
|
15
|
+
"velopack>=0.0.1535.dev45597",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev81"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -40,6 +40,7 @@ from synodic_client.application.screen.spinner import SpinnerCanvas
|
|
|
40
40
|
from synodic_client.application.theme import (
|
|
41
41
|
ACTION_CARD_COMMAND_STYLE,
|
|
42
42
|
ACTION_CARD_DESC_STYLE,
|
|
43
|
+
ACTION_CARD_DISTRO_BADGE_STYLE,
|
|
43
44
|
ACTION_CARD_EXECUTING_STYLE,
|
|
44
45
|
ACTION_CARD_PACKAGE_STYLE,
|
|
45
46
|
ACTION_CARD_SKELETON_BAR_STYLE,
|
|
@@ -64,6 +65,7 @@ from synodic_client.application.theme import (
|
|
|
64
65
|
COPY_BTN_STYLE,
|
|
65
66
|
COPY_FEEDBACK_MS,
|
|
66
67
|
COPY_ICON,
|
|
68
|
+
WSL_DISTRO_HEADER_STYLE,
|
|
67
69
|
)
|
|
68
70
|
|
|
69
71
|
logger = logging.getLogger(__name__)
|
|
@@ -224,7 +226,7 @@ class ActionCard(QFrame):
|
|
|
224
226
|
outer.addWidget(self._build_command_row())
|
|
225
227
|
|
|
226
228
|
def _build_top_row(self) -> QHBoxLayout:
|
|
227
|
-
"""Build the top row: type badge | package name ... version | status/spinner | prerelease."""
|
|
229
|
+
"""Build the top row: type badge | [distro badge] | package name ... version | status/spinner | prerelease."""
|
|
228
230
|
top = QHBoxLayout()
|
|
229
231
|
top.setSpacing(8)
|
|
230
232
|
|
|
@@ -232,6 +234,11 @@ class ActionCard(QFrame):
|
|
|
232
234
|
self._type_badge.setStyleSheet(ACTION_CARD_TYPE_BADGE_STYLE)
|
|
233
235
|
top.addWidget(self._type_badge)
|
|
234
236
|
|
|
237
|
+
self._distro_badge = QLabel()
|
|
238
|
+
self._distro_badge.setStyleSheet(ACTION_CARD_DISTRO_BADGE_STYLE)
|
|
239
|
+
self._distro_badge.hide()
|
|
240
|
+
top.addWidget(self._distro_badge)
|
|
241
|
+
|
|
235
242
|
self._package_label = QLabel()
|
|
236
243
|
self._package_label.setStyleSheet(ACTION_CARD_PACKAGE_STYLE)
|
|
237
244
|
self._package_label.setTextInteractionFlags(
|
|
@@ -360,6 +367,12 @@ class ActionCard(QFrame):
|
|
|
360
367
|
if action.installer:
|
|
361
368
|
self._type_badge.setToolTip(f'Plugin: {action.installer}')
|
|
362
369
|
|
|
370
|
+
if action.distro:
|
|
371
|
+
self._distro_badge.setText(action.distro)
|
|
372
|
+
self._distro_badge.show()
|
|
373
|
+
else:
|
|
374
|
+
self._distro_badge.hide()
|
|
375
|
+
|
|
363
376
|
package_text = str(action.package) if action.package else action.description
|
|
364
377
|
self._package_label.setText(package_text)
|
|
365
378
|
|
|
@@ -635,6 +648,32 @@ class ActionCard(QFrame):
|
|
|
635
648
|
super().mousePressEvent(event)
|
|
636
649
|
|
|
637
650
|
|
|
651
|
+
# ---------------------------------------------------------------------------
|
|
652
|
+
# _DistroGroupHeader — section divider between native and WSL action groups
|
|
653
|
+
# ---------------------------------------------------------------------------
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
class _DistroGroupHeader(QLabel):
|
|
657
|
+
"""Thin section divider between native and per-distro action groups.
|
|
658
|
+
|
|
659
|
+
Shows ``HOST`` for the native group or ``WSL — <distro>`` for each
|
|
660
|
+
WSL2 distro group. Only inserted when both native and WSL actions
|
|
661
|
+
are present.
|
|
662
|
+
"""
|
|
663
|
+
|
|
664
|
+
def __init__(self, distro_name: str | None, parent: QWidget | None = None) -> None:
|
|
665
|
+
"""Initialise the header.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
distro_name: WSL distro name, or ``None`` for the native group.
|
|
669
|
+
parent: Optional parent widget.
|
|
670
|
+
"""
|
|
671
|
+
label = 'HOST' if distro_name is None else f'WSL \u2014 {distro_name}'.upper()
|
|
672
|
+
super().__init__(label, parent)
|
|
673
|
+
self.setObjectName('wslDistroHeader')
|
|
674
|
+
self.setStyleSheet(WSL_DISTRO_HEADER_STYLE)
|
|
675
|
+
|
|
676
|
+
|
|
638
677
|
# ---------------------------------------------------------------------------
|
|
639
678
|
# ActionCardList — card container
|
|
640
679
|
# ---------------------------------------------------------------------------
|
|
@@ -667,6 +706,7 @@ class ActionCardList(QWidget):
|
|
|
667
706
|
self._layout.addStretch()
|
|
668
707
|
|
|
669
708
|
self._cards: list[ActionCard] = []
|
|
709
|
+
self._group_headers: list[QLabel] = []
|
|
670
710
|
self._action_map: dict[SetupAction, ActionCard] = {}
|
|
671
711
|
self._index_map: dict[int, ActionCard] = {}
|
|
672
712
|
|
|
@@ -701,25 +741,58 @@ class ActionCardList(QWidget):
|
|
|
701
741
|
) -> None:
|
|
702
742
|
"""Replace skeleton cards with real action cards.
|
|
703
743
|
|
|
744
|
+
When actions include WSL distro entries (``action.distro is not
|
|
745
|
+
None``), the list is split into groups. Native host actions
|
|
746
|
+
appear first under a ``HOST`` section header; each WSL distro
|
|
747
|
+
gets its own ``WSL — <distro>`` section header below. If all
|
|
748
|
+
actions are native, no headers are inserted.
|
|
749
|
+
|
|
704
750
|
Args:
|
|
705
751
|
actions: The setup actions to display.
|
|
706
752
|
plugin_installed: Plugin name → installed mapping.
|
|
707
753
|
prerelease_overrides: Package names with user pre-release overrides.
|
|
708
754
|
"""
|
|
709
755
|
self.clear()
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
756
|
+
|
|
757
|
+
# Partition into native and per-distro groups.
|
|
758
|
+
native_actions: list[SetupAction] = []
|
|
759
|
+
distro_actions: dict[str, list[SetupAction]] = {}
|
|
760
|
+
for act in actions:
|
|
761
|
+
if act.distro is None:
|
|
762
|
+
native_actions.append(act)
|
|
763
|
+
else:
|
|
764
|
+
distro_actions.setdefault(act.distro, []).append(act)
|
|
765
|
+
|
|
766
|
+
has_wsl = bool(distro_actions)
|
|
767
|
+
|
|
768
|
+
def _add_group(group_actions: list[SetupAction]) -> None:
|
|
769
|
+
for act in sorted(group_actions, key=action_sort_key):
|
|
770
|
+
card = ActionCard(self)
|
|
771
|
+
card.populate(
|
|
772
|
+
act,
|
|
773
|
+
plugin_installed=plugin_installed,
|
|
774
|
+
prerelease_overrides=prerelease_overrides,
|
|
775
|
+
)
|
|
776
|
+
card.prerelease_toggled.connect(self.prerelease_toggled.emit)
|
|
777
|
+
card.navigate_to_tool.connect(self.navigate_to_tool.emit)
|
|
778
|
+
self._layout.insertWidget(self._layout.count() - 1, card)
|
|
779
|
+
self._cards.append(card)
|
|
780
|
+
self._action_map[act] = card
|
|
781
|
+
|
|
782
|
+
def _add_header(distro_name: str | None) -> None:
|
|
783
|
+
header = _DistroGroupHeader(distro_name, self)
|
|
784
|
+
self._layout.insertWidget(self._layout.count() - 1, header)
|
|
785
|
+
self._group_headers.append(header)
|
|
786
|
+
|
|
787
|
+
# Native actions (with host header only when WSL groups also exist)
|
|
788
|
+
if has_wsl and native_actions:
|
|
789
|
+
_add_header(None)
|
|
790
|
+
_add_group(native_actions)
|
|
791
|
+
|
|
792
|
+
# Per-distro groups
|
|
793
|
+
for distro_name in sorted(distro_actions):
|
|
794
|
+
_add_header(distro_name)
|
|
795
|
+
_add_group(distro_actions[distro_name])
|
|
723
796
|
|
|
724
797
|
# Build original-index → card mapping so callers can look up by
|
|
725
798
|
# the action index porringer emits, which is independent of the
|
|
@@ -776,7 +849,11 @@ class ActionCardList(QWidget):
|
|
|
776
849
|
card.finalize_checking()
|
|
777
850
|
|
|
778
851
|
def clear(self) -> None:
|
|
779
|
-
"""Remove all cards."""
|
|
852
|
+
"""Remove all cards and group headers."""
|
|
853
|
+
for header in self._group_headers:
|
|
854
|
+
self._layout.removeWidget(header)
|
|
855
|
+
header.deleteLater()
|
|
856
|
+
self._group_headers.clear()
|
|
780
857
|
for card in self._cards:
|
|
781
858
|
self._layout.removeWidget(card)
|
|
782
859
|
card.deleteLater()
|
|
@@ -50,6 +50,7 @@ from synodic_client.application.screen.schema import (
|
|
|
50
50
|
)
|
|
51
51
|
from synodic_client.application.screen.spinner import LoadingIndicator
|
|
52
52
|
from synodic_client.application.screen.update_banner import UpdateBanner
|
|
53
|
+
from synodic_client.application.screen.wsl import WslView
|
|
53
54
|
from synodic_client.application.theme import (
|
|
54
55
|
COMPACT_MARGINS,
|
|
55
56
|
FILTER_CHIP_SPACING,
|
|
@@ -1434,6 +1435,7 @@ class MainWindow(QMainWindow):
|
|
|
1434
1435
|
_tabs: QTabWidget | None = None
|
|
1435
1436
|
_tools_view: ToolsView | None = None
|
|
1436
1437
|
_projects_view: ProjectsView | None = None
|
|
1438
|
+
_wsl_view: WslView | None = None
|
|
1437
1439
|
|
|
1438
1440
|
def __init__(
|
|
1439
1441
|
self,
|
|
@@ -1517,6 +1519,22 @@ class MainWindow(QMainWindow):
|
|
|
1517
1519
|
self._tabs.addTab(self._tools_view, 'Tools')
|
|
1518
1520
|
self.tools_view_created.emit(self._tools_view)
|
|
1519
1521
|
|
|
1522
|
+
# WSL tab — only on Windows hosts with WSL available.
|
|
1523
|
+
try:
|
|
1524
|
+
from porringer.plugin.wsl.utility import is_wsl_host
|
|
1525
|
+
|
|
1526
|
+
if is_wsl_host():
|
|
1527
|
+
self._wsl_view = WslView(
|
|
1528
|
+
self._porringer,
|
|
1529
|
+
self._store,
|
|
1530
|
+
self,
|
|
1531
|
+
coordinator=self._coordinator,
|
|
1532
|
+
package_store=self._package_store,
|
|
1533
|
+
)
|
|
1534
|
+
self._tabs.addTab(self._wsl_view, 'WSL')
|
|
1535
|
+
except Exception:
|
|
1536
|
+
logger.debug('Could not initialise WSL tab', exc_info=True)
|
|
1537
|
+
|
|
1520
1538
|
# Navigate-to-project: switch to Projects tab and select directory
|
|
1521
1539
|
self._tools_view.navigate_to_project_requested.connect(self._navigate_to_project)
|
|
1522
1540
|
|
|
@@ -1546,6 +1564,8 @@ class MainWindow(QMainWindow):
|
|
|
1546
1564
|
self._tools_view.refresh()
|
|
1547
1565
|
if self._projects_view is not None:
|
|
1548
1566
|
self._projects_view.refresh()
|
|
1567
|
+
if self._wsl_view is not None:
|
|
1568
|
+
self._wsl_view.refresh()
|
|
1549
1569
|
|
|
1550
1570
|
def _navigate_to_project(self, path_str: str) -> None:
|
|
1551
1571
|
"""Switch to the Projects tab and select the given directory."""
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""WSL package management view.
|
|
2
|
+
|
|
3
|
+
Shows packages installed in each WSL2 distro, using the same plugin row
|
|
4
|
+
widgets as :class:`~synodic_client.application.screen.screen.ToolsView`
|
|
5
|
+
but scoped per-distro.
|
|
6
|
+
|
|
7
|
+
Only rendered when :func:`porringer.plugin.wsl.utility.is_wsl_host`
|
|
8
|
+
returns ``True`` (Windows + ``wsl`` on PATH).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import logging
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from porringer.schema.plugin import PluginKind
|
|
18
|
+
from PySide6.QtCore import Qt, Signal
|
|
19
|
+
from PySide6.QtWidgets import (
|
|
20
|
+
QHBoxLayout,
|
|
21
|
+
QLabel,
|
|
22
|
+
QPushButton,
|
|
23
|
+
QScrollArea,
|
|
24
|
+
QVBoxLayout,
|
|
25
|
+
QWidget,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from synodic_client.application.screen.plugin_row import (
|
|
29
|
+
PluginProviderHeader,
|
|
30
|
+
PluginRow,
|
|
31
|
+
RowPhase,
|
|
32
|
+
)
|
|
33
|
+
from synodic_client.application.screen.schema import PackageEntry, PluginRowData
|
|
34
|
+
from synodic_client.application.screen.spinner import LoadingIndicator
|
|
35
|
+
from synodic_client.application.theme import (
|
|
36
|
+
COMPACT_MARGINS,
|
|
37
|
+
PLUGIN_KIND_HEADER_STYLE,
|
|
38
|
+
PLUGIN_SECTION_SPACING,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from porringer.api import API
|
|
43
|
+
from porringer.schema import PluginInfo
|
|
44
|
+
|
|
45
|
+
from synodic_client.application.config_store import ConfigStore
|
|
46
|
+
from synodic_client.application.data import DataCoordinator
|
|
47
|
+
from synodic_client.application.package_state import PackageStateStore
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger(__name__)
|
|
50
|
+
|
|
51
|
+
#: Plugin kinds queried when listing packages inside a WSL distro.
|
|
52
|
+
_WSL_QUERYABLE_KINDS = frozenset({PluginKind.TOOL, PluginKind.PACKAGE})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# _WslDistroHeader — section divider for one WSL distro
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _WslDistroHeader(QLabel):
|
|
61
|
+
"""Uppercase section divider labelling a WSL2 distro group.
|
|
62
|
+
|
|
63
|
+
Styled like :class:`PluginKindHeader` but with a purple tint to
|
|
64
|
+
visually distinguish WSL sections from native host sections.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(self, distro_name: str, parent: QWidget | None = None) -> None:
|
|
68
|
+
super().__init__(distro_name.upper(), parent)
|
|
69
|
+
self.setObjectName('pluginKindHeader')
|
|
70
|
+
self.setStyleSheet(PLUGIN_KIND_HEADER_STYLE)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# WslView
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class WslView(QWidget):
|
|
79
|
+
"""Tool management view for packages installed inside WSL2 distros.
|
|
80
|
+
|
|
81
|
+
Mirrors the structure of ``ToolsView`` — distro section headers,
|
|
82
|
+
per-plugin :class:`PluginProviderHeader` rows, and per-package
|
|
83
|
+
:class:`PluginRow` widgets — but queries are routed through the
|
|
84
|
+
``distro=`` parameter on :meth:`porringer.PackageCommands.list`,
|
|
85
|
+
:meth:`~porringer.PackageCommands.upgrade`, and
|
|
86
|
+
:meth:`~porringer.PackageCommands.uninstall`.
|
|
87
|
+
|
|
88
|
+
Each distro's packages are loaded in parallel. Packages are
|
|
89
|
+
displayed globally (WSL environments don't have per-project venvs
|
|
90
|
+
in the same sense as the host).
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
package_update_requested = Signal(str, str, str)
|
|
94
|
+
"""Emitted with ``(distro, plugin_name, package_name)`` when update is clicked."""
|
|
95
|
+
|
|
96
|
+
package_remove_requested = Signal(str, str, str)
|
|
97
|
+
"""Emitted with ``(distro, plugin_name, package_name)`` when remove is clicked."""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
porringer: API,
|
|
102
|
+
store: ConfigStore,
|
|
103
|
+
parent: QWidget | None = None,
|
|
104
|
+
*,
|
|
105
|
+
coordinator: DataCoordinator | None = None,
|
|
106
|
+
package_store: PackageStateStore | None = None,
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Initialize the WSL view.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
porringer: The porringer API instance.
|
|
112
|
+
store: The centralised :class:`ConfigStore`.
|
|
113
|
+
parent: Optional parent widget.
|
|
114
|
+
coordinator: Shared data coordinator for reusing discovery results.
|
|
115
|
+
package_store: Shared package update state registry (currently unused
|
|
116
|
+
by WslView but accepted for API consistency with ToolsView).
|
|
117
|
+
"""
|
|
118
|
+
super().__init__(parent)
|
|
119
|
+
self._porringer = porringer
|
|
120
|
+
self._store = store
|
|
121
|
+
self._coordinator = coordinator
|
|
122
|
+
self._section_widgets: list[QWidget] = []
|
|
123
|
+
self._plugin_info_map: dict[str, PluginInfo] = {}
|
|
124
|
+
self._refresh_in_progress = False
|
|
125
|
+
self._init_ui()
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
# UI construction
|
|
129
|
+
# ------------------------------------------------------------------
|
|
130
|
+
|
|
131
|
+
def _init_ui(self) -> None:
|
|
132
|
+
"""Build the view layout: toolbar, scroll area, loading indicator."""
|
|
133
|
+
outer = QVBoxLayout(self)
|
|
134
|
+
outer.setContentsMargins(*COMPACT_MARGINS)
|
|
135
|
+
|
|
136
|
+
outer.addLayout(self._build_toolbar())
|
|
137
|
+
|
|
138
|
+
self._scroll = QScrollArea()
|
|
139
|
+
self._scroll.setWidgetResizable(True)
|
|
140
|
+
self._scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
141
|
+
|
|
142
|
+
self._container = QWidget()
|
|
143
|
+
self._container_layout = QVBoxLayout(self._container)
|
|
144
|
+
self._container_layout.setSpacing(PLUGIN_SECTION_SPACING)
|
|
145
|
+
self._container_layout.setContentsMargins(0, 0, 0, 0)
|
|
146
|
+
self._container_layout.addStretch()
|
|
147
|
+
|
|
148
|
+
self._scroll.setWidget(self._container)
|
|
149
|
+
outer.addWidget(self._scroll)
|
|
150
|
+
|
|
151
|
+
self._loading_indicator = LoadingIndicator('Loading WSL packages\u2026')
|
|
152
|
+
outer.addWidget(self._loading_indicator)
|
|
153
|
+
|
|
154
|
+
def _build_toolbar(self) -> QHBoxLayout:
|
|
155
|
+
"""Build the toolbar with a refresh button."""
|
|
156
|
+
toolbar = QHBoxLayout()
|
|
157
|
+
toolbar.addStretch()
|
|
158
|
+
|
|
159
|
+
self._refresh_btn = QPushButton('Refresh')
|
|
160
|
+
self._refresh_btn.setFlat(True)
|
|
161
|
+
self._refresh_btn.setToolTip('Re-scan WSL packages')
|
|
162
|
+
self._refresh_btn.clicked.connect(self.refresh)
|
|
163
|
+
toolbar.addWidget(self._refresh_btn)
|
|
164
|
+
|
|
165
|
+
return toolbar
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------------
|
|
168
|
+
# Public API
|
|
169
|
+
# ------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
def refresh(self) -> None:
|
|
172
|
+
"""Schedule an asynchronous rebuild of the WSL package list."""
|
|
173
|
+
if self._refresh_in_progress:
|
|
174
|
+
return
|
|
175
|
+
asyncio.create_task(self._async_refresh())
|
|
176
|
+
|
|
177
|
+
# ------------------------------------------------------------------
|
|
178
|
+
# Async refresh pipeline
|
|
179
|
+
# ------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
async def _async_refresh(self) -> None:
|
|
182
|
+
"""Rebuild the WSL package tree from porringer data."""
|
|
183
|
+
self._refresh_in_progress = True
|
|
184
|
+
self._scroll.hide()
|
|
185
|
+
self._loading_indicator.start()
|
|
186
|
+
self._refresh_btn.setEnabled(False)
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
data = await self._gather_wsl_data()
|
|
190
|
+
self._build_widget_tree(data)
|
|
191
|
+
except Exception:
|
|
192
|
+
logger.exception('Failed to refresh WSL packages')
|
|
193
|
+
finally:
|
|
194
|
+
self._loading_indicator.stop()
|
|
195
|
+
self._scroll.show()
|
|
196
|
+
self._refresh_btn.setEnabled(True)
|
|
197
|
+
self._refresh_in_progress = False
|
|
198
|
+
|
|
199
|
+
async def _gather_wsl_data(self) -> dict[str, dict[str, list[PackageEntry]]]:
|
|
200
|
+
"""Fetch packages for every available WSL2 distro.
|
|
201
|
+
|
|
202
|
+
For each distro, all TOOL- and PACKAGE-kind plugins are queried
|
|
203
|
+
in parallel using ``porringer.package.list(..., distro=distro)``.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
``{distro_name: {plugin_name: [PackageEntry, ...]}}`` for
|
|
207
|
+
distros that have at least one non-empty plugin result.
|
|
208
|
+
"""
|
|
209
|
+
from porringer.plugin.wsl.utility import available_distros
|
|
210
|
+
|
|
211
|
+
discovered = self._coordinator.discovered_plugins if self._coordinator else None
|
|
212
|
+
|
|
213
|
+
if self._coordinator is not None:
|
|
214
|
+
snapshot = await self._coordinator.refresh()
|
|
215
|
+
plugins = snapshot.plugins
|
|
216
|
+
else:
|
|
217
|
+
plugins = await self._porringer.plugin.list()
|
|
218
|
+
|
|
219
|
+
# Update the plugin-info lookup for PluginProviderHeader construction.
|
|
220
|
+
self._plugin_info_map = {p.name: p for p in plugins}
|
|
221
|
+
|
|
222
|
+
queryable = [p for p in plugins if p.kind in _WSL_QUERYABLE_KINDS and p.installed]
|
|
223
|
+
|
|
224
|
+
loop = asyncio.get_running_loop()
|
|
225
|
+
distros: list[str] = await loop.run_in_executor(None, available_distros)
|
|
226
|
+
|
|
227
|
+
result: dict[str, dict[str, list[PackageEntry]]] = {}
|
|
228
|
+
|
|
229
|
+
async def _fetch_distro(distro: str) -> None:
|
|
230
|
+
distro_pkgs: dict[str, list[PackageEntry]] = {}
|
|
231
|
+
|
|
232
|
+
async def _fetch_plugin(plugin: PluginInfo) -> None:
|
|
233
|
+
try:
|
|
234
|
+
pkgs = await self._porringer.package.list(
|
|
235
|
+
plugin.name,
|
|
236
|
+
plugins=discovered,
|
|
237
|
+
distro=distro,
|
|
238
|
+
)
|
|
239
|
+
if pkgs:
|
|
240
|
+
distro_pkgs[plugin.name] = [
|
|
241
|
+
PackageEntry(
|
|
242
|
+
name=str(pkg.name),
|
|
243
|
+
version=str(pkg.version) if pkg.version else '',
|
|
244
|
+
host_tool=pkg.relation.host if pkg.relation else '',
|
|
245
|
+
)
|
|
246
|
+
for pkg in pkgs
|
|
247
|
+
]
|
|
248
|
+
except Exception:
|
|
249
|
+
logger.debug(
|
|
250
|
+
'Could not list packages for %s in distro %s',
|
|
251
|
+
plugin.name,
|
|
252
|
+
distro,
|
|
253
|
+
exc_info=True,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
async with asyncio.TaskGroup() as tg:
|
|
257
|
+
for plugin in queryable:
|
|
258
|
+
tg.create_task(_fetch_plugin(plugin))
|
|
259
|
+
|
|
260
|
+
if distro_pkgs:
|
|
261
|
+
result[distro] = distro_pkgs
|
|
262
|
+
|
|
263
|
+
async with asyncio.TaskGroup() as tg:
|
|
264
|
+
for distro in distros:
|
|
265
|
+
tg.create_task(_fetch_distro(distro))
|
|
266
|
+
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
# ------------------------------------------------------------------
|
|
270
|
+
# Widget tree construction
|
|
271
|
+
# ------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
def _build_widget_tree(self, data: dict[str, dict[str, list[PackageEntry]]]) -> None:
|
|
274
|
+
"""Clear and rebuild section widgets from *data*."""
|
|
275
|
+
self._clear_section_widgets()
|
|
276
|
+
|
|
277
|
+
if not data:
|
|
278
|
+
empty = QLabel('No WSL packages found.')
|
|
279
|
+
empty.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
280
|
+
empty.setStyleSheet('color: grey; padding: 20px;')
|
|
281
|
+
self._insert_section_widget(empty)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
for distro_name in sorted(data):
|
|
285
|
+
header = _WslDistroHeader(distro_name, parent=self._container)
|
|
286
|
+
self._insert_section_widget(header)
|
|
287
|
+
|
|
288
|
+
for plugin_name, packages in sorted(data[distro_name].items()):
|
|
289
|
+
plugin_info = self._plugin_info_map.get(plugin_name)
|
|
290
|
+
if plugin_info is not None:
|
|
291
|
+
provider = PluginProviderHeader(
|
|
292
|
+
plugin_info,
|
|
293
|
+
show_controls=False,
|
|
294
|
+
parent=self._container,
|
|
295
|
+
)
|
|
296
|
+
self._insert_section_widget(provider)
|
|
297
|
+
|
|
298
|
+
for entry in packages:
|
|
299
|
+
row = PluginRow(
|
|
300
|
+
PluginRowData(
|
|
301
|
+
name=entry.name,
|
|
302
|
+
version=entry.version,
|
|
303
|
+
plugin_name=plugin_name,
|
|
304
|
+
show_toggle=False,
|
|
305
|
+
is_global=True,
|
|
306
|
+
host_tool=entry.host_tool,
|
|
307
|
+
),
|
|
308
|
+
parent=self._container,
|
|
309
|
+
)
|
|
310
|
+
# Capture distro_name in the lambda so the handler
|
|
311
|
+
# knows which distro to route the operation to.
|
|
312
|
+
row.update_requested.connect(
|
|
313
|
+
lambda pn, pkg, d=distro_name: asyncio.create_task(
|
|
314
|
+
self._update_package(d, pn, pkg),
|
|
315
|
+
),
|
|
316
|
+
)
|
|
317
|
+
row.remove_requested.connect(
|
|
318
|
+
lambda pn, pkg, d=distro_name: asyncio.create_task(
|
|
319
|
+
self._remove_package(d, pn, pkg),
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
self._insert_section_widget(row)
|
|
323
|
+
|
|
324
|
+
def _insert_section_widget(self, widget: QWidget) -> None:
|
|
325
|
+
"""Append *widget* to the container layout above the trailing stretch."""
|
|
326
|
+
idx = self._container_layout.count() - 1
|
|
327
|
+
self._container_layout.insertWidget(idx, widget)
|
|
328
|
+
self._section_widgets.append(widget)
|
|
329
|
+
|
|
330
|
+
def _clear_section_widgets(self) -> None:
|
|
331
|
+
"""Remove and delete all current section widgets."""
|
|
332
|
+
for widget in self._section_widgets:
|
|
333
|
+
self._container_layout.removeWidget(widget)
|
|
334
|
+
widget.deleteLater()
|
|
335
|
+
self._section_widgets.clear()
|
|
336
|
+
|
|
337
|
+
# ------------------------------------------------------------------
|
|
338
|
+
# Package operations
|
|
339
|
+
# ------------------------------------------------------------------
|
|
340
|
+
|
|
341
|
+
async def _update_package(self, distro: str, plugin_name: str, package_name: str) -> None:
|
|
342
|
+
"""Upgrade *package_name* inside *distro* via *plugin_name*.
|
|
343
|
+
|
|
344
|
+
Transitions the matching :class:`PluginRow` to the ``UPDATING``
|
|
345
|
+
phase, executes the upgrade, then returns it to ``IDLE`` or
|
|
346
|
+
shows an inline error on failure.
|
|
347
|
+
"""
|
|
348
|
+
from porringer.core.schema import PackageRef
|
|
349
|
+
|
|
350
|
+
discovered = self._coordinator.discovered_plugins if self._coordinator else None
|
|
351
|
+
row = self._find_row(plugin_name, package_name)
|
|
352
|
+
if row is not None:
|
|
353
|
+
row.set_phase(RowPhase.UPDATING)
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
ref = PackageRef(name=package_name)
|
|
357
|
+
action_result = await self._porringer.package.upgrade(
|
|
358
|
+
plugin_name,
|
|
359
|
+
ref,
|
|
360
|
+
plugins=discovered,
|
|
361
|
+
distro=distro,
|
|
362
|
+
)
|
|
363
|
+
if not action_result.success and not action_result.skipped:
|
|
364
|
+
msg = action_result.message or 'Update failed'
|
|
365
|
+
logger.warning('WSL package update failed: %s/%s in %s: %s', plugin_name, package_name, distro, msg)
|
|
366
|
+
if row is not None:
|
|
367
|
+
row.set_error(msg)
|
|
368
|
+
else:
|
|
369
|
+
if row is not None:
|
|
370
|
+
row.set_phase(RowPhase.IDLE)
|
|
371
|
+
self.package_update_requested.emit(distro, plugin_name, package_name)
|
|
372
|
+
except Exception:
|
|
373
|
+
logger.exception('WSL update error: %s/%s in %s', plugin_name, package_name, distro)
|
|
374
|
+
if row is not None:
|
|
375
|
+
row.set_error('Update error — see log')
|
|
376
|
+
|
|
377
|
+
async def _remove_package(self, distro: str, plugin_name: str, package_name: str) -> None:
|
|
378
|
+
"""Uninstall *package_name* from *distro* via *plugin_name*.
|
|
379
|
+
|
|
380
|
+
On success, triggers a refresh so the removed package disappears
|
|
381
|
+
from the list. On failure, shows an inline error on the row.
|
|
382
|
+
"""
|
|
383
|
+
from porringer.core.schema import PackageRef
|
|
384
|
+
|
|
385
|
+
discovered = self._coordinator.discovered_plugins if self._coordinator else None
|
|
386
|
+
row = self._find_row(plugin_name, package_name)
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
ref = PackageRef(name=package_name)
|
|
390
|
+
action_result = await self._porringer.package.uninstall(
|
|
391
|
+
plugin_name,
|
|
392
|
+
ref,
|
|
393
|
+
plugins=discovered,
|
|
394
|
+
distro=distro,
|
|
395
|
+
)
|
|
396
|
+
if action_result.success or action_result.skipped:
|
|
397
|
+
self.package_remove_requested.emit(distro, plugin_name, package_name)
|
|
398
|
+
# Refresh to remove the row from the list.
|
|
399
|
+
self.refresh()
|
|
400
|
+
else:
|
|
401
|
+
msg = action_result.message or 'Removal failed'
|
|
402
|
+
logger.warning('WSL package removal failed: %s/%s in %s: %s', plugin_name, package_name, distro, msg)
|
|
403
|
+
if row is not None:
|
|
404
|
+
row.set_error(msg)
|
|
405
|
+
except Exception:
|
|
406
|
+
logger.exception('WSL removal error: %s/%s in %s', plugin_name, package_name, distro)
|
|
407
|
+
if row is not None:
|
|
408
|
+
row.set_error('Removal error — see log')
|
|
409
|
+
|
|
410
|
+
def _find_row(self, plugin_name: str, package_name: str) -> PluginRow | None:
|
|
411
|
+
"""Return the first :class:`PluginRow` matching *plugin_name* and *package_name*."""
|
|
412
|
+
for widget in self._section_widgets:
|
|
413
|
+
if (
|
|
414
|
+
isinstance(widget, PluginRow)
|
|
415
|
+
and widget._plugin_name == plugin_name
|
|
416
|
+
and widget._package_name == package_name
|
|
417
|
+
):
|
|
418
|
+
return widget
|
|
419
|
+
return None
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/theme.py
RENAMED
|
@@ -448,6 +448,22 @@ ACTION_CARD_TYPE_BADGE_STYLE = (
|
|
|
448
448
|
)
|
|
449
449
|
"""Small type badge (Package, Tool, Runtime, etc.) on each action card."""
|
|
450
450
|
|
|
451
|
+
ACTION_CARD_DISTRO_BADGE_STYLE = (
|
|
452
|
+
'QLabel { font-size: 10px; padding: 1px 6px; border-radius: 3px; background: #3d2a50; color: #c5a0e0; }'
|
|
453
|
+
)
|
|
454
|
+
"""Muted purple pill showing the WSL distro name on a WSL action card."""
|
|
455
|
+
|
|
456
|
+
WSL_DISTRO_HEADER_STYLE = (
|
|
457
|
+
'QLabel#wslDistroHeader {'
|
|
458
|
+
' font-size: 11px;'
|
|
459
|
+
' font-weight: bold;'
|
|
460
|
+
' color: #c5a0e0;'
|
|
461
|
+
' padding: 8px 4px 4px 4px;'
|
|
462
|
+
' border-bottom: 1px solid palette(mid);'
|
|
463
|
+
'}'
|
|
464
|
+
)
|
|
465
|
+
"""Section divider for WSL distro groups inside the action card list."""
|
|
466
|
+
|
|
451
467
|
ACTION_CARD_PACKAGE_STYLE = 'font-weight: bold; font-size: 12px;'
|
|
452
468
|
"""Primary line: package name."""
|
|
453
469
|
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/schema.py
RENAMED
|
@@ -355,6 +355,8 @@ class ActionInfo:
|
|
|
355
355
|
package: str | None = None
|
|
356
356
|
constraint: str | None = None
|
|
357
357
|
installer: str | None = None
|
|
358
|
+
distro: str | None = None
|
|
359
|
+
"""WSL2 distro name, or ``None`` for native host actions."""
|
|
358
360
|
|
|
359
361
|
|
|
360
362
|
@dataclass(frozen=True, slots=True)
|
|
@@ -262,10 +262,9 @@ class Updater:
|
|
|
262
262
|
# Simple semver comparison via packaging.version (accepts
|
|
263
263
|
# semver pre-release tags like ``0.1.0-dev.79``).
|
|
264
264
|
try:
|
|
265
|
-
if Version(ver) > Version(current_semver):
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
best_ver = ver
|
|
265
|
+
if Version(ver) > Version(current_semver) and (best_ver is None or Version(ver) > Version(best_ver)):
|
|
266
|
+
best = asset
|
|
267
|
+
best_ver = ver
|
|
269
268
|
except Exception:
|
|
270
269
|
continue
|
|
271
270
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/config_store.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/data.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/debug.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/icon.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/init.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/instance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/schema.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/card.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/tray.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/application/update_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/__init__.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/bootstrap.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/config.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/project.py
RENAMED
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/operations/update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/synodic_client/subprocess_patch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_config.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_install.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_install_plan.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_project.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_tool.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_update.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_gather_packages.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_install_preview.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_package_state.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_preview_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_tray_window_show.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_banner.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_controller.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_feedback.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/windows/test_protocol.py
RENAMED
|
File without changes
|
{synodic_client-0.0.1.dev80 → synodic_client-0.0.1.dev81}/tests/unit/windows/test_startup.py
RENAMED
|
File without changes
|