clipboard_x11/
lib.rs

1#[forbid(unsafe_code)]
2mod error;
3
4pub use error::Error;
5
6use x11rb::{
7    connection::Connection as _,
8    errors::ConnectError,
9    protocol::{
10        xproto::{self, Atom, AtomEnum, EventMask, Window},
11        Event,
12    },
13    rust_connection::RustConnection as Connection,
14    wrapper::ConnectionExt,
15};
16
17use std::{
18    collections::HashMap,
19    sync::{Arc, RwLock},
20    thread,
21    time::{Duration, Instant},
22};
23
24const POLL_DURATION: std::time::Duration = Duration::from_micros(50);
25
26/// A connection to an X11 [`Clipboard`].
27pub struct Clipboard {
28    reader: Context,
29    writer: Arc<Context>,
30    selections: Arc<RwLock<HashMap<Atom, (Atom, Vec<u8>)>>>,
31}
32
33impl Clipboard {
34    /// Connect to the running X11 server and obtain a [`Clipboard`].
35    pub fn connect() -> Result<Self, Error> {
36        let reader = Context::new(None)?;
37        let writer = Arc::new(Context::new(None)?);
38        let selections = Arc::new(RwLock::new(HashMap::new()));
39
40        let worker = Worker {
41            context: Arc::clone(&writer),
42            selections: Arc::clone(&selections),
43        };
44
45        thread::spawn(move || worker.run());
46
47        Ok(Clipboard {
48            reader,
49            writer,
50            selections,
51        })
52    }
53
54    fn read_selection(&self, selection: Atom) -> Result<String, Error> {
55        Ok(String::from_utf8(self.load(
56            selection,
57            self.reader.atoms.utf8_string,
58            self.reader.atoms.property,
59            std::time::Duration::from_secs(3),
60        )?)
61        .map_err(Error::InvalidUtf8)?)
62    }
63
64    /// Read the current CLIPBOARD [`Clipboard`] value.
65    pub fn read(&self) -> Result<String, Error> {
66        self.read_selection(self.reader.atoms.clipboard)
67    }
68
69    /// Read the current PRIMARY [`Clipboard`] value.
70    pub fn read_primary(&self) -> Result<String, Error> {
71        self.read_selection(self.reader.atoms.primary)
72    }
73
74    fn write_selection(
75        &mut self,
76        selection: Atom,
77        contents: String,
78    ) -> Result<(), Error> {
79        let target = self.writer.atoms.utf8_string;
80
81        self.selections
82            .write()
83            .map_err(|_| Error::SelectionLocked)?
84            .insert(selection, (target, contents.into()));
85
86        let _ = xproto::set_selection_owner(
87            &self.writer.connection,
88            self.writer.window,
89            selection,
90            x11rb::CURRENT_TIME,
91        )?;
92
93        let _ = self.writer.connection.flush()?;
94
95        let reply =
96            xproto::get_selection_owner(&self.writer.connection, selection)
97                .map_err(Into::into)
98                .and_then(|cookie| cookie.reply())?;
99
100        if reply.owner == self.writer.window {
101            Ok(())
102        } else {
103            Err(Error::InvalidOwner)
104        }
105    }
106
107    /// Write a new value to the CLIPBOARD [`Clipboard`].
108    pub fn write(&mut self, contents: String) -> Result<(), Error> {
109        let selection = self.writer.atoms.clipboard;
110        self.write_selection(selection, contents)
111    }
112
113    /// Write a new value to the PRIMARY [`Clipboard`].
114    pub fn write_primary(&mut self, contents: String) -> Result<(), Error> {
115        let selection = self.writer.atoms.primary;
116        self.write_selection(selection, contents)
117    }
118
119    /// load value.
120    fn load(
121        &self,
122        selection: Atom,
123        target: Atom,
124        property: Atom,
125        timeout: impl Into<Option<Duration>>,
126    ) -> Result<Vec<u8>, Error> {
127        let mut buff = Vec::new();
128        let timeout = timeout.into();
129
130        let _ = xproto::convert_selection(
131            &self.reader.connection,
132            self.reader.window,
133            selection,
134            target,
135            property,
136            x11rb::CURRENT_TIME, /* FIXME ^
137                                  * Clients should not use CurrentTime for
138                                  * the time argument of a ConvertSelection
139                                  * request.
140                                  * Instead, they should use the timestamp
141                                  * of the event that caused the request to
142                                  * be made. */
143        )?;
144        let _ = self.reader.connection.flush()?;
145
146        self.process_event(&mut buff, selection, target, property, timeout)?;
147
148        let _ = xproto::delete_property(
149            &self.reader.connection,
150            self.reader.window,
151            property,
152        )?;
153        let _ = self.reader.connection.flush()?;
154
155        Ok(buff)
156    }
157
158    fn process_event<T>(
159        &self,
160        buff: &mut Vec<u8>,
161        selection: Atom,
162        target: Atom,
163        property: Atom,
164        timeout: T,
165    ) -> Result<(), Error>
166    where
167        T: Into<Option<Duration>>,
168    {
169        let mut is_incr = false;
170        let timeout = timeout.into();
171        let start_time = if timeout.is_some() {
172            Some(Instant::now())
173        } else {
174            None
175        };
176
177        loop {
178            if timeout
179                .into_iter()
180                .zip(start_time)
181                .next()
182                .map(|(timeout, time)| (Instant::now() - time) >= timeout)
183                .unwrap_or(false)
184            {
185                return Err(Error::Timeout);
186            }
187
188            let event = match self.reader.connection.poll_for_event()? {
189                Some(event) => event,
190                None => {
191                    thread::park_timeout(POLL_DURATION);
192                    continue;
193                }
194            };
195
196            match event {
197                Event::SelectionNotify(event) => {
198                    if event.selection != selection {
199                        continue;
200                    };
201
202                    // Note that setting the property argument to None indicates
203                    // that the conversion requested could
204                    // not be made.
205                    if event.property == AtomEnum::NONE.into() {
206                        break;
207                    }
208
209                    let reply = xproto::get_property(
210                        &self.reader.connection,
211                        false,
212                        self.reader.window,
213                        event.property,
214                        Atom::from(AtomEnum::ANY),
215                        buff.len() as u32,
216                        ::std::u32::MAX, // FIXME reasonable buffer size
217                    )
218                    .map_err(Into::into)
219                    .and_then(|cookie| cookie.reply())?;
220
221                    if reply.type_ == self.reader.atoms.incr {
222                        if let Some(&size) = reply.value.get(0) {
223                            buff.reserve(size as usize);
224                        }
225
226                        let _ = xproto::delete_property(
227                            &self.reader.connection,
228                            self.reader.window,
229                            property,
230                        );
231
232                        let _ = self.reader.connection.flush();
233                        is_incr = true;
234
235                        continue;
236                    } else if reply.type_ != target {
237                        return Err(Error::UnexpectedType(reply.type_));
238                    }
239
240                    buff.extend_from_slice(&reply.value);
241                    break;
242                }
243                Event::PropertyNotify(event) if is_incr => {
244                    if event.state != xproto::Property::NEW_VALUE {
245                        continue;
246                    };
247
248                    let length = xproto::get_property(
249                        &self.reader.connection,
250                        false,
251                        self.reader.window,
252                        property,
253                        Atom::from(AtomEnum::ANY),
254                        0,
255                        0,
256                    )
257                    .map_err(Into::into)
258                    .and_then(|cookie| cookie.reply())?
259                    .bytes_after;
260
261                    let reply = xproto::get_property(
262                        &self.reader.connection,
263                        true,
264                        self.reader.window,
265                        property,
266                        Atom::from(AtomEnum::ANY),
267                        0,
268                        length,
269                    )
270                    .map_err(Into::into)
271                    .and_then(|cookie| cookie.reply())?;
272
273                    if reply.type_ != target {
274                        continue;
275                    };
276
277                    if reply.value_len != 0 {
278                        buff.extend_from_slice(&reply.value);
279                    } else {
280                        break;
281                    }
282                }
283                _ => {}
284            }
285        }
286
287        Ok(())
288    }
289}
290
291pub struct Context {
292    pub connection: Connection,
293    pub screen: usize,
294    pub window: Window,
295    pub atoms: Atoms,
296}
297
298#[derive(Clone, Debug)]
299pub struct Atoms {
300    pub primary: Atom,
301    pub clipboard: Atom,
302    pub property: Atom,
303    pub targets: Atom,
304    pub string: Atom,
305    pub utf8_string: Atom,
306    pub incr: Atom,
307}
308
309#[inline]
310fn get_atom(connection: &Connection, name: &str) -> Result<Atom, Error> {
311    x11rb::protocol::xproto::intern_atom(connection, false, name.as_bytes())
312        .map_err(Into::into)
313        .and_then(|cookie| cookie.reply())
314        .map(|reply| reply.atom)
315        .map_err(Into::into)
316}
317
318impl Context {
319    pub fn new(displayname: Option<&str>) -> Result<Self, Error> {
320        let (connection, screen) = Connection::connect(displayname)?;
321        let window = connection.generate_id().map_err(|_| {
322            Error::ConnectionFailed(ConnectError::InvalidScreen)
323        })?;
324
325        {
326            let screen =
327                connection.setup().roots.get(screen as usize).ok_or(
328                    Error::ConnectionFailed(ConnectError::InvalidScreen),
329                )?;
330
331            let _ = xproto::create_window(
332                &connection,
333                x11rb::COPY_DEPTH_FROM_PARENT,
334                window,
335                screen.root,
336                0,
337                0,
338                1,
339                1,
340                0,
341                xproto::WindowClass::INPUT_OUTPUT,
342                screen.root_visual,
343                &xproto::CreateWindowAux::new().event_mask(
344                    xproto::EventMask::STRUCTURE_NOTIFY
345                        | xproto::EventMask::PROPERTY_CHANGE,
346                ),
347            )?;
348
349            let _ = connection.flush()?;
350        }
351
352        let atoms = Atoms {
353            primary: AtomEnum::PRIMARY.into(),
354            clipboard: get_atom(&connection, "CLIPBOARD")?,
355            property: get_atom(&connection, "THIS_CLIPBOARD_OUT")?,
356            targets: get_atom(&connection, "TARGETS")?,
357            string: AtomEnum::STRING.into(),
358            utf8_string: get_atom(&connection, "UTF8_STRING")?,
359            incr: get_atom(&connection, "INCR")?,
360        };
361
362        Ok(Context {
363            connection,
364            screen,
365            window,
366            atoms,
367        })
368    }
369}
370
371pub struct Worker {
372    context: Arc<Context>,
373    selections: Arc<RwLock<HashMap<Atom, (Atom, Vec<u8>)>>>,
374}
375
376impl Worker {
377    pub const INCR_CHUNK_SIZE: usize = 4000;
378
379    pub fn run(self) {
380        while let Ok(event) = self.context.connection.wait_for_event() {
381            match event {
382                Event::SelectionRequest(event) => {
383                    let selections = match self.selections.read().ok() {
384                        Some(selections) => selections,
385                        None => continue,
386                    };
387
388                    let &(target, ref value) =
389                        match selections.get(&event.selection) {
390                            Some(key_value) => key_value,
391                            None => continue,
392                        };
393
394                    if event.target == self.context.atoms.targets {
395                        let data = [self.context.atoms.targets, target];
396
397                        self.context
398                            .connection
399                            .change_property32(
400                                xproto::PropMode::REPLACE,
401                                event.requestor,
402                                event.property,
403                                xproto::AtomEnum::ATOM,
404                                &data,
405                            )
406                            .expect("Change property");
407                    } else {
408                        let _ = self
409                            .context
410                            .connection
411                            .change_property8(
412                                xproto::PropMode::REPLACE,
413                                event.requestor,
414                                event.property,
415                                target,
416                                value,
417                            )
418                            .expect("Change property");
419                    }
420
421                    let _ = xproto::send_event(
422                        &self.context.connection,
423                        false,
424                        event.requestor,
425                        EventMask::NO_EVENT,
426                        xproto::SelectionNotifyEvent {
427                            response_type: 31,
428                            sequence: event.sequence,
429                            time: event.time,
430                            requestor: event.requestor,
431                            selection: event.selection,
432                            target: event.target,
433                            property: event.property,
434                        },
435                    )
436                    .expect("Send event");
437
438                    let _ = self.context.connection.flush();
439                }
440                Event::SelectionClear(event) => {
441                    if let Ok(mut write_setmap) = self.selections.write() {
442                        write_setmap.remove(&event.selection);
443                    }
444                }
445                _ => (),
446            }
447        }
448    }
449}