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