d-bus: statusnotifierwatcher spec-complete + kf6-kglobalaccel cleanup (v3.6)

Complete the org.freedesktop.StatusNotifierWatcher D-Bus surface that
was previously stubbed: add UnregisterStatusNotifierItem,
UnregisterStatusNotifierHost, and the StatusNotifierHostRegistered
signal. The struct derives Clone so the new unregister paths are
symmetric with the existing register paths.

7 new unit tests cover the unregister state machine
(item present, item absent, idempotency, and unaffected siblings for
both items and hosts). 12 tests pass total for statusnotifierwatcher.

Also remove a stale TODO from kf6-kglobalaccel recipe.toml. The
comment claimed the recipe needed kf6-kcrash and kf6-kdbusaddons, but
both are already in the [build] dependencies list.

DBUS-PLAN bumped to v3.6 (2026-07-26). Implementation status line
describes redbear-statusnotifierwatcher v0.2 with the expanded D-Bus
surface and 12 unit tests.

Tested: 73 unit tests pass across the five D-Bus daemons
(sessiond 32, upower 7, udisks 9, polkit 13, statusnotifierwatcher 12).
This commit is contained in:
Red Bear OS Builder
2026-07-26 22:40:16 +09:00
parent ba2eee9834
commit b1553f4073
3 changed files with 113 additions and 3 deletions
+2 -2
View File
@@ -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<Mutex>` 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<Mutex>` 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)
@@ -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"
@@ -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<Mutex<HashSet<String>>>,
hosts: Arc<Mutex<HashSet<String>>>,
@@ -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<String> {
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()));
}
}