synodic-client 0.0.1.dev48__tar.gz → 0.0.1.dev50__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.dev48 → synodic_client-0.0.1.dev50}/PKG-INFO +2 -2
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/pyproject.toml +2 -2
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/__init__.py +2 -2
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/bootstrap.py +2 -4
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/data.py +2 -28
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/init.py +7 -4
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/qt.py +2 -4
- synodic_client-0.0.1.dev50/synodic_client/application/schema.py +53 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/__init__.py +38 -2
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/action_card.py +10 -64
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/install.py +12 -435
- synodic_client-0.0.1.dev50/synodic_client/application/screen/install_workers.py +258 -0
- synodic_client-0.0.1.dev50/synodic_client/application/screen/plugin_row.py +595 -0
- synodic_client-0.0.1.dev50/synodic_client/application/screen/projects.py +268 -0
- synodic_client-0.0.1.dev50/synodic_client/application/screen/schema.py +363 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/screen.py +85 -987
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/settings.py +15 -3
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/sidebar.py +1 -1
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/spinner.py +45 -17
- synodic_client-0.0.1.dev48/synodic_client/application/screen/tray.py → synodic_client-0.0.1.dev50/synodic_client/application/screen/tool_update_controller.py +66 -124
- synodic_client-0.0.1.dev50/synodic_client/application/screen/tray.py +137 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/update_banner.py +1 -25
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/theme.py +34 -1
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/update_controller.py +15 -5
- synodic_client-0.0.1.dev50/synodic_client/application/uri.py +80 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/workers.py +2 -14
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/client.py +6 -4
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/config.py +1 -86
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/protocol.py +13 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/resolution.py +6 -59
- synodic_client-0.0.1.dev50/synodic_client/schema.py +235 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/updater.py +34 -117
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_gather_packages.py +5 -5
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_install_preview.py +4 -10
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_log_panel.py +2 -1
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_preview_model.py +2 -6
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_settings.py +3 -1
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_sidebar.py +1 -1
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_tray_window_show.py +6 -6
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_update_banner.py +2 -1
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_update_controller.py +6 -4
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_update_feedback.py +7 -5
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_client_updater.py +10 -2
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_init.py +1 -1
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_resolution.py +5 -51
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_updater.py +106 -17
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_workers.py +1 -1
- synodic_client-0.0.1.dev48/synodic_client/application/uri.py +0 -24
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/LICENSE.md +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/README.md +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/__main__.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/__init__.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/icon.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/instance.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/card.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/screen/log_panel.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/cli.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/logging.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/py.typed +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/startup.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/__init__.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/conftest.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/__init__.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/__init__.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/conftest.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_action_card.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/qt/test_logging.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_cli.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_client_version.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_config.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_examples.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_install.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/test_uri.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/windows/__init__.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/windows/conftest.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/tests/unit/windows/test_protocol.py +0 -0
- {synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/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.dev50
|
|
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.dev74
|
|
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.dev74",
|
|
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.dev50"
|
|
19
19
|
|
|
20
20
|
[project.license]
|
|
21
21
|
text = "LGPL-3.0-or-later"
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
|
|
5
5
|
from synodic_client.client import Client
|
|
6
|
-
from synodic_client.
|
|
6
|
+
from synodic_client.schema import (
|
|
7
7
|
UpdateChannel,
|
|
8
8
|
UpdateConfig,
|
|
9
9
|
UpdateInfo,
|
|
10
|
-
Updater,
|
|
11
10
|
UpdateState,
|
|
12
11
|
)
|
|
12
|
+
from synodic_client.updater import Updater
|
|
13
13
|
|
|
14
14
|
try:
|
|
15
15
|
__version__ = importlib.metadata.version('synodic_client')
|
{synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/bootstrap.py
RENAMED
|
@@ -17,10 +17,9 @@ import sys
|
|
|
17
17
|
|
|
18
18
|
from synodic_client.config import set_dev_mode
|
|
19
19
|
from synodic_client.logging import configure_logging
|
|
20
|
+
from synodic_client.protocol import extract_uri_from_args
|
|
20
21
|
from synodic_client.updater import initialize_velopack
|
|
21
22
|
|
|
22
|
-
_PROTOCOL_SCHEME = 'synodic'
|
|
23
|
-
|
|
24
23
|
# Parse --dev flag early so logging uses the right filename.
|
|
25
24
|
_dev_mode = '--dev' in sys.argv[1:]
|
|
26
25
|
set_dev_mode(_dev_mode)
|
|
@@ -36,5 +35,4 @@ if not _dev_mode:
|
|
|
36
35
|
# Heavy imports happen here — PySide6, porringer, etc.
|
|
37
36
|
from synodic_client.application.qt import application
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
application(uri=_uri, dev_mode=_dev_mode)
|
|
38
|
+
application(uri=extract_uri_from_args(), dev_mode=_dev_mode)
|
{synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/data.py
RENAMED
|
@@ -14,7 +14,6 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import asyncio
|
|
16
16
|
import logging
|
|
17
|
-
from dataclasses import dataclass, field
|
|
18
17
|
|
|
19
18
|
from porringer.api import API
|
|
20
19
|
from porringer.backend.command.core.discovery import DiscoveredPlugins
|
|
@@ -22,36 +21,11 @@ from porringer.core.plugin_schema.plugin_manager import PluginManager
|
|
|
22
21
|
from porringer.schema import (
|
|
23
22
|
CheckParameters,
|
|
24
23
|
CheckResult,
|
|
25
|
-
DirectoryValidationResult,
|
|
26
|
-
ManifestDirectory,
|
|
27
|
-
PluginInfo,
|
|
28
24
|
)
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
@dataclass(slots=True)
|
|
34
|
-
class Snapshot:
|
|
35
|
-
"""Immutable bundle of data produced by a single refresh cycle.
|
|
36
|
-
|
|
37
|
-
All fields are populated by :meth:`DataCoordinator.refresh` and
|
|
38
|
-
remain stable until the next refresh.
|
|
39
|
-
"""
|
|
26
|
+
from synodic_client.application.schema import Snapshot
|
|
40
27
|
|
|
41
|
-
|
|
42
|
-
"""All discovered plugins with install status and version info."""
|
|
43
|
-
|
|
44
|
-
directories: list[ManifestDirectory] = field(default_factory=list)
|
|
45
|
-
"""Cached project directories (un-validated)."""
|
|
46
|
-
|
|
47
|
-
validated_directories: list[DirectoryValidationResult] = field(default_factory=list)
|
|
48
|
-
"""Cached directories with ``exists`` / ``has_manifest`` validation."""
|
|
49
|
-
|
|
50
|
-
discovered: DiscoveredPlugins | None = None
|
|
51
|
-
"""Full plugin discovery result including runtime context."""
|
|
52
|
-
|
|
53
|
-
plugin_managers: dict[str, PluginManager] = field(default_factory=dict)
|
|
54
|
-
"""Project-environment plugins implementing the ``PluginManager`` protocol."""
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
55
29
|
|
|
56
30
|
|
|
57
31
|
class DataCoordinator:
|
{synodic_client-0.0.1.dev48 → synodic_client-0.0.1.dev50}/synodic_client/application/init.py
RENAMED
|
@@ -23,7 +23,11 @@ from synodic_client.startup import register_startup, remove_startup
|
|
|
23
23
|
|
|
24
24
|
logger = logging.getLogger(__name__)
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
class _PreambleState:
|
|
28
|
+
"""Module-level mutable state (avoids ``global`` statements)."""
|
|
29
|
+
|
|
30
|
+
done: bool = False
|
|
27
31
|
|
|
28
32
|
|
|
29
33
|
def run_startup_preamble(exe_path: str | None = None) -> None:
|
|
@@ -39,10 +43,9 @@ def run_startup_preamble(exe_path: str | None = None) -> None:
|
|
|
39
43
|
exe_path: Absolute path to the application executable. Defaults
|
|
40
44
|
to ``sys.executable`` when not supplied.
|
|
41
45
|
"""
|
|
42
|
-
|
|
43
|
-
if _preamble_done:
|
|
46
|
+
if _PreambleState.done:
|
|
44
47
|
return
|
|
45
|
-
|
|
48
|
+
_PreambleState.done = True
|
|
46
49
|
|
|
47
50
|
if exe_path is None:
|
|
48
51
|
exe_path = sys.executable
|
|
@@ -24,6 +24,7 @@ from synodic_client.application.uri import parse_uri
|
|
|
24
24
|
from synodic_client.client import Client
|
|
25
25
|
from synodic_client.config import set_dev_mode
|
|
26
26
|
from synodic_client.logging import configure_logging
|
|
27
|
+
from synodic_client.protocol import extract_uri_from_args
|
|
27
28
|
from synodic_client.resolution import (
|
|
28
29
|
ResolvedConfig,
|
|
29
30
|
resolve_config,
|
|
@@ -187,8 +188,5 @@ def application(*, uri: str | None = None, dev_mode: bool = False) -> None:
|
|
|
187
188
|
loop.run_forever()
|
|
188
189
|
|
|
189
190
|
|
|
190
|
-
_PROTOCOL_SCHEME = 'synodic'
|
|
191
|
-
|
|
192
191
|
if __name__ == '__main__':
|
|
193
|
-
|
|
194
|
-
application(uri=_uri)
|
|
192
|
+
application(uri=extract_uri_from_args())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Application-layer data models.
|
|
2
|
+
|
|
3
|
+
Contains data structures shared across application modules — the
|
|
4
|
+
``DataCoordinator`` snapshot and the tool-update result summary.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from porringer.backend.command.core.discovery import DiscoveredPlugins
|
|
12
|
+
from porringer.core.plugin_schema.plugin_manager import PluginManager
|
|
13
|
+
from porringer.schema import (
|
|
14
|
+
DirectoryValidationResult,
|
|
15
|
+
ManifestDirectory,
|
|
16
|
+
PluginInfo,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class Snapshot:
|
|
22
|
+
"""Immutable bundle of data produced by a single refresh cycle.
|
|
23
|
+
|
|
24
|
+
All fields are populated by :meth:`DataCoordinator.refresh` and
|
|
25
|
+
remain stable until the next refresh.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
plugins: list[PluginInfo] = field(default_factory=list)
|
|
29
|
+
"""All discovered plugins with install status and version info."""
|
|
30
|
+
|
|
31
|
+
directories: list[ManifestDirectory] = field(default_factory=list)
|
|
32
|
+
"""Cached project directories (un-validated)."""
|
|
33
|
+
|
|
34
|
+
validated_directories: list[DirectoryValidationResult] = field(default_factory=list)
|
|
35
|
+
"""Cached directories with ``exists`` / ``has_manifest`` validation."""
|
|
36
|
+
|
|
37
|
+
discovered: DiscoveredPlugins | None = None
|
|
38
|
+
"""Full plugin discovery result including runtime context."""
|
|
39
|
+
|
|
40
|
+
plugin_managers: dict[str, PluginManager] = field(default_factory=dict)
|
|
41
|
+
"""Project-environment plugins implementing the ``PluginManager`` protocol."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(slots=True)
|
|
45
|
+
class ToolUpdateResult:
|
|
46
|
+
"""Summary of a tool-update run across cached manifests."""
|
|
47
|
+
|
|
48
|
+
manifests_processed: int = 0
|
|
49
|
+
updated: int = 0
|
|
50
|
+
already_latest: int = 0
|
|
51
|
+
failed: int = 0
|
|
52
|
+
updated_packages: set[str] = field(default_factory=set)
|
|
53
|
+
"""Package names that were successfully upgraded."""
|
|
@@ -6,9 +6,15 @@ execution log panel live here to avoid circular imports.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
|
|
9
11
|
from porringer.schema import SetupAction, SkipReason
|
|
10
12
|
from porringer.schema.plugin import PluginKind
|
|
11
13
|
|
|
14
|
+
_SECONDS_PER_MINUTE = 60
|
|
15
|
+
_MINUTES_PER_HOUR = 60
|
|
16
|
+
_HOURS_PER_DAY = 24
|
|
17
|
+
|
|
12
18
|
ACTION_KIND_LABELS: dict[PluginKind | None, str] = {
|
|
13
19
|
PluginKind.PACKAGE: 'Package',
|
|
14
20
|
PluginKind.TOOL: 'Tool',
|
|
@@ -57,15 +63,45 @@ def skip_reason_label(reason: SkipReason | None) -> str:
|
|
|
57
63
|
return SKIP_REASON_LABELS.get(reason, reason.name.replace('_', ' ').capitalize())
|
|
58
64
|
|
|
59
65
|
|
|
60
|
-
def format_cli_command(action: SetupAction) -> str:
|
|
66
|
+
def format_cli_command(action: SetupAction, *, suppress_description: bool = False) -> str:
|
|
61
67
|
"""Return a human-readable CLI command string for *action*.
|
|
62
68
|
|
|
63
69
|
Prefers ``cli_command``, falls back to ``command``, then synthesises
|
|
64
70
|
an ``installer install <package>`` string for package actions, and
|
|
65
71
|
finally returns the action description as a last resort.
|
|
72
|
+
|
|
73
|
+
When *suppress_description* is ``True`` the final description
|
|
74
|
+
fallback returns an empty string instead.
|
|
66
75
|
"""
|
|
67
76
|
if parts := (action.cli_command or action.command):
|
|
68
77
|
return ' '.join(parts)
|
|
69
78
|
if action.kind == PluginKind.PACKAGE and action.package:
|
|
70
79
|
return f'{action.installer or "pip"} install {action.package}'
|
|
71
|
-
return action.description
|
|
80
|
+
return '' if suppress_description else action.description
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _format_relative_time(iso_timestamp: str) -> str:
|
|
84
|
+
"""Format an ISO 8601 timestamp as a human-readable relative time.
|
|
85
|
+
|
|
86
|
+
Returns strings like ``'just now'``, ``'5m ago'``, ``'2h ago'``,
|
|
87
|
+
``'3d ago'``. Returns an empty string if the timestamp cannot be
|
|
88
|
+
parsed.
|
|
89
|
+
"""
|
|
90
|
+
try:
|
|
91
|
+
dt = datetime.fromisoformat(iso_timestamp)
|
|
92
|
+
if dt.tzinfo is None:
|
|
93
|
+
dt = dt.replace(tzinfo=UTC)
|
|
94
|
+
delta = datetime.now(UTC) - dt
|
|
95
|
+
seconds = max(int(delta.total_seconds()), 0)
|
|
96
|
+
if seconds < _SECONDS_PER_MINUTE:
|
|
97
|
+
return 'just now'
|
|
98
|
+
minutes = seconds // _SECONDS_PER_MINUTE
|
|
99
|
+
if minutes < _MINUTES_PER_HOUR:
|
|
100
|
+
return f'{minutes}m ago'
|
|
101
|
+
hours = minutes // _MINUTES_PER_HOUR
|
|
102
|
+
if hours < _HOURS_PER_DAY:
|
|
103
|
+
return f'{hours}h ago'
|
|
104
|
+
days = hours // _HOURS_PER_DAY
|
|
105
|
+
return f'{days}d ago'
|
|
106
|
+
except ValueError, TypeError:
|
|
107
|
+
return ''
|
|
@@ -16,8 +16,8 @@ import logging
|
|
|
16
16
|
from porringer.backend.command.core.action_builder import PHASE_ORDER
|
|
17
17
|
from porringer.schema import SetupAction, SetupActionResult, SkipReason
|
|
18
18
|
from porringer.schema.plugin import PluginKind
|
|
19
|
-
from PySide6.QtCore import
|
|
20
|
-
from PySide6.QtGui import QColor
|
|
19
|
+
from PySide6.QtCore import Qt, QTimer, Signal
|
|
20
|
+
from PySide6.QtGui import QColor
|
|
21
21
|
from PySide6.QtWidgets import (
|
|
22
22
|
QApplication,
|
|
23
23
|
QCheckBox,
|
|
@@ -30,6 +30,7 @@ from PySide6.QtWidgets import (
|
|
|
30
30
|
)
|
|
31
31
|
|
|
32
32
|
from synodic_client.application.screen import ACTION_KIND_LABELS, format_cli_command, skip_reason_label
|
|
33
|
+
from synodic_client.application.screen.spinner import SpinnerCanvas
|
|
33
34
|
from synodic_client.application.theme import (
|
|
34
35
|
ACTION_CARD_COMMAND_STYLE,
|
|
35
36
|
ACTION_CARD_DESC_STYLE,
|
|
@@ -66,9 +67,6 @@ _UPDATE_AVAILABLE_COLOR = QColor('#d7ba7d')
|
|
|
66
67
|
#: Timer interval for per-card inline spinner (ms).
|
|
67
68
|
_SPINNER_INTERVAL = 50
|
|
68
69
|
|
|
69
|
-
#: Arc span for per-card spinner (degrees × 16 for Qt drawArc).
|
|
70
|
-
_SPINNER_ARC = 90
|
|
71
|
-
|
|
72
70
|
|
|
73
71
|
#: Sort priority derived from porringer's execution phase order so the
|
|
74
72
|
#: display order always matches the order actions actually execute.
|
|
@@ -101,62 +99,6 @@ def action_sort_key(action: SetupAction) -> int:
|
|
|
101
99
|
return _KIND_ORDER.get(action.kind, len(PHASE_ORDER))
|
|
102
100
|
|
|
103
101
|
|
|
104
|
-
def _format_command(action: SetupAction) -> str:
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# ---------------------------------------------------------------------------
|
|
116
|
-
# _CardSpinner — tiny per-card inline spinner
|
|
117
|
-
# ---------------------------------------------------------------------------
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
class _CardSpinner(QWidget):
|
|
121
|
-
"""Tiny spinning arc used inside an :class:`ActionCard` while checking.
|
|
122
|
-
|
|
123
|
-
The spinner replaces the status text label during the dry-run check
|
|
124
|
-
phase and is hidden once the result arrives.
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
def __init__(self, parent: QWidget | None = None) -> None:
|
|
128
|
-
super().__init__(parent)
|
|
129
|
-
self._angle = 0
|
|
130
|
-
self.setFixedSize(ACTION_CARD_SPINNER_SIZE, ACTION_CARD_SPINNER_SIZE)
|
|
131
|
-
|
|
132
|
-
def paintEvent(self, _event: object) -> None:
|
|
133
|
-
"""Draw the muted track and animated highlight arc."""
|
|
134
|
-
painter = QPainter(self)
|
|
135
|
-
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
|
|
136
|
-
|
|
137
|
-
m = ACTION_CARD_SPINNER_PEN // 2 + 1
|
|
138
|
-
rect = QRect(m, m, self.width() - 2 * m, self.height() - 2 * m)
|
|
139
|
-
|
|
140
|
-
# Track circle
|
|
141
|
-
track_pen = QPen(self.palette().mid(), ACTION_CARD_SPINNER_PEN)
|
|
142
|
-
track_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
143
|
-
painter.setPen(track_pen)
|
|
144
|
-
painter.drawEllipse(rect)
|
|
145
|
-
|
|
146
|
-
# Highlight arc
|
|
147
|
-
hl_pen = QPen(self.palette().highlight(), ACTION_CARD_SPINNER_PEN)
|
|
148
|
-
hl_pen.setCapStyle(Qt.PenCapStyle.RoundCap)
|
|
149
|
-
painter.setPen(hl_pen)
|
|
150
|
-
painter.drawArc(rect, self._angle * 16, _SPINNER_ARC * 16)
|
|
151
|
-
|
|
152
|
-
painter.end()
|
|
153
|
-
|
|
154
|
-
def tick(self) -> None:
|
|
155
|
-
"""Advance the arc and repaint."""
|
|
156
|
-
self._angle = (self._angle - 10) % 360
|
|
157
|
-
self.update()
|
|
158
|
-
|
|
159
|
-
|
|
160
102
|
# ---------------------------------------------------------------------------
|
|
161
103
|
# ActionCard — a single action row
|
|
162
104
|
# ---------------------------------------------------------------------------
|
|
@@ -297,7 +239,11 @@ class ActionCard(QFrame):
|
|
|
297
239
|
top.addWidget(self._version_label)
|
|
298
240
|
|
|
299
241
|
# Inline spinner (replaces status text while checking)
|
|
300
|
-
self._spinner_canvas =
|
|
242
|
+
self._spinner_canvas = SpinnerCanvas(
|
|
243
|
+
size=ACTION_CARD_SPINNER_SIZE,
|
|
244
|
+
pen_width=ACTION_CARD_SPINNER_PEN,
|
|
245
|
+
parent=self,
|
|
246
|
+
)
|
|
301
247
|
self._spinner_canvas.hide()
|
|
302
248
|
self._spinner_timer = QTimer(self)
|
|
303
249
|
self._spinner_timer.setInterval(_SPINNER_INTERVAL)
|
|
@@ -418,7 +364,7 @@ class ActionCard(QFrame):
|
|
|
418
364
|
self._desc_label.hide()
|
|
419
365
|
|
|
420
366
|
# CLI command (always visible when present)
|
|
421
|
-
cmd_text =
|
|
367
|
+
cmd_text = format_cli_command(action, suppress_description=True)
|
|
422
368
|
if cmd_text:
|
|
423
369
|
self._command_label.setText(cmd_text)
|
|
424
370
|
self._command_row.show()
|
|
@@ -461,7 +407,7 @@ class ActionCard(QFrame):
|
|
|
461
407
|
"""
|
|
462
408
|
if self._is_skeleton:
|
|
463
409
|
return
|
|
464
|
-
cmd_text =
|
|
410
|
+
cmd_text = format_cli_command(action, suppress_description=True)
|
|
465
411
|
if cmd_text:
|
|
466
412
|
self._command_label.setText(cmd_text)
|
|
467
413
|
self._command_row.show()
|