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, HashSet<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, HashSet::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().insert(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]
273pub fn get_mime_types(clipboard: ClipboardType, seat: Seat<'_>) -> Result<HashSet<String>, Error> {
274 get_mime_types_internal(clipboard, seat, None)
275}
276
277pub(crate) fn get_mime_types_internal(
279 clipboard: ClipboardType,
280 seat: Seat<'_>,
281 socket_name: Option<OsString>,
282) -> Result<HashSet<String>, Error> {
283 let primary = clipboard == ClipboardType::Primary;
284 let (_, mut state, offer) = get_offer(primary, seat, socket_name)?;
285 Ok(state.offers.remove(&offer).unwrap())
286}
287
288#[inline]
325pub fn get_contents(
326 clipboard: ClipboardType,
327 seat: Seat<'_>,
328 mime_type: MimeType<'_>,
329) -> Result<(PipeReader, String), Error> {
330 get_contents_internal(clipboard, seat, mime_type, None)
331}
332
333pub(crate) fn get_contents_internal(
335 clipboard: ClipboardType,
336 seat: Seat<'_>,
337 mime_type: MimeType<'_>,
338 socket_name: Option<OsString>,
339) -> Result<(PipeReader, String), Error> {
340 let primary = clipboard == ClipboardType::Primary;
341 let (mut queue, mut state, offer) = get_offer(primary, seat, socket_name)?;
342
343 let mut mime_types = state.offers.remove(&offer).unwrap();
344
345 let mime_type = match mime_type {
347 MimeType::Any => mime_types
348 .take("text/plain;charset=utf-8")
349 .or_else(|| mime_types.take("UTF8_STRING"))
350 .or_else(|| mime_types.iter().find(|x| is_text(x)).cloned())
351 .or_else(|| mime_types.drain().next()),
352 MimeType::Text => mime_types
353 .take("text/plain;charset=utf-8")
354 .or_else(|| mime_types.take("UTF8_STRING"))
355 .or_else(|| mime_types.drain().find(|x| is_text(x))),
356 MimeType::TextWithPriority(priority) => mime_types
357 .take(priority)
358 .or_else(|| mime_types.take("text/plain;charset=utf-8"))
359 .or_else(|| mime_types.take("UTF8_STRING"))
360 .or_else(|| mime_types.drain().find(|x| is_text(x))),
361 MimeType::Specific(mime_type) => mime_types.take(mime_type),
362 };
363
364 if mime_type.is_none() {
366 return Err(Error::NoMimeType);
367 }
368
369 let mime_type = mime_type.unwrap();
370
371 let (read, write) = pipe().map_err(Error::PipeCreation)?;
373
374 offer.receive(mime_type.clone(), write.as_fd());
376 drop(write);
377
378 queue
382 .roundtrip(&mut state)
383 .map_err(Error::WaylandCommunication)?;
384
385 Ok((read, mime_type))
386}