synodic-client 0.0.1.dev79__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.
Files changed (106) hide show
  1. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/PKG-INFO +3 -3
  2. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/pyproject.toml +6 -6
  3. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/action_card.py +92 -15
  4. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/plugin_row.py +23 -3
  5. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/screen.py +25 -0
  6. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/tool_update_controller.py +1 -0
  7. synodic_client-0.0.1.dev81/synodic_client/application/screen/wsl.py +419 -0
  8. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/theme.py +46 -13
  9. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/operations/install.py +1 -0
  10. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/operations/schema.py +2 -0
  11. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/updater.py +3 -4
  12. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/LICENSE.md +0 -0
  13. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/README.md +0 -0
  14. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/__init__.py +0 -0
  15. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/__main__.py +0 -0
  16. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/__init__.py +0 -0
  17. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/bootstrap.py +0 -0
  18. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/config_store.py +0 -0
  19. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/data.py +0 -0
  20. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/debug.py +0 -0
  21. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/icon.py +0 -0
  22. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/init.py +0 -0
  23. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/instance.py +0 -0
  24. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/package_state.py +0 -0
  25. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/qt.py +0 -0
  26. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/schema.py +0 -0
  27. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/__init__.py +0 -0
  28. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/card.py +0 -0
  29. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/install.py +0 -0
  30. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/install_workers.py +0 -0
  31. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/log_panel.py +0 -0
  32. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/projects.py +0 -0
  33. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/schema.py +0 -0
  34. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/settings.py +0 -0
  35. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/sidebar.py +0 -0
  36. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/spinner.py +0 -0
  37. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/tray.py +0 -0
  38. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/screen/update_banner.py +0 -0
  39. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/update_controller.py +0 -0
  40. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/update_model.py +0 -0
  41. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/application/uri.py +0 -0
  42. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/__init__.py +0 -0
  43. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/config.py +0 -0
  44. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/context.py +0 -0
  45. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/debug.py +0 -0
  46. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/install.py +0 -0
  47. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/output.py +0 -0
  48. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/project.py +0 -0
  49. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/tool.py +0 -0
  50. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/cli/update.py +0 -0
  51. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/client.py +0 -0
  52. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/config.py +0 -0
  53. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/logging.py +0 -0
  54. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/operations/__init__.py +0 -0
  55. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/operations/bootstrap.py +0 -0
  56. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/operations/config.py +0 -0
  57. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/operations/project.py +0 -0
  58. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/operations/tool.py +0 -0
  59. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/operations/update.py +0 -0
  60. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/protocol.py +0 -0
  61. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/py.typed +0 -0
  62. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/resolution.py +0 -0
  63. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/schema.py +0 -0
  64. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/startup.py +0 -0
  65. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/synodic_client/subprocess_patch.py +0 -0
  66. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/__init__.py +0 -0
  67. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/conftest.py +0 -0
  68. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/__init__.py +0 -0
  69. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/operations/__init__.py +0 -0
  70. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_config.py +0 -0
  71. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_install.py +0 -0
  72. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_install_plan.py +0 -0
  73. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_project.py +0 -0
  74. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_tool.py +0 -0
  75. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/operations/test_update.py +0 -0
  76. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/__init__.py +0 -0
  77. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/conftest.py +0 -0
  78. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_action_card.py +0 -0
  79. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_gather_packages.py +0 -0
  80. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_install_preview.py +0 -0
  81. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_log_panel.py +0 -0
  82. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_logging.py +0 -0
  83. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_package_state.py +0 -0
  84. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_preview_model.py +0 -0
  85. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_settings.py +0 -0
  86. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_sidebar.py +0 -0
  87. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_tray_window_show.py +0 -0
  88. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_banner.py +0 -0
  89. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_controller.py +0 -0
  90. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/qt/test_update_feedback.py +0 -0
  91. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_bootstrap.py +0 -0
  92. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_cli.py +0 -0
  93. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_client_updater.py +0 -0
  94. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_client_version.py +0 -0
  95. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_config.py +0 -0
  96. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_examples.py +0 -0
  97. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_init.py +0 -0
  98. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_install.py +0 -0
  99. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_resolution.py +0 -0
  100. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_updater.py +0 -0
  101. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_uri.py +0 -0
  102. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/test_workers.py +0 -0
  103. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/windows/__init__.py +0 -0
  104. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/windows/conftest.py +0 -0
  105. {synodic_client-0.0.1.dev79 → synodic_client-0.0.1.dev81}/tests/unit/windows/test_protocol.py +0 -0
  106. {synodic_client-0.0.1.dev79 → 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.dev79
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.dev86
11
+ Requires-Dist: porringer>=0.2.1.dev88
12
12
  Requires-Dist: qasync>=0.28.0
13
- Requires-Dist: velopack>=0.0.1521.dev61717
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.dev86",
13
+ "porringer>=0.2.1.dev88",
14
14
  "qasync>=0.28.0",
15
- "velopack>=0.0.1521.dev61717",
15
+ "velopack>=0.0.1535.dev45597",
16
16
  "typer>=0.24.1",
17
17
  ]
