1 /*
2 SPDX-License-Identifier: Apache-2.0 OR MIT
3
4 Copyright 2020 The arboard contributors
5
6 The project to which this file belongs is licensed under either of
7 the Apache 2.0 or the MIT license at the licensee's choice. The terms
8 and conditions of the chosen license apply to this file.
9 */
10
11 //!
12 //!
13 //! This implementation is a port of https://github.com/dacap/clip to Rust
14 //! The structure of the original is more or less maintained.
15 //!
16 //! Disclaimer: The original C++ code is well organized and feels clean but it relies on C++
17 //! allowing a liberal data sharing between threads and it is painfully obvious from certain parts
18 //! of this port that this code was not designed for Rust. It should probably be reworked because
19 //! the absolute plague that the Arc<Mutex<>> objects are in this code is horrible just to look at
20 //! and will forever haunt me in my nightmares.
21 //!
22 //! Most changes are to conform with Rusts rules for example there are multiple overloads of
23 //! the `get_atom` functtion in the original but there's no function overloading in Rust so
24 //! those are split apart into functions with different names. (`get_atom_by_id` and the other one
25 //! at the time of writing I haven't needed to use)
26 //!
27 //! More noteably the `Manager` class had to be split into mutliple `structs` and some member
28 //! functions were made into global functions to conform Rust's aliasing rules.
29 //! Furthermore the signature of many functions was changed to follow a simple locking philosophy;
30 //! namely that the mutex gets locked at the topmost level possible and then most functions don't
31 //! need to attempt to lock, instead they just use the direct object references passed on as arguments.
32 //!
33 //!
34
35 use std::collections::BTreeMap;
36 use std::sync::{Arc, Condvar, Mutex, MutexGuard};
37 use std::time::Duration;
38
39 use lazy_static::lazy_static;
40 use x11rb::protocol::xproto;
41 use x11rb::{
42 connection::Connection,
43 protocol::{
44 xproto::{
45 Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, GetPropertyReply,
46 PropMode, Property, PropertyNotifyEvent, SelectionClearEvent, SelectionNotifyEvent,
47 SelectionRequestEvent, Time, Window, WindowClass,
48 },
49 Event,
50 },
51 rust_connection::RustConnection,
52 wrapper::ConnectionExt as _,
53 };
54
55 use crate::common_linux::encode_as_png;
56
57 use super::common::{Error, ImageData};
58
59 x11rb::atom_manager! {
60 pub CommonAtoms: CommonAtomCookies {
61 ATOM,
62 INCR,
63 TARGETS,
64 CLIPBOARD,
65 MIME_IMAGE_PNG: b"image/png",
66 ATOM_PAIR,
67 SAVE_TARGETS,
68 MULTIPLE,
69 CLIPBOARD_MANAGER,
70 }
71 }
72
73 x11rb::atom_manager! {
74 pub TextAtoms: TextAtomCookies {
75 UTF8_STRING,
76 TEXT_PLAIN_1: b"text/plain;charset=utf-8",
77 TEXT_PLAIN_2: b"text/plain;charset=UTF-8",
78 // ANSI C strings?
79 STRING,
80 TEXT,
81 TEXT_PLAIN_0: b"text/plain",
82 }
83 }
84
85 type BufferPtr = Option<Arc<Mutex<Vec<u8>>>>;
86 type Atoms = Vec<Atom>;
87 type NotifyCallback = Option<Arc<dyn (Fn(&BufferPtr) -> bool) + Send + Sync + 'static>>;
88
89 lazy_static! {
90 static ref LOCKED_OBJECTS: Arc<Mutex<Option<LockedObjects>>> = Arc::new(Mutex::new(None));
91
92 // Used to wait/notify the arrival of the SelectionNotify event when
93 // we requested the clipboard content from other selection owner.
94 static ref CONDVAR: Condvar = Condvar::new();
95 }
96
97 struct LockedObjects {
98 shared: SharedState,
99 manager: Manager,
100 }
101
102 impl LockedObjects {
new() -> Result<LockedObjects, Error>103 fn new() -> Result<LockedObjects, Error> {
104 let (connection, screen) = RustConnection::connect(None).unwrap();
105 match Manager::new(&connection, screen) {
106 Ok(manager) => {
107 //unsafe { libc::atexit(Manager::destruct); }
108 Ok(LockedObjects {
109 shared: SharedState {
110 conn: Some(Arc::new(connection)),
111 common_atoms: Default::default(),
112 text_atoms: Default::default(),
113 },
114 manager,
115 })
116 }
117 Err(e) => Err(e),
118 }
119 }
120 }
121
122 /// The name indicates that objects in this struct are shared between
123 /// the event processing thread and the user tread. However it's important
124 /// that the `Manager` itself is also shared. So the real reason for splitting these
125 /// apart from the `Manager` is to conform to Rust's aliasing rules but that is hard to
126 /// convey in a short name.
127 struct SharedState {
128 conn: Option<Arc<RustConnection>>,
129
130 // Cache of common used atoms by us
131 common_atoms: Option<CommonAtoms>,
132
133 // Cache of atoms related to text or image content
134 text_atoms: Option<TextAtoms>,
135 //image_atoms: Atoms,
136 }
137
138 impl SharedState {
common_atoms(&mut self) -> CommonAtoms139 fn common_atoms(&mut self) -> CommonAtoms {
140 self.common_atoms.unwrap_or_else(|| {
141 CommonAtoms::new(self.conn.as_ref().unwrap().as_ref()).unwrap().reply().unwrap()
142 })
143 }
144
text_atoms(&mut self) -> Atoms145 fn text_atoms(&mut self) -> Atoms {
146 let atoms = self.text_atoms.unwrap_or_else(|| {
147 TextAtoms::new(self.conn.as_ref().unwrap().as_ref()).unwrap().reply().unwrap()
148 });
149
150 vec![
151 atoms.UTF8_STRING,
152 atoms.TEXT_PLAIN_1,
153 atoms.TEXT_PLAIN_2,
154 atoms.STRING,
155 atoms.TEXT,
156 atoms.TEXT_PLAIN_0,
157 ]
158 }
159 }
160
161 struct Manager {
162 // Temporal background window used to own the clipboard and process
163 // all events related about the clipboard in a background thread
164 window: Window,
165
166 // Thread used to run a background message loop to wait X11 events
167 // about clipboard. The X11 selection owner will be a hidden window
168 // created by us just for the clipboard purpose/communication.
169 thread_handle: Option<std::thread::JoinHandle<()>>,
170
171 // WARNING: The callback must not attempt to lock the manager or the shared state.
172 // (Otherwise the code needs to be restructured slightly)
173 //
174 // Internal callback used when a SelectionNotify is received (or the
175 // whole data content is received by the INCR method). So this
176 // callback can use the notification by different purposes (e.g. get
177 // the data length only, or get/process the data content, etc.).
178 callback: NotifyCallback,
179
180 // Result returned by the m_callback. Used as return value in the
181 // get_data_from_selection_owner() function. For example, if the
182 // callback must read a "image/png" file from the clipboard data and
183 // fails, the callback can return false and finally the get_image()
184 // will return false (i.e. there is data, but it's not a valid image
185 // format).
186 callback_result: bool,
187
188 // Actual clipboard data generated by us (when we "copy" content in
189 // the clipboard, it means that we own the X11 "CLIPBOARD"
190 // selection, and in case of SelectionRequest events, we've to
191 // return the data stored in this "m_data" field)
192 data: BTreeMap<Atom, BufferPtr>,
193
194 // Copied image in the clipboard. As we have to transfer the image
195 // in some specific format (e.g. image/png) we want to keep a copy
196 // of the image and make the conversion when the clipboard data is
197 // requested by other process.
198 image: super::common::ImageData<'static>,
199
200 // True if we have received an INCR notification so we're going to
201 // process several PropertyNotify to concatenate all data chunks.
202 incr_process: bool,
203
204 /// Variable used to wait more time if we've received an INCR
205 /// notification, which means that we're going to receive large
206 /// amounts of data from the selection owner.
207 ///mutable bool m_incr_received;
208 incr_received: bool,
209
210 // Target/selection format used in the SelectionNotify. Used in the
211 // INCR method to get data from the same property in the same format
212 // (target) on each PropertyNotify.
213 target_atom: Atom,
214
215 // Each time we receive data from the selection owner, we put that
216 // data in this buffer. If we get the data with the INCR method,
217 // we'll concatenate chunks of data in this buffer to complete the
218 // whole clipboard content.
219 reply_data: BufferPtr,
220
221 // Used to concatenate chunks of data in "m_reply_data" from several
222 // PropertyNotify when we are getting the selection owner data with
223 // the INCR method.
224 reply_offset: usize,
225 // List of user-defined formats/atoms.
226 //custom_formats: Vec<xcb::xproto::Atom>,
227 }
228
229 impl Manager {
new(connection: &RustConnection, screen: usize) -> Result<Self, Error>230 fn new(connection: &RustConnection, screen: usize) -> Result<Self, Error> {
231 let setup = connection.setup();
232 let screen = setup.roots.get(screen).ok_or(Error::Unknown {
233 description: String::from("Could not get screen from setup"),
234 })?;
235 let event_mask =
236 // Just in case that some program reports SelectionNotify events
237 // with XCB_EVENT_MASK_PROPERTY_CHANGE mask.
238 EventMask::PROPERTY_CHANGE |
239 // To receive DestroyNotify event and stop the message loop.
240 EventMask::STRUCTURE_NOTIFY;
241 let window = connection
242 .generate_id()
243 .map_err(|e| Error::Unknown { description: format!("{}", e) })?;
244 connection
245 .create_window(
246 0,
247 window,
248 screen.root,
249 0,
250 0,
251 1,
252 1,
253 0,
254 WindowClass::INPUT_OUTPUT,
255 screen.root_visual,
256 &CreateWindowAux::new().event_mask(event_mask),
257 )
258 .map_err(|e| Error::Unknown { description: format!("{}", e) })?;
259
260 let thread_handle = std::thread::spawn(process_x11_events);
261
262 Ok(Manager {
263 //mutex: Mutex::new(()),
264 window,
265 thread_handle: Some(thread_handle),
266 callback: None,
267 callback_result: false,
268 data: Default::default(),
269 image: super::common::ImageData {
270 width: 0,
271 height: 0,
272 bytes: std::borrow::Cow::from(vec![]),
273 },
274 incr_process: false,
275 incr_received: false,
276 target_atom: 0,
277 reply_data: Default::default(),
278 reply_offset: 0,
279 })
280 }
281
set_x11_selection_owner(&self, shared: &mut SharedState) -> bool282 fn set_x11_selection_owner(&self, shared: &mut SharedState) -> bool {
283 let clipboard_atom = shared.common_atoms().CLIPBOARD;
284 let cookie = shared.conn.as_ref().unwrap().set_selection_owner(
285 self.window,
286 clipboard_atom,
287 Time::CURRENT_TIME,
288 );
289
290 cookie.is_ok()
291 }
292
set_image(&mut self, shared: &mut SharedState, image: ImageData) -> Result<(), Error>293 fn set_image(&mut self, shared: &mut SharedState, image: ImageData) -> Result<(), Error> {
294 if !self.set_x11_selection_owner(shared) {
295 return Err(Error::Unknown {
296 description: "Failed to set x11 selection owner.".into(),
297 });
298 }
299
300 self.image.width = image.width;
301 self.image.height = image.height;
302 self.image.bytes = image.bytes.into_owned().into();
303
304 // Put a ~nullptr~ (None) in the m_data for image/png format and then we'll
305 // encode the png data when the image is requested in this format.
306 self.data.insert(shared.common_atoms().MIME_IMAGE_PNG, None);
307
308 Ok(())
309 }
310
311 /// Rust impl: instead of this function there's a more generic `set_data` which I believe can
312 /// also set user formats, but arboard doesn't support that for now.
set_text(&mut self, shared: &mut SharedState, bytes: Vec<u8>) -> Result<(), Error>313 fn set_text(&mut self, shared: &mut SharedState, bytes: Vec<u8>) -> Result<(), Error> {
314 if !self.set_x11_selection_owner(shared) {
315 return Err(Error::Unknown {
316 description: "Could not take ownership of the x11 selection".into(),
317 });
318 }
319
320 let atoms = shared.text_atoms();
321 if atoms.is_empty() {
322 return Err(Error::Unknown { description:
323 "Couldn't get the atoms that identify supported text formats for the x11 clipboard"
324 .into(),
325 });
326 }
327
328 let arc_data = Arc::new(Mutex::new(bytes));
329 for atom in atoms {
330 self.data.insert(atom, Some(arc_data.clone()));
331 }
332
333 Ok(())
334 }
335
clear_data(&mut self)336 fn clear_data(&mut self) {
337 self.data.clear();
338 self.image.width = 0;
339 self.image.height = 0;
340 self.image.bytes = Vec::new().into();
341 }
342
set_requestor_property_with_clipboard_content( &mut self, shared: &mut SharedState, requestor: Window, property: Atom, target: Atom, ) -> bool343 fn set_requestor_property_with_clipboard_content(
344 &mut self,
345 shared: &mut SharedState,
346 requestor: Window,
347 property: Atom,
348 target: Atom,
349 ) -> bool {
350 let item = {
351 if let Some(item) = self.data.get_mut(&target) {
352 item
353 } else {
354 // Nothing to do (unsupported target)
355 return false;
356 }
357 };
358
359 // This can be null if the data was set from an image but we
360 // didn't encode the image yet (e.g. to image/png format).
361 if item.is_none() {
362 encode_data_on_demand(shared, &mut self.image, target, item);
363
364 // Return nothing, the given "target" cannot be constructed
365 // (maybe by some encoding error).
366 if item.is_none() {
367 return false;
368 }
369 }
370
371 let item = item.as_ref().unwrap().lock().unwrap();
372 // Set the "property" of "requestor" with the
373 // clipboard content in the requested format ("target").
374 if let Err(e) = shared.conn.as_ref().unwrap().change_property8(
375 PropMode::REPLACE,
376 requestor,
377 property,
378 target,
379 item.as_slice(),
380 ) {
381 log::error!("{}", e)
382 }
383
384 true
385 }
386
copy_reply_data(&mut self, reply: &GetPropertyReply)387 fn copy_reply_data(&mut self, reply: &GetPropertyReply) {
388 let src = &reply.value;
389 // n = length of "src" in bytes
390 let n = reply.value_len;
391 let req = self.reply_offset + n as usize;
392 match &mut self.reply_data {
393 None => {
394 self.reply_offset = 0; // Rust impl: I added this just to be extra sure.
395 self.reply_data = Some(Arc::new(Mutex::new(vec![0; req])));
396 }
397 // The "m_reply_data" size can be smaller because the size
398 // specified in INCR property is just a lower bound.
399 Some(reply_data) => {
400 let mut reply_data = reply_data.lock().unwrap();
401 if req > reply_data.len() {
402 reply_data.resize(req, 0);
403 }
404 }
405 }
406 let src_slice = src.as_slice();
407 let mut reply_data_locked = self.reply_data.as_mut().unwrap().lock().unwrap();
408 reply_data_locked[self.reply_offset..req].copy_from_slice(src_slice);
409 self.reply_offset += n as usize;
410 }
411
412 // Rust impl: It's strange, the reply attribute is also unused in the original code.
call_callback(&mut self, _reply: GetPropertyReply)413 fn call_callback(&mut self, _reply: GetPropertyReply) {
414 self.callback_result = false;
415 if let Some(callback) = &self.callback {
416 self.callback_result = callback(&self.reply_data);
417 }
418 CONDVAR.notify_one();
419
420 self.reply_data = None;
421 }
422
423 /// Rust impl: This function was added instead of the destructor because the drop
424 /// does not get called on lazy static objects. This function is registered for `libc::atexit`
425 /// on a successful initialization
destruct()426 fn destruct() {
427 let join_handle;
428
429 // The following scope is to ensure that we release the lock
430 // before attempting to join the thread.
431 {
432 let mut guard = LOCKED_OBJECTS.lock().unwrap();
433 if guard.is_none() {
434 return;
435 }
436 macro_rules! manager {
437 () => {
438 guard.as_mut().unwrap().manager
439 };
440 }
441 macro_rules! shared {
442 () => {
443 guard.as_mut().unwrap().shared
444 };
445 }
446
447 if !manager!().data.is_empty()
448 && manager!().window != 0
449 && manager!().window == get_x11_selection_owner(&mut shared!())
450 {
451 let atoms = vec![shared!().common_atoms().SAVE_TARGETS];
452 let selection = shared!().common_atoms().CLIPBOARD_MANAGER;
453
454 // Start the SAVE_TARGETS mechanism so the X11
455 // CLIPBOARD_MANAGER will save our clipboard data
456 // from now on.
457 guard = get_data_from_selection_owner(
458 guard,
459 &atoms,
460 Some(Arc::new(|_| true)),
461 selection,
462 )
463 .1;
464 }
465
466 if manager!().window != 0 {
467 let window = manager!().window;
468 let _ = shared!().conn.as_ref().unwrap().destroy_window(window);
469 let _ = shared!().conn.as_ref().unwrap().flush();
470 manager!().window = 0;
471 }
472 join_handle = manager!().thread_handle.take();
473 }
474
475 if let Some(handle) = join_handle {
476 handle.join().ok();
477 }
478
479 // This is not needed because the connection is automatically disconnected when droped
480 // if (m_connection)
481 // xcb_disconnect(m_connection);
482 }
483 }
484
process_x11_events()485 fn process_x11_events() {
486 let connection = {
487 let lo = LOCKED_OBJECTS.lock().unwrap();
488 lo.as_ref().unwrap().shared.conn.clone()
489 };
490
491 let mut stop = false;
492 while !stop {
493 let event = {
494 // If this doesn't work, wrap the connection into an Arc
495 std::thread::sleep(Duration::from_millis(5));
496 let maybe_event = connection.as_ref().unwrap().poll_for_event();
497 match maybe_event {
498 Ok(Some(e)) => e,
499 Ok(None) => continue,
500 Err(_) => break,
501 }
502 };
503 match event {
504 Event::DestroyNotify(_) => {
505 //println!("Received destroy event, stopping");
506 stop = true;
507 //panic!("{}", line!());
508 //break;
509 }
510
511 // Someone else has new content in the clipboard, so is
512 // notifying us that we should delete our data now.
513 Event::SelectionClear(event) => {
514 //println!("Received selection clear,");
515 handle_selection_clear_event(event);
516 }
517
518 // Someone is requesting the clipboard content from us.
519 Event::SelectionRequest(event) => {
520 //println!("Received selection request");
521 handle_selection_request_event(event);
522 }
523
524 // We've requested the clipboard content and this is the
525 // answer.
526 Event::SelectionNotify(event) => {
527 //println!("Received selection notify");
528 handle_selection_notify_event(event);
529 }
530
531 Event::PropertyNotify(event) => {
532 //println!("Received property notify");
533 handle_property_notify_event(event);
534 }
535 _ => {}
536 }
537 // The event uses RAII, so it's free'd automatically
538 }
539 }
540
handle_selection_clear_event(event: SelectionClearEvent)541 fn handle_selection_clear_event(event: SelectionClearEvent) {
542 let selection = event.selection;
543 let mut guard = LOCKED_OBJECTS.lock().unwrap();
544 let locked = guard.as_mut().unwrap();
545 let clipboard_atom = { locked.shared.common_atoms().CLIPBOARD };
546 if selection == clipboard_atom {
547 locked.manager.clear_data();
548 }
549 }
550
handle_selection_request_event(event: SelectionRequestEvent)551 fn handle_selection_request_event(event: SelectionRequestEvent) {
552 let target = event.target;
553 let requestor = event.requestor;
554 let property = event.property;
555 let time = event.time;
556 let selection = event.selection;
557 let targets_atom;
558 let save_targets_atom;
559 let multiple_atom;
560 let atom_atom;
561 {
562 let mut guard = LOCKED_OBJECTS.lock().unwrap();
563 let locked = guard.as_mut().unwrap();
564 let shared = &mut locked.shared;
565 targets_atom = shared.common_atoms().TARGETS;
566 save_targets_atom = shared.common_atoms().SAVE_TARGETS;
567 multiple_atom = shared.common_atoms().MULTIPLE;
568 atom_atom = shared.common_atoms().ATOM;
569 }
570 if target == targets_atom {
571 let mut targets = Atoms::with_capacity(4);
572 targets.push(targets_atom);
573 targets.push(save_targets_atom);
574 targets.push(multiple_atom);
575 let mut guard = LOCKED_OBJECTS.lock().unwrap();
576 let locked = guard.as_mut().unwrap();
577 let manager = &locked.manager;
578 for atom in manager.data.keys() {
579 targets.push(*atom);
580 }
581
582 let shared = &locked.shared;
583 // Set the "property" of "requestor" with the clipboard
584 // formats ("targets", atoms) that we provide.
585 if let Err(e) = shared.conn.as_ref().unwrap().change_property32(
586 PropMode::REPLACE,
587 requestor,
588 property,
589 atom_atom,
590 targets.as_slice(),
591 ) {
592 log::error!("{}", e);
593 };
594 } else if target == save_targets_atom {
595 // Do nothing
596 } else if target == multiple_atom {
597 let mut guard = LOCKED_OBJECTS.lock().unwrap();
598 let locked = guard.as_mut().unwrap();
599 let reply = {
600 let atom_pair_atom = locked.shared.common_atoms().ATOM_PAIR;
601 get_and_delete_property(
602 locked.shared.conn.as_ref().unwrap(),
603 requestor,
604 property,
605 atom_pair_atom,
606 false,
607 )
608 };
609 if let Some(reply) = reply {
610 let atoms = reply.value32();
611 for atom in atoms.into_iter().flatten() {
612 let target = atom;
613 let property = atom;
614 let property_set = locked.manager.set_requestor_property_with_clipboard_content(
615 &mut locked.shared,
616 requestor,
617 property,
618 target,
619 );
620 if !property_set {
621 if let Err(e) = locked.shared.conn.as_ref().unwrap().change_property(
622 PropMode::REPLACE,
623 requestor,
624 property,
625 AtomEnum::NONE,
626 0,
627 0,
628 &[],
629 ) {
630 log::error!("{}", e)
631 }
632 }
633 }
634 }
635 } else {
636 let mut guard = LOCKED_OBJECTS.lock().unwrap();
637 let locked = guard.as_mut().unwrap();
638 let property_set = locked.manager.set_requestor_property_with_clipboard_content(
639 &mut locked.shared,
640 requestor,
641 property,
642 target,
643 );
644 if !property_set {
645 return;
646 }
647 }
648
649 let mut guard = LOCKED_OBJECTS.lock().unwrap();
650 let locked = guard.as_mut().unwrap();
651 let shared = &mut locked.shared;
652
653 // Notify the "requestor" that we've already updated the property.
654 let notify = SelectionNotifyEvent {
655 response_type: xproto::SELECTION_NOTIFY_EVENT,
656 sequence: 0,
657 time,
658 requestor,
659 selection,
660 target,
661 property,
662 };
663 if let Err(e) =
664 shared.conn.as_ref().unwrap().send_event(false, requestor, EventMask::NO_EVENT, notify)
665 {
666 log::error!("{}", e)
667 }
668 if let Err(e) = shared.conn.as_ref().unwrap().flush() {
669 log::error!("{}", e)
670 }
671 }
672
handle_selection_notify_event(event: SelectionNotifyEvent)673 fn handle_selection_notify_event(event: SelectionNotifyEvent) {
674 let target = event.target;
675 let requestor = event.requestor;
676 let property = event.property;
677 let mut guard = LOCKED_OBJECTS.lock().unwrap();
678 let mut locked = guard.as_mut().unwrap();
679 assert_eq!(requestor, locked.manager.window);
680
681 if target == locked.shared.common_atoms().TARGETS {
682 locked.manager.target_atom = locked.shared.common_atoms().ATOM;
683 } else {
684 locked.manager.target_atom = target;
685 }
686
687 let target_atom = locked.manager.target_atom;
688 let reply = get_and_delete_property(
689 locked.shared.conn.as_ref().unwrap(),
690 requestor,
691 property,
692 target_atom,
693 true,
694 );
695 if let Some(reply) = reply {
696 let reply_type = reply.type_;
697 // In this case, We're going to receive the clipboard content in
698 // chunks of data with several PropertyNotify events.
699 let incr_atom = locked.shared.common_atoms().INCR;
700 if reply_type == incr_atom {
701 let reply = get_and_delete_property(
702 locked.shared.conn.as_ref().unwrap(),
703 requestor,
704 property,
705 incr_atom,
706 true,
707 );
708 if let Some(reply) = reply {
709 if reply.value_len == 4 {
710 let n = reply.value32().and_then(|mut values| values.next()).unwrap_or(0);
711 locked.manager.reply_data = Some(Arc::new(Mutex::new(vec![0u8; n as usize])));
712 locked.manager.reply_offset = 0;
713 locked.manager.incr_process = true;
714 locked.manager.incr_received = true;
715 }
716 }
717 } else {
718 // Simple case, the whole clipboard content in just one reply
719 // (without the INCR method).
720 locked.manager.reply_data = None;
721 locked.manager.reply_offset = 0;
722 locked.manager.copy_reply_data(&reply);
723 locked.manager.call_callback(reply);
724 }
725 }
726 }
727
handle_property_notify_event(event: PropertyNotifyEvent)728 fn handle_property_notify_event(event: PropertyNotifyEvent) {
729 let state = event.state;
730 let atom = event.atom;
731 let window = event.window;
732 let mut guard = LOCKED_OBJECTS.lock().unwrap();
733 let mut locked = guard.as_mut().unwrap();
734 if locked.manager.incr_process
735 && state == Property::NEW_VALUE
736 && atom == locked.shared.common_atoms().CLIPBOARD
737 {
738 let target_atom = locked.manager.target_atom;
739 let reply = get_and_delete_property(
740 locked.shared.conn.as_ref().unwrap(),
741 window,
742 atom,
743 target_atom,
744 true,
745 );
746 if let Some(reply) = reply {
747 locked.manager.incr_received = true;
748
749 // When the length is 0 it means that the content was
750 // completely sent by the selection owner.
751 if reply.value_len > 0 {
752 locked.manager.copy_reply_data(&reply);
753 } else {
754 // Now that m_reply_data has the complete clipboard content,
755 // we can call the m_callback.
756 locked.manager.call_callback(reply);
757 locked.manager.incr_process = false;
758 }
759 }
760 }
761 }
762
get_and_delete_property( conn: &RustConnection, window: Window, property: Atom, atom: Atom, delete_prop: bool, ) -> Option<GetPropertyReply>763 fn get_and_delete_property(
764 conn: &RustConnection,
765 window: Window,
766 property: Atom,
767 atom: Atom,
768 delete_prop: bool,
769 ) -> Option<GetPropertyReply> {
770 conn.get_property(
771 delete_prop,
772 window,
773 property,
774 atom,
775 0,
776 0x1fffffff, // 0x1fffffff = INT32_MAX / 4
777 )
778 .ok()
779 .and_then(|cookie| cookie.reply().ok())
780 }
781
get_data_from_selection_owner<'a>( mut guard: MutexGuard<'a, Option<LockedObjects>>, atoms: &[Atom], callback: NotifyCallback, mut selection: xproto::Atom, ) -> (bool, MutexGuard<'a, Option<LockedObjects>>)782 fn get_data_from_selection_owner<'a>(
783 mut guard: MutexGuard<'a, Option<LockedObjects>>,
784 atoms: &[Atom],
785 callback: NotifyCallback,
786 mut selection: xproto::Atom,
787 ) -> (bool, MutexGuard<'a, Option<LockedObjects>>) {
788 // Wait a response for 100 milliseconds
789 const CV_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(100);
790 {
791 let locked = guard.as_mut().unwrap();
792 if selection == 0 {
793 selection = locked.shared.common_atoms().CLIPBOARD;
794 }
795 locked.manager.callback = callback;
796
797 // Clear data if we are not the selection owner.
798 if locked.manager.window != get_x11_selection_owner(&mut locked.shared) {
799 locked.manager.data.clear();
800 }
801 }
802
803 // Ask to the selection owner for its content on each known
804 // text format/atom.
805 for atom in atoms.iter() {
806 {
807 let locked = guard.as_mut().unwrap();
808 let clipboard_atom = locked.shared.common_atoms().CLIPBOARD;
809 if let Err(e) = locked.shared.conn.as_ref().unwrap().convert_selection(
810 locked.manager.window,
811 selection,
812 *atom,
813 clipboard_atom,
814 Time::CURRENT_TIME,
815 ) {
816 log::error!("{}", e)
817 }
818 if let Err(e) = locked.shared.conn.as_ref().unwrap().flush() {
819 log::error!("{}", e)
820 }
821 }
822
823 // We use the "m_incr_received" to wait several timeouts in case
824 // that we've received the INCR SelectionNotify or
825 // PropertyNotify events.
826 'incr_loop: loop {
827 guard.as_mut().unwrap().manager.incr_received = false;
828 match CONDVAR.wait_timeout(guard, CV_TIMEOUT) {
829 Ok((new_guard, status)) => {
830 guard = new_guard;
831 if !status.timed_out() {
832 // If the condition variable was notified, it means that the
833 // callback was called correctly.
834 return (guard.as_ref().unwrap().manager.callback_result, guard);
835 }
836
837 if !guard.as_ref().unwrap().manager.incr_received {
838 break 'incr_loop;
839 }
840 }
841 Err(err) => {
842 panic!(
843 "A critical error occured while working with the x11 clipboard. {}",
844 err
845 );
846 }
847 }
848 }
849 }
850
851 guard.as_mut().unwrap().manager.callback = None;
852 (false, guard)
853 }
854
get_x11_selection_owner(shared: &mut SharedState) -> Window855 fn get_x11_selection_owner(shared: &mut SharedState) -> Window {
856 let mut result = 0;
857
858 let clipboard_atom = shared.common_atoms().CLIPBOARD;
859 let cookie = shared.conn.as_ref().unwrap().get_selection_owner(clipboard_atom);
860 let reply = cookie.ok().and_then(|cookie| cookie.reply().ok());
861 if let Some(reply) = reply {
862 result = reply.owner;
863 }
864
865 result
866 }
867
get_text(mut guard: MutexGuard<Option<LockedObjects>>) -> Result<String, Error>868 fn get_text(mut guard: MutexGuard<Option<LockedObjects>>) -> Result<String, Error> {
869 // Rust impl: This function is probably the ugliest Rust code I've ever written
870 // Make no mistake, the original, C++ code was perfectly fine (which I didn't write)
871 let owner = get_x11_selection_owner(&mut guard.as_mut().unwrap().shared);
872 if owner == guard.as_mut().unwrap().manager.window {
873 let atoms = guard.as_mut().unwrap().shared.text_atoms();
874 for atom in atoms.iter() {
875 let mut item = None;
876 if let Some(Some(i)) = guard.as_mut().unwrap().manager.data.get(atom) {
877 item = Some(i.clone());
878 }
879 if let Some(item) = item {
880 // Unwrapping the item because we always initialize text with `Some`
881 let locked = item.lock().unwrap();
882 let result = String::from_utf8(locked.clone());
883 return result.map_err(|_| Error::ConversionFailure);
884 }
885 }
886 } else if owner != 0 {
887 let atoms = guard.as_mut().unwrap().shared.text_atoms();
888 let result = Arc::new(Mutex::new(Ok(String::new())));
889 let callback = {
890 let result = result.clone();
891 Arc::new(move |data: &BufferPtr| {
892 if let Some(reply_data) = data {
893 let locked_data = reply_data.lock().unwrap();
894 let mut locked_result = result.lock().unwrap();
895 *locked_result = String::from_utf8(locked_data.clone());
896 }
897 true
898 })
899 };
900
901 let (success, _) = get_data_from_selection_owner(guard, &atoms, Some(callback as _), 0);
902 if success {
903 let mut taken = Ok(String::new());
904 let mut locked = result.lock().unwrap();
905 std::mem::swap(&mut taken, &mut locked);
906 return taken.map_err(|_| Error::ConversionFailure);
907 }
908 }
909 Err(Error::ContentNotAvailable)
910 }
911
get_image(mut guard: MutexGuard<Option<LockedObjects>>) -> Result<ImageData, Error>912 fn get_image(mut guard: MutexGuard<Option<LockedObjects>>) -> Result<ImageData, Error> {
913 let owner = get_x11_selection_owner(&mut guard.as_mut().unwrap().shared);
914 //let mut result_img;
915 if owner == guard.as_ref().unwrap().manager.window {
916 let image = &guard.as_ref().unwrap().manager.image;
917 if image.width > 0 && image.height > 0 && !image.bytes.is_empty() {
918 return Ok(image.to_owned_img());
919 }
920 } else if owner != 0 {
921 let atoms = vec![guard.as_mut().unwrap().shared.common_atoms().MIME_IMAGE_PNG];
922 let result: Arc<Mutex<Result<ImageData, Error>>> =
923 Arc::new(Mutex::new(Err(Error::ContentNotAvailable)));
924 let callback = {
925 let result = result.clone();
926 Arc::new(move |data: &BufferPtr| {
927 if let Some(reply_data) = data {
928 let locked_data = reply_data.lock().unwrap();
929 let cursor = std::io::Cursor::new(&*locked_data);
930 let mut reader = image::io::Reader::new(cursor);
931 reader.set_format(image::ImageFormat::Png);
932 let image;
933 match reader.decode() {
934 Ok(img) => image = img.into_rgba8(),
935 Err(_e) => {
936 let mut locked_result = result.lock().unwrap();
937 *locked_result = Err(Error::ConversionFailure);
938 return false;
939 }
940 }
941 let (w, h) = image.dimensions();
942 let mut locked_result = result.lock().unwrap();
943 let image_data = ImageData {
944 width: w as usize,
945 height: h as usize,
946 bytes: image.into_raw().into(),
947 };
948 *locked_result = Ok(image_data);
949 }
950 true
951 })
952 };
953 let _success = get_data_from_selection_owner(guard, &atoms, Some(callback as _), 0).0;
954 // Rust impl: We return the result here no matter if it succeeded, because the result will
955 // tell us if it hasn't
956 let mut taken = Err(Error::Unknown {
957 description: format!("Implementation error at {}:{}", file!(), line!()),
958 });
959 let mut locked = result.lock().unwrap();
960 std::mem::swap(&mut taken, &mut locked);
961 return taken;
962 }
963 Err(Error::ContentNotAvailable)
964 }
965
encode_data_on_demand( shared: &mut SharedState, image: &mut ImageData, atom: xproto::Atom, buffer: &mut Option<Arc<Mutex<Vec<u8>>>>, )966 fn encode_data_on_demand(
967 shared: &mut SharedState,
968 image: &mut ImageData,
969 atom: xproto::Atom,
970 buffer: &mut Option<Arc<Mutex<Vec<u8>>>>,
971 ) {
972 if atom == shared.common_atoms().MIME_IMAGE_PNG {
973 if let Ok(image) = encode_as_png(image) {
974 *buffer = Some(Arc::new(Mutex::new(image)));
975 }
976 }
977 }
978
ensure_lo_initialized() -> Result<MutexGuard<'static, Option<LockedObjects>>, Error>979 fn ensure_lo_initialized() -> Result<MutexGuard<'static, Option<LockedObjects>>, Error> {
980 let mut locked = LOCKED_OBJECTS.lock().unwrap();
981 if locked.is_none() {
982 *locked = Some(LockedObjects::new().map_err(|e| Error::Unknown {
983 description: format!(
984 "Could not initialize the x11 clipboard handling facilities. Cause: {}",
985 e
986 ),
987 })?);
988 }
989 Ok(locked)
990 }
991
with_locked_objects<F, T>(action: F) -> Result<T, Error> where F: FnOnce(&mut LockedObjects) -> Result<T, Error>,992 fn with_locked_objects<F, T>(action: F) -> Result<T, Error>
993 where
994 F: FnOnce(&mut LockedObjects) -> Result<T, Error>,
995 {
996 // The gobal may not have been initialized yet or may have been destroyed previously.
997 //
998 // Note: the global objects gets destroyed (replaced with None) when the last
999 // clipboard context is dropped (goes out of scope).
1000 let mut locked = ensure_lo_initialized()?;
1001 let lo = locked.as_mut().unwrap();
1002 action(lo)
1003 }
1004
1005 pub struct X11ClipboardContext {
1006 _owned: Arc<Mutex<Option<LockedObjects>>>,
1007 }
1008
1009 impl Drop for X11ClipboardContext {
drop(&mut self)1010 fn drop(&mut self) {
1011 // If there's no other owner than us and the global,
1012 // then destruct the manager
1013 if Arc::strong_count(&LOCKED_OBJECTS) == 2 {
1014 Manager::destruct();
1015 let mut locked = LOCKED_OBJECTS.lock().unwrap();
1016 *locked = None;
1017 }
1018 }
1019 }
1020
1021 impl X11ClipboardContext {
new() -> Self1022 pub(crate) fn new() -> Self {
1023 X11ClipboardContext { _owned: LOCKED_OBJECTS.clone() }
1024 }
1025
get_text(&mut self) -> Result<String, Error>1026 pub(crate) fn get_text(&mut self) -> Result<String, Error> {
1027 let locked = ensure_lo_initialized()?;
1028 get_text(locked)
1029 }
1030
set_text(&mut self, text: String) -> Result<(), Error>1031 pub(crate) fn set_text(&mut self, text: String) -> Result<(), Error> {
1032 with_locked_objects(|locked| {
1033 let manager = &mut locked.manager;
1034 let shared = &mut locked.shared;
1035 manager.set_text(shared, text.into_bytes())
1036 })
1037 }
1038
get_image(&mut self) -> Result<ImageData, Error>1039 pub(crate) fn get_image(&mut self) -> Result<ImageData, Error> {
1040 let locked = ensure_lo_initialized()?;
1041 get_image(locked)
1042 }
1043
set_image(&mut self, image: ImageData) -> Result<(), Error>1044 pub(crate) fn set_image(&mut self, image: ImageData) -> Result<(), Error> {
1045 with_locked_objects(|locked| {
1046 let manager = &mut locked.manager;
1047 let shared = &mut locked.shared;
1048 manager.set_image(shared, image)
1049 })
1050 }
1051 }
1052