diff --git a/cosmic-comp-config/src/lib.rs b/cosmic-comp-config/src/lib.rs index c120c5d3..f5346c02 100644 --- a/cosmic-comp-config/src/lib.rs +++ b/cosmic-comp-config/src/lib.rs @@ -46,6 +46,8 @@ pub struct CosmicCompConfig { pub focus_follows_cursor_delay: u64, /// Let X11 applications scale themselves pub descale_xwayland: bool, + /// Let X11 applications snoop on certain key-presses to allow for global shortcuts + pub xwayland_eavesdropping: XwaylandEavesdropping, /// The threshold before windows snap themselves to output edges pub edge_snap_threshold: u32, pub accessibility_zoom: ZoomConfig, @@ -79,6 +81,7 @@ impl Default for CosmicCompConfig { cursor_follows_focus: false, focus_follows_cursor_delay: 250, descale_xwayland: false, + xwayland_eavesdropping: XwaylandEavesdropping::default(), edge_snap_threshold: 0, accessibility_zoom: ZoomConfig::default(), } @@ -154,3 +157,18 @@ pub enum ZoomMovement { Centered, Continuously, } + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub struct XwaylandEavesdropping { + pub keyboard: EavesdroppingKeyboardMode, + pub pointer: bool, +} + +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] +pub enum EavesdroppingKeyboardMode { + None, + Modifiers, + #[default] + Combinations, + All, +} diff --git a/src/config/mod.rs b/src/config/mod.rs index bc72918e..5842520e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -45,7 +45,7 @@ pub use self::types::*; use cosmic::config::CosmicTk; use cosmic_comp_config::{ input::InputConfig, workspace::WorkspaceConfig, CosmicCompConfig, KeyboardConfig, TileBehavior, - XkbConfig, ZoomConfig, + XkbConfig, XwaylandEavesdropping, ZoomConfig, }; #[derive(Debug)] @@ -923,6 +923,15 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut state.common.update_xwayland_scale(); } } + "xwayland_eavesdropping" => { + let new = get_config::(&config, "xwayland_eavesdropping"); + if new != state.common.config.cosmic_conf.xwayland_eavesdropping { + state.common.config.cosmic_conf.xwayland_eavesdropping = new; + state + .common + .xwayland_reset_eavesdropping(SERIAL_COUNTER.next_serial()); + } + } "focus_follows_cursor" => { let new = get_config::(&config, "focus_follows_cursor"); if new != state.common.config.cosmic_conf.focus_follows_cursor { diff --git a/src/input/mod.rs b/src/input/mod.rs index e65d759f..5a9de2e8 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -237,9 +237,40 @@ impl State { .lock() .unwrap() = Some(serial); } - Self::filter_keyboard_input( + + let current_focus = seat.get_keyboard().unwrap().current_focus(); + let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { + f.wl_surface() + .and_then(|surface| { + seat.keyboard_shortcuts_inhibitor_for_surface(&surface) + .map(|inhibitor| inhibitor.is_active()) + }) + .unwrap_or(false) + }); + let sym = handle.modified_sym(); + + let result = Self::filter_keyboard_input( data, &event, &seat, modifiers, handle, serial, - ) + ); + + if (matches!(result, FilterResult::Forward) + && !seat.get_keyboard().unwrap().is_grabbed() + && !shortcuts_inhibited + && !matches!( + current_focus, + Some(KeyboardFocusTarget::LockSurface(_)) + )) + // we don't want to accidentally leave any keys pressed + // and do more filtering in `xwayland_notify_key_event` + // for released keys + || state == KeyState::Released + { + data.common.xwayland_notify_key_event( + sym, keycode, state, serial, time, + ); + } + + result }, ) .flatten() @@ -652,7 +683,7 @@ impl State { self.common.idle_notifier_state.notify_activity(&seat); let current_focus = seat.get_keyboard().unwrap().current_focus(); - let shortcuts_inhibited = current_focus.is_some_and(|f| { + let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { f.wl_surface() .and_then(|surface| { seat.keyboard_shortcuts_inhibitor_for_surface(&surface) @@ -663,6 +694,7 @@ impl State { let serial = SERIAL_COUNTER.next_serial(); let button = event.button_code(); + let mut pass_event = !seat.supressed_buttons().remove(button); if event.state() == ButtonState::Pressed { // change the keyboard focus unless the pointer is grabbed @@ -810,6 +842,18 @@ impl State { std::mem::drop(shell); }; + if pass_event + && !matches!(current_focus, Some(KeyboardFocusTarget::LockSurface(_))) + && !shortcuts_inhibited + { + self.common.xwayland_notify_pointer_button_event( + button, + event.state(), + serial, + event.time_msec(), + ); + } + let ptr = seat.get_pointer().unwrap(); if pass_event { ptr.button( diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs index fad3aa89..5dd50f34 100644 --- a/src/shell/focus/mod.rs +++ b/src/shell/focus/mod.rs @@ -274,12 +274,12 @@ fn update_focus_state( } } + let serial = serial.unwrap_or_else(|| SERIAL_COUNTER.next_serial()); + state + .common + .xwayland_notify_focus_change(target.cloned(), serial); ActiveFocus::set(seat, target.cloned()); - keyboard.set_focus( - state, - target.cloned(), - serial.unwrap_or_else(|| SERIAL_COUNTER.next_serial()), - ); + keyboard.set_focus(state, target.cloned(), serial); std::mem::drop(keyboard); //update the focused output or set it to the active output diff --git a/src/shell/focus/target.rs b/src/shell/focus/target.rs index 77e0c941..d211ac59 100644 --- a/src/shell/focus/target.rs +++ b/src/shell/focus/target.rs @@ -28,9 +28,12 @@ use smithay::{ }, Seat, }, - reexports::wayland_server::{backend::ObjectId, protocol::wl_surface::WlSurface, Resource}, + reexports::wayland_server::{ + backend::ObjectId, protocol::wl_surface::WlSurface, Client, Resource, + }, utils::{IsAlive, Logical, Point, Serial, Transform}, wayland::{seat::WaylandFocus, session_lock::LockSurface}, + xwayland::xwm::XwmId, }; #[derive(Debug, Clone, PartialEq)] @@ -155,6 +158,15 @@ impl PointerFocusTarget { _ => None, } } + + pub fn is_client(&self, client: &Client) -> bool { + match self { + PointerFocusTarget::WlSurface { surface, .. } => { + surface.client().is_some_and(|c| c == *client) + } + _ => false, + } + } } impl KeyboardFocusTarget { @@ -167,6 +179,24 @@ impl KeyboardFocusTarget { _ => None, } } + + pub fn is_xwm(&self, xwm: XwmId) -> bool { + match self { + KeyboardFocusTarget::Element(mapped) => { + if let Some(surface) = mapped.active_window().x11_surface() { + return surface.xwm_id().unwrap() == xwm; + } + } + KeyboardFocusTarget::Fullscreen(surface) => { + if let Some(surface) = surface.x11_surface() { + return surface.xwm_id().unwrap() == xwm; + } + } + _ => {} + } + + false + } } #[derive(Debug, Clone)] diff --git a/src/xwayland.rs b/src/xwayland.rs index 71a24d98..eba47b42 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -11,12 +11,16 @@ use crate::{ toplevel_management::minimize_rectangle, xdg_activation::ActivationContext, }, }; +use cosmic_comp_config::EavesdroppingKeyboardMode; use smithay::{ - backend::drm::DrmNode, + backend::{ + drm::DrmNode, + input::{ButtonState, KeyState, Keycode}, + }, desktop::space::SpaceElement, - input::pointer::CursorIcon, + input::{keyboard::ModifiersState, pointer::CursorIcon}, reexports::{wayland_server::Client, x11rb::protocol::xproto::Window as X11Window}, - utils::{Logical, Point, Rectangle, Size, SERIAL_COUNTER}, + utils::{Logical, Point, Rectangle, Serial, Size, SERIAL_COUNTER}, wayland::{ selection::{ data_device::{ @@ -37,12 +41,16 @@ use smithay::{ }, }; use tracing::{error, trace, warn}; +use xkbcommon::xkb::Keysym; #[derive(Debug)] pub struct XWaylandState { pub client: Client, pub xwm: Option, pub display: u32, + pub pressed_keys: Vec, + pub pressed_buttons: Vec, + pub last_modifier_state: Option, } impl State { @@ -84,6 +92,9 @@ impl State { client: client.clone(), xwm: None, display: display_number, + pressed_keys: Vec::new(), + pressed_buttons: Vec::new(), + last_modifier_state: None, }); let mut wm = match X11Wm::start_wm( @@ -137,31 +148,225 @@ impl State { } impl Common { - fn is_x_focused(&self, xwm: XwmId) -> bool { - if let Some(keyboard) = self + fn has_x_keyboard_focus(&self, xwmid: XwmId) -> bool { + let keyboard = self .shell .read() .unwrap() .seats .last_active() .get_keyboard() + .unwrap(); + + keyboard + .current_focus() + .is_some_and(|target| target.is_xwm(xwmid)) + } + + fn has_x_pointer_focus(&self, xwmid: XwmId) -> bool { + let pointer = self + .shell + .read() + .unwrap() + .seats + .last_active() + .get_pointer() + .unwrap(); + + if let Some(x_client) = self.xwayland_state.as_ref().and_then(|xstate| { + xstate + .xwm + .as_ref() + .is_some_and(|xwm| xwm.id() == xwmid) + .then_some(&xstate.client) + }) { + pointer + .current_focus() + .is_some_and(|target| target.is_client(x_client)) + } else { + false + } + } + + pub fn xwayland_notify_focus_change( + &mut self, + target: Option, + serial: Serial, + ) { + if let Some(xwm_id) = self + .xwayland_state + .as_ref() + .and_then(|xstate| xstate.xwm.as_ref()) + .map(|xwm| xwm.id()) { - match keyboard.current_focus() { - Some(KeyboardFocusTarget::Element(mapped)) => { - if let Some(surface) = mapped.active_window().x11_surface() { - return surface.xwm_id().unwrap() == xwm; + if target + .as_ref() + .is_some_and(|target| matches!(target, KeyboardFocusTarget::LockSurface(_))) + || (!self.has_x_keyboard_focus(xwm_id) + && target.as_ref().is_some_and(|target| target.is_xwm(xwm_id))) + { + self.xwayland_reset_eavesdropping(serial); + } + } + } + + pub fn xwayland_reset_eavesdropping(&mut self, serial: Serial) { + let seat = self.shell.read().unwrap().seats.last_active().clone(); + let keyboard = seat.get_keyboard().unwrap(); + let pointer = seat.get_pointer().unwrap(); + + let xstate = self.xwayland_state.as_mut().unwrap(); + xstate.last_modifier_state.take(); + for key in xstate.pressed_keys.drain(..).rev() { + for wl_keyboard in keyboard.client_keyboards(&xstate.client) { + wl_keyboard.key(serial.into(), 0, key.raw() - 8, KeyState::Released.into()); + } + } + for button in xstate.pressed_buttons.drain(..).rev() { + for wl_pointer in pointer.client_pointers(&xstate.client) { + wl_pointer.button(serial.into(), 0, button, ButtonState::Released.into()); + } + } + } + + pub fn xwayland_notify_key_event( + &mut self, + sym: Keysym, + code: Keycode, + state: KeyState, + serial: Serial, + time: u32, + ) { + let config = self.config.cosmic_conf.xwayland_eavesdropping.keyboard; + if config == EavesdroppingKeyboardMode::None { + return; + } + + if self.xwayland_state.as_ref().is_none_or(|xstate| { + xstate + .xwm + .as_ref() + .is_none_or(|xwm| self.has_x_keyboard_focus(xwm.id())) + }) { + return; + } + + let keyboard = self + .shell + .read() + .unwrap() + .seats + .last_active() + .get_keyboard() + .unwrap(); + let modifiers = keyboard.modifier_state(); + let is_modifier = sym.is_modifier_key(); + + let xstate = self.xwayland_state.as_mut().unwrap(); + if state == KeyState::Pressed { + match config { + EavesdroppingKeyboardMode::Modifiers => { + if !is_modifier { + return; } } - Some(KeyboardFocusTarget::Fullscreen(surface)) => { - if let Some(surface) = surface.x11_surface() { - return surface.xwm_id().unwrap() == xwm; + EavesdroppingKeyboardMode::Combinations => { + // don't forward alpha-numeric keys, just because shift is held, but forward shift itself + if !is_modifier && !(modifiers.alt || modifiers.ctrl || modifiers.logo) { + return; } } _ => {} } + + xstate.pressed_keys.push(code); + } else { + let mut removed = false; + xstate.pressed_keys.retain(|c| { + if *c == code { + removed = true; + false + } else { + true + } + }); + + if !removed { + // Don't forward released events, we don't have a record off. + return; + } } - false + tracing::trace!("Forwaring key {} {:?} to xwayland", code.raw() - 8, state); + for wl_keyboard in keyboard.client_keyboards(&xstate.client) { + wl_keyboard.key(serial.into(), time, code.raw() - 8, state.into()); + if xstate.last_modifier_state != Some(modifiers) { + xstate.last_modifier_state = Some(modifiers); + wl_keyboard.modifiers( + serial.into(), + modifiers.serialized.depressed, + modifiers.serialized.latched, + modifiers.serialized.locked, + modifiers.serialized.layout_effective, + ); + } + } + } + + pub fn xwayland_notify_pointer_button_event( + &mut self, + button: u32, + state: ButtonState, + serial: Serial, + time: u32, + ) { + if !self.config.cosmic_conf.xwayland_eavesdropping.pointer { + return; + } + + let pointer = self + .shell + .read() + .unwrap() + .seats + .last_active() + .get_pointer() + .unwrap(); + + if self.xwayland_state.as_ref().is_none_or(|xstate| { + xstate + .xwm + .as_ref() + .is_none_or(|xwm| self.has_x_pointer_focus(xwm.id())) + }) { + return; + } + + let xstate = self.xwayland_state.as_mut().unwrap(); + if state == ButtonState::Pressed { + xstate.pressed_buttons.push(button); + } else { + let mut removed = false; + xstate.pressed_buttons.retain(|b| { + if *b == button { + removed = true; + false + } else { + true + } + }); + + if !removed { + // Don't forward released events, we don't have a record off. + // This can happen if `xwayland_reset_eavesdropping` was called in between + return; + } + } + + tracing::trace!("Forwaring ptr button {} {:?} to Xwayland", button, state); + for wl_pointer in pointer.client_pointers(&xstate.client) { + wl_pointer.button(serial.into(), time, button, state.into()); + } } pub fn update_x11_stacking_order(&mut self) { @@ -756,7 +961,7 @@ impl XwmHandler for State { } fn allow_selection_access(&mut self, xwm: XwmId, _selection: SelectionTarget) -> bool { - self.common.is_x_focused(xwm) + self.common.has_x_keyboard_focus(xwm) } fn new_selection(&mut self, xwm: XwmId, selection: SelectionTarget, mime_types: Vec) {