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