diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/dbus/freedesktop_a11y.rs | 461 | ||||
| -rw-r--r-- | src/dbus/mod.rs | 9 | ||||
| -rw-r--r-- | src/input/mod.rs | 11 | ||||
| -rw-r--r-- | src/niri.rs | 4 |
4 files changed, 485 insertions, 0 deletions
diff --git a/src/dbus/freedesktop_a11y.rs b/src/dbus/freedesktop_a11y.rs new file mode 100644 index 00000000..47ef621f --- /dev/null +++ b/src/dbus/freedesktop_a11y.rs @@ -0,0 +1,461 @@ +// References: +// - https://invent.kde.org/plasma/kwin/-/blob/397fbbe52a8f2d855ad0c9817b51a9bdf06a68e2/src/a11ykeyboardmonitor.cpp#L41 +// - https://gitlab.gnome.org/GNOME/mutter/-/blob/cbb7295ac1f93a2dfd55a7c0544688e7e5c4d2e2/src/backends/meta-a11y-manager.c + +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Duration; + +use anyhow::Context; +use futures_util::StreamExt; +use smithay::backend::input::{KeyState, Keycode}; +use smithay::input::keyboard::{xkb, Keysym}; +use zbus::blocking::object_server::InterfaceRef; +use zbus::fdo::{self, RequestNameFlags}; +use zbus::interface; +use zbus::message::Header; +use zbus::names::{BusName, OwnedUniqueName, UniqueName}; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::NoneValue; + +use super::Start; +use crate::niri::State; + +#[derive(Debug, Default)] +struct Data { + clients: HashMap<OwnedUniqueName, Client>, + + grabbed_mods: HashSet<Keysym>, + grabbed_mod_last_press_time: HashMap<Keysym, Duration>, + suppressed_keys: HashSet<Keysym>, +} + +#[derive(Debug, Default)] +struct Client { + watched: bool, + grabbed: bool, + modifiers: HashSet<Keysym>, + keystrokes: Vec<(Keysym, u32)>, +} + +#[derive(Clone)] +pub struct KeyboardMonitor { + data: Arc<Mutex<Data>>, + iface: Arc<OnceLock<InterfaceRef<Self>>>, +} + +/// Interface for monitoring of keyboard input by assistive technologies. +/// +/// This interface is used by assistive technologies to monitor keyboard input of the compositor. +/// The compositor is expected to listen on the well-known bus name "org.freedesktop.a11y.Manager" +/// at the object path "/org/freedesktop/a11y/Manager". +#[interface(name = "org.freedesktop.a11y.KeyboardMonitor")] +impl KeyboardMonitor { + // Starts grabbing all key events. The client receives the events through the KeyEvent signal, + // and in addition, the events aren't handled normally by the compositor. This includes changes + // to the state of toggles like Caps Lock, Num Lock, and Scroll Lock. + // + // This behavior stays in effect until the same client calls UngrabKeyboard or closes its D-Bus + // connection. + async fn grab_keyboard(&self, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> { + let Some(sender) = hdr.sender() else { + return Err(fdo::Error::Failed("no sender".to_owned())); + }; + let sender = OwnedUniqueName::from(sender.to_owned()); + trace!("enabling keyboard grab for {sender}"); + + let mut data = self.data.lock().unwrap(); + let client = data.clients.entry(sender).or_default(); + client.grabbed = true; + + Ok(()) + } + + // Reverses the effect of calling GrabKeyboard. If GrabKeyboard wasn't previously called, this + // method does nothing. + // + // After calling this method, the key grabs specified in the last call to SetKeyGrabs, if any, + // are still in effect. Also, the client will still receive key events through the KeyEvent + // signal, if it has called WatchKeyboard. + async fn ungrab_keyboard(&self, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> { + let Some(sender) = hdr.sender() else { + return Err(fdo::Error::Failed("no sender".to_owned())); + }; + let sender = OwnedUniqueName::from(sender.to_owned()); + + let mut data = self.data.lock().unwrap(); + if let Some(client) = data.clients.get_mut(&sender) { + trace!("disabling keyboard grab for {sender}"); + client.grabbed = false; + } + + Ok(()) + } + + // Starts watching all key events. The client receives the events through the KeyEvent signal, + // but the events are still handled normally by the compositor. This includes changes to the + // state of toggles like Caps Lock, Num Lock, and Scroll Lock. + // + // This behavior stays in effect until the same client calls UnwatchKeyboard or closes its D-Bus + // connection. + async fn watch_keyboard(&self, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> { + let Some(sender) = hdr.sender() else { + return Err(fdo::Error::Failed("no sender".to_owned())); + }; + let sender = OwnedUniqueName::from(sender.to_owned()); + trace!("enabling keyboard watch for {sender}"); + + let mut data = self.data.lock().unwrap(); + let client = data.clients.entry(sender).or_default(); + client.watched = true; + + Ok(()) + } + + // Reverses the effect of calling WatchKeyboard. If WatchKeyboard wasn't previously called, this + // method does nothing. + // + // After calling this method, the key grabs specified in the last call to SetKeyGrabs, if any, + // are still in effect, but other key events are no longer reported to this client. + async fn unwatch_keyboard(&self, #[zbus(header)] hdr: Header<'_>) -> fdo::Result<()> { + let Some(sender) = hdr.sender() else { + return Err(fdo::Error::Failed("no sender".to_owned())); + }; + let sender = OwnedUniqueName::from(sender.to_owned()); + + let mut data = self.data.lock().unwrap(); + if let Some(client) = data.clients.get_mut(&sender) { + trace!("disabling keyboard watch for {sender}"); + client.watched = false; + } + + Ok(()) + } + + // Sets the current key grabs for the calling client, overriding any previous call to this + // method. For grabbed key events, the KeyEvent signal is emitted, and normal key event handling + // is suppressed, including state changes for toggles like Caps Lock and Num Lock. + // + // The grabs set by this method stay in effect until the same client calls this method again, or + // until that client closes its D-Bus connection. + // + // Each item in `modifiers` is an XKB keysym. All keys in this list will be grabbed, and keys + // pressed while any of these keys are down will also be grabbed. + // + // Each item in `keystrokes` is a struct with the following fields: + // + // - the XKB keysym of the non-modifier key + // - the XKB modifier mask of the modifiers, if any, for this keystroke + // + // If any of the keys in `modifiers` is pressed alone, the compositor is required to ignore the + // key press and release event if a second key press of the same modifier is not received within + // a reasonable time frame, for example, the key repeat delay. If such event is received, this + // second event is processed normally. + async fn set_key_grabs( + &self, + #[zbus(header)] hdr: Header<'_>, + modifiers: Vec<u32>, + keystrokes: Vec<(u32, u32)>, + ) -> fdo::Result<()> { + let Some(sender) = hdr.sender() else { + return Err(fdo::Error::Failed("no sender".to_owned())); + }; + let sender = OwnedUniqueName::from(sender.to_owned()); + trace!("updating key grabs for {sender}"); + + let mut data = self.data.lock().unwrap(); + let client = data.clients.entry(sender).or_default(); + client.modifiers = HashSet::from_iter(modifiers.into_iter().map(Keysym::new)); + client.keystrokes = + Vec::from_iter(keystrokes.into_iter().map(|(k, v)| (Keysym::new(k), v))); + + data.rebuild_grabbed_mods(); + + Ok(()) + } + + // The compositor emits this signal for each key press or release. + // + // - `released`: whether this is a key-up event + // - `state`: XKB modifier mask for currently pressed modifiers + // - `keysym`: XKB keysym for this key + // - `unichar`: Unicode character for this key, or 0 if none + // - `keycode`: hardware-dependent keycode for this key + #[zbus(signal)] + pub async fn key_event( + ctxt: &SignalEmitter<'_>, + released: bool, + state: u32, + keysym: u32, + unichar: u32, + keycode: u16, + ) -> zbus::Result<()>; +} + +impl KeyboardMonitor { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + data: Arc::new(Mutex::new(Data::default())), + iface: Arc::new(OnceLock::new()), + } + } + + #[allow(clippy::too_many_arguments)] + pub fn process_key( + &self, + repeat_delay: Duration, + time: Duration, + keycode: Keycode, + released: bool, + mods: u32, + keysym: Keysym, + unichar: u32, + ) -> bool { + let _span = tracy_client::span!("KeyboardMonitor::process_key"); + + let mut ctxt = self.iface.get().unwrap().signal_emitter().clone(); + + let mut data = self.data.lock().unwrap(); + + // Emit key events as necessary. + for (name, client) in &data.clients { + if client.should_watch_keypress(&data.suppressed_keys, mods, keysym) { + let _span = tracy_client::span!("emitting key event"); + + // Emit to that client only. + ctxt = ctxt.set_destination(BusName::Unique(name.as_ref())); + let ctxt = &ctxt; + async_io::block_on(async move { + if let Err(err) = KeyboardMonitor::key_event( + ctxt, + released, + mods, + keysym.raw(), + unichar, + keycode.raw() as u16, + ) + .await + { + warn!("error emitting key_event: {err:?}"); + } + }); + } + } + + // Check for double-pressed grabbed modifier that should not be captured. + if data.grabbed_mods.contains(&keysym) { + if released { + // If missing from suppressed keys, then this is a release corresponding to the + // second press that got handled normally. + if !data.suppressed_keys.contains(&keysym) { + trace!("handling release for second press of grabbed modifier: {keysym:?}"); + return false; + } + } else { + let last_press_entry = data + .grabbed_mod_last_press_time + .entry(keysym) + .or_insert(Duration::ZERO); + let last_press = *last_press_entry; + *last_press_entry = time; + + // Modifier pressed twice; handle it as normal. + if time <= last_press.saturating_add(repeat_delay) { + trace!("handling second press of grabbed modifier: {keysym:?}"); + return false; + } + } + } + + let mut block = false; + + if released { + // This is a release for a key that was grabbed. + if data.suppressed_keys.remove(&keysym) { + trace!("blocking release for previously suppressed key: {keysym:?}"); + block = true; + } + } else if data.suppressed_keys.contains(&keysym) { + // Second press for an already-pressed key, e.g. from two keyboards. + trace!("blocking press for already-pressed key: {keysym:?}"); + block = true; + } else { + // Check if it's grabbed by any client. + if data + .clients + .values() + .any(|client| client.should_grab_keypress(&data.suppressed_keys, mods, keysym)) + { + trace!("blocking press for grabbed key: {keysym:?}"); + data.suppressed_keys.insert(keysym); + block = true; + } + } + + block + } +} + +impl Data { + fn rebuild_grabbed_mods(&mut self) { + self.grabbed_mods.clear(); + for client in self.clients.values() { + self.grabbed_mods.extend(&client.modifiers); + } + } +} + +impl Client { + fn should_grab_keypress( + &self, + suppressed_keys: &HashSet<Keysym>, + mods: u32, + keysym: Keysym, + ) -> bool { + // Grabbing all keys. + if self.grabbed { + return true; + } + + for modifier in &self.modifiers { + // This is a grabbed modifier, or a grabbed modifier is currently down. + if *modifier == keysym || suppressed_keys.contains(modifier) { + return true; + } + } + + for (grabbed_keysym, grabbed_mods) in &self.keystrokes { + // This is a grabbed keystroke. + if *grabbed_keysym == keysym && *grabbed_mods == mods { + return true; + } + } + + false + } + + fn should_watch_keypress( + &self, + suppressed_keys: &HashSet<Keysym>, + mods: u32, + keysym: Keysym, + ) -> bool { + if self.watched { + return true; + } + + self.should_grab_keypress(suppressed_keys, mods, keysym) + } +} + +async fn monitor_disappeared_clients( + conn: &zbus::Connection, + data: Arc<Mutex<Data>>, +) -> anyhow::Result<()> { + let proxy = fdo::DBusProxy::new(conn) + .await + .context("error creating a DBusProxy")?; + + let mut stream = proxy + .receive_name_owner_changed_with_args(&[(2, UniqueName::null_value())]) + .await + .context("error creating a NameOwnerChanged stream")?; + + while let Some(signal) = stream.next().await { + let args = signal + .args() + .context("error retrieving NameOwnerChanged args")?; + + let Some(name) = &**args.old_owner() else { + continue; + }; + + if args.new_owner().is_none() { + trace!("keyboard monitor client disconnected: {name}"); + + let name = OwnedUniqueName::from(name.to_owned()); + let mut data = data.lock().unwrap(); + data.clients.remove(&name); + data.rebuild_grabbed_mods(); + } else { + error!("non-null new_owner should've been filtered out"); + } + } + + Ok(()) +} + +impl Start for KeyboardMonitor { + fn start(self) -> anyhow::Result<zbus::blocking::Connection> { + let data = self.data.clone(); + + let conn = zbus::blocking::Connection::session()?; + let flags = RequestNameFlags::AllowReplacement + | RequestNameFlags::ReplaceExisting + | RequestNameFlags::DoNotQueue; + + conn.object_server() + .at("/org/freedesktop/a11y/Manager", self.clone())?; + conn.request_name_with_flags("org.freedesktop.a11y.Manager", flags)?; + + let iface = conn + .object_server() + .interface("/org/freedesktop/a11y/Manager")?; + let _ = self.iface.set(iface); + + let async_conn = conn.inner().clone(); + let future = async move { + if let Err(err) = monitor_disappeared_clients(&async_conn, data.clone()).await { + warn!("error monitoring keyboard monitor clients: {err:?}"); + + // Since the monitor is now broken, prevent any further communication. + if let Err(err) = async_conn.close().await { + warn!("error closing connection: {err:?}"); + } + + let mut data = data.lock().unwrap(); + data.clients.clear(); + data.rebuild_grabbed_mods(); + } + }; + let task = conn + .inner() + .executor() + .spawn(future, "monitor disappearing keyboard clients"); + task.detach(); + + Ok(conn) + } +} + +impl State { + pub fn a11y_process_key(&mut self, time: Duration, keycode: Keycode, state: KeyState) -> bool { + if self.niri.a11y_keyboard_monitor.is_none() { + return false; + } + + let keyboard = self.niri.seat.get_keyboard().unwrap(); + + let (mods, keysym, unichar) = keyboard.with_xkb_state(self, |context| { + let xkb = context.xkb().lock().unwrap(); + // SAFETY: we're not changing the ref count. + let state = unsafe { xkb.state() }; + + let keysym = state.key_get_one_sym(keycode); + let mods = state.serialize_mods(xkb::STATE_MODS_EFFECTIVE); + let unichar = state.key_get_utf32(keycode); + + (mods, keysym, unichar) + }); + + let config = self.niri.config.borrow(); + let repeat_delay = Duration::from_millis(u64::from(config.input.keyboard.repeat_delay)); + let released = state == KeyState::Released; + + let Some(monitor) = &self.niri.a11y_keyboard_monitor else { + return false; + }; + monitor.process_key(repeat_delay, time, keycode, released, mods, keysym, unichar) + } +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index aaa7c1ae..6a164d48 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -3,6 +3,7 @@ use zbus::object_server::Interface; use crate::niri::State; +pub mod freedesktop_a11y; pub mod freedesktop_locale1; pub mod freedesktop_screensaver; pub mod gnome_shell_introspect; @@ -15,6 +16,7 @@ pub mod mutter_screen_cast; #[cfg(feature = "xdp-gnome-screencast")] use mutter_screen_cast::ScreenCast; +use self::freedesktop_a11y::KeyboardMonitor; use self::freedesktop_screensaver::ScreenSaver; use self::gnome_shell_introspect::Introspect; use self::mutter_display_config::DisplayConfig; @@ -34,6 +36,7 @@ pub struct DBusServers { #[cfg(feature = "xdp-gnome-screencast")] pub conn_screen_cast: Option<Connection>, pub conn_locale1: Option<Connection>, + pub conn_keyboard_monitor: Option<Connection>, } impl DBusServers { @@ -125,6 +128,12 @@ impl DBusServers { let screen_cast = ScreenCast::new(backend.ipc_outputs(), to_niri); dbus.conn_screen_cast = try_start(screen_cast); } + + let keyboard_monitor = KeyboardMonitor::new(); + if let Some(x) = try_start(keyboard_monitor.clone()) { + dbus.conn_keyboard_monitor = Some(x); + niri.a11y_keyboard_monitor = Some(keyboard_monitor); + } } let (to_niri, from_locale1) = calloop::channel::channel(); diff --git a/src/input/mod.rs b/src/input/mod.rs index 76f575b9..2aba1f97 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -359,6 +359,17 @@ impl State { let is_inhibiting_shortcuts = self.is_inhibiting_shortcuts(); + // Accessibility modifier grabs should override XKB state changes (e.g. Caps Lock), so we + // need to process them before keyboard.input() below. + #[cfg(feature = "dbus")] + if self.a11y_process_key( + Duration::from_millis(u64::from(time)), + event.key_code(), + event.state(), + ) { + return; + } + let Some(Some(bind)) = self.niri.seat.get_keyboard().unwrap().input( self, event.key_code(), diff --git a/src/niri.rs b/src/niri.rs index 03fe6d4f..1d82ba6e 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -385,6 +385,8 @@ pub struct Niri { #[cfg(feature = "dbus")] pub dbus: Option<crate::dbus::DBusServers>, #[cfg(feature = "dbus")] + pub a11y_keyboard_monitor: Option<crate::dbus::freedesktop_a11y::KeyboardMonitor>, + #[cfg(feature = "dbus")] pub inhibit_power_key_fd: Option<zbus::zvariant::OwnedFd>, pub ipc_server: Option<IpcServer>, @@ -2659,6 +2661,8 @@ impl Niri { #[cfg(feature = "dbus")] dbus: None, #[cfg(feature = "dbus")] + a11y_keyboard_monitor: None, + #[cfg(feature = "dbus")] inhibit_power_key_fd: None, ipc_server, |
