Support primary selection

This was more tedious than expected.
Fixes #103
This commit is contained in:
Shawn Wallace 2025-08-14 01:30:04 -04:00
parent 13469566b0
commit 5a184d4359
11 changed files with 1086 additions and 391 deletions

View file

@ -1,5 +1,5 @@
use super::{get_atom_name, XState};
use crate::server::ForeignSelection;
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;
@ -26,7 +26,7 @@ pub struct Selection {
connection: Rc<xcb::Connection>,
window: x::Window,
pending: RefCell<Vec<PendingSelectionData>>,
clipboard: x::Atom,
selection: x::Atom,
selection_time: u32,
incr: x::Atom,
}
@ -46,13 +46,13 @@ impl X11Selection for Selection {
.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.window,
selection: self.clipboard,
selection: self.selection,
target: target.atom,
property: target.atom,
time: self.selection_time,
})
{
error!("Failed to request clipboard data (mime type: {mime}, error: {e})");
error!("Failed to request selection data (mime type: {mime}, error: {e})");
return;
}
@ -162,21 +162,275 @@ impl Selection {
}
}
enum CurrentSelection {
enum CurrentSelection<T: SelectionType> {
X11(Rc<Selection>),
Wayland {
mimes: Vec<SelectionTargetId>,
inner: ForeignSelection,
inner: ForeignSelection<T>,
},
}
pub(crate) struct SelectionData {
struct SelectionData<T: SelectionType> {
last_selection_timestamp: u32,
target_window: x::Window,
current_selection: Option<CurrentSelection>,
atom: x::Atom,
current_selection: Option<CurrentSelection<T>>,
}
impl SelectionData {
pub fn new(connection: &xcb::Connection, root: x::Window) -> Self {
// 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 {
@ -195,39 +449,15 @@ impl SelectionData {
})
.expect("Couldn't create window for selections");
Self {
last_selection_timestamp: x::CURRENT_TIME,
target_window,
current_selection: None,
clipboard: SelectionData::new(atoms.clipboard),
primary: SelectionData::new(atoms.primary),
}
}
}
impl XState {
fn set_clipboard_owner(&mut self) {
self.connection
.send_and_check_request(&x::SetSelectionOwner {
owner: self.wm_window,
selection: self.atoms.clipboard,
time: self.selection_data.last_selection_timestamp,
})
.unwrap();
let reply = self
.connection
.wait_for_reply(self.connection.send_request(&x::GetSelectionOwner {
selection: self.atoms.clipboard,
}))
.unwrap();
if reply.owner() != self.wm_window {
warn!(
"Could not get CLIPBOARD selection (owned by {:?})",
reply.owner()
);
}
}
pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) {
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
@ -273,51 +503,133 @@ impl XState {
});
}
self.selection_data.current_selection = Some(CurrentSelection::Wayland {
self.selection_state.clipboard.current_selection = Some(CurrentSelection::Wayland {
mimes,
inner: selection,
});
self.set_clipboard_owner();
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)) => {
if e.selection() == self.atoms.clipboard {
self.handle_new_selection_owner(e.owner(), e.time());
}
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?");
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 notify requestor: {:?} target: {} selection: {}",
e.requestor(),
get_atom_name(&self.connection, e.target())
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 => {
self.handle_target_list(e.property(), server_state)
}
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_data.target_window {
if let Some(CurrentSelection::X11(selection)) =
&self.selection_data.current_selection
{
} else if e.requestor() == self.selection_state.target_window {
if let Some(selection) = data.x11_selection() {
selection.handle_notify(e.target());
}
} else {
@ -328,6 +640,7 @@ impl XState {
}
}
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 {
@ -349,7 +662,8 @@ impl XState {
if log::log_enabled!(log::Level::Debug) {
let target = get_atom_name(&self.connection, e.target());
debug!("Got selection request for target {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 {
@ -358,76 +672,37 @@ impl XState {
return true;
}
let Some(CurrentSelection::Wayland { mimes, inner }) =
&self.selection_data.current_selection
else {
warn!("Got selection request, but we don't seem to be the selection owner");
refuse();
return true;
};
match e.target() {
x if x == self.atoms.targets => {
let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect();
self.connection
.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: e.requestor(),
property: e.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(&self.connection, other);
debug!("refusing selection request because given atom could not be found ({name})");
}
refuse();
return true;
};
let mime_name = target
.source
.as_ref()
.cloned()
.unwrap_or_else(|| target.name.clone());
let data = inner.receive(mime_name, server_state);
match self.connection.send_and_check_request(&x::ChangeProperty {
mode: x::PropMode::Replace,
window: e.requestor(),
property: e.property(),
r#type: target.atom,
data: &data,
}) {
Ok(_) => success(),
Err(e) => {
warn!("Failed setting selection property: {e:?}");
refuse();
}
}
}
}
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 => match e.subtype() {
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;
}
self.handle_new_selection_owner(e.owner(), e.selection_timestamp());
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_data.current_selection = None;
self.selection_state.clipboard.current_selection = None;
}
},
x if x == self.atoms.xsettings => match e.subtype() {
@ -446,105 +721,17 @@ impl XState {
true
}
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
.connection
.wait_for_reply(self.connection.send_request(&x::GetProperty {
delete: true,
window: self.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 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) {
let targets_str: Vec<String> = targets
.iter()
.map(|t| get_atom_name(&self.connection, *t))
.collect();
debug!("got targets: {targets_str:?}");
}
let mimes = targets
.iter()
.copied()
.filter(|atom| {
![
self.atoms.targets,
self.atoms.multiple,
self.atoms.save_targets,
]
.contains(atom)
})
.map(|target_atom| SelectionTargetId {
name: get_atom_name(&self.connection, target_atom),
atom: target_atom,
source: None,
})
.collect();
let selection = Rc::new(Selection {
mimes,
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,
});
server_state.set_copy_paste_source(&selection);
self.selection_data.current_selection = Some(CurrentSelection::X11(selection));
debug!("Clipboard set from X11");
}
pub(super) fn handle_selection_property_change(
&mut self,
event: &x::PropertyNotifyEvent,
) -> bool {
if let Some(CurrentSelection::X11(selection)) = &self.selection_data.current_selection {
return selection.check_for_incr(event);
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
}