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
26pub struct Clipboard {
28 reader: Context,
29 writer: Arc<Context>,
30 selections: Arc<RwLock<HashMap<Atom, (Atom, Vec<u8>)>>>,
31}
32
33impl Clipboard {
34 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 pub fn read(&self) -> Result<String, Error> {
66 self.read_selection(self.reader.atoms.clipboard)
67 }
68
69 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 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 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 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, )?;
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 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, )
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}