xwayland-satellite/src/xstate/selection.rs
Shawn Wallace 5a184d4359 Support primary selection
This was more tedious than expected.
Fixes #103
2025-08-14 20:59:01 -04:00

753 lines
25 KiB
Rust

use super::{get_atom_name, XState};
use crate::server::selection::{Clipboard, ForeignSelection, Primary, SelectionType};
use crate::{RealServerState, X11Selection};
use log::{debug, error, warn};
use smithay_client_toolkit::data_device_manager::WritePipe;
use std::cell::RefCell;
use std::io::Write;
use std::rc::Rc;
use xcb::x;
#[derive(Debug)]
struct SelectionTargetId {
name: String,
atom: x::Atom,
source: Option<String>,
}
struct PendingSelectionData {
target: x::Atom,
pipe: WritePipe,
incr: bool,
}
pub struct Selection {
mimes: Vec<SelectionTargetId>,
connection: Rc<xcb::Connection>,
window: x::Window,
pending: RefCell<Vec<PendingSelectionData>>,
selection: x::Atom,
selection_time: u32,
incr: x::Atom,
}
impl X11Selection for Selection {
fn mime_types(&self) -> Vec<&str> {
self.mimes
.iter()
.map(|target| target.name.as_str())
.collect()
}
fn write_to(&self, mime: &str, pipe: WritePipe) {
if let Some(target) = self.mimes.iter().find(|target| target.name == mime) {
// We use the target as the property to write to
if let Err(e) = self
.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.window,
selection: self.selection,
target: target.atom,
property: target.atom,
time: self.selection_time,
})
{
error!("Failed to request selection data (mime type: {mime}, error: {e})");
return;
}
self.pending.borrow_mut().push(PendingSelectionData {
target: target.atom,
pipe,
incr: false,
})
} else {
warn!("Could not find mime type {mime}");
}
}
}
impl Selection {
fn handle_notify(&self, target: x::Atom) {
let mut pending = self.pending.borrow_mut();
let Some(idx) = pending.iter().position(|t| t.target == target) else {
warn!(
"Got selection notify for unknown target {}",
get_atom_name(&self.connection, target),
);
return;
};
let PendingSelectionData {
mut pipe,
incr,
target,
} = pending.swap_remove(idx);
let reply = match get_property_any(&self.connection, self.window, target) {
Ok(reply) => reply,
Err(e) => {
warn!(
"Couldn't get mime type for {}: {e:?}",
get_atom_name(&self.connection, target)
);
return;
}
};
debug!(
"got type {} for mime type {}",
get_atom_name(&self.connection, reply.r#type()),
get_atom_name(&self.connection, target)
);
if reply.r#type() == self.incr {
debug!(
"beginning incr for {}",
get_atom_name(&self.connection, target)
);
pending.push(PendingSelectionData {
target,
pipe,
incr: true,
});
return;
}
let data = match reply.format() {
8 => reply.value::<u8>(),
32 => unsafe { reply.value::<u32>().align_to().1 },
other => {
warn!("Unexpected format {other} in selection reply");
return;
}
};
if !incr || !data.is_empty() {
if let Err(e) = pipe.write_all(data) {
warn!("Failed to write selection data: {e:?}");
} else if incr {
debug!(
"received some incr data for {}",
get_atom_name(&self.connection, target)
);
pending.push(PendingSelectionData {
target,
pipe,
incr: true,
})
}
} else if incr {
// data is empty
debug!(
"completed incr for mime {}",
get_atom_name(&self.connection, target)
);
}
}
fn check_for_incr(&self, event: &x::PropertyNotifyEvent) -> bool {
if event.window() != self.window || event.state() != x::Property::NewValue {
return false;
}
let target = self.pending.borrow().iter().find_map(|pending| {
(pending.target == event.atom() && pending.incr).then_some(pending.target)
});
if let Some(target) = target {
self.handle_notify(target);
true
} else {
false
}
}
}
enum CurrentSelection<T: SelectionType> {
X11(Rc<Selection>),
Wayland {
mimes: Vec<SelectionTargetId>,
inner: ForeignSelection<T>,
},
}
struct SelectionData<T: SelectionType> {
last_selection_timestamp: u32,
atom: x::Atom,
current_selection: Option<CurrentSelection<T>>,
}
// This is a trait so that we can use &dyn
trait SelectionDataImpl {
fn set_owner(&self, connection: &xcb::Connection, wm_window: x::Window);
fn handle_new_owner(
&mut self,
connection: &xcb::Connection,
wm_window: x::Window,
atoms: &super::Atoms,
owner: x::Window,
timestamp: u32,
);
fn handle_target_list(
&mut self,
connection: &Rc<xcb::Connection>,
wm_window: x::Window,
atoms: &super::Atoms,
target_window: x::Window,
dest_property: x::Atom,
server_state: &mut RealServerState,
);
fn x11_selection(&self) -> Option<&Selection>;
fn handle_selection_request(
&self,
connection: &xcb::Connection,
atoms: &super::Atoms,
request: &x::SelectionRequestEvent,
success: &dyn Fn(),
refuse: &dyn Fn(),
server_state: &mut RealServerState,
);
fn atom(&self) -> x::Atom;
}
impl<T: SelectionType> SelectionData<T> {
fn new(atom: x::Atom) -> Self {
Self {
last_selection_timestamp: x::CURRENT_TIME,
atom,
current_selection: None,
}
}
}
impl<T: SelectionType> SelectionDataImpl for SelectionData<T> {
fn atom(&self) -> x::Atom {
self.atom
}
fn set_owner(&self, connection: &xcb::Connection, wm_window: x::Window) {
connection
.send_and_check_request(&x::SetSelectionOwner {
owner: wm_window,
selection: self.atom,
time: self.last_selection_timestamp,
})
.unwrap();
let reply = connection
.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
selection: self.atom,
}))
.unwrap();
if reply.owner() != wm_window {
warn!(
"Could not get {} selection (owned by {:?})",
get_atom_name(connection, self.atom),
reply.owner()
);
}
}
fn handle_new_owner(
&mut self,
connection: &xcb::Connection,
wm_window: x::Window,
atoms: &super::Atoms,
owner: x::Window,
timestamp: u32,
) {
debug!(
"new {} owner: {owner:?}",
get_atom_name(connection, self.atom)
);
self.last_selection_timestamp = timestamp;
// Grab targets
connection
.send_and_check_request(&x::ConvertSelection {
requestor: wm_window,
selection: self.atom,
target: atoms.targets,
property: atoms.selection_reply,
time: timestamp,
})
.unwrap();
}
fn handle_target_list(
&mut self,
connection: &Rc<xcb::Connection>,
wm_window: x::Window,
atoms: &super::Atoms,
target_window: x::Window,
dest_property: x::Atom,
server_state: &mut RealServerState,
) {
let reply = connection
.wait_for_reply(connection.send_request(&x::GetProperty {
delete: true,
window: wm_window,
property: dest_property,
r#type: x::ATOM_ATOM,
long_offset: 0,
long_length: 20,
}))
.unwrap();
let targets: &[x::Atom] = reply.value();
if targets.is_empty() {
warn!("Got empty selection target list, trying again...");
match connection.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
selection: self.atom,
})) {
Ok(reply) => {
if reply.owner() == wm_window {
warn!("We are unexpectedly the selection owner? Clipboard may be broken!");
} else {
self.handle_new_owner(
connection,
wm_window,
atoms,
reply.owner(),
self.last_selection_timestamp,
);
}
}
Err(e) => {
error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!");
}
}
return;
}
if log::log_enabled!(log::Level::Debug) {
let targets_str: Vec<String> = targets
.iter()
.map(|t| get_atom_name(connection, *t))
.collect();
debug!("got targets: {targets_str:?}");
}
let mimes = targets
.iter()
.copied()
.filter(|atom| ![atoms.targets, atoms.multiple, atoms.save_targets].contains(atom))
.map(|target_atom| SelectionTargetId {
name: get_atom_name(connection, target_atom),
atom: target_atom,
source: None,
})
.collect();
let selection = Rc::new(Selection {
mimes,
connection: connection.clone(),
window: target_window,
pending: RefCell::default(),
selection: self.atom,
selection_time: self.last_selection_timestamp,
incr: atoms.incr,
});
server_state.set_selection_source::<T>(&selection);
self.current_selection = Some(CurrentSelection::X11(selection));
debug!("{} set from X11", get_atom_name(connection, self.atom));
}
fn x11_selection(&self) -> Option<&Selection> {
match &self.current_selection {
Some(CurrentSelection::X11(selection)) => Some(selection),
_ => None,
}
}
fn handle_selection_request(
&self,
connection: &xcb::Connection,
atoms: &super::Atoms,
request: &x::SelectionRequestEvent,
success: &dyn Fn(),
refuse: &dyn Fn(),
server_state: &mut RealServerState,
) {
let Some(CurrentSelection::Wayland { mimes, inner }) = &self.current_selection else {
warn!("Got selection request, but we don't seem to be the selection owner");
refuse();
return;
};
match request.target() {
x if x == atoms.targets => {
let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect();
connection
.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: request.requestor(),
property: request.property(),
r#type: x::ATOM_ATOM,
data: &atoms,
})
.unwrap();
success();
}
other => {
let Some(target) = mimes.iter().find(|t| t.atom == other) else {
if log::log_enabled!(log::Level::Debug) {
let name = get_atom_name(connection, other);
debug!("refusing selection request because given atom could not be found ({name})");
}
refuse();
return;
};
let mime_name = target
.source
.as_ref()
.cloned()
.unwrap_or_else(|| target.name.clone());
let data = inner.receive(mime_name, server_state);
match connection.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: request.requestor(),
property: request.property(),
r#type: target.atom,
data: &data,
}) {
Ok(_) => success(),
Err(e) => {
warn!("Failed setting selection property: {e:?}");
refuse();
}
}
}
}
}
}
pub(super) struct SelectionState {
clipboard: SelectionData<Clipboard>,
primary: SelectionData<Primary>,
target_window: x::Window,
}
impl SelectionState {
pub fn new(connection: &xcb::Connection, root: x::Window, atoms: &super::Atoms) -> Self {
let target_window = connection.generate_id();
connection
.send_and_check_request(&x::CreateWindow {
wid: target_window,
width: 1,
height: 1,
depth: 0,
parent: root,
x: 0,
y: 0,
border_width: 0,
class: x::WindowClass::InputOnly,
visual: x::COPY_FROM_PARENT,
// Watch for INCR property changes.
value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)],
})
.expect("Couldn't create window for selections");
Self {
target_window,
clipboard: SelectionData::new(atoms.clipboard),
primary: SelectionData::new(atoms.primary),
}
}
}
impl XState {
pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection<Clipboard>) {
let mut utf8_xwl = false;
let mut utf8_wl = false;
let mut mimes: Vec<SelectionTargetId> = selection
.mime_types
.iter()
.map(|mime| {
match mime.as_str() {
"UTF8_STRING" => utf8_xwl = true,
"text/plain;charset=utf-8" => utf8_wl = true,
_ => {}
}
let atom = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: mime.as_bytes(),
}))
.unwrap();
SelectionTargetId {
name: mime.clone(),
atom: atom.atom(),
source: None,
}
})
.collect();
if utf8_wl && !utf8_xwl {
let name = "UTF8_STRING".to_string();
let atom = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: name.as_bytes(),
}))
.unwrap()
.atom();
mimes.push(SelectionTargetId {
name,
atom,
source: Some("text/plain;charset=utf-8".to_string()),
});
}
self.selection_state.clipboard.current_selection = Some(CurrentSelection::Wayland {
mimes,
inner: selection,
});
self.selection_state
.clipboard
.set_owner(&self.connection, self.wm_window);
debug!("Clipboard set from Wayland");
}
pub(crate) fn set_primary_selection(&mut self, selection: ForeignSelection<Primary>) {
let mut utf8_xwl = false;
let mut utf8_wl = false;
let mut mimes: Vec<SelectionTargetId> = selection
.mime_types
.iter()
.map(|mime| {
match mime.as_str() {
"UTF8_STRING" => utf8_xwl = true,
"text/plain;charset=utf-8" => utf8_wl = true,
_ => {}
}
let atom = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: mime.as_bytes(),
}))
.unwrap();
SelectionTargetId {
name: mime.clone(),
atom: atom.atom(),
source: None,
}
})
.collect();
if utf8_wl && !utf8_xwl {
let name = "UTF8_STRING".to_string();
let atom = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: name.as_bytes(),
}))
.unwrap()
.atom();
mimes.push(SelectionTargetId {
name,
atom,
source: Some("text/plain;charset=utf-8".to_string()),
});
}
self.selection_state.primary.current_selection = Some(CurrentSelection::Wayland {
mimes,
inner: selection,
});
self.selection_state
.primary
.set_owner(&self.connection, self.wm_window);
debug!("Primaryset from Wayland");
}
pub(super) fn handle_selection_event(
&mut self,
event: &xcb::Event,
server_state: &mut RealServerState,
) -> bool {
macro_rules! get_selection_data {
($selection:expr) => {
match $selection {
x if x == self.atoms.clipboard => {
&mut self.selection_state.clipboard as &mut dyn SelectionDataImpl
}
x if x == self.atoms.primary => &mut self.selection_state.primary as _,
_ => return true,
}
};
}
match event {
xcb::Event::X(x::Event::SelectionClear(e)) => {
let data = get_selection_data!(e.selection());
data.handle_new_owner(
&self.connection,
self.wm_window,
&self.atoms,
e.owner(),
e.time(),
);
}
xcb::Event::X(x::Event::SelectionNotify(e)) => {
if e.property() == x::ATOM_NONE {
warn!(
"selection notify fail? {}",
get_atom_name(&self.connection, e.selection())
);
return true;
}
let data = get_selection_data!(e.selection());
debug!(
"selection notify requestor: {:?} target: {} selection: {}",
e.requestor(),
get_atom_name(&self.connection, e.target()),
get_atom_name(&self.connection, e.selection()),
);
if e.requestor() == self.wm_window {
match e.target() {
x if x == self.atoms.targets => data.handle_target_list(
&self.connection,
self.wm_window,
&self.atoms,
self.selection_state.target_window,
e.property(),
server_state,
),
other => warn!(
"got unexpected selection notify for target {}",
get_atom_name(&self.connection, other)
),
}
} else if e.requestor() == self.selection_state.target_window {
if let Some(selection) = data.x11_selection() {
selection.handle_notify(e.target());
}
} else {
warn!(
"Got selection notify from unexpected requestor: {:?}",
e.requestor()
);
}
}
xcb::Event::X(x::Event::SelectionRequest(e)) => {
let data = get_selection_data!(e.selection());
let send_notify = |property| {
self.connection
.send_and_check_request(&x::SendEvent {
propagate: false,
destination: x::SendEventDest::Window(e.requestor()),
event_mask: x::EventMask::empty(),
event: &x::SelectionNotifyEvent::new(
e.time(),
e.requestor(),
e.selection(),
e.target(),
property,
),
})
.unwrap();
};
let refuse = || send_notify(x::ATOM_NONE);
let success = || send_notify(e.property());
if log::log_enabled!(log::Level::Debug) {
let target = get_atom_name(&self.connection, e.target());
let selection = get_atom_name(&self.connection, data.atom());
debug!("Got selection request for target {target} (selection: {selection})");
}
if e.property() == x::ATOM_NONE {
debug!("refusing - property is set to none");
refuse();
return true;
}
data.handle_selection_request(
&self.connection,
&self.atoms,
e,
&success,
&refuse,
server_state,
);
}
xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => match e.selection() {
x if x == self.atoms.clipboard || x == self.atoms.primary => match e.subtype() {
xcb::xfixes::SelectionEvent::SetSelectionOwner => {
if e.owner() == self.wm_window {
return true;
}
let data = get_selection_data!(x);
data.handle_new_owner(
&self.connection,
self.wm_window,
&self.atoms,
e.owner(),
e.timestamp(),
);
}
xcb::xfixes::SelectionEvent::SelectionClientClose
| xcb::xfixes::SelectionEvent::SelectionWindowDestroy => {
debug!("Selection owner destroyed, selection will be unset");
self.selection_state.clipboard.current_selection = None;
}
},
x if x == self.atoms.xsettings => match e.subtype() {
xcb::xfixes::SelectionEvent::SelectionClientClose
| xcb::xfixes::SelectionEvent::SelectionWindowDestroy => {
debug!("Xsettings owner disappeared, reacquiring");
self.set_xsettings_owner();
}
_ => {}
},
_ => {}
},
_ => return false,
}
true
}
pub(super) fn handle_selection_property_change(
&mut self,
event: &x::PropertyNotifyEvent,
) -> bool {
for data in [
&self.selection_state.primary as &dyn SelectionDataImpl,
&self.selection_state.clipboard as _,
] {
if let Some(selection) = &data.x11_selection() {
return selection.check_for_incr(event);
}
}
false
}
}
fn get_property_any(
connection: &xcb::Connection,
window: x::Window,
property: x::Atom,
) -> xcb::Result<x::GetPropertyReply> {
connection.wait_for_reply(connection.send_request(&x::GetProperty {
delete: true,
window,
property,
r#type: x::ATOM_ANY,
long_offset: 0,
long_length: u32::MAX,
}))
}