1use std::collections::{HashMap, HashSet};
4use std::ffi::OsString;
5use std::io;
6use std::os::fd::AsFd;
7
8use os_pipe::{pipe, PipeReader};
9use wayland_client::globals::GlobalListContents;
10use wayland_client::protocol::wl_registry::WlRegistry;
11use wayland_client::protocol::wl_seat::WlSeat;
12use wayland_client::{
13 delegate_dispatch, event_created_child, ConnectError, Dispatch, DispatchError, EventQueue,
14};
15
16use crate::common::{self, initialize};
17use crate::data_control::{self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer};
18use crate::utils::is_text;
19
20#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
22#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
23pub enum ClipboardType {
24 #[default]
26 Regular,
27 Primary,
32}
33
34#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)]
36pub enum MimeType<'a> {
37 Any,
43 Text,
48 TextWithPriority(&'a str),
53 Specific(&'a str),
55}
56
57#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
59pub enum Seat<'a> {
60 #[default]
64 Unspecified,
65 Specific(&'a str),
67}
68
69struct State {
70 common: common::State,
71 offers: HashMap<data_control::Offer, Vec<String>>,
74 got_primary_selection: bool,
75}
76
77delegate_dispatch!(State: [WlSeat: ()] => common::State);
78
79impl AsMut<common::State> for State {
80 fn as_mut(&mut self) -> &mut common::State {
81 &mut self.common
82 }
83}
84
85#[derive(thiserror::Error, Debug)]
91pub enum Error {
92 #[error("There are no seats")]
93 NoSeats,
94
95 #[error("The clipboard of the requested seat is empty")]
96 ClipboardEmpty,
97
98 #[error("No suitable type of content copied")]
99 NoMimeType,
100
101 #[error("Couldn't open the provided Wayland socket")]
102 SocketOpenError(#[source] io::Error),
103
104 #[error("Couldn't connect to the Wayland compositor")]
105 WaylandConnection(#[source] ConnectError),
106
107 #[error("Wayland compositor communication error")]
108 WaylandCommunication(#[source] DispatchError),
109
110 #[error(
111 "A required Wayland protocol ({} version {}) is not supported by the compositor",
112 name,
113 version
114 )]
115 MissingProtocol { name: &'static str, version: u32 },
116
117 #[error("The compositor does not support primary selection")]
118 PrimarySelectionUnsupported,
119
120 #[error("The requested seat was not found")]
121 SeatNotFound,
122
123 #[error("Couldn't create a pipe for content transfer")]
124 PipeCreation(#[source] io::Error),
125}
126
127impl From<common::Error> for Error {
128 fn from(x: common::Error) -> Self {
129 use common::Error::*;
130
131 match x {
132 SocketOpenError(err) => Error::SocketOpenError(err),
133 WaylandConnection(err) => Error::WaylandConnection(err),
134 WaylandCommunication(err) => Error::WaylandCommunication(err.into()),
135 MissingProtocol { name, version } => Error::MissingProtocol { name, version },
136 }
137 }
138}
139
140impl Dispatch<WlRegistry, GlobalListContents> for State {
141 fn event(
142 _state: &mut Self,
143 _proxy: &WlRegistry,
144 _event: <WlRegistry as wayland_client::Proxy>::Event,
145 _data: &GlobalListContents,
146 _conn: &wayland_client::Connection,
147 _qhandle: &wayland_client::QueueHandle<Self>,
148 ) {
149 }
150}
151
152impl_dispatch_manager!(State);
153
154impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat| {
155 match event {
156 Event::DataOffer { id } => {
157 let offer = data_control::Offer::from(id);
158 state.offers.insert(offer, Vec::new());
159 }
160 Event::Selection { id } => {
161 let offer = id.map(data_control::Offer::from);
162 let seat = state.common.seats.get_mut(seat).unwrap();
163 seat.set_offer(offer);
164 }
165 Event::Finished => {
166 let seat = state.common.seats.get_mut(seat).unwrap();
168 seat.set_device(None);
169 }
170 Event::PrimarySelection { id } => {
171 let offer = id.map(data_control::Offer::from);
172 state.got_primary_selection = true;
173 let seat = state.common.seats.get_mut(seat).unwrap();
174 seat.set_primary_offer(offer);
175 }
176 _ => (),
177 }
178});
179
180impl_dispatch_offer!(State, |state: &mut Self,
181 offer: data_control::Offer,
182 event| {
183 if let Event::Offer { mime_type } = event {
184 state.offers.get_mut(&offer).unwrap().push(mime_type);
185 }
186});
187
188fn get_offer(
189 primary: bool,
190 seat: Seat<'_>,
191 socket_name: Option<OsString>,
192) -> Result<(EventQueue<State>, State, data_control::Offer), Error> {
193 let (mut queue, mut common) = initialize(primary, socket_name)?;
194
195 if common.seats.is_empty() {
197 return Err(Error::NoSeats);
198 }
199
200 for (seat, data) in &mut common.seats {
202 let device = common
203 .clipboard_manager
204 .get_data_device(seat, &queue.handle(), seat.clone());
205 data.set_device(Some(device));
206 }
207
208 let mut state = State {
209 common,
210 offers: HashMap::new(),
211 got_primary_selection: false,
212 };
213
214 queue
216 .roundtrip(&mut state)
217 .map_err(Error::WaylandCommunication)?;
218
219 if primary && !state.got_primary_selection {
221 return Err(Error::PrimarySelectionUnsupported);
222 }
223
224 let data = match seat {
226 Seat::Unspecified => state.common.seats.values().next(),
227 Seat::Specific(name) => state
228 .common
229 .seats
230 .values()
231 .find(|data| data.name.as_deref() == Some(name)),
232 };
233
234 let Some(data) = data else {
235 return Err(Error::SeatNotFound);
236 };
237
238 let offer = if primary {
239 &data.primary_offer
240 } else {
241 &data.offer
242 };
243
244 match offer.clone() {
246 Some(offer) => Ok((queue, state, offer)),
247 None => Err(Error::ClipboardEmpty),
248 }
249}
250
251#[inline]
275pub fn get_mime_types(clipboard: ClipboardType, seat: Seat<'_>) -> Result<HashSet<String>, Error> {
276 Ok(get_mime_types_internal(clipboard, seat, None)?
277 .into_iter()
278 .collect())
279}
280
281#[inline]
314pub fn get_mime_types_ordered(
315 clipboard: ClipboardType,
316 seat: Seat<'_>,
317) -> Result<Vec<String>, Error> {
318 get_mime_types_internal(clipboard, seat, None)
319}
320
321pub(crate) fn get_mime_types_internal(
323 clipboard: ClipboardType,
324 seat: Seat<'_>,
325 socket_name: Option<OsString>,
326) -> Result<Vec<String>, Error> {
327 let primary = clipboard == ClipboardType::Primary;
328 let (_, mut state, offer) = get_offer(primary, seat, socket_name)?;
329 Ok(state.offers.remove(&offer).unwrap())
330}
331
332#[inline]
369pub fn get_contents(
370 clipboard: ClipboardType,
371 seat: Seat<'_>,
372 mime_type: MimeType<'_>,
373) -> Result<(PipeReader, String), Error> {
374 get_contents_internal(clipboard, seat, mime_type, None)
375}
376
377pub(crate) fn get_contents_internal(
379 clipboard: ClipboardType,
380 seat: Seat<'_>,
381 mime_type: MimeType<'_>,
382 socket_name: Option<OsString>,
383) -> Result<(PipeReader, String), Error> {
384 let primary = clipboard == ClipboardType::Primary;
385 let (mut queue, mut state, offer) = get_offer(primary, seat, socket_name)?;
386
387 let mut mime_types = state.offers.remove(&offer).unwrap();
388
389 macro_rules! take {
390 ($pred:expr) => {
391 'block: {
392 for i in 0..mime_types.len() {
393 if $pred(&mime_types[i]) {
394 break 'block Some(mime_types.swap_remove(i));
396 }
397 }
398 None
399 }
400 };
401 }
402
403 let mime_type = match mime_type {
405 MimeType::Any => take!(|x| x == "text/plain;charset=utf-8")
406 .or_else(|| take!(|x| x == "UTF8_STRING"))
407 .or_else(|| take!(is_text))
408 .or_else(|| take!(|_| true)),
409 MimeType::Text => take!(|x| x == "text/plain;charset=utf-8")
410 .or_else(|| take!(|x| x == "UTF8_STRING"))
411 .or_else(|| take!(is_text)),
412 MimeType::TextWithPriority(priority) => take!(|x| x == priority)
413 .or_else(|| take!(|x| x == "text/plain;charset=utf-8"))
414 .or_else(|| take!(|x| x == "UTF8_STRING"))
415 .or_else(|| take!(is_text)),
416 MimeType::Specific(mime_type) => take!(|x| x == mime_type),
417 };
418
419 let Some(mime_type) = mime_type else {
421 return Err(Error::NoMimeType);
422 };
423
424 let (read, write) = pipe().map_err(Error::PipeCreation)?;
426
427 offer.receive(mime_type.clone(), write.as_fd());
429 drop(write);
430
431 queue
435 .roundtrip(&mut state)
436 .map_err(Error::WaylandCommunication)?;
437
438 Ok((read, mime_type))
439}