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