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
8 changes: 4 additions & 4 deletions docs/updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,12 @@ client.initialize_updater(config)
# Check for updates
info = client.check_for_update()
if info and info.available:
print(f"Update available: {info.current_version} -> {info.latest_version}")
print(f'Update available: {info.current_version} -> {info.latest_version}')

# Download with progress
def on_progress(percent: int) -> None:
print(f"Downloading: {percent}%")
print(f'Downloading: {percent}%')

if client.download_update(on_progress):
# Apply and restart
client.apply_update_on_exit(restart=True)
Expand Down
46 changes: 23 additions & 23 deletions pdm.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ requires-python = ">=3.14, <3.15"
dependencies = [
"pyside6>=6.10.2",
"packaging>=26.0",
"porringer>=0.2.1.dev74",
"porringer>=0.2.1.dev77",
"qasync>=0.28.0",
"velopack>=0.0.1444.dev49733",
"typer>=0.24.1",
Expand All @@ -29,7 +29,7 @@ build = [
"pyinstaller>=6.19.0",
]
lint = [
"ruff>=0.15.4",
"ruff>=0.15.5",
"pyrefly>=0.55.0",
]
test = [
Expand Down
22 changes: 21 additions & 1 deletion synodic_client/application/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
CheckParameters,
CheckResult,
)
from porringer.schema.check import RuntimeCheckResult

from synodic_client.application.schema import Snapshot

Expand Down Expand Up @@ -107,7 +108,26 @@ async def check_updates(
A list of :class:`CheckResult` per plugin.
"""
params = CheckParameters(plugins=plugins)
return await self._porringer.sync.check_updates(
return await self._porringer.package.check_updates(
params,
plugins=self._snapshot.discovered,
)

async def check_updates_by_runtime(
self,
plugins: list[str] | None = None,
) -> list[RuntimeCheckResult]:
"""Run per-runtime update detection using cached ``DiscoveredPlugins``.

Args:
plugins: Optional include-set of plugin names. ``None``
means all plugins.

Returns:
A list of :class:`RuntimeCheckResult` per runtime.
"""
params = CheckParameters(plugins=plugins)
return await self._porringer.package.check_updates_by_runtime(
params,
plugins=self._snapshot.discovered,
)
Expand Down
104 changes: 68 additions & 36 deletions synodic_client/application/screen/plugin_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
FILTER_CHIP_STYLE,
PLUGIN_KIND_HEADER_STYLE,
PLUGIN_PROVIDER_NAME_STYLE,
PLUGIN_PROVIDER_RUNTIME_TAG_DEFAULT_STYLE,
PLUGIN_PROVIDER_RUNTIME_TAG_STYLE,
PLUGIN_PROVIDER_STATUS_INSTALLED_STYLE,
PLUGIN_PROVIDER_STATUS_MISSING_STYLE,
PLUGIN_PROVIDER_STYLE,
Expand Down Expand Up @@ -140,17 +142,19 @@ def __init__(
self.setObjectName('pluginProvider')
self.setStyleSheet(PLUGIN_PROVIDER_STYLE)
self._plugin_name = plugin.name
self._runtime_tag = ''
self._signal_key = plugin.name
self._update_btn: QPushButton | None = None
self._checking_spinner: _RowSpinner | None = None

layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(6)
self._layout = QHBoxLayout(self)
self._layout.setContentsMargins(0, 0, 0, 0)
self._layout.setSpacing(6)

# Plugin name
name_label = QLabel(plugin.name)
name_label.setStyleSheet(PLUGIN_PROVIDER_NAME_STYLE)
layout.addWidget(name_label)
self._layout.addWidget(name_label)

# Version
version_text = (
Expand All @@ -162,55 +166,81 @@ def __init__(
)
version_label = QLabel(version_text)
version_label.setStyleSheet(PLUGIN_PROVIDER_VERSION_STYLE)
layout.addWidget(version_label)
self._layout.addWidget(version_label)

# Installed indicator
status_label = QLabel('\u25cf' if plugin.installed else '\u25cb')
status_label.setStyleSheet(
PLUGIN_PROVIDER_STATUS_INSTALLED_STYLE if plugin.installed else PLUGIN_PROVIDER_STATUS_MISSING_STYLE
)
status_label.setToolTip('Installed' if plugin.installed else 'Not installed')
layout.addWidget(status_label)
self._layout.addWidget(status_label)

layout.addStretch()
self._layout.addStretch()

# Transient inline error label (hidden by default)
self._status_label = QLabel()
self._status_label.setStyleSheet(PLUGIN_ROW_ERROR_STYLE)
self._status_label.hide()
layout.addWidget(self._status_label)
self._layout.addWidget(self._status_label)

# Auto / Update controls (only for updatable kinds)
if show_controls:
toggle_btn = QPushButton('Auto')
toggle_btn.setCheckable(True)
toggle_btn.setChecked(auto_update)
toggle_btn.setStyleSheet(PLUGIN_TOGGLE_STYLE)
toggle_btn.setToolTip('Enable automatic updates for this plugin')
toggle_btn.clicked.connect(
lambda checked: self.auto_update_toggled.emit(self._plugin_name, checked),
)
layout.addWidget(toggle_btn)
self._build_controls(self._layout, plugin, auto_update, has_updates)

self._checking_spinner = _RowSpinner(self)
layout.addWidget(self._checking_spinner)
def set_runtime(self, tag: str, label: str = '') -> None:
"""Set runtime identity and optionally insert a runtime tag pill.

update_btn = QPushButton('Update')
update_btn.setStyleSheet(PLUGIN_UPDATE_STYLE)
update_btn.setToolTip(f'Upgrade packages via {plugin.name} now')
update_btn.clicked.connect(
lambda: self.update_requested.emit(self._plugin_name),
Must be called before the widget is added to a visible layout.
"""
self._runtime_tag = tag
self._signal_key = f'{self._plugin_name}:{tag}' if tag else self._plugin_name
if label:
is_default = '(default)' in label
pill = QLabel(label)
pill.setStyleSheet(
PLUGIN_PROVIDER_RUNTIME_TAG_DEFAULT_STYLE if is_default else PLUGIN_PROVIDER_RUNTIME_TAG_STYLE
)
update_btn.setVisible(has_updates)
self._update_btn = update_btn
layout.addWidget(update_btn)
# Insert after the name label (index 1)
self._layout.insertWidget(1, pill)

def _build_controls(
self,
layout: QHBoxLayout,
plugin: PluginInfo,
auto_update: bool,
has_updates: bool,
) -> None:
"""Build Auto/Update control buttons."""
toggle_btn = QPushButton('Auto')
toggle_btn.setCheckable(True)
toggle_btn.setChecked(auto_update)
toggle_btn.setStyleSheet(PLUGIN_TOGGLE_STYLE)
toggle_btn.setToolTip('Enable automatic updates for this plugin')
toggle_btn.clicked.connect(
lambda checked: self.auto_update_toggled.emit(self._signal_key, checked),
)
layout.addWidget(toggle_btn)

self._checking_spinner = _RowSpinner(self)
layout.addWidget(self._checking_spinner)

update_btn = QPushButton('Update')
update_btn.setStyleSheet(PLUGIN_UPDATE_STYLE)
update_btn.setToolTip(f'Upgrade packages via {plugin.name} now')
update_btn.clicked.connect(
lambda: self.update_requested.emit(self._signal_key),
)
update_btn.setVisible(has_updates)
self._update_btn = update_btn
layout.addWidget(update_btn)

if not plugin.installed:
toggle_btn.setEnabled(False)
toggle_btn.setChecked(False)
toggle_btn.setToolTip('Not installed \u2014 cannot auto-update')
update_btn.setEnabled(False)
update_btn.setToolTip('Not installed \u2014 cannot update')
if not plugin.installed:
toggle_btn.setEnabled(False)
toggle_btn.setChecked(False)
toggle_btn.setToolTip('Not installed \u2014 cannot auto-update')
update_btn.setEnabled(False)
update_btn.setToolTip('Not installed \u2014 cannot update')

def set_updating(self, updating: bool) -> None:
"""Toggle the button between *Updating…* and *Update* states."""
Expand Down Expand Up @@ -291,6 +321,8 @@ def __init__(
self.setStyleSheet(PLUGIN_ROW_STYLE)
self._plugin_name = data.plugin_name
self._package_name = data.name
self._runtime_tag = data.runtime_tag
self._signal_key = f'{data.plugin_name}:{data.runtime_tag}' if data.runtime_tag else data.plugin_name
self._update_btn: QPushButton | None = None
self._remove_btn: QPushButton | None = None
self._checking_spinner: _RowSpinner | None = None
Expand Down Expand Up @@ -391,7 +423,7 @@ def _build_toggle(self, layout: QHBoxLayout, data: PluginRowData) -> None:
toggle_btn.setToolTip('Auto-update this package')
toggle_btn.clicked.connect(
lambda checked: self.auto_update_toggled.emit(
self._plugin_name,
self._signal_key,
self._package_name,
checked,
),
Expand All @@ -408,7 +440,7 @@ def _build_update_button(self, layout: QHBoxLayout, data: PluginRowData) -> None
update_btn.setFixedWidth(PLUGIN_ROW_UPDATE_WIDTH)
update_btn.setToolTip(f'Update {data.name}')
update_btn.clicked.connect(
lambda: self.update_requested.emit(self._plugin_name, self._package_name),
lambda: self.update_requested.emit(self._signal_key, self._package_name),
)
update_btn.setVisible(data.has_update)
self._update_btn = update_btn
Expand All @@ -435,7 +467,7 @@ def _build_remove_button(self, layout: QHBoxLayout, data: PluginRowData) -> None
if data.is_global:
remove_btn.setToolTip(f'Remove {data.name}')
remove_btn.clicked.connect(
lambda: self.remove_requested.emit(self._plugin_name, self._package_name),
lambda: self.remove_requested.emit(self._signal_key, self._package_name),
)
else:
remove_btn.setEnabled(False)
Expand Down
12 changes: 11 additions & 1 deletion synodic_client/application/screen/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
SubActionProgress,
SyncStrategy,
)
from porringer.schema.plugin import RuntimePackageResult

from synodic_client.application.screen.action_card import action_key
from synodic_client.application.uri import normalize_manifest_key
Expand Down Expand Up @@ -139,6 +140,9 @@ class PluginRowData:
host_tool: str = ''
"""Host-tool name for injected packages."""

runtime_tag: str = ''
"""Runtime tag for per-runtime packages (e.g. ``\"3.12\"``)."""

project_paths: list[str] = field(default_factory=list)
"""Filesystem paths for project-scoped packages."""

Expand All @@ -150,7 +154,7 @@ class PluginRowData:


@dataclass(slots=True)
class _RefreshData:
class RefreshData:
"""Internal data bundle returned by ``ToolsView._gather_refresh_data``."""

plugins: list[PluginInfo]
Expand All @@ -162,6 +166,12 @@ class _RefreshData:
manifest_packages: dict[str, set[str]]
"""Mapping of plugin name → manifest-referenced package names."""

runtime_packages: dict[str, list[RuntimePackageResult]] = field(default_factory=dict)
"""Mapping of plugin name → per-runtime package results (RuntimeConsumer plugins only)."""

default_runtime_executable: Path | None = None
"""Executable path of the resolved default runtime, if any."""


# ---------------------------------------------------------------------------
# Install preview data models (from install.py)
Expand Down
Loading
Loading