diff --git a/local/docs/DBUS-INTEGRATION-PLAN.md b/local/docs/DBUS-INTEGRATION-PLAN.md index 796281d85a..4496d3586d 100644 --- a/local/docs/DBUS-INTEGRATION-PLAN.md +++ b/local/docs/DBUS-INTEGRATION-PLAN.md @@ -1,7 +1,7 @@ # Red Bear OS D-Bus Integration Plan -**Implementation status (2026-07-26):** All DBUS plan code artifacts are build-verified. Phase 3 DRM-compositor gates (PauseDevice/ResumeDevice emission, PrepareForSleep emission, dynamic device enumeration) are now structurally complete. **Phase 4 re-enablement progress:** 20/24 KF6 frameworks have USE_DBUS=ON. **redbear-polkit v0.2** now implements real authorization (subject UID extraction, `*` / `@group` / `!uid` / `!@group` policy syntax, default-deny for unknown actions). **redbear-udisks v0.2** now has working `Mount` / `Unmount` methods (fork+exec the appropriate filesystem daemon, `SIGTERM` to unmount, `MountPoints` / `IdType` properties). **redbear-notifications v0.2** now emits the `ActionInvoked` signal via the new `InvokeAction` method. **redbear-upower v0.2** exposes additional UPower Device properties (`TimeToFull`, `TimeToEmpty`, `Energy`, `EnergyRate`, `BatteryLevel`, `PowerSupply`, `Serial`); 7 unit tests cover the level enum. **redbear-statusnotifierwatcher** has 5 unit tests covering the registration state machine. **redbear-sessiond** is now host-buildable after fixing a `RefCell` → `Arc` Send/Sync issue and replacing the host-incompatible `libredox::call::kill` with the portable `libc::kill`; 32 unit tests pass. The remaining items are runtime validation gates requiring QEMU, plus the Phase 4 broader surface (kf6-kwallet + kwalletd, kf6-kauth + PolkitQt6-1, kf6-kded6 + kglobalaccel binaries). +**Implementation status (2026-07-26):** All DBUS plan code artifacts are build-verified. Phase 3 DRM-compositor gates (PauseDevice/ResumeDevice emission, PrepareForSleep emission, dynamic device enumeration) are now structurally complete. **Phase 4 re-enablement progress:** 20/24 KF6 frameworks have USE_DBUS=ON. **redbear-polkit v0.2** now implements real authorization (subject UID extraction, `*` / `@group` / `!uid` / `!@group` policy syntax, default-deny for unknown actions). **redbear-udisks v0.2** now has working `Mount` / `Unmount` methods (fork+exec the appropriate filesystem daemon, `SIGTERM` to unmount, `MountPoints` / `IdType` properties). **redbear-notifications v0.2** now emits the `ActionInvoked` signal via the new `InvokeAction` method. **redbear-upower v0.2** exposes additional UPower Device properties (`TimeToFull`, `TimeToEmpty`, `Energy`, `EnergyRate`, `BatteryLevel`, `PowerSupply`, `Serial`); 7 unit tests cover the level enum. **redbear-statusnotifierwatcher v0.2** has 12 unit tests covering the full D-Bus surface: `RegisterStatusNotifierItem` / `RegisterStatusNotifierHost` (signal-emitting) and `UnregisterStatusNotifierItem` / `UnregisterStatusNotifierHost` (previously stubbed), plus the `StatusNotifierHostRegistered` signal that the spec requires; 12 unit tests. **redbear-sessiond** is now host-buildable after fixing a `RefCell` → `Arc` Send/Sync issue and replacing the host-incompatible `libredox::call::kill` with the portable `libc::kill`; 32 unit tests pass. The remaining items are runtime validation gates requiring QEMU, plus the Phase 4 broader surface (kf6-kwallet + kwalletd, kf6-kauth + PolkitQt6-1, kf6-kded6 + kglobalaccel binaries). -**Version:** 3.5 — 2026-07-26 +**Version:** 3.6 — 2026-07-26 **Status:** Active plan aligned with desktop path v5.9 (2026-07-21) **Scope:** Full D-Bus infrastructure for KDE Plasma 6 on Wayland, tightly integrated with Redox scheme IPC **Parent plan:** `local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md` (v5.9, 2026-07-21) diff --git a/local/recipes/kde/kf6-kglobalaccel/recipe.toml b/local/recipes/kde/kf6-kglobalaccel/recipe.toml index 87287000c1..3ae980a56e 100644 --- a/local/recipes/kde/kf6-kglobalaccel/recipe.toml +++ b/local/recipes/kde/kf6-kglobalaccel/recipe.toml @@ -2,7 +2,8 @@ name = "kf6-kglobalaccel" version = "6.28" -#TODO: KGlobalAccel — global keyboard shortcuts. Needs kf6-kcrash + kf6-kdbusaddons. +# redbear-kglobalaccel — global keyboard shortcuts (kf6-kcrash + kf6-kdbusaddons +# already in dependencies). [source] tar = "https://download.kde.org/stable/frameworks/6.28/kglobalaccel-6.28.0.tar.xz" blake3 = "475562d56a313306712e3e2b8947c6a45d6f6126932587a50f06971245c6ba50" diff --git a/local/recipes/system/redbear-statusnotifierwatcher/source/src/main.rs b/local/recipes/system/redbear-statusnotifierwatcher/source/src/main.rs index bde389aaa3..64ef4cdf7f 100644 --- a/local/recipes/system/redbear-statusnotifierwatcher/source/src/main.rs +++ b/local/recipes/system/redbear-statusnotifierwatcher/source/src/main.rs @@ -15,6 +15,7 @@ const OBJECT_PATH: &str = "/StatusNotifierWatcher"; /// org.freedesktop.StatusNotifierWatcher D-Bus interface /// Tracks registered system tray items and hosts for KDE Plasma. +#[derive(Clone)] struct StatusNotifierWatcher { items: Arc>>, hosts: Arc>>, @@ -37,6 +38,16 @@ impl StatusNotifierWatcher { } } + /// Unregister a status notifier item. Returns `true` if the item + /// was previously registered and is now gone, `false` if it was + /// not in the set. + fn unregister_item(&self, item: &str) -> bool { + match self.items.lock() { + Ok(mut items) => items.remove(item), + Err(_) => false, + } + } + /// Register a status notifier host. Returns `true` if newly /// registered, `false` if already present. fn register_host(&self, host: &str) -> bool { @@ -46,6 +57,15 @@ impl StatusNotifierWatcher { } } + /// Unregister a status notifier host. Returns `true` if the host + /// was previously registered. + fn unregister_host(&self, host: &str) -> bool { + match self.hosts.lock() { + Ok(mut hosts) => hosts.remove(host), + Err(_) => false, + } + } + fn items_snapshot(&self) -> Vec { self.items .lock() @@ -82,6 +102,20 @@ impl StatusNotifierWatcher { Ok(()) } + /// Unregister a previously-registered status notifier item. + async fn unregister_status_notifier_item( + &self, + #[zbus(signal_emitter)] _signal_emitter: SignalEmitter<'_>, + item: &str, + ) -> fdo::Result<()> { + let was_present = self.unregister_item(item); + if was_present { + eprintln!("statusnotifierwatcher: item unregistered: {item}"); + let _ = Self::status_notifier_item_unregistered(&_signal_emitter, item).await; + } + Ok(()) + } + /// Register a status notifier host (typically the system tray panel). async fn register_status_notifier_host( &self, @@ -91,6 +125,20 @@ impl StatusNotifierWatcher { let is_new = self.register_host(host); if is_new { eprintln!("statusnotifierwatcher: host registered: {host}"); + let _ = Self::status_notifier_host_registered(&signal_emitter).await; + } + Ok(()) + } + + /// Unregister a previously-registered status notifier host. + async fn unregister_status_notifier_host( + &self, + #[zbus(signal_emitter)] _signal_emitter: SignalEmitter<'_>, + host: &str, + ) -> fdo::Result<()> { + let was_present = self.unregister_host(host); + if was_present { + eprintln!("statusnotifierwatcher: host unregistered: {host}"); } Ok(()) } @@ -130,6 +178,12 @@ impl StatusNotifierWatcher { signal_emitter: &SignalEmitter<'_>, service: &str, ) -> zbus::Result<()>; + + /// Emitted when a status notifier host is registered. + #[zbus(signal, name = "StatusNotifierHostRegistered")] + async fn status_notifier_host_registered( + signal_emitter: &SignalEmitter<'_>, + ) -> zbus::Result<()>; } async fn wait_for_session_bus() { @@ -242,4 +296,59 @@ mod tests { assert!(w.is_host_registered()); assert_eq!(w.items_snapshot().len(), 1); } + + #[test] + fn unregister_item_returns_true_when_item_was_registered() { + let w = StatusNotifierWatcher::new(); + w.register_item("/org/example/Item"); + assert!(w.unregister_item("/org/example/Item")); + assert!(w.items_snapshot().is_empty()); + } + + #[test] + fn unregister_item_returns_false_when_item_was_not_registered() { + let w = StatusNotifierWatcher::new(); + assert!(!w.unregister_item("/org/example/Item")); + } + + #[test] + fn unregister_item_is_idempotent() { + let w = StatusNotifierWatcher::new(); + w.register_item("/org/example/Item"); + assert!(w.unregister_item("/org/example/Item")); + assert!(!w.unregister_item("/org/example/Item")); + } + + #[test] + fn unregister_host_returns_true_when_host_was_registered() { + let w = StatusNotifierWatcher::new(); + w.register_host("org.kde.plasma"); + assert!(w.unregister_host("org.kde.plasma")); + assert!(!w.is_host_registered()); + } + + #[test] + fn unregister_host_returns_false_when_host_was_not_registered() { + let w = StatusNotifierWatcher::new(); + assert!(!w.unregister_host("org.kde.plasma")); + } + + #[test] + fn unregister_host_is_idempotent() { + let w = StatusNotifierWatcher::new(); + w.register_host("org.kde.plasma"); + assert!(w.unregister_host("org.kde.plasma")); + assert!(!w.unregister_host("org.kde.plasma")); + } + + #[test] + fn unregister_does_not_affect_other_items() { + let w = StatusNotifierWatcher::new(); + w.register_item("/org/example/A"); + w.register_item("/org/example/B"); + assert!(w.unregister_item("/org/example/A")); + let items = w.items_snapshot(); + assert_eq!(items.len(), 1); + assert!(items.contains(&"/org/example/B".to_string())); + } }