xstate: refactor clipboard selections

Before, every time the clipboard selection changed, satellite would copy
everything on it and force itself to be the clipboard owner, regardless
of X11 or Wayland. Now, satellite is only the owner when the clipboard
owner comes from Wayland, and uses the XFixes extension to watch for
changes in clipboard ownership X11 side. Satellite also now avoids
copying all of the clipboard contents into memory every time, instead
copying directly on request. This is a pretty big change, but should
hopefully help make the clipboard more stable.

Also added some misc test cleanup/using helper functions where possible.
Using the XFixes extension may also end up being necessary for
implementing drag and drop, so it's good the infrastructure is there
now.
This commit is contained in:
Shawn Wallace 2025-01-08 23:57:22 -05:00
parent 42ffd06d1e
commit 47e7357eab
7 changed files with 755 additions and 777 deletions

View file

@ -7,6 +7,7 @@ use crate::server::{PendingSurfaceState, ServerState};
use crate::xstate::{RealConnection, XState}; use crate::xstate::{RealConnection, XState};
use log::{error, info}; use log::{error, info};
use rustix::event::{poll, PollFd, PollFlags}; use rustix::event::{poll, PollFd, PollFlags};
use smithay_client_toolkit::data_device_manager::WritePipe;
use std::io::{BufRead, BufReader, Read, Write}; use std::io::{BufRead, BufReader, Read, Write};
use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
@ -16,7 +17,7 @@ use xcb::x;
pub trait XConnection: Sized + 'static { pub trait XConnection: Sized + 'static {
type ExtraData: FromServerState<Self>; type ExtraData: FromServerState<Self>;
type MimeTypeData: MimeTypeData; type X11Selection: X11Selection;
fn root_window(&self) -> x::Window; fn root_window(&self) -> x::Window;
fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState); fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState);
@ -35,9 +36,9 @@ pub trait FromServerState<C: XConnection> {
fn create(state: &ServerState<C>) -> Self; fn create(state: &ServerState<C>) -> Self;
} }
pub trait MimeTypeData { pub trait X11Selection {
fn name(&self) -> &str; fn mime_types(&self) -> Vec<&str>;
fn data(&self) -> &[u8]; fn write_to(&self, mime: &str, pipe: WritePipe);
} }
type RealServerState = ServerState<RealConnection>; type RealServerState = ServerState<RealConnection>;

View file

