···2020 audio_recorder.py Stereo audio recording (mic + system via soundcard)
2121 audio_detect.py Audio device detection via ultrasonic tone
2222 audio_mute.py PulseAudio mute state detection
2323- activity.py GNOME-specific activity detection (idle, screen lock, power save)
2323+ activity.py Cross-desktop activity detection (screen lock, power save) via DBus
2424 monitor_positions.py Monitor position assignment from geometry
2525 session_env.py Desktop session environment checks and recovery
2626 streams.py Stream name derivation (hostname-based)
···101101102102## Key Patterns
103103104104-- **Activity detection is GNOME-specific.** Uses Mutter IdleMonitor, GNOME ScreenSaver, and Mutter DisplayConfig DBus interfaces. Other desktops capture screen and audio but won't get activity-based segment boundaries.
104104+- **Activity detection is cross-desktop.** Uses ordered DBus fallback chains for screen lock (freedesktop.org ScreenSaver → GNOME ScreenSaver) and power save (Mutter DisplayConfig → KDE Solid PowerManagement). All backends degrade gracefully to safe defaults.
105105- **Audio is stereo-interleaved.** Left channel = microphone, right channel = system audio. When muted, channels are split into separate mono FLAC files.
106106- **Screencast uses xdg-desktop-portal.** Session persistence via restore tokens avoids re-prompting the user. GStreamer subprocess (`gst-launch-1.0`) handles the actual PipeWire recording.
107107- **Crash recovery runs on startup.** `recovery.py` scans for orphaned `.incomplete` directories older than 2 minutes and finalizes or marks them as failed.
+25-21
src/solstone_linux/activity.py
···3344"""Activity detection using DBus APIs.
5566-Detects screen lock and power save state across GNOME and KDE desktops
77-using freedesktop, GNOME, and KDE DBus interfaces with ordered fallback
88-chains. Every function degrades gracefully — returning a safe default —
99-so the observer keeps running regardless of desktop environment.
66+Detects screen lock and display power-save state via DBus, with ordered
77+fallback chains that cover GNOME and KDE desktops. Every function
88+degrades gracefully — returning a safe default — so the observer keeps
99+running regardless of desktop environment.
1010"""
11111212import logging
···3030except (ImportError, ValueError):
3131 _HAS_GTK = False
32323333-# DBus service constants
3333+# DBus service constants — screen lock
3434FDO_SCREENSAVER_BUS = "org.freedesktop.ScreenSaver"
3535FDO_SCREENSAVER_PATH = "/ScreenSaver"
3636FDO_SCREENSAVER_IFACE = "org.freedesktop.ScreenSaver"
···3939GNOME_SCREENSAVER_PATH = "/org/gnome/ScreenSaver"
4040GNOME_SCREENSAVER_IFACE = "org.gnome.ScreenSaver"
41414242+# DBus service constants — power save
4243DISPLAY_CONFIG_BUS = "org.gnome.Mutter.DisplayConfig"
4344DISPLAY_CONFIG_PATH = "/org/gnome/Mutter/DisplayConfig"
4445DISPLAY_CONFIG_IFACE = "org.gnome.Mutter.DisplayConfig"
···6465 except Exception:
6566 results[name] = False
66676868+ # Log grouped by function
6769 lock_backends = ["fdo_screensaver", "gnome_screensaver"]
6870 power_backends = ["gnome_display_config", "kde_power"]
69717070- lock_available = [name for name in lock_backends if results.get(name)]
7171- power_available = [name for name in power_backends if results.get(name)]
7272+ def _status(keys):
7373+ return ", ".join(f"{k} [{'ok' if results[k] else 'missing'}]" for k in keys)
72747373- if lock_available:
7474- logger.info("Screen lock backends: %s", ", ".join(lock_available))
7575- else:
7676- logger.warning("No screen lock backends available — will assume unlocked")
7575+ logger.info("Screen lock backends: %s", _status(lock_backends))
7676+ logger.info("Power save backends: %s", _status(power_backends))
77777878- if power_available:
7979- logger.info("Power save backends: %s", ", ".join(power_available))
8080- else:
8181- logger.warning("No power save backends available — will assume active display")
7878+ any_lock = any(results[k] for k in lock_backends)
7979+ any_power = any(results[k] for k in power_backends)
8080+ if not any_lock and not any_power:
8181+ logger.warning(
8282+ "No activity backends available — running in always-capture mode"
8383+ )
82848385 results["gtk4"] = _HAS_GTK
8486 if not _HAS_GTK:
···889089919092async def is_screen_locked(bus: MessageBus) -> bool:
9191- """Check if the screen is currently locked.
9393+ """Check if the screen is locked via FDO ScreenSaver, then GNOME ScreenSaver.
92949393- Tries freedesktop.ScreenSaver first (works on KDE, GNOME, and others),
9494- then falls back to GNOME ScreenSaver.
9595+ Returns True if locked, False if unlocked or all backends unavailable.
9596 """
9797+ # Try freedesktop.org ScreenSaver first (KDE kwin + GNOME)
9698 try:
9799 intro = await bus.introspect(FDO_SCREENSAVER_BUS, FDO_SCREENSAVER_PATH)
98100 obj = bus.get_proxy_object(FDO_SCREENSAVER_BUS, FDO_SCREENSAVER_PATH, intro)
···101103 except Exception:
102104 pass
103105106106+ # Fall back to GNOME ScreenSaver
104107 try:
105108 intro = await bus.introspect(GNOME_SCREENSAVER_BUS, GNOME_SCREENSAVER_PATH)
106109 obj = bus.get_proxy_object(GNOME_SCREENSAVER_BUS, GNOME_SCREENSAVER_PATH, intro)
···111114112115113116async def is_power_save_active(bus: MessageBus) -> bool:
114114- """Check if display power save mode is active.
117117+ """Check display power save via GNOME Mutter, then KDE Solid.
115118116116- Tries GNOME Mutter DisplayConfig first (DPMS state), then falls back
117117- to KDE Solid PowerManagement lid state.
119119+ Returns True if power save is active, False otherwise.
118120 """
121121+ # Try GNOME Mutter DisplayConfig first
119122 try:
120123 intro = await bus.introspect(DISPLAY_CONFIG_BUS, DISPLAY_CONFIG_PATH)
121124 obj = bus.get_proxy_object(DISPLAY_CONFIG_BUS, DISPLAY_CONFIG_PATH, intro)
···126129 except Exception:
127130 pass
128131132132+ # Fall back to KDE Solid PowerManagement
129133 try:
130134 intro = await bus.introspect(KDE_POWER_BUS, KDE_POWER_PATH)
131135 obj = bus.get_proxy_object(KDE_POWER_BUS, KDE_POWER_PATH, intro)