Add support for ext-data-control

Since the protocol is more or less exactly the same as wlr-data-control, I
added some wrappers and macros to re-use the same existing code for both
protocols.
This commit is contained in:
Ivan Molodetskikh
2025-02-25 11:09:00 +03:00
parent 148d6c70cd
commit e977111bea
11 changed files with 610 additions and 337 deletions

View File

@@ -38,12 +38,12 @@ thiserror = "2"
tree_magic_mini = "3.1.6"
wayland-backend = "0.3.8"
wayland-client = "0.31.8"
wayland-protocols = { version = "0.32.6", features = ["client"] }
wayland-protocols = { version = "0.32.6", features = ["client", "staging"] }
wayland-protocols-wlr = { version = "0.3.6", features = ["client"] }
[dev-dependencies]
wayland-server = "0.31.7"
wayland-protocols = { version = "0.32.6", features = ["server"] }
wayland-protocols = { version = "0.32.6", features = ["server", "staging"] }
wayland-protocols-wlr = { version = "0.3.6", features = ["server"] }
proptest = "1.6.0"
proptest-derive = "0.5.1"

View File

@@ -15,10 +15,10 @@ please use the appropriate Wayland protocols for interacting with the Wayland cl
primary selection), for example via the
[smithay-clipboard](https://crates.io/crates/smithay-clipboard) crate.
The protocol used for clipboard interaction is `data-control` from
[wlroots](https://github.com/swaywm/wlr-protocols). When using the regular clipboard, the
compositor must support the first version of the protocol. When using the "primary" clipboard,
the compositor must support the second version of the protocol (or higher).
The protocol used for clipboard interaction is `ext-data-control` or `wlr-data-control`. When
using the regular clipboard, the compositor must support any version of either protocol. When
using the "primary" clipboard, the compositor must support any version of `ext-data-control`,
or the second version of the `wlr-data-control` protocol.
For example applications using these features, see `wl-clipboard-rs-tools/src/bin/wl_copy.rs`
and `wl-clipboard-rs-tools/src/bin/wl_paste.rs` which implement terminal apps similar to
@@ -72,19 +72,19 @@ use wl_clipboard_rs::utils::{is_primary_selection_supported, PrimarySelectionChe
match is_primary_selection_supported() {
Ok(supported) => {
// We have our definitive result. False means that either data-control version 1
// is present (which does not support the primary selection), or that data-control
// version 2 is present and it did not signal the primary selection support.
// We have our definitive result. False means that ext/wlr-data-control is present
// and did not signal the primary selection support, or that only wlr-data-control
// version 1 is present (which does not support primary selection).
},
Err(PrimarySelectionCheckError::NoSeats) => {
// Impossible to give a definitive result. Primary selection may or may not be
// supported.
// The required protocol (data-control version 2) is there, but there are no seats.
// Unfortunately, at least one seat is needed to check for the primary clipboard
// support.
// The required protocol (ext-data-control, or wlr-data-control version 2) is there,
// but there are no seats. Unfortunately, at least one seat is needed to check for the
// primary clipboard support.
},
Err(PrimarySelectionCheckError::MissingProtocol { .. }) => {
Err(PrimarySelectionCheckError::MissingProtocol) => {
// The data-control protocol (required for wl-clipboard-rs operation) is not
// supported by the compositor.
},

View File

@@ -5,17 +5,19 @@ use std::path::PathBuf;
use std::{env, io};
use wayland_backend::client::WaylandError;
use wayland_client::globals::{registry_queue_init, BindError, GlobalError, GlobalListContents};
use wayland_client::globals::{registry_queue_init, GlobalError, GlobalListContents};
use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::{self, WlSeat};
use wayland_client::{ConnectError, Connection, Dispatch, EventQueue, Proxy};
use wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
use crate::data_control::Manager;
use crate::seat_data::SeatData;
pub struct State {
pub seats: HashMap<WlSeat, SeatData>,
pub clipboard_manager: ZwlrDataControlManagerV1,
pub clipboard_manager: Manager,
}
#[derive(thiserror::Error, Debug)]
@@ -30,9 +32,10 @@ pub enum Error {
WaylandCommunication(#[source] WaylandError),
#[error(
"A required Wayland protocol ({name} version {version}) is not supported by the compositor"
"A required Wayland protocol (ext-data-control, or wlr-data-control version {version}) \
is not supported by the compositor"
)]
MissingProtocol { name: &'static str, version: u32 },
MissingProtocol { version: u32 },
}
impl<S> Dispatch<WlSeat, (), S> for State
@@ -62,6 +65,7 @@ pub fn initialize<S>(
where
S: Dispatch<WlRegistry, GlobalListContents> + 'static,
S: Dispatch<ZwlrDataControlManagerV1, ()>,
S: Dispatch<ExtDataControlManagerV1, ()>,
S: Dispatch<WlSeat, ()>,
S: AsMut<State>,
{
@@ -95,18 +99,15 @@ where
})?;
let qh = &queue.handle();
let data_control_version = if primary { 2 } else { 1 };
// Verify that we got the clipboard manager.
let clipboard_manager = match globals.bind(qh, data_control_version..=data_control_version, ())
{
Ok(manager) => manager,
Err(BindError::NotPresent | BindError::UnsupportedVersion) => {
return Err(Error::MissingProtocol {
name: ZwlrDataControlManagerV1::interface().name,
version: data_control_version,
})
}
let ext_manager = globals.bind(qh, 1..=1, ()).ok().map(Manager::Ext);
let wlr_v = if primary { 2 } else { 1 };
let wlr_manager = || globals.bind(qh, wlr_v..=wlr_v, ()).ok().map(Manager::Zwlr);
let clipboard_manager = match ext_manager.or_else(wlr_manager) {
Some(manager) => manager,
None => return Err(Error::MissingProtocol { version: wlr_v }),
};
let registry = globals.registry();

View File

@@ -16,18 +16,12 @@ use wayland_client::protocol::wl_registry::WlRegistry;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{
delegate_dispatch, event_created_child, ConnectError, Dispatch, DispatchError, EventQueue,
Proxy,
};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::{
self, ZwlrDataControlDeviceV1,
};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::{
self, ZwlrDataControlSourceV1,
};
use crate::common::{self, initialize};
use crate::data_control::{
self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, impl_dispatch_source,
};
use crate::seat_data::SeatData;
use crate::utils::is_text;
@@ -40,8 +34,8 @@ pub enum ClipboardType {
Regular,
/// The "primary" clipboard.
///
/// Working with the "primary" clipboard requires the compositor to support the data-control
/// protocol of version 2 or above.
/// Working with the "primary" clipboard requires the compositor to support ext-data-control,
/// or wlr-data-control version 2 or above.
Primary,
/// Operate on both clipboards at once.
///
@@ -144,7 +138,7 @@ pub struct Options {
pub struct PreparedCopy {
queue: EventQueue<State>,
state: State,
sources: Vec<ZwlrDataControlSourceV1>,
sources: Vec<data_control::Source>,
}
/// Errors that can occur for copying the source data to a temporary file.
@@ -194,11 +188,10 @@ pub enum Error {
WaylandCommunication(#[source] DispatchError),
#[error(
"A required Wayland protocol ({} version {}) is not supported by the compositor",
name,
version
"A required Wayland protocol (ext-data-control, or wlr-data-control version {version}) \
is not supported by the compositor"
)]
MissingProtocol { name: &'static str, version: u32 },
MissingProtocol { version: u32 },
#[error("The compositor does not support primary selection")]
PrimarySelectionUnsupported,
@@ -227,7 +220,7 @@ impl From<common::Error> for Error {
SocketOpenError(err) => Error::SocketOpenError(err),
WaylandConnection(err) => Error::WaylandConnection(err),
WaylandCommunication(err) => Error::WaylandCommunication(err.into()),
MissingProtocol { name, version } => Error::MissingProtocol { name, version },
MissingProtocol { version } => Error::MissingProtocol { version },
}
}
}
@@ -273,115 +266,75 @@ impl Dispatch<WlRegistry, GlobalListContents> for State {
}
}
impl Dispatch<ZwlrDataControlManagerV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &ZwlrDataControlManagerV1,
_event: <ZwlrDataControlManagerV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
impl_dispatch_manager!(State);
impl Dispatch<ZwlrDataControlDeviceV1, WlSeat> for State {
fn event(
state: &mut Self,
_device: &ZwlrDataControlDeviceV1,
event: <ZwlrDataControlDeviceV1 as Proxy>::Event,
seat: &WlSeat,
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
match event {
zwlr_data_control_device_v1::Event::DataOffer { id } => id.destroy(),
zwlr_data_control_device_v1::Event::Finished => {
state.common.seats.get_mut(seat).unwrap().set_device(None);
}
zwlr_data_control_device_v1::Event::PrimarySelection { .. } => {
state.got_primary_selection = true;
}
_ => (),
impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat| {
match event {
Event::DataOffer { id } => id.destroy(),
Event::Finished => {
state.common.seats.get_mut(seat).unwrap().set_device(None);
}
}
event_created_child!(State, ZwlrDataControlDeviceV1, [
zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, ()),
]);
}
impl Dispatch<ZwlrDataControlOfferV1, ()> for State {
fn event(
_state: &mut Self,
_offer: &ZwlrDataControlOfferV1,
_event: <ZwlrDataControlOfferV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
impl Dispatch<ZwlrDataControlSourceV1, ()> for State {
fn event(
state: &mut Self,
source: &ZwlrDataControlSourceV1,
event: <ZwlrDataControlSourceV1 as Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
match event {
zwlr_data_control_source_v1::Event::Send { mime_type, fd } => {
// Check if some other source already handled a paste request and indicated that we should
// quit.
if state.should_quit {
source.destroy();
return;
}
// I'm not sure if it's the compositor's responsibility to check that the mime type is
// valid. Let's check here just in case.
if !state.data_paths.contains_key(&mime_type) {
return;
}
let data_path = &state.data_paths[&mime_type];
let file = File::open(data_path).map_err(DataSourceError::FileOpen);
let result = file.and_then(|mut data_file| {
// Clear O_NONBLOCK, otherwise io::copy() will stop halfway.
fcntl_setfl(&fd, OFlags::empty())
.map_err(io::Error::from)
.map_err(DataSourceError::Copy)?;
let mut target_file = File::from(fd);
io::copy(&mut data_file, &mut target_file).map_err(DataSourceError::Copy)
});
if let Err(err) = result {
state.error = Some(err);
}
let done = if let ServeRequests::Only(left) = state.serve_requests {
let left = left.checked_sub(1).unwrap();
state.serve_requests = ServeRequests::Only(left);
left == 0
} else {
false
};
if done || state.error.is_some() {
state.should_quit = true;
source.destroy();
}
}
zwlr_data_control_source_v1::Event::Cancelled => source.destroy(),
_ => (),
Event::PrimarySelection { .. } => {
state.got_primary_selection = true;
}
_ => (),
}
}
});
impl_dispatch_offer!(State);
impl_dispatch_source!(State, |state: &mut Self,
source: data_control::Source,
event| {
match event {
Event::Send { mime_type, fd } => {
// Check if some other source already handled a paste request and indicated that we should
// quit.
if state.should_quit {
source.destroy();
return;
}
// I'm not sure if it's the compositor's responsibility to check that the mime type is
// valid. Let's check here just in case.
if !state.data_paths.contains_key(&mime_type) {
return;
}
let data_path = &state.data_paths[&mime_type];
let file = File::open(data_path).map_err(DataSourceError::FileOpen);
let result = file.and_then(|mut data_file| {
// Clear O_NONBLOCK, otherwise io::copy() will stop halfway.
fcntl_setfl(&fd, OFlags::empty())
.map_err(io::Error::from)
.map_err(DataSourceError::Copy)?;
let mut target_file = File::from(fd);
io::copy(&mut data_file, &mut target_file).map_err(DataSourceError::Copy)
});
if let Err(err) = result {
state.error = Some(err);
}
let done = if let ServeRequests::Only(left) = state.serve_requests {
let left = left.checked_sub(1).unwrap();
state.serve_requests = ServeRequests::Only(left);
left == 0
} else {
false
};
if done || state.error.is_some() {
state.should_quit = true;
source.destroy();
}
}
Event::Cancelled => source.destroy(),
_ => (),
}
});
impl Options {
/// Creates a blank new set of options ready for configuration.
@@ -676,7 +629,7 @@ fn get_devices(
primary: bool,
seat: Seat,
socket_name: Option<OsString>,
) -> Result<(EventQueue<State>, State, Vec<ZwlrDataControlDeviceV1>), Error> {
) -> Result<(EventQueue<State>, State, Vec<data_control::Device>), Error> {
let (mut queue, mut common) = initialize(primary, socket_name)?;
// Check if there are no seats.
@@ -980,7 +933,7 @@ fn prepare_copy_internal(
let data_source = state
.common
.clipboard_manager
.create_data_source(&queue.handle(), ());
.create_data_source(&queue.handle());
for mime_type in state.data_paths.keys() {
data_source.offer(mime_type.clone());

303
src/data_control.rs Normal file
View File

@@ -0,0 +1,303 @@
//! Abstraction over ext/wlr-data-control.
use std::os::fd::BorrowedFd;
use ext::ext_data_control_device_v1::ExtDataControlDeviceV1;
use ext::ext_data_control_manager_v1::ExtDataControlManagerV1;
use ext::ext_data_control_offer_v1::ExtDataControlOfferV1;
use ext::ext_data_control_source_v1::ExtDataControlSourceV1;
use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{Dispatch, Proxy as _, QueueHandle};
use wayland_protocols::ext::data_control::v1::client as ext;
use wayland_protocols_wlr::data_control::v1::client as zwlr;
use zwlr::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1;
use zwlr::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
use zwlr::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1;
use zwlr::zwlr_data_control_source_v1::ZwlrDataControlSourceV1;
#[derive(Clone)]
pub enum Manager {
Zwlr(ZwlrDataControlManagerV1),
Ext(ExtDataControlManagerV1),
}
#[derive(Clone)]
pub enum Device {
Zwlr(ZwlrDataControlDeviceV1),
Ext(ExtDataControlDeviceV1),
}
#[derive(Clone)]
pub enum Source {
Zwlr(ZwlrDataControlSourceV1),
Ext(ExtDataControlSourceV1),
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub enum Offer {
Zwlr(ZwlrDataControlOfferV1),
Ext(ExtDataControlOfferV1),
}
impl Manager {
pub fn get_data_device<D, U>(&self, seat: &WlSeat, qh: &QueueHandle<D>, udata: U) -> Device
where
D: Dispatch<ZwlrDataControlDeviceV1, U> + 'static,
D: Dispatch<ExtDataControlDeviceV1, U> + 'static,
U: Send + Sync + 'static,
{
match self {
Manager::Zwlr(manager) => Device::Zwlr(manager.get_data_device(seat, qh, udata)),
Manager::Ext(manager) => Device::Ext(manager.get_data_device(seat, qh, udata)),
}
}
pub fn create_data_source<D>(&self, qh: &QueueHandle<D>) -> Source
where
D: Dispatch<ZwlrDataControlSourceV1, ()> + 'static,
D: Dispatch<ExtDataControlSourceV1, ()> + 'static,
{
match self {
Manager::Zwlr(manager) => Source::Zwlr(manager.create_data_source(qh, ())),
Manager::Ext(manager) => Source::Ext(manager.create_data_source(qh, ())),
}
}
}
impl Device {
pub fn destroy(&self) {
match self {
Device::Zwlr(device) => device.destroy(),
Device::Ext(device) => device.destroy(),
}
}
#[track_caller]
pub fn set_selection(&self, source: Option<&Source>) {
match self {
Device::Zwlr(device) => device.set_selection(source.map(Source::zwlr)),
Device::Ext(device) => device.set_selection(source.map(Source::ext)),
}
}
#[track_caller]
pub fn set_primary_selection(&self, source: Option<&Source>) {
match self {
Device::Zwlr(device) => device.set_primary_selection(source.map(Source::zwlr)),
Device::Ext(device) => device.set_primary_selection(source.map(Source::ext)),
}
}
}
impl Source {
pub fn destroy(&self) {
match self {
Source::Zwlr(source) => source.destroy(),
Source::Ext(source) => source.destroy(),
}
}
pub fn offer(&self, mime_type: String) {
match self {
Source::Zwlr(source) => source.offer(mime_type),
Source::Ext(source) => source.offer(mime_type),
}
}
pub fn is_alive(&self) -> bool {
match self {
Source::Zwlr(source) => source.is_alive(),
Source::Ext(source) => source.is_alive(),
}
}
#[track_caller]
pub fn zwlr(&self) -> &ZwlrDataControlSourceV1 {
if let Self::Zwlr(v) = self {
v
} else {
panic!("tried to convert non-Zwlr Source to Zwlr")
}
}
#[track_caller]
pub fn ext(&self) -> &ExtDataControlSourceV1 {
if let Self::Ext(v) = self {
v
} else {
panic!("tried to convert non-Ext Source to Ext")
}
}
}
impl Offer {
pub fn destroy(&self) {
match self {
Offer::Zwlr(offer) => offer.destroy(),
Offer::Ext(offer) => offer.destroy(),
}
}
pub fn receive(&self, mime_type: String, fd: BorrowedFd) {
match self {
Offer::Zwlr(offer) => offer.receive(mime_type, fd),
Offer::Ext(offer) => offer.receive(mime_type, fd),
}
}
}
impl From<ZwlrDataControlSourceV1> for Source {
fn from(v: ZwlrDataControlSourceV1) -> Self {
Self::Zwlr(v)
}
}
impl From<ExtDataControlSourceV1> for Source {
fn from(v: ExtDataControlSourceV1) -> Self {
Self::Ext(v)
}
}
impl From<ZwlrDataControlOfferV1> for Offer {
fn from(v: ZwlrDataControlOfferV1) -> Self {
Self::Zwlr(v)
}
}
impl From<ExtDataControlOfferV1> for Offer {
fn from(v: ExtDataControlOfferV1) -> Self {
Self::Ext(v)
}
}
// Some mildly cursed macros to avoid code duplication.
macro_rules! impl_dispatch_manager {
($handler:ty => [$($iface:ty),*]) => {
$(
impl Dispatch<$iface, ()> for $handler {
fn event(
_state: &mut Self,
_proxy: &$iface,
_event: <$iface as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
)*
};
($handler:ty) => {
impl_dispatch_manager!($handler => [
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1
]);
};
}
pub(crate) use impl_dispatch_manager;
macro_rules! impl_dispatch_device {
($handler:ty, $udata:ty, $code:expr => [$(($iface:ty, $opcode:path, $offer:ty)),*]) => {
$(
impl Dispatch<$iface, $udata> for $handler {
fn event(
state: &mut Self,
_proxy: &$iface,
event: <$iface as wayland_client::Proxy>::Event,
data: &$udata,
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
type Event = <$iface as wayland_client::Proxy>::Event;
($code)(state, event, data)
}
event_created_child!($handler, $iface, [
$opcode => ($offer, ()),
]);
}
)*
};
($handler:ty, $udata:ty, $code:expr) => {
impl_dispatch_device!($handler, $udata, $code => [
(
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1,
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE,
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1
),
(
wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::ExtDataControlDeviceV1,
wayland_protocols::ext::data_control::v1::client::ext_data_control_device_v1::EVT_DATA_OFFER_OPCODE,
wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1
)
]);
};
}
pub(crate) use impl_dispatch_device;
macro_rules! impl_dispatch_source {
($handler:ty, $code:expr => [$($iface:ty),*]) => {
$(
impl Dispatch<$iface, ()> for $handler {
fn event(
state: &mut Self,
proxy: &$iface,
event: <$iface as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
type Event = <$iface as wayland_client::Proxy>::Event;
let source = $crate::data_control::Source::from(proxy.clone());
($code)(state, source, event)
}
}
)*
};
($handler:ty, $code:expr) => {
impl_dispatch_source!($handler, $code => [
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_source_v1::ZwlrDataControlSourceV1,
wayland_protocols::ext::data_control::v1::client::ext_data_control_source_v1::ExtDataControlSourceV1
]);
};
}
pub(crate) use impl_dispatch_source;
macro_rules! impl_dispatch_offer {
($handler:ty, $code:expr => [$($iface:ty),*]) => {
$(
impl Dispatch<$iface, ()> for $handler {
fn event(
state: &mut Self,
proxy: &$iface,
event: <$iface as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
type Event = <$iface as wayland_client::Proxy>::Event;
let offer = $crate::data_control::Offer::from(proxy.clone());
($code)(state, offer, event)
}
}
)*
};
($handler:ty, $code:expr) => {
impl_dispatch_offer!($handler, $code => [
wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1,
wayland_protocols::ext::data_control::v1::client::ext_data_control_offer_v1::ExtDataControlOfferV1
]);
};
($handler:ty) => {
impl_dispatch_offer!($handler, |_, _, _: Event| ());
};
}
pub(crate) use impl_dispatch_offer;

View File

@@ -7,10 +7,10 @@
//! primary selection), for example via the
//! [smithay-clipboard](https://crates.io/crates/smithay-clipboard) crate.
//!
//! The protocol used for clipboard interaction is `data-control` from
//! [wlroots](https://github.com/swaywm/wlr-protocols). When using the regular clipboard, the
//! compositor must support the first version of the protocol. When using the "primary" clipboard,
//! the compositor must support the second version of the protocol (or higher).
//! The protocol used for clipboard interaction is `ext-data-control` or `wlr-data-control`. When
//! using the regular clipboard, the compositor must support any version of either protocol. When
//! using the "primary" clipboard, the compositor must support any version of `ext-data-control`,
//! or the second version of the `wlr-data-control` protocol.
//!
//! For example applications using these features, see `wl-clipboard-rs-tools/src/bin/wl_copy.rs`
//! and `wl-clipboard-rs-tools/src/bin/wl_paste.rs` which implement terminal apps similar to
@@ -74,19 +74,19 @@
//!
//! match is_primary_selection_supported() {
//! Ok(supported) => {
//! // We have our definitive result. False means that either data-control version 1
//! // is present (which does not support the primary selection), or that data-control
//! // version 2 is present and it did not signal the primary selection support.
//! // We have our definitive result. False means that ext/wlr-data-control is present
//! // and did not signal the primary selection support, or that only wlr-data-control
//! // version 1 is present (which does not support primary selection).
//! },
//! Err(PrimarySelectionCheckError::NoSeats) => {
//! // Impossible to give a definitive result. Primary selection may or may not be
//! // supported.
//!
//! // The required protocol (data-control version 2) is there, but there are no seats.
//! // Unfortunately, at least one seat is needed to check for the primary clipboard
//! // support.
//! // The required protocol (ext-data-control, or wlr-data-control version 2) is there,
//! // but there are no seats. Unfortunately, at least one seat is needed to check for the
//! // primary clipboard support.
//! },
//! Err(PrimarySelectionCheckError::MissingProtocol { .. }) => {
//! Err(PrimarySelectionCheckError::MissingProtocol) => {
//! // The data-control protocol (required for wl-clipboard-rs operation) is not
//! // supported by the compositor.
//! },
@@ -109,6 +109,7 @@
#![deny(unsafe_code)]
mod common;
mod data_control;
mod seat_data;
#[cfg(test)]

View File

@@ -12,15 +12,9 @@ use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{
delegate_dispatch, event_created_child, ConnectError, Dispatch, DispatchError, EventQueue,
};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::{
self, ZwlrDataControlDeviceV1,
};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::{
self, ZwlrDataControlOfferV1,
};
use crate::common::{self, initialize};
use crate::data_control::{self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer};
use crate::utils::is_text;
/// The clipboard to operate on.
@@ -32,8 +26,8 @@ pub enum ClipboardType {
Regular,
/// The "primary" clipboard.
///
/// Working with the "primary" clipboard requires the compositor to support the data-control
/// protocol of version 2 or above.
/// Working with the "primary" clipboard requires the compositor to support ext-data-control,
/// or wlr-data-control version 2 or above.
Primary,
}
@@ -76,7 +70,7 @@ struct State {
common: common::State,
// The value is the set of MIME types in the offer.
// TODO: We never remove offers from here, even if we don't use them or after destroying them.
offers: HashMap<ZwlrDataControlOfferV1, HashSet<String>>,
offers: HashMap<data_control::Offer, HashSet<String>>,
got_primary_selection: bool,
}
@@ -114,11 +108,10 @@ pub enum Error {
WaylandCommunication(#[source] DispatchError),
#[error(
"A required Wayland protocol ({} version {}) is not supported by the compositor",
name,
version
"A required Wayland protocol (ext-data-control, or wlr-data-control version {version}) \
is not supported by the compositor"
)]
MissingProtocol { name: &'static str, version: u32 },
MissingProtocol { version: u32 },
#[error("The compositor does not support primary selection")]
PrimarySelectionUnsupported,
@@ -138,7 +131,7 @@ impl From<common::Error> for Error {
SocketOpenError(err) => Error::SocketOpenError(err),
WaylandConnection(err) => Error::WaylandConnection(err),
WaylandCommunication(err) => Error::WaylandCommunication(err.into()),
MissingProtocol { name, version } => Error::MissingProtocol { name, version },
MissingProtocol { version } => Error::MissingProtocol { version },
}
}
}
@@ -155,76 +148,47 @@ impl Dispatch<WlRegistry, GlobalListContents> for State {
}
}
impl Dispatch<ZwlrDataControlManagerV1, ()> for State {
fn event(
_state: &mut Self,
_proxy: &ZwlrDataControlManagerV1,
_event: <ZwlrDataControlManagerV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
impl_dispatch_manager!(State);
impl Dispatch<ZwlrDataControlDeviceV1, WlSeat> for State {
fn event(
state: &mut Self,
_device: &ZwlrDataControlDeviceV1,
event: <ZwlrDataControlDeviceV1 as wayland_client::Proxy>::Event,
seat: &WlSeat,
_conn: &wayland_client::Connection,
_qh: &wayland_client::QueueHandle<Self>,
) {
match event {
zwlr_data_control_device_v1::Event::DataOffer { id } => {
state.offers.insert(id, HashSet::new());
}
zwlr_data_control_device_v1::Event::Selection { id } => {
state.common.seats.get_mut(seat).unwrap().set_offer(id);
}
zwlr_data_control_device_v1::Event::Finished => {
// Destroy the device stored in the seat as it's no longer valid.
state.common.seats.get_mut(seat).unwrap().set_device(None);
}
zwlr_data_control_device_v1::Event::PrimarySelection { id } => {
state.got_primary_selection = true;
state
.common
.seats
.get_mut(seat)
.unwrap()
.set_primary_offer(id);
}
_ => (),
impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat| {
match event {
Event::DataOffer { id } => {
let offer = data_control::Offer::from(id);
state.offers.insert(offer, HashSet::new());
}
}
event_created_child!(State, ZwlrDataControlDeviceV1, [
zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, ()),
]);
}
impl Dispatch<ZwlrDataControlOfferV1, ()> for State {
fn event(
state: &mut Self,
offer: &ZwlrDataControlOfferV1,
event: <ZwlrDataControlOfferV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
if let zwlr_data_control_offer_v1::Event::Offer { mime_type } = event {
state.offers.get_mut(offer).unwrap().insert(mime_type);
Event::Selection { id } => {
let offer = id.map(data_control::Offer::from);
let seat = state.common.seats.get_mut(seat).unwrap();
seat.set_offer(offer);
}
Event::Finished => {
// Destroy the device stored in the seat as it's no longer valid.
let seat = state.common.seats.get_mut(seat).unwrap();
seat.set_device(None);
}
Event::PrimarySelection { id } => {
let offer = id.map(data_control::Offer::from);
state.got_primary_selection = true;
let seat = state.common.seats.get_mut(seat).unwrap();
seat.set_primary_offer(offer);
}
_ => (),
}
}
});
impl_dispatch_offer!(State, |state: &mut Self,
offer: data_control::Offer,
event| {
if let Event::Offer { mime_type } = event {
state.offers.get_mut(&offer).unwrap().insert(mime_type);
}
});
fn get_offer(
primary: bool,
seat: Seat<'_>,
socket_name: Option<OsString>,
) -> Result<(EventQueue<State>, State, ZwlrDataControlOfferV1), Error> {
) -> Result<(EventQueue<State>, State, data_control::Offer), Error> {
let (mut queue, mut common) = initialize(primary, socket_name)?;
// Check if there are no seats.

View File

@@ -1,5 +1,4 @@
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1;
use crate::data_control::{Device, Offer};
#[derive(Default)]
pub struct SeatData {
@@ -7,13 +6,13 @@ pub struct SeatData {
pub name: Option<String>,
/// The data device of this seat, if any.
pub device: Option<ZwlrDataControlDeviceV1>,
pub device: Option<Device>,
/// The data offer of this seat, if any.
pub offer: Option<ZwlrDataControlOfferV1>,
pub offer: Option<Offer>,
/// The primary-selection data offer of this seat, if any.
pub primary_offer: Option<ZwlrDataControlOfferV1>,
pub primary_offer: Option<Offer>,
}
impl SeatData {
@@ -25,7 +24,7 @@ impl SeatData {
/// Sets this seat's device.
///
/// Destroys the old one, if any.
pub fn set_device(&mut self, device: Option<ZwlrDataControlDeviceV1>) {
pub fn set_device(&mut self, device: Option<Device>) {
let old_device = self.device.take();
self.device = device;
@@ -37,7 +36,7 @@ impl SeatData {
/// Sets this seat's data offer.
///
/// Destroys the old one, if any.
pub fn set_offer(&mut self, new_offer: Option<ZwlrDataControlOfferV1>) {
pub fn set_offer(&mut self, new_offer: Option<Offer>) {
let old_offer = self.offer.take();
self.offer = new_offer;
@@ -49,7 +48,7 @@ impl SeatData {
/// Sets this seat's primary-selection data offer.
///
/// Destroys the old one, if any.
pub fn set_primary_offer(&mut self, new_offer: Option<ZwlrDataControlOfferV1>) {
pub fn set_primary_offer(&mut self, new_offer: Option<Offer>) {
let old_offer = self.primary_offer.take();
self.primary_offer = new_offer;

View File

@@ -65,13 +65,7 @@ fn get_mime_types_no_data_control() {
let result =
get_mime_types_internal(ClipboardType::Regular, Seat::Unspecified, Some(socket_name));
assert!(matches!(
result,
Err(Error::MissingProtocol {
name: "zwlr_data_control_manager_v1",
version: 1
})
));
assert!(matches!(result, Err(Error::MissingProtocol { version: 1 })));
}
#[test]
@@ -94,13 +88,7 @@ fn get_mime_types_no_data_control_2() {
let result =
get_mime_types_internal(ClipboardType::Primary, Seat::Unspecified, Some(socket_name));
assert!(matches!(
result,
Err(Error::MissingProtocol {
name: "zwlr_data_control_manager_v1",
version: 2
})
));
assert!(matches!(result, Err(Error::MissingProtocol { version: 2 })));
}
#[test]

View File

@@ -1,3 +1,7 @@
use wayland_protocols::ext::data_control::v1::server::ext_data_control_device_v1::ExtDataControlDeviceV1;
use wayland_protocols::ext::data_control::v1::server::ext_data_control_manager_v1::{
self, ExtDataControlManagerV1,
};
use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_device_v1::ZwlrDataControlDeviceV1;
use wayland_protocols_wlr::data_control::v1::server::zwlr_data_control_manager_v1::{
self, ZwlrDataControlManagerV1,
@@ -13,8 +17,8 @@ struct State {
advertise_primary_selection: bool,
}
server_ignore_global_impl!(State => [WlSeat, ZwlrDataControlManagerV1]);
server_ignore_impl!(State => [WlSeat, ZwlrDataControlDeviceV1]);
server_ignore_global_impl!(State => [WlSeat, ZwlrDataControlManagerV1, ExtDataControlManagerV1]);
server_ignore_impl!(State => [WlSeat, ZwlrDataControlDeviceV1, ExtDataControlDeviceV1]);
impl Dispatch<ZwlrDataControlManagerV1, ()> for State {
fn request(
@@ -36,6 +40,26 @@ impl Dispatch<ZwlrDataControlManagerV1, ()> for State {
}
}
impl Dispatch<ExtDataControlManagerV1, ()> for State {
fn request(
state: &mut Self,
_client: &wayland_server::Client,
_resource: &ExtDataControlManagerV1,
request: <ExtDataControlManagerV1 as wayland_server::Resource>::Request,
_data: &(),
_dhandle: &wayland_server::DisplayHandle,
data_init: &mut wayland_server::DataInit<'_, Self>,
) {
if let ext_data_control_manager_v1::Request::GetDataDevice { id, .. } = request {
let data_device = data_init.init(id, ());
if state.advertise_primary_selection {
data_device.primary_selection(None);
}
}
}
}
#[test]
fn is_primary_selection_supported_test() {
let server = TestServer::new();
@@ -165,9 +189,79 @@ fn is_primary_selection_supported_no_data_control() {
let result = is_primary_selection_supported_internal(Some(socket_name));
assert!(matches!(
result,
Err(PrimarySelectionCheckError::MissingProtocol {
name: "zwlr_data_control_manager_v1",
version: 1
})
Err(PrimarySelectionCheckError::MissingProtocol)
));
}
#[test]
fn is_primary_selection_supported_ext_data_control() {
let server = TestServer::new();
server
.display
.handle()
.create_global::<State, WlSeat, ()>(6, ());
server
.display
.handle()
.create_global::<State, ExtDataControlManagerV1, ()>(1, ());
let state = State {
advertise_primary_selection: true,
};
let socket_name = server.socket_name().to_owned();
server.run(state);
let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap();
assert!(result);
}
#[test]
fn is_primary_selection_supported_primary_selection_unsupported_ext_data_control() {
let server = TestServer::new();
server
.display
.handle()
.create_global::<State, WlSeat, ()>(6, ());
server
.display
.handle()
.create_global::<State, ExtDataControlManagerV1, ()>(1, ());
let state = State {
advertise_primary_selection: false,
};
let socket_name = server.socket_name().to_owned();
server.run(state);
let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap();
assert!(!result);
}
#[test]
fn is_primary_selection_supported_data_control_v1_and_ext_data_control() {
let server = TestServer::new();
server
.display
.handle()
.create_global::<State, WlSeat, ()>(6, ());
server
.display
.handle()
.create_global::<State, ZwlrDataControlManagerV1, ()>(1, ());
server
.display
.handle()
.create_global::<State, ExtDataControlManagerV1, ()>(1, ());
let state = State {
advertise_primary_selection: true,
};
let socket_name = server.socket_name().to_owned();
server.run(state);
let result = is_primary_selection_supported_internal(Some(socket_name)).unwrap();
assert!(result);
}

View File

@@ -10,11 +10,12 @@ use wayland_client::protocol::wl_seat::WlSeat;
use wayland_client::{
event_created_child, ConnectError, Connection, Dispatch, DispatchError, Proxy,
};
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_device_v1::{
self, ZwlrDataControlDeviceV1,
};
use wayland_protocols::ext::data_control::v1::client::ext_data_control_manager_v1::ExtDataControlManagerV1;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_manager_v1::ZwlrDataControlManagerV1;
use wayland_protocols_wlr::data_control::v1::client::zwlr_data_control_offer_v1::ZwlrDataControlOfferV1;
use crate::data_control::{
impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, Manager,
};
/// Checks if the given MIME type represents plain text.
///
@@ -37,8 +38,8 @@ pub fn is_text(mime_type: &str) -> bool {
struct PrimarySelectionState {
// Any seat that we get from the compositor.
seat: Option<WlSeat>,
clipboard_manager: Option<ZwlrDataControlManagerV1>,
clipboard_manager_was_v1: bool,
clipboard_manager: Option<Manager>,
saw_zwlr_v1: bool,
got_primary_selection: bool,
}
@@ -62,14 +63,19 @@ impl Dispatch<WlRegistry, ()> for PrimarySelectionState {
state.seat = Some(seat);
}
if interface == ZwlrDataControlManagerV1::interface().name {
assert_eq!(state.clipboard_manager, None);
if state.clipboard_manager.is_none() {
if interface == ZwlrDataControlManagerV1::interface().name {
if version == 1 {
state.saw_zwlr_v1 = true;
} else {
let manager = registry.bind(name, 2, qh, ());
state.clipboard_manager = Some(Manager::Zwlr(manager));
}
}
if version == 1 {
state.clipboard_manager_was_v1 = true;
} else {
let manager = registry.bind(name, 2, qh, ());
state.clipboard_manager = Some(manager);
if interface == ExtDataControlManagerV1::interface().name {
let manager = registry.bind(name, 1, qh, ());
state.clipboard_manager = Some(Manager::Ext(manager));
}
}
}
@@ -88,48 +94,15 @@ impl Dispatch<WlSeat, ()> for PrimarySelectionState {
}
}
impl Dispatch<ZwlrDataControlManagerV1, ()> for PrimarySelectionState {
fn event(
_state: &mut Self,
_proxy: &ZwlrDataControlManagerV1,
_event: <ZwlrDataControlManagerV1 as Proxy>::Event,
_data: &(),
_conn: &Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
impl_dispatch_manager!(PrimarySelectionState);
impl Dispatch<ZwlrDataControlDeviceV1, ()> for PrimarySelectionState {
fn event(
state: &mut Self,
_device: &ZwlrDataControlDeviceV1,
event: <ZwlrDataControlDeviceV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qh: &wayland_client::QueueHandle<Self>,
) {
if let zwlr_data_control_device_v1::Event::PrimarySelection { id: _ } = event {
state.got_primary_selection = true;
}
impl_dispatch_device!(PrimarySelectionState, (), |state: &mut Self, event, _| {
if let Event::PrimarySelection { id: _ } = event {
state.got_primary_selection = true;
}
});
event_created_child!(PrimarySelectionState, ZwlrDataControlDeviceV1, [
zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, ()),
]);
}
impl Dispatch<ZwlrDataControlOfferV1, ()> for PrimarySelectionState {
fn event(
_state: &mut Self,
_offer: &ZwlrDataControlOfferV1,
_event: <ZwlrDataControlOfferV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
impl_dispatch_offer!(PrimarySelectionState);
/// Errors that can occur when checking whether the primary selection is supported.
#[derive(thiserror::Error, Debug)]
@@ -147,11 +120,10 @@ pub enum PrimarySelectionCheckError {
WaylandCommunication(#[source] DispatchError),
#[error(
"A required Wayland protocol ({} version {}) is not supported by the compositor",
name,
version
"A required Wayland protocol (ext-data-control, or wlr-data-control version 1) \
is not supported by the compositor"
)]
MissingProtocol { name: &'static str, version: u32 },
MissingProtocol,
}
/// Checks if the compositor supports the primary selection.
@@ -165,19 +137,19 @@ pub enum PrimarySelectionCheckError {
///
/// match is_primary_selection_supported() {
/// Ok(supported) => {
/// // We have our definitive result. False means that either data-control version 1
/// // is present (which does not support the primary selection), or that data-control
/// // version 2 is present and it did not signal the primary selection support.
/// // We have our definitive result. False means that ext/wlr-data-control is present
/// // and did not signal the primary selection support, or that only wlr-data-control
/// // version 1 is present (which does not support primary selection).
/// },
/// Err(PrimarySelectionCheckError::NoSeats) => {
/// // Impossible to give a definitive result. Primary selection may or may not be
/// // supported.
///
/// // The required protocol (data-control version 2) is there, but there are no seats.
/// // Unfortunately, at least one seat is needed to check for the primary clipboard
/// // support.
/// // The required protocol (ext-data-control, or wlr-data-control version 2) is there,
/// // but there are no seats. Unfortunately, at least one seat is needed to check for the
/// // primary clipboard support.
/// },
/// Err(PrimarySelectionCheckError::MissingProtocol { .. }) => {
/// Err(PrimarySelectionCheckError::MissingProtocol) => {
/// // The data-control protocol (required for wl-clipboard-rs operation) is not
/// // supported by the compositor.
/// },
@@ -225,7 +197,7 @@ pub(crate) fn is_primary_selection_supported_internal(
let mut state = PrimarySelectionState {
seat: None,
clipboard_manager: None,
clipboard_manager_was_v1: false,
saw_zwlr_v1: false,
got_primary_selection: false,
};
@@ -235,17 +207,15 @@ pub(crate) fn is_primary_selection_supported_internal(
.roundtrip(&mut state)
.map_err(PrimarySelectionCheckError::WaylandCommunication)?;
// If data control is present but is version 1, then return false as version 1 does not support primary clipboard.
if state.clipboard_manager_was_v1 {
// If data control is present but is version 1, then return false as version 1 does not support
// primary clipboard.
if state.clipboard_manager.is_none() && state.saw_zwlr_v1 {
return Ok(false);
}
// Verify that we got the clipboard manager.
let Some(ref clipboard_manager) = state.clipboard_manager else {
return Err(PrimarySelectionCheckError::MissingProtocol {
name: ZwlrDataControlManagerV1::interface().name,
version: 1,
});
return Err(PrimarySelectionCheckError::MissingProtocol);
};
// Check if there are no seats.