18
- version = "0.0.1.dev79"
18
+ version = "0.0.1.dev81"
19
19
 
20
20
  [project.license]
21
21
  text = "LGPL-3.0-or-later"
@@ -35,12 +35,12 @@ build = [
35
35
  "pyinstaller>=6.19.0",
36
36
  ]
37
37
  lint = [
38
- "ruff>=0.15.6",
39
- "pyrefly>=0.57.0",
38
+ "ruff>=0.15.7",
39
+ "pyrefly>=0.57.1",
40
40
  ]
41
41
  test = [
42
42
  "pytest>=9.0.2",
43
- "pytest-cov>=7.0.0",
43
+ "pytest-cov>=7.1.0",
44
44
  "pytest-mock>=3.15.1",
45
45
  ]
46
46
 
@@ -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
- sorted_actions = sorted(actions, key=action_sort_key)
711
- for act in sorted_actions:
712
- card = ActionCard(self)
713
- card.populate(
714
- act,
715
- plugin_installed=plugin_installed,
716
- prerelease_overrides=prerelease_overrides,
717
- )
718
- card.prerelease_toggled.connect(self.prerelease_toggled.emit)
719
- card.navigate_to_tool.connect(self.navigate_to_tool.emit)
720
- self._layout.insertWidget(self._layout.count() - 1, card)
721
- self._cards.append(card)
722
- self._action_map[act] = card
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()
@@ -25,6 +25,7 @@ from synodic_client.application.screen.schema import PluginRowData, ProjectInsta
25
25
  from synodic_client.application.screen.spinner import SpinnerCanvas
26
26
  from synodic_client.application.theme import (
27
27
  FILTER_CHIP_STYLE,
28
+ PLUGIN_CHECK_STYLE,
28
29
  PLUGIN_KIND_HEADER_STYLE,
29
30
  PLUGIN_PROVIDER_NAME_STYLE,
30
31
  PLUGIN_PROVIDER_RUNTIME_TAG_DEFAULT_STYLE,
@@ -138,6 +139,9 @@ class PluginProviderHeader(QFrame):
138
139
  auto_update_toggled = Signal(str, bool)
139
140
  """Emitted with ``(plugin_name, enabled)`` when the auto-update toggle changes."""
140
141
 
142
+ check_requested = Signal(str)
143
+ """Emitted with the plugin name when the manual check-for-updates button is clicked."""
144
+
141
145
  update_requested = Signal(str)
142
146
  """Emitted with the plugin name when the per-plugin *Update* button is clicked."""
143
147
 
@@ -159,6 +163,7 @@ class PluginProviderHeader(QFrame):
159
163
  self._runtime_tag = ''
160
164
  self._signal_key = plugin.name
161
165
  self._update_btn: QPushButton | None = None
166
+ self._check_btn: QPushButton | None = None
162
167
  self._checking_spinner: _RowSpinner | None = None
163
168
 
164
169
  self._layout = QHBoxLayout(self)
@@ -230,8 +235,8 @@ class PluginProviderHeader(QFrame):
230
235
  auto_update: bool,
231
236
  has_updates: bool,
232
237
  ) -> None:
233
- """Build Auto/Update control buttons."""
234
- toggle_btn = QPushButton('Auto')
238
+ """Build auto-update toggle, check, and Update control buttons."""
239
+ toggle_btn = QPushButton('\u21ba')
235
240
  toggle_btn.setCheckable(True)
236
241
  toggle_btn.setChecked(auto_update)
237
242
  toggle_btn.setStyleSheet(PLUGIN_TOGGLE_STYLE)
@@ -241,6 +246,15 @@ class PluginProviderHeader(QFrame):
241
246
  )
242
247
  layout.addWidget(toggle_btn)
243
248
 
249
+ check_btn = QPushButton('\u27f3')
250
+ check_btn.setStyleSheet(PLUGIN_CHECK_STYLE)
251
+ check_btn.setToolTip('Check for updates now')
252
+ check_btn.clicked.connect(
253
+ lambda: self.check_requested.emit(self._signal_key),
254
+ )
255
+ self._check_btn = check_btn
256
+ layout.addWidget(check_btn)
257
+
244
258
  self._checking_spinner = _RowSpinner(self)
245
259
  layout.addWidget(self._checking_spinner)
246
260
 
@@ -258,6 +272,8 @@ class PluginProviderHeader(QFrame):
258
272
  toggle_btn.setEnabled(False)
259
273
  toggle_btn.setChecked(False)
260
274
  toggle_btn.setToolTip('Not installed \u2014 cannot auto-update')
