xwayland: Allow eavesdropping on certain keyboard/pointer events

This commit is contained in:
Victoria Brekenfeld 2025-03-28 17:45:28 +01:00 committed by Victoria Brekenfeld
parent 23f51eb150
commit cbc4ad6fc2
6 changed files with 330 additions and 24 deletions

View file

@ -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,
}

View file

@ -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<String>, state: &mut
state.common.update_xwayland_scale();
}
}
"xwayland_eavesdropping" => {
let new = get_config::<XwaylandEavesdropping>(&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::<bool>(&config, "focus_follows_cursor");
if new != state.common.config.cosmic_conf.focus_follows_cursor {

View file

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

View file

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

View file

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

View file

@ -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<X11Wm>,
pub display: u32,
pub pressed_keys: Vec<Keycode>,
pub pressed_buttons: Vec<u32>,
pub last_modifier_state: Option<ModifiersState>,
}
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<KeyboardFocusTarget>,
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<String>) {