From 2545b76dad9c5eac131ca9479ba044dea4713779 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 21 Aug 2024 20:20:27 -0700 Subject: [PATCH] Add `cosmic_atspi_v1` protocol Used to provide a backend for `AtspiDevice` in `at-spi2-core`, so Orca keybindings can work. --- Cargo.lock | 15 +- Cargo.toml | 3 +- src/config/mod.rs | 1 + src/input/mod.rs | 67 ++++++++ src/state.rs | 10 ++ src/wayland/handlers/atspi.rs | 304 +++++++++++++++++++++++++++++++++ src/wayland/handlers/mod.rs | 1 + src/wayland/protocols/atspi.rs | 162 ++++++++++++++++++ src/wayland/protocols/mod.rs | 1 + 9 files changed, 561 insertions(+), 3 deletions(-) create mode 100644 src/wayland/handlers/atspi.rs create mode 100644 src/wayland/protocols/atspi.rs diff --git a/Cargo.lock b/Cargo.lock index c55aeee8..aebb3e94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -833,6 +833,7 @@ dependencies = [ "profiling", "rand", "regex", + "reis", "ron", "rust-embed", "rustix", @@ -854,7 +855,7 @@ dependencies = [ "xcursor", "xdg", "xdg-user", - "xkbcommon 0.7.0", + "xkbcommon 0.8.0", "zbus", ] @@ -898,7 +899,7 @@ dependencies = [ [[package]] name = "cosmic-protocols" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols?branch=main#91aeb55052a8e6e15a7ddd53e039a9350f16fa69" +source = "git+https://github.com/pop-os/cosmic-protocols?branch=main#ec1616b90fa6b4568709cfe2c0627b1e8cc887e0" dependencies = [ "bitflags 2.6.0", "wayland-backend", @@ -4250,6 +4251,16 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "reis" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "827073dbe443c57fd72ae05491c6b94213218627ac6ac169850673b0cb7034f1" +dependencies = [ + "calloop 0.14.1", + "rustix", +] + [[package]] name = "renderdoc-sys" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7f69bca9..6aa22761 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,12 +54,13 @@ wayland-scanner = "0.31.1" xcursor = "0.3.3" xdg = "^2.1" xdg-user = "0.2.1" -xkbcommon = "0.7" +xkbcommon = "0.8" zbus = "4.4.0" profiling = { version = "1.0" } rustix = { version = "0.38.32", features = ["process"] } smallvec = "1.13.2" rand = "0.8.5" +reis = { version = "0.4", features = ["calloop"] } drm-ffi = "0.8.0" [dependencies.id_tree] diff --git a/src/config/mod.rs b/src/config/mod.rs index 77db2951..b944a6ed 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -640,6 +640,7 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut } } } + state.common.atspi_ei.update_keymap(value.clone()); state.common.config.cosmic_conf.xkb_config = value; } "input_default" => { diff --git a/src/input/mod.rs b/src/input/mod.rs index 02f97e3b..d36edc66 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -1464,6 +1464,13 @@ impl State { .unwrap_or(false) }); + self.common.atspi_ei.input( + modifiers, + &handle, + event.state(), + event.time() as u64 * 1000, + ); + // Leave move overview mode, if any modifier was released if let Some(Trigger::KeyboardMove(action_modifiers)) = shell.overview_mode().0.active_trigger() @@ -1625,6 +1632,57 @@ impl State { ))); } + if event.state() == KeyState::Released { + let removed = self + .common + .atspi_ei + .active_virtual_mods + .remove(&event.key_code()); + // If `Caps_Lock` is a virtual modifier, and is in locked state, clear it + if removed && handle.modified_sym() == Keysym::Caps_Lock { + if (modifiers.serialized.locked & 2) != 0 { + let serial = SERIAL_COUNTER.next_serial(); + let time = self.common.clock.now().as_millis(); + keyboard.input( + self, + event.key_code(), + KeyState::Pressed, + serial, + time, + |_, _, _| FilterResult::<()>::Forward, + ); + let serial = SERIAL_COUNTER.next_serial(); + keyboard.input( + self, + event.key_code(), + KeyState::Released, + serial, + time, + |_, _, _| FilterResult::<()>::Forward, + ); + } + } + } else if event.state() == KeyState::Pressed + && self + .common + .atspi_ei + .virtual_mods + .contains(&event.key_code()) + { + self.common + .atspi_ei + .active_virtual_mods + .insert(event.key_code()); + + tracing::debug!( + "active virtual mods: {:?}", + self.common.atspi_ei.active_virtual_mods + ); + seat.supressed_keys().add(&handle, None); + + return FilterResult::Intercept(None); + } + // Skip released events for initially surpressed keys if event.state() == KeyState::Released { if let Some(tokens) = seat.supressed_keys().filter(&handle) { @@ -1649,6 +1707,15 @@ impl State { return FilterResult::Intercept(None); } + if self.common.atspi_ei.has_keyboard_grab() + || self + .common + .atspi_ei + .has_key_grab(modifiers.serialized.layout_effective, event.key_code()) + { + return FilterResult::Intercept(None); + } + // handle the rest of the global shortcuts let mut clear_queue = true; if !shortcuts_inhibited { diff --git a/src/state.rs b/src/state.rs index b3e641ef..bc89614b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,6 +12,7 @@ use crate::{ shell::{grabs::SeatMoveGrabState, CosmicSurface, SeatExt, Shell}, utils::prelude::OutputExt, wayland::protocols::{ + atspi::AtspiState, drm::WlDrmState, image_source::ImageSourceState, output_configuration::OutputConfigurationState, @@ -229,6 +230,9 @@ pub struct Common { pub xwayland_state: Option, pub xwayland_shell_state: XWaylandShellState, pub pointer_focus_state: Option, + + pub atspi_state: AtspiState, + pub atspi_ei: crate::wayland::handlers::atspi::AtspiEiState, } #[derive(Debug)] @@ -559,6 +563,9 @@ impl State { tracing::warn!(?err, "Failed to initialize dbus handlers"); } + // TODO: Restrict to only specific client? + let atspi_state = AtspiState::new::(dh, client_is_privileged); + State { common: Common { config, @@ -615,6 +622,9 @@ impl State { xwayland_state: None, xwayland_shell_state, pointer_focus_state: None, + + atspi_state, + atspi_ei: Default::default(), }, backend: BackendData::Unset, ready: Once::new(), diff --git a/src/wayland/handlers/atspi.rs b/src/wayland/handlers/atspi.rs new file mode 100644 index 00000000..277e0ef4 --- /dev/null +++ b/src/wayland/handlers/atspi.rs @@ -0,0 +1,304 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_comp_config::XkbConfig; +use cosmic_protocols::atspi::v1::server::cosmic_atspi_manager_v1::CosmicAtspiManagerV1; +use reis::{ + calloop::{EisRequestSource, EisRequestSourceEvent}, + eis::{self, device::DeviceType}, + request::{Connection, Device, DeviceCapability, EisRequest, Seat}, +}; +use smithay::{ + backend::input::{KeyState, Keycode}, + input::keyboard::ModifiersState, + utils::SealedFile, +}; +use std::{ + collections::{HashMap, HashSet}, + ffi::{CStr, CString}, + mem, + os::unix::{io::AsFd, net::UnixStream}, +}; +use xkbcommon::xkb; + +use crate::{ + state::State, + wayland::protocols::atspi::{delegate_atspi, AtspiHandler}, +}; + +#[derive(PartialEq, Debug)] +pub struct AtspiKeyGrab { + pub mods: u32, + pub virtual_mods: HashSet, + pub key: Keycode, +} + +#[derive(Debug, Default)] +struct AtspiClient { + key_grabs: Vec, + has_keyboard_grab: bool, + // TODO: purge old instances + keyboards: Vec<(Connection, Device, eis::Keyboard)>, +} + +impl AtspiClient { + fn add_keyboard( + &mut self, + connection: &Connection, + seat: &Seat, + keymap: &xkb::Keymap, + modifiers: &ModifiersState, + ) { + let keymap_text = keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1); + let name = CStr::from_bytes_with_nul(b"eis-keymap\0").unwrap(); + let file = SealedFile::with_content(name, &CString::new(keymap_text).unwrap()).unwrap(); + + let device = seat.add_device( + Some("keyboard"), + DeviceType::Virtual, + &[DeviceCapability::Keyboard], + |device| { + let keyboard = device.interface::().unwrap(); + keyboard.keymap( + eis::keyboard::KeymapType::Xkb, + file.size() as u32 - 1, + file.as_fd(), + ); + }, + ); + device.resumed(); + + let keyboard = device.interface::().unwrap(); + + connection.with_next_serial(|serial| { + keyboard.modifiers( + serial, + modifiers.serialized.depressed, + modifiers.serialized.locked, + modifiers.serialized.latched, + modifiers.serialized.layout_effective, + ) + }); + + device.start_emulating(0); + + self.keyboards.push((connection.clone(), device, keyboard)); + } +} + +#[derive(Debug, Default)] +pub struct AtspiEiState { + modifiers: ModifiersState, + clients: HashMap, + pub virtual_mods: HashSet, + pub active_virtual_mods: HashSet, +} + +impl AtspiEiState { + pub fn input( + &mut self, + modifiers: &smithay::input::keyboard::ModifiersState, + keysym: &smithay::input::keyboard::KeysymHandle, + state: KeyState, + time: u64, + ) { + let state = match state { + KeyState::Pressed => eis::keyboard::KeyState::Press, + KeyState::Released => eis::keyboard::KeyState::Released, + }; + if &self.modifiers != modifiers { + self.modifiers = *modifiers; + for client in self.clients.values() { + for (connection, _, keyboard) in &client.keyboards { + connection.with_next_serial(|serial| { + keyboard.modifiers( + serial, + modifiers.serialized.depressed, + modifiers.serialized.locked, + modifiers.serialized.latched, + modifiers.serialized.layout_effective, + ) + }); + } + } + } + for client in self.clients.values() { + for (connection, device, keyboard) in &client.keyboards { + keyboard.key(keysym.raw_code().raw() - 8, state); + device.frame(time); + let _ = connection.flush(); + } + } + } + + pub fn has_keyboard_grab(&self) -> bool { + self.clients.values().any(|client| client.has_keyboard_grab) + } + + /// Key grab exists for mods, key, with active virtual mods + pub fn has_key_grab(&self, mods: u32, key: Keycode) -> bool { + self.clients + .values() + .flat_map(|client| &client.key_grabs) + .any(|grab| { + grab.mods == mods + && grab.virtual_mods == self.active_virtual_mods + && grab.key == key + }) + } + + fn update_virtual_mods(&mut self) { + self.virtual_mods.clear(); + self.virtual_mods.extend( + self.clients + .values() + .flat_map(|client| &client.key_grabs) + .flat_map(|grab| &grab.virtual_mods), + ); + } + + pub fn update_keymap(&mut self, xkb_config: XkbConfig) { + let keymap = keymap_or_default(xkb_config); + for client in self.clients.values_mut() { + let old_keyboards = mem::take(&mut client.keyboards); + for (connection, device, _keyboard) in old_keyboards { + device.remove(); + client.add_keyboard(&connection, device.seat(), &keymap, &self.modifiers); + let _ = connection.flush(); + } + } + } +} + +impl AtspiHandler for State { + fn client_connected(&mut self, manager: &CosmicAtspiManagerV1, socket: UnixStream) { + self.common + .atspi_ei + .clients + .insert(manager.clone(), AtspiClient::default()); + + let context = eis::Context::new(socket).unwrap(); + let source = EisRequestSource::new(context, 0); + let manager = manager.clone(); + self.common + .event_loop_handle + .insert_source(source, move |event, connected_state, state| { + Ok(handle_event(&manager, event, connected_state, state)) + }) + .unwrap(); + } + + fn client_disconnected(&mut self, manager: &CosmicAtspiManagerV1) { + self.common.atspi_ei.clients.remove(manager); + self.common.atspi_ei.update_virtual_mods(); + } + + fn add_key_grab( + &mut self, + manager: &CosmicAtspiManagerV1, + mods: u32, + virtual_mods: Vec, + key: Keycode, + ) { + let grab = AtspiKeyGrab { + mods, + virtual_mods: virtual_mods.into_iter().collect(), + key, + }; + let client = self.common.atspi_ei.clients.get_mut(manager).unwrap(); + client.key_grabs.push(grab); + self.common.atspi_ei.update_virtual_mods(); + } + + fn remove_key_grab( + &mut self, + manager: &CosmicAtspiManagerV1, + mods: u32, + virtual_mods: Vec, + key: Keycode, + ) { + let grab = AtspiKeyGrab { + mods, + virtual_mods: virtual_mods.into_iter().collect(), + key, + }; + let client = self.common.atspi_ei.clients.get_mut(manager).unwrap(); + if let Some(idx) = client.key_grabs.iter().position(|x| *x == grab) { + client.key_grabs.remove(idx); + } + self.common.atspi_ei.update_virtual_mods(); + } + + fn grab_keyboard(&mut self, manager: &CosmicAtspiManagerV1) { + let client = self.common.atspi_ei.clients.get_mut(manager).unwrap(); + client.has_keyboard_grab = true; + } + + fn ungrab_keyboard(&mut self, manager: &CosmicAtspiManagerV1) { + let client = self.common.atspi_ei.clients.get_mut(manager).unwrap(); + client.has_keyboard_grab = false; + } +} + +fn handle_event( + manager: &CosmicAtspiManagerV1, + event: Result, + connection: &Connection, + state: &mut State, +) -> calloop::PostAction { + let Some(client) = state.common.atspi_ei.clients.get_mut(manager) else { + return calloop::PostAction::Remove; + }; + match event { + Ok(EisRequestSourceEvent::Connected) => { + if connection.context_type() != reis::ei::handshake::ContextType::Receiver { + return calloop::PostAction::Remove; + } + let _seat = connection.add_seat(Some("default"), &[DeviceCapability::Keyboard]); + } + Ok(EisRequestSourceEvent::Request(EisRequest::Disconnect)) => { + return calloop::PostAction::Remove; + } + Ok(EisRequestSourceEvent::Request(EisRequest::Bind(request))) => { + if connection.has_interface("ei_keyboard") + && request.capabilities & 2 << DeviceCapability::Keyboard as u64 != 0 + { + let keymap = keymap_or_default(state.common.config.xkb_config()); + client.add_keyboard( + connection, + &request.seat, + &keymap, + &state.common.atspi_ei.modifiers, + ); + } + } + Ok(EisRequestSourceEvent::Request(_request)) => { + // seat / keyboard / device release? + } + Ok(EisRequestSourceEvent::InvalidObject(_)) => {} + Err(_) => { + // TODO + } + } + let _ = connection.flush(); + calloop::PostAction::Continue +} + +// TODO: use keymap of seat? +fn keymap_or_default(xkb_config: XkbConfig) -> xkb::Keymap { + keymap(xkb_config).unwrap_or_else(|| keymap(XkbConfig::default()).unwrap()) +} + +fn keymap(xkb_config: XkbConfig) -> Option { + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + xkb::Keymap::new_from_names( + &context, + &xkb_config.rules, + &xkb_config.model, + &xkb_config.layout, + &xkb_config.variant, + xkb_config.options.clone(), + xkb::KEYMAP_COMPILE_NO_FLAGS, + ) +} + +delegate_atspi!(State); diff --git a/src/wayland/handlers/mod.rs b/src/wayland/handlers/mod.rs index 78604722..52414103 100644 --- a/src/wayland/handlers/mod.rs +++ b/src/wayland/handlers/mod.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only pub mod alpha_modifier; +pub mod atspi; pub mod buffer; pub mod compositor; pub mod data_control; diff --git a/src/wayland/protocols/atspi.rs b/src/wayland/protocols/atspi.rs new file mode 100644 index 00000000..c4f73ee7 --- /dev/null +++ b/src/wayland/protocols/atspi.rs @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use cosmic_protocols::atspi::v1::server::cosmic_atspi_manager_v1; + +use smithay::{ + backend::input::Keycode, + reexports::wayland_server::{ + backend::GlobalId, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, + }, +}; +use std::os::unix::{io::AsFd, net::UnixStream}; +use wayland_backend::server::ClientId; + +pub trait AtspiHandler { + fn client_connected( + &mut self, + manager: &cosmic_atspi_manager_v1::CosmicAtspiManagerV1, + key_event_socket: UnixStream, + ); + fn client_disconnected(&mut self, manager: &cosmic_atspi_manager_v1::CosmicAtspiManagerV1); + fn add_key_grab( + &mut self, + manager: &cosmic_atspi_manager_v1::CosmicAtspiManagerV1, + mods: u32, + virtual_mods: Vec, + key: Keycode, + ); + fn remove_key_grab( + &mut self, + manager: &cosmic_atspi_manager_v1::CosmicAtspiManagerV1, + mods: u32, + virtual_mods: Vec, + key: Keycode, + ); + fn grab_keyboard(&mut self, manager: &cosmic_atspi_manager_v1::CosmicAtspiManagerV1); + fn ungrab_keyboard(&mut self, manager: &cosmic_atspi_manager_v1::CosmicAtspiManagerV1); +} + +#[derive(Debug)] +pub struct AtspiState { + global: GlobalId, +} + +impl AtspiState { + pub fn new(dh: &DisplayHandle, client_filter: F) -> AtspiState + where + D: GlobalDispatch + 'static, + F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, + { + let global = dh.create_global::( + 1, + AtspiGlobalData { + filter: Box::new(client_filter), + }, + ); + AtspiState { global } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } +} + +pub struct AtspiGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +impl GlobalDispatch + for AtspiState +where + D: GlobalDispatch + + Dispatch + + AtspiHandler + + 'static, +{ + fn bind( + state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &AtspiGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let instance = data_init.init(resource, ()); + let (client_socket, server_socket) = UnixStream::pair().unwrap(); + state.client_connected(&instance, server_socket); + instance.key_events_eis(client_socket.as_fd()); + } + + fn can_view(client: Client, global_data: &AtspiGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for AtspiState +where + D: Dispatch + AtspiHandler + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + manager: &cosmic_atspi_manager_v1::CosmicAtspiManagerV1, + request: cosmic_atspi_manager_v1::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + cosmic_atspi_manager_v1::Request::AddKeyGrab { + mods, + virtual_mods, + key, + } => { + let virtual_mods = virtual_mods + .chunks_exact(4) + .map(|x| (u32::from_ne_bytes(<[u8; 4]>::try_from(x).unwrap()) + 8).into()) + .collect(); + state.add_key_grab(manager, mods, virtual_mods, (key + 8).into()); + } + cosmic_atspi_manager_v1::Request::RemoveKeyGrab { + mods, + virtual_mods, + key, + } => { + let virtual_mods = virtual_mods + .chunks_exact(4) + .map(|x| (u32::from_ne_bytes(<[u8; 4]>::try_from(x).unwrap()) + 8).into()) + .collect(); + state.remove_key_grab(manager, mods, virtual_mods, (key + 8).into()); + } + cosmic_atspi_manager_v1::Request::GrabKeyboard => { + state.grab_keyboard(manager); + } + cosmic_atspi_manager_v1::Request::UngrabKeyboard => { + state.ungrab_keyboard(manager); + } + cosmic_atspi_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + manager: &cosmic_atspi_manager_v1::CosmicAtspiManagerV1, + _data: &(), + ) { + state.client_disconnected(manager); + } +} + +macro_rules! delegate_atspi { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::atspi::v1::server::cosmic_atspi_manager_v1::CosmicAtspiManagerV1: $crate::wayland::protocols::atspi::AtspiGlobalData + ] => $crate::wayland::protocols::atspi::AtspiState); + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + cosmic_protocols::atspi::v1::server::cosmic_atspi_manager_v1::CosmicAtspiManagerV1: () + ] => $crate::wayland::protocols::atspi::AtspiState); + }; +} +pub(crate) use delegate_atspi; diff --git a/src/wayland/protocols/mod.rs b/src/wayland/protocols/mod.rs index 7d18b003..3fe5285d 100644 --- a/src/wayland/protocols/mod.rs +++ b/src/wayland/protocols/mod.rs @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only +pub mod atspi; pub mod drm; pub mod image_source; pub mod output_configuration;