Skip to main content

wl_clipboard_rs/
watch.rs

1//! Watching the clipboard for selection changes with a [`Watcher`].
2
3use std::collections::{HashMap, VecDeque};
4use std::ffi::OsString;
5use std::io;
6use std::os::fd::AsFd;
7use std::sync::Arc;
8
9use os_pipe::{pipe, PipeReader, PipeWriter};
10use rustix::event::{PollFd, PollFlags};
11use wayland_backend::client::WaylandError;
12use wayland_client::globals::GlobalListContents;
13use wayland_client::protocol::wl_registry::WlRegistry;
14use wayland_client::protocol::wl_seat::WlSeat;
15use wayland_client::{delegate_dispatch, event_created_child, Dispatch, EventQueue};
16
17use crate::common::{self, initialize};
18use crate::data_control::{self, impl_dispatch_device, impl_dispatch_manager, impl_dispatch_offer};
19use crate::paste::{self, Error, Seat};
20
21struct State {
22    common: common::State,
23    // Maps each newly introduced offer to its advertised MIME types, populated as Offer events
24    // arrive. The Selection event that follows moves the entry into its `SelectionEvent`.
25    offers: HashMap<data_control::Offer, Vec<String>>,
26    selection_events: VecDeque<SelectionEvent>,
27}
28
29impl State {
30    fn got_primary_selection(&self) -> bool {
31        self.selection_events
32            .iter()
33            .any(|event| event.clipboard == paste::ClipboardType::Primary)
34    }
35
36    fn push_selection_event(
37        &mut self,
38        clipboard: paste::ClipboardType,
39        seat: &WlSeat,
40        offer: Option<data_control::Offer>,
41    ) {
42        let offer = offer.map(|offer| {
43            // Each offer belongs to exactly one selection event, so take its MIME types here.
44            let mime_types = self.offers.remove(&offer).unwrap_or_default();
45            SelectionOffer {
46                offer: OwnedOffer(offer),
47                mime_types,
48            }
49        });
50        self.selection_events.push_back(SelectionEvent {
51            clipboard,
52            seat: seat.clone(),
53            offer,
54        });
55    }
56}
57
58/// Pending selection event to report from the watch loop.
59struct SelectionEvent {
60    clipboard: paste::ClipboardType,
61    seat: WlSeat,
62    offer: Option<SelectionOffer>,
63}
64
65struct SelectionOffer {
66    offer: OwnedOffer,
67    mime_types: Vec<String>,
68}
69
70// Not `Clone`: dropping it destroys the underlying Wayland object.
71struct OwnedOffer(data_control::Offer);
72
73impl Drop for OwnedOffer {
74    fn drop(&mut self) {
75        self.0.destroy();
76    }
77}
78
79delegate_dispatch!(State: [WlSeat: ()] => common::State);
80
81impl AsMut<common::State> for State {
82    fn as_mut(&mut self) -> &mut common::State {
83        &mut self.common
84    }
85}
86
87impl Dispatch<WlRegistry, GlobalListContents> for State {
88    fn event(
89        _state: &mut Self,
90        _proxy: &WlRegistry,
91        _event: <WlRegistry as wayland_client::Proxy>::Event,
92        _data: &GlobalListContents,
93        _conn: &wayland_client::Connection,
94        _qhandle: &wayland_client::QueueHandle<Self>,
95    ) {
96    }
97}
98
99impl_dispatch_manager!(State);
100
101impl_dispatch_device!(State, WlSeat, |state: &mut Self, event, seat: &WlSeat| {
102    match event {
103        Event::DataOffer { id } => {
104            let offer = data_control::Offer::from(id);
105            state.offers.insert(offer, Vec::new());
106        }
107        Event::Selection { id } => {
108            state.push_selection_event(
109                paste::ClipboardType::Regular,
110                seat,
111                id.map(data_control::Offer::from),
112            );
113        }
114        Event::Finished => {
115            // Destroy the device stored in the seat as it's no longer valid.
116            let seat_data = state.common.seats.get_mut(seat).unwrap();
117            seat_data.set_device(None);
118        }
119        Event::PrimarySelection { id } => {
120            state.push_selection_event(
121                paste::ClipboardType::Primary,
122                seat,
123                id.map(data_control::Offer::from),
124            );
125        }
126        _ => (),
127    }
128});
129
130impl_dispatch_offer!(State, |state: &mut Self,
131                             offer: data_control::Offer,
132                             event| {
133    if let Event::Offer { mime_type } = event {
134        // The protocol says all offer events come before the selection event that hands the
135        // offer out, but KWin's XWayland clipboard bridge can send one more afterwards (e.g.
136        // SAVE_TARGETS). By then push_selection_event() has already taken the offer out of
137        // `offers`, so ignore late MIME types instead of panicking.
138        if let Some(mime_types) = state.offers.get_mut(&offer) {
139            mime_types.push(mime_type);
140        }
141    }
142});
143
144/// Handle used to stop a running [`Watcher`] from another thread.
145///
146/// Obtain one with [`Watcher::cancel_handle`]. Clone-able and safe to send across threads.
147#[derive(Clone)]
148pub struct CancelHandle(Arc<PipeWriter>);
149
150impl CancelHandle {
151    /// Signals the associated [`Watcher`] to stop.
152    ///
153    /// Returns immediately; the watcher exits before its next blocking wait.
154    pub fn cancel(&self) {
155        let _ = rustix::io::write(&*self.0, &[0u8]);
156    }
157}
158
159/// The clipboard to watch.
160#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, PartialOrd, Ord, Default)]
161pub enum ClipboardType {
162    /// The regular clipboard.
163    #[default]
164    Regular,
165    /// The "primary" clipboard.
166    ///
167    /// Watching the "primary" clipboard requires the compositor to support ext-data-control,
168    /// or wlr-data-control version 2 or above.
169    Primary,
170    /// Watch both clipboards at once.
171    ///
172    /// Each [`ClipboardEvent`] reports which clipboard it came from. This option requires the
173    /// "primary" clipboard to be supported.
174    Both,
175}
176
177impl ClipboardType {
178    fn includes(self, clipboard: paste::ClipboardType) -> bool {
179        match self {
180            ClipboardType::Regular => clipboard == paste::ClipboardType::Regular,
181            ClipboardType::Primary => clipboard == paste::ClipboardType::Primary,
182            ClipboardType::Both => true,
183        }
184    }
185}
186
187impl From<paste::ClipboardType> for ClipboardType {
188    fn from(clipboard: paste::ClipboardType) -> Self {
189        match clipboard {
190            paste::ClipboardType::Regular => ClipboardType::Regular,
191            paste::ClipboardType::Primary => ClipboardType::Primary,
192        }
193    }
194}
195
196/// A clipboard selection event reported by [`Watcher::next_event`].
197pub enum ClipboardEvent<'a> {
198    /// The selection has changed.
199    Changed {
200        /// The clipboard whose selection changed.
201        clipboard: paste::ClipboardType,
202        /// The offered MIME types in protocol order.
203        mime_types: Vec<String>,
204        /// The offer used to receive the data.
205        offer: Offer<'a>,
206    },
207    /// The selection was cleared.
208    Cleared {
209        /// The clipboard that was cleared.
210        clipboard: paste::ClipboardType,
211    },
212}
213
214/// Watches the clipboard for selection changes.
215///
216/// Construct one with [`Watcher::new`], then drive it by calling [`Watcher::next_event`] in a
217/// loop. The first call reports the current selection state; subsequent calls block until the
218/// selection changes again.
219///
220/// Seats are resolved when the watcher starts, so passing [`Seat::Unspecified`] will use the
221/// first seat found on construction rather than re-enumerating seats as offers come in.
222pub struct Watcher {
223    queue: EventQueue<State>,
224    state: State,
225    clipboard: ClipboardType,
226    // The single seat whose selections we report, resolved at construction.
227    watched: WlSeat,
228    // Cancellation pipe: the read end is polled in `wait`; the write end is handed out as a
229    // `CancelHandle` and becomes readable once `CancelHandle::cancel` is called.
230    cancel_read: PipeReader,
231    cancel_write: Arc<PipeWriter>,
232}
233
234impl Watcher {
235    /// Starts watching the clipboard.
236    ///
237    /// Returns an error immediately if there are no seats, the requested seat or protocol is
238    /// missing, or primary selection was requested but is unsupported.
239    pub fn new(clipboard: ClipboardType, seat: Seat<'_>) -> Result<Self, Error> {
240        Self::with_socket(clipboard, seat, None)
241    }
242
243    // The internal constructor accepts the socket name, used for tests.
244    pub(crate) fn with_socket(
245        clipboard: ClipboardType,
246        seat: Seat<'_>,
247        socket_name: Option<OsString>,
248    ) -> Result<Self, Error> {
249        let (mut queue, mut common) = initialize(clipboard != ClipboardType::Regular, socket_name)?;
250
251        if common.seats.is_empty() {
252            return Err(Error::NoSeats);
253        }
254
255        for (seat, data) in &mut common.seats {
256            let device =
257                common
258                    .clipboard_manager
259                    .get_data_device(seat, &queue.handle(), seat.clone());
260            data.set_device(Some(device));
261        }
262
263        let mut state = State {
264            common,
265            offers: HashMap::new(),
266            selection_events: VecDeque::new(),
267        };
268
269        queue
270            .roundtrip(&mut state)
271            .map_err(Error::WaylandCommunication)?;
272
273        if clipboard != ClipboardType::Regular && !state.got_primary_selection() {
274            return Err(Error::PrimarySelectionUnsupported);
275        }
276
277        let seats = &state.common.seats;
278        let watched = match seat {
279            Seat::Unspecified => seats.keys().next().cloned(),
280            Seat::Specific(name) => seats
281                .iter()
282                .find(|(_, data)| data.name.as_deref() == Some(name))
283                .map(|(seat, _)| seat.clone()),
284        };
285        let Some(watched) = watched else {
286            return Err(Error::SeatNotFound);
287        };
288
289        let (cancel_read, cancel_write) = pipe().map_err(Error::PipeCreation)?;
290
291        Ok(Watcher {
292            queue,
293            state,
294            clipboard,
295            watched,
296            cancel_read,
297            cancel_write: Arc::new(cancel_write),
298        })
299    }
300
301    /// Returns a handle that stops this watcher when [`CancelHandle::cancel`] is called.
302    ///
303    /// The handle can be cloned and sent to another thread to interrupt a [`Watcher::next_event`]
304    /// call that is blocked waiting for the next selection change.
305    pub fn cancel_handle(&self) -> CancelHandle {
306        CancelHandle(Arc::clone(&self.cancel_write))
307    }
308
309    /// Blocks until the next selection event for the watched clipboard and seat.
310    ///
311    /// On success yields the [`ClipboardEvent`] and an [`Offer`] to read its contents from.
312    /// Returns `Ok(None)` if cancelled via [`CancelHandle::cancel`], or `Err` on a Wayland
313    /// communication failure.
314    pub fn next_event<'a>(&'a mut self) -> Result<Option<ClipboardEvent<'a>>, Error> {
315        while !self.front_matches() {
316            if self.wait()? {
317                return Ok(None);
318            }
319        }
320        Ok(Some(self.take_front_event()))
321    }
322
323    // Discards leading events for other clipboards/seats, returning whether a matching event is now
324    // at the front of the queue.
325    fn front_matches(&mut self) -> bool {
326        while let Some(event) = self.state.selection_events.front() {
327            if self.clipboard.includes(event.clipboard) && event.seat == self.watched {
328                return true;
329            }
330            self.state.selection_events.pop_front();
331        }
332        false
333    }
334
335    // Removes the front event, returning its kind and an [`Offer`] to receive from. Only call when
336    // `front_matches` returned `true`.
337    fn take_front_event<'a>(&'a mut self) -> ClipboardEvent<'a> {
338        let SelectionEvent {
339            clipboard, offer, ..
340        } = self.state.selection_events.pop_front().unwrap();
341        match offer {
342            Some(SelectionOffer { offer, mime_types }) => ClipboardEvent::Changed {
343                clipboard,
344                mime_types,
345                offer: Offer {
346                    watcher: self,
347                    offer,
348                },
349            },
350            None => ClipboardEvent::Cleared { clipboard },
351        }
352    }
353
354    // Blocks until more Wayland events arrive (or cancellation). Returns `Ok(true)` if cancelled.
355    fn wait(&mut self) -> Result<bool, Error> {
356        self.queue
357            .flush()
358            .map_err(|e| Error::WaylandCommunication(e.into()))?;
359
360        if let Some(guard) = self.queue.prepare_read() {
361            let wayland_fd = guard.connection_fd();
362            let mut poll_fds = [
363                PollFd::new(&wayland_fd, PollFlags::IN | PollFlags::ERR),
364                PollFd::new(&self.cancel_read, PollFlags::IN),
365            ];
366            loop {
367                match rustix::event::poll(&mut poll_fds, None) {
368                    Ok(_) => break,
369                    Err(rustix::io::Errno::INTR) => continue,
370                    Err(e) => {
371                        return Err(Error::WaylandCommunication(
372                            WaylandError::Io(e.into()).into(),
373                        ))
374                    }
375                }
376            }
377            // Got data on the cancel pipe, bail with `true`.
378            if poll_fds[1].revents().contains(PollFlags::IN) {
379                return Ok(true);
380            }
381            match guard.read() {
382                Ok(_) => {}
383                Err(WaylandError::Io(e)) if e.kind() == io::ErrorKind::WouldBlock => {}
384                Err(e) => return Err(Error::WaylandCommunication(e.into())),
385            }
386        }
387
388        self.queue
389            .dispatch_pending(&mut self.state)
390            .map_err(Error::WaylandCommunication)?;
391        Ok(false)
392    }
393}
394
395/// The data offer accompanying a [`ClipboardEvent`], borrowed from its [`Watcher`].
396pub struct Offer<'a> {
397    watcher: &'a mut Watcher,
398    offer: OwnedOffer,
399}
400
401impl Offer<'_> {
402    /// Reads the clipboard content of the given MIME type into a pipe.
403    ///
404    /// Returns `Err(Error::ClipboardEmpty)` on a [`ClipboardEvent::Cleared`] event.
405    pub fn receive(&mut self, mime_type: &str) -> Result<PipeReader, Error> {
406        let (read, write) = pipe().map_err(Error::PipeCreation)?;
407        self.offer.0.receive(mime_type.to_string(), write.as_fd());
408        drop(write);
409        self.watcher
410            .queue
411            .flush()
412            .map_err(|e| Error::WaylandCommunication(e.into()))?;
413        Ok(read)
414    }
415}