Skip to main content

wl_clipboard_rs/
paste.rs

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