smithay_clipboard/
lib.rs

1//! Smithay Clipboard
2//!
3//! Provides access to the Wayland clipboard for gui applications. The user
4//! should have surface around.
5
6#![deny(clippy::all, clippy::if_not_else, clippy::enum_glob_use)]
7use std::borrow::Cow;
8use std::ffi::c_void;
9use std::io::Result;
10use std::sync::mpsc::{self, Receiver};
11
12use sctk::reexports::calloop::channel::{self, Sender};
13use sctk::reexports::client::backend::Backend;
14use sctk::reexports::client::protocol::wl_surface::WlSurface;
15use sctk::reexports::client::Connection;
16
17#[cfg(feature = "dnd")]
18pub mod dnd;
19pub mod mime;
20mod state;
21pub mod text;
22mod worker;
23
24use mime::{AllowedMimeTypes, AsMimeTypes, MimeType};
25use state::Target;
26use text::Text;
27
28pub type SimpleClipboard = Clipboard<WlSurface>;
29
30/// Access to a Wayland clipboard.
31pub struct Clipboard<T> {
32    request_sender: Sender<worker::Command<T>>,
33    request_receiver: Receiver<Result<(Vec<u8>, MimeType)>>,
34    clipboard_thread: Option<std::thread::JoinHandle<()>>,
35    connection: Connection,
36}
37
38impl<T: 'static + Send + Clone> Clipboard<T> {
39    /// Creates new clipboard which will be running on its own thread with its
40    /// own event queue to handle clipboard requests.
41    ///
42    /// # Safety
43    ///
44    /// `display` must be a valid `*mut wl_display` pointer, and it must remain
45    /// valid for as long as `Clipboard` object is alive.
46    pub unsafe fn new(display: *mut c_void) -> Self {
47        let backend = unsafe { Backend::from_foreign_display(display.cast()) };
48        let connection = Connection::from_backend(backend);
49
50        // Create channel to send data to clipboard thread.
51        let (request_sender, rx_chan) = channel::channel();
52        // Create channel to get data from the clipboard thread.
53        let (clipboard_reply_sender, request_receiver) = mpsc::channel();
54
55        let name = String::from("smithay-clipboard");
56        let clipboard_thread =
57            worker::spawn(name, connection.clone(), rx_chan, clipboard_reply_sender);
58
59        Self { request_receiver, request_sender, clipboard_thread, connection }
60    }
61
62    /// Load custom clipboard data.
63    ///
64    /// Load the requested type from a clipboard on the last observed seat.
65    pub fn load<D: AllowedMimeTypes + 'static>(&self) -> Result<D> {
66        self.load_inner(Target::Clipboard, D::allowed())
67    }
68
69    /// Load clipboard data.
70    ///
71    /// Loads content from a clipboard on a last observed seat.
72    pub fn load_text(&self) -> Result<String> {
73        self.load::<Text>().map(|t| t.0)
74    }
75
76    /// Load custom primary clipboard data.
77    ///
78    /// Load the requested type from a primary clipboard on the last observed
79    /// seat.
80    pub fn load_primary<D: AllowedMimeTypes + 'static>(&self) -> Result<D> {
81        self.load_inner(Target::Primary, D::allowed())
82    }
83
84    /// Load primary clipboard data.
85    ///
86    /// Loads content from a  primary clipboard on a last observed seat.
87    pub fn load_primary_text(&self) -> Result<String> {
88        self.load_primary::<Text>().map(|t| t.0)
89    }
90
91    /// Load clipboard data for sepecific mime types.
92    ///
93    /// Loads content from a  primary clipboard on a last observed seat.
94    pub fn load_mime<D: TryFrom<(Vec<u8>, MimeType)>>(
95        &self,
96        allowed: impl Into<Cow<'static, [MimeType]>>,
97    ) -> Result<D> {
98        self.load_inner(Target::Clipboard, allowed).and_then(|d| {
99            D::try_from(d).map_err(|_| {
100                std::io::Error::new(
101                    std::io::ErrorKind::Other,
102                    "Failed to load data of the requested type.",
103                )
104            })
105        })
106    }
107
108    /// Load primary clipboard data for specific mime types.
109    ///
110    /// Loads content from a  primary clipboard on a last observed seat.
111    pub fn load_primary_mime<D: TryFrom<(Vec<u8>, MimeType)>>(
112        &self,
113        allowed: impl Into<Cow<'static, [MimeType]>>,
114    ) -> Result<D> {
115        self.load_inner(Target::Primary, allowed).and_then(|d| {
116            D::try_from(d).map_err(|_| {
117                std::io::Error::new(
118                    std::io::ErrorKind::Other,
119                    "Failed to load data of the requested type.",
120                )
121            })
122        })
123    }
124
125    /// Store custom data to a clipboard.
126    ///
127    /// Stores data of the provided type to a clipboard on a last observed seat.
128    pub fn store<D: AsMimeTypes + Send + 'static>(&self, data: D) {
129        self.store_inner(data, Target::Clipboard);
130    }
131
132    /// Store to a clipboard.
133    ///
134    /// Stores to a clipboard on a last observed seat.
135    pub fn store_text<D: Into<String>>(&self, text: D) {
136        self.store(Text(text.into()));
137    }
138
139    /// Store custom data to a primary clipboard.
140    ///
141    /// Stores data of the provided type to a primary clipboard on a last
142    /// observed seat.
143    pub fn store_primary<D: AsMimeTypes + Send + 'static>(&self, data: D) {
144        self.store_inner(data, Target::Primary);
145    }
146
147    /// Store to a primary clipboard.
148    ///
149    /// Stores to a primary clipboard on a last observed seat.
150    pub fn store_primary_text<D: Into<String>>(&self, text: D) {
151        self.store_primary(Text(text.into()));
152    }
153
154    fn load_inner<D: TryFrom<(Vec<u8>, MimeType)> + 'static>(
155        &self,
156        target: Target,
157        allowed: impl Into<Cow<'static, [MimeType]>>,
158    ) -> Result<D> {
159        let _ = self.request_sender.send(worker::Command::Load(allowed.into(), target));
160
161        match self.request_receiver.recv() {
162            Ok(res) => res.and_then(|(data, mime)| {
163                D::try_from((data, mime)).map_err(|_| {
164                    std::io::Error::new(
165                        std::io::ErrorKind::Other,
166                        "Failed to load data of the requested type.",
167                    )
168                })
169            }),
170            // The clipboard thread is dead, however we shouldn't crash downstream,
171            // so propogating an error.
172            Err(_) => Err(std::io::Error::new(std::io::ErrorKind::Other, "clipboard is dead.")),
173        }
174    }
175
176    fn store_inner<D: AsMimeTypes + Send + 'static>(&self, data: D, target: Target) {
177        let request = worker::Command::Store(Box::new(data), target);
178        let _ = self.request_sender.send(request);
179    }
180}
181
182impl<T> Drop for Clipboard<T> {
183    fn drop(&mut self) {
184        // Shutdown smithay-clipboard.
185        let _ = self.request_sender.send(worker::Command::Exit);
186        if let Some(clipboard_thread) = self.clipboard_thread.take() {
187            let _ = clipboard_thread.join();
188        }
189    }
190}