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,