Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 292 additions & 85 deletions pdm.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ requires-python = ">=3.14, <3.15"
dependencies = [
"pyside6>=6.10.2",
"packaging>=26.0",
"porringer>=0.2.1.dev86",
"porringer>=0.2.1.dev88",
"qasync>=0.28.0",
"velopack>=0.0.1521.dev61717",
"velopack>=0.0.1535.dev45597",
"typer>=0.24.1",
]

Expand Down
107 changes: 92 additions & 15 deletions synodic_client/application/screen/action_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from synodic_client.application.theme import (
ACTION_CARD_COMMAND_STYLE,
ACTION_CARD_DESC_STYLE,
ACTION_CARD_DISTRO_BADGE_STYLE,
ACTION_CARD_EXECUTING_STYLE,
ACTION_CARD_PACKAGE_STYLE,
ACTION_CARD_SKELETON_BAR_STYLE,
Expand All @@ -64,6 +65,7 @@
COPY_BTN_STYLE,
COPY_FEEDBACK_MS,
COPY_ICON,
WSL_DISTRO_HEADER_STYLE,
)

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -224,14 +226,19 @@ def _init_real_ui(self) -> None:
outer.addWidget(self._build_command_row())

def _build_top_row(self) -> QHBoxLayout:
"""Build the top row: type badge | package name ... version | status/spinner | prerelease."""
"""Build the top row: type badge | [distro badge] | package name ... version | status/spinner | prerelease."""
top = QHBoxLayout()
top.setSpacing(8)

self._type_badge = QLabel()
self._type_badge.setStyleSheet(ACTION_CARD_TYPE_BADGE_STYLE)
top.addWidget(self._type_badge)

self._distro_badge = QLabel()
self._distro_badge.setStyleSheet(ACTION_CARD_DISTRO_BADGE_STYLE)
self._distro_badge.hide()
top.addWidget(self._distro_badge)

