synodic-client 0.0.1.dev25__tar.gz → 0.0.1.dev26__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.dev25 → synodic_client-0.0.1.dev26}/PKG-INFO +4 -3
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/pyproject.toml +5 -4
- synodic_client-0.0.1.dev26/synodic_client/_version.py +1 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/icon.py +0 -2
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/qt.py +9 -5
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/__init__.py +2 -1
- synodic_client-0.0.1.dev26/synodic_client/application/screen/card.py +148 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/install.py +254 -97
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/log_panel.py +5 -15
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/screen.py +228 -139
- synodic_client-0.0.1.dev26/synodic_client/application/screen/spinner.py +94 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/tray.py +11 -6
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/theme.py +14 -2
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/updater.py +1 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/qt/test_install_preview.py +84 -47
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/qt/test_log_panel.py +1 -1
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_updater.py +0 -103
- synodic_client-0.0.1.dev25/synodic_client/_version.py +0 -1
- synodic_client-0.0.1.dev25/tests/unit/test_install_preview.py +0 -491
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/README.md +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/__init__.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/bootstrap.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/application/uri.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/client.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/config.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/protocol.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/synodic_client/resolution.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_client_updater.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_resolution.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev25 → synodic_client-0.0.1.dev26}/tests/unit/windows/test_protocol.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.dev26
|
|
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,10 @@ 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.dev31
|
|
12
|
+
Requires-Dist: qasync>=0.28.0
|
|
12
13
|
Requires-Dist: velopack>=0.0.1369.dev7516
|
|
13
|
-
Requires-Dist: typer>=0.
|
|
14
|
+
Requires-Dist: typer>=0.24.0
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
15
16
|
|
|
16
17
|
# Synodic Client
|
|
@@ -10,11 +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.dev31",
|
|
14
|
+
"qasync>=0.28.0",
|
|
14
15
|
"velopack>=0.0.1369.dev7516",
|
|
15
|
-
"typer>=0.
|
|
16
|
+
"typer>=0.24.0",
|
|
16
17
|
]
|
|
17
|
-
version = "0.0.1.
|
|
18
|
+
version = "0.0.1.dev26"
|
|
18
19
|
|
|
19
20
|
[project.license]
|
|
20
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -35,7 +36,7 @@ build = [
|
|
|
35
36
|
]
|
|
36
37
|
lint = [
|
|
37
38
|
"ruff>=0.15.1",
|
|
38
|
-
"pyrefly>=0.
|
|
39
|
+
"pyrefly>=0.53.0",
|
|
39
40
|
]
|
|
40
41
|
test = [
|
|
41
42
|
"pytest>=9.0.2",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.0.1.dev26'
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""GUI entry point for the Synodic Client application."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import ctypes
|
|
4
5
|
import logging
|
|
5
6
|
import signal
|
|
@@ -7,6 +8,7 @@ import sys
|
|
|
7
8
|
import types
|
|
8
9
|
from collections.abc import Callable
|
|
9
10
|
|
|
11
|
+
import qasync
|
|
10
12
|
from porringer.api import API
|
|
11
13
|
from porringer.schema import LocalConfiguration
|
|
12
14
|
from PySide6.QtCore import Qt, QTimer
|
|
@@ -48,8 +50,6 @@ def _init_services(logger: logging.Logger) -> tuple[Client, API, GlobalConfigura
|
|
|
48
50
|
update_config.repo_url,
|
|
49
51
|
)
|
|
50
52
|
|
|
51
|
-
porringer.plugin.list()
|
|
52
|
-
|
|
53
53
|
return client, porringer, config
|
|
54
54
|
|
|
55
55
|
|
|
@@ -142,6 +142,9 @@ def application(*, uri: str | None = None, dev_mode: bool = False) -> None:
|
|
|
142
142
|
|
|
143
143
|
app = _init_app()
|
|
144
144
|
|
|
145
|
+
loop = qasync.QEventLoop(app)
|
|
146
|
+
asyncio.set_event_loop(loop)
|
|
147
|
+
|
|
145
148
|
instance = SingleInstance(app)
|
|
146
149
|
if instance.try_send_to_existing(uri or ''):
|
|
147
150
|
logger.info('Another instance is already running, exiting')
|
|
@@ -168,9 +171,10 @@ def application(*, uri: str | None = None, dev_mode: bool = False) -> None:
|
|
|
168
171
|
if uri:
|
|
169
172
|
_process_uri(uri, _handle_install_uri)
|
|
170
173
|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
+
# qasync integrates the asyncio event loop with Qt's event loop,
|
|
175
|
+
# enabling async/await usage in the GUI layer without dedicated threads.
|
|
176
|
+
with loop:
|
|
177
|
+
loop.run_forever()
|
|
174
178
|
|
|
175
179
|
|
|
176
180
|
_PROTOCOL_SCHEME = 'synodic'
|
|
@@ -6,7 +6,8 @@ execution log panel live here to avoid circular imports.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from porringer.schema import
|
|
9
|
+
from porringer.schema import SkipReason
|
|
10
|
+
from porringer.schema.plugin import PluginKind
|
|
10
11
|
|
|
11
12
|
ACTION_KIND_LABELS: dict[PluginKind | None, str] = {
|
|
12
13
|
PluginKind.PACKAGE: 'Package',
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Reusable card frame and clickable header widgets.
|
|
2
|
+
|
|
3
|
+
:class:`CardFrame` provides a styled, optionally collapsible container
|
|
4
|
+
for grouping related UI elements. :class:`ClickableHeader` extracts the
|
|
5
|
+
ad-hoc clickable-header pattern used in log panels and plugin sections
|
|
6
|
+
into a proper widget with a ``clicked`` signal.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import Qt, Signal
|
|
12
|
+
from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QVBoxLayout, QWidget
|
|
13
|
+
|
|
14
|
+
from synodic_client.application.theme import (
|
|
15
|
+
CARD_FRAME_STYLE,
|
|
16
|
+
CARD_HEADER_STYLE,
|
|
17
|
+
LOG_CHEVRON_STYLE,
|
|
18
|
+
NO_MARGINS,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Unicode chevrons
|
|
22
|
+
CHEVRON_DOWN = '\u25bc'
|
|
23
|
+
CHEVRON_RIGHT = '\u25b6'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ClickableHeader(QWidget):
|
|
27
|
+
"""A clickable header widget with an optional chevron for collapse/expand.
|
|
28
|
+
|
|
29
|
+
Emits :attr:`clicked` on mouse press and sets a pointing-hand cursor.
|
|
30
|
+
Callers supply *object_name* and *stylesheet* to control appearance.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
clicked = Signal()
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
object_name: str,
|
|
38
|
+
stylesheet: str,
|
|
39
|
+
*,
|
|
40
|
+
parent: QWidget | None = None,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialise the header.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
object_name: ``QObject`` name (for CSS selectors).
|
|
46
|
+
stylesheet: Stylesheet applied to this widget.
|
|
47
|
+
parent: Optional parent widget.
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(parent)
|
|
50
|
+
self.setObjectName(object_name)
|
|
51
|
+
self.setStyleSheet(stylesheet)
|
|
52
|
+
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
53
|
+
|
|
54
|
+
self._layout = QHBoxLayout(self)
|
|
55
|
+
self._layout.setContentsMargins(0, 0, 0, 0)
|
|
56
|
+
self._layout.setSpacing(6)
|
|
57
|
+
|
|
58
|
+
# --- Layout access ---------------------------------------------------
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def header_layout(self) -> QHBoxLayout:
|
|
62
|
+
"""Return the header's horizontal layout for adding child widgets."""
|
|
63
|
+
return self._layout
|
|
64
|
+
|
|
65
|
+
# --- Event handling ---------------------------------------------------
|
|
66
|
+
|
|
67
|
+
def mousePressEvent(self, _event: object) -> None: # noqa: N802
|
|
68
|
+
"""Emit :attr:`clicked` on any mouse press."""
|
|
69
|
+
self.clicked.emit()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class CardFrame(QFrame):
|
|
73
|
+
"""A rounded-border card container with an optional title and collapse.
|
|
74
|
+
|
|
75
|
+
When *collapsible* is ``True`` the title becomes a
|
|
76
|
+
:class:`ClickableHeader` and clicking it toggles the content area.
|
|
77
|
+
Use :meth:`content_layout` to add child widgets.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
title: str = '',
|
|
83
|
+
*,
|
|
84
|
+
collapsible: bool = False,
|
|
85
|
+
parent: QWidget | None = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialise the card.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
title: Optional heading text shown at the top of the card.
|
|
91
|
+
collapsible: When ``True``, the title row toggles content
|
|
92
|
+
visibility on click.
|
|
93
|
+
parent: Optional parent widget.
|
|
94
|
+
"""
|
|
95
|
+
super().__init__(parent)
|
|
96
|
+
self.setObjectName('card')
|
|
97
|
+
self.setStyleSheet(CARD_FRAME_STYLE)
|
|
98
|
+
self._expanded = True
|
|
99
|
+
self._collapsible = collapsible
|
|
100
|
+
|
|
101
|
+
outer = QVBoxLayout(self)
|
|
102
|
+
outer.setContentsMargins(*NO_MARGINS)
|
|
103
|
+
outer.setSpacing(4)
|
|
104
|
+
|
|
105
|
+
# --- Optional title / header -----------------------------------------
|
|
106
|
+
self._chevron: QLabel | None = None
|
|
107
|
+
if title:
|
|
108
|
+
if collapsible:
|
|
109
|
+
header = ClickableHeader('cardHeader', '', parent=self)
|
|
110
|
+
header.clicked.connect(self._toggle)
|
|
111
|
+
|
|
112
|
+
self._chevron = QLabel(CHEVRON_DOWN)
|
|
113
|
+
self._chevron.setStyleSheet(LOG_CHEVRON_STYLE)
|
|
114
|
+
self._chevron.setFixedWidth(14)
|
|
115
|
+
header.header_layout.addWidget(self._chevron)
|
|
116
|
+
|
|
117
|
+
label = QLabel(title)
|
|
118
|
+
label.setStyleSheet(CARD_HEADER_STYLE)
|
|
119
|
+
header.header_layout.addWidget(label)
|
|
120
|
+
header.header_layout.addStretch()
|
|
121
|
+
outer.addWidget(header)
|
|
122
|
+
else:
|
|
123
|
+
label = QLabel(title)
|
|
124
|
+
label.setStyleSheet(CARD_HEADER_STYLE)
|
|
125
|
+
outer.addWidget(label)
|
|
126
|
+
|
|
127
|
+
# --- Content area -----------------------------------------------------
|
|
128
|
+
self._content = QWidget()
|
|
129
|
+
self._content_layout = QVBoxLayout(self._content)
|
|
130
|
+
self._content_layout.setContentsMargins(*NO_MARGINS)
|
|
131
|
+
self._content_layout.setSpacing(4)
|
|
132
|
+
outer.addWidget(self._content)
|
|
133
|
+
|
|
134
|
+
# --- Public API -----------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def content_layout(self) -> QVBoxLayout:
|
|
138
|
+
"""Return the inner layout for adding child widgets to the card."""
|
|
139
|
+
return self._content_layout
|
|
140
|
+
|
|
141
|
+
# --- Collapse / expand ----------------------------------------------------
|
|
142
|
+
|
|
143
|
+
def _toggle(self) -> None:
|
|
144
|
+
"""Toggle the content area visibility."""
|
|
145
|
+
self._expanded = not self._expanded
|
|
146
|
+
self._content.setVisible(self._expanded)
|
|
147
|
+
if self._chevron is not None:
|
|
148
|
+
self._chevron.setText(CHEVRON_DOWN if self._expanded else CHEVRON_RIGHT)
|