synodic-client 0.0.1.dev36__tar.gz → 0.0.1.dev38__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.dev36 → synodic_client-0.0.1.dev38}/PKG-INFO +3 -3
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/pyproject.toml +12 -4
- synodic_client-0.0.1.dev38/synodic_client/_version.py +1 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/bootstrap.py +1 -1
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/icon.py +5 -7
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/__init__.py +15 -1
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/action_card.py +83 -150
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/card.py +1 -1
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/install.py +469 -403
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/log_panel.py +26 -20
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/screen.py +35 -93
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/spinner.py +1 -1
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/tray.py +5 -1
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/update_banner.py +54 -50
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/theme.py +3 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/cli.py +1 -1
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/config.py +8 -5
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/logging.py +9 -1
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/resolution.py +1 -1
- synodic_client-0.0.1.dev38/tests/unit/qt/conftest.py +24 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/qt/test_action_card.py +110 -75
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/qt/test_install_preview.py +7 -158
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/qt/test_log_panel.py +16 -17
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/qt/test_logging.py +71 -0
- synodic_client-0.0.1.dev38/tests/unit/qt/test_preview_model.py +195 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/qt/test_settings.py +4 -9
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/qt/test_update_banner.py +56 -28
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_config.py +0 -16
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_resolution.py +4 -16
- synodic_client-0.0.1.dev36/synodic_client/_version.py +0 -1
- synodic_client-0.0.1.dev36/tests/unit/qt/conftest.py +0 -10
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/README.md +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/qt.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/settings.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/workers.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/updater.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_updater.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/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.dev38
|
|
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.
|
|
11
|
+
Requires-Dist: porringer>=0.2.1.dev54
|
|
12
12
|
Requires-Dist: qasync>=0.28.0
|
|
13
|
-
Requires-Dist: velopack>=0.0.
|
|
13
|
+
Requires-Dist: velopack>=0.0.1444.dev49733
|
|
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.
|
|
13
|
+
"porringer>=0.2.1.dev54",
|
|
14
14
|
"qasync>=0.28.0",
|
|
15
|
-
"velopack>=0.0.
|
|
15
|
+
"velopack>=0.0.1444.dev49733",
|
|
16
16
|
"typer>=0.24.1",
|
|
17
17
|
]
|
|
18
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev38"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -35,7 +35,7 @@ build = [
|
|
|
35
35
|
"pyinstaller>=6.19.0",
|
|
36
36
|
]
|
|
37
37
|
lint = [
|
|
38
|
-
"ruff>=0.15.
|
|
38
|
+
"ruff>=0.15.3",
|
|
39
39
|
"pyrefly>=0.54.0",
|
|
40
40
|
]
|
|
41
41
|
test = [
|
|
@@ -75,6 +75,14 @@ select = [
|
|
|
75
75
|
"PT",
|
|
76
76
|
]
|
|
77
77
|
|
|
78
|
+
[tool.ruff.lint.per-file-ignores]
|
|
79
|
+
"synodic_client/application/bootstrap.py" = [
|
|
80
|
+
"E402",
|
|
81
|
+
]
|
|
82
|
+
"synodic_client/cli.py" = [
|
|
83
|
+
"PLC0415",
|
|
84
|
+
]
|
|
85
|
+
|
|
78
86
|
[tool.ruff.lint.pydocstyle]
|
|
79
87
|
convention = "google"
|
|
80
88
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.0.1.dev38'
|
{synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/bootstrap.py
RENAMED
|
@@ -44,7 +44,7 @@ if not _dev_mode:
|
|
|
44
44
|
remove_startup()
|
|
45
45
|
|
|
46
46
|
# Heavy imports happen here — PySide6, porringer, etc.
|
|
47
|
-
from synodic_client.application.qt import application
|
|
47
|
+
from synodic_client.application.qt import application
|
|
48
48
|
|
|
49
49
|
_uri = next((a for a in sys.argv[1:] if a.lower().startswith(f'{_PROTOCOL_SCHEME}://')), None)
|
|
50
50
|
application(uri=_uri, dev_mode=_dev_mode)
|
{synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/icon.py
RENAMED
|
@@ -4,13 +4,14 @@ Loads the icon once from package resources and caches the ``QIcon``
|
|
|
4
4
|
so every caller shares the same instance.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import functools
|
|
8
|
+
|
|
7
9
|
from PySide6.QtGui import QIcon, QPixmap
|
|
8
10
|
|
|
9
11
|
from synodic_client.client import Client
|
|
10
12
|
|
|
11
|
-
_cached_icon: QIcon | None = None
|
|
12
|
-
|
|
13
13
|
|
|
14
|
+
@functools.cache
|
|
14
15
|
def app_icon() -> QIcon:
|
|
15
16
|
"""Return the shared application ``QIcon``, loading it on first call.
|
|
16
17
|
|
|
@@ -20,8 +21,5 @@ def app_icon() -> QIcon:
|
|
|
20
21
|
Returns:
|
|
21
22
|
A ``QIcon`` backed by the application logo.
|
|
22
23
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
with Client.resource(Client.icon) as icon_path:
|
|
26
|
-
_cached_icon = QIcon(QPixmap(str(icon_path)))
|
|
27
|
-
return _cached_icon
|
|
24
|
+
with Client.resource(Client.icon) as icon_path:
|
|
25
|
+
return QIcon(QPixmap(str(icon_path)))
|
|
@@ -6,7 +6,7 @@ execution log panel live here to avoid circular imports.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from porringer.schema import SkipReason
|
|
9
|
+
from porringer.schema import SetupAction, SkipReason
|
|
10
10
|
from porringer.schema.plugin import PluginKind
|
|
11
11
|
|
|
12
12
|
ACTION_KIND_LABELS: dict[PluginKind | None, str] = {
|
|
@@ -54,3 +54,17 @@ def skip_reason_label(reason: SkipReason | None) -> str:
|
|
|
54
54
|
if reason is None:
|
|
55
55
|
return 'Skipped'
|
|
56
56
|
return SKIP_REASON_LABELS.get(reason, reason.name.replace('_', ' ').capitalize())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def format_cli_command(action: SetupAction) -> str:
|
|
60
|
+
"""Return a human-readable CLI command string for *action*.
|
|
61
|
+
|
|
62
|
+
Prefers ``cli_command``, falls back to ``command``, then synthesises
|
|
63
|
+
an ``installer install <package>`` string for package actions, and
|
|
64
|
+
finally returns the action description as a last resort.
|
|
65
|
+
"""
|
|
66
|
+
if parts := (action.cli_command or action.command):
|
|
67
|
+
return ' '.join(parts)
|
|
68
|
+
if action.kind == PluginKind.PACKAGE and action.package:
|
|
69
|
+
return f'{action.installer or "pip"} install {action.package}'
|
|
70
|
+
return action.description
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"""Action card widgets for the install preview screen.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
Each card shows essential information (package name, type, version,
|
|
4
|
+
status badge). During install, execution output is routed to the
|
|
5
|
+
unified :class:`~synodic_client.application.screen.log_panel.ExecutionLogPanel`
|
|
6
|
+
rather than displayed inline.
|
|
7
7
|
|
|
8
8
|
:class:`ActionCard` is the per-action widget.
|
|
9
9
|
:class:`ActionCardList` is the scrollable container that holds them.
|
|
@@ -11,34 +11,29 @@ expands inline to display execution output during install.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import html as html_mod
|
|
15
14
|
import logging
|
|
16
15
|
|
|
17
16
|
from porringer.backend.command.core.action_builder import PHASE_ORDER
|
|
18
17
|
from porringer.schema import SetupAction, SetupActionResult, SkipReason
|
|
19
18
|
from porringer.schema.plugin import PluginKind
|
|
20
19
|
from PySide6.QtCore import QRect, Qt, QTimer, Signal
|
|
21
|
-
from PySide6.QtGui import QColor,
|
|
20
|
+
from PySide6.QtGui import QColor, QPainter, QPen
|
|
22
21
|
from PySide6.QtWidgets import (
|
|
23
22
|
QApplication,
|
|
24
23
|
QCheckBox,
|
|
25
24
|
QFrame,
|
|
26
25
|
QHBoxLayout,
|
|
27
26
|
QLabel,
|
|
28
|
-
QScrollArea,
|
|
29
|
-
QSizePolicy,
|
|
30
|
-
QTextEdit,
|
|
31
27
|
QToolButton,
|
|
32
28
|
QVBoxLayout,
|
|
33
29
|
QWidget,
|
|
34
30
|
)
|
|
35
31
|
|
|
36
|
-
from synodic_client.application.screen import ACTION_KIND_LABELS, skip_reason_label
|
|
32
|
+
from synodic_client.application.screen import ACTION_KIND_LABELS, format_cli_command, skip_reason_label
|
|
37
33
|
from synodic_client.application.theme import (
|
|
38
34
|
ACTION_CARD_COMMAND_STYLE,
|
|
39
35
|
ACTION_CARD_DESC_STYLE,
|
|
40
36
|
ACTION_CARD_EXECUTING_STYLE,
|
|
41
|
-
ACTION_CARD_LOG_STYLE,
|
|
42
37
|
ACTION_CARD_PACKAGE_STYLE,
|
|
43
38
|
ACTION_CARD_SKELETON_BAR_STYLE,
|
|
44
39
|
ACTION_CARD_SKELETON_STYLE,
|
|
@@ -48,6 +43,7 @@ from synodic_client.application.theme import (
|
|
|
48
43
|
ACTION_CARD_STATUS_DONE,
|
|
49
44
|
ACTION_CARD_STATUS_FAILED,
|
|
50
45
|
ACTION_CARD_STATUS_NEEDED,
|
|
46
|
+
ACTION_CARD_STATUS_PENDING,
|
|
51
47
|
ACTION_CARD_STATUS_RUNNING,
|
|
52
48
|
ACTION_CARD_STATUS_SATISFIED,
|
|
53
49
|
ACTION_CARD_STATUS_SKIPPED,
|
|
@@ -60,13 +56,6 @@ from synodic_client.application.theme import (
|
|
|
60
56
|
COPY_BTN_STYLE,
|
|
61
57
|
COPY_FEEDBACK_MS,
|
|
62
58
|
COPY_ICON,
|
|
63
|
-
LOG_COLOR_ERROR,
|
|
64
|
-
LOG_COLOR_PHASE,
|
|
65
|
-
LOG_COLOR_STDERR,
|
|
66
|
-
LOG_COLOR_STDOUT,
|
|
67
|
-
LOG_COLOR_SUCCESS,
|
|
68
|
-
MONOSPACE_FAMILY,
|
|
69
|
-
MONOSPACE_SIZE,
|
|
70
59
|
)
|
|
71
60
|
|
|
72
61
|
logger = logging.getLogger(__name__)
|
|
@@ -113,12 +102,14 @@ def action_sort_key(action: SetupAction) -> int:
|
|
|
113
102
|
|
|
114
103
|
|
|
115
104
|
def _format_command(action: SetupAction) -> str:
|
|
116
|
-
"""Return a short CLI command string for display.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
105
|
+
"""Return a short CLI command string for display.
|
|
106
|
+
|
|
107
|
+
Wraps :func:`~synodic_client.application.screen.format_cli_command`
|
|
108
|
+
but returns an empty string instead of the description fallback so
|
|
109
|
+
cards only show an explicit command line.
|
|
110
|
+
"""
|
|
111
|
+
text = format_cli_command(action)
|
|
112
|
+
return '' if text == action.description else text
|
|
122
113
|
|
|
123
114
|
|
|
124
115
|
# ---------------------------------------------------------------------------
|
|
@@ -138,7 +129,7 @@ class _CardSpinner(QWidget):
|
|
|
138
129
|
self._angle = 0
|
|
139
130
|
self.setFixedSize(ACTION_CARD_SPINNER_SIZE, ACTION_CARD_SPINNER_SIZE)
|
|
140
131
|
|
|
141
|
-
def paintEvent(self, _event: object) -> None:
|
|
132
|
+
def paintEvent(self, _event: object) -> None:
|
|
142
133
|
"""Draw the muted track and animated highlight arc."""
|
|
143
134
|
painter = QPainter(self)
|
|
144
135
|
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
@@ -211,7 +202,6 @@ class ActionCard(QFrame):
|
|
|
211
202
|
self.setObjectName('actionCard')
|
|
212
203
|
self._action: SetupAction | None = None
|
|
213
204
|
self._is_skeleton = skeleton
|
|
214
|
-
self._log_expanded = False
|
|
215
205
|
self._checking = False
|
|
216
206
|
self._check_available_version: str | None = None
|
|
217
207
|
|
|
@@ -275,7 +265,6 @@ class ActionCard(QFrame):
|
|
|
275
265
|
def _init_real_ui(self) -> None:
|
|
276
266
|
"""Build the action card layout."""
|
|
277
267
|
self.setStyleSheet(ACTION_CARD_STYLE)
|
|
278
|
-
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
279
268
|
|
|
280
269
|
outer = QVBoxLayout(self)
|
|
281
270
|
outer.setContentsMargins(6, 6, 6, 6)
|
|
@@ -284,7 +273,6 @@ class ActionCard(QFrame):
|
|
|
284
273
|
outer.addLayout(self._build_top_row())
|
|
285
274
|
outer.addWidget(self._build_description_row())
|
|
286
275
|
outer.addWidget(self._build_command_row())
|
|
287
|
-
outer.addWidget(self._build_log_output())
|
|
288
276
|
|
|
289
277
|
def _build_top_row(self) -> QHBoxLayout:
|
|
290
278
|
"""Build the top row: type badge | package name ... version | status/spinner | prerelease."""
|
|
@@ -363,40 +351,10 @@ class ActionCard(QFrame):
|
|
|
363
351
|
self._command_row.hide()
|
|
364
352
|
return self._command_row
|
|
365
353
|
|
|
366
|
-
def _build_log_output(self) -> QTextEdit:
|
|
367
|
-
"""Build the inline log body (hidden by default)."""
|
|
368
|
-
self._log_output = QTextEdit()
|
|
369
|
-
self._log_output.setReadOnly(True)
|
|
370
|
-
self._log_output.setFont(QFont(MONOSPACE_FAMILY, MONOSPACE_SIZE))
|
|
371
|
-
self._log_output.setStyleSheet(ACTION_CARD_LOG_STYLE)
|
|
372
|
-
self._log_output.setMinimumHeight(40)
|
|
373
|
-
self._log_output.setMaximumHeight(250)
|
|
374
|
-
self._log_output.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
|
375
|
-
self._log_output.hide()
|
|
376
|
-
return self._log_output
|
|
377
|
-
|
|
378
354
|
# ------------------------------------------------------------------
|
|
379
|
-
# Mouse events (
|
|
355
|
+
# Mouse events (copy button)
|
|
380
356
|
# ------------------------------------------------------------------
|
|
381
357
|
|
|
382
|
-
def mousePressEvent(self, event: object) -> None: # noqa: N802
|
|
383
|
-
"""Toggle the inline log body on click."""
|
|
384
|
-
if self._is_skeleton or not hasattr(self, '_log_output'):
|
|
385
|
-
return
|
|
386
|
-
# Don't toggle the log when clicking interactive child widgets
|
|
387
|
-
if hasattr(self, '_copy_btn') and self._copy_btn.underMouse():
|
|
388
|
-
return
|
|
389
|
-
if hasattr(self, '_package_label') and self._package_label.underMouse():
|
|
390
|
-
return
|
|
391
|
-
if hasattr(self, '_desc_label') and self._desc_label.underMouse():
|
|
392
|
-
return
|
|
393
|
-
self._toggle_log()
|
|
394
|
-
|
|
395
|
-
def _toggle_log(self) -> None:
|
|
396
|
-
"""Expand or collapse the inline log body."""
|
|
397
|
-
self._log_expanded = not self._log_expanded
|
|
398
|
-
self._log_output.setVisible(self._log_expanded)
|
|
399
|
-
|
|
400
358
|
def _copy_command(self) -> None:
|
|
401
359
|
"""Copy the command label text to the clipboard with brief feedback."""
|
|
402
360
|
clipboard = QApplication.clipboard()
|
|
@@ -471,23 +429,8 @@ class ActionCard(QFrame):
|
|
|
471
429
|
|
|
472
430
|
self._version_label.setText('')
|
|
473
431
|
|
|
474
|
-
# Status
|
|
475
|
-
|
|
476
|
-
action.installer is not None
|
|
477
|
-
and action.installer in plugin_installed
|
|
478
|
-
and not plugin_installed[action.installer]
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
if installer_missing:
|
|
482
|
-
self._status_label.setText('Not installed')
|
|
483
|
-
self._status_label.setStyleSheet(ACTION_CARD_STATUS_UNAVAILABLE)
|
|
484
|
-
self._status_label.show()
|
|
485
|
-
else:
|
|
486
|
-
# Show spinner instead of status text while checking
|
|
487
|
-
self._status_label.hide()
|
|
488
|
-
self._checking = True
|
|
489
|
-
self._spinner_canvas.show()
|
|
490
|
-
self._spinner_timer.start()
|
|
432
|
+
# Status
|
|
433
|
+
self._populate_status(action, plugin_installed)
|
|
491
434
|
|
|
492
435
|
# Pre-release checkbox
|
|
493
436
|
if action.package is not None:
|
|
@@ -525,6 +468,40 @@ class ActionCard(QFrame):
|
|
|
525
468
|
else:
|
|
526
469
|
self._command_row.hide()
|
|
527
470
|
|
|
471
|
+
def _populate_status(
|
|
472
|
+
self,
|
|
473
|
+
action: SetupAction,
|
|
474
|
+
plugin_installed: dict[str, bool],
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Set the initial status badge during :meth:`populate`.
|
|
477
|
+
|
|
478
|
+
Bare-command actions (``kind is None``) show a static *Pending*
|
|
479
|
+
badge. Plugin-backed actions either flag a missing installer or
|
|
480
|
+
start the dry-run spinner.
|
|
481
|
+
"""
|
|
482
|
+
if action.kind is None:
|
|
483
|
+
self._status_label.setText('Pending')
|
|
484
|
+
self._status_label.setStyleSheet(ACTION_CARD_STATUS_PENDING)
|
|
485
|
+
self._status_label.show()
|
|
486
|
+
return
|
|
487
|
+
|
|
488
|
+
installer_missing = (
|
|
489
|
+
action.installer is not None
|
|
490
|
+
and action.installer in plugin_installed
|
|
491
|
+
and not plugin_installed[action.installer]
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
if installer_missing:
|
|
495
|
+
self._status_label.setText('Not installed')
|
|
496
|
+
self._status_label.setStyleSheet(ACTION_CARD_STATUS_UNAVAILABLE)
|
|
497
|
+
self._status_label.show()
|
|
498
|
+
else:
|
|
499
|
+
# Show spinner instead of status text while checking
|
|
500
|
+
self._status_label.hide()
|
|
501
|
+
self._checking = True
|
|
502
|
+
self._spinner_canvas.show()
|
|
503
|
+
self._spinner_timer.start()
|
|
504
|
+
|
|
528
505
|
def initial_status(self) -> str:
|
|
529
506
|
"""Return the initial status text set during :meth:`populate`."""
|
|
530
507
|
if self._is_skeleton or not hasattr(self, '_status_label'):
|
|
@@ -549,6 +526,15 @@ class ActionCard(QFrame):
|
|
|
549
526
|
def set_check_result(self, result: SetupActionResult) -> None:
|
|
550
527
|
"""Update the card with a dry-run check result.
|
|
551
528
|
|
|
529
|
+
Handles four cases:
|
|
530
|
+
|
|
531
|
+
* **Skipped (update available)** — amber "Update available" badge.
|
|
532
|
+
* **Skipped (other)** — muted satisfied badge.
|
|
533
|
+
* **Failed** — red "Failed" badge with diagnostic tooltip.
|
|
534
|
+
This covers backend failures surfaced during the dry-run
|
|
535
|
+
(e.g. missing SCM plugin, unresolvable deferred action).
|
|
536
|
+
* **Needed** — default blue badge.
|
|
537
|
+
|
|
552
538
|
Args:
|
|
553
539
|
result: The action check result from the preview worker.
|
|
554
540
|
"""
|
|
@@ -565,6 +551,15 @@ class ActionCard(QFrame):
|
|
|
565
551
|
label = skip_reason_label(result.skip_reason)
|
|
566
552
|
self._status_label.setText(label)
|
|
567
553
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_SATISFIED)
|
|
554
|
+
elif not result.success:
|
|
555
|
+
label = 'Failed'
|
|
556
|
+
self._status_label.setText(label)
|
|
557
|
+
self._status_label.setStyleSheet(ACTION_CARD_STATUS_FAILED)
|
|
558
|
+
logger.warning(
|
|
559
|
+
'Dry-run check failed for %s: %s',
|
|
560
|
+
self._action.description if self._action else '(unknown)',
|
|
561
|
+
result.message or 'unknown error',
|
|
562
|
+
)
|
|
568
563
|
else:
|
|
569
564
|
label = 'Needed'
|
|
570
565
|
self._status_label.setText(label)
|
|
@@ -604,7 +599,8 @@ class ActionCard(QFrame):
|
|
|
604
599
|
def set_executing(self) -> None:
|
|
605
600
|
"""Transition the card into the *executing* state.
|
|
606
601
|
|
|
607
|
-
|
|
602
|
+
Updates the status badge. Execution output is routed to the
|
|
603
|
+
unified :class:`~synodic_client.application.screen.log_panel.ExecutionLogPanel`.
|
|
608
604
|
"""
|
|
609
605
|
if self._is_skeleton:
|
|
610
606
|
return
|
|
@@ -612,37 +608,13 @@ class ActionCard(QFrame):
|
|
|
612
608
|
self.setStyleSheet(ACTION_CARD_EXECUTING_STYLE)
|
|
613
609
|
self._status_label.setText('Running\u2026')
|
|
614
610
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_RUNNING)
|
|
615
|
-
self._log_expanded = True
|
|
616
|
-
self._log_output.setVisible(True)
|
|
617
|
-
|
|
618
|
-
def append_output(self, text: str, stream: str | None = None) -> None:
|
|
619
|
-
"""Append a line of output to the inline log.
|
|
620
|
-
|
|
621
|
-
Args:
|
|
622
|
-
text: The output line.
|
|
623
|
-
stream: ``'stdout'``, ``'stderr'``, or ``None`` for phase messages.
|
|
624
|
-
"""
|
|
625
|
-
if self._is_skeleton or not hasattr(self, '_log_output'):
|
|
626
|
-
return
|
|
627
|
-
|
|
628
|
-
colour = LOG_COLOR_STDOUT
|
|
629
|
-
if stream == 'stderr':
|
|
630
|
-
colour = LOG_COLOR_STDERR
|
|
631
|
-
elif stream is None:
|
|
632
|
-
colour = LOG_COLOR_PHASE
|
|
633
|
-
|
|
634
|
-
escaped = html_mod.escape(text)
|
|
635
|
-
self._log_output.append(f'<span style="color: {colour};">{escaped}</span>')
|
|
636
|
-
|
|
637
|
-
cursor = self._log_output.textCursor()
|
|
638
|
-
cursor.movePosition(QTextCursor.MoveOperation.End)
|
|
639
|
-
self._log_output.setTextCursor(cursor)
|
|
640
611
|
|
|
641
612
|
def set_result(self, result: SetupActionResult) -> None:
|
|
642
613
|
"""Update the card with the final execution result.
|
|
643
614
|
|
|
644
|
-
The card returns to the default border style.
|
|
645
|
-
|
|
615
|
+
The card returns to the default border style. Detailed output
|
|
616
|
+
is displayed in the unified
|
|
617
|
+
:class:`~synodic_client.application.screen.log_panel.ExecutionLogPanel`.
|
|
646
618
|
|
|
647
619
|
Args:
|
|
648
620
|
result: The action execution result.
|
|
@@ -656,14 +628,9 @@ class ActionCard(QFrame):
|
|
|
656
628
|
label = skip_reason_label(result.skip_reason)
|
|
657
629
|
self._status_label.setText(label)
|
|
658
630
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_SKIPPED)
|
|
659
|
-
self.append_output(f'\u23ed Skipped: {label}', None)
|
|
660
631
|
elif result.success:
|
|
661
632
|
self._status_label.setText('Done')
|
|
662
633
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_DONE)
|
|
663
|
-
msg = result.message or 'Completed successfully'
|
|
664
|
-
self._log_output.append(
|
|
665
|
-
f'<span style="color: {LOG_COLOR_SUCCESS};">\u2713 {html_mod.escape(msg)}</span>',
|
|
666
|
-
)
|
|
667
634
|
# Update version if an upgrade completed
|
|
668
635
|
new_version = result.available_version or self._check_available_version
|
|
669
636
|
if new_version:
|
|
@@ -672,10 +639,6 @@ class ActionCard(QFrame):
|
|
|
672
639
|
else:
|
|
673
640
|
self._status_label.setText('Failed')
|
|
674
641
|
self._status_label.setStyleSheet(ACTION_CARD_STATUS_FAILED)
|
|
675
|
-
msg = result.message or 'Unknown error'
|
|
676
|
-
self._log_output.append(
|
|
677
|
-
f'<span style="color: {LOG_COLOR_ERROR};">\u2717 {html_mod.escape(msg)}</span>',
|
|
678
|
-
)
|
|
679
642
|
|
|
680
643
|
# ------------------------------------------------------------------
|
|
681
644
|
# Public API — status text accessors (for counting)
|
|
@@ -695,15 +658,12 @@ class ActionCard(QFrame):
|
|
|
695
658
|
|
|
696
659
|
|
|
697
660
|
# ---------------------------------------------------------------------------
|
|
698
|
-
# ActionCardList —
|
|
661
|
+
# ActionCardList — card container
|
|
699
662
|
# ---------------------------------------------------------------------------
|
|
700
663
|
|
|
701
664
|
|
|
702
|
-
class ActionCardList(
|
|
703
|
-
"""
|
|
704
|
-
|
|
705
|
-
Replaces both the ``QTableWidget`` and the ``ExecutionLogPanel`` from
|
|
706
|
-
the previous install screen layout. One scrollbar, no nesting.
|
|
665
|
+
class ActionCardList(QWidget):
|
|
666
|
+
"""Container of :class:`ActionCard` widgets.
|
|
707
667
|
|
|
708
668
|
Cards are keyed by :func:`action_key` (content-based) so that
|
|
709
669
|
look-ups work across different ``execute_stream`` runs where the
|
|
@@ -716,18 +676,12 @@ class ActionCardList(QScrollArea):
|
|
|
716
676
|
def __init__(self, parent: QWidget | None = None) -> None:
|
|
717
677
|
"""Initialise the card list."""
|
|
718
678
|
super().__init__(parent)
|
|
719
|
-
self.setWidgetResizable(True)
|
|
720
|
-
self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
|
721
|
-
self.setFrameShape(QFrame.Shape.NoFrame)
|
|
722
679
|
|
|
723
|
-
self.
|
|
724
|
-
self._layout = QVBoxLayout(self._container)
|
|
680
|
+
self._layout = QVBoxLayout(self)
|
|
725
681
|
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
726
682
|
self._layout.setSpacing(ACTION_CARD_SPACING)
|
|
727
683
|
self._layout.addStretch()
|
|
728
684
|
|
|
729
|
-
self.setWidget(self._container)
|
|
730
|
-
|
|
731
685
|
self._cards: list[ActionCard] = []
|
|
732
686
|
self._action_map: dict[tuple[object, ...], ActionCard] = {}
|
|
733
687
|
|
|
@@ -745,7 +699,7 @@ class ActionCardList(QScrollArea):
|
|
|
745
699
|
"""
|
|
746
700
|
self.clear()
|
|
747
701
|
for _ in range(count):
|
|
748
|
-
card = ActionCard(self
|
|
702
|
+
card = ActionCard(self, skeleton=True)
|
|
749
703
|
self._layout.insertWidget(self._layout.count() - 1, card)
|
|
750
704
|
self._cards.append(card)
|
|
751
705
|
|
|
@@ -768,12 +722,9 @@ class ActionCardList(QScrollArea):
|
|
|
768
722
|
prerelease_overrides: Package names with user pre-release overrides.
|
|
769
723
|
"""
|
|
770
724
|
self.clear()
|
|
771
|
-
sorted_actions = sorted(
|
|
772
|
-
(a for a in actions if a.kind is not None),
|
|
773
|
-
key=action_sort_key,
|
|
774
|
-
)
|
|
725
|
+
sorted_actions = sorted(actions, key=action_sort_key)
|
|
775
726
|
for act in sorted_actions:
|
|
776
|
-
card = ActionCard(self
|
|
727
|
+
card = ActionCard(self)
|
|
777
728
|
card.populate(
|
|
778
729
|
act,
|
|
779
730
|
plugin_installed=plugin_installed,
|
|
@@ -829,21 +780,3 @@ class ActionCardList(QScrollArea):
|
|
|
829
780
|
card.deleteLater()
|
|
830
781
|
self._cards.clear()
|
|
831
782
|
self._action_map.clear()
|
|
832
|
-
|
|
833
|
-
# ------------------------------------------------------------------
|
|
834
|
-
# Scroll helpers
|
|
835
|
-
# ------------------------------------------------------------------
|
|
836
|
-
|
|
837
|
-
def scroll_to_card(self, card: ActionCard) -> None:
|
|
838
|
-
"""Ensure *card* is visible in the scroll area."""
|
|
839
|
-
self.ensureWidgetVisible(card)
|
|
840
|
-
|
|
841
|
-
def scroll_to_card_bottom(self, card: ActionCard) -> None:
|
|
842
|
-
"""Scroll so the bottom of *card* is visible.
|
|
843
|
-
|
|
844
|
-
Used during execution to follow the growing inline log output.
|
|
845
|
-
Unlike :meth:`scroll_to_card` (which may show the top of a tall
|
|
846
|
-
card), this always brings the bottom edge into view.
|
|
847
|
-
"""
|
|
848
|
-
bottom_y = card.geometry().bottom()
|
|
849
|
-
self.ensureVisible(0, bottom_y, 0, 50)
|
{synodic_client-0.0.1.dev36 → synodic_client-0.0.1.dev38}/synodic_client/application/screen/card.py
RENAMED
|
@@ -64,7 +64,7 @@ class ClickableHeader(QWidget):
|
|
|
64
64
|
|
|
65
65
|
# --- Event handling ---------------------------------------------------
|
|
66
66
|
|
|
67
|
-
def mousePressEvent(self, _event: object) -> None:
|
|
67
|
+
def mousePressEvent(self, _event: object) -> None:
|
|
68
68
|
"""Emit :attr:`clicked` on any mouse press."""
|
|
69
69
|
self.clicked.emit()
|
|
70
70
|
|