fix: uniquely define property separate from target

Both PRIMARY and CLIPBOARD were using the same target atoms for their
property. When both are simultaneously requested (the behavior of
`wl-clip-persist --clipboard both`), the first would delete the property
the second was prepared to write data to, resulting in getting the
GetProperty reply containing the failure data.

This commit distinguishes `target` as the atom of a mime type contained
within `TARGETS`, and `property` as the atom which contains both data of
`target` and which selection requested it.
This commit is contained in:
En-En 2025-10-23 15:13:40 +00:00 committed by Supreeeme
parent e991cb39c2
commit 114d48e2e1

View file

@ -11,12 +11,14 @@ use xcb::x;
#[derive(Debug)]
struct SelectionTargetId {
name: String,
atom: x::Atom,
target: x::Atom,
property: x::Atom,
source: Option<String>,
}
struct PendingSelectionData {
target: x::Atom,
property: x::Atom,
pipe: WritePipe,
incr: bool,
}
@ -41,14 +43,15 @@ impl X11Selection for Selection {
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
// A concatenation of the target and the selection type are used to create a distinct
// property to write to (see XState::intern_target_property_atoms).
if let Err(e) = self
.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.window,
selection: self.selection,
target: target.atom,
property: target.atom,
target: target.target,
property: target.property,
time: self.selection_time,
})
{
@ -57,7 +60,8 @@ impl X11Selection for Selection {
}
self.pending.borrow_mut().push(PendingSelectionData {
target: target.atom,
target: target.target,
property: target.property,
pipe,
incr: false,
})
@ -82,8 +86,17 @@ impl Selection {
mut pipe,
incr,
target,
property,
} = pending.swap_remove(idx);
let reply = match get_property_any(&self.connection, self.window, target) {
let request = self.connection.send_request(&x::GetProperty {
delete: true,
window: self.window,
property,
r#type: x::ATOM_ANY,
long_offset: 0,
long_length: u32::MAX,
});
let reply = match self.connection.wait_for_reply(request) {
Ok(reply) => reply,
Err(e) => {
warn!(
@ -97,16 +110,17 @@ impl Selection {
debug!(
"got type {} for mime type {}",
get_atom_name(&self.connection, reply.r#type()),
get_atom_name(&self.connection, target)
get_atom_name(&self.connection, target),
);
if reply.r#type() == self.incr {
debug!(
"beginning incr for {}",
get_atom_name(&self.connection, target)
get_atom_name(&self.connection, property)
);
pending.push(PendingSelectionData {
target,
property,
pipe,
incr: true,
});
@ -132,6 +146,7 @@ impl Selection {
);
pending.push(PendingSelectionData {
target,
property,
pipe,
incr: true,
})
@ -152,7 +167,7 @@ impl Selection {
}
let target = self.pending.borrow().iter().find_map(|pending| {
(pending.target == event.atom() && pending.incr).then_some(pending.target)
(pending.property == event.atom() && pending.incr).then_some(pending.target)
});
if let Some(target) = target {
self.handle_notify(target);
@ -416,14 +431,26 @@ impl<T: SelectionType> SelectionDataImpl for SelectionData<T> {
debug!("got targets: {targets_str:?}");
}
let selection = get_atom_name(connection, self.atom);
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,
.map(|target| {
let name = get_atom_name(connection, target);
let property = connection
.wait_for_reply(connection.send_request(&x::InternAtom {
only_if_exists: false,
name: &[name.as_bytes(), b"_", selection.as_bytes()].concat(),
}))
.unwrap()
.atom();
SelectionTargetId {
name,
target,
property,
source: None,
}
})
.collect();
@ -469,7 +496,7 @@ impl<T: SelectionType> SelectionDataImpl for SelectionData<T> {
let req_target = request.target();
if req_target == atoms.targets {
let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect();
let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.target).collect();
match connection.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
@ -485,7 +512,7 @@ impl<T: SelectionType> SelectionDataImpl for SelectionData<T> {
}
}
} else {
let Some(target) = mimes.iter().find(|t| t.atom == req_target) else {
let Some(target) = mimes.iter().find(|t| t.target == req_target) else {
if log::log_enabled!(log::Level::Debug) {
let name = get_atom_name(connection, req_target);
debug!(
@ -521,14 +548,14 @@ impl<T: SelectionType> SelectionDataImpl for SelectionData<T> {
}
debug!(
"beginning incr for {}",
get_atom_name(connection, target.atom)
get_atom_name(connection, target.target)
);
*incr_data = Some(WaylandIncrInfo {
data,
start: 0,
target_window: request.requestor(),
property: request.property(),
target_type: target.atom,
target_type: target.target,
max_req_bytes,
});
true
@ -537,7 +564,7 @@ impl<T: SelectionType> SelectionDataImpl for SelectionData<T> {
mode: x::PropMode::Replace,
window: request.requestor(),
property: request.property(),
r#type: target.atom,
r#type: target.target,
data: &data,
}) {
Ok(_) => true,
@ -585,6 +612,26 @@ impl SelectionState {
}
impl XState {
fn intern_target_property_atoms(&self, mime: &[u8], suffix: &[u8]) -> (x::Atom, x::Atom) {
let target = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: mime,
}))
.unwrap()
.atom();
let property = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: &[mime, suffix].concat(),
}))
.unwrap()
.atom();
(target, property)
}
pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection<Clipboard>) {
let mut utf8_xwl = false;
let mut utf8_wl = false;
@ -598,17 +645,12 @@ impl XState {
_ => {}
}
let atom = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: mime.as_bytes(),
}))
.unwrap();
let (target, property) =
self.intern_target_property_atoms(mime.as_bytes(), b"_CLIPBOARD");
SelectionTargetId {
name: mime.clone(),
atom: atom.atom(),
target,
property,
source: None,
}
})
@ -616,17 +658,12 @@ impl XState {
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();
let (target, property) =
self.intern_target_property_atoms(name.as_bytes(), b"_CLIPBOARD");
mimes.push(SelectionTargetId {
name,
atom,
target,
property,
source: Some("text/plain;charset=utf-8".to_string()),
});
}
@ -660,17 +697,12 @@ impl XState {
_ => {}
}
let atom = self
.connection
.wait_for_reply(self.connection.send_request(&x::InternAtom {
only_if_exists: false,
name: mime.as_bytes(),
}))
.unwrap();
let (target, property) =
self.intern_target_property_atoms(mime.as_bytes(), b"_PRIMARY");
SelectionTargetId {
name: mime.clone(),
atom: atom.atom(),
target,
property,
source: None,
}
})
@ -678,17 +710,12 @@ impl XState {
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();
let (target, property) =
self.intern_target_property_atoms(name.as_bytes(), b"_PRIMARY");
mimes.push(SelectionTargetId {
name,
atom,
target,
property,
source: Some("text/plain;charset=utf-8".to_string()),
});
}
@ -889,18 +916,3 @@ impl XState {
|| inner(&self.connection, event, &mut self.selection_state.clipboard)
}
}
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,
}))
}