synodic-client 0.0.1.dev24__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.
Files changed (51) hide show
  1. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/PKG-INFO +4 -3
  2. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/pyproject.toml +5 -4
  3. synodic_client-0.0.1.dev26/synodic_client/_version.py +1 -0
  4. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/icon.py +0 -2
  5. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/qt.py +9 -5
  6. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/__init__.py +2 -1
  7. synodic_client-0.0.1.dev26/synodic_client/application/screen/card.py +148 -0
  8. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/install.py +254 -97
  9. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/log_panel.py +5 -15
  10. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/screen.py +228 -139
  11. synodic_client-0.0.1.dev26/synodic_client/application/screen/spinner.py +94 -0
  12. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/screen/tray.py +11 -6
  13. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/theme.py +14 -2
  14. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/updater.py +6 -2
  15. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/qt/test_install_preview.py +84 -47
  16. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/qt/test_log_panel.py +1 -1
  17. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_updater.py +0 -103
  18. synodic_client-0.0.1.dev24/synodic_client/_version.py +0 -1
  19. synodic_client-0.0.1.dev24/tests/unit/test_install_preview.py +0 -491
  20. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/LICENSE.md +0 -0
  21. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/README.md +0 -0
  22. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/__init__.py +0 -0
  23. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/__main__.py +0 -0
  24. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/__init__.py +0 -0
  25. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/bootstrap.py +0 -0
  26. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/instance.py +0 -0
  27. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/application/uri.py +0 -0
  28. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/cli.py +0 -0
  29. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/client.py +0 -0
  30. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/config.py +0 -0
  31. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/logging.py +0 -0
  32. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/protocol.py +0 -0
  33. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/py.typed +0 -0
  34. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/synodic_client/resolution.py +0 -0
  35. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/__init__.py +0 -0
  36. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/conftest.py +0 -0
  37. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/__init__.py +0 -0
  38. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/qt/__init__.py +0 -0
  39. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/qt/conftest.py +0 -0
  40. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/qt/test_logging.py +0 -0
  41. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_cli.py +0 -0
  42. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_client_updater.py +0 -0
  43. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_client_version.py +0 -0
  44. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_config.py +0 -0
  45. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_examples.py +0 -0
  46. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_install.py +0 -0
  47. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_resolution.py +0 -0
  48. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/test_uri.py +0 -0
  49. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/windows/__init__.py +0 -0
  50. {synodic_client-0.0.1.dev24 → synodic_client-0.0.1.dev26}/tests/unit/windows/conftest.py +0 -0
  51. {synodic_client-0.0.1.dev24 → 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.dev24
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.dev20
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.23.1
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.dev20",
13
+ "porringer>=0.2.1.dev31",
14
+ "qasync>=0.28.0",
14
15
  "velopack>=0.0.1369.dev7516",
15
- "typer>=0.23.1",
16
+ "typer>=0.24.0",
16
17
  ]
17
- version = "0.0.1.dev24"
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.52.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'
@@ -4,8 +4,6 @@ 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
- from __future__ import annotations
8
-
9
7
  from PySide6.QtGui import QIcon, QPixmap
10
8
 
11
9
  from synodic_client.client import Client
@@ -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
- # sys.exit ensures proper cleanup and exit code propagation
172
- # Leading underscore indicates references kept alive intentionally until exec() returns
173
- sys.exit(app.exec())
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 PluginKind, SkipReason
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)