From f065143d3eec5df56c8b4f97c96d2d0a991d22e2 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 14 May 2025 16:21:57 -0700 Subject: [PATCH] dbus: Implement `org.freedesktop.a11y.Manager` protocol This protocol is now the upstream solution in at-spi2-core/orca for registering keyboard grabs and watching key events. It should also be a bit better than the current verious of our custom Wayland protocol for this purpose. Like Mutter and Kwin, we currently restrict this to only be called by the client that holds the name `org.gnome.Orca.KeyboardMonitor` on the session bus. We also send the `KeyEvent` signal only to registered watchers, rather than broadcasting, as DBus does by default. --- src/dbus/a11y_keyboard_monitor.rs | 330 ++++++++++++++++++++++++++++++ src/dbus/mod.rs | 1 + src/input/mod.rs | 23 ++- src/shell/mod.rs | 1 + src/state.rs | 5 + 5 files changed, 358 insertions(+), 2 deletions(-) create mode 100644 src/dbus/a11y_keyboard_monitor.rs diff --git a/src/dbus/a11y_keyboard_monitor.rs b/src/dbus/a11y_keyboard_monitor.rs new file mode 100644 index 00000000..a31d1d95 --- /dev/null +++ b/src/dbus/a11y_keyboard_monitor.rs @@ -0,0 +1,330 @@ +// https://gitlab.gnome.org/GNOME/mutter/-/blob/main/data/dbus-interfaces/org.freedesktop.a11y.xml + +use futures_executor::ThreadPool; +use smithay::{ + backend::input::KeyState, + input::keyboard::{KeysymHandle, ModifiersState}, +}; +use std::{ + collections::{HashMap, HashSet}, + sync::{Arc, Mutex, OnceLock}, +}; +use tracing::debug; +use xkbcommon::xkb::Keysym; +use zbus::{ + message::Header, + names::{UniqueName, WellKnownName}, + object_server::SignalEmitter, +}; + +use super::name_owners::NameOwners; + +static ALLOWED_NAMES: &'static [WellKnownName] = &[WellKnownName::from_static_str_unchecked( + "org.gnome.Orca.KeyboardMonitor", +)]; + +// As defined in at-spi2-core +const ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START: u32 = 15; + +#[derive(PartialEq, Eq, Debug)] +struct KeyGrab { + pub mods: u32, + pub virtual_mods: HashSet, + pub key: Keysym, +} + +impl KeyGrab { + fn new(virtual_mods: &[Keysym], key: Keysym, raw_mods: u32) -> Self { + let mods = raw_mods & ((1 << ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START) - 1); + let virtual_mods = virtual_mods + .iter() + .copied() + .enumerate() + .filter(|(i, _)| { + raw_mods & (1 << (ATSPI_DEVICE_A11Y_MANAGER_VIRTUAL_MOD_START + *i as u32)) != 0 + }) + .map(|(_, x)| x) + .collect(); + Self { + mods, + virtual_mods, + key, + } + } +} + +#[derive(Debug, Default)] +struct Client { + grabbed: bool, + watched: bool, + virtual_mods: HashSet, + key_grabs: Vec, +} + +#[derive(Debug, Default)] +struct Clients(HashMap, Client>); + +impl Clients { + fn get(&mut self, name: &UniqueName<'_>) -> &mut Client { + self.0.entry(name.to_owned()).or_default() + } +} + +#[derive(Debug)] +pub struct A11yKeyboardMonitorState { + executor: ThreadPool, + clients: Arc>, + active_virtual_mods: HashSet, + conn: Arc>, + name_owners: Arc>, +} + +impl A11yKeyboardMonitorState { + pub fn new(executor: &ThreadPool) -> Self { + let clients = Arc::new(Mutex::new(Clients::default())); + let clients_clone = clients.clone(); + let conn_cell = Arc::new(OnceLock::new()); + let conn_cell_clone = conn_cell.clone(); + let name_owners_cell = Arc::new(OnceLock::new()); + let name_owners_cell_clone = name_owners_cell.clone(); + let executor_clone = executor.clone(); + executor.spawn_ok(async move { + match serve(clients_clone, &executor_clone).await { + Ok((conn, name_owners)) => { + conn_cell_clone.set(conn).unwrap(); + name_owners_cell_clone.set(name_owners).unwrap(); + } + Err(err) => { + tracing::error!("Failed to serve `org.freedesktop.a11y.Manager`: {err}"); + } + } + }); + Self { + executor: executor.clone(), + clients, + active_virtual_mods: HashSet::new(), + conn: conn_cell, + name_owners: name_owners_cell, + } + } + + pub fn has_virtual_mod(&self, keysym: Keysym) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .any(|client| client.virtual_mods.contains(&keysym)) + } + + pub fn add_active_virtual_mod(&mut self, keysym: Keysym) { + self.active_virtual_mods.insert(keysym); + } + + pub fn remove_active_virtual_mod(&mut self, keysym: Keysym) -> bool { + self.active_virtual_mods.remove(&keysym) + } + + pub fn active_virtual_mods(&self) -> &HashSet { + &self.active_virtual_mods + } + + pub fn has_keyboard_grab(&self) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .any(|client| client.grabbed) + } + + /// Key grab exists for mods, key, with active virtual mods + pub fn has_key_grab(&self, modifiers: &ModifiersState, key: Keysym) -> bool { + self.clients + .lock() + .unwrap() + .0 + .values() + .flat_map(|client| &client.key_grabs) + .any(|grab| { + grab.mods == modifiers.serialized.depressed + && grab.virtual_mods == self.active_virtual_mods + && grab.key == key + }) + } + + pub fn key_event(&self, modifiers: &ModifiersState, keysym: &KeysymHandle, state: KeyState) { + let Some(conn) = self.conn.get() else { + return; + }; + + let clients = self.clients.lock().unwrap(); + for (unique_name, client) in clients.0.iter() { + if !client.watched && !self.has_key_grab(modifiers, keysym.modified_sym()) { + continue; + } + + let mut signal_context = + SignalEmitter::new(conn, "/org/freedesktop/a11y/Manager").unwrap(); + // Instead of sending signal to all clients, send only to authorized + // clients with registed watches. + signal_context = signal_context.set_destination(unique_name.clone().into()); + + let released = match state { + KeyState::Pressed => false, + KeyState::Released => true, + }; + let unichar = { + let xkb = keysym.xkb().lock().unwrap(); + unsafe { xkb.state() }.key_get_utf32(keysym.raw_code()) + }; + let future = KeyboardMonitor::key_event( + signal_context, + released, + modifiers.serialized.depressed, + keysym.modified_sym().raw(), + unichar, + keysym.raw_code().raw() as u16, + ); + self.executor.spawn_ok(async { + let _ = future.await; + }); + } + } + + pub fn refresh(&mut self) { + // Remove clients and associated grabs when unique names are no longer + // present on bus, or no longer hold approved name on bus. + if let Some(name_owners) = self.name_owners.get() { + self.clients + .lock() + .unwrap() + .0 + .retain(|k, _| name_owners.check_owner_no_poll(k, ALLOWED_NAMES)) + } + } +} + +struct KeyboardMonitor { + clients: Arc>, + name_owners: NameOwners, +} + +impl KeyboardMonitor { + async fn check_sender_allowed(&self, sender: &UniqueName<'_>) -> zbus::fdo::Result<()> { + if self.name_owners.check_owner(sender, ALLOWED_NAMES).await { + Ok(()) + } else { + Err(zbus::fdo::Error::AccessDenied("Access denied".to_string())) + } + } +} + +#[zbus::interface(name = "org.freedesktop.a11y.KeyboardMonitor")] +impl KeyboardMonitor { + async fn grab_keyboard(&mut self, #[zbus(header)] header: Header<'_>) -> zbus::fdo::Result<()> { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).grabbed = true; + debug!("grab keyboard by {}", sender); + } + Ok(()) + } + + async fn ungrab_keyboard( + &mut self, + #[zbus(header)] header: Header<'_>, + ) -> zbus::fdo::Result<()> { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).grabbed = false; + debug!("ungrab keyboard by {}", sender); + } + Ok(()) + } + + async fn watch_keyboard( + &mut self, + #[zbus(header)] header: Header<'_>, + ) -> zbus::fdo::Result<()> { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).watched = true; + debug!("watch keyboard by {}", sender); + } + Ok(()) + } + + async fn unwatch_keyboard( + &mut self, + #[zbus(header)] header: Header<'_>, + ) -> zbus::fdo::Result<()> { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + clients.get(sender).watched = false; + debug!("unwatch keyboard by {}", sender); + } + Ok(()) + } + + async fn set_key_grabs( + &self, + #[zbus(header)] header: Header<'_>, + virtual_mods: Vec, + keystrokes: Vec<(u32, u32)>, + ) -> zbus::fdo::Result<()> { + let virtual_mods = virtual_mods + .into_iter() + .map(Keysym::from) + .collect::>(); + let key_grabs = keystrokes + .into_iter() + .map(|(k, mods)| KeyGrab::new(&virtual_mods, Keysym::from(k), mods)) + .collect::>(); + + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + let mut clients = self.clients.lock().unwrap(); + let client = clients.get(sender); + debug!( + "key grabs set by {}: {:?}", + sender, + (&virtual_mods, &key_grabs) + ); + client.virtual_mods = virtual_mods.into_iter().collect::>(); + client.key_grabs = key_grabs; + } + Ok(()) + } + + #[zbus(signal)] + async fn key_event( + ctx: SignalEmitter<'_>, + released: bool, + state: u32, + keysym: u32, + unichar: u32, + keycode: u16, + ) -> zbus::Result<()>; +} + +async fn serve( + clients: Arc>, + executor: &ThreadPool, +) -> zbus::Result<(zbus::Connection, NameOwners)> { + let conn = zbus::Connection::session().await?; + let name_owners = NameOwners::new(&conn, executor).await?; + let keyboard_monitor = KeyboardMonitor { + clients, + name_owners: name_owners.clone(), + }; + conn.object_server() + .at("/org/freedesktop/a11y/Manager", keyboard_monitor) + .await?; + conn.request_name("org.freedesktop.a11y.Manager").await?; + Ok((conn, name_owners)) +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index c824e880..dcb7f257 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -11,6 +11,7 @@ use std::collections::HashMap; use tracing::{error, warn}; use zbus::blocking::{Connection, fdo::DBusProxy}; +pub mod a11y_keyboard_monitor; #[cfg(feature = "systemd")] pub mod logind; mod name_owners; diff --git a/src/input/mod.rs b/src/input/mod.rs index 06cd440e..a50b967c 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1595,6 +1595,9 @@ impl State { self.common .atspi_ei .input(modifiers, &handle, event.state(), event.time() * 1000); + self.common + .a11y_keyboard_monitor_state + .key_event(modifiers, &handle, event.state()); // Leave move overview mode, if any modifier was released if let Some(Trigger::KeyboardMove(action_modifiers)) = @@ -1759,11 +1762,15 @@ impl State { } if event.state() == KeyState::Released { - let removed = self + let mut removed = self .common .atspi_ei .active_virtual_mods .remove(&event.key_code()); + removed |= self + .common + .a11y_keyboard_monitor_state + .remove_active_virtual_mod(handle.modified_sym()); // If `Caps_Lock` is a virtual modifier, and is in locked state, clear it if removed && handle.modified_sym() == Keysym::Caps_Lock @@ -1790,16 +1797,23 @@ impl State { ); } } else if event.state() == KeyState::Pressed - && self + && (self .common .atspi_ei .virtual_mods .contains(&event.key_code()) + || self + .common + .a11y_keyboard_monitor_state + .has_virtual_mod(handle.modified_sym())) { self.common .atspi_ei .active_virtual_mods .insert(event.key_code()); + self.common + .a11y_keyboard_monitor_state + .add_active_virtual_mod(handle.modified_sym()); tracing::debug!( "active virtual mods: {:?}", @@ -1835,10 +1849,15 @@ impl State { } if self.common.atspi_ei.has_keyboard_grab() + || self.common.a11y_keyboard_monitor_state.has_keyboard_grab() || self .common .atspi_ei .has_key_grab(modifiers.serialized.layout_effective, event.key_code()) + || self + .common + .a11y_keyboard_monitor_state + .has_key_grab(modifiers, handle.modified_sym()) { return FilterResult::Intercept(None); } diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 902c3fe1..2090ddf9 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -1454,6 +1454,7 @@ impl Common { self.popups.cleanup(); self.toplevel_info_state.refresh(&self.workspace_state); self.refresh_idle_inhibit(); + self.a11y_keyboard_monitor_state.refresh(); } pub fn refresh_idle_inhibit(&mut self) { diff --git a/src/state.rs b/src/state.rs index 3d755e4a..596cce2d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -8,6 +8,7 @@ use crate::{ x11::X11State, }, config::{CompOutputConfig, Config, ScreenFilter}, + dbus::a11y_keyboard_monitor::A11yKeyboardMonitorState, input::{PointerFocusState, gestures::GestureState}, shell::{CosmicSurface, SeatExt, Shell, grabs::SeatMoveGrabState}, utils::prelude::OutputExt, @@ -255,6 +256,7 @@ pub struct Common { pub xdg_decoration_state: XdgDecorationState, pub overlap_notify_state: OverlapNotifyState, pub a11y_state: A11yState, + pub a11y_keyboard_monitor_state: A11yKeyboardMonitorState, // shell-related wayland state pub xdg_shell_state: XdgShellState, @@ -704,6 +706,8 @@ impl State { let a11y_state = A11yState::new::(dh, client_not_sandboxed); + let a11y_keyboard_monitor_state = A11yKeyboardMonitorState::new(&async_executor); + // TODO: Restrict to only specific client? let atspi_state = AtspiState::new::(dh, client_has_no_security_context); @@ -764,6 +768,7 @@ impl State { xdg_foreign_state, workspace_state, a11y_state, + a11y_keyboard_monitor_state, xwayland_scale: None, xwayland_state: None, xwayland_shell_state,