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