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.
This commit is contained in:
Ian Douglas Scott 2025-05-14 16:21:57 -07:00 committed by Ian Douglas Scott
parent d08ac9645b
commit f065143d3e
5 changed files with 358 additions and 2 deletions

View file

@ -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<Keysym>,
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<Keysym>,
key_grabs: Vec<KeyGrab>,
}
#[derive(Debug, Default)]
struct Clients(HashMap<UniqueName<'static>, 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<Mutex<Clients>>,
active_virtual_mods: HashSet<Keysym>,
conn: Arc<OnceLock<zbus::Connection>>,
name_owners: Arc<OnceLock<NameOwners>>,
}
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<Keysym> {
&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<Mutex<Clients>>,
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<u32>,
keystrokes: Vec<(u32, u32)>,
) -> zbus::fdo::Result<()> {
let virtual_mods = virtual_mods
.into_iter()
.map(Keysym::from)
.collect::<Vec<_>>();
let key_grabs = keystrokes
.into_iter()
.map(|(k, mods)| KeyGrab::new(&virtual_mods, Keysym::from(k), mods))
.collect::<Vec<_>>();
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::<HashSet<_>>();
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<Mutex<Clients>>,
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))
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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) {

View file

@ -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::<State, _>(dh, client_not_sandboxed);
let a11y_keyboard_monitor_state = A11yKeyboardMonitorState::new(&async_executor);
// TODO: Restrict to only specific client?
let atspi_state = AtspiState::new::<State, _>(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,