1/*
2 * Copyright 2022 - 2025 Zed Industries, Inc.
3 * License: Apache-2.0
4 * See LICENSE-APACHE for complete license terms
5 *
6 * Adapted from the x11 submodule of the arboard project https://github.com/1Password/arboard
7 *
8 * SPDX-License-Identifier: Apache-2.0 OR MIT
9 *
10 * Copyright 2022 The Arboard contributors
11 *
12 * The project to which this file belongs is licensed under either of
13 * the Apache 2.0 or the MIT license at the licensee's choice. The terms
14 * and conditions of the chosen license apply to this file.
15*/
16
17// More info about using the clipboard on X11:
18// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6
19// https://freedesktop.org/wiki/ClipboardManager/
20
21use std::{
22 borrow::Cow,
23 cell::RefCell,
24 collections::{HashMap, hash_map::Entry},
25 sync::{
26 Arc,
27 atomic::{AtomicBool, Ordering},
28 },
29 thread::JoinHandle,
30 thread_local,
31 time::{Duration, Instant},
32};
33
34use parking_lot::{Condvar, Mutex, MutexGuard, RwLock};
35use x11rb::{
36 COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE,
37 connection::Connection,
38 protocol::{
39 Event,
40 xproto::{
41 Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property,
42 PropertyNotifyEvent, SELECTION_NOTIFY_EVENT, SelectionNotifyEvent,
43 SelectionRequestEvent, Time, WindowClass,
44 },
45 },
46 rust_connection::RustConnection,
47 wrapper::ConnectionExt as _,
48};
49
50use crate::{ClipboardItem, Image, ImageFormat, hash};
51
52type Result<T, E = Error> = std::result::Result<T, E>;
53
54static CLIPBOARD: Mutex<Option<GlobalClipboard>> = parking_lot::const_mutex(None);
55
56x11rb::atom_manager! {
57 pub Atoms: AtomCookies {
58 CLIPBOARD,
59 PRIMARY,
60 SECONDARY,
61
62 CLIPBOARD_MANAGER,
63 SAVE_TARGETS,
64 TARGETS,
65 ATOM,
66 INCR,
67
68 UTF8_STRING,
69 UTF8_MIME_0: b"text/plain;charset=utf-8",
70 UTF8_MIME_1: b"text/plain;charset=UTF-8",
71 // Text in ISO Latin-1 encoding
72 // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
73 STRING,
74 // Text in unknown encoding
75 // See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
76 TEXT,
77 TEXT_MIME_UNKNOWN: b"text/plain",
78
79 // HTML: b"text/html",
80 // URI_LIST: b"text/uri-list",
81
82 PNG__MIME: ImageFormat::mime_type(ImageFormat::Png ).as_bytes(),
83 JPEG_MIME: ImageFormat::mime_type(ImageFormat::Jpeg).as_bytes(),
84 WEBP_MIME: ImageFormat::mime_type(ImageFormat::Webp).as_bytes(),
85 GIF__MIME: ImageFormat::mime_type(ImageFormat::Gif ).as_bytes(),
86 SVG__MIME: ImageFormat::mime_type(ImageFormat::Svg ).as_bytes(),
87 BMP__MIME: ImageFormat::mime_type(ImageFormat::Bmp ).as_bytes(),
88 TIFF_MIME: ImageFormat::mime_type(ImageFormat::Tiff).as_bytes(),
89 ICO__MIME: ImageFormat::mime_type(ImageFormat::Ico ).as_bytes(),
90
91 // This is just some random name for the property on our window, into which
92 // the clipboard owner writes the data we requested.
93 ARBOARD_CLIPBOARD,
94 }
95}
96
97thread_local! {
98 static ATOM_NAME_CACHE: RefCell<HashMap<Atom, &'static str>> = Default::default();
99}
100
101// Some clipboard items, like images, may take a very long time to produce a
102// `SelectionNotify`. Multiple seconds long.
103const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000);
104const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10);
105
106#[derive(Debug, PartialEq, Eq)]
107enum ManagerHandoverState {
108 Idle,
109 InProgress,
110 Finished,
111}
112
113struct GlobalClipboard {
114 inner: Arc<Inner>,
115
116 /// Join handle to the thread which serves selection requests.
117 server_handle: JoinHandle<()>,
118}
119
120struct XContext {
121 conn: RustConnection,
122 win_id: u32,
123}
124
125struct Inner {
126 /// The context for the thread which serves clipboard read
127 /// requests coming to us.
128 server: XContext,
129 atoms: Atoms,
130
131 clipboard: Selection,
132 primary: Selection,
133 secondary: Selection,
134
135 handover_state: Mutex<ManagerHandoverState>,
136 handover_cv: Condvar,
137
138 serve_stopped: AtomicBool,
139}
140
141impl XContext {
142 fn new() -> Result<Self> {
143 // create a new connection to an X11 server
144 let (conn, screen_num): (RustConnection, _) =
145 RustConnection::connect(None).map_err(|_| {
146 Error::unknown("X11 server connection timed out because it was unreachable")
147 })?;
148 let screen = conn
149 .setup()
150 .roots
151 .get(screen_num)
152 .ok_or(Error::unknown("no screen found"))?;
153 let win_id = conn.generate_id().map_err(into_unknown)?;
154
155 let event_mask =
156 // Just in case that some program reports SelectionNotify events
157 // with XCB_EVENT_MASK_PROPERTY_CHANGE mask.
158 EventMask::PROPERTY_CHANGE |
159 // To receive DestroyNotify event and stop the message loop.
160 EventMask::STRUCTURE_NOTIFY;
161 // create the window
162 conn.create_window(
163 // copy as much as possible from the parent, because no other specific input is needed
164 COPY_DEPTH_FROM_PARENT,
165 win_id,
166 screen.root,
167 0,
168 0,
169 1,
170 1,
171 0,
172 WindowClass::COPY_FROM_PARENT,
173 COPY_FROM_PARENT,
174 // don't subscribe to any special events because we are requesting everything we need ourselves
175 &CreateWindowAux::new().event_mask(event_mask),
176 )
177 .map_err(into_unknown)?;
178 conn.flush().map_err(into_unknown)?;
179
180 Ok(Self { conn, win_id })
181 }
182}
183
184#[derive(Default)]
185struct Selection {
186 data: RwLock<Option<Vec<ClipboardData>>>,
187 /// Mutex around nothing to use with the below condvar.
188 mutex: Mutex<()>,
189 /// A condvar that is notified when the contents of this clipboard are changed.
190 ///
191 /// This is associated with `Self::mutex`.
192 data_changed: Condvar,
193}
194
195#[derive(Debug, Clone)]
196struct ClipboardData {
197 bytes: Vec<u8>,
198
199 /// The atom representing the format in which the data is encoded.
200 format: Atom,
201}
202
203enum ReadSelNotifyResult {
204 GotData(ClipboardData),
205 IncrStarted,
206 EventNotRecognized,
207}
208
209impl Inner {
210 fn new() -> Result<Self> {
211 let server = XContext::new()?;
212 let atoms = Atoms::new(&server.conn)
213 .map_err(into_unknown)?
214 .reply()
215 .map_err(into_unknown)?;
216
217 Ok(Self {
218 server,
219 atoms,
220 clipboard: Selection::default(),
221 primary: Selection::default(),
222 secondary: Selection::default(),
223 handover_state: Mutex::new(ManagerHandoverState::Idle),
224 handover_cv: Condvar::new(),
225 serve_stopped: AtomicBool::new(false),
226 })
227 }
228
229 fn write(
230 &self,
231 data: Vec<ClipboardData>,
232 selection: ClipboardKind,
233 wait: WaitConfig,
234 ) -> Result<()> {
235 if self.serve_stopped.load(Ordering::Relaxed) {
236 return Err(Error::unknown(
237 "The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)",
238 ));
239 }
240
241 let server_win = self.server.win_id;
242
243 // ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data
244 // changes.
245 self.server
246 .conn
247 .set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME)
248 .map_err(|_| Error::ClipboardOccupied)?;
249
250 self.server.conn.flush().map_err(into_unknown)?;
251
252 // Just setting the data, and the `serve_requests` will take care of the rest.
253 let selection = self.selection_of(selection);
254 let mut data_guard = selection.data.write();
255 *data_guard = Some(data);
256
257 // Lock the mutex to both ensure that no wakers of `data_changed` can wake us between
258 // dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other
259 // threads in that position.
260 let mut guard = selection.mutex.lock();
261
262 // Notify any existing waiting threads that we have changed the data in the selection.
263 // It is important that the mutex is locked to prevent this notification getting lost.
264 selection.data_changed.notify_all();
265
266 match wait {
267 WaitConfig::None => {}
268 WaitConfig::Forever => {
269 drop(data_guard);
270 selection.data_changed.wait(&mut guard);
271 }
272 WaitConfig::Until(deadline) => {
273 drop(data_guard);
274 selection.data_changed.wait_until(&mut guard, deadline);
275 }
276 }
277
278 Ok(())
279 }
280
281 /// `formats` must be a slice of atoms, where each atom represents a target format.
282 /// The first format from `formats`, which the clipboard owner supports will be the
283 /// format of the return value.
284 fn read(&self, formats: &[Atom], selection: ClipboardKind) -> Result<ClipboardData> {
285 // if we are the current owner, we can get the current clipboard ourselves
286 if self.is_owner(selection)? {
287 let data = self.selection_of(selection).data.read();
288 if let Some(data_list) = &*data {
289 for data in data_list {
290 for format in formats {
291 if *format == data.format {
292 return Ok(data.clone());
293 }
294 }
295 }
296 }
297 return Err(Error::ContentNotAvailable);
298 }
299 let reader = XContext::new()?;
300
301 let highest_precedence_format =
302 match self.read_single(&reader, selection, self.atoms.TARGETS) {
303 Err(err) => {
304 log::trace!("Clipboard TARGETS query failed with {err:?}");
305 None
306 }
307 Ok(ClipboardData { bytes, format }) => {
308 if format == self.atoms.ATOM {
309 let available_formats = Self::parse_formats(&bytes);
310 formats
311 .iter()
312 .find(|format| available_formats.contains(format))
313 } else {
314 log::trace!(
315 "Unexpected clipboard TARGETS format {}",
316 self.atom_name(format)
317 );
318 None
319 }
320 }
321 };
322
323 if let Some(&format) = highest_precedence_format {
324 let data = self.read_single(&reader, selection, format)?;
325 if !formats.contains(&data.format) {
326 // This shouldn't happen since the format is from the TARGETS list.
327 log::trace!(
328 "Conversion to {} responded with {} which is not supported",
329 self.atom_name(format),
330 self.atom_name(data.format),
331 );
332 return Err(Error::ConversionFailure);
333 }
334 return Ok(data);
335 }
336
337 log::trace!("Falling back on attempting to convert clipboard to each format.");
338 for format in formats {
339 match self.read_single(&reader, selection, *format) {
340 Ok(data) => {
341 if formats.contains(&data.format) {
342 return Ok(data);
343 } else {
344 log::trace!(
345 "Conversion to {} responded with {} which is not supported",
346 self.atom_name(*format),
347 self.atom_name(data.format),
348 );
349 continue;
350 }
351 }
352 Err(Error::ContentNotAvailable) => {
353 continue;
354 }
355 Err(e) => {
356 log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e);
357 return Err(e);
358 }
359 }
360 }
361 log::trace!("All conversions to supported formats failed.");
362 Err(Error::ContentNotAvailable)
363 }
364
365 fn parse_formats(bytes: &[u8]) -> Vec<Atom> {
366 bytes
367 .chunks_exact(4)
368 .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
369 .collect()
370 }
371
372 fn read_single(
373 &self,
374 reader: &XContext,
375 selection: ClipboardKind,
376 target_format: Atom,
377 ) -> Result<ClipboardData> {
378 // Delete the property so that we can detect (using property notify)
379 // when the selection owner receives our request.
380 reader
381 .conn
382 .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD)
383 .map_err(into_unknown)?;
384
385 // request to convert the clipboard selection to our data type(s)
386 reader
387 .conn
388 .convert_selection(
389 reader.win_id,
390 self.atom_of(selection),
391 target_format,
392 self.atoms.ARBOARD_CLIPBOARD,
393 Time::CURRENT_TIME,
394 )
395 .map_err(into_unknown)?;
396 reader.conn.sync().map_err(into_unknown)?;
397
398 log::trace!("Finished `convert_selection`");
399
400 let mut incr_data: Vec<u8> = Vec::new();
401 let mut using_incr = false;
402
403 let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR;
404
405 while Instant::now() < timeout_end {
406 let event = reader.conn.poll_for_event().map_err(into_unknown)?;
407 let event = match event {
408 Some(e) => e,
409 None => {
410 std::thread::sleep(Duration::from_millis(1));
411 continue;
412 }
413 };
414 match event {
415 // The first response after requesting a selection.
416 Event::SelectionNotify(event) => {
417 log::trace!("Read SelectionNotify");
418 let result = self.handle_read_selection_notify(
419 reader,
420 target_format,
421 &mut using_incr,
422 &mut incr_data,
423 event,
424 )?;
425 match result {
426 ReadSelNotifyResult::GotData(data) => return Ok(data),
427 ReadSelNotifyResult::IncrStarted => {
428 // This means we received an indication that an the
429 // data is going to be sent INCRementally. Let's
430 // reset our timeout.
431 timeout_end += SHORT_TIMEOUT_DUR;
432 }
433 ReadSelNotifyResult::EventNotRecognized => (),
434 }
435 }
436 // If the previous SelectionNotify event specified that the data
437 // will be sent in INCR segments, each segment is transferred in
438 // a PropertyNotify event.
439 Event::PropertyNotify(event) => {
440 let result = self.handle_read_property_notify(
441 reader,
442 target_format,
443 using_incr,
444 &mut incr_data,
445 &mut timeout_end,
446 event,
447 )?;
448 if result {
449 return Ok(ClipboardData {
450 bytes: incr_data,
451 format: target_format,
452 });
453 }
454 }
455 _ => log::trace!(
456 "An unexpected event arrived while reading the clipboard: {:?}",
457 event
458 ),
459 }
460 }
461 log::info!("Time-out hit while reading the clipboard.");
462 Err(Error::ContentNotAvailable)
463 }
464
465 fn atom_of(&self, selection: ClipboardKind) -> Atom {
466 match selection {
467 ClipboardKind::Clipboard => self.atoms.CLIPBOARD,
468 ClipboardKind::Primary => self.atoms.PRIMARY,
469 ClipboardKind::Secondary => self.atoms.SECONDARY,
470 }
471 }
472
473 fn selection_of(&self, selection: ClipboardKind) -> &Selection {
474 match selection {
475 ClipboardKind::Clipboard => &self.clipboard,
476 ClipboardKind::Primary => &self.primary,
477 ClipboardKind::Secondary => &self.secondary,
478 }
479 }
480
481 fn kind_of(&self, atom: Atom) -> Option<ClipboardKind> {
482 match atom {
483 a if a == self.atoms.CLIPBOARD => Some(ClipboardKind::Clipboard),
484 a if a == self.atoms.PRIMARY => Some(ClipboardKind::Primary),
485 a if a == self.atoms.SECONDARY => Some(ClipboardKind::Secondary),
486 _ => None,
487 }
488 }
489
490 fn is_owner(&self, selection: ClipboardKind) -> Result<bool> {
491 let current = self
492 .server
493 .conn
494 .get_selection_owner(self.atom_of(selection))
495 .map_err(into_unknown)?
496 .reply()
497 .map_err(into_unknown)?
498 .owner;
499
500 Ok(current == self.server.win_id)
501 }
502
503 fn query_atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
504 String::from_utf8(
505 self.server
506 .conn
507 .get_atom_name(atom)
508 .map_err(into_unknown)?
509 .reply()
510 .map_err(into_unknown)?
511 .name,
512 )
513 .map_err(into_unknown)
514 }
515
516 fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
517 ATOM_NAME_CACHE.with(|cache| {
518 let mut cache = cache.borrow_mut();
519 match cache.entry(atom) {
520 Entry::Occupied(entry) => *entry.get(),
521 Entry::Vacant(entry) => {
522 let s = self
523 .query_atom_name(atom)
524 .map(|s| Box::leak(s.into_boxed_str()) as &str)
525 .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME");
526 entry.insert(s);
527 s
528 }
529 }
530 })
531 }
532
533 fn handle_read_selection_notify(
534 &self,
535 reader: &XContext,
536 target_format: u32,
537 using_incr: &mut bool,
538 incr_data: &mut Vec<u8>,
539 event: SelectionNotifyEvent,
540 ) -> Result<ReadSelNotifyResult> {
541 // The property being set to NONE means that the `convert_selection`
542 // failed.
543
544 // According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4
545 // the target must be set to the same as what we requested.
546 if event.property == NONE || event.target != target_format {
547 return Err(Error::ContentNotAvailable);
548 }
549 if self.kind_of(event.selection).is_none() {
550 log::info!(
551 "Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected."
552 );
553 return Ok(ReadSelNotifyResult::EventNotRecognized);
554 }
555 if *using_incr {
556 log::warn!("Received a SelectionNotify while already expecting INCR segments.");
557 return Ok(ReadSelNotifyResult::EventNotRecognized);
558 }
559 // Accept any property type. The property type will typically match the format type except
560 // when it is `TARGETS` in which case it is `ATOM`. `ANY` is provided to handle the case
561 // where the clipboard is not convertible to the requested format. In this case
562 // `reply.type_` will have format information, but `bytes` will only be non-empty if `ANY`
563 // is provided.
564 let property_type = AtomEnum::ANY;
565 // request the selection
566 let mut reply = reader
567 .conn
568 .get_property(
569 true,
570 event.requestor,
571 event.property,
572 property_type,
573 0,
574 u32::MAX / 4,
575 )
576 .map_err(into_unknown)?
577 .reply()
578 .map_err(into_unknown)?;
579
580 // we found something
581 if reply.type_ == self.atoms.INCR {
582 // Note that we call the get_property again because we are
583 // indicating that we are ready to receive the data by deleting the
584 // property, however deleting only works if the type matches the
585 // property type. But the type didn't match in the previous call.
586 reply = reader
587 .conn
588 .get_property(
589 true,
590 event.requestor,
591 event.property,
592 self.atoms.INCR,
593 0,
594 u32::MAX / 4,
595 )
596 .map_err(into_unknown)?
597 .reply()
598 .map_err(into_unknown)?;
599 log::trace!("Receiving INCR segments");
600 *using_incr = true;
601 if reply.value_len == 4 {
602 let min_data_len = reply
603 .value32()
604 .and_then(|mut vals| vals.next())
605 .unwrap_or(0);
606 incr_data.reserve(min_data_len as usize);
607 }
608 Ok(ReadSelNotifyResult::IncrStarted)
609 } else {
610 Ok(ReadSelNotifyResult::GotData(ClipboardData {
611 bytes: reply.value,
612 format: reply.type_,
613 }))
614 }
615 }
616
617 /// Returns Ok(true) when the incr_data is ready
618 fn handle_read_property_notify(
619 &self,
620 reader: &XContext,
621 target_format: u32,
622 using_incr: bool,
623 incr_data: &mut Vec<u8>,
624 timeout_end: &mut Instant,
625 event: PropertyNotifyEvent,
626 ) -> Result<bool> {
627 if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE {
628 return Ok(false);
629 }
630 if !using_incr {
631 // This must mean the selection owner received our request, and is
632 // now preparing the data
633 return Ok(false);
634 }
635 let reply = reader
636 .conn
637 .get_property(
638 true,
639 event.window,
640 event.atom,
641 if target_format == self.atoms.TARGETS {
642 self.atoms.ATOM
643 } else {
644 target_format
645 },
646 0,
647 u32::MAX / 4,
648 )
649 .map_err(into_unknown)?
650 .reply()
651 .map_err(into_unknown)?;
652
653 // log::trace!("Received segment. value_len {}", reply.value_len,);
654 if reply.value_len == 0 {
655 // This indicates that all the data has been sent.
656 return Ok(true);
657 }
658 incr_data.extend(reply.value);
659
660 // Let's reset our timeout, since we received a valid chunk.
661 *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR;
662
663 // Not yet complete
664 Ok(false)
665 }
666
667 fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
668 let selection = match self.kind_of(event.selection) {
669 Some(kind) => kind,
670 None => {
671 log::warn!(
672 "Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected."
673 );
674 return Ok(());
675 }
676 };
677
678 let success;
679 // we are asked for a list of supported conversion targets
680 if event.target == self.atoms.TARGETS {
681 log::trace!(
682 "Handling TARGETS, dst property is {}",
683 self.atom_name(event.property)
684 );
685 let mut targets = Vec::with_capacity(10);
686 targets.push(self.atoms.TARGETS);
687 targets.push(self.atoms.SAVE_TARGETS);
688 let data = self.selection_of(selection).data.read();
689 if let Some(data_list) = &*data {
690 for data in data_list {
691 targets.push(data.format);
692 if data.format == self.atoms.UTF8_STRING {
693 // When we are storing a UTF8 string,
694 // add all equivalent formats to the supported targets
695 targets.push(self.atoms.UTF8_MIME_0);
696 targets.push(self.atoms.UTF8_MIME_1);
697 }
698 }
699 }
700 self.server
701 .conn
702 .change_property32(
703 PropMode::REPLACE,
704 event.requestor,
705 event.property,
706 // TODO: change to `AtomEnum::ATOM`
707 self.atoms.ATOM,
708 &targets,
709 )
710 .map_err(into_unknown)?;
711 self.server.conn.flush().map_err(into_unknown)?;
712 success = true;
713 } else {
714 log::trace!("Handling request for (probably) the clipboard contents.");
715 let data = self.selection_of(selection).data.read();
716 if let Some(data_list) = &*data {
717 success = match data_list.iter().find(|d| d.format == event.target) {
718 Some(data) => {
719 self.server
720 .conn
721 .change_property8(
722 PropMode::REPLACE,
723 event.requestor,
724 event.property,
725 event.target,
726 &data.bytes,
727 )
728 .map_err(into_unknown)?;
729 self.server.conn.flush().map_err(into_unknown)?;
730 true
731 }
732 None => false,
733 };
734 } else {
735 // This must mean that we lost ownership of the data
736 // since the other side requested the selection.
737 // Let's respond with the property set to none.
738 success = false;
739 }
740 }
741 // on failure we notify the requester of it
742 let property = if success {
743 event.property
744 } else {
745 AtomEnum::NONE.into()
746 };
747 // tell the requestor that we finished sending data
748 self.server
749 .conn
750 .send_event(
751 false,
752 event.requestor,
753 EventMask::NO_EVENT,
754 SelectionNotifyEvent {
755 response_type: SELECTION_NOTIFY_EVENT,
756 sequence: event.sequence,
757 time: event.time,
758 requestor: event.requestor,
759 selection: event.selection,
760 target: event.target,
761 property,
762 },
763 )
764 .map_err(into_unknown)?;
765
766 self.server.conn.flush().map_err(into_unknown)
767 }
768
769 fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> {
770 if self.server.win_id == 0 {
771 // This shouldn't really ever happen but let's just check.
772 log::error!("The server's window id was 0. This is unexpected");
773 return Ok(());
774 }
775
776 if !self.is_owner(ClipboardKind::Clipboard)? {
777 // We are not owning the clipboard, nothing to do.
778 return Ok(());
779 }
780 if self
781 .selection_of(ClipboardKind::Clipboard)
782 .data
783 .read()
784 .is_none()
785 {
786 // If we don't have any data, there's nothing to do.
787 return Ok(());
788 }
789
790 // It's important that we lock the state before sending the request
791 // because we don't want the request server thread to lock the state
792 // after the request but before we can lock it here.
793 let mut handover_state = self.handover_state.lock();
794
795 log::trace!("Sending the data to the clipboard manager");
796 self.server
797 .conn
798 .convert_selection(
799 self.server.win_id,
800 self.atoms.CLIPBOARD_MANAGER,
801 self.atoms.SAVE_TARGETS,
802 self.atoms.ARBOARD_CLIPBOARD,
803 Time::CURRENT_TIME,
804 )
805 .map_err(into_unknown)?;
806 self.server.conn.flush().map_err(into_unknown)?;
807
808 *handover_state = ManagerHandoverState::InProgress;
809 let max_handover_duration = Duration::from_millis(100);
810
811 // Note that we are using a parking_lot condvar here, which doesn't wake up
812 // spuriously
813 let result = self
814 .handover_cv
815 .wait_for(&mut handover_state, max_handover_duration);
816
817 if *handover_state == ManagerHandoverState::Finished {
818 return Ok(());
819 }
820 if result.timed_out() {
821 log::warn!(
822 "Could not hand the clipboard contents over to the clipboard manager. The request timed out."
823 );
824 return Ok(());
825 }
826
827 Err(Error::unknown(
828 "The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable.",
829 ))
830 }
831}
832
833fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>> {
834 fn handover_finished(clip: &Arc<Inner>, mut handover_state: MutexGuard<ManagerHandoverState>) {
835 log::trace!("Finishing clipboard manager handover.");
836 *handover_state = ManagerHandoverState::Finished;
837
838 // Not sure if unlocking the mutex is necessary here but better safe than sorry.
839 drop(handover_state);
840
841 clip.handover_cv.notify_all();
842 }
843
844 log::trace!("Started serve requests thread.");
845
846 let _guard = util::defer(|| {
847 context.serve_stopped.store(true, Ordering::Relaxed);
848 });
849
850 let mut written = false;
851 let mut notified = false;
852
853 loop {
854 match context.server.conn.wait_for_event().map_err(into_unknown)? {
855 Event::DestroyNotify(_) => {
856 // This window is being destroyed.
857 log::trace!("Clipboard server window is being destroyed x_x");
858 return Ok(());
859 }
860 Event::SelectionClear(event) => {
861 // TODO: check if this works
862 // Someone else has new content in the clipboard, so it is
863 // notifying us that we should delete our data now.
864 log::trace!("Somebody else owns the clipboard now");
865
866 if let Some(selection) = context.kind_of(event.selection) {
867 let selection = context.selection_of(selection);
868 let mut data_guard = selection.data.write();
869 *data_guard = None;
870
871 // It is important that this mutex is locked at the time of calling
872 // `notify_all` to prevent notifications getting lost in case the sleeping
873 // thread has unlocked its `data_guard` and is just about to sleep.
874 // It is also important that the RwLock is kept write-locked for the same
875 // reason.
876 let _guard = selection.mutex.lock();
877 selection.data_changed.notify_all();
878 }
879 }
880 Event::SelectionRequest(event) => {
881 log::trace!(
882 "SelectionRequest - selection is: {}, target is {}",
883 context.atom_name(event.selection),
884 context.atom_name(event.target),
885 );
886 // Someone is requesting the clipboard content from us.
887 context
888 .handle_selection_request(event)
889 .map_err(into_unknown)?;
890
891 // if we are in the progress of saving to the clipboard manager
892 // make sure we save that we have finished writing
893 let handover_state = context.handover_state.lock();
894 if *handover_state == ManagerHandoverState::InProgress {
895 // Only set written, when the actual contents were written,
896 // not just a response to what TARGETS we have.
897 if event.target != context.atoms.TARGETS {
898 log::trace!("The contents were written to the clipboard manager.");
899 written = true;
900 // if we have written and notified, make sure to notify that we are done
901 if notified {
902 handover_finished(&context, handover_state);
903 }
904 }
905 }
906 }
907 Event::SelectionNotify(event) => {
908 // We've requested the clipboard content and this is the answer.
909 // Considering that this thread is not responsible for reading
910 // clipboard contents, this must come from the clipboard manager
911 // signaling that the data was handed over successfully.
912 if event.selection != context.atoms.CLIPBOARD_MANAGER {
913 log::error!(
914 "Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread."
915 );
916 continue;
917 }
918 let handover_state = context.handover_state.lock();
919 if *handover_state == ManagerHandoverState::InProgress {
920 // Note that some clipboard managers send a selection notify
921 // before even sending a request for the actual contents.
922 // (That's why we use the "notified" & "written" flags)
923 log::trace!(
924 "The clipboard manager indicated that it's done requesting the contents from us."
925 );
926 notified = true;
927
928 // One would think that we could also finish if the property
929 // here is set 0, because that indicates failure. However
930 // this is not the case; for example on KDE plasma 5.18, we
931 // immediately get a SelectionNotify with property set to 0,
932 // but following that, we also get a valid SelectionRequest
933 // from the clipboard manager.
934 if written {
935 handover_finished(&context, handover_state);
936 }
937 }
938 }
939 _event => {
940 // May be useful for debugging but nothing else really.
941 //log::trace!("Received unwanted event: {:?}", event);
942 }
943 }
944 }
945}
946
947pub(crate) struct Clipboard {
948 inner: Arc<Inner>,
949}
950
951impl Clipboard {
952 pub(crate) fn new() -> Result<Self> {
953 let mut global_cb = CLIPBOARD.lock();
954 if let Some(global_cb) = &*global_cb {
955 return Ok(Self {
956 inner: Arc::clone(&global_cb.inner),
957 });
958 }
959 // At this point we know that the clipboard does not exist.
960 let ctx = Arc::new(Inner::new()?);
961 let join_handle = std::thread::Builder::new()
962 .name("Clipboard".to_owned())
963 .spawn({
964 let ctx = Arc::clone(&ctx);
965 move || {
966 if let Err(error) = serve_requests(ctx) {
967 log::error!("Worker thread errored with: {}", error);
968 }
969 }
970 })
971 .unwrap();
972 *global_cb = Some(GlobalClipboard {
973 inner: Arc::clone(&ctx),
974 server_handle: join_handle,
975 });
976 Ok(Self { inner: ctx })
977 }
978
979 pub(crate) fn set_text(
980 &self,
981 message: Cow<'_, str>,
982 selection: ClipboardKind,
983 wait: WaitConfig,
984 ) -> Result<()> {
985 let data = vec![ClipboardData {
986 bytes: message.into_owned().into_bytes(),
987 format: self.inner.atoms.UTF8_STRING,
988 }];
989 self.inner.write(data, selection, wait)
990 }
991
992 #[allow(unused)]
993 pub(crate) fn set_image(
994 &self,
995 image: Image,
996 selection: ClipboardKind,
997 wait: WaitConfig,
998 ) -> Result<()> {
999 let format = match image.format {
1000 ImageFormat::Png => self.inner.atoms.PNG__MIME,
1001 ImageFormat::Jpeg => self.inner.atoms.JPEG_MIME,
1002 ImageFormat::Webp => self.inner.atoms.WEBP_MIME,
1003 ImageFormat::Gif => self.inner.atoms.GIF__MIME,
1004 ImageFormat::Svg => self.inner.atoms.SVG__MIME,
1005 ImageFormat::Bmp => self.inner.atoms.BMP__MIME,
1006 ImageFormat::Tiff => self.inner.atoms.TIFF_MIME,
1007 ImageFormat::Ico => self.inner.atoms.ICO__MIME,
1008 };
1009 let data = vec![ClipboardData {
1010 bytes: image.bytes,
1011 format: self.inner.atoms.PNG__MIME,
1012 }];
1013 self.inner.write(data, selection, wait)
1014 }
1015
1016 pub(crate) fn get_any(&self, selection: ClipboardKind) -> Result<ClipboardItem> {
1017 const IMAGE_FORMAT_COUNT: usize = 7;
1018 let image_format_atoms: [Atom; IMAGE_FORMAT_COUNT] = [
1019 self.inner.atoms.PNG__MIME,
1020 self.inner.atoms.JPEG_MIME,
1021 self.inner.atoms.WEBP_MIME,
1022 self.inner.atoms.GIF__MIME,
1023 self.inner.atoms.SVG__MIME,
1024 self.inner.atoms.BMP__MIME,
1025 self.inner.atoms.TIFF_MIME,
1026 ];
1027 let image_formats: [ImageFormat; IMAGE_FORMAT_COUNT] = [
1028 ImageFormat::Png,
1029 ImageFormat::Jpeg,
1030 ImageFormat::Webp,
1031 ImageFormat::Gif,
1032 ImageFormat::Svg,
1033 ImageFormat::Bmp,
1034 ImageFormat::Tiff,
1035 ];
1036
1037 const TEXT_FORMAT_COUNT: usize = 6;
1038 let text_format_atoms: [Atom; TEXT_FORMAT_COUNT] = [
1039 self.inner.atoms.UTF8_STRING,
1040 self.inner.atoms.UTF8_MIME_0,
1041 self.inner.atoms.UTF8_MIME_1,
1042 self.inner.atoms.STRING,
1043 self.inner.atoms.TEXT,
1044 self.inner.atoms.TEXT_MIME_UNKNOWN,
1045 ];
1046
1047 let atom_none: Atom = AtomEnum::NONE.into();
1048
1049 const FORMAT_ATOM_COUNT: usize = TEXT_FORMAT_COUNT + IMAGE_FORMAT_COUNT;
1050
1051 let mut format_atoms: [Atom; FORMAT_ATOM_COUNT] = [atom_none; FORMAT_ATOM_COUNT];
1052
1053 // image formats first, as they are more specific, and read will return the first
1054 // format that the contents can be converted to
1055 format_atoms[0..IMAGE_FORMAT_COUNT].copy_from_slice(&image_format_atoms);
1056 format_atoms[IMAGE_FORMAT_COUNT..].copy_from_slice(&text_format_atoms);
1057 debug_assert!(!format_atoms.contains(&atom_none));
1058
1059 let result = self.inner.read(&format_atoms, selection)?;
1060
1061 log::trace!(
1062 "read clipboard as format {:?}",
1063 self.inner.atom_name(result.format)
1064 );
1065
1066 for (format_atom, image_format) in image_format_atoms.into_iter().zip(image_formats) {
1067 if result.format == format_atom {
1068 let bytes = result.bytes;
1069 let id = hash(&bytes);
1070 return Ok(ClipboardItem::new_image(&Image {
1071 id,
1072 format: image_format,
1073 bytes,
1074 }));
1075 }
1076 }
1077
1078 let text = if result.format == self.inner.atoms.STRING {
1079 // ISO Latin-1
1080 // See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8
1081 result.bytes.into_iter().map(|c| c as char).collect()
1082 } else {
1083 String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)?
1084 };
1085 Ok(ClipboardItem::new_string(text))
1086 }
1087
1088 pub fn is_owner(&self, selection: ClipboardKind) -> bool {
1089 self.inner.is_owner(selection).unwrap_or(false)
1090 }
1091}
1092
1093impl Drop for Clipboard {
1094 fn drop(&mut self) {
1095 // There are always at least 3 owners:
1096 // the global, the server thread, and one `Clipboard::inner`
1097 const MIN_OWNERS: usize = 3;
1098
1099 // We start with locking the global guard to prevent race
1100 // conditions below.
1101 let mut global_cb = CLIPBOARD.lock();
1102 if Arc::strong_count(&self.inner) == MIN_OWNERS {
1103 // If the are the only owners of the clipboard are ourselves and
1104 // the global object, then we should destroy the global object,
1105 // and send the data to the clipboard manager
1106
1107 if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() {
1108 log::error!(
1109 "Could not hand the clipboard data over to the clipboard manager: {}",
1110 e
1111 );
1112 }
1113 let global_cb = global_cb.take();
1114 if let Err(e) = self
1115 .inner
1116 .server
1117 .conn
1118 .destroy_window(self.inner.server.win_id)
1119 {
1120 log::error!("Failed to destroy the clipboard window. Error: {}", e);
1121 return;
1122 }
1123 if let Err(e) = self.inner.server.conn.flush() {
1124 log::error!("Failed to flush the clipboard window. Error: {}", e);
1125 return;
1126 }
1127 if let Some(global_cb) = global_cb
1128 && let Err(e) = global_cb.server_handle.join()
1129 {
1130 // Let's try extracting the error message
1131 let message;
1132 if let Some(msg) = e.downcast_ref::<&'static str>() {
1133 message = Some((*msg).to_string());
1134 } else if let Some(msg) = e.downcast_ref::<String>() {
1135 message = Some(msg.clone());
1136 } else {
1137 message = None;
1138 }
1139 if let Some(message) = message {
1140 log::error!(
1141 "The clipboard server thread panicked. Panic message: '{}'",
1142 message,
1143 );
1144 } else {
1145 log::error!("The clipboard server thread panicked.");
1146 }
1147 }
1148 }
1149 }
1150}
1151
1152fn into_unknown<E: std::fmt::Display>(error: E) -> Error {
1153 Error::Unknown {
1154 description: error.to_string(),
1155 }
1156}
1157
1158/// Clipboard selection
1159///
1160/// Linux has a concept of clipboard "selections" which tend to be used in different contexts. This
1161/// enum provides a way to get/set to a specific clipboard
1162///
1163/// See <https://specifications.freedesktop.org/clipboards-spec/clipboards-0.1.txt> for a better
1164/// description of the different clipboards.
1165#[derive(Copy, Clone, Debug)]
1166pub enum ClipboardKind {
1167 /// Typically used selection for explicit cut/copy/paste actions (ie. windows/macos like
1168 /// clipboard behavior)
1169 Clipboard,
1170
1171 /// Typically used for mouse selections and/or currently selected text. Accessible via middle
1172 /// mouse click.
1173 Primary,
1174
1175 /// The secondary clipboard is rarely used but theoretically available on X11.
1176 Secondary,
1177}
1178
1179/// Configuration on how long to wait for a new X11 copy event is emitted.
1180#[derive(Default)]
1181pub(crate) enum WaitConfig {
1182 /// Waits until the given [`Instant`] has reached.
1183 #[allow(
1184 unused,
1185 reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future"
1186 )]
1187 Until(Instant),
1188
1189 /// Waits forever until a new event is reached.
1190 #[allow(unused)]
1191 #[allow(
1192 unused,
1193 reason = "Right now we don't wait for clipboard contents to sync on app close, but we may in the future"
1194 )]
1195 Forever,
1196
1197 /// It shouldn't wait.
1198 #[default]
1199 None,
1200}
1201
1202#[non_exhaustive]
1203pub enum Error {
1204 /// The clipboard contents were not available in the requested format.
1205 /// This could either be due to the clipboard being empty or the clipboard contents having
1206 /// an incompatible format to the requested one (eg when calling `get_image` on text)
1207 ContentNotAvailable,
1208
1209 /// The native clipboard is not accessible due to being held by an other party.
1210 ///
1211 /// This "other party" could be a different process or it could be within
1212 /// the same program. So for example you may get this error when trying
1213 /// to interact with the clipboard from multiple threads at once.
1214 ///
1215 /// Note that it's OK to have multiple `Clipboard` instances. The underlying
1216 /// implementation will make sure that the native clipboard is only
1217 /// opened for transferring data and then closed as soon as possible.
1218 ClipboardOccupied,
1219
1220 /// The image or the text that was about the be transferred to/from the clipboard could not be
1221 /// converted to the appropriate format.
1222 ConversionFailure,
1223
1224 /// Any error that doesn't fit the other error types.
1225 ///
1226 /// The `description` field is only meant to help the developer and should not be relied on as a
1227 /// means to identify an error case during runtime.
1228 Unknown { description: String },
1229}
1230
1231impl std::fmt::Display for Error {
1232 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1233 match self {
1234 Error::ContentNotAvailable => f.write_str("The clipboard contents were not available in the requested format or the clipboard is empty."),
1235 Error::ClipboardOccupied => f.write_str("The native clipboard is not accessible due to being held by an other party."),
1236 Error::ConversionFailure => f.write_str("The image or the text that was about the be transferred to/from the clipboard could not be converted to the appropriate format."),
1237 Error::Unknown { description } => f.write_fmt(format_args!("Unknown error while interacting with the clipboard: {description}")),
1238 }
1239 }
1240}
1241
1242impl std::error::Error for Error {}
1243
1244impl std::fmt::Debug for Error {
1245 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1246 use Error::*;
1247 macro_rules! kind_to_str {
1248 ($( $e: pat ),*) => {
1249 match self {
1250 $(
1251 $e => stringify!($e),
1252 )*
1253 }
1254 }
1255 }
1256 let name = kind_to_str!(
1257 ContentNotAvailable,
1258 ClipboardOccupied,
1259 ConversionFailure,
1260 Unknown { .. }
1261 );
1262 f.write_fmt(format_args!("{name} - \"{self}\""))
1263 }
1264}
1265
1266impl Error {
1267 pub(crate) fn unknown<M: Into<String>>(message: M) -> Self {
1268 Error::Unknown {
1269 description: message.into(),
1270 }
1271 }
1272}