solstone-linux 0.4.2__tar.gz → 0.4.4__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.4.2 → solstone_linux-0.4.4}/AGENTS.md +11 -1
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/CHANGELOG.md +17 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/INSTALL.md +11 -2
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/Makefile +1 -1
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/PKG-INFO +3 -3
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/README.md +1 -1
- solstone_linux-0.4.4/contrib/icons/hicolor/128x128/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/contrib/icons/hicolor/16x16/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/contrib/icons/hicolor/24x24/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/contrib/icons/hicolor/256x256/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/contrib/icons/hicolor/32x32/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/contrib/icons/hicolor/48x48/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/contrib/icons/hicolor/512x512/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/contrib/icons/hicolor/64x64/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/contrib/icons/hicolor/scalable/apps/solstone-observer.svg +6 -6
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/contrib/icons/hicolor/scalable/status/solstone-error.svg +1 -1
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/contrib/icons/hicolor/scalable/status/solstone-paused.svg +1 -1
- {solstone_linux-0.4.2/src/solstone_linux → solstone_linux-0.4.4/contrib}/icons/hicolor/scalable/status/solstone-recording.svg +1 -1
- {solstone_linux-0.4.2/src/solstone_linux → solstone_linux-0.4.4/contrib}/icons/hicolor/scalable/status/solstone-syncing.svg +5 -5
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/pyproject.toml +2 -2
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/scripts/release.sh +10 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/__init__.py +1 -1
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/activity.py +65 -36
- solstone_linux-0.4.4/src/solstone_linux/audio_detect.py +66 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/audio_recorder.py +59 -4
- solstone_linux-0.4.4/src/solstone_linux/capture_stats.py +88 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/chat_bridge.py +122 -100
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/cli.py +19 -19
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/config.py +41 -16
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/dbus_service.py +5 -34
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/dbusmenu.py +3 -3
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/doctor.py +13 -3
- solstone_linux-0.4.4/src/solstone_linux/event_sender.py +123 -0
- solstone_linux-0.4.4/src/solstone_linux/icons/hicolor/128x128/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/src/solstone_linux/icons/hicolor/16x16/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/src/solstone_linux/icons/hicolor/24x24/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/src/solstone_linux/icons/hicolor/256x256/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/src/solstone_linux/icons/hicolor/32x32/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/src/solstone_linux/icons/hicolor/48x48/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/src/solstone_linux/icons/hicolor/512x512/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.4/src/solstone_linux/icons/hicolor/64x64/apps/solstone-observer.png +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/scalable/apps/solstone-observer.svg +6 -6
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/scalable/status/solstone-error.svg +1 -1
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/icons/hicolor/scalable/status/solstone-paused.svg +1 -1
- {solstone_linux-0.4.2/contrib → solstone_linux-0.4.4/src/solstone_linux}/icons/hicolor/scalable/status/solstone-recording.svg +1 -1
- {solstone_linux-0.4.2/contrib → solstone_linux-0.4.4/src/solstone_linux}/icons/hicolor/scalable/status/solstone-syncing.svg +5 -5
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/observer.py +78 -70
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/recovery.py +36 -5
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/screencast.py +209 -54
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/sni.py +7 -7
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/solstone-linux.service.in +3 -2
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/sync.py +191 -20
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/sync_health.py +21 -21
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/tray.py +24 -59
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/upload.py +78 -16
- solstone_linux-0.4.4/tests/fixtures/introspection/dbusmenu.xml +58 -0
- solstone_linux-0.4.4/tests/fixtures/introspection/observer1.xml +35 -0
- solstone_linux-0.4.4/tests/fixtures/introspection/status_notifier_item.xml +51 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_activity.py +114 -96
- solstone_linux-0.4.4/tests/test_audio_detect.py +75 -0
- solstone_linux-0.4.4/tests/test_audio_recorder.py +366 -0
- solstone_linux-0.4.4/tests/test_capture_stats.py +40 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_chat_bridge.py +283 -9
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_cli.py +55 -3
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_config.py +62 -1
- solstone_linux-0.4.4/tests/test_dbus_introspection.py +98 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_dbus_service.py +34 -18
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_dbusmenu.py +1 -1
- solstone_linux-0.4.4/tests/test_docs_mirror.py +57 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_doctor.py +39 -7
- solstone_linux-0.4.4/tests/test_event_sender.py +121 -0
- solstone_linux-0.4.4/tests/test_observer.py +575 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_observer_emits_stream_silent_event.py +5 -4
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_observer_health_beacon.py +15 -6
- solstone_linux-0.4.4/tests/test_screencast.py +837 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_sync.py +602 -43
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_sync_health_surfaces.py +7 -8
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_tray.py +37 -34
- solstone_linux-0.4.4/tests/test_upload.py +366 -0
- solstone_linux-0.4.4/tests/test_version_match.py +27 -0
- solstone_linux-0.4.2/contrib/icons/hicolor/128x128/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/contrib/icons/hicolor/16x16/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/contrib/icons/hicolor/24x24/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/contrib/icons/hicolor/256x256/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/contrib/icons/hicolor/32x32/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/contrib/icons/hicolor/48x48/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/contrib/icons/hicolor/512x512/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/contrib/icons/hicolor/64x64/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/src/solstone_linux/audio_detect.py +0 -79
- solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/128x128/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/16x16/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/24x24/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/256x256/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/32x32/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/48x48/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/512x512/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/src/solstone_linux/icons/hicolor/64x64/apps/solstone-observer.png +0 -0
- solstone_linux-0.4.2/tests/test_observer.py +0 -149
- solstone_linux-0.4.2/tests/test_screencast.py +0 -411
- solstone_linux-0.4.2/tests/test_upload.py +0 -137
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/.gitignore +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/CLAUDE.md +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/LICENSE +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/scripts/extract_changelog.sh +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/audio_mute.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/install_guard.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/monitor_positions.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/session_env.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/src/solstone_linux/streams.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/__init__.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/conftest.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_extract_changelog.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_install_guard.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_monitor_positions.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_screencast_stop_filters_silent_streams.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_session_env.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_streams.py +0 -0
- {solstone_linux-0.4.2 → solstone_linux-0.4.4}/tests/test_sync_health.py +0 -0
|
@@ -16,7 +16,10 @@ src/solstone_linux/
|
|
|
16
16
|
cli.py CLI entry point (run, setup, settings, install-service, status)
|
|
17
17
|
solstone-linux.service.in Systemd unit template (rendered by install-service)
|
|
18
18
|
config.py Config loading/persistence (config under ~/.config/solstone-linux/)
|
|
19
|
+
doctor.py Install prerequisite checks for the doctor command
|
|
20
|
+
install_guard.py Install ownership guard for pipx-managed service installs
|
|
19
21
|
observer.py Main capture loop — state machine (idle/screencast), audio + video
|
|
22
|
+
capture_stats.py Shared capture cache statistics
|
|
20
23
|
screencast.py Portal-based multi-monitor recording (xdg-desktop-portal + GStreamer)
|
|
21
24
|
audio_recorder.py Stereo audio recording (mic + system via soundcard)
|
|
22
25
|
audio_detect.py Audio device detection via ultrasonic tone
|
|
@@ -25,9 +28,16 @@ src/solstone_linux/
|
|
|
25
28
|
monitor_positions.py Monitor position assignment from geometry
|
|
26
29
|
session_env.py Desktop session environment checks and recovery
|
|
27
30
|
streams.py Stream name derivation (hostname-based)
|
|
31
|
+
event_sender.py Background sender for observer event relay
|
|
28
32
|
sync.py Background sync service — uploads completed segments to server
|
|
33
|
+
sync_health.py Sync health facts, derivation, persistence, and surface copy
|
|
29
34
|
upload.py HTTP upload client for solstone ingest server
|
|
30
35
|
recovery.py Crash recovery for orphaned .incomplete segments
|
|
36
|
+
chat_bridge.py Server-initiated chat event bridge to local notifications
|
|
37
|
+
dbus_service.py Observer status/control D-Bus service interface
|
|
38
|
+
dbusmenu.py D-Bus menu protocol implementation for tray menus
|
|
39
|
+
sni.py StatusNotifierItem D-Bus interface for tray icons
|
|
40
|
+
tray.py In-process D-Bus SNI tray icon, menu, and tooltip
|
|
31
41
|
|
|
32
42
|
tests/ pytest test suite
|
|
33
43
|
contrib/ Reference icons for development fallback
|
|
@@ -123,7 +133,7 @@ Python packages (in pyproject.toml):
|
|
|
123
133
|
- `numpy` — Audio buffer manipulation and RMS computation
|
|
124
134
|
- `soundfile` — FLAC encoding
|
|
125
135
|
- `soundcard` — Audio device enumeration and recording
|
|
126
|
-
- `dbus-
|
|
136
|
+
- `dbus-fast` — Async DBus client for portal and activity detection
|
|
127
137
|
- `PyGObject` — GDK monitor geometry (installed from system)
|
|
128
138
|
|
|
129
139
|
## Data Paths
|
|
@@ -4,6 +4,23 @@ 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.4.4] - 2026-07-04
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
- stopping or restarting sol now shuts down promptly, even with an upload mid-retry. that upload used to wait out its full retry delay first; now it ends the moment you quit sol or the machine powers off. a bad retry setting in your config no longer leaves the uploader stuck, either.
|
|
11
|
+
|
|
12
|
+
## [0.4.3] - 2026-07-03
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- the app now calls itself sol everywhere you see it — the launcher, tray, menus, status, and notifications. your journal is the memory it keeps, and solstone is the platform underneath. the command you run stays `solstone-linux`; nothing about what it does changed, only what it's called.
|
|
16
|
+
- segments your journal rejected or sol couldn't recover are now held for 30 days before they're removed, instead of being dropped silently. `status` and `doctor` show the count, so you can see when any are waiting.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- local cleanup now deletes a synced segment only after your journal confirms, file by file, that it holds everything in it. previously a segment could be cleaned up while your journal was missing part of it. if your journal can't yet confirm file by file, cleanup holds off and keeps the local copy.
|
|
20
|
+
- sol now recovers on its own from situations that used to leave it quietly stalled or stopped: the first screen-share dialog being dismissed, a journal that's slow to respond or offline, and speakers muted at startup (it goes on without audio and picks it back up when a device is available). an accidental second copy now declines to run rather than disturb the one already going. a round of smaller stability improvements rides along.
|
|
21
|
+
- closing the lid on a docked KDE laptop no longer makes sol go idle.
|
|
22
|
+
- chat notifications now come back on their own after a network drop, and dismissing a notification no longer counts as opening it.
|
|
23
|
+
|
|
7
24
|
## [0.4.2] - 2026-06-29
|
|
8
25
|
|
|
9
26
|
### Added
|
|
@@ -52,7 +52,7 @@ this is the developer/from-source path; most installs should use the `pipx insta
|
|
|
52
52
|
|
|
53
53
|
**arch:**
|
|
54
54
|
```
|
|
55
|
-
sudo pacman -S python-gobject gtk4 gstreamer gst-plugin-pipewire libpulse alsa-lib xdg-desktop-portal pipx
|
|
55
|
+
sudo pacman -S python-gobject gtk4 gstreamer gst-plugin-pipewire gst-plugins-good libpulse alsa-lib xdg-desktop-portal python-pipx uv python-cairo
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
**opensuse:**
|
|
@@ -88,6 +88,15 @@ this is the developer/from-source path; most installs should use the `pipx insta
|
|
|
88
88
|
systemctl --user status solstone-linux
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
## updating from PyPI
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
pipx upgrade solstone-linux
|
|
95
|
+
systemctl --user restart solstone-linux
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
the systemd unit template and icons only refresh when you re-run `solstone-linux install-service`, so after upgrading across a release that changed the unit, re-run `solstone-linux install-service`.
|
|
99
|
+
|
|
91
100
|
## updating after a code change
|
|
92
101
|
|
|
93
102
|
```
|
|
@@ -96,7 +105,7 @@ git pull && make install-service
|
|
|
96
105
|
|
|
97
106
|
## notes
|
|
98
107
|
|
|
99
|
-
-
|
|
108
|
+
- Activity detection uses screen-lock and power-save signals to notice when you step away. Coverage varies by desktop: GNOME provides both signals; KDE (Wayland) provides screen lock only; any X11 session also provides DPMS power save; other Wayland desktops provide screen lock where the compositor exposes it. Where neither signal is available, solstone-linux still experiences your screen and audio, but activity-based segment boundaries won't trigger.
|
|
100
109
|
- the tray icon uses the StatusNotifierItem (SNI) D-Bus protocol. it works on KDE natively and GNOME with the AppIndicator extension. if no SNI host is available, the observer runs normally without a tray icon.
|
|
101
110
|
|
|
102
111
|
## appendix: GNOME tray support
|
|
@@ -156,7 +156,7 @@ versions: .installed
|
|
|
156
156
|
$(PYTHON) --version
|
|
157
157
|
@echo ""
|
|
158
158
|
@echo "=== Installed packages ==="
|
|
159
|
-
@$(UV) pip list | grep -E "^(pytest|ruff|requests|numpy|soundfile|soundcard|dbus-
|
|
159
|
+
@$(UV) pip list | grep -E "^(pytest|ruff|requests|numpy|soundfile|soundcard|dbus-fast|PyGObject)" || true
|
|
160
160
|
|
|
161
161
|
release: ## Publish solstone-linux to PyPI (production)
|
|
162
162
|
@bash scripts/release.sh
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: solstone-linux
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Standalone Linux desktop observer for solstone
|
|
5
5
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.10
|
|
8
|
-
Requires-Dist: dbus-
|
|
8
|
+
Requires-Dist: dbus-fast>=5.0
|
|
9
9
|
Requires-Dist: numpy
|
|
10
10
|
Requires-Dist: pygobject
|
|
11
11
|
Requires-Dist: requests
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
|
|
18
18
|
Standalone Linux desktop observer for [solstone](https://solpbc.org). Experiences your screen and audio along with you on a GNOME Wayland session, stores segments locally, and syncs to your solstone journal.
|
|
19
19
|
|
|
20
|
-
**Note:** Activity detection (
|
|
20
|
+
**Note:** Activity detection uses screen-lock and power-save signals to notice when you step away. Coverage varies by desktop: GNOME provides both signals; KDE (Wayland) provides screen lock only; any X11 session also provides DPMS power save; other Wayland desktops provide screen lock where the compositor exposes it. Where neither signal is available, solstone-linux still experiences your screen and audio, but activity-based segment boundaries won't trigger.
|
|
21
21
|
|
|
22
22
|
## System Dependencies
|
|
23
23
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Standalone Linux desktop observer for [solstone](https://solpbc.org). Experiences your screen and audio along with you on a GNOME Wayland session, stores segments locally, and syncs to your solstone journal.
|
|
4
4
|
|
|
5
|
-
**Note:** Activity detection (
|
|
5
|
+
**Note:** Activity detection uses screen-lock and power-save signals to notice when you step away. Coverage varies by desktop: GNOME provides both signals; KDE (Wayland) provides screen lock only; any X11 session also provides DPMS power save; other Wayland desktops provide screen lock where the compositor exposes it. Where neither signal is available, solstone-linux still experiences your screen and audio, but activity-based segment boundaries won't trigger.
|
|
6
6
|
|
|
7
7
|
## System Dependencies
|
|
8
8
|
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
<!-- canonical sol app icon (transparent) — UNIFIED WORDMARK direction (locked 2026-06-25, founder-approved).
|
|
4
4
|
mark: the sol wordmark (sun ring + "sol" Comfortaa Bold) from sol-wordmark.svg, at full extents.
|
|
5
5
|
ground: NONE (transparent), open center — the background shows through the ring.
|
|
6
|
-
color: #
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
color: #E8923A sol orange (ring + "sol" glyph) + #FFCF33 sol gold rays — one recipe on every ground.
|
|
7
|
+
the mark is a logotype (WCAG-exempt), so the brand orange holds on light and dark alike
|
|
8
|
+
(palette simplification 2026-06-29, records/decisions/260629-cmo-sol-mark-palette-simplification.md).
|
|
9
9
|
geometry: identical mark + scale (1.185x, 35.9529) to sol-app-icon-cream.svg; only the ground differs.
|
|
10
10
|
use: Windows + Linux (and any controlled/dark surface — docs, dark UI, marketing on dark).
|
|
11
11
|
decision: records/decisions/260625-cmo-sol-app-icon-unified-wordmark.md
|
|
12
12
|
RENDERING RULE: render from this SVG at every target size; never downsample. -->
|
|
13
13
|
<g transform="translate(512,512) scale(35.9529) translate(-16,-16)">
|
|
14
|
-
<path fill="#
|
|
15
|
-
<circle cx="16" cy="16" r="8.0" fill="none" stroke="#
|
|
16
|
-
<path fill="#
|
|
14
|
+
<path fill="#FFCF33" d="M16.0 2.5 L18.6 7.3 A9.1 9.1 0 0 0 13.4 7.3 Z M23.9 5.1 L23.2 10.5 A9.1 9.1 0 0 0 19.0 7.4 Z M28.8 11.8 L25.1 15.8 A9.1 9.1 0 0 0 23.5 10.9 Z M28.8 20.2 L23.5 21.1 A9.1 9.1 0 0 0 25.1 16.2 Z M23.9 26.9 L19.0 24.6 A9.1 9.1 0 0 0 23.2 21.5 Z M16.0 29.5 L13.4 24.7 A9.1 9.1 0 0 0 18.6 24.7 Z M8.1 26.9 L8.8 21.5 A9.1 9.1 0 0 0 13.0 24.6 Z M3.2 20.2 L6.9 16.2 A9.1 9.1 0 0 0 8.5 21.1 Z M3.2 11.8 L8.5 10.9 A9.1 9.1 0 0 0 6.9 15.8 Z M8.1 5.1 L13.0 7.4 A9.1 9.1 0 0 0 8.8 10.5 Z"/>
|
|
15
|
+
<circle cx="16" cy="16" r="8.0" fill="none" stroke="#E8923A" stroke-width="1.2"/>
|
|
16
|
+
<path fill="#E8923A" fill-rule="evenodd" d="M12.079 18.795C13.489 18.795 14.229 18.065 14.229 17.155C14.229 16.365 13.729 15.835 12.229 15.535C11.149 15.315 10.939 15.095 10.939 14.725C10.939 14.345 11.399 14.135 11.989 14.135C12.499 14.135 12.859 14.235 13.199 14.555C13.399 14.745 13.729 14.815 13.949 14.665C14.159 14.505 14.169 14.255 13.989 14.035C13.589 13.545 12.889 13.245 12.009 13.245C10.989 13.245 9.959 13.735 9.959 14.755C9.959 15.525 10.529 16.075 11.879 16.335C12.919 16.525 13.249 16.815 13.239 17.215C13.229 17.615 12.809 17.895 12.039 17.895C11.429 17.895 10.889 17.625 10.659 17.375C10.469 17.175 10.189 17.125 9.929 17.335C9.699 17.515 9.659 17.825 9.859 18.035C10.299 18.475 11.149 18.795 12.079 18.795Z M16.999 18.795C18.609 18.795 19.749 17.645 19.749 16.025C19.739 14.395 18.599 13.245 16.999 13.245C15.379 13.245 14.239 14.395 14.239 16.025C14.239 17.645 15.379 18.795 16.999 18.795ZM16.999 17.895C15.959 17.895 15.219 17.125 15.219 16.025C15.219 14.925 15.959 14.145 16.999 14.145C18.039 14.145 18.769 14.925 18.769 16.025C18.769 17.125 18.039 17.895 16.999 17.895Z M21.569 18.755H21.589C21.989 18.755 22.269 18.545 22.269 18.255C22.269 17.965 22.079 17.755 21.819 17.755H21.569C21.279 17.755 21.069 17.405 21.069 16.905V11.445C21.069 11.155 20.859 10.945 20.569 10.945C20.279 10.945 20.069 11.155 20.069 11.445V16.905C20.069 17.985 20.689 18.755 21.569 18.755Z"/>
|
|
17
17
|
</g>
|
|
18
18
|
</svg>
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</mask>
|
|
10
10
|
</defs>
|
|
11
11
|
<g mask="url(#ixko)">
|
|
12
|
-
<path fill="#
|
|
12
|
+
<path fill="#FFCF33" d="M16.0 2.5 L18.6 7.3 A9.1 9.1 0 0 0 13.4 7.3 Z M23.9 5.1 L23.2 10.5 A9.1 9.1 0 0 0 19.0 7.4 Z M28.8 11.8 L25.1 15.8 A9.1 9.1 0 0 0 23.5 10.9 Z M28.8 20.2 L23.5 21.1 A9.1 9.1 0 0 0 25.1 16.2 Z M23.9 26.9 L19.0 24.6 A9.1 9.1 0 0 0 23.2 21.5 Z M16.0 29.5 L13.4 24.7 A9.1 9.1 0 0 0 18.6 24.7 Z M8.1 26.9 L8.8 21.5 A9.1 9.1 0 0 0 13.0 24.6 Z M3.2 20.2 L6.9 16.2 A9.1 9.1 0 0 0 8.5 21.1 Z M3.2 11.8 L8.5 10.9 A9.1 9.1 0 0 0 6.9 15.8 Z M8.1 5.1 L13.0 7.4 A9.1 9.1 0 0 0 8.8 10.5 Z"/>
|
|
13
13
|
<circle cx="16.0" cy="16.0" r="6.5" fill="none" stroke="#E8923A" stroke-width="1.7"/>
|
|
14
14
|
</g>
|
|
15
15
|
<path d="M7 7 L25 25 M25 7 L7 25" fill="none" stroke="#E8923A"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</mask>
|
|
10
10
|
</defs>
|
|
11
11
|
<g mask="url(#icko)">
|
|
12
|
-
<path fill="#
|
|
12
|
+
<path fill="#FFCF33" d="M16.0 2.5 L18.6 7.3 A9.1 9.1 0 0 0 13.4 7.3 Z M23.9 5.1 L23.2 10.5 A9.1 9.1 0 0 0 19.0 7.4 Z M28.8 11.8 L25.1 15.8 A9.1 9.1 0 0 0 23.5 10.9 Z M28.8 20.2 L23.5 21.1 A9.1 9.1 0 0 0 25.1 16.2 Z M23.9 26.9 L19.0 24.6 A9.1 9.1 0 0 0 23.2 21.5 Z M16.0 29.5 L13.4 24.7 A9.1 9.1 0 0 0 18.6 24.7 Z M8.1 26.9 L8.8 21.5 A9.1 9.1 0 0 0 13.0 24.6 Z M3.2 20.2 L6.9 16.2 A9.1 9.1 0 0 0 8.5 21.1 Z M3.2 11.8 L8.5 10.9 A9.1 9.1 0 0 0 6.9 15.8 Z M8.1 5.1 L13.0 7.4 A9.1 9.1 0 0 0 8.8 10.5 Z"/>
|
|
13
13
|
<circle cx="16.0" cy="16.0" r="6.5" fill="none" stroke="#E8923A" stroke-width="1.7"/>
|
|
14
14
|
</g>
|
|
15
15
|
<path d="M 8.50 21.50 A 4.5 4.5 0 0 1 12.02 18.00 A 5.2 5.2 0 0 1 21.35 13.65 A 4.0 4.0 0 0 1 26.91 18.89 C 28 22, 27.5 25.5, 23 25.5 C 20 28.8, 16 28.8, 13 25.5 C 10 26.5, 8 23.5, 8.50 21.50 Z" fill="none" stroke="#999"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="2.5 2.5 27 27" role="img" aria-label="solstone icon observing state">
|
|
2
2
|
<title>solstone — observing</title>
|
|
3
3
|
<!-- Sun rays: 10 floating wedges with curved inner arc matching the ring -->
|
|
4
|
-
<path fill="#
|
|
4
|
+
<path fill="#FFCF33" d="M16.0 2.5 L18.6 7.3 A9.1 9.1 0 0 0 13.4 7.3 Z M23.9 5.1 L23.2 10.5 A9.1 9.1 0 0 0 19.0 7.4 Z M28.8 11.8 L25.1 15.8 A9.1 9.1 0 0 0 23.5 10.9 Z M28.8 20.2 L23.5 21.1 A9.1 9.1 0 0 0 25.1 16.2 Z M23.9 26.9 L19.0 24.6 A9.1 9.1 0 0 0 23.2 21.5 Z M16.0 29.5 L13.4 24.7 A9.1 9.1 0 0 0 18.6 24.7 Z M8.1 26.9 L8.8 21.5 A9.1 9.1 0 0 0 13.0 24.6 Z M3.2 20.2 L6.9 16.2 A9.1 9.1 0 0 0 8.5 21.1 Z M3.2 11.8 L8.5 10.9 A9.1 9.1 0 0 0 6.9 15.8 Z M8.1 5.1 L13.0 7.4 A9.1 9.1 0 0 0 8.8 10.5 Z"/>
|
|
5
5
|
<!-- Sun ring: open annulus -->
|
|
6
6
|
<circle cx="16" cy="16" r="6.5" fill="none" stroke="#E8923A" stroke-width="1.7"/>
|
|
7
7
|
</svg>
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
role="img" aria-label="solstone icon syncing state">
|
|
3
3
|
<title>solstone — syncing</title>
|
|
4
4
|
<!-- Top 5 rays only (no clip path needed) -->
|
|
5
|
-
<path fill="#
|
|
6
|
-
<path fill="#
|
|
7
|
-
<path fill="#
|
|
8
|
-
<path fill="#
|
|
9
|
-
<path fill="#
|
|
5
|
+
<path fill="#FFCF33" d="M16.0 2.5 L18.6 7.3 A9.1 9.1 0 0 0 13.4 7.3 Z"/>
|
|
6
|
+
<path fill="#FFCF33" d="M23.9 5.1 L23.2 10.5 A9.1 9.1 0 0 0 19.0 7.4 Z"/>
|
|
7
|
+
<path fill="#FFCF33" d="M28.8 11.8 L25.1 15.8 A9.1 9.1 0 0 0 23.5 10.9 Z"/>
|
|
8
|
+
<path fill="#FFCF33" d="M3.2 11.8 L8.5 10.9 A9.1 9.1 0 0 0 6.9 15.8 Z"/>
|
|
9
|
+
<path fill="#FFCF33" d="M8.1 5.1 L13.0 7.4 A9.1 9.1 0 0 0 8.8 10.5 Z"/>
|
|
10
10
|
<!-- Top arc of the ring -->
|
|
11
11
|
<path fill="none" stroke="#E8923A" stroke-width="1.7"
|
|
12
12
|
d="M 9.5 16.0 A 6.5 6.5 0 0 1 22.5 16.0"/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "solstone-linux"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.4"
|
|
4
4
|
description = "Standalone Linux desktop observer for solstone"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "AGPL-3.0-only"
|
|
@@ -10,7 +10,7 @@ dependencies = [
|
|
|
10
10
|
"numpy",
|
|
11
11
|
"soundfile",
|
|
12
12
|
"soundcard",
|
|
13
|
-
"dbus-
|
|
13
|
+
"dbus-fast>=5.0",
|
|
14
14
|
"PyGObject",
|
|
15
15
|
]
|
|
16
16
|
|
|
@@ -75,6 +75,16 @@ SDIST_NAME=$(basename "${SDISTS[0]}")
|
|
|
75
75
|
VERSION="${SDIST_NAME#solstone_linux-}"
|
|
76
76
|
VERSION="${VERSION%.tar.gz}"
|
|
77
77
|
|
|
78
|
+
INIT_VERSION=$(grep -E '^__version__ = "[^"]+"' src/solstone_linux/__init__.py | sed 's/^__version__ = "\([^"]*\)".*/\1/' || true)
|
|
79
|
+
if [[ -z "$INIT_VERSION" ]]; then
|
|
80
|
+
echo "error: could not read __version__ from src/solstone_linux/__init__.py" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
if [[ "$INIT_VERSION" != "$VERSION" ]]; then
|
|
84
|
+
echo "error: version mismatch: sdist version ${VERSION}, src/solstone_linux/__init__.py __version__ ${INIT_VERSION}" >&2
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
78
88
|
uvx twine check dist/*
|
|
79
89
|
|
|
80
90
|
# Pre-flight: verify the CHANGELOG block exists before publishing.
|
|
@@ -16,9 +16,9 @@ import re
|
|
|
16
16
|
import shutil
|
|
17
17
|
import subprocess
|
|
18
18
|
|
|
19
|
-
from
|
|
20
|
-
from
|
|
21
|
-
from
|
|
19
|
+
from dbus_fast import Variant
|
|
20
|
+
from dbus_fast.aio import MessageBus
|
|
21
|
+
from dbus_fast.errors import (
|
|
22
22
|
DBusError,
|
|
23
23
|
InvalidIntrospectionError,
|
|
24
24
|
InvalidMemberNameError,
|
|
@@ -28,6 +28,10 @@ logger = logging.getLogger(__name__)
|
|
|
28
28
|
|
|
29
29
|
_DBUS_PROBE_TIMEOUT_SEC = 2.0
|
|
30
30
|
_POWER_SAVE_WARNED_BACKENDS: set[str] = set()
|
|
31
|
+
# One session bus lives for the whole process, so id(bus) is stable and
|
|
32
|
+
# never recycled — a plain dict needs no eviction. Keyed by bus identity
|
|
33
|
+
# because dbus-fast's cython MessageBus cannot be weak-referenced.
|
|
34
|
+
_PROXY_CACHE: dict = {}
|
|
31
35
|
|
|
32
36
|
_SERVICE_MISSING_ERRORS = (
|
|
33
37
|
"org.freedesktop.DBus.Error.ServiceUnknown",
|
|
@@ -62,10 +66,6 @@ DISPLAY_CONFIG_BUS = "org.gnome.Mutter.DisplayConfig"
|
|
|
62
66
|
DISPLAY_CONFIG_PATH = "/org/gnome/Mutter/DisplayConfig"
|
|
63
67
|
DISPLAY_CONFIG_IFACE = "org.gnome.Mutter.DisplayConfig"
|
|
64
68
|
|
|
65
|
-
KDE_POWER_BUS = "org.kde.Solid.PowerManagement"
|
|
66
|
-
KDE_POWER_PATH = "/org/kde/Solid/PowerManagement"
|
|
67
|
-
KDE_POWER_IFACE = "org.kde.Solid.PowerManagement"
|
|
68
|
-
|
|
69
69
|
# DBus service constants — monitor geometry (KDE)
|
|
70
70
|
KSCREEN_BUS = "org.kde.KScreen"
|
|
71
71
|
KSCREEN_PATH = "/backend"
|
|
@@ -88,6 +88,23 @@ def _is_gnome_desktop() -> bool:
|
|
|
88
88
|
)
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
async def _cached_interface(bus: MessageBus, service: str, path: str, iface_name: str):
|
|
92
|
+
key = (id(bus), service, path, iface_name)
|
|
93
|
+
if key in _PROXY_CACHE:
|
|
94
|
+
return _PROXY_CACHE[key]
|
|
95
|
+
intro = await bus.introspect(service, path)
|
|
96
|
+
obj = bus.get_proxy_object(service, path, intro)
|
|
97
|
+
iface = obj.get_interface(iface_name)
|
|
98
|
+
_PROXY_CACHE[key] = iface
|
|
99
|
+
return iface
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _invalidate_interface(
|
|
103
|
+
bus: MessageBus, service: str, path: str, iface_name: str
|
|
104
|
+
) -> None:
|
|
105
|
+
_PROXY_CACHE.pop((id(bus), service, path, iface_name), None)
|
|
106
|
+
|
|
107
|
+
|
|
91
108
|
async def _name_has_owner(bus: MessageBus, bus_name: str) -> bool:
|
|
92
109
|
"""Ask the bus daemon whether a well-known name is currently owned.
|
|
93
110
|
|
|
@@ -199,7 +216,6 @@ async def probe_activity_services(bus: MessageBus) -> dict[str, bool]:
|
|
|
199
216
|
"fdo_screensaver": FDO_SCREENSAVER_BUS,
|
|
200
217
|
"gnome_screensaver": GNOME_SCREENSAVER_BUS,
|
|
201
218
|
"gnome_display_config": DISPLAY_CONFIG_BUS,
|
|
202
|
-
"kde_power": KDE_POWER_BUS,
|
|
203
219
|
"kscreen": KSCREEN_BUS,
|
|
204
220
|
}
|
|
205
221
|
results = {}
|
|
@@ -212,7 +228,7 @@ async def probe_activity_services(bus: MessageBus) -> dict[str, bool]:
|
|
|
212
228
|
|
|
213
229
|
# Log grouped by function
|
|
214
230
|
lock_backends = ["fdo_screensaver", "gnome_screensaver"]
|
|
215
|
-
power_backends = ["gnome_display_config"
|
|
231
|
+
power_backends = ["gnome_display_config"]
|
|
216
232
|
monitor_backends = ["kscreen"]
|
|
217
233
|
|
|
218
234
|
def _status(keys):
|
|
@@ -253,9 +269,12 @@ async def is_screen_locked(bus: MessageBus) -> bool:
|
|
|
253
269
|
if not _is_gnome_desktop():
|
|
254
270
|
# Try freedesktop.org ScreenSaver first (KDE kwin and other non-GNOME desktops)
|
|
255
271
|
try:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
272
|
+
iface = await _cached_interface(
|
|
273
|
+
bus,
|
|
274
|
+
FDO_SCREENSAVER_BUS,
|
|
275
|
+
FDO_SCREENSAVER_PATH,
|
|
276
|
+
FDO_SCREENSAVER_IFACE,
|
|
277
|
+
)
|
|
259
278
|
return bool(await iface.call_get_active())
|
|
260
279
|
except (
|
|
261
280
|
DBusError,
|
|
@@ -263,6 +282,12 @@ async def is_screen_locked(bus: MessageBus) -> bool:
|
|
|
263
282
|
InvalidIntrospectionError,
|
|
264
283
|
OSError,
|
|
265
284
|
) as exc:
|
|
285
|
+
_invalidate_interface(
|
|
286
|
+
bus,
|
|
287
|
+
FDO_SCREENSAVER_BUS,
|
|
288
|
+
FDO_SCREENSAVER_PATH,
|
|
289
|
+
FDO_SCREENSAVER_IFACE,
|
|
290
|
+
)
|
|
266
291
|
if not _is_service_missing(exc):
|
|
267
292
|
logger.warning(
|
|
268
293
|
"is_screen_locked FDO backend failed: service=%s path=%s: %s: %s",
|
|
@@ -274,9 +299,12 @@ async def is_screen_locked(bus: MessageBus) -> bool:
|
|
|
274
299
|
|
|
275
300
|
# Fall back to GNOME ScreenSaver
|
|
276
301
|
try:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
302
|
+
iface = await _cached_interface(
|
|
303
|
+
bus,
|
|
304
|
+
GNOME_SCREENSAVER_BUS,
|
|
305
|
+
GNOME_SCREENSAVER_PATH,
|
|
306
|
+
GNOME_SCREENSAVER_IFACE,
|
|
307
|
+
)
|
|
280
308
|
return bool(await iface.call_get_active())
|
|
281
309
|
except (
|
|
282
310
|
DBusError,
|
|
@@ -284,6 +312,12 @@ async def is_screen_locked(bus: MessageBus) -> bool:
|
|
|
284
312
|
InvalidIntrospectionError,
|
|
285
313
|
OSError,
|
|
286
314
|
) as exc:
|
|
315
|
+
_invalidate_interface(
|
|
316
|
+
bus,
|
|
317
|
+
GNOME_SCREENSAVER_BUS,
|
|
318
|
+
GNOME_SCREENSAVER_PATH,
|
|
319
|
+
GNOME_SCREENSAVER_IFACE,
|
|
320
|
+
)
|
|
287
321
|
if not _is_service_missing(exc):
|
|
288
322
|
logger.warning(
|
|
289
323
|
"is_screen_locked GNOME backend failed: service=%s path=%s: %s: %s",
|
|
@@ -296,9 +330,10 @@ async def is_screen_locked(bus: MessageBus) -> bool:
|
|
|
296
330
|
|
|
297
331
|
|
|
298
332
|
async def is_power_save_active(bus: MessageBus) -> bool:
|
|
299
|
-
"""
|
|
333
|
+
"""Return True when the session reports a power-saving/display-off state.
|
|
300
334
|
|
|
301
|
-
|
|
335
|
+
Checks GNOME Mutter PowerSaveMode, then falls back to X11 DPMS when
|
|
336
|
+
XDG_SESSION_TYPE is x11; degrades to False when no backend is available.
|
|
302
337
|
"""
|
|
303
338
|
|
|
304
339
|
def log_backend_failure_once(backend: str, bus_name: str, path: str, exc) -> None:
|
|
@@ -318,9 +353,12 @@ async def is_power_save_active(bus: MessageBus) -> bool:
|
|
|
318
353
|
|
|
319
354
|
# Try GNOME Mutter DisplayConfig first
|
|
320
355
|
try:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
356
|
+
iface = await _cached_interface(
|
|
357
|
+
bus,
|
|
358
|
+
DISPLAY_CONFIG_BUS,
|
|
359
|
+
DISPLAY_CONFIG_PATH,
|
|
360
|
+
"org.freedesktop.DBus.Properties",
|
|
361
|
+
)
|
|
324
362
|
mode_variant = await iface.call_get(DISPLAY_CONFIG_IFACE, "PowerSaveMode")
|
|
325
363
|
mode = int(mode_variant.value)
|
|
326
364
|
return mode != 0
|
|
@@ -330,6 +368,12 @@ async def is_power_save_active(bus: MessageBus) -> bool:
|
|
|
330
368
|
InvalidIntrospectionError,
|
|
331
369
|
OSError,
|
|
332
370
|
) as exc:
|
|
371
|
+
_invalidate_interface(
|
|
372
|
+
bus,
|
|
373
|
+
DISPLAY_CONFIG_BUS,
|
|
374
|
+
DISPLAY_CONFIG_PATH,
|
|
375
|
+
"org.freedesktop.DBus.Properties",
|
|
376
|
+
)
|
|
333
377
|
if not _is_service_missing(exc):
|
|
334
378
|
log_backend_failure_once(
|
|
335
379
|
"Mutter",
|
|
@@ -338,21 +382,6 @@ async def is_power_save_active(bus: MessageBus) -> bool:
|
|
|
338
382
|
exc,
|
|
339
383
|
)
|
|
340
384
|
|
|
341
|
-
# Fall back to KDE Solid PowerManagement
|
|
342
|
-
try:
|
|
343
|
-
intro = await bus.introspect(KDE_POWER_BUS, KDE_POWER_PATH)
|
|
344
|
-
obj = bus.get_proxy_object(KDE_POWER_BUS, KDE_POWER_PATH, intro)
|
|
345
|
-
iface = obj.get_interface(KDE_POWER_IFACE)
|
|
346
|
-
return bool(await iface.call_is_lid_closed())
|
|
347
|
-
except (
|
|
348
|
-
DBusError,
|
|
349
|
-
InvalidMemberNameError,
|
|
350
|
-
InvalidIntrospectionError,
|
|
351
|
-
OSError,
|
|
352
|
-
) as exc:
|
|
353
|
-
if not _is_service_missing(exc):
|
|
354
|
-
log_backend_failure_once("KDE", KDE_POWER_BUS, KDE_POWER_PATH, exc)
|
|
355
|
-
|
|
356
385
|
# X11-only fallback: DPMS via xset
|
|
357
386
|
if os.environ.get("XDG_SESSION_TYPE", "").lower() == "x11":
|
|
358
387
|
return await is_dpms_active()
|
|
@@ -407,7 +436,7 @@ def get_monitor_geometries() -> list[dict]:
|
|
|
407
436
|
|
|
408
437
|
|
|
409
438
|
def _unwrap_variants(obj):
|
|
410
|
-
"""Recursively unwrap dbus-
|
|
439
|
+
"""Recursively unwrap dbus-fast Variants in nested DBus structures."""
|
|
411
440
|
if isinstance(obj, Variant):
|
|
412
441
|
return _unwrap_variants(obj.value)
|
|
413
442
|
if isinstance(obj, dict):
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (c) 2026 sol pbc
|
|
3
|
+
|
|
4
|
+
"""Audio device detection.
|
|
5
|
+
|
|
6
|
+
Changes from monorepo version:
|
|
7
|
+
- Uses structural soundcard isloopback metadata instead of amplitude-thresholding
|
|
8
|
+
on a played tone, so muted sinks and silent rooms no longer fail detection.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
import soundcard as sc
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def input_detect(timeout=3.0):
|
|
21
|
+
try:
|
|
22
|
+
# Fully wedged PulseAudio enumeration is a pre-existing out-of-scope hang.
|
|
23
|
+
devices = sc.all_microphones(include_loopback=True)
|
|
24
|
+
except Exception:
|
|
25
|
+
logger.warning("Failed to enumerate audio devices")
|
|
26
|
+
return None, None
|
|
27
|
+
if not devices:
|
|
28
|
+
logger.warning("No audio devices found")
|
|
29
|
+
return None, None
|
|
30
|
+
|
|
31
|
+
results = {}
|
|
32
|
+
lock = threading.Lock()
|
|
33
|
+
|
|
34
|
+
def classify(index, mic):
|
|
35
|
+
try:
|
|
36
|
+
is_loopback = bool(mic.isloopback)
|
|
37
|
+
except Exception:
|
|
38
|
+
is_loopback = None
|
|
39
|
+
with lock:
|
|
40
|
+
results[index] = is_loopback
|
|
41
|
+
|
|
42
|
+
threads = []
|
|
43
|
+
deadline = time.monotonic() + timeout
|
|
44
|
+
for index, mic in enumerate(devices):
|
|
45
|
+
thread = threading.Thread(target=classify, args=(index, mic), daemon=True)
|
|
46
|
+
thread.start()
|
|
47
|
+
threads.append(thread)
|
|
48
|
+
|
|
49
|
+
for thread in threads:
|
|
50
|
+
remaining = max(0.0, deadline - time.monotonic())
|
|
51
|
+
thread.join(timeout=remaining)
|
|
52
|
+
|
|
53
|
+
with lock:
|
|
54
|
+
final_results = dict(results)
|
|
55
|
+
|
|
56
|
+
mic_detected = None
|
|
57
|
+
loopback_detected = None
|
|
58
|
+
for index, mic in enumerate(devices):
|
|
59
|
+
is_loopback = final_results.get(index)
|
|
60
|
+
if is_loopback is None:
|
|
61
|
+
continue
|
|
62
|
+
if is_loopback and loopback_detected is None:
|
|
63
|
+
loopback_detected = mic
|
|
64
|
+
elif not is_loopback and mic_detected is None:
|
|
65
|
+
mic_detected = mic
|
|
66
|
+
return mic_detected, loopback_detected
|