clipboard_master/master/
x11.rs

1use crate::{CallbackResult, ClipboardHandler};
2
3use std::io;
4use std::sync::OnceLock;
5use std::sync::mpsc::{self, SyncSender, Receiver, sync_channel};
6
7use x11rb::protocol::xfixes;
8use x11rb::connection::Connection;
9use x11rb::protocol::xproto::ConnectionExt;
10
11///Shutdown channel
12///
13///On drop requests shutdown to gracefully close clipboard listener as soon as possible.
14pub struct Shutdown {
15    sender: SyncSender<()>,
16}
17
18impl Drop for Shutdown {
19    #[inline(always)]
20    fn drop(&mut self) {
21        let _ = self.sender.send(());
22    }
23}
24
25///Clipboard master.
26///
27///Tracks changes of clipboard and invokes corresponding callbacks.
28///
29///# Platform notes:
30///
31///- On `windows` it creates dummy window that monitors each clipboard change message.
32pub struct Master<H> {
33    handler: H,
34    sender: SyncSender<()>,
35    recv: Receiver<()>
36}
37
38impl<H: ClipboardHandler> Master<H> {
39    #[inline(always)]
40    ///Creates new instance.
41    pub fn new(handler: H) -> io::Result<Self> {
42        let (sender, recv) = sync_channel(0);
43
44        Ok(Self {
45            handler,
46            sender,
47            recv,
48        })
49    }
50
51    #[inline(always)]
52    ///Creates shutdown channel.
53    pub fn shutdown_channel(&self) -> Shutdown {
54        Shutdown {
55            sender: self.sender.clone()
56        }
57    }
58
59
60    ///Starts Master by waiting for any change
61    pub fn run(&mut self) -> io::Result<()> {
62        let clipboard = match Self::x11_clipboard() {
63            Ok(clipboard) => clipboard,
64            Err(error) => {
65                return Err(io::Error::new(
66                    io::ErrorKind::Other,
67                    format!("Failed to initialize clipboard: {:?}", error),
68                ))
69            }
70        };
71
72
73        if let Err(error) = xfixes::query_version(&clipboard.getter.connection, 5, 0) {
74            return Err(io::Error::new(io::ErrorKind::Other, error));
75        }
76
77        let mut result = Ok(());
78        'main: loop {
79            let selection = clipboard.getter.atoms.clipboard;
80
81            let screen = match clipboard.getter.connection.setup().roots.get(clipboard.getter.screen) {
82                Some(screen) => screen,
83                None => match self.handler.on_clipboard_error(io::Error::new(io::ErrorKind::Other, "Screen is not available")) {
84                    CallbackResult::Next => continue,
85                    CallbackResult::Stop => break,
86                    CallbackResult::StopWithError(error) => {
87                        result = Err(error);
88                        break;
89                    }
90                }
91            };
92
93            // Clear selection sources...
94            let cookie = xfixes::select_selection_input(
95                &clipboard.getter.connection,
96                screen.root,
97                clipboard.getter.atoms.primary,
98                xfixes::SelectionEventMask::default()
99            ).and_then(|_| xfixes::select_selection_input(
100                &clipboard.getter.connection,
101                screen.root,
102                clipboard.getter.atoms.clipboard,
103                xfixes::SelectionEventMask::default()
104            // ...and set the one requested now
105            )).and_then(|_| xfixes::select_selection_input(
106                &clipboard.getter.connection,
107                screen.root,
108                selection,
109                xfixes::SelectionEventMask::SET_SELECTION_OWNER | xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE | xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY
110            ));
111
112            if let Err(error) = clipboard.getter.connection.flush() {
113                match self.handler.on_clipboard_error(io::Error::new(io::ErrorKind::Other, error)) {
114                    CallbackResult::Next => continue,
115                    CallbackResult::Stop => break,
116                    CallbackResult::StopWithError(error) => {
117                        result = Err(error);
118                        break;
119                    }
120                }
121            }
122
123            let sequence_number = match cookie {
124                Ok(cookie) => {
125                    let sequence_number = cookie.sequence_number();
126                    if let Err(error) = cookie.check() {
127                        match self.handler.on_clipboard_error(io::Error::new(io::ErrorKind::Other, error)) {
128                            CallbackResult::Next => continue,
129                            CallbackResult::Stop => break,
130                            CallbackResult::StopWithError(error) => {
131                                result = Err(error);
132                                break;
133                            }
134                        }
135                    }
136                    sequence_number
137                },
138                Err(error) => match self.handler.on_clipboard_error(io::Error::new(io::ErrorKind::Other, error)) {
139                    CallbackResult::Next => continue,
140                    CallbackResult::Stop => break,
141                    CallbackResult::StopWithError(error) => {
142                        result = Err(error);
143                        break;
144                    }
145                }
146            };
147
148            'poll: loop {
149                match clipboard.getter.connection.poll_for_event_with_sequence() {
150                    Ok(Some((_, seq))) if seq >= sequence_number => {
151                        match self.handler.on_clipboard_change() {
152                            CallbackResult::Next => break 'poll,
153                            CallbackResult::Stop => break 'main,
154                            CallbackResult::StopWithError(error) => {
155                                result =  Err(error);
156                                break 'main;
157                            }
158                        }
159                    },
160                    Ok(_) => {
161                        match self.recv.recv_timeout(self.handler.sleep_interval()) {
162                            Ok(()) => break 'main,
163                            //timeout
164                            Err(mpsc::RecvTimeoutError::Timeout) => continue 'poll,
165                            Err(mpsc::RecvTimeoutError::Disconnected) => break 'main,
166                        }
167                    }
168                    Err(error) => {
169                        let error = io::Error::new(
170                            io::ErrorKind::Other,
171                            format!("Failed to load clipboard: {:?}", error),
172                        );
173
174                        match self.handler.on_clipboard_error(error) {
175                            CallbackResult::Next => break 'poll,
176                            CallbackResult::Stop => break 'main,
177                            CallbackResult::StopWithError(error) => {
178                                result = Err(error);
179                                break 'main;
180                            }
181                        }
182                    }
183                }
184            }
185
186            let delete = clipboard.getter.connection.delete_property(clipboard.getter.window, clipboard.getter.atoms.property)
187                                                    .map_err(|error| io::Error::new(io::ErrorKind::Other, error))
188                                                    .and_then(|cookie| cookie.check().map_err(|error| io::Error::new(io::ErrorKind::Other, error)));
189            if let Err(error) = delete {
190                match self.handler.on_clipboard_error(error) {
191                    CallbackResult::Next => (),
192                    CallbackResult::Stop => break,
193                    CallbackResult::StopWithError(error) => {
194                        result = Err(error);
195                        break;
196                    }
197                }
198            }
199
200            match self.recv.recv_timeout(self.handler.sleep_interval()) {
201                Ok(()) => break,
202                //timeout
203                Err(mpsc::RecvTimeoutError::Timeout) => continue,
204                Err(mpsc::RecvTimeoutError::Disconnected) => break,
205            }
206        }
207
208        match clipboard.getter.connection.delete_property(clipboard.getter.window, clipboard.getter.atoms.property) {
209            Ok(cookie) => match cookie.check() {
210                Ok(_) => result,
211                Err(error) => Err(io::Error::new(io::ErrorKind::Other, error)),
212            },
213            Err(error) => Err(io::Error::new(io::ErrorKind::Other, error)),
214        }
215    }
216
217    ///Gets one time initialized x11 clipboard.
218    ///
219    ///This is only available on linux
220    ///
221    ///Prefer to use it on Linux as underlying `x11-clipboard` crate has buggy dtor
222    ///and doesn't clean up all resources associated with `Clipboard`
223    ///
224    ///You should use [load](https://docs.rs/x11-clipboard/latest/x11_clipboard/struct.Clipboard.html#method.load) with no timeout on every callback call
225    pub fn x11_clipboard() -> &'static Result<x11_clipboard::Clipboard, x11_clipboard::error::Error> {
226        static CLIP: OnceLock<Result<x11_clipboard::Clipboard, x11_clipboard::error::Error>> = OnceLock::new();
227        CLIP.get_or_init(x11_clipboard::Clipboard::new)
228    }
229}