@ -8,7 +8,7 @@ use self::event::*;
use super::FromServerState; use super::FromServerState;
use crate::clientside::*; use crate::clientside::*;
use crate::xstate::{Atoms, WindowDims, WmHints, WmName, WmNormalHints}; use crate::xstate::{Atoms, WindowDims, WmHints, WmName, WmNormalHints};
use crate::{MimeTypeData, XConnection}; use crate::{X11Selection, XConnection};
use log::{debug, warn}; use log::{debug, warn};
use rustix::event::{poll, PollFd, PollFlags}; use rustix::event::{poll, PollFd, PollFlags};
use slotmap::{new_key_type, HopSlotMap, SparseSecondaryMap}; use slotmap::{new_key_type, HopSlotMap, SparseSecondaryMap};
@ -18,10 +18,9 @@ use smithay_client_toolkit::data_device_manager::{
}; };
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Read; use std::io::Read;
use std::io::Write;
use std::os::fd::{AsFd, BorrowedFd}; use std::os::fd::{AsFd, BorrowedFd};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::rc::Rc; use std::rc::{Rc, Weak};
use wayland_client::{globals::Global, protocol as client, Proxy}; use wayland_client::{globals::Global, protocol as client, Proxy};
use wayland_protocols::{ use wayland_protocols::{
wp::{ wp::{
@ -492,7 +491,7 @@ pub struct ServerState<C: XConnection> {
pub connection: Option<C>, pub connection: Option<C>,
xdg_wm_base: XdgWmBase, xdg_wm_base: XdgWmBase,
clipboard_data: Option<ClipboardData<C::MimeTypeData>>, clipboard_data: Option<ClipboardData<C::X11Selection>>,
last_kb_serial: Option<u32>, last_kb_serial: Option<u32>,
} }
@ -518,7 +517,7 @@ impl<C: XConnection> ServerState<C> {
let clipboard_data = manager.map(|manager| ClipboardData { let clipboard_data = manager.map(|manager| ClipboardData {
manager, manager,
device: None, device: None,
source: None::<CopyPasteData<C::MimeTypeData>>, source: None::<CopyPasteData<C::X11Selection>>,
}); });
dh.create_global::<Self, XwaylandShellV1, _>(1, ()); dh.create_global::<Self, XwaylandShellV1, _>(1, ());
@ -680,11 +679,7 @@ impl<C: XConnection> ServerState<C> {
return true; return true;
}; };
if win.mapped && !win.attrs.override_redirect { !(win.mapped && !win.attrs.override_redirect)
false
} else {
true
}
} }
pub fn reconfigure_window(&mut self, event: x::ConfigureNotifyEvent) { pub fn reconfigure_window(&mut self, event: x::ConfigureNotifyEvent) {
@ -805,14 +800,14 @@ impl<C: XConnection> ServerState<C> {
let _ = self.windows.remove(&window); let _ = self.windows.remove(&window);
} }
pub(crate) fn set_copy_paste_source(&mut self, mime_types: Rc<Vec<C::MimeTypeData>>) { pub(crate) fn set_copy_paste_source(&mut self, selection: &Rc<C::X11Selection>) {
if let Some(d) = &mut self.clipboard_data { if let Some(d) = &mut self.clipboard_data {
let src = d let src = d
.manager .manager
.create_copy_paste_source(&self.qh, mime_types.iter().map(|m| m.name())); .create_copy_paste_source(&self.qh, selection.mime_types());
let data = CopyPasteData::X11 { let data = CopyPasteData::X11 {
inner: src, inner: src,
data: mime_types, data: Rc::downgrade(selection),
}; };
let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else { let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else {
unreachable!(); unreachable!();
@ -891,13 +886,12 @@ impl<C: XConnection> ServerState<C> {
let globals = &mut self.clientside.globals; let globals = &mut self.clientside.globals;
if let Some(clipboard) = self.clipboard_data.as_mut() { if let Some(clipboard) = self.clipboard_data.as_mut() {
for (mime_type, mut fd) in std::mem::take(&mut globals.selection_requests) { for (mime_type, fd) in std::mem::take(&mut globals.selection_requests) {
let CopyPasteData::X11 { data, .. } = clipboard.source.as_ref().unwrap() else { let CopyPasteData::X11 { data, .. } = clipboard.source.as_ref().unwrap() else {
unreachable!() unreachable!("Got selection request without having set the selection?")
}; };
let pos = data.iter().position(|m| m.name() == mime_type).unwrap(); if let Some(data) = data.upgrade() {
if let Err(e) = fd.write_all(data[pos].data()) { data.write_to(&mime_type, fd);
warn!("Failed to write selection data: {e:?}");
} }
} }
@ -1090,10 +1084,10 @@ pub struct PendingSurfaceState {
pub height: i32, pub height: i32,
} }
struct ClipboardData<M: MimeTypeData> { struct ClipboardData<X: X11Selection> {
manager: DataDeviceManagerState, manager: DataDeviceManagerState,
device: Option<DataDevice>, device: Option<DataDevice>,
source: Option<CopyPasteData<M>>, source: Option<CopyPasteData<X>>,
} }
pub struct ForeignSelection { pub struct ForeignSelection {
@ -1121,10 +1115,10 @@ impl Drop for ForeignSelection {
} }
} }
enum CopyPasteData<M: MimeTypeData> { enum CopyPasteData<X: X11Selection> {
X11 { X11 {
inner: CopyPasteSource, inner: CopyPasteSource,
data: Rc<Vec<M>>, data: Weak<X>,
}, },
Foreign(ForeignSelection), Foreign(ForeignSelection),
} }

View file

@ -3,6 +3,7 @@ use crate::xstate::{SetState, WmName};
use paste::paste; use paste::paste;
use rustix::event::{poll, PollFd, PollFlags}; use rustix::event::{poll, PollFd, PollFlags};
use std::collections::HashMap; use std::collections::HashMap;
use std::io::Write;
use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::fd::{AsRawFd, BorrowedFd};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -182,19 +183,30 @@ impl super::FromServerState<FakeXConnection> for () {
fn create(_: &FakeServerState) -> Self {} fn create(_: &FakeServerState) -> Self {}
} }
impl crate::MimeTypeData for testwl::PasteData { impl crate::X11Selection for Vec<testwl::PasteData> {
fn name(&self) -> &str { fn mime_types(&self) -> Vec<&str> {
&self.mime_type self.iter().map(|data| data.mime_type.as_str()).collect()
} }
fn data(&self) -> &[u8] { fn write_to(
&self.data &self,
mime: &str,
mut pipe: smithay_client_toolkit::data_device_manager::WritePipe,
) {
println!("writing");
let data = self
.iter()
.find(|data| data.mime_type == mime)
.unwrap_or_else(|| panic!("Couldn't find mime type {mime}"));
pipe.write_all(&data.data)
.expect("Couldn't write paste data");
println!("goodbye pipe {mime}");
} }
} }
impl super::XConnection for FakeXConnection { impl super::XConnection for FakeXConnection {
type ExtraData = (); type ExtraData = ();
type MimeTypeData = testwl::PasteData; type X11Selection = Vec<testwl::PasteData>;
fn root_window(&self) -> Window { fn root_window(&self) -> Window {
self.root self.root
} }
@ -1162,7 +1174,7 @@ fn copy_from_x11() {
}, },
]); ]);
f.satellite.set_copy_paste_source(mimes.clone()); f.satellite.set_copy_paste_source(&mimes);
f.run(); f.run();
let server_mimes = f.testwl.data_source_mimes(); let server_mimes = f.testwl.data_source_mimes();
@ -1170,9 +1182,10 @@ fn copy_from_x11() {
assert!(server_mimes.contains(&mime.mime_type)); assert!(server_mimes.contains(&mime.mime_type));
} }
let data = f.testwl.paste_data(); let data = f.testwl.paste_data(|_, _| {
f.run(); f.satellite.run();
let data = data.resolve(); true
});
assert_eq!(*mimes, data); assert_eq!(*mimes, data);
} }
@ -1236,7 +1249,7 @@ fn clipboard_x11_then_wayland() {
}, },
]); ]);
f.satellite.set_copy_paste_source(x11data.clone()); f.satellite.set_copy_paste_source(&x11data);
f.run(); f.run();
let waylanddata = vec![ let waylanddata = vec![

View file

@ -1,5 +1,5 @@
mod selection; mod selection;
use selection::{SelectionData, SelectionTarget}; use selection::{Selection, SelectionData};
use crate::{server::WindowAttributes, XConnection}; use crate::{server::WindowAttributes, XConnection};
use bitflags::bitflags; use bitflags::bitflags;
@ -117,7 +117,11 @@ impl XState {
xcb::Connection::connect_to_fd_with_extensions( xcb::Connection::connect_to_fd_with_extensions(
fd.as_raw_fd(), fd.as_raw_fd(),
None, None,
&[xcb::Extension::Composite, xcb::Extension::RandR], &[
xcb::Extension::Composite,
xcb::Extension::RandR,
xcb::Extension::XFixes,
],
&[], &[],
) )
.unwrap(), .unwrap(),
@ -156,6 +160,28 @@ impl XState {
}) })
.unwrap(); .unwrap();
// negotiate xfixes version
let reply = connection
.wait_for_reply(connection.send_request(&xcb::xfixes::QueryVersion {
client_major_version: 1,
client_minor_version: 0,
}))
.unwrap();
log::info!(
"xfixes version: {}.{}",
reply.major_version(),
reply.minor_version()
);
use xcb::xfixes::SelectionEventMask;
connection
.send_and_check_request(&xcb::xfixes::SelectSelectionInput {
window: root,
selection: atoms.clipboard,
event_mask: SelectionEventMask::SET_SELECTION_OWNER
| SelectionEventMask::SELECTION_WINDOW_DESTROY
| SelectionEventMask::SELECTION_CLIENT_CLOSE,
})
.unwrap();
{ {
// Setup default cursor theme // Setup default cursor theme
let ctx = CursorContext::new(&connection, screen).unwrap(); let ctx = CursorContext::new(&connection, screen).unwrap();
@ -169,13 +195,14 @@ impl XState {
} }
let wm_window = connection.generate_id(); let wm_window = connection.generate_id();
let selection_data = SelectionData::new(&connection, root);
let mut r = Self { let mut r = Self {
connection, connection,
wm_window, wm_window,
root, root,
atoms, atoms,
selection_data: Default::default(), selection_data,
}; };
r.create_ewmh_window(); r.create_ewmh_window();
r r
@ -240,8 +267,6 @@ impl XState {
data: b"xwayland-satellite", data: b"xwayland-satellite",
}) })
.unwrap(); .unwrap();
self.set_clipboard_owner(x::CURRENT_TIME);
} }
pub fn handle_events(&mut self, server_state: &mut super::RealServerState) { pub fn handle_events(&mut self, server_state: &mut super::RealServerState) {
@ -666,7 +691,7 @@ impl XState {
server_state.set_win_class(window, class); server_state.set_win_class(window, class);
} }
_ => { _ => {
if !self.handle_selection_property_change(&event, server_state) if !self.handle_selection_property_change(&event)
&& log::log_enabled!(log::Level::Debug) && log::log_enabled!(log::Level::Debug)
{ {
debug!( debug!(
@ -872,7 +897,7 @@ impl RealConnection {
impl XConnection for RealConnection { impl XConnection for RealConnection {
type ExtraData = Atoms; type ExtraData = Atoms;
type MimeTypeData = SelectionTarget; type X11Selection = Selection;
fn root_window(&self) -> x::Window { fn root_window(&self) -> x::Window {
self.connection.get_setup().roots().next().unwrap().root() self.connection.get_setup().roots().next().unwrap().root()
@ -898,14 +923,16 @@ impl XConnection for RealConnection {
&[] &[]
}; };
if let Err(e) = self.connection if let Err(e) = self
.connection
.send_and_check_request(&x::ChangeProperty::<x::Atom> { .send_and_check_request(&x::ChangeProperty::<x::Atom> {
mode: x::PropMode::Replace, mode: x::PropMode::Replace,
window, window,
property: atoms.net_wm_state, property: atoms.net_wm_state,
r#type: x::ATOM_ATOM, r#type: x::ATOM_ATOM,
data, data,
}) { })
{
warn!("Failed to set fullscreen state on {window:?} ({e})"); warn!("Failed to set fullscreen state on {window:?} ({e})");
} }
} }

View file

@ -1,91 +1,213 @@
use super::{get_atom_name, XState}; use super::{get_atom_name, XState};
use crate::server::ForeignSelection; use crate::server::ForeignSelection;
use crate::{MimeTypeData, RealServerState}; use crate::{RealServerState, X11Selection};
use log::{debug, trace, warn}; 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 std::rc::Rc;
use xcb::x; use xcb::x;
#[derive(Debug)]
enum TargetValue {
U8(Vec<u8>),
U16(Vec<u16>),
U32(Vec<u32>),
Foreign,
}
#[derive(Debug)] #[derive(Debug)]
struct SelectionTargetId { struct SelectionTargetId {
name: String, name: String,
atom: x::Atom, atom: x::Atom,
} }
pub struct SelectionTarget { struct PendingSelectionData {
id: SelectionTargetId, target: x::Atom,
value: TargetValue, pipe: WritePipe,
incr: bool,
} }
impl MimeTypeData for SelectionTarget { pub struct Selection {
fn name(&self) -> &str { mimes: Vec<SelectionTargetId>,
&self.id.name connection: Rc<xcb::Connection>,
window: x::Window,
pending: RefCell<Vec<PendingSelectionData>>,
clipboard: x::Atom,
selection_time: u32,
incr: x::Atom,
} }
fn data(&self) -> &[u8] { impl X11Selection for Selection {
match &self.value { fn mime_types(&self) -> Vec<&str> {
TargetValue::U8(v) => v, self.mimes
TargetValue::U32(v) => unsafe { v.align_to().1 }, .iter()
other => { .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.clipboard,
target: target.atom,
property: target.atom,
time: self.selection_time,
})
{
error!("Failed to request clipboard 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!( warn!(
"Unexpectedly requesting data from mime type with data type {} - nothing will be copied", "Got selection notify for unknown target {}",
std::any::type_name_of_val(other) 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!(
"recieved 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 PendingMimeDataType { enum CurrentSelection {
Standard, X11(Rc<Selection>),
Incremental(TargetValue), Wayland {
} mimes: Vec<SelectionTargetId>,
inner: ForeignSelection,
struct PendingMimeData {
ty: PendingMimeDataType,
id: SelectionTargetId,
dest_property: x::Atom,
}
enum MimeTypes {
Temporary {
/// Temporary mime data, being built
data: Vec<SelectionTarget>,
/// Mime types we still need to receive feedback on
to_grab: Vec<PendingMimeData>,
}, },
/// Done grabbing mime data
Complete(Rc<Vec<SelectionTarget>>),
} }
impl Default for MimeTypes {
fn default() -> Self {
Self::Complete(Default::default())
}
}
#[derive(Default)]
pub(crate) struct SelectionData { pub(crate) struct SelectionData {
clear_time: Option<u32>, last_selection_timestamp: u32,
mime_types: MimeTypes, target_window: x::Window,
foreign_data: Option<ForeignSelection>, current_selection: Option<CurrentSelection>,
}
impl SelectionData {
pub fn new(connection: &xcb::Connection, root: x::Window) -> 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 {
last_selection_timestamp: x::CURRENT_TIME,
target_window,
current_selection: None,
}
}
} }
impl XState { impl XState {
pub(crate) fn set_clipboard_owner(&mut self, time: u32) { fn set_clipboard_owner(&mut self) {
self.connection self.connection
.send_and_check_request(&x::SetSelectionOwner { .send_and_check_request(&x::SetSelectionOwner {
owner: self.wm_window, owner: self.wm_window,
selection: self.atoms.clipboard, selection: self.atoms.clipboard,
time, time: self.selection_data.last_selection_timestamp,
}) })
.unwrap(); .unwrap();
@ -105,7 +227,7 @@ impl XState {
} }
pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) { pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) {
let types = selection let mimes = selection
.mime_types .mime_types
.iter() .iter()
.map(|mime| { .map(|mime| {
@ -117,18 +239,18 @@ impl XState {
})) }))
.unwrap(); .unwrap();
SelectionTarget { SelectionTargetId {
id: SelectionTargetId {
name: mime.clone(), name: mime.clone(),
atom: atom.atom(), atom: atom.atom(),
},
value: TargetValue::Foreign,
} }
}) })
.collect(); .collect();
self.selection_data.mime_types = MimeTypes::Complete(Rc::new(types)); self.selection_data.current_selection = Some(CurrentSelection::Wayland {
self.selection_data.foreign_data = Some(selection); mimes,
inner: selection,
});
self.set_clipboard_owner();
debug!("Clipboard set from Wayland"); debug!("Clipboard set from Wayland");
} }
@ -138,29 +260,9 @@ impl XState {
server_state: &mut RealServerState, server_state: &mut RealServerState,
) -> bool { ) -> bool {
match event { match event {
// Someone else is the clipboard owner - get the data from them and then reestablish // Someone else took the clipboard owner
// ourselves as the owner
xcb::Event::X(x::Event::SelectionClear(e)) => { xcb::Event::X(x::Event::SelectionClear(e)) => {
if e.selection() != self.atoms.clipboard { self.handle_new_selection_owner(e.owner(), e.time());
warn!(
"Got SelectionClear for unexpected atom {}, ignoring",
get_atom_name(&self.connection, e.selection())
);
return true;
}
// get the mime types
self.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.wm_window,
selection: self.atoms.clipboard,
target: self.atoms.targets,
property: self.atoms.selection_reply,
time: e.time(),
})
.unwrap();
self.selection_data.clear_time = Some(e.time());
} }
xcb::Event::X(x::Event::SelectionNotify(e)) => { xcb::Event::X(x::Event::SelectionNotify(e)) => {
if e.property() == x::ATOM_NONE { if e.property() == x::ATOM_NONE {
@ -168,24 +270,33 @@ impl XState {
return true; return true;
} }
trace!( debug!(
"selection notify target: {}", "selection notify requestor: {:?} target: {}",
e.requestor(),
get_atom_name(&self.connection, e.target()) get_atom_name(&self.connection, e.target())
); );
match e.target() {
x if x == self.atoms.targets => self.handle_target_list(e.property()),
atom => self.handle_clipboard_data(atom),
}
if let MimeTypes::Temporary { to_grab, .. } = &self.selection_data.mime_types { if e.requestor() == self.wm_window {
if to_grab.is_empty() { match e.target() {
let MimeTypes::Temporary { data, .. } = x if x == self.atoms.targets => {
std::mem::take(&mut self.selection_data.mime_types) self.handle_target_list(e.property(), server_state)
else {
unreachable!()
};
self.finish_mime_data(server_state, data);
} }
other => warn!(
"got unexpected selection notify for target {}",
get_atom_name(&self.connection, other)
),
}
} else if e.requestor() == self.selection_data.target_window {
if let Some(CurrentSelection::X11(selection)) =
&self.selection_data.current_selection
{
selection.handle_notify(e.target());
}
} else {
warn!(
"Got selection notify from unexpected requestor: {:?}",
e.requestor()
);
} }
} }
xcb::Event::X(x::Event::SelectionRequest(e)) => { xcb::Event::X(x::Event::SelectionRequest(e)) => {
@ -219,15 +330,17 @@ impl XState {
return true; return true;
} }
let MimeTypes::Complete(mime_data) = &self.selection_data.mime_types else { let Some(CurrentSelection::Wayland { mimes, inner }) =
warn!("Got selection request, but mime data is incomplete"); &self.selection_data.current_selection
else {
warn!("Got selection request, but we don't seem to be the selection owner");
refuse(); refuse();
return true; return true;
}; };
match e.target() { match e.target() {
x if x == self.atoms.targets => { x if x == self.atoms.targets => {
let atoms: Box<[x::Atom]> = mime_data.iter().map(|t| t.id.atom).collect(); let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect();
self.connection self.connection
.send_and_check_request(&x::ChangeProperty { .send_and_check_request(&x::ChangeProperty {
@ -242,7 +355,7 @@ impl XState {
success(); success();
} }
other => { other => {
let Some(target) = mime_data.iter().find(|t| t.id.atom == other) else { let Some(target) = mimes.iter().find(|t| t.atom == other) else {
if log::log_enabled!(log::Level::Debug) { if log::log_enabled!(log::Level::Debug) {
let name = get_atom_name(&self.connection, other); let name = get_atom_name(&self.connection, other);
debug!("refusing selection request because given atom could not be found ({})", name); debug!("refusing selection request because given atom could not be found ({})", name);
@ -251,14 +364,13 @@ impl XState {
return true; return true;
}; };
macro_rules! set_property { let data = inner.receive(target.name.clone(), server_state);
($data:expr) => {
match self.connection.send_and_check_request(&x::ChangeProperty { match self.connection.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace, mode: x::PropMode::Replace,
window: e.requestor(), window: e.requestor(),
property: e.property(), property: e.property(),
r#type: target.id.atom, r#type: target.atom,
data: $data, data: &data,
}) { }) {
Ok(_) => success(), Ok(_) => success(),
Err(e) => { Err(e) => {
@ -266,23 +378,24 @@ impl XState {
refuse(); refuse();
} }
} }
}; }
}
} }
match &target.value { xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => {
TargetValue::U8(v) => set_property!(v), assert_eq!(e.selection(), self.atoms.clipboard);
TargetValue::U16(v) => set_property!(v), match e.subtype() {
TargetValue::U32(v) => set_property!(v), xcb::xfixes::SelectionEvent::SetSelectionOwner => {
TargetValue::Foreign => { if e.owner() == self.wm_window {
let data = self return true;
.selection_data
.foreign_data
.as_ref()
.unwrap()
.receive(target.id.name.clone(), server_state);
set_property!(&data);
} }
self.handle_new_selection_owner(e.owner(), e.selection_timestamp());
} }
xcb::xfixes::SelectionEvent::SelectionClientClose
| xcb::xfixes::SelectionEvent::SelectionWindowDestroy => {
debug!("Selection owner destroyed, selection will be unset");
self.selection_data.current_selection = None;
} }
} }
} }
@ -292,7 +405,22 @@ impl XState {
true true
} }
fn handle_target_list(&mut self, dest_property: x::Atom) { fn handle_new_selection_owner(&mut self, owner: x::Window, timestamp: u32) {
debug!("new selection owner: {:?}", owner);
self.selection_data.last_selection_timestamp = timestamp;
// Grab targets
self.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.wm_window,
selection: self.atoms.clipboard,
target: self.atoms.targets,
property: self.atoms.selection_reply,
time: timestamp,
})
.unwrap();
}
fn handle_target_list(&mut self, dest_property: x::Atom, server_state: &mut RealServerState) {
let reply = self let reply = self
.connection .connection
.wait_for_reply(self.connection.send_request(&x::GetProperty { .wait_for_reply(self.connection.send_request(&x::GetProperty {
@ -306,6 +434,29 @@ impl XState {
.unwrap(); .unwrap();
let targets: &[x::Atom] = reply.value(); let targets: &[x::Atom] = reply.value();
if targets.is_empty() {
warn!("Got empty selection target list, trying again...");
match self.connection.wait_for_reply(self.connection.send_request(
&x::GetSelectionOwner {
selection: self.atoms.clipboard,
},
)) {
Ok(reply) => {
if reply.owner() == self.wm_window {
warn!("We are unexpectedly the selection owner? Clipboard may be broken!");
} else {
self.handle_new_selection_owner(
reply.owner(),
self.selection_data.last_selection_timestamp,
);
}
}
Err(e) => {
error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!");
}
}
return;
}
if log::log_enabled!(log::Level::Debug) { if log::log_enabled!(log::Level::Debug) {
let targets_str: Vec<String> = targets let targets_str: Vec<String> = targets
.iter() .iter()
@ -314,7 +465,7 @@ impl XState {
debug!("got targets: {targets_str:?}"); debug!("got targets: {targets_str:?}");
} }
let to_grab = targets let mimes = targets
.iter() .iter()
.copied() .copied()
.filter(|atom| { .filter(|atom| {
@ -325,218 +476,35 @@ impl XState {
] ]
.contains(atom) .contains(atom)
}) })
.enumerate() .map(|target_atom| SelectionTargetId {
.map(|(idx, target_atom)| { name: get_atom_name(&self.connection, target_atom),
let dest_name = [b"dest", idx.to_string().as_bytes()].concat();
let reply = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
name: &dest_name,
only_if_exists: false,
}))
.unwrap();
let dest_property = reply.atom();
self.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.wm_window,
selection: self.atoms.clipboard,
target: target_atom,
property: dest_property,
time: self.selection_data.clear_time.as_ref().copied().unwrap(),
})
.unwrap();
let target_name = get_atom_name(&self.connection, target_atom);
PendingMimeData {
ty: PendingMimeDataType::Standard,
id: SelectionTargetId {
name: target_name,
atom: target_atom, atom: target_atom,
},
dest_property,
}
}) })
.collect(); .collect();
self.selection_data.mime_types = MimeTypes::Temporary { let selection = Rc::new(Selection {
to_grab, mimes,
data: Vec::new(), connection: self.connection.clone(),
}; window: self.selection_data.target_window,
} pending: RefCell::default(),
clipboard: self.atoms.clipboard,
selection_time: self.selection_data.last_selection_timestamp,
incr: self.atoms.incr,
});
fn handle_clipboard_data(&mut self, atom: x::Atom) { server_state.set_copy_paste_source(&selection);
let MimeTypes::Temporary { data, to_grab } = &mut self.selection_data.mime_types else { self.selection_data.current_selection = Some(CurrentSelection::X11(selection));
warn!("Got selection notify, but not awaiting selection data..."); debug!("Clipboard set from X11");
return;
};
let Some(idx) = to_grab
.iter()
.position(|PendingMimeData { id, .. }| id.atom == atom)
else {
warn!(
"unexpected SelectionNotify type: {}",
get_atom_name(&self.connection, atom)
);
return;
};
let PendingMimeData {
ty,
id,
dest_property,
} = to_grab.swap_remove(idx);
let value = match atom {
x if x == self.atoms.timestamp => TargetValue::U32(vec![self
.selection_data
.clear_time
.as_ref()
.copied()
.unwrap()]),
_ => {
let reply = get_property_any(&self.connection, self.wm_window, dest_property);
trace!(
"got type {} for mime type {}",
get_atom_name(&self.connection, reply.r#type()),
get_atom_name(&self.connection, atom)
);
match reply.r#type() {
x if x == self.atoms.incr => {
assert!(matches!(ty, PendingMimeDataType::Standard));
debug!(
"beginning incr process for {}",
get_atom_name(&self.connection, atom)
);
if let Some(data) =
begin_incr(&self.connection, self.wm_window, reply, id, dest_property)
{
to_grab.push(data);
}
return;
}
_ => match reply.format() {
8 => TargetValue::U8(reply.value().to_vec()),
16 => TargetValue::U16(reply.value().to_vec()),
32 => TargetValue::U32(reply.value().to_vec()),
other => {
if log::log_enabled!(log::Level::Debug) {
let target_name = &id.name;
let ty = if reply.r#type() == x::ATOM_NONE {
"None".to_string()
} else {
get_atom_name(&self.connection, reply.r#type())
};
let dest = get_atom_name(&self.connection, dest_property);
let value = reply.value::<u8>().to_vec();
debug!("unexpected format: {other} (atom: {target_name}, type: {ty:?}, property: {dest}, value: {value:?})");
}
return;
}
},
}
}
};
trace!("Selection data: {id:?} {value:?}");
data.push(SelectionTarget { id, value });
} }
pub(super) fn handle_selection_property_change( pub(super) fn handle_selection_property_change(
&mut self, &mut self,
event: &x::PropertyNotifyEvent, event: &x::PropertyNotifyEvent,
server_state: &mut RealServerState,
) -> bool { ) -> bool {
if event.window() != self.wm_window { if let Some(CurrentSelection::X11(selection)) = &self.selection_data.current_selection {
return false; return selection.check_for_incr(event);
} }
false
let MimeTypes::Temporary { data, to_grab } = &mut self.selection_data.mime_types else {
debug!("Got potential selection property change, but not awaiting mime data");
return false;
};
let Some(idx) = to_grab.iter().position(|p| {
matches!(p.ty, PendingMimeDataType::Incremental(_)) && p.dest_property == event.atom()
}) else {
debug!(
"Changed non selection property: {}",
get_atom_name(&self.connection, event.atom())
);
return false;
};
let pending = &mut to_grab[idx];
let reply = get_property_any(&self.connection, self.wm_window, pending.dest_property);
if reply.r#type() != pending.id.atom {
warn!(
"wrong getproperty type: {}",
get_atom_name(&self.connection, reply.r#type())
);
return false;
}
match reply.format() {
8 => {
let value: &[u8] = reply.value();
trace!("got incr data ({} bytes)", value.len());
if value.is_empty() {
let pending = to_grab.swap_remove(idx);
let PendingMimeDataType::Incremental(value) = pending.ty else {
unreachable!()
};
let atom = pending.id.atom;
data.push(SelectionTarget {
id: pending.id,
value,
});
trace!(
"finalized incr for {}",
get_atom_name(&self.connection, atom)
);
} else {
let PendingMimeDataType::Incremental(TargetValue::U8(data)) = &mut pending.ty
else {
unreachable!()
};
data.extend_from_slice(value);
trace!("new incr len: {}", data.len());
}
}
other => {
warn!("Got unexpected format {other} for INCR data - copy/pasting with mime type {} will fail!", get_atom_name(&self.connection, reply.r#type()));
to_grab.swap_remove(idx);
}
}
if to_grab.is_empty() {
let MimeTypes::Temporary { data, .. } =
std::mem::take(&mut self.selection_data.mime_types)
else {
unreachable!()
};
self.finish_mime_data(server_state, data);
}
true
}
fn finish_mime_data(&mut self, server_state: &mut RealServerState, data: Vec<SelectionTarget>) {
self.connection
.send_and_check_request(&x::ChangeWindowAttributes {
window: self.wm_window,
value_list: &[x::Cw::EventMask(x::EventMask::empty())],
})
.unwrap();
let data = Rc::new(data);
self.selection_data.mime_types = MimeTypes::Complete(data.clone());
self.set_clipboard_owner(self.selection_data.clear_time.unwrap());
server_state.set_copy_paste_source(data);
debug!("Clipboard set from X11");
} }
} }
@ -544,9 +512,8 @@ fn get_property_any(
connection: &xcb::Connection, connection: &xcb::Connection,
window: x::Window, window: x::Window,
property: x::Atom, property: x::Atom,
) -> x::GetPropertyReply { ) -> xcb::Result<x::GetPropertyReply> {
connection connection.wait_for_reply(connection.send_request(&x::GetProperty {
.wait_for_reply(connection.send_request(&x::GetProperty {
delete: true, delete: true,
window, window,
property, property,
@ -554,38 +521,4 @@ fn get_property_any(
long_offset: 0, long_offset: 0,
long_length: u32::MAX, long_length: u32::MAX,
})) }))
.unwrap()
}
fn begin_incr(
connection: &xcb::Connection,
window: x::Window,
reply: x::GetPropertyReply,
id: SelectionTargetId,
dest_property: x::Atom,
) -> Option<PendingMimeData> {
let size = match reply.format() {
8 => reply.value::<u8>()[0] as usize,
16 => reply.value::<u16>()[0] as usize,
32 => reply.value::<u32>()[0] as usize,
other => {
warn!("unexpected incr format: {other}");
return None;
}
};
connection
.send_and_check_request(&x::ChangeWindowAttributes {
window,
value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)],
})
.unwrap();
// XXX: storing INCR property data in memory could significantly bloat memory depending on how
// much data is going to be stuck into the clipboard, but we'll cross that bridge when we get
// to it.
Some(PendingMimeData {
ty: PendingMimeDataType::Incremental(TargetValue::U8(Vec::with_capacity(size))),
id,
dest_property,
})
} }

View file

@ -185,11 +185,9 @@ impl Fixture {
.configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]); .configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]);
self.testwl.focus_toplevel(surface); self.testwl.focus_toplevel(surface);
self.wait_and_dispatch(); self.wait_and_dispatch();
let geometry = connection let geometry = connection.get_reply(&x::GetGeometry {
.wait_for_reply(connection.send_request(&x::GetGeometry {
drawable: x::Drawable::Window(window), drawable: x::Drawable::Window(window),
})) });
.unwrap();
assert_eq!(geometry.x(), 0); assert_eq!(geometry.x(), 0);
assert_eq!(geometry.y(), 0); assert_eq!(geometry.y(), 0);
@ -278,6 +276,7 @@ struct Connection {
atoms: Atoms, atoms: Atoms,
root: x::Window, root: x::Window,
visual: u32, visual: u32,
wm_window: x::Window,
} }
impl std::ops::Deref for Connection { impl std::ops::Deref for Connection {
@ -289,7 +288,18 @@ impl std::ops::Deref for Connection {
impl Connection { impl Connection {
fn new(display: &str) -> Self { fn new(display: &str) -> Self {
let (inner, _) = xcb::Connection::connect(Some(display)).unwrap(); let (inner, _) =
xcb::Connection::connect_with_extensions(Some(display), &[xcb::Extension::XFixes], &[])
.unwrap();
// xfixes init
let reply = inner
.wait_for_reply(inner.send_request(&xcb::xfixes::QueryVersion {
client_major_version: 1,
client_minor_version: 0,
}))
.unwrap();
assert_eq!(reply.major_version(), 1);
let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) }; let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) };
let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN); let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN);
let atoms = Atoms::intern_all(&inner).unwrap(); let atoms = Atoms::intern_all(&inner).unwrap();
@ -297,12 +307,25 @@ impl Connection {
let root = screen.root(); let root = screen.root();
let visual = screen.root_visual(); let visual = screen.root_visual();
let wm_window: x::Window = inner
.wait_for_reply(inner.send_request(&x::GetProperty {
delete: false,
window: root,
property: atoms.wm_check,
r#type: x::ATOM_WINDOW,
long_offset: 0,
long_length: 1,
}))
.expect("Couldn't get WM window")
.value()[0];
Self { Self {
inner, inner,
pollfd, pollfd,
atoms, atoms,
root, root,
visual, visual,
wm_window,
} }
} }
@ -368,6 +391,7 @@ impl Connection {
#[track_caller] #[track_caller]
fn await_event(&mut self) { fn await_event(&mut self) {
self.pollfd.clear_revents();
assert!( assert!(
poll(&mut [self.pollfd.clone()], 100).expect("poll failed") > 0, poll(&mut [self.pollfd.clone()], 100).expect("poll failed") > 0,
"Did not get any X11 events" "Did not get any X11 events"
@ -393,12 +417,29 @@ impl Connection {
time: x::CURRENT_TIME, time: x::CURRENT_TIME,
}) })
.unwrap(); .unwrap();
let owner = self let owner = self.get_reply(&x::GetSelectionOwner {
.wait_for_reply(self.send_request(&x::GetSelectionOwner {
selection: self.atoms.clipboard, selection: self.atoms.clipboard,
})) });
.unwrap();
assert_eq!(window, owner.owner()); assert_eq!(window, owner.owner(), "Unexpected selection owner");
}
#[track_caller]
fn await_selection_request(&mut self) -> x::SelectionRequestEvent {
self.await_event();
match self.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
other => panic!("Didn't get selection request event, instead got {other:?}"),
}
}
#[track_caller]
fn await_selection_notify(&mut self) -> x::SelectionNotifyEvent {
self.await_event();
match self.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
other => panic!("Didn't get selection notify event, instead got {other:?}"),
}
} }
#[track_caller] #[track_caller]
@ -419,11 +460,35 @@ impl Connection {
} }
#[track_caller] #[track_caller]
fn atom_name(&self, atom: x::Atom) -> String { fn verify_clipboard_owner(&self, window: x::Window) {
self.get_reply(&x::GetAtomName { atom }) let owner = self.get_reply(&x::GetSelectionOwner {
.name() selection: self.atoms.clipboard,
.as_ascii() });
.to_string() assert_eq!(owner.owner(), window, "Clipboard owner does not match");
}
#[track_caller]
fn await_selection_owner_change(&mut self) -> xcb::xfixes::SelectionNotifyEvent {
self.await_event();
match self.poll_for_event().unwrap() {
Some(xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e))) => e,
other => panic!("Expected XFixes SelectionNotify, got {other:?}"),
}
}
#[track_caller]
fn get_selection_owner_change_events(&self, enable: bool, window: x::Window) {
let event_mask = if enable {
xcb::xfixes::SelectionEventMask::SET_SELECTION_OWNER
} else {
xcb::xfixes::SelectionEventMask::empty()
};
self.send_and_check_request(&xcb::xfixes::SelectSelectionInput {
window,
selection: self.atoms.clipboard,
event_mask,
})
.unwrap();
} }
} }
@ -607,22 +672,17 @@ fn input_focus() {
let conn = std::cell::RefCell::new(&mut connection); let conn = std::cell::RefCell::new(&mut connection);
let check_focus = |win: x::Window| { let check_focus = |win: x::Window| {
let connection = conn.borrow(); let connection = conn.borrow();
let focus = connection let focus = connection.get_reply(&x::GetInputFocus {}).focus();
.wait_for_reply(connection.send_request(&x::GetInputFocus {}))
.unwrap()
.focus();
assert_eq!(win, focus); assert_eq!(win, focus);
let reply = connection let reply = connection.get_reply(&x::GetProperty {
.wait_for_reply(connection.send_request(&x::GetProperty {
delete: false, delete: false,
window: connection.root, window: connection.root,
property: connection.atoms.net_active_window, property: connection.atoms.net_active_window,
r#type: x::ATOM_WINDOW, r#type: x::ATOM_WINDOW,
long_offset: 0, long_offset: 0,
long_length: 1, long_length: 1,
})) });
.unwrap();
assert_eq!(&[win], reply.value::<x::Window>()); assert_eq!(&[win], reply.value::<x::Window>());
}; };
@ -717,23 +777,10 @@ fn copy_from_x11() {
let mut connection = Connection::new(&f.display); let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false); let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.map_window(window); f.map_as_toplevel(&mut connection, window);
f.wait_and_dispatch();
let surface = f
.testwl
.last_created_surface_id()
.expect("No surface created");
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
connection.set_selection_owner(window); connection.set_selection_owner(window);
// wait for requests to come through let request = connection.await_selection_request();
std::thread::sleep(std::time::Duration::from_millis(100));
let request = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
other => panic!("Didn't get selection request event, instead got {other:?}"),
};
assert_eq!(request.target(), connection.atoms.targets); assert_eq!(request.target(), connection.atoms.targets);
connection.set_property( connection.set_property(
request.requestor(), request.requestor(),
@ -742,75 +789,61 @@ fn copy_from_x11() {
&[connection.atoms.mime1, connection.atoms.mime2], &[connection.atoms.mime1, connection.atoms.mime2],
); );
connection.send_selection_notify(&request); connection.send_selection_notify(&request);
connection.await_event();
let mut mime_data = vec![
(
connection.atoms.mime1,
x::ATOM_STRING,
b"hello world".as_slice(),
),
(connection.atoms.mime2, x::ATOM_INTEGER, &[1u8, 2, 3, 4]),
];
while let Some(request) = connection.poll_for_event().unwrap() {
let xcb::Event::X(x::Event::SelectionRequest(request)) = request else {
continue;
};
let target = request.target();
let Some(idx) = mime_data.iter().position(|(atom, _, _)| *atom == target) else {
panic!("Expected atom in {mime_data:?}, got {target:?}");
};
let (_, ty, data) = mime_data.swap_remove(idx);
connection.set_property(request.requestor(), ty, request.property(), data);
connection
.send_and_check_request(&x::SendEvent {
propagate: false,
destination: x::SendEventDest::Window(request.requestor()),
event_mask: x::EventMask::empty(),
event: &x::SelectionNotifyEvent::new(
request.time(),
request.requestor(),
request.selection(),
request.target(),
request.property(),
),
})
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
}
assert!(
mime_data.is_empty(),
"Didn't get all mime types: {mime_data:?}"
);
f.wait_and_dispatch(); f.wait_and_dispatch();
let owner = connection struct MimeData {
.wait_for_reply(connection.send_request(&x::GetSelectionOwner { mime: x::Atom,
selection: connection.atoms.clipboard, data: testwl::PasteData,
})) }
.unwrap(); let mimes_truth = [
assert_ne!(window, owner.owner()); MimeData {
mime: connection.atoms.mime1,
data: testwl::PasteData {
mime_type: "text/plain".to_string(),
data: b"hello world".to_vec(),
},
},
MimeData {
mime: connection.atoms.mime2,
data: testwl::PasteData {
mime_type: "blah/blah".to_string(),
data: vec![1, 2, 3, 4],
},
},
];
let mimes = f.testwl.data_source_mimes(); let advertised_mimes = f.testwl.data_source_mimes();
assert_eq!(
advertised_mimes.len(),
mimes_truth.len(),
"Wrong number of advertised mimes: {advertised_mimes:?}"
);
for MimeData { data, .. } in &mimes_truth {
assert!( assert!(
mimes.contains(&"text/plain".into()), advertised_mimes.contains(&data.mime_type),
"text/plain not in mimes: {mimes:?}" "Missing mime type {}",
); // mime1 data.mime_type
assert!( );
mimes.contains(&"blah/blah".into()), }
"blah/blah not in mimes: {mimes:?}"
); // mime2
let data = f.testwl.paste_data(); let data = f.testwl.paste_data(|mime, _| {
f.testwl.dispatch(); let request = connection.await_selection_request();
let data = data.resolve(); let data = mimes_truth
.iter()
.find(|data| data.data.mime_type == mime)
.unwrap_or_else(|| panic!("Asked for unknown mime: {mime}"));
connection.set_property(
request.requestor(),
data.mime,
request.property(),
&data.data.data,
);
connection.send_selection_notify(&request);
true
});
let mut found_mimes = Vec::new();
for testwl::PasteData { mime_type, data } in data { for testwl::PasteData { mime_type, data } in data {
match mime_type { match &mime_type {
x if x == "text/plain" => { x if x == "text/plain" => {
assert_eq!(&data, b"hello world"); assert_eq!(&data, b"hello world");
} }
@ -819,7 +852,17 @@ fn copy_from_x11() {
} }
other => panic!("unexpected mime type: {other} ({data:?})"), other => panic!("unexpected mime type: {other} ({data:?})"),
} }
found_mimes.push(mime_type);
} }
assert!(
found_mimes.contains(&"text/plain".to_string()),
"Didn't get mime data for text/plain"
);
assert!(
found_mimes.contains(&"blah/blah".to_string()),
"Didn't get mime data for blah/blah"
);
} }
#[test] #[test]
@ -828,13 +871,8 @@ fn copy_from_wayland() {
let mut connection = Connection::new(&f.display); let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false); let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.map_window(window); connection.get_selection_owner_change_events(true, window);
f.wait_and_dispatch(); f.map_as_toplevel(&mut connection, window);
let surface = f
.testwl
.last_created_surface_id()
.expect("No surface created");
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
let offer = vec![ let offer = vec![
testwl::PasteData { testwl::PasteData {
mime_type: "text/plain".into(), mime_type: "text/plain".into(),
@ -847,22 +885,10 @@ fn copy_from_wayland() {
]; ];
f.testwl.create_data_offer(offer.clone()); f.testwl.create_data_offer(offer.clone());
connection.await_selection_owner_change();
connection.verify_clipboard_owner(connection.wm_window);
connection.get_selection_owner_change_events(false, window);
let wm_window: x::Window = connection
.get_reply(&x::GetProperty {
delete: false,
window: connection.root,
property: connection.atoms.wm_check,
r#type: x::ATOM_WINDOW,
long_offset: 0,
long_length: 1,
})
.value()[0];
let reply = connection.get_reply(&x::GetSelectionOwner {
selection: connection.atoms.clipboard,
});
assert_eq!(reply.owner(), wm_window);
let dest1_atom = connection let dest1_atom = connection
.get_reply(&x::InternAtom { .get_reply(&x::InternAtom {
name: b"dest1", name: b"dest1",
@ -870,9 +896,6 @@ fn copy_from_wayland() {
}) })
.atom(); .atom();
// I don't know why, but omitting this little sleep prevents the SelectionRequest notification
// from being sent, and I don't have the heart to determine why.
std::thread::sleep(std::time::Duration::from_millis(1));
connection connection
.send_and_check_request(&x::ConvertSelection { .send_and_check_request(&x::ConvertSelection {
requestor: window, requestor: window,
@ -883,12 +906,7 @@ fn copy_from_wayland() {
}) })
.unwrap(); .unwrap();
connection.await_event(); let request = connection.await_selection_notify();
let request = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
other => panic!("Didn't get selection notify event, instead got {other:?}"),
};
assert_eq!(request.requestor(), window); assert_eq!(request.requestor(), window);
assert_eq!(request.selection(), connection.atoms.clipboard); assert_eq!(request.selection(), connection.atoms.clipboard);
assert_eq!(request.target(), connection.atoms.targets); assert_eq!(request.target(), connection.atoms.targets);
@ -927,11 +945,7 @@ fn copy_from_wayland() {
.unwrap(); .unwrap();
f.wait_and_dispatch(); f.wait_and_dispatch();
let request = match connection.poll_for_event().unwrap() { let request = connection.await_selection_notify();
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
other => panic!("Didn't get selection notify event, instead got {other:?}"),
};
assert_eq!(request.requestor(), window); assert_eq!(request.requestor(), window);
assert_eq!(request.selection(), connection.atoms.clipboard); assert_eq!(request.selection(), connection.atoms.clipboard);
assert_eq!(request.target(), atom); assert_eq!(request.target(), atom);
@ -974,7 +988,7 @@ fn different_output_position() {
f.testwl.move_pointer_to(surface, 10.0, 10.0); f.testwl.move_pointer_to(surface, 10.0, 10.0);
f.wait_and_dispatch(); f.wait_and_dispatch();
let reply = connection.get_reply(&x::QueryPointer { window }); let reply = connection.get_reply(&x::QueryPointer { window });
assert_eq!(reply.same_screen(), true); assert!(reply.same_screen());
assert_eq!(reply.win_x(), 10); assert_eq!(reply.win_x(), 10);
assert_eq!(reply.win_y(), 10); assert_eq!(reply.win_y(), 10);
@ -985,7 +999,7 @@ fn different_output_position() {
f.wait_and_dispatch(); f.wait_and_dispatch();
let reply = connection.get_reply(&x::QueryPointer { window }); let reply = connection.get_reply(&x::QueryPointer { window });
println!("reply: {reply:?}"); println!("reply: {reply:?}");
assert_eq!(reply.same_screen(), true); assert!(reply.same_screen());
assert_eq!(reply.win_x(), 150); assert_eq!(reply.win_x(), 150);
assert_eq!(reply.win_y(), 12); assert_eq!(reply.win_y(), 12);
} }
@ -1002,20 +1016,9 @@ fn bad_clipboard_data() {
.last_created_surface_id() .last_created_surface_id()
.expect("No surface created"); .expect("No surface created");
f.configure_and_verify_new_toplevel(&mut connection, window, surface); f.configure_and_verify_new_toplevel(&mut connection, window, surface);
connection.set_selection_owner(window);
connection let request = connection.await_selection_request();
.send_and_check_request(&x::SetSelectionOwner {
owner: window,
selection: connection.atoms.clipboard,
time: x::CURRENT_TIME,
})
.unwrap();
connection.await_event();
let request = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
other => panic!("Didn't get selection request event, instead got {other:?}"),
};
assert_eq!(request.target(), connection.atoms.targets); assert_eq!(request.target(), connection.atoms.targets);
connection.set_property( connection.set_property(
request.requestor(), request.requestor(),
@ -1023,73 +1026,22 @@ fn bad_clipboard_data() {
request.property(), request.property(),
&[connection.atoms.mime2], &[connection.atoms.mime2],
); );
connection connection.send_selection_notify(&request);
.send_and_check_request(&x::SendEvent {
propagate: false,
destination: x::SendEventDest::Window(request.requestor()),
event_mask: x::EventMask::empty(),
event: &x::SelectionNotifyEvent::new(
request.time(),
request.requestor(),
request.selection(),
request.target(),
request.property(),
),
})
.unwrap();
connection.await_event(); f.wait_and_dispatch();
let request = match connection.poll_for_event().unwrap() { let mut data = f.testwl.paste_data(|_, _| {
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, let request = connection.await_selection_request();
other => panic!("Didn't get selection request event, instead got {other:?}"),
};
assert_eq!(request.target(), connection.atoms.mime2); assert_eq!(request.target(), connection.atoms.mime2);
// Don't actually set any data as requested - just report success // Don't actually set any data as requested - just report success
connection.send_selection_notify(&request);
true
});
connection connection.verify_clipboard_owner(window);
.send_and_check_request(&x::SendEvent { assert_eq!(data.len(), 1, "Unexpected data: {data:?}");
propagate: false, let data = data.pop().unwrap();
destination: x::SendEventDest::Window(request.requestor()), assert_eq!(data.mime_type, "blah/blah");
event_mask: x::EventMask::empty(), assert!(data.data.is_empty(), "Unexpected data: {:?}", data.data);
event: &x::SelectionNotifyEvent::new(
request.time(),
request.requestor(),
request.selection(),
request.target(),
request.property(),
),
})
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
let owner = connection
.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
selection: connection.atoms.clipboard,
}))
.unwrap();
assert_ne!(window, owner.owner());
connection
.send_and_check_request(&x::ConvertSelection {
requestor: window,
selection: connection.atoms.clipboard,
target: connection.atoms.mime2,
property: connection.atoms.mime1,
time: x::CURRENT_TIME,
})
.unwrap();
connection.await_event();
let mut e = None;
while let Some(event) = connection.poll_for_event().unwrap() {
if let xcb::Event::X(x::Event::SelectionNotify(event)) = event {
e = Some(event);
break;
}
}
let e = e.expect("No selection notify event");
assert_eq!(e.property(), x::ATOM_NONE);
} }
// issue #42 // issue #42
@ -1161,7 +1113,7 @@ fn primary_output() {
let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root }); let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root });
let config_timestamp = reply.config_timestamp(); let config_timestamp = reply.config_timestamp();
let mut it = reply.outputs().into_iter().copied().map(|output| { let mut it = reply.outputs().iter().copied().map(|output| {
let reply = conn.get_reply(&xcb::randr::GetOutputInfo { let reply = conn.get_reply(&xcb::randr::GetOutputInfo {
output, output,
config_timestamp, config_timestamp,
@ -1214,21 +1166,15 @@ fn primary_output() {
assert_eq!(reply.output(), output3); assert_eq!(reply.output(), output3);
} }
// TODO: these sleeps are horrible.
#[test] #[test]
fn incr_copy_from_x11() { fn incr_copy_from_x11() {
let mut f = Fixture::new(); let mut f = Fixture::new();
let mut connection = Connection::new(&f.display); let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false); let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
f.map_as_toplevel(&mut connection, window); f.map_as_toplevel(&mut connection, window);
connection.set_selection_owner(window); connection.set_selection_owner(window);
std::thread::sleep(std::time::Duration::from_millis(10)); let request = connection.await_selection_request();
let request = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
other => panic!("Didn't get selection request event, instead got {other:?}"),
};
assert_eq!(request.target(), connection.atoms.targets); assert_eq!(request.target(), connection.atoms.targets);
connection.set_property( connection.set_property(
request.requestor(), request.requestor(),
@ -1237,14 +1183,12 @@ fn incr_copy_from_x11() {
&[connection.atoms.targets, connection.atoms.mime1], &[connection.atoms.targets, connection.atoms.mime1],
); );
connection.send_selection_notify(&request); connection.send_selection_notify(&request);
connection.await_event(); f.wait_and_dispatch();
let request = match connection.poll_for_event().unwrap() { let mut destination_property = x::Atom::none();
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, let mut begin_incr = Some(|connection: &mut Connection| {
other => panic!("Didn't get selection request event, instead got {other:?}"), let request = connection.await_selection_request();
};
assert_eq!(request.target(), connection.atoms.mime1); assert_eq!(request.target(), connection.atoms.mime1);
let destination_property = request.property();
connection connection
.send_and_check_request(&x::ChangeWindowAttributes { .send_and_check_request(&x::ChangeWindowAttributes {
@ -1255,7 +1199,7 @@ fn incr_copy_from_x11() {
connection.set_property( connection.set_property(
request.requestor(), request.requestor(),
connection.atoms.incr, connection.atoms.incr,
destination_property, request.property(),
&[3000u32], &[3000u32],
); );
connection.send_selection_notify(&request); connection.send_selection_notify(&request);
@ -1266,25 +1210,38 @@ fn incr_copy_from_x11() {
}; };
assert_eq!(notify.atom(), request.property()); assert_eq!(notify.atom(), request.property());
assert_eq!(notify.state(), x::Property::NewValue); assert_eq!(notify.state(), x::Property::NewValue);
request.property()
});
let data: Vec<u8> = std::iter::successors(Some(1u8), |n| Some(n.wrapping_add(1))) let data: Vec<u8> = std::iter::successors(Some(1u8), |n| Some(n.wrapping_add(1)))
.take(3000) .take(3000)
.collect(); .collect();
for (idx, chunk) in data.chunks(500).enumerate() { let mut it = data.chunks(500).enumerate();
std::thread::sleep(std::time::Duration::from_millis(10)); let mut paste_data = f.testwl.paste_data(|_, testwl| {
if let Some(begin) = begin_incr.take() {
destination_property = begin(&mut connection);
testwl.dispatch();
return false;
}
assert_ne!(destination_property, x::Atom::none());
connection.await_event();
let notify = match connection.poll_for_event().unwrap() { let notify = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p, Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
other => panic!("Didn't get property notify event, instead got {other:?}"), other => panic!("Didn't get property notify event, instead got {other:?}"),
}; };
match it.next() {
Some((idx, chunk)) => {
assert_eq!(notify.atom(), destination_property, "chunk {idx}"); assert_eq!(notify.atom(), destination_property, "chunk {idx}");
assert_eq!(notify.state(), x::Property::Delete, "chunk {idx}"); assert_eq!(notify.state(), x::Property::Delete, "chunk {idx}");
connection.set_property( connection.set_property(
request.requestor(), notify.window(),
connection.atoms.mime1, connection.atoms.mime1,
destination_property, destination_property,
chunk, chunk,
); );
testwl.dispatch();
// skip NewValue // skip NewValue
let notify = match connection.poll_for_event().unwrap() { let notify = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p, Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
@ -1292,37 +1249,60 @@ fn incr_copy_from_x11() {
}; };
assert_eq!(notify.atom(), destination_property, "chunk {idx}"); assert_eq!(notify.atom(), destination_property, "chunk {idx}");
assert_eq!(notify.state(), x::Property::NewValue, "chunk {idx}"); assert_eq!(notify.state(), x::Property::NewValue, "chunk {idx}");
false
} }
None => {
std::thread::sleep(std::time::Duration::from_millis(10)); // INCR completed!
let notify = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
other => panic!("Didn't get property notify event, instead got {other:?}"),
};
assert_eq!(notify.atom(), destination_property); assert_eq!(notify.atom(), destination_property);
assert_eq!(notify.state(), x::Property::Delete); assert_eq!(notify.state(), x::Property::Delete);
connection.set_property::<u8>( connection.set_property::<u8>(
request.requestor(), notify.window(),
connection.atoms.mime1, connection.atoms.mime1,
destination_property, destination_property,
&[], &[],
); );
true
std::thread::sleep(std::time::Duration::from_millis(100)); }
let owner = connection }
.wait_for_reply(connection.send_request(&x::GetSelectionOwner { });
selection: connection.atoms.clipboard,
}))
.unwrap();
assert_ne!(window, owner.owner());
f.wait_and_dispatch();
assert_eq!(f.testwl.data_source_mimes(), vec!["text/plain"]); assert_eq!(f.testwl.data_source_mimes(), vec!["text/plain"]);
let wl_data = f.testwl.paste_data(); assert_eq!(paste_data.len(), 1);
f.testwl.dispatch(); let paste_data = paste_data.swap_remove(0);
let mut wl_data = wl_data.resolve(); assert_eq!(paste_data.mime_type, "text/plain");
assert_eq!(wl_data.len(), 1); assert_eq!(&paste_data.data, &data);
let wl_data = wl_data.swap_remove(0); }
assert_eq!(wl_data.mime_type, "text/plain");
assert_eq!(&wl_data.data, &data); #[test]
fn wayland_then_x11_clipboard_owner() {
let mut f = Fixture::new();
let mut connection = Connection::new(&f.display);
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
connection.get_selection_owner_change_events(true, window);
f.map_as_toplevel(&mut connection, window);
let offer = vec![
testwl::PasteData {
mime_type: "text/plain".into(),
data: b"boingloings".to_vec(),
},
testwl::PasteData {
mime_type: "yah/hah".into(),
data: vec![1, 2, 3, 2, 1],
},
];
f.testwl.create_data_offer(offer.clone());
connection.await_selection_owner_change();
connection.verify_clipboard_owner(connection.wm_window);
connection.get_selection_owner_change_events(false, window);
connection.set_selection_owner(window);
f.testwl.dispatch();
connection.verify_clipboard_owner(window);
connection.await_event();
let request = connection.await_selection_request();
assert_eq!(request.selection(), connection.atoms.clipboard);
assert_eq!(request.target(), connection.atoms.targets);
} }

View file

@ -503,6 +503,7 @@ impl Server {
self.state.pointer.as_ref().unwrap() self.state.pointer.as_ref().unwrap()
} }
#[track_caller]
pub fn data_source_mimes(&self) -> Vec<String> { pub fn data_source_mimes(&self) -> Vec<String> {
let Some(selection) = &self.state.selection else { let Some(selection) = &self.state.selection else {
panic!("No selection set on data device"); panic!("No selection set on data device");
@ -513,13 +514,79 @@ impl Server {
data.mimes.to_vec() data.mimes.to_vec()
} }
pub fn paste_data(&mut self) -> PasteDataResolver { #[track_caller]
let Some(selection) = &self.state.selection else { pub fn paste_data(
&mut self,
mut send_data_for_mime: impl FnMut(&str, &mut Self) -> bool,
) -> Vec<PasteData> {
struct PendingData {
rx: std::fs::File,
data: Vec<u8>,
}
let Some(selection) = self.state.selection.take() else {
panic!("No selection set on data device"); panic!("No selection set on data device");
}; };
type PendingRet = Vec<(String, Option<PendingData>)>;
let mut pending_ret: PendingRet = {
let data: &Mutex<DataSourceData> = selection.data().unwrap();
data.lock()
.unwrap()
.mimes
.iter()
.rev()
.map(|mime| (mime.clone(), None))
.collect()
};
let ret = PasteDataResolver::new(&selection); let mut ret = Vec::new();
let mut try_transfer =
|pending_ret: &mut PendingRet, mime: String, mut pending: PendingData| {
self.display.flush_clients().unwrap(); self.display.flush_clients().unwrap();
let transfer_complete = send_data_for_mime(&mime, self);
if transfer_complete {
pending.rx.read_to_end(&mut pending.data).unwrap();
ret.push(PasteData {
mime_type: mime,
data: pending.data,
});
} else {
loop {
match pending.rx.read(&mut pending.data) {
Ok(0) => break,
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
Err(e) => panic!("Failed reading data for mime {mime}: {e:?}"),
}
}
pending_ret.push((mime, Some(pending)));
self.dispatch();
}
};
while !pending_ret.is_empty() {
let (mime, pending) = pending_ret.pop().unwrap();
match pending {
Some(pending) => try_transfer(&mut pending_ret, mime, pending),
None => {
let (rx, tx) = rustix::pipe::pipe().unwrap();
selection.send(mime.clone(), tx.as_fd());
drop(tx);
let rx = std::fs::File::from(rx);
try_transfer(
&mut pending_ret,
mime,
PendingData {
rx,
data: Vec::new(),
},
);
}
}
}
self.state.selection = Some(selection);
ret ret
} }
@ -527,6 +594,7 @@ impl Server {
self.state.selection.is_none() self.state.selection.is_none()
} }
#[track_caller]
pub fn create_data_offer(&mut self, data: Vec<PasteData>) { pub fn create_data_offer(&mut self, data: Vec<PasteData>) {
let Some(dev) = &self.state.data_device else { let Some(dev) = &self.state.data_device else {
panic!("No data device created"); panic!("No data device created");
@ -611,45 +679,6 @@ impl Server {
} }
} }
pub struct PasteDataResolver {
fds: Vec<(String, OwnedFd, OwnedFd)>,
}
impl PasteDataResolver {
fn new(source: &WlDataSource) -> Self {
let data: &Mutex<DataSourceData> = source.data().unwrap();
let data = data.lock().unwrap();
let mimes = &data.mimes;
let fds = mimes
.iter()
.map(|mime| {
let (rx, tx) = rustix::pipe::pipe().unwrap();
source.send(mime.clone(), tx.as_fd());
(mime.clone(), tx, rx)
})
.collect();
PasteDataResolver { fds }
}
pub fn resolve(self) -> Vec<PasteData> {
self.fds
.into_iter()
.map(|(mime, tx, rx)| {
drop(tx);
let mut data = Vec::new();
let mut file = std::fs::File::from(rx);
file.read_to_end(&mut data).unwrap();
PasteData {
mime_type: mime,
data,
}
})
.collect()
}
}
#[derive(Clone, Eq, PartialEq, Debug)] #[derive(Clone, Eq, PartialEq, Debug)]
pub struct PasteData { pub struct PasteData {
pub mime_type: String, pub mime_type: String,
@ -857,6 +886,7 @@ impl Dispatch<WlDataOffer, Vec<PasteData>> for State {
let mut stream = UnixStream::from(fd); let mut stream = UnixStream::from(fd);
stream.write_all(&data[pos].data).unwrap(); stream.write_all(&data[pos].data).unwrap();
} }
wl_data_offer::Request::Destroy => {}
other => todo!("unhandled request: {other:?}"), other => todo!("unhandled request: {other:?}"),
} }
} }