use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::io;
use std::os::fd::AsFd;
use os_pipe::{pipe, PipeReader};
use wayland_client::globals::GlobalListContents;
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,
};
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::utils::is_text;
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
pub enum ClipboardType {
#[default]
Regular,
Primary,
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)]
pub enum MimeType<'a> {
Any,
Text,
TextWithPriority(&'a str),
Specific(&'a str),
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
pub enum Seat<'a> {
#[default]
Unspecified,
Specific(&'a str),
}
struct State {
common: common::State,
offers: HashMap<ZwlrDataControlOfferV1, HashSet<String>>,
got_primary_selection: bool,
}
delegate_dispatch!(State: [WlSeat: ()] => common::State);
impl AsMut<common::State> for State {
fn as_mut(&mut self) -> &mut common::State {
&mut self.common
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("There are no seats")]
NoSeats,
#[error("The clipboard of the requested seat is empty")]
ClipboardEmpty,
#[error("No suitable type of content copied")]
NoMimeType,
#[error("Couldn't open the provided Wayland socket")]
SocketOpenError(#[source] io::Error),
#[error("Couldn't connect to the Wayland compositor")]
WaylandConnection(#[source] ConnectError),
#[error("Wayland compositor communication error")]
WaylandCommunication(#[source] DispatchError),
#[error(
"A required Wayland protocol ({} version {}) is not supported by the compositor",
name,
version
)]
MissingProtocol { name: &'static str, version: u32 },
#[error("The compositor does not support primary selection")]
PrimarySelectionUnsupported,
#[error("The requested seat was not found")]
SeatNotFound,
#[error("Couldn't create a pipe for content transfer")]
PipeCreation(#[source] io::Error),
}
impl From<common::Error> for Error {
fn from(x: common::Error) -> Self {
use common::Error::*;
match x {
SocketOpenError(err) => Error::SocketOpenError(err),
WaylandConnection(err) => Error::WaylandConnection(err),
WaylandCommunication(err) => Error::WaylandCommunication(err.into()),
MissingProtocol { name, version } => Error::MissingProtocol { name, version },
}
}
}
impl Dispatch<WlRegistry, GlobalListContents> for State {
fn event(
_state: &mut Self,
_proxy: &WlRegistry,
_event: <WlRegistry as wayland_client::Proxy>::Event,
_data: &GlobalListContents,
_conn: &wayland_client::Connection,
_qhandle: &wayland_client::QueueHandle<Self>,
) {
}
}
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<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 => {
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);
}
_ => (),
}
}
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);
}
}
}
fn get_offer(
primary: bool,
seat: Seat<'_>,
socket_name: Option<OsString>,
) -> Result<(EventQueue<State>, State, ZwlrDataControlOfferV1), Error> {
let (mut queue, mut common) = initialize(primary, socket_name)?;
if common.seats.is_empty() {
return Err(Error::NoSeats);
}
for (seat, data) in &mut common.seats {
let device = common
.clipboard_manager
.get_data_device(seat, &queue.handle(), seat.clone());
data.set_device(Some(device));
}
let mut state = State {
common,
offers: HashMap::new(),
got_primary_selection: false,
};
queue
.roundtrip(&mut state)
.map_err(Error::WaylandCommunication)?;
if primary && !state.got_primary_selection {
return Err(Error::PrimarySelectionUnsupported);
}
let data = match seat {
Seat::Unspecified => state.common.seats.values().next(),
Seat::Specific(name) => state
.common
.seats
.values()
.find(|data| data.name.as_deref() == Some(name)),
};
let Some(data) = data else {
return Err(Error::SeatNotFound);
};
let offer = if primary {
&data.primary_offer
} else {
&data.offer
};
match offer.clone() {
Some(offer) => Ok((queue, state, offer)),
None => Err(Error::ClipboardEmpty),
}
}
#[inline]
pub fn get_mime_types(clipboard: ClipboardType, seat: Seat<'_>) -> Result<HashSet<String>, Error> {
get_mime_types_internal(clipboard, seat, None)
}
pub(crate) fn get_mime_types_internal(
clipboard: ClipboardType,
seat: Seat<'_>,
socket_name: Option<OsString>,
) -> Result<HashSet<String>, Error> {
let primary = clipboard == ClipboardType::Primary;
let (_, mut state, offer) = get_offer(primary, seat, socket_name)?;
Ok(state.offers.remove(&offer).unwrap())
}
#[inline]
pub fn get_contents(
clipboard: ClipboardType,
seat: Seat<'_>,
mime_type: MimeType<'_>,
) -> Result<(PipeReader, String), Error> {
get_contents_internal(clipboard, seat, mime_type, None)
}
pub(crate) fn get_contents_internal(
clipboard: ClipboardType,
seat: Seat<'_>,
mime_type: MimeType<'_>,
socket_name: Option<OsString>,
) -> Result<(PipeReader, String), Error> {
let primary = clipboard == ClipboardType::Primary;
let (mut queue, mut state, offer) = get_offer(primary, seat, socket_name)?;
let mut mime_types = state.offers.remove(&offer).unwrap();
let mime_type = match mime_type {
MimeType::Any => mime_types
.take("text/plain;charset=utf-8")
.or_else(|| mime_types.take("UTF8_STRING"))
.or_else(|| mime_types.iter().find(|x| is_text(x)).cloned())
.or_else(|| mime_types.drain().next()),
MimeType::Text => mime_types
.take("text/plain;charset=utf-8")
.or_else(|| mime_types.take("UTF8_STRING"))
.or_else(|| mime_types.drain().find(|x| is_text(x))),
MimeType::TextWithPriority(priority) => mime_types
.take(priority)
.or_else(|| mime_types.take("text/plain;charset=utf-8"))
.or_else(|| mime_types.take("UTF8_STRING"))
.or_else(|| mime_types.drain().find(|x| is_text(x))),
MimeType::Specific(mime_type) => mime_types.take(mime_type),
};
if mime_type.is_none() {
return Err(Error::NoMimeType);
}
let mime_type = mime_type.unwrap();
let (read, write) = pipe().map_err(Error::PipeCreation)?;
offer.receive(mime_type.clone(), write.as_fd());
drop(write);
queue
.roundtrip(&mut state)
.map_err(Error::WaylandCommunication)?;
Ok((read, mime_type))
}