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