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}