self._package_label = QLabel()
self._package_label.setStyleSheet(ACTION_CARD_PACKAGE_STYLE)
self._package_label.setTextInteractionFlags(
Expand Down Expand Up @@ -360,6 +367,12 @@ def populate(
if action.installer:
self._type_badge.setToolTip(f'Plugin: {action.installer}')

if action.distro:
self._distro_badge.setText(action.distro)
self._distro_badge.show()
else:
self._distro_badge.hide()

package_text = str(action.package) if action.package else action.description
self._package_label.setText(package_text)

Expand Down Expand Up @@ -635,6 +648,32 @@ def mousePressEvent(self, event: QMouseEvent) -> None:
super().mousePressEvent(event)


# ---------------------------------------------------------------------------
# _DistroGroupHeader — section divider between native and WSL action groups
# ---------------------------------------------------------------------------


class _DistroGroupHeader(QLabel):
"""Thin section divider between native and per-distro action groups.

Shows ``HOST`` for the native group or ``WSL — <distro>`` for each
WSL2 distro group. Only inserted when both native and WSL actions
are present.
"""

def __init__(self, distro_name: str | None, parent: QWidget | None = None) -> None:
"""Initialise the header.

Args:
distro_name: WSL distro name, or ``None`` for the native group.
parent: Optional parent widget.
"""
label = 'HOST' if distro_name is None else f'WSL \u2014 {distro_name}'.upper()
super().__init__(label, parent)
self.setObjectName('wslDistroHeader')
self.setStyleSheet(WSL_DISTRO_HEADER_STYLE)


# ---------------------------------------------------------------------------
# ActionCardList — card container
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -667,6 +706,7 @@ def __init__(self, parent: QWidget | None = None) -> None:
self._layout.addStretch()

self._cards: list[ActionCard] = []
self._group_headers: list[QLabel] = []
self._action_map: dict[SetupAction, ActionCard] = {}
self._index_map: dict[int, ActionCard] = {}

Expand Down Expand Up @@ -701,25 +741,58 @@ def populate(
) -> None:
"""Replace skeleton cards with real action cards.

When actions include WSL distro entries (``action.distro is not
None``), the list is split into groups. Native host actions
appear first under a ``HOST`` section header; each WSL distro
gets its own ``WSL — <distro>`` section header below. If all
actions are native, no headers are inserted.

Args:
actions: The setup actions to display.
plugin_installed: Plugin name → installed mapping.
prerelease_overrides: Package names with user pre-release overrides.
"""
self.clear()
sorted_actions = sorted(actions, key=action_sort_key)
for act in sorted_actions:
card = ActionCard(self)
card.populate(
act,
plugin_installed=plugin_installed,
prerelease_overrides=prerelease_overrides,
)
card.prerelease_toggled.connect(self.prerelease_toggled.emit)
card.navigate_to_tool.connect(self.navigate_to_tool.emit)
self._layout.insertWidget(self._layout.count() - 1, card)
self._cards.append(card)
self._action_map[act] = card

# Partition into native and per-distro groups.
native_actions: list[SetupAction] = []
distro_actions: dict[str, list[SetupAction]] = {}
for act in actions:
if act.distro is None:
native_actions.append(act)
else:
distro_actions.setdefault(act.distro, []).append(act)

has_wsl = bool(distro_actions)

def _add_group(group_actions: list[SetupAction]) -> None:
for act in sorted(group_actions, key=action_sort_key):
card = ActionCard(self)
card.populate(
act,
plugin_installed=plugin_installed,
prerelease_overrides=prerelease_overrides,
)
card.prerelease_toggled.connect(self.prerelease_toggled.emit)
card.navigate_to_tool.connect(self.navigate_to_tool.emit)
self._layout.insertWidget(self._layout.count() - 1, card)
self._cards.append(card)
self._action_map[act] = card

def _add_header(distro_name: str | None) -> None:
header = _DistroGroupHeader(distro_name, self)
self._layout.insertWidget(self._layout.count() - 1, header)
self._group_headers.append(header)

# Native actions (with host header only when WSL groups also exist)
if has_wsl and native_actions:
_add_header(None)
_add_group(native_actions)

# Per-distro groups
for distro_name in sorted(distro_actions):
_add_header(distro_name)
_add_group(distro_actions[distro_name])

# Build original-index → card mapping so callers can look up by
# the action index porringer emits, which is independent of the
Expand Down Expand Up @@ -776,7 +849,11 @@ def finalize_all_checking(self) -> None:
card.finalize_checking()

def clear(self) -> None:
"""Remove all cards."""
"""Remove all cards and group headers."""
for header in self._group_headers:
self._layout.removeWidget(header)
header.deleteLater()
self._group_headers.clear()
for card in self._cards:
self._layout.removeWidget(card)
card.deleteLater()
Expand Down
20 changes: 20 additions & 0 deletions synodic_client/application/screen/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
)
from synodic_client.application.screen.spinner import LoadingIndicator
from synodic_client.application.screen.update_banner import UpdateBanner
from synodic_client.application.screen.wsl import WslView
from synodic_client.application.theme import (
COMPACT_MARGINS,
FILTER_CHIP_SPACING,
Expand Down Expand Up @@ -1434,6 +1435,7 @@ class MainWindow(QMainWindow):
_tabs: QTabWidget | None = None
_tools_view: ToolsView | None = None
_projects_view: ProjectsView | None = None
_wsl_view: WslView | None = None

def __init__(
self,
Expand Down Expand Up @@ -1517,6 +1519,22 @@ def show(self) -> None:
self._tabs.addTab(self._tools_view, 'Tools')
self.tools_view_created.emit(self._tools_view)

# WSL tab — only on Windows hosts with WSL available.
try:
from porringer.plugin.wsl.utility import is_wsl_host

if is_wsl_host():
self._wsl_view = WslView(
self._porringer,
self._store,
self,
coordinator=self._coordinator,
package_store=self._package_store,
)
self._tabs.addTab(self._wsl_view, 'WSL')
except Exception:
logger.debug('Could not initialise WSL tab', exc_info=True)

# Navigate-to-project: switch to Projects tab and select directory
self._tools_view.navigate_to_project_requested.connect(self._navigate_to_project)

Expand Down Expand Up @@ -1546,6 +1564,8 @@ def show(self) -> None:
self._tools_view.refresh()
if self._projects_view is not None:
self._projects_view.refresh()
if self._wsl_view is not None:
self._wsl_view.refresh()

def _navigate_to_project(self, path_str: str) -> None:
"""Switch to the Projects tab and select the given directory."""
Expand Down
Loading
Loading