275
+ check_btn.setEnabled(False)
276
+ check_btn.setToolTip('Not installed \u2014 cannot check for updates')
261
277
  update_btn.setEnabled(False)
262
278
  update_btn.setToolTip('Not installed \u2014 cannot update')
263
279
 
@@ -281,10 +297,14 @@ class PluginProviderHeader(QFrame):
281
297
  return
282
298
  if checking:
283
299
  self._checking_spinner.start()
300
+ if self._check_btn is not None:
301
+ self._check_btn.hide()
284
302
  if self._update_btn is not None:
285
303
  self._update_btn.hide()
286
304
  else:
287
305
  self._checking_spinner.stop()
306
+ if self._check_btn is not None:
307
+ self._check_btn.show()
288
308
 
289
309
  def set_error(self, message: str) -> None:
290
310
  """Show a transient inline error that auto-hides after ~5 seconds."""
@@ -443,7 +463,7 @@ class PluginRow(QFrame):
443
463
 
444
464
  def _build_toggle(self, layout: QHBoxLayout, data: PluginRowData) -> None:
445
465
  """Add the auto-update toggle button."""
446
- toggle_btn = QPushButton('Auto')
466
+ toggle_btn = QPushButton('\u21ba')
447
467
  toggle_btn.setCheckable(True)
448
468
  toggle_btn.setChecked(data.auto_update)
449
469
  toggle_btn.setStyleSheet(PLUGIN_ROW_TOGGLE_STYLE)
@@ -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,
@@ -95,6 +96,9 @@ class ToolsView(QWidget):
95
96
  update_all_requested = Signal()
96
97
  """Emitted when the global *Update All* button is clicked."""
97
98
 
99
+ plugin_check_requested = Signal(str)
100
+ """Emitted with a plugin name when its manual check-for-updates button is clicked."""
101
+
98
102
  plugin_update_requested = Signal(str)
99
103
  """Emitted with a plugin name when its per-plugin *Update* button is clicked."""
100
104
 
@@ -495,6 +499,7 @@ class ToolsView(QWidget):
495
499
  )
496
500
  provider.set_runtime(rt.tag, label=tag_text)
497
501
  provider.auto_update_toggled.connect(self._on_auto_update_toggled)
502
+ provider.check_requested.connect(self.plugin_check_requested.emit)
498
503
  provider.update_requested.connect(self.plugin_update_requested.emit)
499
504
  self._insert_section_widget(provider)
500
505
 
@@ -558,6 +563,7 @@ class ToolsView(QWidget):
558
563
  parent=self._container,
559
564
  )
560
565
  provider.auto_update_toggled.connect(self._on_auto_update_toggled)
566
+ provider.check_requested.connect(self.plugin_check_requested.emit)
561
567
  provider.update_requested.connect(self.plugin_update_requested.emit)
562
568
  self._insert_section_widget(provider)
563
569
 
@@ -1429,6 +1435,7 @@ class MainWindow(QMainWindow):
1429
1435
  _tabs: QTabWidget | None = None
1430
1436
  _tools_view: ToolsView | None = None
1431
1437
  _projects_view: ProjectsView | None = None
1438
+ _wsl_view: WslView | None = None
1432
1439
 
1433
1440
  def __init__(
1434
1441
  self,
@@ -1512,6 +1519,22 @@ class MainWindow(QMainWindow):
1512
1519
  self._tabs.addTab(self._tools_view, 'Tools')
1513
1520
  self.tools_view_created.emit(self._tools_view)
1514
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
+
1515
1538
  # Navigate-to-project: switch to Projects tab and select directory
1516
1539
  self._tools_view.navigate_to_project_requested.connect(self._navigate_to_project)
1517
1540
 
@@ -1541,6 +1564,8 @@ class MainWindow(QMainWindow):
1541
1564
  self._tools_view.refresh()
1542
1565
  if self._projects_view is not None:
1543
1566
  self._projects_view.refresh()
1567
+ if self._wsl_view is not None:
1568
+ self._wsl_view.refresh()
1544
1569
 
1545
1570
  def _navigate_to_project(self, path_str: str) -> None:
1546
1571
  """Switch to the Projects tab and select the given directory."""
@@ -148,6 +148,7 @@ class ToolUpdateOrchestrator:
148
148
  def connect_tools_view(self, tools_view: ToolsView) -> None:
149
149
  """Wire ToolsView signals once the view is lazily created."""
150
150
  tools_view.update_all_requested.connect(self.on_tool_update)
151
+ tools_view.plugin_check_requested.connect(self.on_single_plugin_update)
151
152
  tools_view.plugin_update_requested.connect(self.on_single_plugin_update)
152
153
  tools_view.package_update_requested.connect(self.on_single_package_update)
153
154
  tools_view.package_remove_requested.connect(self.on_single_package_remove)