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