wl_clipboard_rs/
copy.rs

1//! Copying and clearing clipboard contents.
2
3use std::collections::hash_map::Entry;
4use std::collections::HashMap;
5use std::ffi::OsString;
6use std::fs::File;
7use std::io::{self, Cursor};
8use std::sync::mpsc::sync_channel;
9use std::sync::Arc;
10use std::{iter, thread};
11
12use rustix::fs::{fcntl_setfl, OFlags};
13use wayland_client::globals::GlobalListContents;
14use wayland_client::protocol::wl_registry::WlRegistry;
15use wayland_client::protocol::wl_seat::WlSeat;
16use wayland_client::{
17    delegate_dispatch, event_created_child, ConnectError, Dispatch, DispatchError, EventQueue,
18};
19
20use crate::common::{self, initialize};
21use crate::data_control::{
22    self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer, impl_dispatch_source,
23};
24use crate::seat_data::SeatData;
25use crate::utils::is_text;
26
27const TEXT_PLAIN_MIME: &str = "text/plain";
28
29/// The clipboard to operate on.
30#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
31#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
32pub enum ClipboardType {
33    /// The regular clipboard.
34    #[default]
35    Regular,
36    /// The "primary" clipboard.
37    ///
38    /// Working with the "primary" clipboard requires the compositor to support ext-data-control,
39    /// or wlr-data-control version 2 or above.
40    Primary,
41    /// Operate on both clipboards at once.
42    ///
43    /// Useful for atomically setting both clipboards at once. This option requires the "primary"
44    /// clipboard to be supported.
45    Both,
46}
47
48/// MIME type to offer the copied data under.
49#[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)]
50#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
51pub enum MimeType {
52    /// Detect the MIME type automatically from the data.
53    #[cfg_attr(test, proptest(skip))]
54    Autodetect,
55    /// Offer a number of common plain text MIME types.
56    Text,
57    /// Offer a specific MIME type.
58    Specific(String),
59}
60
61/// Source for copying.
62#[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)]
63#[cfg_attr(test, derive(proptest_derive::Arbitrary))]
64pub enum Source {
65    /// Copy contents of the standard input.
66    #[cfg_attr(test, proptest(skip))]
67    StdIn,
68    /// Copy the given bytes.
69    Bytes(Box<[u8]>),
70}
71
72/// Source for copying, with a MIME type.
73///
74/// Used for [`copy_multi`].
75///
76/// [`copy_multi`]: fn.copy_multi.html
77#[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord)]
78pub struct MimeSource {
79    pub source: Source,
80    pub mime_type: MimeType,
81}
82
83/// Seat to operate on.
84#[derive(Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
85pub enum Seat {
86    /// Operate on all existing seats at once.
87    #[default]
88    All,
89    /// Operate on a seat with the given name.
90    Specific(String),
91}
92
93/// Number of paste requests to serve.
94#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
95pub enum ServeRequests {
96    /// Serve requests indefinitely.
97    #[default]
98    Unlimited,
99    /// Serve only the given number of requests.
100    Only(usize),
101}
102
103/// Options and flags that are used to customize the copying.
104#[derive(Clone, Eq, PartialEq, Debug, Default, Hash, PartialOrd, Ord)]
105pub struct Options {
106    /// The clipboard to work with.
107    clipboard: ClipboardType,
108
109    /// The seat to work with.
110    seat: Seat,
111
112    /// Trim the trailing newline character before copying.
113    ///
114    /// This flag is only applied for text MIME types.
115    trim_newline: bool,
116
117    /// Do not spawn a separate thread for serving copy requests.
118    ///
119    /// Setting this flag will result in the call to `copy()` **blocking** until all data sources
120    /// it creates are destroyed, e.g. until someone else copies something into the clipboard.
121    foreground: bool,
122
123    /// Number of paste requests to serve.
124    ///
125    /// Limiting the number of paste requests to one effectively clears the clipboard after the
126    /// first paste. It can be used when copying e.g. sensitive data, like passwords. Note however
127    /// that certain apps may have issues pasting when this option is used, in particular XWayland
128    /// clients are known to suffer from this.
129    serve_requests: ServeRequests,
130
131    /// Omit additional text mime types which are offered by default if at least one text mime type is provided.
132    ///
133    /// Omits additionally offered `text/plain;charset=utf-8`, `text/plain`, `STRING`, `UTF8_STRING` and
134    /// `TEXT` mime types which are offered by default if at least one text mime type is provided.
135    omit_additional_text_mime_types: bool,
136}
137
138/// A copy operation ready to start serving requests.
139pub struct PreparedCopy {
140    queue: EventQueue<State>,
141    state: State,
142    sources: Vec<data_control::Source>,
143}
144
145/// Errors that can occur for copying the source data to a temporary file.
146#[derive(thiserror::Error, Debug)]
147pub enum SourceCreationError {
148    #[error("Couldn't create a temporary directory")]
149    TempDirCreate(#[source] io::Error),
150
151    #[error("Couldn't create a temporary file")]
152    TempFileCreate(#[source] io::Error),
153
154    #[error("Couldn't copy data to the temporary file")]
155    DataCopy(#[source] io::Error),
156
157    #[error("Couldn't write to the temporary file")]
158    TempFileWrite(#[source] io::Error),
159
160    #[error("Couldn't open the temporary file for newline trimming")]
161    TempFileOpen(#[source] io::Error),
162
163    #[error("Couldn't get the temporary file metadata for newline trimming")]
164    TempFileMetadata(#[source] io::Error),
165
166    #[error("Couldn't seek the temporary file for newline trimming")]
167    TempFileSeek(#[source] io::Error),
168
169    #[error("Couldn't read the last byte of the temporary file for newline trimming")]
170    TempFileRead(#[source] io::Error),
171
172    #[error("Couldn't truncate the temporary file for newline trimming")]
173    TempFileTruncate(#[source] io::Error),
174}
175
176/// Errors that can occur for copying and clearing the clipboard.
177#[derive(thiserror::Error, Debug)]
178pub enum Error {
179    #[error("There are no seats")]
180    NoSeats,
181
182    #[error("Couldn't open the provided Wayland socket")]
183    SocketOpenError(#[source] io::Error),
184
185    #[error("Couldn't connect to the Wayland compositor")]
186    WaylandConnection(#[source] ConnectError),
187
188    #[error("Wayland compositor communication error")]
189    WaylandCommunication(#[source] DispatchError),
190
191    #[error(
192        "A required Wayland protocol ({} version {}) is not supported by the compositor",
193        name,
194        version
195    )]
196    MissingProtocol { name: &'static str, version: u32 },
197
198    #[error("The compositor does not support primary selection")]
199    PrimarySelectionUnsupported,
200
201    #[error("The requested seat was not found")]
202    SeatNotFound,
203
204    #[error("Error copying the source into a temporary file")]
205    TempCopy(#[source] SourceCreationError),
206
207    #[error("Couldn't remove the temporary file")]
208    TempFileRemove(#[source] io::Error),
209
210    #[error("Couldn't remove the temporary directory")]
211    TempDirRemove(#[source] io::Error),
212
213    #[error("Error satisfying a paste request")]
214    Paste(#[source] DataSourceError),
215}
216
217impl From<common::Error> for Error {
218    fn from(x: common::Error) -> Self {
219        use common::Error::*;
220
221        match x {
222            SocketOpenError(err) => Error::SocketOpenError(err),
223            WaylandConnection(err) => Error::WaylandConnection(err),
224            WaylandCommunication(err) => Error::WaylandCommunication(err.into()),
225            MissingProtocol { name, version } => Error::MissingProtocol { name, version },
226        }
227    }
228}
229
230#[derive(thiserror::Error, Debug)]
231pub enum DataSourceError {
232    #[error("Couldn't open the data file")]
233    FileOpen(#[source] io::Error),
234
235    #[error("Couldn't copy the data to the target file descriptor")]
236    Copy(#[source] io::Error),
237}
238
239/// An inner source of data in-memory.
240///
241/// This is always cheap to clone.
242#[derive(Clone)]
243struct DataSourceStorage(Arc<[u8]>);
244
245struct DataSource {
246    mime_type: String,
247    source: DataSourceStorage,
248}
249
250struct State {
251    common: common::State,
252    got_primary_selection: bool,
253    // This bool can be set to true when serving a request: either if an error occurs, or if the
254    // number of requests to serve was limited and the last request was served.
255    should_quit: bool,
256    data_sources: HashMap<String, DataSourceStorage>,
257    serve_requests: ServeRequests,
258    // An error that occurred while serving a request, if any.
259    error: Option<DataSourceError>,
260}
261
262delegate_dispatch!(State: [WlSeat: ()] => common::State);
263
264impl AsMut<common::State> for State {
265    fn as_mut(&mut self) -> &mut common::State {
266        &mut self.common
267    }
268}
269
270impl Dispatch<WlRegistry, GlobalListContents> for State {
271    fn event(
272        _state: &mut Self,
273        _proxy: &WlRegistry,
274        _event: <WlRegistry as wayland_client::Proxy>::Event,
275        _data: &GlobalListContents,
276        _conn: &wayland_client::Connection,
277        _qhandle: &wayland_client::QueueHandle<Self>,
278    ) {
279    }
280}
281
282impl_dispatch_manager!(State);
283
284impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat| {
285    match event {
286        Event::DataOffer { id } => id.destroy(),
287        Event::Finished => {
288            state.common.seats.get_mut(seat).unwrap().set_device(None);
289        }
290        Event::PrimarySelection { .. } => {
291            state.got_primary_selection = true;
292        }
293        _ => (),
294    }
295});
296
297impl_dispatch_offer!(State);
298
299impl_dispatch_source!(State, |state: &mut Self,
300                              source: data_control::Source,
301                              event| {
302    match event {
303        Event::Send { mime_type, fd } => {
304            // Check if some other source already handled a paste request and indicated that we should
305            // quit.
306            if state.should_quit {
307                source.destroy();
308                return;
309            }
310
311            // I'm not sure if it's the compositor's responsibility to check that the mime type is
312            // valid. Let's check here just in case.
313            let data_source = match state.data_sources.get_mut(&mime_type) {
314                Some(source) => source,
315                None => {
316                    return;
317                }
318            };
319
320            let copy_result = || {
321                // Clear O_NONBLOCK, otherwise io::copy() will stop halfway.
322                fcntl_setfl(&fd, OFlags::empty()).map_err(io::Error::from)?;
323                let mut target_file = File::from(fd);
324
325                let mut source_content = Cursor::new(&data_source.0);
326                io::copy(&mut source_content, &mut target_file).map(drop)
327            };
328
329            if let Err(err) = copy_result().map_err(DataSourceError::Copy) {
330                state.error = Some(err);
331            }
332
333            let done = if let ServeRequests::Only(left) = state.serve_requests {
334                let left = left.checked_sub(1).unwrap();
335                state.serve_requests = ServeRequests::Only(left);
336                left == 0
337            } else {
338                false
339            };
340
341            if done || state.error.is_some() {
342                state.should_quit = true;
343                source.destroy();
344            }
345        }
346        Event::Cancelled => source.destroy(),
347        _ => (),
348    }
349});
350
351impl Options {
352    /// Creates a blank new set of options ready for configuration.
353    #[inline]
354    pub fn new() -> Self {
355        Self::default()
356    }
357
358    /// Sets the clipboard to work with.
359    #[inline]
360    pub fn clipboard(&mut self, clipboard: ClipboardType) -> &mut Self {
361        self.clipboard = clipboard;
362        self
363    }
364
365    /// Sets the seat to use for copying.
366    #[inline]
367    pub fn seat(&mut self, seat: Seat) -> &mut Self {
368        self.seat = seat;
369        self
370    }
371
372    /// Sets the flag for trimming the trailing newline.
373    ///
374    /// This flag is only applied for text MIME types.
375    #[inline]
376    pub fn trim_newline(&mut self, trim_newline: bool) -> &mut Self {
377        self.trim_newline = trim_newline;
378        self
379    }
380
381    /// Sets the flag for not spawning a separate thread for serving copy requests.
382    ///
383    /// Setting this flag will result in the call to `copy()` **blocking** until all data sources
384    /// it creates are destroyed, e.g. until someone else copies something into the clipboard.
385    #[inline]
386    pub fn foreground(&mut self, foreground: bool) -> &mut Self {
387        self.foreground = foreground;
388        self
389    }
390
391    /// Sets the number of requests to serve.
392    ///
393    /// Limiting the number of requests to one effectively clears the clipboard after the first
394    /// paste. It can be used when copying e.g. sensitive data, like passwords. Note however that
395    /// certain apps may have issues pasting when this option is used, in particular XWayland
396    /// clients are known to suffer from this.
397    #[inline]
398    pub fn serve_requests(&mut self, serve_requests: ServeRequests) -> &mut Self {
399        self.serve_requests = serve_requests;
400        self
401    }
402
403    /// Sets the flag for omitting additional text mime types which are offered by default if at least one text mime type is provided.
404    ///
405    /// Omits additionally offered `text/plain;charset=utf-8`, `text/plain`, `STRING`, `UTF8_STRING` and
406    /// `TEXT` mime types which are offered by default if at least one text mime type is provided.
407    #[inline]
408    pub fn omit_additional_text_mime_types(
409        &mut self,
410        omit_additional_text_mime_types: bool,
411    ) -> &mut Self {
412        self.omit_additional_text_mime_types = omit_additional_text_mime_types;
413        self
414    }
415
416    /// Invokes the copy operation. See `copy()`.
417    ///
418    /// # Examples
419    ///
420    /// ```no_run
421    /// # extern crate wl_clipboard_rs;
422    /// # use wl_clipboard_rs::copy::Error;
423    /// # fn foo() -> Result<(), Error> {
424    /// use wl_clipboard_rs::copy::{MimeType, Options, Source};
425    ///
426    /// let opts = Options::new();
427    /// opts.copy(Source::Bytes([1, 2, 3][..].into()), MimeType::Autodetect)?;
428    /// # Ok(())
429    /// # }
430    /// ```
431    #[inline]
432    pub fn copy(self, source: Source, mime_type: MimeType) -> Result<(), Error> {
433        copy(self, source, mime_type)
434    }
435
436    /// Invokes the copy_multi operation. See `copy_multi()`.
437    ///
438    /// # Examples
439    ///
440    /// ```no_run
441    /// # extern crate wl_clipboard_rs;
442    /// # use wl_clipboard_rs::copy::Error;
443    /// # fn foo() -> Result<(), Error> {
444    /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source};
445    ///
446    /// let opts = Options::new();
447    /// opts.copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()),
448    ///                                   mime_type: MimeType::Autodetect },
449    ///                      MimeSource { source: Source::Bytes([7, 8, 9][..].into()),
450    ///                                   mime_type: MimeType::Text }])?;
451    /// # Ok(())
452    /// # }
453    /// ```
454    #[inline]
455    pub fn copy_multi(self, sources: Vec<MimeSource>) -> Result<(), Error> {
456        copy_multi(self, sources)
457    }
458
459    /// Invokes the prepare_copy operation. See `prepare_copy()`.
460    ///
461    /// # Panics
462    ///
463    /// Panics if `foreground` is `false`.
464    ///
465    /// # Examples
466    ///
467    /// ```no_run
468    /// # extern crate wl_clipboard_rs;
469    /// # use wl_clipboard_rs::copy::Error;
470    /// # fn foo() -> Result<(), Error> {
471    /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source};
472    ///
473    /// let mut opts = Options::new();
474    /// opts.foreground(true);
475    /// let prepared_copy = opts.prepare_copy(Source::Bytes([1, 2, 3][..].into()),
476    ///                                       MimeType::Autodetect)?;
477    /// prepared_copy.serve()?;
478    ///
479    /// # Ok(())
480    /// # }
481    /// ```
482    #[inline]
483    pub fn prepare_copy(self, source: Source, mime_type: MimeType) -> Result<PreparedCopy, Error> {
484        prepare_copy(self, source, mime_type)
485    }
486
487    /// Invokes the prepare_copy_multi operation. See `prepare_copy_multi()`.
488    ///
489    /// # Panics
490    ///
491    /// Panics if `foreground` is `false`.
492    ///
493    /// # Examples
494    ///
495    /// ```no_run
496    /// # extern crate wl_clipboard_rs;
497    /// # use wl_clipboard_rs::copy::Error;
498    /// # fn foo() -> Result<(), Error> {
499    /// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source};
500    ///
501    /// let mut opts = Options::new();
502    /// opts.foreground(true);
503    /// let prepared_copy =
504    ///     opts.prepare_copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()),
505    ///                                               mime_type: MimeType::Autodetect },
506    ///                                  MimeSource { source: Source::Bytes([7, 8, 9][..].into()),
507    ///                                               mime_type: MimeType::Text }])?;
508    /// prepared_copy.serve()?;
509    ///
510    /// # Ok(())
511    /// # }
512    /// ```
513    #[inline]
514    pub fn prepare_copy_multi(self, sources: Vec<MimeSource>) -> Result<PreparedCopy, Error> {
515        prepare_copy_multi(self, sources)
516    }
517}
518
519impl PreparedCopy {
520    /// Starts serving copy requests.
521    ///
522    /// This function **blocks** until all requests are served or the clipboard is taken over by
523    /// some other application.
524    pub fn serve(mut self) -> Result<(), Error> {
525        // Loop until we're done.
526        while !self.state.should_quit {
527            self.queue
528                .blocking_dispatch(&mut self.state)
529                .map_err(Error::WaylandCommunication)?;
530
531            // Check if all sources have been destroyed.
532            let all_destroyed = self.sources.iter().all(|x| !x.is_alive());
533            if all_destroyed {
534                self.state.should_quit = true;
535            }
536        }
537
538        // Check if an error occurred during data transfer.
539        if let Some(err) = self.state.error.take() {
540            return Err(Error::Paste(err));
541        }
542
543        Ok(())
544    }
545}
546
547fn make_source(
548    source: Source,
549    mime_type: MimeType,
550    trim_newline: bool,
551) -> Result<DataSource, SourceCreationError> {
552    let mut output_place = if let Source::Bytes(data) = source {
553        data.into_vec()
554    } else {
555        let mut contents = Cursor::new(Vec::new());
556        io::copy(&mut io::stdin(), &mut contents).map_err(SourceCreationError::DataCopy)?;
557        contents.into_inner()
558    };
559
560    let mime_type = match mime_type {
561        MimeType::Autodetect => tree_magic_mini::from_u8(&output_place).to_string(),
562        MimeType::Text => TEXT_PLAIN_MIME.to_string(),
563        MimeType::Specific(mime_type) => mime_type,
564    };
565    log::trace!("Base MIME type: {}", mime_type);
566
567    // Trim the trailing newline if needed.
568    if trim_newline && is_text(&mime_type) && output_place.last().copied() == Some(b'\n') {
569        output_place.pop();
570    }
571
572    Ok(DataSource {
573        mime_type,
574        source: DataSourceStorage(Arc::from(output_place)),
575    })
576}
577
578fn get_devices(
579    primary: bool,
580    seat: Seat,
581    socket_name: Option<OsString>,
582) -> Result<(EventQueue<State>, State, Vec<data_control::Device>), Error> {
583    let (mut queue, mut common) = initialize(primary, socket_name)?;
584
585    // Check if there are no seats.
586    if common.seats.is_empty() {
587        return Err(Error::NoSeats);
588    }
589
590    // Go through the seats and get their data devices.
591    for (seat, data) in &mut common.seats {
592        let device = common
593            .clipboard_manager
594            .get_data_device(seat, &queue.handle(), seat.clone());
595        data.set_device(Some(device));
596    }
597
598    let mut state = State {
599        common,
600        got_primary_selection: false,
601        should_quit: false,
602        data_sources: HashMap::new(),
603        serve_requests: ServeRequests::default(),
604        error: None,
605    };
606
607    // Retrieve all seat names.
608    queue
609        .roundtrip(&mut state)
610        .map_err(Error::WaylandCommunication)?;
611
612    // Check if the compositor supports primary selection.
613    if primary && !state.got_primary_selection {
614        return Err(Error::PrimarySelectionUnsupported);
615    }
616
617    // Figure out which devices we're interested in.
618    let devices = state
619        .common
620        .seats
621        .values()
622        .filter_map(|data| {
623            let SeatData { name, device, .. } = data;
624
625            let device = device.clone();
626
627            match seat {
628                Seat::All => {
629                    // If no seat was specified, handle all of them.
630                    return device;
631                }
632                Seat::Specific(ref desired_name) => {
633                    if name.as_deref() == Some(desired_name) {
634                        return device;
635                    }
636                }
637            }
638
639            None
640        })
641        .collect::<Vec<_>>();
642
643    // If we didn't find the seat, print an error message and exit.
644    //
645    // This also triggers when we found the seat but it had no data device; is this what we want?
646    if devices.is_empty() {
647        return Err(Error::SeatNotFound);
648    }
649
650    Ok((queue, state, devices))
651}
652
653/// Clears the clipboard for the given seat.
654///
655/// If `seat` is `None`, clears clipboards of all existing seats.
656///
657/// # Examples
658///
659/// ```no_run
660/// # extern crate wl_clipboard_rs;
661/// # use wl_clipboard_rs::copy::Error;
662/// # fn foo() -> Result<(), Error> {
663/// use wl_clipboard_rs::{copy::{clear, ClipboardType, Seat}};
664///
665/// clear(ClipboardType::Regular, Seat::All)?;
666/// # Ok(())
667/// # }
668/// ```
669#[inline]
670pub fn clear(clipboard: ClipboardType, seat: Seat) -> Result<(), Error> {
671    clear_internal(clipboard, seat, None)
672}
673
674pub(crate) fn clear_internal(
675    clipboard: ClipboardType,
676    seat: Seat,
677    socket_name: Option<OsString>,
678) -> Result<(), Error> {
679    let primary = clipboard != ClipboardType::Regular;
680    let (mut queue, mut state, devices) = get_devices(primary, seat, socket_name)?;
681
682    for device in devices {
683        if clipboard == ClipboardType::Primary || clipboard == ClipboardType::Both {
684            device.set_primary_selection(None);
685        }
686        if clipboard == ClipboardType::Regular || clipboard == ClipboardType::Both {
687            device.set_selection(None);
688        }
689    }
690
691    // We're clearing the clipboard so just do one roundtrip and quit.
692    queue
693        .roundtrip(&mut state)
694        .map_err(Error::WaylandCommunication)?;
695
696    Ok(())
697}
698
699/// Prepares a data copy to the clipboard.
700///
701/// The data is copied from `source` and offered in the `mime_type` MIME type. See `Options` for
702/// customizing the behavior of this operation.
703///
704/// This function can be used instead of `copy()` when it's desirable to separately prepare the
705/// copy operation, handle any errors that this may produce, and then start the serving loop,
706/// potentially past a fork (which is how `wl-copy` uses it). It is meant to be used in the
707/// foreground mode and does not spawn any threads.
708///
709/// # Panics
710///
711/// Panics if `foreground` is `false`.
712///
713/// # Examples
714///
715/// ```no_run
716/// # extern crate wl_clipboard_rs;
717/// # use wl_clipboard_rs::copy::Error;
718/// # fn foo() -> Result<(), Error> {
719/// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source};
720///
721/// let mut opts = Options::new();
722/// opts.foreground(true);
723/// let prepared_copy = opts.prepare_copy(Source::Bytes([1, 2, 3][..].into()),
724///                                       MimeType::Autodetect)?;
725/// prepared_copy.serve()?;
726///
727/// # Ok(())
728/// # }
729/// ```
730#[inline]
731pub fn prepare_copy(
732    options: Options,
733    source: Source,
734    mime_type: MimeType,
735) -> Result<PreparedCopy, Error> {
736    assert!(options.foreground);
737
738    let sources = vec![MimeSource { source, mime_type }];
739
740    prepare_copy_internal(options, sources, None)
741}
742
743/// Prepares a data copy to the clipboard, offering multiple data sources.
744///
745/// The data from each source in `sources` is copied and offered in the corresponding MIME type.
746/// See `Options` for customizing the behavior of this operation.
747///
748/// If multiple sources specify the same MIME type, the first one is offered. If one of the MIME
749/// types is text, all automatically added plain text offers will fall back to the first source
750/// with a text MIME type.
751///
752/// This function can be used instead of `copy()` when it's desirable to separately prepare the
753/// copy operation, handle any errors that this may produce, and then start the serving loop,
754/// potentially past a fork (which is how `wl-copy` uses it). It is meant to be used in the
755/// foreground mode and does not spawn any threads.
756///
757/// # Panics
758///
759/// Panics if `foreground` is `false`.
760///
761/// # Examples
762///
763/// ```no_run
764/// # extern crate wl_clipboard_rs;
765/// # use wl_clipboard_rs::copy::Error;
766/// # fn foo() -> Result<(), Error> {
767/// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source};
768///
769/// let mut opts = Options::new();
770/// opts.foreground(true);
771/// let prepared_copy =
772///     opts.prepare_copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()),
773///                                               mime_type: MimeType::Autodetect },
774///                                  MimeSource { source: Source::Bytes([7, 8, 9][..].into()),
775///                                               mime_type: MimeType::Text }])?;
776/// prepared_copy.serve()?;
777///
778/// # Ok(())
779/// # }
780/// ```
781#[inline]
782pub fn prepare_copy_multi(
783    options: Options,
784    sources: Vec<MimeSource>,
785) -> Result<PreparedCopy, Error> {
786    assert!(options.foreground);
787
788    prepare_copy_internal(options, sources, None)
789}
790
791fn prepare_copy_internal(
792    options: Options,
793    sources: Vec<MimeSource>,
794    socket_name: Option<OsString>,
795) -> Result<PreparedCopy, Error> {
796    let Options {
797        clipboard,
798        seat,
799        trim_newline,
800        serve_requests,
801        ..
802    } = options;
803
804    let primary = clipboard != ClipboardType::Regular;
805    let (queue, mut state, devices) = get_devices(primary, seat, socket_name)?;
806
807    state.serve_requests = serve_requests;
808
809    // Collect the source data to copy.
810    state.data_sources = {
811        let mut data_sources = HashMap::new();
812        let mut text_data = None;
813
814        for MimeSource { source, mime_type } in sources.into_iter() {
815            let DataSource { mime_type, source } =
816                make_source(source, mime_type, trim_newline).map_err(Error::TempCopy)?;
817
818            match data_sources.entry(mime_type) {
819                // This MIME type has already been specified, so ignore it.
820                Entry::Occupied(_) => drop(source),
821                Entry::Vacant(entry) => {
822                    if !options.omit_additional_text_mime_types
823                        && text_data.is_none()
824                        && is_text(entry.key())
825                    {
826                        text_data = Some(source.clone());
827                    }
828
829                    entry.insert(source);
830                }
831            }
832        }
833
834        // If the MIME type is text, offer it in some other common formats.
835        if let Some(text_data) = text_data {
836            let text_mimes = [
837                "text/plain;charset=utf-8",
838                TEXT_PLAIN_MIME,
839                "STRING",
840                "UTF8_STRING",
841                "TEXT",
842            ];
843
844            for mime_type in text_mimes {
845                // We don't want to overwrite an explicit mime type, because it might be bound to a
846                // different data_path
847                if !data_sources.contains_key(mime_type) {
848                    data_sources.insert(mime_type.to_string(), text_data.clone());
849                }
850            }
851        }
852        data_sources
853    };
854
855    // Create an iterator over (device, primary) for source creation later.
856    //
857    // This is needed because for ClipboardType::Both each device needs to appear twice because
858    // separate data sources need to be made for the regular and the primary clipboards (data
859    // sources cannot be reused).
860    let devices_iter = devices.iter().flat_map(|device| {
861        let first = match clipboard {
862            ClipboardType::Regular => iter::once((device, false)),
863            ClipboardType::Primary => iter::once((device, true)),
864            ClipboardType::Both => iter::once((device, false)),
865        };
866
867        let second = if clipboard == ClipboardType::Both {
868            iter::once(Some((device, true)))
869        } else {
870            iter::once(None)
871        };
872
873        first.chain(second.flatten())
874    });
875
876    // Create the data sources and set them as selections.
877    let sources = devices_iter
878        .map(|(device, primary)| {
879            let data_source = state
880                .common
881                .clipboard_manager
882                .create_data_source(&queue.handle());
883
884            for mime_type in state.data_sources.keys() {
885                data_source.offer(mime_type.clone());
886            }
887
888            if primary {
889                device.set_primary_selection(Some(&data_source));
890            } else {
891                device.set_selection(Some(&data_source));
892            }
893
894            // If we need to serve 0 requests, kill the data source right away.
895            if let ServeRequests::Only(0) = state.serve_requests {
896                data_source.destroy();
897            }
898            data_source
899        })
900        .collect::<Vec<_>>();
901
902    Ok(PreparedCopy {
903        queue,
904        state,
905        sources,
906    })
907}
908
909/// Copies data to the clipboard.
910///
911/// The data is copied from `source` and offered in the `mime_type` MIME type. See `Options` for
912/// customizing the behavior of this operation.
913///
914/// # Examples
915///
916/// ```no_run
917/// # extern crate wl_clipboard_rs;
918/// # use wl_clipboard_rs::copy::Error;
919/// # fn foo() -> Result<(), Error> {
920/// use wl_clipboard_rs::copy::{copy, MimeType, Options, Source};
921///
922/// let opts = Options::new();
923/// copy(opts, Source::Bytes([1, 2, 3][..].into()), MimeType::Autodetect)?;
924/// # Ok(())
925/// # }
926/// ```
927#[inline]
928pub fn copy(options: Options, source: Source, mime_type: MimeType) -> Result<(), Error> {
929    let sources = vec![MimeSource { source, mime_type }];
930    copy_internal(options, sources, None)
931}
932
933/// Copies data to the clipboard, offering multiple data sources.
934///
935/// The data from each source in `sources` is copied and offered in the corresponding MIME type.
936/// See `Options` for customizing the behavior of this operation.
937///
938/// If multiple sources specify the same MIME type, the first one is offered. If one of the MIME
939/// types is text, all automatically added plain text offers will fall back to the first source
940/// with a text MIME type.
941///
942/// # Examples
943///
944/// ```no_run
945/// # extern crate wl_clipboard_rs;
946/// # use wl_clipboard_rs::copy::Error;
947/// # fn foo() -> Result<(), Error> {
948/// use wl_clipboard_rs::copy::{MimeSource, MimeType, Options, Source};
949///
950/// let opts = Options::new();
951/// opts.copy_multi(vec![MimeSource { source: Source::Bytes([1, 2, 3][..].into()),
952///                                   mime_type: MimeType::Autodetect },
953///                      MimeSource { source: Source::Bytes([7, 8, 9][..].into()),
954///                                   mime_type: MimeType::Text }])?;
955/// # Ok(())
956/// # }
957/// ```
958#[inline]
959pub fn copy_multi(options: Options, sources: Vec<MimeSource>) -> Result<(), Error> {
960    copy_internal(options, sources, None)
961}
962
963pub(crate) fn copy_internal(
964    options: Options,
965    sources: Vec<MimeSource>,
966    socket_name: Option<OsString>,
967) -> Result<(), Error> {
968    if options.foreground {
969        prepare_copy_internal(options, sources, socket_name)?.serve()
970    } else {
971        // The copy must be prepared on the thread because PreparedCopy isn't Send.
972        // To receive errors from prepare_copy, use a channel.
973        let (tx, rx) = sync_channel(1);
974
975        thread::spawn(
976            move || match prepare_copy_internal(options, sources, socket_name) {
977                Ok(prepared_copy) => {
978                    // prepare_copy completed successfully, report that.
979                    drop(tx.send(None));
980
981                    // There's nobody listening for errors at this point, just drop it.
982                    drop(prepared_copy.serve());
983                }
984                Err(err) => drop(tx.send(Some(err))),
985            },
986        );
987
988        if let Some(err) = rx.recv().unwrap() {
989            return Err(err);
990        }
991
992        Ok(())
993    }
994}