wl_clipboard_rs/
paste.rs

1//! Getting the offered MIME types and the clipboard contents.
2
3use 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/// The clipboard to operate on.
21#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
22#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
23pub enum ClipboardType {
24    /// The regular clipboard.
25    #[default]
26    Regular,
27    /// The "primary" clipboard.
28    ///
29    /// Working with the "primary" clipboard requires the compositor to support ext-data-control,
30    /// or wlr-data-control version 2 or above.
31    Primary,
32}
33
34/// MIME types that can be requested from the clipboard.
35#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)]
36pub enum MimeType<'a> {
37    /// Request any available MIME type.
38    ///
39    /// If multiple MIME types are offered, the requested MIME type is unspecified and depends on
40    /// the order they are received from the Wayland compositor. However, plain text formats are
41    /// prioritized, so if a plain text format is available among others then it will be requested.
42    Any,
43    /// Request a plain text MIME type.
44    ///
45    /// This will request one of the multiple common plain text MIME types. It will prioritize MIME
46    /// types known to return UTF-8 text.
47    Text,
48    /// Request the given MIME type, and if it's not available fall back to `MimeType::Text`.
49    ///
50    /// Example use-case: pasting `text/html` should try `text/html` first, but if it's not
51    /// available, any other plain text format will do fine too.
52    TextWithPriority(&'a str),
53    /// Request a specific MIME type.
54    Specific(&'a str),
55}
56
57/// Seat to operate on.
58#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
59pub enum Seat<'a> {
60    /// Operate on one of the existing seats depending on the order returned by the compositor.
61    ///
62    /// This is perfectly fine when only a single seat is present, so for most configurations.
63    #[default]
64    Unspecified,
65    /// Operate on a seat with the given name.
66    Specific(&'a str),
67}
68
69struct State {
70    common: common::State,
71    // The value is the set of MIME types in the offer.
72    // TODO: We never remove offers from here, even if we don't use them or after destroying them.
73    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/// Errors that can occur for pasting and listing MIME types.
86///
87/// You may want to ignore some of these errors (rather than show an error message), like
88/// `NoSeats`, `ClipboardEmpty` or `NoMimeType` as they are essentially equivalent to an empty
89/// clipboard.
90#[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            // Destroy the device stored in the seat as it's no longer valid.
167            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    // Check if there are no seats.
196    if common.seats.is_empty() {
197        return Err(Error::NoSeats);
198    }
199
200    // Go through the seats and get their data devices.
201    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    // Retrieve all seat names and offers.
215    queue
216        .roundtrip(&mut state)
217        .map_err(Error::WaylandCommunication)?;
218
219    // Check if the compositor supports primary selection.
220    if primary && !state.got_primary_selection {
221        return Err(Error::PrimarySelectionUnsupported);
222    }
223
224    // Figure out which offer we're interested in.
225    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    // Check if we found anything.
245    match offer.clone() {
246        Some(offer) => Ok((queue, state, offer)),
247        None => Err(Error::ClipboardEmpty),
248    }
249}
250
251/// Retrieves the offered MIME types.
252///
253/// Also see [`get_mime_types_ordered()`], an order-preserving version.
254///
255/// If `seat` is `None`, uses an unspecified seat (it depends on the order returned by the
256/// compositor). This is perfectly fine when only a single seat is present, so for most
257/// configurations.
258///
259/// # Examples
260///
261/// ```no_run
262/// # extern crate wl_clipboard_rs;
263/// # use wl_clipboard_rs::paste::Error;
264/// # fn foo() -> Result<(), Error> {
265/// use wl_clipboard_rs::{paste::{get_mime_types, ClipboardType, Seat}};
266///
267/// let mime_types = get_mime_types(ClipboardType::Regular, Seat::Unspecified)?;
268/// for mime_type in mime_types {
269///     println!("{}", mime_type);
270/// }
271/// # Ok(())
272/// # }
273/// ```
274#[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/// Retrieves the offered MIME types, preserving their original order.
282///
283/// Applications are generally expected to offer not just the "native" data type, but some
284/// conversions generated on the fly. For example, when copying a PNG image from a browser, it will
285/// offer `image/png` as well as `image/jpeg`, `image/webp`, and others, to maximize compatibility.
286/// When these converted MIME types are pasted, the application will generate the data on the fly
287/// (by converting the image to the requested MIME type).
288///
289/// There's no defined way to know which of the offered MIME types is native (if any). However,
290/// some applications will offer the native data types first, followed by converted ones. While
291/// [`get_mime_types()`] loses this order (a `HashSet` is unordered), this function returns the
292/// MIME types in their original order.
293///
294/// If `seat` is `None`, uses an unspecified seat (it depends on the order returned by the
295/// compositor). This is perfectly fine when only a single seat is present, so for most
296/// configurations.
297///
298/// # Examples
299///
300/// ```no_run
301/// # extern crate wl_clipboard_rs;
302/// # use wl_clipboard_rs::paste::Error;
303/// # fn foo() -> Result<(), Error> {
304/// use wl_clipboard_rs::{paste::{get_mime_types_ordered, ClipboardType, Seat}};
305///
306/// let mime_types = get_mime_types_ordered(ClipboardType::Regular, Seat::Unspecified)?;
307/// for mime_type in mime_types {
308///     println!("{}", mime_type);
309/// }
310/// # Ok(())
311/// # }
312/// ```
313#[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
321// The internal function accepts the socket name, used for tests.
322pub(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/// Retrieves the clipboard contents.
333///
334/// This function returns a tuple of the reading end of a pipe containing the clipboard contents
335/// and the actual MIME type of the contents.
336///
337/// If `seat` is `None`, uses an unspecified seat (it depends on the order returned by the
338/// compositor). This is perfectly fine when only a single seat is present, so for most
339/// configurations.
340///
341/// # Examples
342///
343/// ```no_run
344/// # extern crate wl_clipboard_rs;
345/// # fn foo() -> Result<(), Box<dyn std::error::Error>> {
346/// use std::io::Read;
347/// use wl_clipboard_rs::{paste::{get_contents, ClipboardType, Error, MimeType, Seat}};
348///
349/// let result = get_contents(ClipboardType::Regular, Seat::Unspecified, MimeType::Any);
350/// match result {
351///     Ok((mut pipe, mime_type)) => {
352///         println!("Got data of the {} MIME type", &mime_type);
353///
354///         let mut contents = vec![];
355///         pipe.read_to_end(&mut contents)?;
356///         println!("Read {} bytes of data", contents.len());
357///     }
358///
359///     Err(Error::NoSeats) | Err(Error::ClipboardEmpty) | Err(Error::NoMimeType) => {
360///         // The clipboard is empty, nothing to worry about.
361///     }
362///
363///     Err(err) => Err(err)?
364/// }
365/// # Ok(())
366/// # }
367/// ```
368#[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
377// The internal function accepts the socket name, used for tests.
378pub(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                        // We only remove once, so the swap doesn't affect anything.
395                        break 'block Some(mime_types.swap_remove(i));
396                    }
397                }
398                None
399            }
400        };
401    }
402
403    // Find the desired MIME type.
404    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    // Check if a suitable MIME type is copied.
420    let Some(mime_type) = mime_type else {
421        return Err(Error::NoMimeType);
422    };
423
424    // Create a pipe for content transfer.
425    let (read, write) = pipe().map_err(Error::PipeCreation)?;
426
427    // Start the transfer.
428    offer.receive(mime_type.clone(), write.as_fd());
429    drop(write);
430
431    // A flush() is not enough here, it will result in sometimes pasting empty contents. I suspect this is due to a
432    // race between the compositor reacting to the receive request, and the compositor reacting to wl-paste
433    // disconnecting after queue is dropped. The roundtrip solves that race.
434    queue
435        .roundtrip(&mut state)
436        .map_err(Error::WaylandCommunication)?;
437
438    Ok((read, mime_type))
439}