susops 3.0.0rc3.dev7__tar.gz → 3.0.0rc3.dev9__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.
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/PKG-INFO +1 -1
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/pyproject.toml +7 -2
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/client.py +18 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/rpc_server.py +3 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/facade.py +59 -13
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tray/base.py +9 -1
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/app.py +10 -3
- susops-3.0.0rc3.dev9/src/susops/tui/app.tcss +63 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/screens/dashboard.py +24 -4
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops.egg-info/PKG-INFO +1 -1
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops.egg-info/SOURCES.txt +1 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/LICENSE +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/README.md +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/setup.cfg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/__init__.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icon.png +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/dark/error.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/dark/running.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/dark/stopped.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/dark/stopped_partially.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/light/error.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/light/running.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/light/stopped.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/light/stopped_partially.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/dark/error.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/dark/running.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/dark/stopped.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/dark/stopped_partially.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/light/error.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/light/running.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/light/stopped.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/light/stopped_partially.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/dark/error.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/dark/running.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/dark/stopped.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/dark/stopped_partially.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/light/error.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/light/running.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/light/stopped.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/light/stopped_partially.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/status/error.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/status/running.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/status/stopped.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/status/stopped_partially.svg +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/__init__.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/browsers.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/config.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/log_style.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/pac.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/ports.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/process.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/rpc_protocol.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/services_daemon.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/share.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/socat.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/ssh.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/ssh_config.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/status.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/core/types.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tray/__init__.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tray/linux.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tray/mac.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/__init__.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/__main__.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/cli.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/screens/__init__.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/screens/connections.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/screens/shares.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/widgets/__init__.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/tui/widgets/connection_card.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/version.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops.egg-info/dependency_links.txt +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops.egg-info/entry_points.txt +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops.egg-info/requires.txt +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops.egg-info/top_level.txt +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_bw_totals.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_cli.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_client.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_config.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_conftest_smoke.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_facade.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_openapi.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_pac.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_packaging.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_process.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_rpc_protocol.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_rpc_server.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_scripts.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_services_daemon.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_share.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_share_aiohttp.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_socat.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_ssh.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_status.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_update_homebrew_sha.py +0 -0
- {susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/tests/test_version.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "susops"
|
|
7
|
-
version = "3.0.0-rc3.
|
|
7
|
+
version = "3.0.0-rc3.dev9"
|
|
8
8
|
description = "SusOps — SSH SOCKS5 proxy manager with PAC server, Textual TUI, and system tray apps"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -51,7 +51,12 @@ susops-tray = "susops.tray:main"
|
|
|
51
51
|
where = ["src"]
|
|
52
52
|
|
|
53
53
|
[tool.setuptools.package-data]
|
|
54
|
-
susops = [
|
|
54
|
+
susops = [
|
|
55
|
+
"assets/*.png",
|
|
56
|
+
"assets/icons/**/*.svg",
|
|
57
|
+
"assets/icons/**/*.png",
|
|
58
|
+
"tui/**/*.tcss",
|
|
59
|
+
]
|
|
55
60
|
|
|
56
61
|
[tool.pytest.ini_options]
|
|
57
62
|
testpaths = ["tests"]
|
|
@@ -69,6 +69,24 @@ def ensure_daemon_running(workspace: Path = _WORKSPACE_DEFAULT) -> int:
|
|
|
69
69
|
port = _read_port(workspace)
|
|
70
70
|
if port:
|
|
71
71
|
return port
|
|
72
|
+
# Daemon claimed its PID file but hasn't published the port yet.
|
|
73
|
+
# Wait for it instead of spawning a competitor that would race the
|
|
74
|
+
# O_EXCL claim and exit rc=2 ("another daemon is already running").
|
|
75
|
+
deadline = time.monotonic() + _DAEMON_SPAWN_TIMEOUT
|
|
76
|
+
while time.monotonic() < deadline:
|
|
77
|
+
if not _is_daemon_alive(workspace):
|
|
78
|
+
break # died mid-startup, fall through to spawn a new one
|
|
79
|
+
port = _read_port(workspace)
|
|
80
|
+
if port:
|
|
81
|
+
return port
|
|
82
|
+
time.sleep(0.1)
|
|
83
|
+
else:
|
|
84
|
+
raise DaemonUnavailableError(
|
|
85
|
+
"An existing susops-services daemon is alive but never "
|
|
86
|
+
"published an RPC port. Try `kill " +
|
|
87
|
+
(_pid_path(workspace).read_text().strip() or "<pid>") +
|
|
88
|
+
"` and start again."
|
|
89
|
+
)
|
|
72
90
|
|
|
73
91
|
proc = subprocess.Popen(
|
|
74
92
|
[sys.executable, "-m", "susops.core.services_daemon",
|
|
@@ -45,6 +45,9 @@ _ALLOWED_METHODS: set[str] = {
|
|
|
45
45
|
"get_pac_url", "get_status_url",
|
|
46
46
|
# Bandwidth
|
|
47
47
|
"get_bandwidth", "get_bandwidth_totals", "get_bandwidth_global",
|
|
48
|
+
"get_bandwidth_history",
|
|
49
|
+
# Frontend coordination
|
|
50
|
+
"sse_client_count",
|
|
48
51
|
# Reconnect introspection
|
|
49
52
|
"reconnect_monitor_info",
|
|
50
53
|
# Process introspection
|
|
@@ -376,6 +376,11 @@ class _BandwidthSampler:
|
|
|
376
376
|
with self._lock:
|
|
377
377
|
return self._totals.get(tag, (0.0, 0.0))
|
|
378
378
|
|
|
379
|
+
def get_history(self, tag: str) -> list[list[float]]:
|
|
380
|
+
"""Return the persisted (rx_bps, tx_bps) samples for *tag* (newest last)."""
|
|
381
|
+
with self._lock:
|
|
382
|
+
return list(self._history.get(tag, []))
|
|
383
|
+
|
|
379
384
|
def reset_totals(self, tag: str | None = None) -> None:
|
|
380
385
|
"""Reset cumulative counters. Pass tag=None to reset all."""
|
|
381
386
|
with self._lock:
|
|
@@ -620,26 +625,54 @@ class SusOpsManager:
|
|
|
620
625
|
print(full, file=sys.stderr)
|
|
621
626
|
|
|
622
627
|
def _notify(self, title: str, body: str) -> None:
|
|
623
|
-
"""Send a desktop notification. Best-effort
|
|
628
|
+
"""Send a desktop notification with the SusOps icon. Best-effort."""
|
|
624
629
|
import platform
|
|
625
|
-
import
|
|
630
|
+
from pathlib import Path
|
|
631
|
+
icon = Path(__file__).parent / "assets" / "icon.png"
|
|
626
632
|
try:
|
|
627
633
|
if platform.system() == "Darwin":
|
|
628
|
-
|
|
629
|
-
["osascript", "-e",
|
|
630
|
-
f'display notification "{body}" with title "{title}"'],
|
|
631
|
-
stdout=subprocess.DEVNULL,
|
|
632
|
-
stderr=subprocess.DEVNULL,
|
|
633
|
-
)
|
|
634
|
+
self._notify_macos(title, body, icon)
|
|
634
635
|
elif platform.system() == "Linux":
|
|
635
|
-
|
|
636
|
-
["notify-send", title, body],
|
|
637
|
-
stdout=subprocess.DEVNULL,
|
|
638
|
-
stderr=subprocess.DEVNULL,
|
|
639
|
-
)
|
|
636
|
+
self._notify_linux(title, body, icon)
|
|
640
637
|
except Exception:
|
|
641
638
|
pass
|
|
642
639
|
|
|
640
|
+
@staticmethod
|
|
641
|
+
def _notify_macos(title: str, body: str, icon: "Path") -> None:
|
|
642
|
+
# NSUserNotification's setContentImage_ puts the SusOps icon on the
|
|
643
|
+
# right side of the banner. The source-app slot (left side) reflects
|
|
644
|
+
# the running process — bundled .app shows SusOps, raw pip install
|
|
645
|
+
# shows Python. osascript can't set either, so it loses both.
|
|
646
|
+
try:
|
|
647
|
+
from Foundation import NSUserNotification, NSUserNotificationCenter # type: ignore
|
|
648
|
+
from AppKit import NSImage # type: ignore
|
|
649
|
+
n = NSUserNotification.alloc().init()
|
|
650
|
+
n.setTitle_(title)
|
|
651
|
+
n.setInformativeText_(body)
|
|
652
|
+
if icon.exists():
|
|
653
|
+
img = NSImage.alloc().initWithContentsOfFile_(str(icon))
|
|
654
|
+
if img is not None:
|
|
655
|
+
n.setContentImage_(img)
|
|
656
|
+
NSUserNotificationCenter.defaultUserNotificationCenter().deliverNotification_(n)
|
|
657
|
+
return
|
|
658
|
+
except Exception:
|
|
659
|
+
pass
|
|
660
|
+
# Fallback for non-PyObjC environments
|
|
661
|
+
import subprocess
|
|
662
|
+
subprocess.Popen(
|
|
663
|
+
["osascript", "-e", f'display notification "{body}" with title "{title}"'],
|
|
664
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
665
|
+
)
|
|
666
|
+
|
|
667
|
+
@staticmethod
|
|
668
|
+
def _notify_linux(title: str, body: str, icon: "Path") -> None:
|
|
669
|
+
import subprocess
|
|
670
|
+
cmd = ["notify-send"]
|
|
671
|
+
if icon.exists():
|
|
672
|
+
cmd += ["-i", str(icon)]
|
|
673
|
+
cmd += [title, body]
|
|
674
|
+
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
675
|
+
|
|
643
676
|
def _emit(self, event: str, data: dict) -> None:
|
|
644
677
|
"""Emit an SSE event and log it when verbose (bandwidth excluded — too noisy)."""
|
|
645
678
|
self._status_server.emit(event, data)
|
|
@@ -2143,6 +2176,19 @@ class SusOpsManager:
|
|
|
2143
2176
|
"""Return cumulative (rx_bytes, tx_bytes) since last start. Resets on stop."""
|
|
2144
2177
|
return self._bw_sampler.get_totals(tag)
|
|
2145
2178
|
|
|
2179
|
+
def get_bandwidth_history(self, tag: str) -> list[list[float]]:
|
|
2180
|
+
"""Return persisted [rx_bps, tx_bps] samples for *tag* (oldest → newest)."""
|
|
2181
|
+
return self._bw_sampler.get_history(tag)
|
|
2182
|
+
|
|
2183
|
+
def sse_client_count(self) -> int:
|
|
2184
|
+
"""Live SSE subscriber count. Used by frontends to decide whether
|
|
2185
|
+
a stop-on-quit should actually stop, or skip because another
|
|
2186
|
+
frontend is still attached to the same daemon."""
|
|
2187
|
+
try:
|
|
2188
|
+
return int(self._status_server.client_count())
|
|
2189
|
+
except Exception:
|
|
2190
|
+
return 0
|
|
2191
|
+
|
|
2146
2192
|
def get_bandwidth_global(self) -> tuple[float, float]:
|
|
2147
2193
|
"""Return (rx_bps, tx_bps) summed across every connection."""
|
|
2148
2194
|
rx_total = 0.0
|
|
@@ -641,7 +641,15 @@ class AbstractTrayApp(ABC):
|
|
|
641
641
|
|
|
642
642
|
def do_quit(self) -> None:
|
|
643
643
|
if self.manager.app_config.stop_on_quit:
|
|
644
|
-
|
|
644
|
+
# Skip the destructive stop when another frontend (e.g. TUI)
|
|
645
|
+
# is still attached, otherwise it ends up with its SSH
|
|
646
|
+
# masters / PAC / reconnect torn out from under it.
|
|
647
|
+
try:
|
|
648
|
+
other_clients = int(self.manager.sse_client_count()) - 1
|
|
649
|
+
except Exception:
|
|
650
|
+
other_clients = 0
|
|
651
|
+
if other_clients <= 0:
|
|
652
|
+
self.manager.stop()
|
|
645
653
|
# else: the daemon is a separate process — it keeps running with
|
|
646
654
|
# PAC server, status SSE, and reconnect monitor independent of the
|
|
647
655
|
# tray's lifetime. No detach calls needed.
|
|
@@ -121,9 +121,16 @@ class SusOpsTuiApp(App):
|
|
|
121
121
|
|
|
122
122
|
def action_quit(self) -> None:
|
|
123
123
|
if self.manager.app_config.stop_on_quit:
|
|
124
|
-
#
|
|
125
|
-
#
|
|
126
|
-
|
|
124
|
+
# Skip the destructive stop when another frontend (e.g. tray)
|
|
125
|
+
# is still attached. Otherwise the still-running frontend
|
|
126
|
+
# finds its SSH masters / PAC / reconnect monitor torn out
|
|
127
|
+
# from under it and looks broken until it's restarted.
|
|
128
|
+
try:
|
|
129
|
+
other_clients = int(self.manager.sse_client_count()) - 1
|
|
130
|
+
except Exception:
|
|
131
|
+
other_clients = 0
|
|
132
|
+
if other_clients <= 0:
|
|
133
|
+
self.manager.stop_quick()
|
|
127
134
|
# No detach calls — the daemon is already a separate process and
|
|
128
135
|
# outlives the TUI.
|
|
129
136
|
self.exit()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/* SusOps TUI — global theme */
|
|
2
|
+
|
|
3
|
+
/* ── Base ── */
|
|
4
|
+
Screen { background: $background; }
|
|
5
|
+
Header { background: $primary-darken-3; }
|
|
6
|
+
Footer { background: $surface-darken-2; }
|
|
7
|
+
.footer-row { dock: bottom; height: 1; }
|
|
8
|
+
.footer-row Footer { dock: none; width: 1fr; height: 1; }
|
|
9
|
+
.footer-version { width: auto; padding: 0 1; background: $surface-darken-2; color: $text-muted; content-align: center middle; }
|
|
10
|
+
.footer-edit-config { width: auto; padding: 0 1; background: $surface-darken-2; color: $accent; content-align: center middle; }
|
|
11
|
+
.footer-logo { width: auto; padding: 0 1; background: $surface-darken-2; content-align: center middle; }
|
|
12
|
+
|
|
13
|
+
/* ── Dashboard ── */
|
|
14
|
+
#conn-list > ListItem { height: 1; padding: 0 1; }
|
|
15
|
+
|
|
16
|
+
/* ── Connection editor ── */
|
|
17
|
+
.tab-actions { height: 3; layout: horizontal; padding: 0 1; background: $surface-darken-1; }
|
|
18
|
+
.tab-actions Button { margin-right: 1; }
|
|
19
|
+
#detail-preview { height: 6; border: round $primary-darken-1; border-title-align: left; padding: 0 1; margin: 0 1 1 1; color: $text-muted; }
|
|
20
|
+
|
|
21
|
+
/* ── Share ── */
|
|
22
|
+
#share-toolbar { height: 3; layout: horizontal; padding: 0 1; background: $surface-darken-1; }
|
|
23
|
+
#share-toolbar Button { margin-right: 1; }
|
|
24
|
+
#share-list-panel { width: 35; background: $surface-darken-1; border-right: solid $primary-darken-2; }
|
|
25
|
+
#share-list { height: 1fr; margin: 1; border: round $primary-darken-1; border-title-align: left; }
|
|
26
|
+
#share-list > ListItem { height: 1; padding: 0 1; }
|
|
27
|
+
#share-detail { width: 1fr; padding: 1 2; height: 1fr; }
|
|
28
|
+
#share-status { height: 1; padding: 0 1; background: $surface-darken-2; color: $text-muted; }
|
|
29
|
+
|
|
30
|
+
/* ── Logs ── */
|
|
31
|
+
#log-toolbar { height: 3; layout: horizontal; padding: 0 1; background: $surface-darken-1; align: left middle; }
|
|
32
|
+
#log-toolbar Label { margin-right: 1; height: 1; }
|
|
33
|
+
#log-toolbar Switch { margin-right: 2; }
|
|
34
|
+
#log-toolbar Button { margin-left: 2; }
|
|
35
|
+
#log-view { height: 1fr; border: round $primary-darken-1; border-title-align: left; margin: 0 1 1 1; }
|
|
36
|
+
|
|
37
|
+
/* ── Config ── */
|
|
38
|
+
#config-toolbar { height: 3; layout: horizontal; padding: 0 1; background: $surface-darken-1; }
|
|
39
|
+
#config-toolbar Button { margin-right: 1; }
|
|
40
|
+
#config-area { height: 1fr; border: round $primary-darken-1; border-title-align: left; margin: 0 1 1 1; }
|
|
41
|
+
|
|
42
|
+
/* ── PAC viewer ── */
|
|
43
|
+
#pac-area { height: 1fr; border: round $primary-darken-1; border-title-align: left; margin: 0 1 1 1; }
|
|
44
|
+
|
|
45
|
+
/* ── Modals ── */
|
|
46
|
+
ModalScreen { align: center middle; background: rgba(0, 0, 0, 0.75); }
|
|
47
|
+
.modal-dialog { width: 76; height: auto; border: round $primary; padding: 1 2; background: $surface; }
|
|
48
|
+
.modal-dialog Label { margin-top: 1; }
|
|
49
|
+
.modal-dialog > Label:first-child { margin-top: 0; }
|
|
50
|
+
.modal-dialog Input { margin-bottom: 1; }
|
|
51
|
+
.modal-dialog Select { margin-bottom: 1; }
|
|
52
|
+
.modal-dialog .modal-error { color: $error; margin-top: 0; }
|
|
53
|
+
.modal-dialog .modal-hint { margin-top: 0; }
|
|
54
|
+
.modal-btn-row { layout: horizontal; height: auto; margin-top: 1; }
|
|
55
|
+
.modal-btn-row Button { margin-right: 1; }
|
|
56
|
+
/* 2-column form rows */
|
|
57
|
+
.modal-form-row { height: auto; margin-top: 1; }
|
|
58
|
+
.modal-proto-row { height: auto; }
|
|
59
|
+
.modal-field { width: 1fr; height: auto; layout: vertical; padding-right: 1; }
|
|
60
|
+
.modal-field:last-child { padding-right: 0; }
|
|
61
|
+
.modal-field Label { margin-top: 0; }
|
|
62
|
+
.modal-field Input { margin-bottom: 0; }
|
|
63
|
+
.modal-field Select { margin-bottom: 0; }
|
|
@@ -290,11 +290,19 @@ class DashboardScreen(Screen):
|
|
|
290
290
|
bw: dict[str, tuple[float, float]] = {}
|
|
291
291
|
bw_totals: dict[str, tuple[float, float]] = {}
|
|
292
292
|
uptimes: dict[str, float | None] = {}
|
|
293
|
+
bw_history_seed: dict[str, list] = {}
|
|
293
294
|
for cs in result.connection_statuses:
|
|
294
295
|
extras[cs.tag] = mgr.get_process_info(cs.tag)
|
|
295
296
|
bw[cs.tag] = mgr.get_bandwidth(cs.tag)
|
|
296
297
|
bw_totals[cs.tag] = mgr.get_bandwidth_totals(cs.tag)
|
|
297
298
|
uptimes[cs.tag] = mgr.get_uptime(cs.tag)
|
|
299
|
+
# Only fetch persisted history for tags we haven't seeded
|
|
300
|
+
# yet — saves an RPC per tick once the chart is rolling.
|
|
301
|
+
if cs.tag not in self._rx_history:
|
|
302
|
+
try:
|
|
303
|
+
bw_history_seed[cs.tag] = mgr.get_bandwidth_history(cs.tag) or []
|
|
304
|
+
except Exception:
|
|
305
|
+
bw_history_seed[cs.tag] = []
|
|
298
306
|
|
|
299
307
|
shares = mgr.list_shares()
|
|
300
308
|
config = mgr.list_config()
|
|
@@ -312,7 +320,8 @@ class DashboardScreen(Screen):
|
|
|
312
320
|
)
|
|
313
321
|
return
|
|
314
322
|
self.app.call_from_thread(
|
|
315
|
-
self._apply_status, result, extras, bw, bw_totals, uptimes, shares,
|
|
323
|
+
self._apply_status, result, extras, bw, bw_totals, uptimes, shares,
|
|
324
|
+
config, reconnect, bw_history_seed,
|
|
316
325
|
)
|
|
317
326
|
|
|
318
327
|
def _apply_status(
|
|
@@ -325,7 +334,9 @@ class DashboardScreen(Screen):
|
|
|
325
334
|
shares: list,
|
|
326
335
|
config,
|
|
327
336
|
reconnect: dict,
|
|
337
|
+
bw_history_seed: dict | None = None,
|
|
328
338
|
) -> None:
|
|
339
|
+
bw_history_seed = bw_history_seed or {}
|
|
329
340
|
self._last_config = config
|
|
330
341
|
self._last_shares = shares
|
|
331
342
|
|
|
@@ -352,13 +363,22 @@ class DashboardScreen(Screen):
|
|
|
352
363
|
}
|
|
353
364
|
self._conn_data = new_conn_data
|
|
354
365
|
|
|
355
|
-
# Update rolling bandwidth history (60 samples)
|
|
366
|
+
# Update rolling bandwidth history (60 samples). Seed from the
|
|
367
|
+
# daemon's persisted history (pre-fetched by refresh_status) on
|
|
368
|
+
# first encounter so reopening the TUI doesn't reset the chart to
|
|
369
|
+
# flatlined zeros.
|
|
356
370
|
for cs in result.connection_statuses:
|
|
357
371
|
tag = cs.tag
|
|
358
372
|
rx, tx = bw.get(tag, (0.0, 0.0))
|
|
359
373
|
if tag not in self._rx_history:
|
|
360
|
-
|
|
361
|
-
|
|
374
|
+
rx_seed = [0.0] * 60
|
|
375
|
+
tx_seed = [0.0] * 60
|
|
376
|
+
persisted = (bw_history_seed.get(tag) or [])[-60:]
|
|
377
|
+
if persisted:
|
|
378
|
+
rx_seed[-len(persisted):] = [s[0] for s in persisted]
|
|
379
|
+
tx_seed[-len(persisted):] = [s[1] for s in persisted]
|
|
380
|
+
self._rx_history[tag] = deque(rx_seed, maxlen=60)
|
|
381
|
+
self._tx_history[tag] = deque(tx_seed, maxlen=60)
|
|
362
382
|
self._rx_history[tag].append(rx)
|
|
363
383
|
self._tx_history[tag].append(tx)
|
|
364
384
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_glasses/dark/error.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/dark/error.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/dark/running.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/dark/stopped.svg
RENAMED
|
File without changes
|
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/light/error.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/light/running.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/colored_s/light/stopped.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/light/running.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/gear/light/stopped.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{susops-3.0.0rc3.dev7 → susops-3.0.0rc3.dev9}/src/susops/assets/icons/status/stopped_partially.svg
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|