Skip to main content

pixelflux/wayland/
dcclient.rs

1//! Data-control CLIENT: the native clipboard bridge to a NESTED app compositor
2//! (labwc/kwin running under pixelflux).
3//!
4//! Apps in a nested session use the inner compositor's selection, which
5//! pixelflux's own clipboard machinery never sees. selkies bridges it over the
6//! ScreenCapture ABI backed by this module instead of forking wl-copy/wl-paste
7//! per operation: one-shot [`list_types`]/[`read`], a [`write`] that returns with
8//! the selection taken (a detached thread then serves paste requests until
9//! another client takes the selection), and [`watch`] (a thread reporting
10//! selection changes to a `SelectionWatch`). The compositor is spoken to through
11//! `ext_data_control_manager_v1` (KWin since Plasma 6.3, wlroots since 0.19) or
12//! `zwlr_data_control_manager_v1` (earlier wlroots and KWin), preferring the
13//! standardized ext form when both are advertised. Every compositor round-trip
14//! is deadline-bounded via [`wlclient::bounded_roundtrip`].
15
16use std::collections::HashMap;
17use std::os::fd::AsFd;
18use std::os::unix::net::UnixStream;
19use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
20use std::sync::{Arc, Condvar, Mutex};
21use std::time::Duration;
22
23use crate::sink::SelectionWatch;
24use wayland_client::backend::ObjectId;
25use wayland_client::protocol::{wl_registry, wl_seat};
26use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
27use wayland_protocols::ext::data_control::v1::client::{
28    ext_data_control_device_v1::{self, ExtDataControlDeviceV1},
29    ext_data_control_manager_v1::ExtDataControlManagerV1,
30    ext_data_control_offer_v1::{self, ExtDataControlOfferV1},
31    ext_data_control_source_v1::{self, ExtDataControlSourceV1},
32};
33use wayland_protocols_wlr::data_control::v1::client::{
34    zwlr_data_control_device_v1::{self, ZwlrDataControlDeviceV1},
35    zwlr_data_control_manager_v1::ZwlrDataControlManagerV1,
36    zwlr_data_control_offer_v1::{self, ZwlrDataControlOfferV1},
37    zwlr_data_control_source_v1::{self, ZwlrDataControlSourceV1},
38};
39
40use crate::wayland::wlclient::{
41    IO_TIMEOUT, SyncState, bounded_roundtrip, impl_sync_callback, pipe_cloexec, read_fd_to_end,
42    wait_readable, write_fd_all,
43};
44
45/// How often a background thread wakes from its socket poll to check its stop
46/// flag, bounding unwatch/shutdown latency.
47const STOP_POLL: Duration = Duration::from_millis(500);
48
49/// The number of the last [`write`] and the mimes its selection has served in
50/// full, in order: a caller restoring the clipboard after a paste waits on it
51/// ([`served`]) for the paster's fetch instead of guessing how long it takes.
52static SERVED: Mutex<(u64, Vec<String>)> = Mutex::new((0, Vec::new()));
53static SERVED_GREW: Condvar = Condvar::new();
54/// Reads by this process in flight ([`read`]): a send answering one is not a
55/// paster's, and goes uncounted (a paste served beside one only waits longer).
56static OWN_READS: AtomicUsize = AtomicUsize::new(0);
57
58struct OwnRead;
59
60impl OwnRead {
61    fn begin() -> Self {
62        OWN_READS.fetch_add(1, Ordering::SeqCst);
63        OwnRead
64    }
65}
66
67impl Drop for OwnRead {
68    fn drop(&mut self) {
69        OWN_READS.fetch_sub(1, Ordering::SeqCst);
70    }
71}
72
73/// The two data-control protocol families behind one shape: both define the
74/// same manager/device/source/offer object graph with identical semantics, so
75/// each wrapper dispatches the one request set to whichever family the
76/// compositor advertised. Objects never mix families: everything descends from
77/// the single manager chosen at bind time.
78enum DcManager {
79    Ext(ExtDataControlManagerV1),
80    Wlr(ZwlrDataControlManagerV1),
81}
82
83enum DcDevice {
84    Ext(ExtDataControlDeviceV1),
85    Wlr(ZwlrDataControlDeviceV1),
86}
87
88enum DcSource {
89    Ext(ExtDataControlSourceV1),
90    Wlr(ZwlrDataControlSourceV1),
91}
92
93#[derive(Clone)]
94enum DcOffer {
95    Ext(ExtDataControlOfferV1),
96    Wlr(ZwlrDataControlOfferV1),
97}
98
99impl DcManager {
100    fn get_data_device(&self, seat: &wl_seat::WlSeat, qh: &QueueHandle<DcState>) -> DcDevice {
101        match self {
102            DcManager::Ext(m) => DcDevice::Ext(m.get_data_device(seat, qh, ())),
103            DcManager::Wlr(m) => DcDevice::Wlr(m.get_data_device(seat, qh, ())),
104        }
105    }
106
107    fn create_data_source(&self, qh: &QueueHandle<DcState>) -> DcSource {
108        match self {
109            DcManager::Ext(m) => DcSource::Ext(m.create_data_source(qh, ())),
110            DcManager::Wlr(m) => DcSource::Wlr(m.create_data_source(qh, ())),
111        }
112    }
113}
114
115impl DcDevice {
116    fn set_selection(&self, source: Option<&DcSource>) {
117        match self {
118            DcDevice::Ext(d) => d.set_selection(source.map(|s| match s {
119                DcSource::Ext(s) => s,
120                DcSource::Wlr(_) => unreachable!("mixed data-control families"),
121            })),
122            DcDevice::Wlr(d) => d.set_selection(source.map(|s| match s {
123                DcSource::Wlr(s) => s,
124                DcSource::Ext(_) => unreachable!("mixed data-control families"),
125            })),
126        }
127    }
128
129    fn destroy(&self) {
130        match self {
131            DcDevice::Ext(d) => d.destroy(),
132            DcDevice::Wlr(d) => d.destroy(),
133        }
134    }
135}
136
137impl DcSource {
138    fn offer(&self, mime: String) {
139        match self {
140            DcSource::Ext(s) => s.offer(mime),
141            DcSource::Wlr(s) => s.offer(mime),
142        }
143    }
144
145    fn destroy(&self) {
146        match self {
147            DcSource::Ext(s) => s.destroy(),
148            DcSource::Wlr(s) => s.destroy(),
149        }
150    }
151}
152
153impl DcOffer {
154    fn id(&self) -> ObjectId {
155        match self {
156            DcOffer::Ext(o) => o.id(),
157            DcOffer::Wlr(o) => o.id(),
158        }
159    }
160
161    fn receive(&self, mime: String, fd: std::os::fd::BorrowedFd<'_>) {
162        match self {
163            DcOffer::Ext(o) => o.receive(mime, fd),
164            DcOffer::Wlr(o) => o.receive(mime, fd),
165        }
166    }
167
168    fn destroy(&self) {
169        match self {
170            DcOffer::Ext(o) => o.destroy(),
171            DcOffer::Wlr(o) => o.destroy(),
172        }
173    }
174}
175
176#[derive(Default)]
177struct DcState {
178    seat: Option<wl_seat::WlSeat>,
179    manager_ext: Option<ExtDataControlManagerV1>,
180    manager_wlr: Option<ZwlrDataControlManagerV1>,
181    /// Advertised mimes per live offer.
182    offer_mimes: HashMap<ObjectId, Vec<String>>,
183    selection: Option<DcOffer>,
184    /// Set on every `selection` event (the watch loop's change edge).
185    selection_changed: bool,
186    /// Compositor told this device it is done (seat gone).
187    finished: bool,
188    /// The write path's source lost the selection to another client.
189    canceled: bool,
190    /// Mime -> bytes served by the write path's source.
191    serve: Vec<(String, Vec<u8>)>,
192    /// The write this source serves, as [`SERVED`] numbers it.
193    write_no: u64,
194    sync_done: bool,
195}
196
197impl SyncState for DcState {
198    fn sync_done_mut(&mut self) -> &mut bool {
199        &mut self.sync_done
200    }
201}
202impl_sync_callback!(DcState);
203
204impl DcState {
205    /// The bound manager, preferring ext over wlr; the loser is released.
206    fn take_manager(&mut self) -> Result<DcManager, String> {
207        if let Some(m) = self.manager_ext.take() {
208            if let Some(w) = self.manager_wlr.take() {
209                w.destroy();
210            }
211            return Ok(DcManager::Ext(m));
212        }
213        self.manager_wlr.take().map(DcManager::Wlr).ok_or_else(|| {
214            "app compositor advertises neither ext_data_control_manager_v1 \
215             nor zwlr_data_control_manager_v1"
216                .to_string()
217        })
218    }
219
220    fn on_data_offer(&mut self, id: ObjectId) {
221        self.offer_mimes.entry(id).or_default();
222    }
223
224    fn on_offer_mime(&mut self, id: ObjectId, mime_type: String) {
225        self.offer_mimes.entry(id).or_default().push(mime_type);
226    }
227
228    fn on_selection(&mut self, offer: Option<DcOffer>) {
229        // Replaced offers are dead objects; drop their proxy and mimes so a
230        // long-lived watch connection doesn't accumulate them.
231        if let Some(old) = self.selection.take()
232            && offer.as_ref().map(|o| o.id()) != Some(old.id())
233        {
234            self.offer_mimes.remove(&old.id());
235            old.destroy();
236        }
237        self.selection = offer;
238        self.selection_changed = true;
239    }
240
241    /// This bridge carries the regular selection only, but ext v1 still
242    /// introduces an offer per primary-selection change; release it (unless the
243    /// compositor reused the regular selection's object) so it can't pile up.
244    fn on_primary_selection(&mut self, offer: Option<DcOffer>) {
245        if let Some(o) = offer
246            && self.selection.as_ref().map(|s| s.id()) != Some(o.id())
247        {
248            self.offer_mimes.remove(&o.id());
249            o.destroy();
250        }
251    }
252
253    fn on_send(&mut self, mime_type: &str, fd: std::os::fd::OwnedFd) {
254        let sent = self
255            .serve
256            .iter()
257            .find(|(m, _)| m == mime_type)
258            .is_some_and(|(_, data)| write_fd_all(&fd, data, IO_TIMEOUT).is_ok());
259        // Taken while the fd is open: an own read cannot have returned yet.
260        let own = OWN_READS.load(Ordering::SeqCst) > 0;
261        // Closed before it counts as served, so the paster has seen EOF by then.
262        drop(fd);
263        let mut served = SERVED.lock().unwrap_or_else(|e| e.into_inner());
264        if sent && !own && served.0 == self.write_no {
265            served.1.push(mime_type.to_string());
266            SERVED_GREW.notify_all();
267        }
268    }
269}
270
271impl Dispatch<wl_registry::WlRegistry, ()> for DcState {
272    fn event(
273        state: &mut Self,
274        registry: &wl_registry::WlRegistry,
275        event: wl_registry::Event,
276        _: &(),
277        _: &Connection,
278        qh: &QueueHandle<Self>,
279    ) {
280        if let wl_registry::Event::Global {
281            name, interface, ..
282        } = event
283        {
284            // Version 1 of each suffices: the seat is only an argument, and v1
285            // data-control carries the regular selection this bridge needs.
286            match interface.as_str() {
287                "wl_seat" if state.seat.is_none() => {
288                    state.seat = Some(registry.bind(name, 1, qh, ()));
289                }
290                "ext_data_control_manager_v1" if state.manager_ext.is_none() => {
291                    state.manager_ext = Some(registry.bind(name, 1, qh, ()));
292                }
293                "zwlr_data_control_manager_v1" if state.manager_wlr.is_none() => {
294                    state.manager_wlr = Some(registry.bind(name, 1, qh, ()));
295                }
296                _ => {}
297            }
298        }
299    }
300}
301
302delegate_noop!(DcState: ignore wl_seat::WlSeat);
303delegate_noop!(DcState: ExtDataControlManagerV1);
304delegate_noop!(DcState: ZwlrDataControlManagerV1);
305
306impl Dispatch<ExtDataControlOfferV1, ()> for DcState {
307    fn event(
308        state: &mut Self,
309        offer: &ExtDataControlOfferV1,
310        event: ext_data_control_offer_v1::Event,
311        _: &(),
312        _: &Connection,
313        _: &QueueHandle<Self>,
314    ) {
315        if let ext_data_control_offer_v1::Event::Offer { mime_type } = event {
316            state.on_offer_mime(offer.id(), mime_type);
317        }
318    }
319}
320
321impl Dispatch<ZwlrDataControlOfferV1, ()> for DcState {
322    fn event(
323        state: &mut Self,
324        offer: &ZwlrDataControlOfferV1,
325        event: zwlr_data_control_offer_v1::Event,
326        _: &(),
327        _: &Connection,
328        _: &QueueHandle<Self>,
329    ) {
330        if let zwlr_data_control_offer_v1::Event::Offer { mime_type } = event {
331            state.on_offer_mime(offer.id(), mime_type);
332        }
333    }
334}
335
336impl Dispatch<ExtDataControlDeviceV1, ()> for DcState {
337    fn event(
338        state: &mut Self,
339        _: &ExtDataControlDeviceV1,
340        event: ext_data_control_device_v1::Event,
341        _: &(),
342        _: &Connection,
343        _: &QueueHandle<Self>,
344    ) {
345        match event {
346            ext_data_control_device_v1::Event::DataOffer { id } => {
347                state.on_data_offer(id.id());
348            }
349            ext_data_control_device_v1::Event::Selection { id } => {
350                state.on_selection(id.map(DcOffer::Ext));
351            }
352            ext_data_control_device_v1::Event::PrimarySelection { id } => {
353                state.on_primary_selection(id.map(DcOffer::Ext));
354            }
355            ext_data_control_device_v1::Event::Finished => {
356                state.finished = true;
357            }
358            _ => {}
359        }
360    }
361
362    wayland_client::event_created_child!(DcState, ExtDataControlDeviceV1, [
363        ext_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ExtDataControlOfferV1, ()),
364    ]);
365}
366
367impl Dispatch<ZwlrDataControlDeviceV1, ()> for DcState {
368    fn event(
369        state: &mut Self,
370        _: &ZwlrDataControlDeviceV1,
371        event: zwlr_data_control_device_v1::Event,
372        _: &(),
373        _: &Connection,
374        _: &QueueHandle<Self>,
375    ) {
376        match event {
377            zwlr_data_control_device_v1::Event::DataOffer { id } => {
378                state.on_data_offer(id.id());
379            }
380            zwlr_data_control_device_v1::Event::Selection { id } => {
381                state.on_selection(id.map(DcOffer::Wlr));
382            }
383            zwlr_data_control_device_v1::Event::Finished => {
384                state.finished = true;
385            }
386            _ => {}
387        }
388    }
389
390    wayland_client::event_created_child!(DcState, ZwlrDataControlDeviceV1, [
391        zwlr_data_control_device_v1::EVT_DATA_OFFER_OPCODE => (ZwlrDataControlOfferV1, ()),
392    ]);
393}
394
395impl Dispatch<ExtDataControlSourceV1, ()> for DcState {
396    fn event(
397        state: &mut Self,
398        _: &ExtDataControlSourceV1,
399        event: ext_data_control_source_v1::Event,
400        _: &(),
401        _: &Connection,
402        _: &QueueHandle<Self>,
403    ) {
404        match event {
405            ext_data_control_source_v1::Event::Send { mime_type, fd } => {
406                state.on_send(&mime_type, fd);
407            }
408            ext_data_control_source_v1::Event::Cancelled => {
409                state.canceled = true;
410            }
411            _ => {}
412        }
413    }
414}
415
416impl Dispatch<ZwlrDataControlSourceV1, ()> for DcState {
417    fn event(
418        state: &mut Self,
419        _: &ZwlrDataControlSourceV1,
420        event: zwlr_data_control_source_v1::Event,
421        _: &(),
422        _: &Connection,
423        _: &QueueHandle<Self>,
424    ) {
425        match event {
426            zwlr_data_control_source_v1::Event::Send { mime_type, fd } => {
427                state.on_send(&mime_type, fd);
428            }
429            zwlr_data_control_source_v1::Event::Cancelled => {
430                state.canceled = true;
431            }
432            _ => {}
433        }
434    }
435}
436
437/// Connect to `socket_path` and return (connection, queue, state, device) with
438/// the current selection already delivered.
439fn open_device(
440    socket_path: &str,
441) -> Result<
442    (
443        Connection,
444        wayland_client::EventQueue<DcState>,
445        DcState,
446        DcDevice,
447    ),
448    String,
449> {
450    let stream =
451        UnixStream::connect(socket_path).map_err(|e| format!("connect {socket_path}: {e}"))?;
452    let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
453    let mut queue = conn.new_event_queue();
454    let qh = queue.handle();
455    let _registry = conn.display().get_registry(&qh, ());
456    let mut state = DcState::default();
457    bounded_roundtrip(&conn, &mut queue, &mut state)?;
458    let seat = state
459        .seat
460        .clone()
461        .ok_or("app compositor advertises no wl_seat")?;
462    let manager = state.take_manager()?;
463    let device = manager.get_data_device(&seat, &qh);
464    bounded_roundtrip(&conn, &mut queue, &mut state)?;
465    Ok((conn, queue, state, device))
466}
467
468/// Mimes offered by the current selection (empty when nothing is copied).
469pub(crate) fn list_types(socket_path: &str) -> Result<Vec<String>, String> {
470    let (_conn, _queue, state, device) = open_device(socket_path)?;
471    let out = state
472        .selection
473        .as_ref()
474        .and_then(|o| state.offer_mimes.get(&o.id()).cloned())
475        .unwrap_or_default();
476    device.destroy();
477    Ok(out)
478}
479
480/// The current selection's payload for `mime`, or None when there is no
481/// selection or it does not offer that mime.
482pub(crate) fn read(socket_path: &str, mime: &str) -> Result<Option<Vec<u8>>, String> {
483    let (conn, mut queue, mut state, device) = open_device(socket_path)?;
484    let Some(offer) = state.selection.clone() else {
485        device.destroy();
486        return Ok(None);
487    };
488    let offered = state
489        .offer_mimes
490        .get(&offer.id())
491        .is_some_and(|m| m.iter().any(|x| x == mime));
492    if !offered {
493        device.destroy();
494        return Ok(None);
495    }
496    let (rd, wr) = pipe_cloexec()?;
497    let _own = OwnRead::begin();
498    offer.receive(mime.to_string(), wr.as_fd());
499    queue.flush().map_err(|e| format!("flush: {e}"))?;
500    drop(wr);
501    // The source app writes into the pipe as it pleases; dispatch is not needed
502    // for the bytes, only the fd read.
503    let data = read_fd_to_end(&rd, IO_TIMEOUT)?;
504    let _ = bounded_roundtrip(&conn, &mut queue, &mut state);
505    device.destroy();
506    Ok(Some(data))
507}
508
509/// Take the selection, serving `entries` (mime, bytes) to every paster from a
510/// detached thread until another client takes the selection (or the compositor
511/// goes away). The selection is compositor-acknowledged when this returns, so a
512/// caller may immediately trigger a paste against it. Replacing a previous
513/// write is implicit: the compositor cancels the old source when the new one
514/// takes the selection.
515pub(crate) fn write(socket_path: &str, entries: Vec<(String, Vec<u8>)>) -> Result<(), String> {
516    let stream =
517        UnixStream::connect(socket_path).map_err(|e| format!("connect {socket_path}: {e}"))?;
518    let conn = Connection::from_socket(stream).map_err(|e| format!("wayland setup: {e}"))?;
519    let mut queue = conn.new_event_queue();
520    let qh = queue.handle();
521    let _registry = conn.display().get_registry(&qh, ());
522    let mut state = DcState::default();
523    bounded_roundtrip(&conn, &mut queue, &mut state)?;
524    let seat = state
525        .seat
526        .clone()
527        .ok_or("app compositor advertises no wl_seat")?;
528    let manager = state.take_manager()?;
529    let device = manager.get_data_device(&seat, &qh);
530    let source = manager.create_data_source(&qh);
531    for (mime, _) in &entries {
532        source.offer(mime.clone());
533    }
534    state.serve = entries;
535    state.write_no = {
536        let mut served = SERVED.lock().unwrap_or_else(|e| e.into_inner());
537        served.0 += 1;
538        served.1.clear();
539        served.0
540    };
541    device.set_selection(Some(&source));
542    bounded_roundtrip(&conn, &mut queue, &mut state)?;
543    std::thread::Builder::new()
544        .name("pf-dc-selection".into())
545        .spawn(move || {
546            if let Err(e) = serve_selection(conn, queue, state, device, source) {
547                eprintln!("[Clipboard] app-compositor selection serve ended: {e}");
548            }
549        })
550        .map_err(|e| format!("spawn: {e}"))?;
551    Ok(())
552}
553
554fn serve_selection(
555    conn: Connection,
556    mut queue: wayland_client::EventQueue<DcState>,
557    mut state: DcState,
558    device: DcDevice,
559    source: DcSource,
560) -> Result<(), String> {
561    while !state.canceled && !state.finished {
562        // Sends are served inside dispatch; block until the compositor has
563        // something (with a poll so a dead compositor can't pin the thread).
564        queue.flush().map_err(|e| format!("flush: {e}"))?;
565        if let Some(guard) = conn.prepare_read() {
566            use std::os::fd::AsRawFd;
567            if wait_readable(guard.connection_fd().as_raw_fd(), STOP_POLL)? {
568                guard.read().map_err(|e| format!("read: {e}"))?;
569            }
570        }
571        queue
572            .dispatch_pending(&mut state)
573            .map_err(|e| format!("dispatch: {e}"))?;
574    }
575    source.destroy();
576    device.destroy();
577    let _ = queue.flush();
578    Ok(())
579}
580
581/// The mimes the last [`write`]'s selection has served, once it has served more
582/// than `after` of them or `timeout` has passed.
583pub(crate) fn served(after: usize, timeout: Duration) -> Vec<String> {
584    let served = SERVED.lock().unwrap_or_else(|e| e.into_inner());
585    let (served, _) = SERVED_GREW
586        .wait_timeout_while(served, timeout, |s| s.1.len() <= after)
587        .unwrap_or_else(|e| e.into_inner());
588    served.1.clone()
589}
590
591/// Drop the selection (the compositor also cancels whatever source held it).
592pub(crate) fn clear(socket_path: &str) -> Result<(), String> {
593    let (conn, mut queue, mut state, device) = open_device(socket_path)?;
594    device.set_selection(None);
595    bounded_roundtrip(&conn, &mut queue, &mut state)?;
596    device.destroy();
597    Ok(())
598}
599
600struct WatchHandle {
601    stop: Arc<AtomicBool>,
602}
603
604static WATCHERS: Mutex<Option<HashMap<String, WatchHandle>>> = Mutex::new(None);
605
606/// Report every selection change on `socket_path` (including the one current at
607/// start) to `callback(mimes: list[str])` from a background thread, a cleared
608/// selection as no mimes. A second watch on the same socket replaces the first.
609pub(crate) fn watch(socket_path: &str, callback: Arc<dyn SelectionWatch>) -> Result<(), String> {
610    let stop = Arc::new(AtomicBool::new(false));
611    {
612        let mut reg = WATCHERS.lock().unwrap();
613        let map = reg.get_or_insert_with(HashMap::new);
614        if let Some(old) = map.insert(socket_path.to_string(), WatchHandle { stop: stop.clone() }) {
615            old.stop.store(true, Ordering::Relaxed);
616        }
617    }
618    let path = socket_path.to_string();
619    std::thread::Builder::new()
620        .name("pf-dc-watch".into())
621        .spawn(move || {
622            if let Err(e) = watch_loop(&path, callback, &stop) {
623                eprintln!("[Clipboard] app-compositor watch ended: {e}");
624            }
625        })
626        .map_err(|e| format!("spawn: {e}"))?;
627    Ok(())
628}
629
630/// Stop every clipboard watch (process teardown sweep; watches are not tied to
631/// captures, so the global stop helper must reach them explicitly).
632pub(crate) fn unwatch_all() {
633    let mut reg = WATCHERS.lock().unwrap();
634    if let Some(map) = reg.as_mut() {
635        for (_, handle) in map.drain() {
636            handle.stop.store(true, Ordering::Relaxed);
637        }
638    }
639}
640
641/// Stop the watch on `socket_path` (no-op when none is running).
642pub(crate) fn unwatch(socket_path: &str) {
643    let mut reg = WATCHERS.lock().unwrap();
644    if let Some(map) = reg.as_mut()
645        && let Some(handle) = map.remove(socket_path)
646    {
647        handle.stop.store(true, Ordering::Relaxed);
648    }
649}
650
651fn watch_loop(
652    socket_path: &str,
653    callback: Arc<dyn SelectionWatch>,
654    stop: &AtomicBool,
655) -> Result<(), String> {
656    let (conn, mut queue, mut state, device) = open_device(socket_path)?;
657    while !stop.load(Ordering::Relaxed) && !state.finished {
658        if state.selection_changed {
659            state.selection_changed = false;
660            let mimes = state
661                .selection
662                .as_ref()
663                .and_then(|o| state.offer_mimes.get(&o.id()).cloned())
664                .unwrap_or_default();
665            if crate::SHUTDOWN.load(Ordering::Relaxed) {
666                break;
667            }
668            callback.mimes(mimes);
669        }
670        queue.flush().map_err(|e| format!("flush: {e}"))?;
671        if let Some(guard) = conn.prepare_read() {
672            use std::os::fd::AsRawFd;
673            if wait_readable(guard.connection_fd().as_raw_fd(), STOP_POLL)? {
674                guard.read().map_err(|e| format!("read: {e}"))?;
675            }
676        }
677        queue
678            .dispatch_pending(&mut state)
679            .map_err(|e| format!("dispatch: {e}"))?;
680    }
681    device.destroy();
682    let _ = queue.flush();
683    Ok(())
684}