synodic-client 0.0.1.dev65__tar.gz → 0.0.1.dev67__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.dev65 → synodic_client-0.0.1.dev67}/PKG-INFO +2 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/pyproject.toml +2 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/__init__.py +14 -6
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/action_card.py +19 -42
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/install.py +5 -11
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/install_workers.py +8 -8
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/log_panel.py +3 -4
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/schema.py +7 -71
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/settings.py +28 -34
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/tray.py +15 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/update_banner.py +64 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/theme.py +2 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/update_controller.py +105 -75
- synodic_client-0.0.1.dev67/synodic_client/application/update_model.py +121 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_action_card.py +52 -108
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_install_preview.py +29 -13
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_log_panel.py +0 -1
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_preview_model.py +2 -4
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_settings.py +27 -16
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_update_banner.py +1 -2
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_update_controller.py +101 -62
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/README.md +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/config_store.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/data.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/init.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/package_state.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/schema.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/plugin_row.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/projects.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/screen.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/sidebar.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/spinner.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/tool_update_controller.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/schema.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/subprocess_patch.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_gather_packages.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_sidebar.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_tray_window_show.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/qt/test_update_feedback.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_init.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/test_workers.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/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.dev67
|
|
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,7 +8,7 @@ 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.dev79
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
13
|
Requires-Dist: velopack>=0.0.1444.dev49733
|
|
14
14
|
Requires-Dist: typer>=0.24.1
|
|
@@ -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.dev79",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
15
|
"velopack>=0.0.1444.dev49733",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev67"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
from datetime import UTC, datetime
|
|
10
10
|
|
|
11
|
-
from porringer.schema import SetupAction, SkipReason
|
|
11
|
+
from porringer.schema import SetupAction, SetupActionResult, SkipReason
|
|
12
12
|
from porringer.schema.plugin import PluginKind
|
|
13
13
|
|
|
14
14
|
_SECONDS_PER_MINUTE = 60
|
|
@@ -63,18 +63,26 @@ def skip_reason_label(reason: SkipReason | None) -> str:
|
|
|
63
63
|
return SKIP_REASON_LABELS.get(reason, reason.name.replace('_', ' ').capitalize())
|
|
64
64
|
|
|
65
65
|
|
|
66
|
-
def format_cli_command(
|
|
66
|
+
def format_cli_command(
|
|
67
|
+
action: SetupAction,
|
|
68
|
+
*,
|
|
69
|
+
result: SetupActionResult | None = None,
|
|
70
|
+
suppress_description: bool = False,
|
|
71
|
+
) -> str:
|
|
67
72
|
"""Return a human-readable CLI command string for *action*.
|
|
68
73
|
|
|
69
|
-
Prefers ``cli_command
|
|
70
|
-
|
|
74
|
+
Prefers ``result.cli_command`` (populated after dry-run), falls
|
|
75
|
+
back to ``action.command``, then synthesises an
|
|
76
|
+
``installer install <package>`` string for package actions, and
|
|
71
77
|
finally returns the action description as a last resort.
|
|
72
78
|
|
|
73
79
|
When *suppress_description* is ``True`` the final description
|
|
74
80
|
fallback returns an empty string instead.
|
|
75
81
|
"""
|
|
76
|
-
if
|
|
77
|
-
return ' '.join(
|
|
82
|
+
if result is not None and result.cli_command:
|
|
83
|
+
return ' '.join(result.cli_command)
|
|
84
|
+
if action.command:
|
|
85
|
+
return ' '.join(action.command)
|
|
78
86
|
if action.kind == PluginKind.PACKAGE and action.package:
|
|
79
87
|
return f'{action.installer or "pip"} install {action.package}'
|
|
80
88
|
return '' if suppress_description else action.description
|
|
@@ -73,19 +73,6 @@ _SPINNER_INTERVAL = 50
|
|
|
73
73
|
_KIND_ORDER: dict[PluginKind | None, int] = {kind: i for i, kind in enumerate(PHASE_ORDER)}
|
|
74
74
|
|
|
75
75
|
|
|
76
|
-
def action_key(action: SetupAction) -> tuple[object, ...]:
|
|
77
|
-
"""Return a stable identity key for *action*.
|
|
78
|
-
|
|
79
|
-
Uses content-based fields (kind, installer, package name, command)
|
|
80
|
-
so the same logical action from different ``execute_stream`` runs
|
|
81
|
-
resolves to the same key.
|
|
82
|
-
"""
|
|
83
|
-
pkg_name = str(action.package.name) if action.package else None
|
|
84
|
-
pt_name = str(action.plugin_target.name) if action.plugin_target else None
|
|
85
|
-
cmd = tuple(action.command) if action.command else None
|
|
86
|
-
return (action.kind, action.installer, pkg_name, pt_name, cmd)
|
|
87
|
-
|
|
88
|
-
|
|
89
76
|
def action_sort_key(action: SetupAction) -> int:
|
|
90
77
|
"""Return a sort key that groups cards by execution phase.
|
|
91
78
|
|
|
@@ -396,24 +383,6 @@ class ActionCard(QFrame):
|
|
|
396
383
|
else:
|
|
397
384
|
self._prerelease_cb.hide()
|
|
398
385
|
|
|
399
|
-
def update_command(self, action: SetupAction) -> None:
|
|
400
|
-
"""Update the CLI command label after the resolved preview arrives.
|
|
401
|
-
|
|
402
|
-
Called from the two-phase display flow once ``MANIFEST_LOADED``
|
|
403
|
-
provides actions with their ``cli_command`` populated.
|
|
404
|
-
|
|
405
|
-
Args:
|
|
406
|
-
action: The setup action with resolved CLI command.
|
|
407
|
-
"""
|
|
408
|
-
if self._is_skeleton:
|
|
409
|
-
return
|
|
410
|
-
cmd_text = format_cli_command(action, suppress_description=True)
|
|
411
|
-
if cmd_text:
|
|
412
|
-
self._command_label.setText(cmd_text)
|
|
413
|
-
self._command_row.show()
|
|
414
|
-
else:
|
|
415
|
-
self._command_row.hide()
|
|
416
|
-
|
|
417
386
|
def _populate_status(
|
|
418
387
|
self,
|
|
419
388
|
action: SetupAction,
|
|
@@ -494,7 +463,7 @@ class ActionCard(QFrame):
|
|
|
494
463
|
self._status_label.setText(label)
|
|
495
464
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_UPDATE)
|
|
496
465
|
elif result.skipped:
|
|
497
|
-
label = skip_reason_label(result.skip_reason)
|
|
466
|
+
label = '\u2713 ' + skip_reason_label(result.skip_reason)
|
|
498
467
|
self._status_label.setText(label)
|
|
499
468
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_SATISFIED)
|
|
500
469
|
elif not result.success:
|
|
@@ -517,6 +486,13 @@ class ActionCard(QFrame):
|
|
|
517
486
|
else:
|
|
518
487
|
self._status_label.setToolTip('')
|
|
519
488
|
|
|
489
|
+
# CLI command — update with resolved cli_command from result
|
|
490
|
+
assert self._action is not None
|
|
491
|
+
cmd_text = format_cli_command(self._action, result=result, suppress_description=True)
|
|
492
|
+
if cmd_text:
|
|
493
|
+
self._command_label.setText(cmd_text)
|
|
494
|
+
self._command_row.show()
|
|
495
|
+
|
|
520
496
|
# Version column
|
|
521
497
|
self._check_available_version = result.available_version
|
|
522
498
|
if result.installed_version and result.available_version:
|
|
@@ -524,6 +500,9 @@ class ActionCard(QFrame):
|
|
|
524
500
|
self._version_label.setStyleSheet(ACTION_CARD_VERSION_STYLE + ' color: #d7ba7d;')
|
|
525
501
|
elif result.installed_version:
|
|
526
502
|
self._version_label.setText(result.installed_version)
|
|
503
|
+
elif result.available_version:
|
|
504
|
+
self._version_label.setText(f'\u2192 {result.available_version}')
|
|
505
|
+
self._version_label.setStyleSheet(ACTION_CARD_VERSION_STYLE + ' color: grey;')
|
|
527
506
|
|
|
528
507
|
def finalize_checking(self) -> None:
|
|
529
508
|
"""Resolve a still-pending 'Checking\u2026' status to 'Needed'.
|
|
@@ -611,9 +590,8 @@ class ActionCard(QFrame):
|
|
|
611
590
|
class ActionCardList(QWidget):
|
|
612
591
|
"""Container of :class:`ActionCard` widgets.
|
|
613
592
|
|
|
614
|
-
Cards are keyed by
|
|
615
|
-
look-ups work across different ``execute_stream`` runs
|
|
616
|
-
``SetupAction`` objects are different Python instances.
|
|
593
|
+
Cards are keyed by ``SetupAction`` directly (frozen dataclass) so
|
|
594
|
+
that look-ups work across different ``execute_stream`` runs.
|
|
617
595
|
"""
|
|
618
596
|
|
|
619
597
|
prerelease_toggled = Signal(str, bool)
|
|
@@ -629,7 +607,7 @@ class ActionCardList(QWidget):
|
|
|
629
607
|
self._layout.addStretch()
|
|
630
608
|
|
|
631
609
|
self._cards: list[ActionCard] = []
|
|
632
|
-
self._action_map: dict[
|
|
610
|
+
self._action_map: dict[SetupAction, ActionCard] = {}
|
|
633
611
|
|
|
634
612
|
# ------------------------------------------------------------------
|
|
635
613
|
# Skeleton loading
|
|
@@ -679,7 +657,7 @@ class ActionCardList(QWidget):
|
|
|
679
657
|
card.prerelease_toggled.connect(self.prerelease_toggled.emit)
|
|
680
658
|
self._layout.insertWidget(self._layout.count() - 1, card)
|
|
681
659
|
self._cards.append(card)
|
|
682
|
-
self._action_map[
|
|
660
|
+
self._action_map[act] = card
|
|
683
661
|
|
|
684
662
|
# ------------------------------------------------------------------
|
|
685
663
|
# Card lookup
|
|
@@ -696,11 +674,10 @@ class ActionCardList(QWidget):
|
|
|
696
674
|
return len(self._cards)
|
|
697
675
|
|
|
698
676
|
def get_card(self, action: SetupAction) -> ActionCard | None:
|
|
699
|
-
"""Look up the card for a given action
|
|
677
|
+
"""Look up the card for a given action.
|
|
700
678
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
but the same logical action maps to the same card.
|
|
679
|
+
``SetupAction`` is a frozen dataclass, so the same logical
|
|
680
|
+
action from different ``execute_stream`` runs hashes equally.
|
|
704
681
|
|
|
705
682
|
Args:
|
|
706
683
|
action: The setup action to find.
|
|
@@ -708,7 +685,7 @@ class ActionCardList(QWidget):
|
|
|
708
685
|
Returns:
|
|
709
686
|
The card widget, or ``None`` if not found.
|
|
710
687
|
"""
|
|
711
|
-
return self._action_map.get(
|
|
688
|
+
return self._action_map.get(action)
|
|
712
689
|
|
|
713
690
|
# ------------------------------------------------------------------
|
|
714
691
|
# Bulk operations
|
|
@@ -45,7 +45,7 @@ from PySide6.QtWidgets import (
|
|
|
45
45
|
|
|
46
46
|
from synodic_client.application.package_state import PackageStateStore
|
|
47
47
|
from synodic_client.application.screen import skip_reason_label
|
|
48
|
-
from synodic_client.application.screen.action_card import ActionCardList
|
|
48
|
+
from synodic_client.application.screen.action_card import ActionCardList
|
|
49
49
|
from synodic_client.application.screen.card import CardFrame
|
|
50
50
|
from synodic_client.application.screen.install_workers import run_install, run_preview
|
|
51
51
|
from synodic_client.application.screen.log_panel import ExecutionLogPanel
|
|
@@ -595,11 +595,11 @@ class SetupPreviewWidget(QWidget):
|
|
|
595
595
|
self._install_btn.setEnabled(True)
|
|
596
596
|
|
|
597
597
|
def _on_preview_resolved(self, preview: SetupResults, manifest_path: str, temp_dir_path: str) -> None:
|
|
598
|
-
"""Handle the fully-resolved preview
|
|
598
|
+
"""Handle the fully-resolved preview.
|
|
599
599
|
|
|
600
600
|
Called after ``MANIFEST_LOADED`` — cards are already visible
|
|
601
|
-
from the earlier ``_on_manifest_parsed`` handler. This
|
|
602
|
-
|
|
601
|
+
from the earlier ``_on_manifest_parsed`` handler. This updates
|
|
602
|
+
the temp-dir reference and emits metadata.
|
|
603
603
|
"""
|
|
604
604
|
if self._model.preview is None:
|
|
605
605
|
return
|
|
@@ -609,19 +609,13 @@ class SetupPreviewWidget(QWidget):
|
|
|
609
609
|
if preview.metadata:
|
|
610
610
|
self.metadata_ready.emit(preview)
|
|
611
611
|
|
|
612
|
-
for action in preview.actions:
|
|
613
|
-
if action.cli_command:
|
|
614
|
-
card = self._card_list.get_card(action)
|
|
615
|
-
if card is not None:
|
|
616
|
-
card.update_command(action)
|
|
617
|
-
|
|
618
612
|
def _on_action_checked(self, row: int, result: SetupActionResult) -> None:
|
|
619
613
|
"""Update the model and action card with a dry-run result."""
|
|
620
614
|
m = self._model
|
|
621
615
|
if result.skipped and result.skip_reason == SkipReason.UPDATE_AVAILABLE:
|
|
622
616
|
label = skip_reason_label(result.skip_reason)
|
|
623
617
|
if 0 <= row < len(m.action_states):
|
|
624
|
-
m.upgradable_keys.add(
|
|
618
|
+
m.upgradable_keys.add(m.action_states[row].action)
|
|
625
619
|
elif result.skipped:
|
|
626
620
|
label = skip_reason_label(result.skip_reason)
|
|
627
621
|
elif not result.success:
|
|
@@ -159,10 +159,9 @@ def _dispatch_preview_event(
|
|
|
159
159
|
) -> None:
|
|
160
160
|
"""Route a single preview stream event to the appropriate callback.
|
|
161
161
|
|
|
162
|
-
Mutates *state* in-place
|
|
162
|
+
Mutates *state* in-place (``got_parsed`` flag).
|
|
163
163
|
"""
|
|
164
164
|
if event.kind == ProgressEventKind.MANIFEST_PARSED and event.manifest:
|
|
165
|
-
state.action_index = {id(a): i for i, a in enumerate(event.manifest.actions)}
|
|
166
165
|
if cb.on_manifest_parsed is not None:
|
|
167
166
|
cb.on_manifest_parsed(event.manifest, manifest_path, temp_dir_str)
|
|
168
167
|
state.got_parsed = True
|
|
@@ -176,16 +175,17 @@ def _dispatch_preview_event(
|
|
|
176
175
|
return
|
|
177
176
|
|
|
178
177
|
if event.kind == ProgressEventKind.MANIFEST_LOADED and event.manifest:
|
|
179
|
-
if not state.got_parsed:
|
|
180
|
-
state.action_index = {id(a): i for i, a in enumerate(event.manifest.actions)}
|
|
181
178
|
if cb.on_preview_ready is not None:
|
|
182
179
|
cb.on_preview_ready(event.manifest, manifest_path, temp_dir_str)
|
|
183
180
|
return
|
|
184
181
|
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
182
|
+
if (
|
|
183
|
+
event.kind == ProgressEventKind.ACTION_COMPLETED
|
|
184
|
+
and event.result
|
|
185
|
+
and event.action_index is not None
|
|
186
|
+
and cb.on_action_checked is not None
|
|
187
|
+
):
|
|
188
|
+
cb.on_action_checked(event.action_index, event.result)
|
|
189
189
|
|
|
190
190
|
|
|
191
191
|
# ---------------------------------------------------------------------------
|
|
@@ -26,7 +26,6 @@ from PySide6.QtWidgets import (
|
|
|
26
26
|
)
|
|
27
27
|
|
|
28
28
|
from synodic_client.application.screen import ACTION_KIND_LABELS, skip_reason_label
|
|
29
|
-
from synodic_client.application.screen.action_card import action_key
|
|
30
29
|
from synodic_client.application.screen.card import CHEVRON_DOWN, CHEVRON_RIGHT, ClickableHeader
|
|
31
30
|
from synodic_client.application.theme import (
|
|
32
31
|
LOG_CHEVRON_STYLE,
|
|
@@ -186,7 +185,7 @@ class ExecutionLogPanel(QWidget):
|
|
|
186
185
|
self._layout.addStretch()
|
|
187
186
|
|
|
188
187
|
# Map action content-key → section widget for quick lookup
|
|
189
|
-
self._sections: dict[
|
|
188
|
+
self._sections: dict[SetupAction, ActionLogSection] = {}
|
|
190
189
|
self._section_count = 0
|
|
191
190
|
|
|
192
191
|
# --- Public API ---
|
|
@@ -204,7 +203,7 @@ class ExecutionLogPanel(QWidget):
|
|
|
204
203
|
section = ActionLogSection(action, self._section_count, self)
|
|
205
204
|
# Insert before the stretch
|
|
206
205
|
self._layout.insertWidget(self._layout.count() - 1, section)
|
|
207
|
-
self._sections[
|
|
206
|
+
self._sections[action] = section
|
|
208
207
|
|
|
209
208
|
return section
|
|
210
209
|
|
|
@@ -217,7 +216,7 @@ class ExecutionLogPanel(QWidget):
|
|
|
217
216
|
Returns:
|
|
218
217
|
The section widget, or ``None`` if not found.
|
|
219
218
|
"""
|
|
220
|
-
return self._sections.get(
|
|
219
|
+
return self._sections.get(action)
|
|
221
220
|
|
|
222
221
|
def on_sub_progress(self, action: SetupAction, progress: SubActionProgress) -> None:
|
|
223
222
|
"""Handle a sub-action progress event.
|
|
@@ -11,9 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import enum
|
|
12
12
|
from collections.abc import Callable
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
|
-
from enum import Enum, auto
|
|
15
14
|
from pathlib import Path
|
|
16
|
-
from typing import Protocol, runtime_checkable
|
|
17
15
|
|
|
18
16
|
from porringer.schema import (
|
|
19
17
|
PluginInfo,
|
|
@@ -25,7 +23,6 @@ from porringer.schema import (
|
|
|
25
23
|
)
|
|
26
24
|
from porringer.schema.plugin import RuntimePackageResult
|
|
27
25
|
|
|
28
|
-
from synodic_client.application.screen.action_card import action_key
|
|
29
26
|
from synodic_client.application.uri import normalize_manifest_key
|
|
30
27
|
|
|
31
28
|
# ---------------------------------------------------------------------------
|
|
@@ -239,7 +236,6 @@ class PreviewModel:
|
|
|
239
236
|
|
|
240
237
|
def __init__(self) -> None:
|
|
241
238
|
"""Initialise a blank preview model."""
|
|
242
|
-
self._action_key = action_key
|
|
243
239
|
self._normalize = normalize_manifest_key
|
|
244
240
|
|
|
245
241
|
self.phase: PreviewPhase = PreviewPhase.IDLE
|
|
@@ -250,19 +246,19 @@ class PreviewModel:
|
|
|
250
246
|
self.plugin_installed: dict[str, bool] = {}
|
|
251
247
|
self.prerelease_overrides: set[str] = set()
|
|
252
248
|
self.action_states: list[ActionState] = []
|
|
253
|
-
self._action_state_map: dict[
|
|
249
|
+
self._action_state_map: dict[SetupAction, ActionState] = {}
|
|
254
250
|
self._action_state_map_len: int = 0
|
|
255
|
-
self.upgradable_keys: set[
|
|
251
|
+
self.upgradable_keys: set[SetupAction] = set()
|
|
256
252
|
self.checked_count: int = 0
|
|
257
253
|
self.completed_count: int = 0
|
|
258
254
|
self.temp_dir: str | None = None
|
|
259
255
|
|
|
260
256
|
# -- Computed helpers --------------------------------------------------
|
|
261
257
|
|
|
262
|
-
def _ensure_action_state_map(self) -> dict[
|
|
263
|
-
"""Return the action
|
|
258
|
+
def _ensure_action_state_map(self) -> dict[SetupAction, ActionState]:
|
|
259
|
+
"""Return the action → state lookup, rebuilding if stale."""
|
|
264
260
|
if len(self.action_states) != self._action_state_map_len:
|
|
265
|
-
self._action_state_map = {
|
|
261
|
+
self._action_state_map = {s.action: s for s in self.action_states}
|
|
266
262
|
self._action_state_map_len = len(self.action_states)
|
|
267
263
|
return self._action_state_map
|
|
268
264
|
|
|
@@ -281,8 +277,8 @@ class PreviewModel:
|
|
|
281
277
|
return self.actionable_count > 0 or any(s.action.kind is None for s in self.action_states)
|
|
282
278
|
|
|
283
279
|
def action_state_for(self, act: SetupAction) -> ActionState | None:
|
|
284
|
-
"""Look up :class:`ActionState`
|
|
285
|
-
return self._ensure_action_state_map().get(
|
|
280
|
+
"""Look up :class:`ActionState` for *act* (O(1) amortized)."""
|
|
281
|
+
return self._ensure_action_state_map().get(act)
|
|
286
282
|
|
|
287
283
|
def has_same_manifest(self, key: str) -> bool:
|
|
288
284
|
"""Return ``True`` if *key* matches the current manifest key."""
|
|
@@ -341,64 +337,4 @@ class PreviewConfig:
|
|
|
341
337
|
class _DispatchState:
|
|
342
338
|
"""Mutable accumulator for :func:`_dispatch_preview_event`."""
|
|
343
339
|
|
|
344
|
-
action_index: dict[int, int] = field(default_factory=dict)
|
|
345
340
|
got_parsed: bool = False
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
# ---------------------------------------------------------------------------
|
|
349
|
-
# Update view protocol & banner data models
|
|
350
|
-
# ---------------------------------------------------------------------------
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
@runtime_checkable
|
|
354
|
-
class UpdateView(Protocol):
|
|
355
|
-
"""Minimal display contract for the self-update lifecycle.
|
|
356
|
-
|
|
357
|
-
:class:`UpdateBanner` satisfies this protocol implicitly via
|
|
358
|
-
structural typing. The controller broadcasts state transitions
|
|
359
|
-
through a ``list[UpdateView]`` so that every window showing update
|
|
360
|
-
status stays in sync.
|
|
361
|
-
"""
|
|
362
|
-
|
|
363
|
-
def show_downloading(self, version: str) -> None:
|
|
364
|
-
"""Indicate that *version* is being downloaded."""
|
|
365
|
-
...
|
|
366
|
-
|
|
367
|
-
def show_downloading_progress(self, percentage: int) -> None:
|
|
368
|
-
"""Update the download progress indicator."""
|
|
369
|
-
...
|
|
370
|
-
|
|
371
|
-
def show_ready(self, version: str) -> None:
|
|
372
|
-
"""Indicate that *version* is downloaded and ready to install."""
|
|
373
|
-
...
|
|
374
|
-
|
|
375
|
-
def show_error(self, message: str) -> None:
|
|
376
|
-
"""Display an error *message* in the update area."""
|
|
377
|
-
...
|
|
378
|
-
|
|
379
|
-
def hide_banner(self) -> None:
|
|
380
|
-
"""Hide the update banner."""
|
|
381
|
-
...
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
class UpdateBannerState(Enum):
|
|
385
|
-
"""Visual states for the update banner."""
|
|
386
|
-
|
|
387
|
-
HIDDEN = auto()
|
|
388
|
-
DOWNLOADING = auto()
|
|
389
|
-
READY = auto()
|
|
390
|
-
ERROR = auto()
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
@dataclass(frozen=True, slots=True)
|
|
394
|
-
class _BannerConfig:
|
|
395
|
-
"""Bundled visual configuration for a banner state transition."""
|
|
396
|
-
|
|
397
|
-
state: UpdateBannerState
|
|
398
|
-
style: str
|
|
399
|
-
icon: str
|
|
400
|
-
text: str
|
|
401
|
-
text_style: str
|
|
402
|
-
version: str = ''
|
|
403
|
-
action_label: str = ''
|
|
404
|
-
show_progress: bool = False
|
|
@@ -32,7 +32,8 @@ from synodic_client.application.config_store import ConfigStore
|
|
|
32
32
|
from synodic_client.application.icon import app_icon
|
|
33
33
|
from synodic_client.application.screen import _format_relative_time
|
|
34
34
|
from synodic_client.application.screen.card import CardFrame
|
|
35
|
-
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
35
|
+
from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE
|
|
36
|
+
from synodic_client.application.update_model import UpdateModel
|
|
36
37
|
from synodic_client.logging import log_path, set_debug_level
|
|
37
38
|
from synodic_client.schema import GITHUB_REPO_URL
|
|
38
39
|
from synodic_client.startup import is_startup_registered, register_startup, remove_startup
|
|
@@ -237,6 +238,32 @@ class SettingsWindow(QMainWindow):
|
|
|
237
238
|
card.content_layout.addLayout(row)
|
|
238
239
|
return card
|
|
239
240
|
|
|
241
|
+
# ------------------------------------------------------------------
|
|
242
|
+
# Model binding
|
|
243
|
+
# ------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
def connect_model(self, model: UpdateModel) -> None:
|
|
246
|
+
"""Connect to an :class:`UpdateModel` for state observation.
|
|
247
|
+
|
|
248
|
+
The model's settings-facing signals drive the update status
|
|
249
|
+
label, check button, restart button, and timestamp label.
|
|
250
|
+
"""
|
|
251
|
+
model.status_text_changed.connect(self._on_status_changed)
|
|
252
|
+
model.check_button_enabled_changed.connect(self._check_updates_btn.setEnabled)
|
|
253
|
+
model.restart_visible_changed.connect(self._restart_btn.setVisible)
|
|
254
|
+
model.last_checked_changed.connect(self._on_last_checked_changed)
|
|
255
|
+
|
|
256
|
+
def _on_status_changed(self, text: str, style: str) -> None:
|
|
257
|
+
"""Apply a status text and style from the model."""
|
|
258
|
+
self._update_status_label.setText(text)
|
|
259
|
+
self._update_status_label.setStyleSheet(style)
|
|
260
|
+
|
|
261
|
+
def _on_last_checked_changed(self, timestamp: str) -> None:
|
|
262
|
+
"""Apply a *last updated* timestamp from the model."""
|
|
263
|
+
relative = _format_relative_time(timestamp)
|
|
264
|
+
self._last_client_update_label.setText(f'Last updated: {relative}')
|
|
265
|
+
self._last_client_update_label.setToolTip(f'Last updated: {timestamp}')
|
|
266
|
+
|
|
240
267
|
# ------------------------------------------------------------------
|
|
241
268
|
# Public API
|
|
242
269
|
# ------------------------------------------------------------------
|
|
@@ -275,37 +302,6 @@ class SettingsWindow(QMainWindow):
|
|
|
275
302
|
else:
|
|
276
303
|
self._last_client_update_label.setText('')
|
|
277
304
|
|
|
278
|
-
def set_update_status(self, text: str, style: str = '') -> None:
|
|
279
|
-
"""Set the inline status text next to the *Check for Updates* button.
|
|
280
|
-
|
|
281
|
-
Args:
|
|
282
|
-
text: The status message.
|
|
283
|
-
style: Optional stylesheet for the label (e.g. color).
|
|
284
|
-
"""
|
|
285
|
-
self._update_status_label.setText(text)
|
|
286
|
-
self._update_status_label.setStyleSheet(style)
|
|
287
|
-
|
|
288
|
-
def set_checking(self) -> None:
|
|
289
|
-
"""Enter the *checking* state — disable button and show status."""
|
|
290
|
-
self._check_updates_btn.setEnabled(False)
|
|
291
|
-
self._restart_btn.hide()
|
|
292
|
-
self._update_status_label.setText('Checking\u2026')
|
|
293
|
-
self._update_status_label.setStyleSheet(UPDATE_STATUS_CHECKING_STYLE)
|
|
294
|
-
|
|
295
|
-
def reset_check_updates_button(self) -> None:
|
|
296
|
-
"""Re-enable the *Check for Updates* button after a check completes."""
|
|
297
|
-
self._check_updates_btn.setEnabled(True)
|
|
298
|
-
|
|
299
|
-
def set_last_checked(self, timestamp: str) -> None:
|
|
300
|
-
"""Update the *last updated* label from an ISO 8601 timestamp."""
|
|
301
|
-
relative = _format_relative_time(timestamp)
|
|
302
|
-
self._last_client_update_label.setText(f'Last updated: {relative}')
|
|
303
|
-
self._last_client_update_label.setToolTip(f'Last updated: {timestamp}')
|
|
304
|
-
|
|
305
|
-
def show_restart_button(self) -> None:
|
|
306
|
-
"""Show the *Restart & Update* button."""
|
|
307
|
-
self._restart_btn.show()
|
|
308
|
-
|
|
309
305
|
def show(self) -> None:
|
|
310
306
|
"""Sync controls from config, size to content, then show the window."""
|
|
311
307
|
self.sync_from_config()
|
|
@@ -360,8 +356,6 @@ class SettingsWindow(QMainWindow):
|
|
|
360
356
|
|
|
361
357
|
def _on_check_updates_clicked(self) -> None:
|
|
362
358
|
"""Handle the *Check for Updates* button click."""
|
|
363
|
-
self._check_updates_btn.setEnabled(False)
|
|
364
|
-
self._update_status_label.setText('Checking\u2026')
|
|
365
359
|
self.check_updates_requested.emit()
|
|
366
360
|
|
|
367
361
|
def _on_channel_changed(self, index: int) -> None:
|
{synodic_client-0.0.1.dev65 → synodic_client-0.0.1.dev67}/synodic_client/application/screen/tray.py
RENAMED
|
@@ -16,6 +16,7 @@ from synodic_client.application.screen.screen import MainWindow
|
|
|
16
16
|
from synodic_client.application.screen.settings import SettingsWindow
|
|
17
17
|
from synodic_client.application.screen.tool_update_controller import ToolUpdateOrchestrator
|
|
18
18
|
from synodic_client.application.update_controller import UpdateController
|
|
19
|
+
from synodic_client.application.update_model import UpdateModel
|
|
19
20
|
from synodic_client.client import Client
|
|
20
21
|
|
|
21
22
|
if TYPE_CHECKING:
|
|
@@ -66,17 +67,29 @@ class TrayScreen:
|
|
|
66
67
|
# MainWindow gear button -> open settings
|
|
67
68
|
window.settings_requested.connect(self._show_settings)
|
|
68
69
|
|
|
70
|
+
# Update model — centralised observable state for the update lifecycle
|
|
71
|
+
self._update_model = UpdateModel()
|
|
72
|
+
|
|
69
73
|
# Update controller - owns the self-update lifecycle & timer
|
|
70
74
|
self._banner = window.update_banner
|
|
71
75
|
self._update_controller = UpdateController(
|
|
72
76
|
app,
|
|
73
77
|
client,
|
|
74
|
-
|
|
75
|
-
settings_window=self._settings_window,
|
|
78
|
+
self._update_model,
|
|
76
79
|
store=self._store,
|
|
77
80
|
)
|
|
78
81
|
self._update_controller.set_user_active_predicate(self._is_user_active)
|
|
79
82
|
|
|
83
|
+
# Connect views to the model
|
|
84
|
+
self._banner.connect_model(self._update_model)
|
|
85
|
+
self._settings_window.connect_model(self._update_model)
|
|
86
|
+
|
|
87
|
+
# Wire user-action signals back to the controller
|
|
88
|
+
self._banner.restart_requested.connect(self._update_controller.request_apply)
|
|
89
|
+
self._banner.retry_requested.connect(self._update_controller.request_retry)
|
|
90
|
+
self._settings_window.check_updates_requested.connect(self._update_controller.request_check)
|
|
91
|
+
self._settings_window.restart_requested.connect(self._update_controller.request_apply)
|
|
92
|
+
|
|
80
93
|
# Tool update orchestrator - owns tool/package update lifecycle
|
|
81
94
|
self._tool_orchestrator = ToolUpdateOrchestrator(
|
|
82
95
|
window,
|