solstone-linux 0.3.2__tar.gz → 0.3.3__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.
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/CHANGELOG.md +5 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/PKG-INFO +1 -1
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/pyproject.toml +1 -1
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/__init__.py +1 -1
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/dbusmenu.py +10 -2
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/tray.py +27 -6
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_dbusmenu.py +22 -1
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_tray.py +149 -2
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/.gitignore +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/AGENTS.md +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/CLAUDE.md +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/INSTALL.md +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/LICENSE +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/Makefile +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/README.md +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/contrib/icons/hicolor/scalable/status/solstone-error.svg +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/contrib/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/contrib/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/contrib/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/scripts/extract_changelog.sh +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/scripts/release.sh +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/activity.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/audio_detect.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/audio_mute.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/audio_recorder.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/chat_bridge.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/cli.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/config.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/dbus_service.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/doctor.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/icons/hicolor/scalable/status/solstone-error.svg +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/icons/hicolor/scalable/status/solstone-paused.svg +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/icons/hicolor/scalable/status/solstone-recording.svg +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/icons/hicolor/scalable/status/solstone-syncing.svg +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/install_guard.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/monitor_positions.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/observer.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/recovery.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/screencast.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/session_env.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/sni.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/solstone-linux.service.in +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/streams.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/sync.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/sync_health.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/src/solstone_linux/upload.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/__init__.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_activity.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_chat_bridge.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_cli.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_config.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_dbus_service.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_doctor.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_extract_changelog.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_install_guard.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_monitor_positions.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_observer.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_observer_emits_stream_silent_event.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_screencast.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_screencast_stop_filters_silent_streams.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_session_env.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_streams.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_sync.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_sync_health.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_sync_health_surfaces.py +0 -0
- {solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_upload.py +0 -0
|
@@ -4,6 +4,11 @@ All notable changes to solstone-linux are documented here.
|
|
|
4
4
|
The format is based on Keep a Changelog (https://keepachangelog.com/),
|
|
5
5
|
and this project adheres to Semantic Versioning.
|
|
6
6
|
|
|
7
|
+
## [0.3.3] - 2026-06-16
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- the tray status submenu now refreshes its values every time you open it. the segment countdown, cache size, captures today, uptime, and sync line had been showing stale values on reopen on some desktops; they now reflect the current state each time you open the menu.
|
|
11
|
+
|
|
7
12
|
## [0.3.2] - 2026-06-16
|
|
8
13
|
|
|
9
14
|
### Changed
|
|
@@ -86,6 +86,8 @@ class DBusMenu(ServiceInterface):
|
|
|
86
86
|
|
|
87
87
|
def __init__(self):
|
|
88
88
|
super().__init__("com.canonical.dbusmenu")
|
|
89
|
+
self.on_about_to_show = None
|
|
90
|
+
self._props_emitted = 0
|
|
89
91
|
self._revision = 1
|
|
90
92
|
self._root = MenuItem() # id 0 is root
|
|
91
93
|
self._root.id = 0
|
|
@@ -106,6 +108,7 @@ class DBusMenu(ServiceInterface):
|
|
|
106
108
|
return
|
|
107
109
|
|
|
108
110
|
updated = {name: self._property_variant(item, name) for name in names}
|
|
111
|
+
self._props_emitted += 1
|
|
109
112
|
self.ItemsPropertiesUpdated([[item.id, updated]], [])
|
|
110
113
|
|
|
111
114
|
def _register_items(self, items: list[MenuItem]):
|
|
@@ -198,11 +201,16 @@ class DBusMenu(ServiceInterface):
|
|
|
198
201
|
|
|
199
202
|
@method()
|
|
200
203
|
def AboutToShow(self, item_id: "i") -> "b":
|
|
201
|
-
|
|
204
|
+
if self.on_about_to_show is None:
|
|
205
|
+
return False
|
|
206
|
+
return bool(self.on_about_to_show())
|
|
202
207
|
|
|
203
208
|
@method()
|
|
204
209
|
def AboutToShowGroup(self, ids: "ai") -> "aiai":
|
|
205
|
-
|
|
210
|
+
if self.on_about_to_show is None:
|
|
211
|
+
return [[], []]
|
|
212
|
+
changed = bool(self.on_about_to_show())
|
|
213
|
+
return [list(ids), []] if changed else [[], []]
|
|
206
214
|
|
|
207
215
|
# ── D-Bus Properties ──
|
|
208
216
|
|
|
@@ -160,9 +160,10 @@ class TrayApp:
|
|
|
160
160
|
|
|
161
161
|
return True
|
|
162
162
|
|
|
163
|
-
def update(self):
|
|
163
|
+
def update(self, force_stats=False):
|
|
164
164
|
"""Read observer state and update tray display."""
|
|
165
165
|
obs = self._observer
|
|
166
|
+
now = time.monotonic()
|
|
166
167
|
|
|
167
168
|
# Determine status
|
|
168
169
|
if obs._paused:
|
|
@@ -178,18 +179,17 @@ class TrayApp:
|
|
|
178
179
|
if obs._paused or obs.segment_dir is None:
|
|
179
180
|
segment_timer = 0
|
|
180
181
|
else:
|
|
181
|
-
remaining = obs.interval - (
|
|
182
|
+
remaining = obs.interval - (now - obs.start_at_mono)
|
|
182
183
|
segment_timer = max(0, int(remaining))
|
|
183
184
|
|
|
184
185
|
# Pause remaining
|
|
185
186
|
if not obs._paused or obs._pause_until <= 0:
|
|
186
187
|
pause_remaining = 0
|
|
187
188
|
else:
|
|
188
|
-
pause_remaining = max(0, int(obs._pause_until -
|
|
189
|
+
pause_remaining = max(0, int(obs._pause_until - now))
|
|
189
190
|
|
|
190
191
|
# Compute stats (throttled — filesystem walk every 60s)
|
|
191
|
-
now
|
|
192
|
-
if now - self._last_stats_time >= 60:
|
|
192
|
+
if force_stats or now - self._last_stats_time >= 60:
|
|
193
193
|
self._last_stats_time = now
|
|
194
194
|
captures_today = 0
|
|
195
195
|
total_size = 0
|
|
@@ -220,7 +220,7 @@ class TrayApp:
|
|
|
220
220
|
pass
|
|
221
221
|
|
|
222
222
|
total_size_mb = int(total_size / (1024 * 1024))
|
|
223
|
-
uptime_seconds = int(
|
|
223
|
+
uptime_seconds = int(now - obs._start_mono)
|
|
224
224
|
|
|
225
225
|
self.stats = {
|
|
226
226
|
"captures_today": captures_today,
|
|
@@ -234,6 +234,19 @@ class TrayApp:
|
|
|
234
234
|
self._update_live_stats(segment_timer, pause_remaining)
|
|
235
235
|
self.paused_remaining = pause_remaining
|
|
236
236
|
|
|
237
|
+
def _on_about_to_show(self) -> bool:
|
|
238
|
+
"""Full recompute on menu open; returns True if any item changed.
|
|
239
|
+
|
|
240
|
+
Runs outside _refresh_tray so a failure here never tears down the tray.
|
|
241
|
+
"""
|
|
242
|
+
before = self.menu._props_emitted
|
|
243
|
+
try:
|
|
244
|
+
self.update(force_stats=True)
|
|
245
|
+
except Exception:
|
|
246
|
+
log.warning("Tray on-open recompute failed", exc_info=True)
|
|
247
|
+
return False
|
|
248
|
+
return self.menu._props_emitted > before
|
|
249
|
+
|
|
237
250
|
def _build_menu(self):
|
|
238
251
|
"""Build the full tray menu structure."""
|
|
239
252
|
|
|
@@ -361,6 +374,7 @@ class TrayApp:
|
|
|
361
374
|
service_hint,
|
|
362
375
|
]
|
|
363
376
|
)
|
|
377
|
+
self.menu.on_about_to_show = self._on_about_to_show
|
|
364
378
|
|
|
365
379
|
def _icon_for_health(self, status: str, health: SyncHealth) -> str:
|
|
366
380
|
if self.error:
|
|
@@ -420,6 +434,7 @@ class TrayApp:
|
|
|
420
434
|
self._status_header.label = label
|
|
421
435
|
self._status_item.label = label
|
|
422
436
|
self.menu.update_properties(self._status_header, "label")
|
|
437
|
+
self.menu.update_properties(self._status_item, "label")
|
|
423
438
|
|
|
424
439
|
def _update_sync(self, health: SyncHealth):
|
|
425
440
|
"""Update sync status display."""
|
|
@@ -427,6 +442,7 @@ class TrayApp:
|
|
|
427
442
|
return
|
|
428
443
|
self.health = health
|
|
429
444
|
self._sync_item.label = health.sync_line
|
|
445
|
+
self.menu.update_properties(self._sync_item, "label")
|
|
430
446
|
|
|
431
447
|
if not self.error:
|
|
432
448
|
self.sni.set_icon(self._icon_for_health(self.status, health))
|
|
@@ -446,6 +462,7 @@ class TrayApp:
|
|
|
446
462
|
new_label = f"segment: {mins}:{secs:02d} remaining"
|
|
447
463
|
if self._segment_item.label != new_label:
|
|
448
464
|
self._segment_item.label = new_label
|
|
465
|
+
self.menu.update_properties(self._segment_item, "label")
|
|
449
466
|
|
|
450
467
|
# Stats (computed in update())
|
|
451
468
|
if self.stats:
|
|
@@ -462,10 +479,13 @@ class TrayApp:
|
|
|
462
479
|
|
|
463
480
|
if self._cache_item.label != new_cache:
|
|
464
481
|
self._cache_item.label = new_cache
|
|
482
|
+
self.menu.update_properties(self._cache_item, "label")
|
|
465
483
|
if self._captures_item.label != new_captures:
|
|
466
484
|
self._captures_item.label = new_captures
|
|
485
|
+
self.menu.update_properties(self._captures_item, "label")
|
|
467
486
|
if self._uptime_item.label != new_uptime:
|
|
468
487
|
self._uptime_item.label = new_uptime
|
|
488
|
+
self.menu.update_properties(self._uptime_item, "label")
|
|
469
489
|
|
|
470
490
|
# Update pause remaining in resume button
|
|
471
491
|
if self.status == "paused" and pause_remaining > 0:
|
|
@@ -473,6 +493,7 @@ class TrayApp:
|
|
|
473
493
|
new_resume = f"resume ({pr_mins}m remaining)"
|
|
474
494
|
if self._resume_item.label != new_resume:
|
|
475
495
|
self._resume_item.label = new_resume
|
|
496
|
+
self.menu.update_properties(self._resume_item, "label")
|
|
476
497
|
|
|
477
498
|
def _build_tooltip(self, health: SyncHealth | None = None) -> str:
|
|
478
499
|
"""Build plain-text tooltip body (cross-DE compatible)."""
|
|
@@ -55,6 +55,7 @@ def test_update_properties_emits_items_properties_updated():
|
|
|
55
55
|
menu.ItemsPropertiesUpdated.assert_called_once()
|
|
56
56
|
menu.LayoutUpdated.assert_not_called()
|
|
57
57
|
assert menu._revision == revision
|
|
58
|
+
assert menu._props_emitted == 1
|
|
58
59
|
|
|
59
60
|
updated_props, removed_props = menu.ItemsPropertiesUpdated.call_args.args
|
|
60
61
|
assert removed_props == []
|
|
@@ -79,9 +80,29 @@ def test_update_properties_noop_when_no_names():
|
|
|
79
80
|
menu.ItemsPropertiesUpdated.assert_not_called()
|
|
80
81
|
menu.LayoutUpdated.assert_not_called()
|
|
81
82
|
assert menu._revision == revision
|
|
83
|
+
assert menu._props_emitted == 0
|
|
82
84
|
|
|
83
85
|
|
|
84
|
-
def
|
|
86
|
+
def test_about_to_show_uses_optional_hook():
|
|
85
87
|
menu = DBusMenu()
|
|
86
88
|
|
|
87
89
|
assert DBusMenu.AboutToShow.__wrapped__(menu, 0) is False
|
|
90
|
+
|
|
91
|
+
menu.on_about_to_show = lambda: True
|
|
92
|
+
assert DBusMenu.AboutToShow.__wrapped__(menu, 0) is True
|
|
93
|
+
|
|
94
|
+
menu.on_about_to_show = lambda: False
|
|
95
|
+
assert DBusMenu.AboutToShow.__wrapped__(menu, 0) is False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_about_to_show_group_uses_optional_hook():
|
|
99
|
+
menu = DBusMenu()
|
|
100
|
+
ids = [1, 2, 3]
|
|
101
|
+
|
|
102
|
+
assert DBusMenu.AboutToShowGroup.__wrapped__(menu, ids) == [[], []]
|
|
103
|
+
|
|
104
|
+
menu.on_about_to_show = lambda: True
|
|
105
|
+
assert DBusMenu.AboutToShowGroup.__wrapped__(menu, ids) == [ids, []]
|
|
106
|
+
|
|
107
|
+
menu.on_about_to_show = lambda: False
|
|
108
|
+
assert DBusMenu.AboutToShowGroup.__wrapped__(menu, ids) == [[], []]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
# Copyright (c) 2026 sol pbc
|
|
3
3
|
|
|
4
4
|
import time
|
|
5
|
+
from datetime import datetime
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from unittest.mock import call
|
|
7
8
|
from unittest.mock import MagicMock
|
|
@@ -10,7 +11,7 @@ from unittest.mock import patch
|
|
|
10
11
|
import pytest
|
|
11
12
|
|
|
12
13
|
from solstone_linux.config import Config
|
|
13
|
-
from solstone_linux.dbusmenu import MenuItem, separator
|
|
14
|
+
from solstone_linux.dbusmenu import DBusMenu, MenuItem, separator
|
|
14
15
|
from solstone_linux.sni import StatusNotifierItem
|
|
15
16
|
from solstone_linux.sync_health import ErrorType, HealthState, SyncFacts, derive_health
|
|
16
17
|
from solstone_linux.tray import (
|
|
@@ -60,6 +61,28 @@ def _offline_health():
|
|
|
60
61
|
return _health(SyncFacts(last_error_class=ErrorType.TRANSIENT))
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
def _create_capture_segment(app, size=1024 * 1024):
|
|
65
|
+
today = datetime.now().strftime("%Y%m%d")
|
|
66
|
+
segment_dir = app.config.captures_dir / today / "test-stream" / "120000_300"
|
|
67
|
+
segment_dir.mkdir(parents=True)
|
|
68
|
+
(segment_dir / "screen.mp4").write_bytes(b"x" * size)
|
|
69
|
+
return segment_dir
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _prepare_open_refresh_state(app, now):
|
|
73
|
+
segment_dir = _create_capture_segment(app)
|
|
74
|
+
app._observer.current_mode = "screencast"
|
|
75
|
+
app._observer._paused = False
|
|
76
|
+
app._observer.segment_dir = segment_dir
|
|
77
|
+
app._observer.start_at_mono = now - 75
|
|
78
|
+
app._observer._start_mono = now - 3661
|
|
79
|
+
app._observer.interval = 300
|
|
80
|
+
app._observer._sync = MagicMock()
|
|
81
|
+
app._observer._sync.health = _connected_health()
|
|
82
|
+
app._last_stats_time = now - 10
|
|
83
|
+
return segment_dir
|
|
84
|
+
|
|
85
|
+
|
|
63
86
|
class TestResolveIconThemePath:
|
|
64
87
|
def test_resolve_icon_theme_path_prefers_installed(self, tmp_path):
|
|
65
88
|
installed_icon = (
|
|
@@ -158,6 +181,22 @@ class TestUpdateStatus:
|
|
|
158
181
|
|
|
159
182
|
|
|
160
183
|
class TestUpdateSync:
|
|
184
|
+
def test_update_sync_signals_label_change_only_once(self):
|
|
185
|
+
app = _make_app()
|
|
186
|
+
app._build_menu()
|
|
187
|
+
app.menu.update_properties = MagicMock()
|
|
188
|
+
health = _connected_health()
|
|
189
|
+
|
|
190
|
+
app._update_sync(health)
|
|
191
|
+
|
|
192
|
+
app.menu.update_properties.assert_called_once_with(app._sync_item, "label")
|
|
193
|
+
|
|
194
|
+
app.menu.update_properties.reset_mock()
|
|
195
|
+
|
|
196
|
+
app._update_sync(health)
|
|
197
|
+
|
|
198
|
+
app.menu.update_properties.assert_not_called()
|
|
199
|
+
|
|
161
200
|
def test_update_sync_synced(self):
|
|
162
201
|
app = _make_app()
|
|
163
202
|
app._build_menu()
|
|
@@ -223,12 +262,40 @@ class TestUpdateLiveStats:
|
|
|
223
262
|
}
|
|
224
263
|
|
|
225
264
|
app._update_live_stats(245, 0)
|
|
265
|
+
|
|
266
|
+
assert app.menu.update_properties.call_args_list == [
|
|
267
|
+
call(app._segment_item, "label"),
|
|
268
|
+
call(app._cache_item, "label"),
|
|
269
|
+
call(app._captures_item, "label"),
|
|
270
|
+
call(app._uptime_item, "label"),
|
|
271
|
+
]
|
|
272
|
+
|
|
226
273
|
app.menu.update_properties.reset_mock()
|
|
227
274
|
|
|
228
275
|
app._update_live_stats(245, 0)
|
|
229
276
|
|
|
230
277
|
app.menu.update_properties.assert_not_called()
|
|
231
278
|
|
|
279
|
+
def test_update_live_stats_signals_resume_countdown_change_only_once(self):
|
|
280
|
+
app = _make_app()
|
|
281
|
+
app._build_menu()
|
|
282
|
+
app.status = "paused"
|
|
283
|
+
app._segment_item.label = "segment: 0:00 remaining"
|
|
284
|
+
app.menu.update_properties = MagicMock()
|
|
285
|
+
|
|
286
|
+
app._update_live_stats(0, 600)
|
|
287
|
+
|
|
288
|
+
app.menu.update_properties.assert_called_once_with(
|
|
289
|
+
app._resume_item,
|
|
290
|
+
"label",
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
app.menu.update_properties.reset_mock()
|
|
294
|
+
|
|
295
|
+
app._update_live_stats(0, 600)
|
|
296
|
+
|
|
297
|
+
app.menu.update_properties.assert_not_called()
|
|
298
|
+
|
|
232
299
|
|
|
233
300
|
class TestHeaderLabel:
|
|
234
301
|
def test_update_header_emits_label_property_update(self):
|
|
@@ -243,8 +310,21 @@ class TestHeaderLabel:
|
|
|
243
310
|
|
|
244
311
|
app._update_header(0, _connected_health())
|
|
245
312
|
|
|
246
|
-
|
|
313
|
+
assert (
|
|
314
|
+
call(app._status_header, "label")
|
|
315
|
+
in app.menu.update_properties.call_args_list
|
|
316
|
+
)
|
|
317
|
+
assert (
|
|
318
|
+
call(app._status_item, "label") in app.menu.update_properties.call_args_list
|
|
319
|
+
)
|
|
247
320
|
assert app._status_header.label == "observing — connected"
|
|
321
|
+
assert app._status_item.label == "observing — connected"
|
|
322
|
+
|
|
323
|
+
app.menu.update_properties.reset_mock()
|
|
324
|
+
|
|
325
|
+
app._update_header(0, _connected_health())
|
|
326
|
+
|
|
327
|
+
app.menu.update_properties.assert_not_called()
|
|
248
328
|
|
|
249
329
|
def test_header_recording_connected(self):
|
|
250
330
|
app = _make_app()
|
|
@@ -354,6 +434,73 @@ class TestStatusNotifierItem:
|
|
|
354
434
|
|
|
355
435
|
|
|
356
436
|
class TestUpdate:
|
|
437
|
+
def test_on_about_to_show_forces_recompute(self, tmp_path):
|
|
438
|
+
app = _make_app(tmp_path)
|
|
439
|
+
app._build_menu()
|
|
440
|
+
now = 10_000.0
|
|
441
|
+
_prepare_open_refresh_state(app, now)
|
|
442
|
+
|
|
443
|
+
with patch("solstone_linux.tray.time.monotonic", return_value=now):
|
|
444
|
+
changed = app._on_about_to_show()
|
|
445
|
+
|
|
446
|
+
assert changed is True
|
|
447
|
+
assert app.stats == {
|
|
448
|
+
"captures_today": 1,
|
|
449
|
+
"total_size_mb": 1,
|
|
450
|
+
"uptime_seconds": 3661,
|
|
451
|
+
}
|
|
452
|
+
assert app._segment_item.label == "segment: 3:45 remaining"
|
|
453
|
+
assert app._cache_item.label == "cache: 1 MB"
|
|
454
|
+
assert app._captures_item.label == "captures today: 1 segments"
|
|
455
|
+
assert app._uptime_item.label == "uptime: 1h 1m"
|
|
456
|
+
assert app._sync_item.label == "sync: up to date"
|
|
457
|
+
assert app._status_item.label == "observing — connected"
|
|
458
|
+
|
|
459
|
+
def test_about_to_show_returns_true_and_layout_has_refreshed_labels(self, tmp_path):
|
|
460
|
+
app = _make_app(tmp_path)
|
|
461
|
+
app._build_menu()
|
|
462
|
+
now = 10_000.0
|
|
463
|
+
_prepare_open_refresh_state(app, now)
|
|
464
|
+
|
|
465
|
+
with patch("solstone_linux.tray.time.monotonic", return_value=now):
|
|
466
|
+
assert DBusMenu.AboutToShow.__wrapped__(app.menu, 0) is True
|
|
467
|
+
|
|
468
|
+
row_items = [
|
|
469
|
+
app._status_item,
|
|
470
|
+
app._sync_item,
|
|
471
|
+
app._segment_item,
|
|
472
|
+
app._cache_item,
|
|
473
|
+
app._captures_item,
|
|
474
|
+
app._uptime_item,
|
|
475
|
+
]
|
|
476
|
+
props_by_id = {
|
|
477
|
+
item_id: props
|
|
478
|
+
for item_id, props in DBusMenu.GetGroupProperties.__wrapped__(
|
|
479
|
+
app.menu,
|
|
480
|
+
[item.id for item in row_items],
|
|
481
|
+
[],
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for item in row_items:
|
|
486
|
+
assert props_by_id[item.id]["label"].value == item.label
|
|
487
|
+
|
|
488
|
+
def test_on_about_to_show_failure_keeps_tray_and_last_known_layout(self):
|
|
489
|
+
app = _make_app()
|
|
490
|
+
app._build_menu()
|
|
491
|
+
app._observer._tray = app
|
|
492
|
+
app.update = MagicMock(side_effect=RuntimeError("boom"))
|
|
493
|
+
|
|
494
|
+
assert app._on_about_to_show() is False
|
|
495
|
+
assert app._observer._tray is app
|
|
496
|
+
|
|
497
|
+
props = DBusMenu.GetGroupProperties.__wrapped__(
|
|
498
|
+
app.menu,
|
|
499
|
+
[app._status_item.id],
|
|
500
|
+
[],
|
|
501
|
+
)
|
|
502
|
+
assert props[0][1]["label"].value == "observing"
|
|
503
|
+
|
|
357
504
|
def test_first_update_clears_starting_tooltip(self):
|
|
358
505
|
"""Tray tooltip must not stay on 'starting...' after first update."""
|
|
359
506
|
app = _make_app()
|
|
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
|
{solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_observer_emits_stream_silent_event.py
RENAMED
|
File without changes
|
|
File without changes
|
{solstone_linux-0.3.2 → solstone_linux-0.3.3}/tests/test_screencast_stop_filters_silent_streams.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|