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
62 changes: 31 additions & 31 deletions pdm.lock

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

6 changes: 3 additions & 3 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.dev85",
"porringer>=0.2.1.dev86",
"qasync>=0.28.0",
"velopack>=0.0.1444.dev49733",
"velopack>=0.0.1521.dev61717",
"typer>=0.24.1",
]

Expand All @@ -30,7 +30,7 @@ build = [
]
lint = [
"ruff>=0.15.6",
"pyrefly>=0.56.0",
"pyrefly>=0.57.0",
]
test = [
"pytest>=9.0.2",
Expand Down
1 change: 1 addition & 0 deletions synodic_client/application/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def bootstrap() -> None:
run_startup_preamble(sys.executable)

# Heavy imports happen here — PySide6, porringer, etc.

from synodic_client.application.qt import application

application(uri=extract_uri_from_args(), dev_mode=dev_mode, debug=debug)
Expand Down
7 changes: 4 additions & 3 deletions synodic_client/application/config_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

from PySide6.QtCore import QObject, Signal

from synodic_client.resolution import ResolvedConfig, resolve_config, update_user_config
from synodic_client.operations.config import get_config, update_config
from synodic_client.schema import ResolvedConfig


class ConfigStore(QObject):
Expand All @@ -33,7 +34,7 @@ class ConfigStore(QObject):
def __init__(self, config: ResolvedConfig | None = None, parent: QObject | None = None) -> None:
"""Create a new store, optionally seeded with *config*."""
super().__init__(parent)
self._config = config if config is not None else resolve_config()
self._config = config if config is not None else get_config()

@property
def config(self) -> ResolvedConfig:
Expand All @@ -52,7 +53,7 @@ def update(self, **changes: object) -> ResolvedConfig:
Returns:
The fresh :class:`ResolvedConfig`.
"""
self._config = update_user_config(**changes)
self._config = update_config(**changes)
self.changed.emit(self._config)
return self._config

Expand Down
2 changes: 1 addition & 1 deletion synodic_client/application/debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def _handle_project_status(self, arg: str | None) -> str:
needed = sum(1 for s in model.action_states if classify_status(s.status) == 'needed')
satisfied = sum(1 for s in model.action_states if classify_status(s.status) == 'satisfied')
pending = sum(1 for s in model.action_states if classify_status(s.status) == 'pending')
upgradable = len(model.upgradable_keys)
upgradable = sum(1 for s in model.action_states if s.status == 'Update available')

return json.dumps({
'path': str(target),
Expand Down
23 changes: 23 additions & 0 deletions synodic_client/application/package_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,26 @@ def has_data(self) -> bool:
def clear(self) -> None:
"""Remove all recorded state."""
self._data.clear()

def record_updates_completed(
self,
signal_key: str,
version_map: dict[str, tuple[str, str]],
) -> None:
"""Mark packages as updated, clearing stale ``has_update`` flags.

Called after a successful tool update run. For each entry in
*version_map* (``{package_name: (old_version, new_version)}``),
the corresponding :class:`PackageState` is updated to reflect
the new installed version and ``has_update`` is cleared.
"""
changed = False
bucket = self._data.get(signal_key, {})
for pkg_name, (_, new_ver) in version_map.items():
existing = bucket.get(pkg_name)
if existing is not None:
existing.installed_version = new_ver
existing.has_update = False
changed = True
if changed:
self.state_changed.emit()
39 changes: 38 additions & 1 deletion synodic_client/application/screen/action_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
from porringer.schema import SetupAction, SetupActionResult
from porringer.schema.plugin import PluginKind
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QColor
from PySide6.QtGui import QColor, QCursor, QMouseEvent
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
QFrame,
QGraphicsOpacityEffect,
QHBoxLayout,
QLabel,
QToolButton,
Expand Down Expand Up @@ -57,6 +58,7 @@
ACTION_CARD_STATUS_UPDATE,
ACTION_CARD_STYLE,
ACTION_CARD_TYPE_BADGE_STYLE,
ACTION_CARD_UPDATE_AVAILABLE_STYLE,
ACTION_CARD_VERSION_STYLE,
COPY_BTN_SIZE,
COPY_BTN_STYLE,
Expand Down Expand Up @@ -119,6 +121,10 @@ class ActionCard(QFrame):
"""Emitted with ``(package_name, checked)`` when the user toggles the
per-row pre-release checkbox."""

navigate_to_tool = Signal(str, str)
"""Emitted with ``(installer, package_name)`` when the user clicks an
'Update available' card to navigate to the Tools view."""

def __init__(
self,
parent: QWidget | None = None,
Expand Down Expand Up @@ -491,6 +497,10 @@ def set_check_result(self, result: SetupActionResult, status: str) -> None:
else:
self._status_label.setToolTip('')

# "Update available" — fade the card and make it clickable
if status == 'Update available':
self._apply_update_available_style()

# CLI command — update with resolved cli_command from result
assert self._action is not None
cmd_text = format_cli_command(self._action, result=result, suppress_description=True)
Expand Down Expand Up @@ -524,6 +534,15 @@ def _update_version_column(self, result: SetupActionResult) -> None:
self._version_label.setText(f'\u2192 {result.available_version}')
self._version_label.setStyleSheet(ACTION_CARD_VERSION_STYLE + ' color: grey;')

def _apply_update_available_style(self) -> None:
"""Fade the card and make it clickable for 'Update available' status."""
self.setStyleSheet(ACTION_CARD_UPDATE_AVAILABLE_STYLE)
opacity = QGraphicsOpacityEffect(self)
opacity.setOpacity(0.55)
self.setGraphicsEffect(opacity)
self.setCursor(QCursor(Qt.CursorShape.PointingHandCursor))
self.setToolTip('Manage this update in the Tools view')

def finalize_checking(self) -> None:
"""Resolve a still-pending 'Checking\u2026' status to 'Needed'.

Expand Down Expand Up @@ -605,6 +624,16 @@ def is_update_available(self) -> bool:
"""Return whether the card shows an 'Update available' status."""
return self.status_text() == 'Update available'

def mousePressEvent(self, event: QMouseEvent) -> None:
"""Navigate to Tools view when clicking an 'Update available' card."""
if self.is_update_available() and self._action is not None:
installer = self._action.installer or ''
package = str(self._action.package.name) if self._action.package else ''
if installer and package:
self.navigate_to_tool.emit(installer, package)
return
super().mousePressEvent(event)


# ---------------------------------------------------------------------------
# ActionCardList — card container
Expand All @@ -621,6 +650,13 @@ class ActionCardList(QWidget):
prerelease_toggled = Signal(str, bool)
"""Forwarded from child :class:`ActionCard` widgets."""

navigate_to_tool = Signal(str, str)
"""Forwarded from child :class:`ActionCard` widgets.

Emitted with ``(installer, package_name)`` when the user clicks an
'Update available' card.
"""

def __init__(self, parent: QWidget | None = None) -> None:
"""Initialise the card list."""
super().__init__(parent)
Expand Down Expand Up @@ -680,6 +716,7 @@ def populate(
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
Expand Down
Loading
Loading