susops 3.0.0rc6.dev1__tar.gz → 3.0.0rc6.dev2__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.0rc6.dev1 → susops-3.0.0rc6.dev2}/PKG-INFO +1 -1
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/pyproject.toml +1 -1
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tray/config_window_model.py +2 -1
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tray/mac.py +29 -4
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tray/mac_config_window.py +236 -17
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops.egg-info/PKG-INFO +1 -1
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_update_homebrew_sha.py +2 -2
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/LICENSE +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/README.md +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/setup.cfg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/__init__.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icon.png +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_glasses/dark/error.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_glasses/dark/running.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_glasses/dark/stopped.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_glasses/dark/stopped_partially.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_glasses/light/error.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_glasses/light/running.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_glasses/light/stopped.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_glasses/light/stopped_partially.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/dark/error.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/dark/running.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/dark/stopped.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/dark/stopped_partially.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/light/error.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/light/running.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/light/stopped.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/light/stopped_partially.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/dark/error.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/dark/running.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/dark/stopped.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/dark/stopped_partially.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/light/error.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/light/running.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/light/stopped.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/light/stopped_partially.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/status/error.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/status/running.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/status/stopped.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/status/stopped_partially.svg +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/client.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/__init__.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/browsers.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/config.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/log_style.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/pac.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/ports.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/process.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/rpc_protocol.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/rpc_server.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/services_daemon.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/share.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/socat.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/ssh.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/ssh_config.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/status.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/core/types.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/facade.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tray/__init__.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tray/base.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tray/debug_server.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tray/linux.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/__init__.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/__main__.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/app.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/app.tcss +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/cli.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/screens/__init__.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/screens/connections.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/screens/dashboard.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/screens/shares.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/widgets/__init__.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/tui/widgets/connection_card.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/version.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops.egg-info/SOURCES.txt +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops.egg-info/dependency_links.txt +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops.egg-info/entry_points.txt +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops.egg-info/requires.txt +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops.egg-info/top_level.txt +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_bw_totals.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_cli.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_client.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_config.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_conftest_smoke.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_facade.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_openapi.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_pac.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_packaging.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_process.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_rpc_protocol.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_rpc_server.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_scripts.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_services_daemon.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_share.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_share_aiohttp.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_socat.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_ssh.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/tests/test_status.py +0 -0
- {susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/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-rc6.
|
|
7
|
+
version = "3.0.0-rc6.dev2"
|
|
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" }
|
|
@@ -453,7 +453,8 @@ def build_share_detail(info, status, conn_tags=()) -> DetailSpec:
|
|
|
453
453
|
status_dot = "red"
|
|
454
454
|
fields = [
|
|
455
455
|
FormField(key="file", label="File", kind="static",
|
|
456
|
-
value=str(info.file_path)
|
|
456
|
+
value=str(info.file_path),
|
|
457
|
+
trailing=(("share.open_folder", "Open Folder"),)),
|
|
457
458
|
FormField(key="conn_tag", label="Connection", kind="popup",
|
|
458
459
|
value=getattr(info, "conn_tag", "") or "",
|
|
459
460
|
options=list(conn_tags)),
|
|
@@ -1471,12 +1471,22 @@ def _set_launch_at_login(enable: bool) -> None:
|
|
|
1471
1471
|
path = _login_item_path()
|
|
1472
1472
|
name = Path(path).stem
|
|
1473
1473
|
if enable:
|
|
1474
|
+
# Wrap in try-catch to handle the case where the login item already exists.
|
|
1475
|
+
# Creating a duplicate will fail; error suppression allows idempotency.
|
|
1474
1476
|
script = (
|
|
1475
|
-
'
|
|
1476
|
-
|
|
1477
|
+
'try\n'
|
|
1478
|
+
' tell application "System Events" to make login item '
|
|
1479
|
+
f'at end with properties {{path:"{path}", hidden:false}}\n'
|
|
1480
|
+
'end try'
|
|
1477
1481
|
)
|
|
1478
1482
|
else:
|
|
1479
|
-
|
|
1483
|
+
# Wrap delete in try-catch to gracefully handle missing login items.
|
|
1484
|
+
# Error -1728 occurs when the item doesn't exist (e.g., in dev environments).
|
|
1485
|
+
script = (
|
|
1486
|
+
'try\n'
|
|
1487
|
+
f' tell application "System Events" to delete login item "{name}"\n'
|
|
1488
|
+
'end try'
|
|
1489
|
+
)
|
|
1480
1490
|
try:
|
|
1481
1491
|
subprocess.run(["osascript", "-e", script], check=False, timeout=3)
|
|
1482
1492
|
except Exception:
|
|
@@ -1789,6 +1799,18 @@ class SusOpsMacTray(AbstractTrayApp):
|
|
|
1789
1799
|
self._copy_to_pasteboard(f"http://localhost:{port}")
|
|
1790
1800
|
elif action_id == "share.copy_password":
|
|
1791
1801
|
self._copy_to_pasteboard(info.password or "")
|
|
1802
|
+
elif action_id == "share.open_folder":
|
|
1803
|
+
try:
|
|
1804
|
+
from AppKit import NSWorkspace, NSURL # type: ignore[import]
|
|
1805
|
+
|
|
1806
|
+
p = Path(str(info.file_path)).expanduser()
|
|
1807
|
+
if p.exists():
|
|
1808
|
+
u = NSURL.fileURLWithPath_(str(p))
|
|
1809
|
+
NSWorkspace.sharedWorkspace().activateFileViewerSelectingURLs_([u])
|
|
1810
|
+
else:
|
|
1811
|
+
self.show_alert("File Not Found", str(p))
|
|
1812
|
+
except Exception as e:
|
|
1813
|
+
self.show_alert("Could Not Open Folder", str(e))
|
|
1792
1814
|
self._refresh_config_window()
|
|
1793
1815
|
|
|
1794
1816
|
def _reserve_share_silent(self, info) -> None:
|
|
@@ -2925,12 +2947,15 @@ class SusOpsMacTray(AbstractTrayApp):
|
|
|
2925
2947
|
return self._apply_server_ports(rpc, sse, pac, ctx)
|
|
2926
2948
|
|
|
2927
2949
|
def _done(err):
|
|
2950
|
+
cw = getattr(self, "_config_window", None)
|
|
2928
2951
|
if err:
|
|
2952
|
+
if cw is not None:
|
|
2953
|
+
cw.mark_settings_save_failed()
|
|
2929
2954
|
self.show_alert("Could Not Apply Settings", err)
|
|
2930
2955
|
return
|
|
2931
|
-
cw = getattr(self, "_config_window", None)
|
|
2932
2956
|
if cw is not None:
|
|
2933
2957
|
cw.clear_settings_dirty()
|
|
2958
|
+
cw.mark_settings_saved()
|
|
2934
2959
|
cw.refresh()
|
|
2935
2960
|
self.run_in_background(_work, _done)
|
|
2936
2961
|
|
|
@@ -45,6 +45,8 @@ _action_handler_cls = None
|
|
|
45
45
|
_text_delegate_cls = None
|
|
46
46
|
_row_view_cls = None
|
|
47
47
|
_vcenter_text_cell_cls = None
|
|
48
|
+
_padded_secure_cell_cls = None
|
|
49
|
+
_padded_plain_pw_cell_cls = None
|
|
48
50
|
_TEXT_CELL_FLAG_ACCESSORS = (
|
|
49
51
|
("isEditable", "setEditable_"),
|
|
50
52
|
("isSelectable", "setSelectable_"),
|
|
@@ -62,9 +64,6 @@ def _make_vcenter_cell_cls(base_cell_cls, padding: float = 0.0):
|
|
|
62
64
|
from Foundation import NSMakeRect # type: ignore[import]
|
|
63
65
|
|
|
64
66
|
class _SusOpsVCenterCell(base_cell_cls):
|
|
65
|
-
def __init__(self):
|
|
66
|
-
super().__init__()
|
|
67
|
-
self._padding = 0.0
|
|
68
67
|
|
|
69
68
|
def _baseTitleRect_(self, frame):
|
|
70
69
|
try:
|
|
@@ -140,6 +139,108 @@ def _get_vcenter_text_cell_cls():
|
|
|
140
139
|
return _vcenter_text_cell_cls
|
|
141
140
|
|
|
142
141
|
|
|
142
|
+
def _get_padded_secure_cell_cls():
|
|
143
|
+
global _padded_secure_cell_cls
|
|
144
|
+
if _padded_secure_cell_cls is not None:
|
|
145
|
+
return _padded_secure_cell_cls
|
|
146
|
+
from Cocoa import NSMakeRect, NSSecureTextFieldCell # type: ignore[import]
|
|
147
|
+
|
|
148
|
+
class _SusOpsPaddedSecureCell(NSSecureTextFieldCell):
|
|
149
|
+
def _insetCenteredRect_(self, frame):
|
|
150
|
+
try:
|
|
151
|
+
rect = objc.super(_SusOpsPaddedSecureCell, self).titleRectForBounds_(frame)
|
|
152
|
+
except Exception:
|
|
153
|
+
rect = objc.super(_SusOpsPaddedSecureCell, self).drawingRectForBounds_(frame)
|
|
154
|
+
text_h = rect.size.height
|
|
155
|
+
try:
|
|
156
|
+
size = objc.super(_SusOpsPaddedSecureCell, self).cellSizeForBounds_(frame)
|
|
157
|
+
if size is not None and getattr(size, "height", 0) > 0:
|
|
158
|
+
text_h = min(rect.size.height, float(size.height))
|
|
159
|
+
except Exception:
|
|
160
|
+
pass
|
|
161
|
+
text_h = max(1.0, min(float(frame.size.height), float(text_h)))
|
|
162
|
+
p = float(getattr(self, "_padding", 0.0))
|
|
163
|
+
return NSMakeRect(
|
|
164
|
+
float(frame.origin.x) + max(0.0, p),
|
|
165
|
+
float(frame.origin.y) + max(0.0, (float(frame.size.height) - text_h) / 2.0),
|
|
166
|
+
max(1.0, float(frame.size.width) - max(0.0, p)),
|
|
167
|
+
text_h,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def drawInteriorWithFrame_inView_(self, frame, view):
|
|
171
|
+
objc.super(_SusOpsPaddedSecureCell, self).drawInteriorWithFrame_inView_(
|
|
172
|
+
self._insetCenteredRect_(frame), view
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def selectWithFrame_inView_editor_delegate_start_length_(
|
|
176
|
+
self, frame, view, editor, delegate, start, length
|
|
177
|
+
):
|
|
178
|
+
objc.super(_SusOpsPaddedSecureCell, self).selectWithFrame_inView_editor_delegate_start_length_(
|
|
179
|
+
self._insetCenteredRect_(frame), view, editor, delegate, start, length
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
def editWithFrame_inView_editor_delegate_event_(
|
|
183
|
+
self, frame, view, editor, delegate, event
|
|
184
|
+
):
|
|
185
|
+
objc.super(_SusOpsPaddedSecureCell, self).editWithFrame_inView_editor_delegate_event_(
|
|
186
|
+
self._insetCenteredRect_(frame), view, editor, delegate, event
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
_padded_secure_cell_cls = _SusOpsPaddedSecureCell
|
|
190
|
+
return _padded_secure_cell_cls
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _get_padded_plain_password_cell_cls():
|
|
194
|
+
global _padded_plain_pw_cell_cls
|
|
195
|
+
if _padded_plain_pw_cell_cls is not None:
|
|
196
|
+
return _padded_plain_pw_cell_cls
|
|
197
|
+
from Cocoa import NSMakeRect, NSTextFieldCell # type: ignore[import]
|
|
198
|
+
|
|
199
|
+
class _SusOpsPaddedPlainPasswordCell(NSTextFieldCell):
|
|
200
|
+
def _insetCenteredRect_(self, frame):
|
|
201
|
+
try:
|
|
202
|
+
rect = objc.super(_SusOpsPaddedPlainPasswordCell, self).titleRectForBounds_(frame)
|
|
203
|
+
except Exception:
|
|
204
|
+
rect = objc.super(_SusOpsPaddedPlainPasswordCell, self).drawingRectForBounds_(frame)
|
|
205
|
+
text_h = rect.size.height
|
|
206
|
+
try:
|
|
207
|
+
size = objc.super(_SusOpsPaddedPlainPasswordCell, self).cellSizeForBounds_(frame)
|
|
208
|
+
if size is not None and getattr(size, "height", 0) > 0:
|
|
209
|
+
text_h = min(rect.size.height, float(size.height))
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
text_h = max(1.0, min(float(frame.size.height), float(text_h)))
|
|
213
|
+
p = float(getattr(self, "_padding", 0.0))
|
|
214
|
+
return NSMakeRect(
|
|
215
|
+
float(frame.origin.x) + max(0.0, p),
|
|
216
|
+
float(frame.origin.y) + max(0.0, (float(frame.size.height) - text_h) / 2.0),
|
|
217
|
+
max(1.0, float(frame.size.width) - max(0.0, p)),
|
|
218
|
+
text_h,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def drawInteriorWithFrame_inView_(self, frame, view):
|
|
222
|
+
objc.super(_SusOpsPaddedPlainPasswordCell, self).drawInteriorWithFrame_inView_(
|
|
223
|
+
self._insetCenteredRect_(frame), view
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def selectWithFrame_inView_editor_delegate_start_length_(
|
|
227
|
+
self, frame, view, editor, delegate, start, length
|
|
228
|
+
):
|
|
229
|
+
objc.super(_SusOpsPaddedPlainPasswordCell, self).selectWithFrame_inView_editor_delegate_start_length_(
|
|
230
|
+
self._insetCenteredRect_(frame), view, editor, delegate, start, length
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def editWithFrame_inView_editor_delegate_event_(
|
|
234
|
+
self, frame, view, editor, delegate, event
|
|
235
|
+
):
|
|
236
|
+
objc.super(_SusOpsPaddedPlainPasswordCell, self).editWithFrame_inView_editor_delegate_event_(
|
|
237
|
+
self._insetCenteredRect_(frame), view, editor, delegate, event
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
_padded_plain_pw_cell_cls = _SusOpsPaddedPlainPasswordCell
|
|
241
|
+
return _padded_plain_pw_cell_cls
|
|
242
|
+
|
|
243
|
+
|
|
143
244
|
def _truncate_tail(field) -> None:
|
|
144
245
|
"""Single-line + truncate an NSTextField so overflow shows an ellipsis
|
|
145
246
|
instead of a raw hard cut at the field edge. NSLineBreakByTruncatingMiddle = 5
|
|
@@ -445,11 +546,12 @@ TOP_INSET = 45 # traffic lights overlay col 1; start content below them
|
|
|
445
546
|
SIDEBAR_TOP_INSET = TOP_INSET - 9 # nav-only top inset (kept independent for fine tuning)
|
|
446
547
|
SEARCH_H = 30
|
|
447
548
|
ADDBAR_H = 40
|
|
549
|
+
ADDBAR_BOTTOM_INSET = 4
|
|
448
550
|
ROW_PILL_INSET_X = 12 # left/right margin for nav/list rows and selection pill
|
|
449
551
|
ROW_TEXT_RIGHT_PAD = 18 # extra right breathing room before row edge
|
|
450
552
|
NAV_ICON_INSET_X = 8 # nav icon left inset within row pill
|
|
451
553
|
NAV_COUNT_RIGHT_INSET = NAV_ICON_INSET_X + 28 # match count inset to icon inset
|
|
452
|
-
DEBUG_ROW_BOUNDS =
|
|
554
|
+
DEBUG_ROW_BOUNDS = False # temporary visual aid: draw row pills in red
|
|
453
555
|
|
|
454
556
|
# Column-3 content is constrained to a fixed-width column anchored top-left,
|
|
455
557
|
# NOT stretched to the window edge. The Enabled toggle anchors to the RIGHT
|
|
@@ -531,11 +633,16 @@ class ConfigWindow:
|
|
|
531
633
|
self._settings_widgets: dict = {} # field key -> control
|
|
532
634
|
self._settings_kinds: dict = {} # field key -> kind
|
|
533
635
|
self._settings_ctx: dict = {} # ctx from settings_field_specs
|
|
636
|
+
self._settings_status_label = None # tiny status text next to Save
|
|
637
|
+
self._settings_save_message = ""
|
|
638
|
+
self._settings_save_error = False
|
|
639
|
+
self._settings_save_clear_timer = None # timer for auto-clearing "Saved"
|
|
534
640
|
# Settings staging: all settings changes are staged locally and only
|
|
535
641
|
# persist on Apply. _settings_dirty tracks pending edits so leaving the
|
|
536
642
|
# category or closing the window can discard them (and revert the logo
|
|
537
643
|
# preview).
|
|
538
644
|
self._settings_dirty = False
|
|
645
|
+
self._revealed_secure_tokens: set[tuple] = set()
|
|
539
646
|
|
|
540
647
|
# ------------------------------------------------------------------ #
|
|
541
648
|
# Lifecycle
|
|
@@ -573,6 +680,13 @@ class ConfigWindow:
|
|
|
573
680
|
# Closing the window without Apply discards any staged settings
|
|
574
681
|
# changes (and reverts a logo icon preview to the saved logo).
|
|
575
682
|
self.discard_settings()
|
|
683
|
+
# Cancel any pending auto-clear timer.
|
|
684
|
+
if self._settings_save_clear_timer is not None:
|
|
685
|
+
try:
|
|
686
|
+
self._settings_save_clear_timer.cancel()
|
|
687
|
+
except Exception:
|
|
688
|
+
pass
|
|
689
|
+
self._settings_save_clear_timer = None
|
|
576
690
|
if self._policy_scope is not None:
|
|
577
691
|
try:
|
|
578
692
|
self._policy_scope.__exit__(None, None, None)
|
|
@@ -798,7 +912,7 @@ class ConfigWindow:
|
|
|
798
912
|
self._search_field = sf
|
|
799
913
|
|
|
800
914
|
list_top = ch - TOP_INSET - SEARCH_H - 8
|
|
801
|
-
list_h = list_top - ADDBAR_H -
|
|
915
|
+
list_h = list_top - ADDBAR_H - ADDBAR_BOTTOM_INSET
|
|
802
916
|
tv = NSTableView.alloc().initWithFrame_(
|
|
803
917
|
NSMakeRect(0, 0, COL2_W, list_h))
|
|
804
918
|
col = NSTableColumn.alloc().initWithIdentifier_("list")
|
|
@@ -816,7 +930,7 @@ class ConfigWindow:
|
|
|
816
930
|
tv.setDataSource_(ds)
|
|
817
931
|
tv.setDelegate_(ds)
|
|
818
932
|
scroll = NSScrollView.alloc().initWithFrame_(
|
|
819
|
-
NSMakeRect(0, ADDBAR_H +
|
|
933
|
+
NSMakeRect(0, ADDBAR_H + ADDBAR_BOTTOM_INSET, COL2_W, list_h))
|
|
820
934
|
scroll.setDrawsBackground_(False)
|
|
821
935
|
scroll.setHasHorizontalScroller_(False)
|
|
822
936
|
scroll.setHasVerticalScroller_(True)
|
|
@@ -975,10 +1089,13 @@ class ConfigWindow:
|
|
|
975
1089
|
return [] # settings has no list
|
|
976
1090
|
|
|
977
1091
|
def _reload_list(self, *, preserve: bool, skip_detail: bool = False) -> None:
|
|
978
|
-
# When
|
|
979
|
-
#
|
|
980
|
-
|
|
981
|
-
|
|
1092
|
+
# When skip_detail is set, keep the col-2 highlight pinned to the dirty
|
|
1093
|
+
# identity when available, otherwise preserve the current selected item
|
|
1094
|
+
# (focused-but-not-dirty detail fields still need stable row selection).
|
|
1095
|
+
if skip_detail:
|
|
1096
|
+
prev = self._dirty_identity or self.selected_identity
|
|
1097
|
+
else:
|
|
1098
|
+
prev = self.selected_identity if preserve else None
|
|
982
1099
|
self._all_rows = self._build_category_rows()
|
|
983
1100
|
self.rows = filter_rows(self._all_rows, self.search_text)
|
|
984
1101
|
self._suppress_selection_cb = True
|
|
@@ -1019,6 +1136,8 @@ class ConfigWindow:
|
|
|
1019
1136
|
# _on_selection which discards; this covers the programmatic/debug path.
|
|
1020
1137
|
if self.category == "settings" and category != "settings":
|
|
1021
1138
|
self.discard_settings()
|
|
1139
|
+
if category != self.category:
|
|
1140
|
+
self._clear_revealed_password_state()
|
|
1022
1141
|
self.category = category
|
|
1023
1142
|
idx = next((i for i, n in enumerate(self.nav_items)
|
|
1024
1143
|
if n.key == category), None)
|
|
@@ -1062,6 +1181,8 @@ class ConfigWindow:
|
|
|
1062
1181
|
self._dirty = False
|
|
1063
1182
|
self._dirty_identity = None
|
|
1064
1183
|
self._create_kind = None
|
|
1184
|
+
if new_cat != self.category:
|
|
1185
|
+
self._clear_revealed_password_state()
|
|
1065
1186
|
self.category = new_cat
|
|
1066
1187
|
self.selected_identity = None
|
|
1067
1188
|
self.search_text = ""
|
|
@@ -1095,6 +1216,8 @@ class ConfigWindow:
|
|
|
1095
1216
|
self._dirty = False
|
|
1096
1217
|
self._dirty_identity = None
|
|
1097
1218
|
self._create_kind = None
|
|
1219
|
+
if new_identity != self.selected_identity:
|
|
1220
|
+
self._clear_revealed_password_state()
|
|
1098
1221
|
self.selected_identity = new_identity
|
|
1099
1222
|
self._render_selection_placeholder()
|
|
1100
1223
|
|
|
@@ -1447,7 +1570,9 @@ class ConfigWindow:
|
|
|
1447
1570
|
specs = self._add_button_specs()
|
|
1448
1571
|
if not specs:
|
|
1449
1572
|
return
|
|
1450
|
-
bar = NSView.alloc().initWithFrame_(
|
|
1573
|
+
bar = NSView.alloc().initWithFrame_(
|
|
1574
|
+
NSMakeRect(0, ADDBAR_BOTTOM_INSET, COL2_W, ADDBAR_H)
|
|
1575
|
+
)
|
|
1451
1576
|
bar.setAutoresizingMask_(2 | 32) # WidthSizable | MaxYMargin
|
|
1452
1577
|
n = len(specs)
|
|
1453
1578
|
gap = 8
|
|
@@ -1501,6 +1626,7 @@ class ConfigWindow:
|
|
|
1501
1626
|
preselect = self._preselected_conn_tag(conn_tags)
|
|
1502
1627
|
self._dirty = False
|
|
1503
1628
|
self._dirty_identity = None
|
|
1629
|
+
self._clear_revealed_password_state()
|
|
1504
1630
|
self._create_kind = kind
|
|
1505
1631
|
self.selected_identity = None
|
|
1506
1632
|
# Deselect any highlighted col-2 row without firing the selection cb.
|
|
@@ -1523,6 +1649,7 @@ class ConfigWindow:
|
|
|
1523
1649
|
|
|
1524
1650
|
def exit_create_mode(self) -> None:
|
|
1525
1651
|
"""Leave create mode and restore normal selection rendering."""
|
|
1652
|
+
self._clear_revealed_password_state()
|
|
1526
1653
|
self._create_kind = None
|
|
1527
1654
|
self._dirty = False
|
|
1528
1655
|
self._dirty_identity = None
|
|
@@ -1670,6 +1797,53 @@ class ConfigWindow:
|
|
|
1670
1797
|
self._header_toggle_label = None
|
|
1671
1798
|
self._settings_widgets = {}
|
|
1672
1799
|
self._settings_kinds = {}
|
|
1800
|
+
self._settings_status_label = None
|
|
1801
|
+
|
|
1802
|
+
def _set_settings_save_feedback(self, message: str = "", *, is_error: bool = False) -> None:
|
|
1803
|
+
"""Update the settings Save feedback state and repaint the inline label
|
|
1804
|
+
when it exists."""
|
|
1805
|
+
from Cocoa import NSColor # type: ignore[import]
|
|
1806
|
+
|
|
1807
|
+
# Cancel any pending auto-clear timer.
|
|
1808
|
+
if self._settings_save_clear_timer is not None:
|
|
1809
|
+
try:
|
|
1810
|
+
self._settings_save_clear_timer.cancel()
|
|
1811
|
+
except Exception:
|
|
1812
|
+
pass
|
|
1813
|
+
self._settings_save_clear_timer = None
|
|
1814
|
+
|
|
1815
|
+
self._settings_save_message = str(message or "")
|
|
1816
|
+
self._settings_save_error = bool(is_error)
|
|
1817
|
+
lbl = self._settings_status_label
|
|
1818
|
+
if lbl is None:
|
|
1819
|
+
return
|
|
1820
|
+
lbl.setStringValue_(self._settings_save_message)
|
|
1821
|
+
if not self._settings_save_message:
|
|
1822
|
+
lbl.setTextColor_(NSColor.tertiaryLabelColor())
|
|
1823
|
+
return
|
|
1824
|
+
if self._settings_save_error:
|
|
1825
|
+
lbl.setTextColor_(NSColor.systemRedColor())
|
|
1826
|
+
return
|
|
1827
|
+
lbl.setTextColor_(NSColor.systemGreenColor())
|
|
1828
|
+
|
|
1829
|
+
def mark_settings_saved(self) -> None:
|
|
1830
|
+
"""Show 'Saved' and auto-clear it after 3 seconds."""
|
|
1831
|
+
self._set_settings_save_feedback("Saved", is_error=False)
|
|
1832
|
+
# Schedule auto-clear on a background thread to avoid blocking the UI.
|
|
1833
|
+
import threading
|
|
1834
|
+
def _clear():
|
|
1835
|
+
try:
|
|
1836
|
+
# Marshal the UI update back to the main thread.
|
|
1837
|
+
from susops.tray.mac import _on_main
|
|
1838
|
+
_on_main(lambda: self._set_settings_save_feedback())
|
|
1839
|
+
except Exception:
|
|
1840
|
+
pass
|
|
1841
|
+
self._settings_save_clear_timer = threading.Timer(1.0, _clear)
|
|
1842
|
+
self._settings_save_clear_timer.daemon = True
|
|
1843
|
+
self._settings_save_clear_timer.start()
|
|
1844
|
+
|
|
1845
|
+
def mark_settings_save_failed(self) -> None:
|
|
1846
|
+
self._set_settings_save_feedback("Save failed", is_error=True)
|
|
1673
1847
|
|
|
1674
1848
|
def _content_column(self):
|
|
1675
1849
|
"""Create the content column: a fixed CONTENT_MAX_W view anchored
|
|
@@ -2121,6 +2295,7 @@ class ConfigWindow:
|
|
|
2121
2295
|
frame = NSMakeRect(x, cy, ctrl_w, 22)
|
|
2122
2296
|
secure = NSSecureTextField.alloc().initWithFrame_(frame)
|
|
2123
2297
|
plain = NSTextField.alloc().initWithFrame_(frame)
|
|
2298
|
+
token = self._secure_state_token(f.key)
|
|
2124
2299
|
for tf in (secure, plain):
|
|
2125
2300
|
tf.setStringValue_(str(f.value or ""))
|
|
2126
2301
|
tf.setFont_(NSFont.systemFontOfSize_(13))
|
|
@@ -2135,14 +2310,27 @@ class ConfigWindow:
|
|
|
2135
2310
|
except Exception:
|
|
2136
2311
|
pass
|
|
2137
2312
|
self._style_input_field(tf)
|
|
2138
|
-
|
|
2313
|
+
if tf is plain:
|
|
2314
|
+
_apply_vcenter_cell(tf, _get_padded_plain_password_cell_cls(), padding=10.0)
|
|
2315
|
+
else:
|
|
2316
|
+
_apply_vcenter_cell(tf, _get_padded_secure_cell_cls(), padding=10.0)
|
|
2139
2317
|
self._wire_text_dirty(tf)
|
|
2140
2318
|
card.addSubview_(tf)
|
|
2141
|
-
|
|
2319
|
+
revealed = token in self._revealed_secure_tokens
|
|
2320
|
+
plain.setHidden_(not revealed)
|
|
2321
|
+
secure.setHidden_(revealed)
|
|
2142
2322
|
# The visible field is the one collect_form_values / set_field read.
|
|
2143
|
-
self._field_widgets[f.key] = secure
|
|
2323
|
+
self._field_widgets[f.key] = plain if revealed else secure
|
|
2144
2324
|
self._secure_pair[f.key] = (secure, plain)
|
|
2145
2325
|
|
|
2326
|
+
def _secure_state_token(self, key: str) -> tuple:
|
|
2327
|
+
"""Stable token for preserving secure-field reveal state across rerenders."""
|
|
2328
|
+
return (self.category, self._create_kind, self.selected_identity, key)
|
|
2329
|
+
|
|
2330
|
+
def _clear_revealed_password_state(self) -> None:
|
|
2331
|
+
"""Auto-hide any revealed password when navigating away from its context."""
|
|
2332
|
+
self._revealed_secure_tokens.clear()
|
|
2333
|
+
|
|
2146
2334
|
def _reserve_trailing(self, card, f, x, ry, width) -> float:
|
|
2147
2335
|
"""Render f.trailing buttons right-aligned within [x, x+width] and
|
|
2148
2336
|
return the control width remaining on the left. share.reveal is a local
|
|
@@ -2171,8 +2359,11 @@ class ConfigWindow:
|
|
|
2171
2359
|
ctrl_w = max(60, rx - x - 8)
|
|
2172
2360
|
bx = rx
|
|
2173
2361
|
for (aid, label), bw in zip(trailing, widths):
|
|
2362
|
+
btn_label = label
|
|
2363
|
+
if aid == "share.reveal" and self._secure_state_token(f.key) in self._revealed_secure_tokens:
|
|
2364
|
+
btn_label = "Hide"
|
|
2174
2365
|
btn = self._styled_neutral_button(
|
|
2175
|
-
|
|
2366
|
+
btn_label, NSMakeRect(bx, btn_y, bw, btn_h))
|
|
2176
2367
|
if aid == "share.reveal":
|
|
2177
2368
|
handler = _get_action_handler_cls().alloc().initWithCallback_(
|
|
2178
2369
|
lambda _s, key=f.key, b=btn: self._on_reveal_password(key, b))
|
|
@@ -2226,12 +2417,14 @@ class ConfigWindow:
|
|
|
2226
2417
|
return
|
|
2227
2418
|
secure, plain = pair
|
|
2228
2419
|
showing_plain = bool(plain.isHidden()) is False
|
|
2420
|
+
token = self._secure_state_token(key)
|
|
2229
2421
|
try:
|
|
2230
2422
|
if showing_plain:
|
|
2231
2423
|
secure.setStringValue_(str(plain.stringValue() or ""))
|
|
2232
2424
|
plain.setHidden_(True)
|
|
2233
2425
|
secure.setHidden_(False)
|
|
2234
2426
|
self._field_widgets[key] = secure
|
|
2427
|
+
self._revealed_secure_tokens.discard(token)
|
|
2235
2428
|
btn.setAttributedTitle_(
|
|
2236
2429
|
self._attr_title("Reveal", _hex_color(PALETTE["input_text"])))
|
|
2237
2430
|
else:
|
|
@@ -2239,6 +2432,7 @@ class ConfigWindow:
|
|
|
2239
2432
|
secure.setHidden_(True)
|
|
2240
2433
|
plain.setHidden_(False)
|
|
2241
2434
|
self._field_widgets[key] = plain
|
|
2435
|
+
self._revealed_secure_tokens.add(token)
|
|
2242
2436
|
btn.setAttributedTitle_(
|
|
2243
2437
|
self._attr_title("Hide", _hex_color(PALETTE["input_text"])))
|
|
2244
2438
|
except Exception:
|
|
@@ -2943,7 +3137,8 @@ class ConfigWindow:
|
|
|
2943
3137
|
single row. Save commits ALL staged settings (toggles + logo + login +
|
|
2944
3138
|
ports); it is always enabled. Open Config File opens the YAML in
|
|
2945
3139
|
$EDITOR."""
|
|
2946
|
-
from
|
|
3140
|
+
from AppKit import NSFont # type: ignore[import]
|
|
3141
|
+
from Cocoa import NSColor, NSMakeRect, NSTextField # type: ignore[import]
|
|
2947
3142
|
btn_h = 28
|
|
2948
3143
|
# Match the button baseline to the section-label baseline used in
|
|
2949
3144
|
# _section_label (label frame origin: section_top - 20, height: 18).
|
|
@@ -2972,6 +3167,26 @@ class ConfigWindow:
|
|
|
2972
3167
|
save_btn.setTarget_(save_handler)
|
|
2973
3168
|
save_btn.setAction_("fire:")
|
|
2974
3169
|
doc.addSubview_(save_btn)
|
|
3170
|
+
|
|
3171
|
+
# Inline save feedback gives immediate visual confirmation without
|
|
3172
|
+
# requiring another dialog.
|
|
3173
|
+
status_x = save_x + save_w + 12
|
|
3174
|
+
status_w = max(80, width - (status_x - x))
|
|
3175
|
+
status = NSTextField.alloc().initWithFrame_(
|
|
3176
|
+
NSMakeRect(status_x, btn_y + 5, status_w, 16))
|
|
3177
|
+
status.setStringValue_("")
|
|
3178
|
+
status.setFont_(NSFont.systemFontOfSize_(11))
|
|
3179
|
+
status.setBezeled_(False)
|
|
3180
|
+
status.setDrawsBackground_(False)
|
|
3181
|
+
status.setEditable_(False)
|
|
3182
|
+
status.setSelectable_(False)
|
|
3183
|
+
status.setTextColor_(NSColor.tertiaryLabelColor())
|
|
3184
|
+
doc.addSubview_(status)
|
|
3185
|
+
self._settings_status_label = status
|
|
3186
|
+
self._set_settings_save_feedback(
|
|
3187
|
+
self._settings_save_message,
|
|
3188
|
+
is_error=self._settings_save_error,
|
|
3189
|
+
)
|
|
2975
3190
|
# Keep the section height consistent with other rows.
|
|
2976
3191
|
return btn_y - 6
|
|
2977
3192
|
|
|
@@ -2980,6 +3195,7 @@ class ConfigWindow:
|
|
|
2980
3195
|
Marks the settings pane dirty. For logo_style, show a LIVE icon preview
|
|
2981
3196
|
(reverted on leave/close without Apply)."""
|
|
2982
3197
|
self._settings_dirty = True
|
|
3198
|
+
self._set_settings_save_feedback()
|
|
2983
3199
|
if key == "logo_style":
|
|
2984
3200
|
try:
|
|
2985
3201
|
self.tray.preview_logo_style(int(value))
|
|
@@ -2995,7 +3211,7 @@ class ConfigWindow:
|
|
|
2995
3211
|
try:
|
|
2996
3212
|
self.tray.apply_all_settings(values)
|
|
2997
3213
|
except Exception:
|
|
2998
|
-
|
|
3214
|
+
self.mark_settings_save_failed()
|
|
2999
3215
|
|
|
3000
3216
|
def discard_settings(self) -> None:
|
|
3001
3217
|
"""Drop all staged settings changes: revert any logo icon preview to the
|
|
@@ -3053,6 +3269,7 @@ class ConfigWindow:
|
|
|
3053
3269
|
except Exception:
|
|
3054
3270
|
return {"error": f"could not set '{key}'"}
|
|
3055
3271
|
self._settings_dirty = True
|
|
3272
|
+
self._set_settings_save_feedback()
|
|
3056
3273
|
return {"ok": True, "key": key, "value": value}
|
|
3057
3274
|
|
|
3058
3275
|
def clear_settings_dirty(self) -> None:
|
|
@@ -3090,6 +3307,8 @@ class ConfigWindow:
|
|
|
3090
3307
|
"settings": (self._settings_values()
|
|
3091
3308
|
if self.category == "settings" else None),
|
|
3092
3309
|
"settings_dirty": bool(getattr(self, "_settings_dirty", False)),
|
|
3310
|
+
"settings_save_message": str(getattr(self, "_settings_save_message", "")),
|
|
3311
|
+
"settings_save_error": bool(getattr(self, "_settings_save_error", False)),
|
|
3093
3312
|
"fields": self._dump_fields(),
|
|
3094
3313
|
# Cumulative auto-answered modal panels (debug mode) so tests can
|
|
3095
3314
|
# assert no unexpected dialog fired.
|
|
@@ -52,9 +52,9 @@ def test_cask_update_handles_legacy_initial_state(tmp_path: Path) -> None:
|
|
|
52
52
|
p = tmp_path / "susops.rb"
|
|
53
53
|
p.write_text(LEGACY_CASK)
|
|
54
54
|
|
|
55
|
-
update_cask_sha(p, "3.0.0-rc6.
|
|
55
|
+
update_cask_sha(p, "3.0.0-rc6.dev2", "c" * 64)
|
|
56
56
|
out = p.read_text()
|
|
57
|
-
assert 'version "3.0.0-rc6.
|
|
57
|
+
assert 'version "3.0.0-rc6.dev2"' in out
|
|
58
58
|
assert f'sha256 "{"c" * 64}"' in out
|
|
59
59
|
assert '#{version}' in out
|
|
60
60
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/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.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/dark/error.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/dark/running.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/dark/stopped.svg
RENAMED
|
File without changes
|
|
File without changes
|
{susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/light/error.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/colored_s/light/running.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/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.0rc6.dev1 → susops-3.0.0rc6.dev2}/src/susops/assets/icons/gear/light/running.svg
RENAMED
|
File without changes
|
{susops-3.0.0rc6.dev1 → susops-3.0.0rc6.dev2}/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.0rc6.dev1 → susops-3.0.0rc6.dev2}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|