Skip to main content

lamco_data_control/
client.rs

1//! The public clipboard handle.
2
3use std::{
4    collections::HashMap,
5    io::{Read, Write},
6    os::{
7        fd::OwnedFd,
8        unix::{io::AsFd, net::UnixStream},
9    },
10    sync::{
11        Arc, Mutex, MutexGuard,
12        atomic::{AtomicBool, Ordering},
13        mpsc,
14    },
15    thread::JoinHandle,
16    time::Duration,
17};
18
19use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
20
21use crate::{
22    error::{Error, Result},
23    mime::find_mime_match,
24    options::{Options, Protocol},
25    state::{Command, Shared},
26    worker,
27};
28
29/// How long a read waits for the source to deliver more data.
30const READ_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
31
32/// What to offer as the clipboard selection.
33///
34/// Each MIME type is either given data up front with [`data`](Self::data), or
35/// only advertised with [`advertise`](Self::advertise). A paste of an
36/// advertised type with no data raises a [`TransferRequest`], which the owner
37/// answers later. That is delayed rendering: the data is produced only if
38/// something pastes it.
39#[derive(Debug, Clone, Default)]
40pub struct Content {
41    mime_types: Vec<String>,
42    data: HashMap<String, Vec<u8>>,
43}
44
45impl Content {
46    /// Empty content.
47    #[must_use]
48    pub fn new() -> Self {
49        Self::default()
50    }
51
52    /// Offer `mime_type` with its data.
53    #[must_use]
54    pub fn data(mut self, mime_type: impl Into<String>, data: impl Into<Vec<u8>>) -> Self {
55        let mime_type = mime_type.into();
56        self.push_type(&mime_type);
57        self.data.insert(mime_type, data.into());
58        self
59    }
60
61    /// Offer `mime_type` without data; pastes of it raise a [`TransferRequest`].
62    #[must_use]
63    pub fn advertise(mut self, mime_type: impl Into<String>) -> Self {
64        self.push_type(&mime_type.into());
65        self
66    }
67
68    fn push_type(&mut self, mime_type: &str) {
69        if !self.mime_types.iter().any(|m| m == mime_type) {
70            self.mime_types.push(mime_type.to_owned());
71        }
72    }
73}
74
75/// Channel to the worker thread: a command queue and a socket that wakes its
76/// poll loop.
77struct Link {
78    commands: mpsc::Sender<Command>,
79    wake: UnixStream,
80}
81
82impl Link {
83    fn send(&self, command: Command) -> Result<()> {
84        self.commands.send(command).map_err(|_| Error::Stopped)?;
85        self.notify();
86        Ok(())
87    }
88
89    fn notify(&self) {
90        // A full socket buffer means a wake-up is already pending.
91        let _ = (&self.wake).write(&[1]);
92    }
93}
94
95/// A paste waiting for data that was advertised without any.
96///
97/// Answer it with [`complete`](Self::complete), or [`fail`](Self::fail) if the
98/// data cannot be produced. Dropping it without answering leaves the paste
99/// waiting until the selection is replaced.
100#[derive(Debug)]
101#[must_use = "a paste stays open until the request is answered"]
102pub struct TransferRequest {
103    serial: u32,
104    mime_type: String,
105    link: Arc<Link>,
106}
107
108impl std::fmt::Debug for Link {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        f.debug_struct("Link").finish_non_exhaustive()
111    }
112}
113
114impl TransferRequest {
115    /// The MIME type being pasted.
116    #[must_use]
117    pub fn mime_type(&self) -> &str {
118        &self.mime_type
119    }
120
121    /// Supply the data. It is written to every paste of this type that is
122    /// waiting, and cached for later pastes.
123    ///
124    /// # Errors
125    ///
126    /// [`Error::Stopped`] if the clipboard worker has shut down.
127    pub fn complete(self, data: impl Into<Vec<u8>>) -> Result<()> {
128        self.link.send(Command::CompleteTransfer {
129            serial: self.serial,
130            data: Some(data.into()),
131        })
132    }
133
134    /// Close the waiting pastes with no data.
135    ///
136    /// # Errors
137    ///
138    /// [`Error::Stopped`] if the clipboard worker has shut down.
139    pub fn fail(self) -> Result<()> {
140        self.link.send(Command::CompleteTransfer {
141            serial: self.serial,
142            data: None,
143        })
144    }
145}
146
147/// A connection to the Wayland compositor's clipboard.
148///
149/// Connecting spawns one thread that owns the Wayland connection. Dropping the
150/// handle stops and joins it.
151///
152/// ```no_run
153/// use lamco_data_control::{Content, DataControl};
154///
155/// # fn main() -> lamco_data_control::Result<()> {
156/// let clipboard = DataControl::connect()?;
157/// clipboard.set_selection(Content::new().data("text/plain;charset=utf-8", "hello"))?;
158/// # Ok(())
159/// # }
160/// ```
161pub struct DataControl {
162    link: Arc<Link>,
163    shared: Arc<Mutex<Shared>>,
164    protocol: Protocol,
165    max_read_bytes: usize,
166    stop: Arc<AtomicBool>,
167    thread: Option<JoinHandle<()>>,
168}
169
170impl std::fmt::Debug for DataControl {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        f.debug_struct("DataControl")
173            .field("protocol", &self.protocol)
174            .finish_non_exhaustive()
175    }
176}
177
178impl DataControl {
179    /// Connect with the default [`Options`].
180    ///
181    /// # Errors
182    ///
183    /// See [`Error`]: no compositor, no data-control protocol, or no seat.
184    pub fn connect() -> Result<Self> {
185        Self::connect_with(&Options::new())
186    }
187
188    /// Connect with explicit options.
189    ///
190    /// # Errors
191    ///
192    /// See [`Error`]: no compositor, no data-control protocol, or no seat.
193    pub fn connect_with(options: &Options) -> Result<Self> {
194        let (command_tx, command_rx) = mpsc::channel();
195        let (wake_tx, wake_rx) = UnixStream::pair()?;
196        wake_tx.set_nonblocking(true)?;
197        wake_rx.set_nonblocking(true)?;
198        let stop = Arc::new(AtomicBool::new(false));
199        let (ready_tx, ready_rx) = mpsc::sync_channel(1);
200
201        let thread_options = options.clone();
202        let thread_stop = Arc::clone(&stop);
203        let thread = std::thread::Builder::new()
204            .name("lamco-data-control".into())
205            .spawn(move || {
206                match worker::connect(&thread_options, command_rx, wake_rx, thread_stop) {
207                    Ok((worker, connected)) => {
208                        if ready_tx.send(Ok(connected)).is_ok() {
209                            worker.run();
210                        }
211                    }
212                    Err(error) => {
213                        let _ = ready_tx.send(Err(error));
214                    }
215                }
216            })?;
217
218        let connected = match ready_rx.recv() {
219            Ok(Ok(connected)) => connected,
220            Ok(Err(error)) => {
221                let _ = thread.join();
222                return Err(error);
223            }
224            Err(_) => {
225                let _ = thread.join();
226                return Err(Error::Stopped);
227            }
228        };
229
230        Ok(Self {
231            link: Arc::new(Link {
232                commands: command_tx,
233                wake: wake_tx,
234            }),
235            shared: connected.shared,
236            protocol: connected.protocol,
237            max_read_bytes: options.max_read_bytes,
238            stop,
239            thread: Some(thread),
240        })
241    }
242
243    /// The protocol the compositor is speaking.
244    #[must_use]
245    pub fn protocol(&self) -> Protocol {
246        self.protocol
247    }
248
249    fn shared(&self) -> MutexGuard<'_, Shared> {
250        self.shared
251            .lock()
252            .unwrap_or_else(std::sync::PoisonError::into_inner)
253    }
254
255    /// MIME types the current selection offers; empty if there is none.
256    #[must_use]
257    pub fn selection_mime_types(&self) -> Vec<String> {
258        self.shared().mime_types.clone()
259    }
260
261    /// A counter that increases on every selection change.
262    #[must_use]
263    pub fn serial(&self) -> u32 {
264        self.shared().serial
265    }
266
267    /// Call `callback` with the offered MIME types whenever another client
268    /// changes the selection.
269    ///
270    /// A selection this handle set itself is not reported; it still advances
271    /// [`serial`](Self::serial) and updates
272    /// [`selection_mime_types`](Self::selection_mime_types).
273    ///
274    /// The callback runs on the worker thread and must not block.
275    pub fn on_change(&self, callback: impl Fn(Vec<String>) + Send + Sync + 'static) {
276        self.shared().on_change = Some(Arc::new(callback));
277    }
278
279    /// Call `callback` when something pastes a type that was advertised
280    /// without data.
281    ///
282    /// The callback runs on the worker thread and must not block; hand the
283    /// request to another thread and answer it there.
284    pub fn on_transfer(&self, callback: impl Fn(TransferRequest) + Send + Sync + 'static) {
285        let link = Arc::clone(&self.link);
286        self.shared().on_transfer = Some(Arc::new(move |serial, mime_type| {
287            callback(TransferRequest {
288                serial,
289                mime_type,
290                link: Arc::clone(&link),
291            });
292        }));
293    }
294
295    /// Become the clipboard owner and offer `content`.
296    ///
297    /// # Errors
298    ///
299    /// [`Error::Stopped`] if the worker has shut down.
300    pub fn set_selection(&self, content: Content) -> Result<()> {
301        self.link.send(Command::SetSelection {
302            mime_types: content.mime_types,
303            data: content.data,
304        })
305    }
306
307    /// Add data for a type after [`set_selection`](Self::set_selection),
308    /// without replacing the selection.
309    ///
310    /// # Errors
311    ///
312    /// [`Error::Stopped`] if the worker has shut down.
313    pub fn update_data(
314        &self,
315        mime_type: impl Into<String>,
316        data: impl Into<Vec<u8>>,
317    ) -> Result<()> {
318        self.link.send(Command::UpdateSourceData {
319            mime_type: mime_type.into(),
320            data: data.into(),
321        })
322    }
323
324    /// Give up the selection if this handle still owns it.
325    ///
326    /// # Errors
327    ///
328    /// [`Error::Stopped`] if the worker has shut down.
329    pub fn clear_selection(&self) -> Result<()> {
330        self.link.send(Command::ClearSelection)
331    }
332
333    /// Read the current selection as `mime_type`.
334    ///
335    /// Charset differences are tolerated, see [`find_mime_match`]. Returns
336    /// `Ok(None)` when there is no selection or it does not offer the type.
337    ///
338    /// # Errors
339    ///
340    /// [`Error::TooLarge`] past the size limit, [`Error::Timeout`] if the
341    /// source stalls for five seconds, [`Error::Stopped`] if the worker has
342    /// shut down, [`Error::Io`] for pipe failures.
343    pub fn read(&self, mime_type: &str) -> Result<Option<Vec<u8>>> {
344        let available = self.selection_mime_types();
345        let Some(matched) = find_mime_match(mime_type, &available) else {
346            return Ok(None);
347        };
348
349        let (mut reader, writer) = std::io::pipe()?;
350        self.link.send(Command::ReceiveFromOffer {
351            mime_type: matched.to_owned(),
352            fd: OwnedFd::from(writer),
353        })?;
354
355        let mut data = Vec::new();
356        let mut chunk = vec![0u8; 64 * 1024];
357        loop {
358            let mut fds = [PollFd::new(reader.as_fd(), PollFlags::POLLIN)];
359            let timeout = PollTimeout::try_from(READ_IDLE_TIMEOUT).unwrap_or(PollTimeout::NONE);
360            match poll(&mut fds, timeout) {
361                Ok(0) => return Err(Error::Timeout),
362                Ok(_) => {}
363                Err(nix::errno::Errno::EINTR) => continue,
364                Err(errno) => return Err(std::io::Error::from(errno).into()),
365            }
366            match reader.read(&mut chunk) {
367                // The writer closed: the source has sent everything.
368                Ok(0) => return Ok(Some(data)),
369                Ok(n) => {
370                    if data.len() + n > self.max_read_bytes {
371                        return Err(Error::TooLarge {
372                            size: data.len() + n,
373                            limit: self.max_read_bytes,
374                        });
375                    }
376                    data.extend_from_slice(&chunk[..n]);
377                }
378                Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
379                Err(error) => return Err(error.into()),
380            }
381        }
382    }
383}
384
385impl Drop for DataControl {
386    fn drop(&mut self) {
387        self.stop.store(true, Ordering::Relaxed);
388        self.link.notify();
389        if let Some(thread) = self.thread.take() {
390            let _ = thread.join();
391        }
392    }
393}