1use std::{
16 borrow::Cow,
17 cell::RefCell,
18 collections::{hash_map::Entry, HashMap},
19 path::{Path, PathBuf},
20 sync::{
21 atomic::{AtomicBool, Ordering},
22 Arc,
23 },
24 thread::JoinHandle,
25 thread_local,
26 time::{Duration, Instant},
27};
28
29use log::{error, trace, warn};
30use parking_lot::{Condvar, Mutex, MutexGuard, RwLock};
31use x11rb::{
32 connection::Connection,
33 protocol::{
34 xproto::{
35 Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property,
36 PropertyNotifyEvent, SelectionNotifyEvent, SelectionRequestEvent, Time, WindowClass,
37 SELECTION_NOTIFY_EVENT,
38 },
39 Event,
40 },
41 rust_connection::RustConnection,
42 wrapper::ConnectionExt as _,
43 COPY_DEPTH_FROM_PARENT, COPY_FROM_PARENT, NONE,
44};
45
46#[cfg(feature = "image-data")]
47use super::encode_as_png;
48use super::{
49 into_unknown, paths_from_uri_list, paths_to_uri_list, LinuxClipboardKind, WaitConfig,
50 KDE_EXCLUSION_HINT, KDE_EXCLUSION_MIME,
51};
52#[cfg(feature = "image-data")]
53use crate::ImageData;
54use crate::{common::ScopeGuard, Error};
55
56type Result<T, E = Error> = std::result::Result<T, E>;
57
58static CLIPBOARD: Mutex<Option<GlobalClipboard>> = parking_lot::const_mutex(None);
59
60x11rb::atom_manager! {
61 pub Atoms: AtomCookies {
62 CLIPBOARD,
63 PRIMARY,
64 SECONDARY,
65
66 CLIPBOARD_MANAGER,
67 SAVE_TARGETS,
68 TARGETS,
69 ATOM,
70 INCR,
71
72 UTF8_STRING,
73 UTF8_MIME_0: b"text/plain;charset=utf-8",
74 UTF8_MIME_1: b"text/plain;charset=UTF-8",
75 STRING,
78 TEXT,
81 TEXT_MIME_UNKNOWN: b"text/plain",
82
83 HTML: b"text/html",
84 URI_LIST: b"text/uri-list",
85
86 PNG_MIME: b"image/png",
87 X_KDE_PASSWORDMANAGERHINT: KDE_EXCLUSION_MIME.as_bytes(),
88
89 ARBOARD_CLIPBOARD,
92 }
93}
94
95thread_local! {
96 static ATOM_NAME_CACHE: RefCell<HashMap<Atom, &'static str>> = Default::default();
97}
98
99const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000);
102const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10);
103
104#[derive(Debug, PartialEq, Eq)]
105enum ManagerHandoverState {
106 Idle,
107 InProgress,
108 Finished,
109}
110
111struct GlobalClipboard {
112 inner: Arc<Inner>,
113
114 server_handle: JoinHandle<()>,
116}
117
118struct XContext {
119 conn: RustConnection,
120 win_id: u32,
121}
122
123struct Inner {
124 server: XContext,
127 atoms: Atoms,
128
129 clipboard: Selection,
130 primary: Selection,
131 secondary: Selection,
132
133 handover_state: Mutex<ManagerHandoverState>,
134 handover_cv: Condvar,
135
136 serve_stopped: AtomicBool,
137}
138
139impl XContext {
140 fn new() -> Result<Self> {
141 let (conn, screen_num): (RustConnection, _) =
143 RustConnection::connect(None).map_err(|_| {
144 Error::unknown("X11 server connection timed out because it was unreachable")
145 })?;
146 let screen = conn.setup().roots.get(screen_num).ok_or(Error::unknown("no screen found"))?;
147 let win_id = conn.generate_id().map_err(into_unknown)?;
148
149 let event_mask =
150 EventMask::PROPERTY_CHANGE |
153 EventMask::STRUCTURE_NOTIFY;
155 conn.create_window(
157 COPY_DEPTH_FROM_PARENT,
159 win_id,
160 screen.root,
161 0,
162 0,
163 1,
164 1,
165 0,
166 WindowClass::COPY_FROM_PARENT,
167 COPY_FROM_PARENT,
168 &CreateWindowAux::new().event_mask(event_mask),
170 )
171 .map_err(into_unknown)?;
172 conn.flush().map_err(into_unknown)?;
173
174 Ok(Self { conn, win_id })
175 }
176}
177
178#[derive(Default)]
179struct Selection {
180 data: RwLock<Option<Vec<ClipboardData>>>,
181 mutex: Mutex<Option<Instant>>,
184 data_changed: Condvar,
188 data_served: Condvar,
192}
193
194#[derive(Debug, Clone)]
195struct ClipboardData {
196 bytes: Vec<u8>,
197
198 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 =
212 Atoms::new(&server.conn).map_err(into_unknown)?.reply().map_err(into_unknown)?;
213
214 Ok(Self {
215 server,
216 atoms,
217 clipboard: Selection::default(),
218 primary: Selection::default(),
219 secondary: Selection::default(),
220 handover_state: Mutex::new(ManagerHandoverState::Idle),
221 handover_cv: Condvar::new(),
222 serve_stopped: AtomicBool::new(false),
223 })
224 }
225
226 fn clear(&self, selection: LinuxClipboardKind) -> Result<()> {
234 let selection = self.atom_of(selection);
235
236 self.server
237 .conn
238 .set_selection_owner(NONE, selection, Time::CURRENT_TIME)
239 .map_err(into_unknown)?;
240
241 self.server.conn.flush().map_err(into_unknown)
242 }
243
244 fn write(
245 &self,
246 data: Vec<ClipboardData>,
247 clipboard_selection: LinuxClipboardKind,
248 wait: WaitConfig,
249 ) -> Result<()> {
250 if self.serve_stopped.load(Ordering::Relaxed) {
251 return Err(Error::unknown("The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)"));
252 }
253
254 let server_win = self.server.win_id;
255
256 let selection = self.selection_of(clipboard_selection);
258 let mut data_guard = selection.data.write();
259 *data_guard = Some(data);
260
261 self.server
264 .conn
265 .set_selection_owner(server_win, self.atom_of(clipboard_selection), Time::CURRENT_TIME)
266 .map_err(|_| Error::ClipboardOccupied)?;
267
268 self.server.conn.flush().map_err(into_unknown)?;
269
270 let mut guard = selection.mutex.lock();
274 *guard = Some(Instant::now());
276
277 selection.data_changed.notify_all();
280
281 match wait {
282 WaitConfig::None => {}
283 WaitConfig::Forever => {
284 drop(data_guard);
285 selection.data_changed.wait(&mut guard);
286 }
287
288 WaitConfig::Until(deadline) => {
289 drop(data_guard);
290 selection.data_changed.wait_until(&mut guard, deadline);
291 }
292
293 WaitConfig::Once => {
294 drop(data_guard);
295 selection.data_served.wait(&mut guard);
296 }
297 }
298
299 Ok(())
300 }
301
302 fn read(&self, formats: &[Atom], selection: LinuxClipboardKind) -> Result<ClipboardData> {
306 if self.is_owner(selection)? {
308 let data = self.selection_of(selection).data.read();
309 if let Some(data_list) = &*data {
310 for data in data_list {
311 for format in formats {
312 if *format == data.format {
313 return Ok(data.clone());
314 }
315 }
316 }
317 }
318 return Err(Error::ContentNotAvailable);
319 }
320 let reader = XContext::new()?;
324
325 trace!("Trying to get the clipboard data.");
326 for format in formats {
327 match self.read_single(&reader, selection, *format) {
328 Ok(bytes) => {
329 return Ok(ClipboardData { bytes, format: *format });
330 }
331 Err(Error::ContentNotAvailable) => {
332 continue;
333 }
334 Err(e) => return Err(e),
335 }
336 }
337 Err(Error::ContentNotAvailable)
338 }
339
340 fn read_single(
341 &self,
342 reader: &XContext,
343 selection: LinuxClipboardKind,
344 target_format: Atom,
345 ) -> Result<Vec<u8>> {
346 reader
349 .conn
350 .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD)
351 .map_err(into_unknown)?;
352
353 reader
355 .conn
356 .convert_selection(
357 reader.win_id,
358 self.atom_of(selection),
359 target_format,
360 self.atoms.ARBOARD_CLIPBOARD,
361 Time::CURRENT_TIME,
362 )
363 .map_err(into_unknown)?;
364 reader.conn.sync().map_err(into_unknown)?;
365
366 trace!("Finished `convert_selection`");
367
368 let mut incr_data: Vec<u8> = Vec::new();
369 let mut using_incr = false;
370
371 let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR;
372
373 while Instant::now() < timeout_end {
374 let event = reader.conn.poll_for_event().map_err(into_unknown)?;
375 let event = match event {
376 Some(e) => e,
377 None => {
378 std::thread::sleep(Duration::from_millis(1));
379 continue;
380 }
381 };
382 match event {
383 Event::SelectionNotify(event) => {
385 trace!("Read SelectionNotify");
386 let result = self.handle_read_selection_notify(
387 reader,
388 target_format,
389 &mut using_incr,
390 &mut incr_data,
391 event,
392 )?;
393 match result {
394 ReadSelNotifyResult::GotData(data) => return Ok(data),
395 ReadSelNotifyResult::IncrStarted => {
396 timeout_end += SHORT_TIMEOUT_DUR;
400 }
401 ReadSelNotifyResult::EventNotRecognized => (),
402 }
403 }
404 Event::PropertyNotify(event) => {
408 let result = self.handle_read_property_notify(
409 reader,
410 target_format,
411 using_incr,
412 &mut incr_data,
413 &mut timeout_end,
414 event,
415 )?;
416 if result {
417 return Ok(incr_data);
418 }
419 }
420 _ => log::trace!("An unexpected event arrived while reading the clipboard."),
421 }
422 }
423 log::info!("Time-out hit while reading the clipboard.");
424 Err(Error::ContentNotAvailable)
425 }
426
427 fn atom_of(&self, selection: LinuxClipboardKind) -> Atom {
428 match selection {
429 LinuxClipboardKind::Clipboard => self.atoms.CLIPBOARD,
430 LinuxClipboardKind::Primary => self.atoms.PRIMARY,
431 LinuxClipboardKind::Secondary => self.atoms.SECONDARY,
432 }
433 }
434
435 fn selection_of(&self, selection: LinuxClipboardKind) -> &Selection {
436 match selection {
437 LinuxClipboardKind::Clipboard => &self.clipboard,
438 LinuxClipboardKind::Primary => &self.primary,
439 LinuxClipboardKind::Secondary => &self.secondary,
440 }
441 }
442
443 fn kind_of(&self, atom: Atom) -> Option<LinuxClipboardKind> {
444 match atom {
445 a if a == self.atoms.CLIPBOARD => Some(LinuxClipboardKind::Clipboard),
446 a if a == self.atoms.PRIMARY => Some(LinuxClipboardKind::Primary),
447 a if a == self.atoms.SECONDARY => Some(LinuxClipboardKind::Secondary),
448 _ => None,
449 }
450 }
451
452 fn is_owner(&self, selection: LinuxClipboardKind) -> Result<bool> {
453 let current = self
454 .server
455 .conn
456 .get_selection_owner(self.atom_of(selection))
457 .map_err(into_unknown)?
458 .reply()
459 .map_err(into_unknown)?
460 .owner;
461
462 Ok(current == self.server.win_id)
463 }
464
465 fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
466 String::from_utf8(
467 self.server
468 .conn
469 .get_atom_name(atom)
470 .map_err(into_unknown)?
471 .reply()
472 .map_err(into_unknown)?
473 .name,
474 )
475 .map_err(into_unknown)
476 }
477 fn atom_name_dbg(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
478 ATOM_NAME_CACHE.with(|cache| {
479 let mut cache = cache.borrow_mut();
480 match cache.entry(atom) {
481 Entry::Occupied(entry) => *entry.get(),
482 Entry::Vacant(entry) => {
483 let s = self
484 .atom_name(atom)
485 .map(|s| Box::leak(s.into_boxed_str()) as &str)
486 .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME");
487 entry.insert(s);
488 s
489 }
490 }
491 })
492 }
493
494 fn handle_read_selection_notify(
495 &self,
496 reader: &XContext,
497 target_format: u32,
498 using_incr: &mut bool,
499 incr_data: &mut Vec<u8>,
500 event: SelectionNotifyEvent,
501 ) -> Result<ReadSelNotifyResult> {
502 if event.property == NONE || event.target != target_format {
508 return Err(Error::ContentNotAvailable);
509 }
510 if self.kind_of(event.selection).is_none() {
511 log::info!("Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
512 return Ok(ReadSelNotifyResult::EventNotRecognized);
513 }
514 if *using_incr {
515 log::warn!("Received a SelectionNotify while already expecting INCR segments.");
516 return Ok(ReadSelNotifyResult::EventNotRecognized);
517 }
518 let mut reply = reader
520 .conn
521 .get_property(true, event.requestor, event.property, event.target, 0, u32::MAX / 4)
522 .map_err(into_unknown)?
523 .reply()
524 .map_err(into_unknown)?;
525
526 if reply.type_ == target_format {
530 Ok(ReadSelNotifyResult::GotData(reply.value))
531 } else if reply.type_ == self.atoms.INCR {
532 reply = reader
537 .conn
538 .get_property(
539 true,
540 event.requestor,
541 event.property,
542 self.atoms.INCR,
543 0,
544 u32::MAX / 4,
545 )
546 .map_err(into_unknown)?
547 .reply()
548 .map_err(into_unknown)?;
549 log::trace!("Receiving INCR segments");
550 *using_incr = true;
551 if reply.value_len == 4 {
552 let min_data_len = reply.value32().and_then(|mut vals| vals.next()).unwrap_or(0);
553 incr_data.reserve(min_data_len as usize);
554 }
555 Ok(ReadSelNotifyResult::IncrStarted)
556 } else {
557 Err(Error::unknown("incorrect type received from clipboard"))
559 }
560 }
561
562 fn handle_read_property_notify(
564 &self,
565 reader: &XContext,
566 target_format: u32,
567 using_incr: bool,
568 incr_data: &mut Vec<u8>,
569 timeout_end: &mut Instant,
570 event: PropertyNotifyEvent,
571 ) -> Result<bool> {
572 if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE {
573 return Ok(false);
574 }
575 if !using_incr {
576 return Ok(false);
579 }
580 let reply = reader
581 .conn
582 .get_property(true, event.window, event.atom, target_format, 0, u32::MAX / 4)
583 .map_err(into_unknown)?
584 .reply()
585 .map_err(into_unknown)?;
586
587 if reply.value_len == 0 {
589 return Ok(true);
591 }
592 incr_data.extend(reply.value);
593
594 *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR;
596
597 Ok(false)
599 }
600
601 fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
602 let selection = match self.kind_of(event.selection) {
603 Some(kind) => kind,
604 None => {
605 warn!("Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
606 return Ok(());
607 }
608 };
609
610 let success;
611 if event.target == self.atoms.TARGETS {
613 trace!("Handling TARGETS, dst property is {}", self.atom_name_dbg(event.property));
614
615 let data = self.selection_of(selection).data.read();
616 let (data_targets, excluded) = if let Some(data_list) = &*data {
617 let mut targets = Vec::with_capacity(data_list.len() + 3);
619 let mut excluded = false;
620
621 for data in data_list {
622 targets.push(data.format);
623 if data.format == self.atoms.UTF8_STRING {
624 targets.push(self.atoms.UTF8_MIME_0);
627 targets.push(self.atoms.UTF8_MIME_1);
628 }
629
630 if data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT {
631 excluded = true;
632 }
633 }
634 (targets, excluded)
635 } else {
636 (Vec::with_capacity(2), false)
638 };
639
640 let mut targets = data_targets;
641 targets.push(self.atoms.TARGETS);
642
643 if !excluded {
652 targets.push(self.atoms.SAVE_TARGETS);
653 }
654
655 self.server
656 .conn
657 .change_property32(
658 PropMode::REPLACE,
659 event.requestor,
660 event.property,
661 self.atoms.ATOM,
663 &targets,
664 )
665 .map_err(into_unknown)?;
666 self.server.conn.flush().map_err(into_unknown)?;
667 success = true;
668 } else {
669 trace!("Handling request for (probably) the clipboard contents.");
670 let data = self.selection_of(selection).data.read();
671 if let Some(data_list) = &*data {
672 success = match data_list.iter().find(|d| d.format == event.target) {
673 Some(data) => {
674 self.server
675 .conn
676 .change_property8(
677 PropMode::REPLACE,
678 event.requestor,
679 event.property,
680 event.target,
681 &data.bytes,
682 )
683 .map_err(into_unknown)?;
684 self.server.conn.flush().map_err(into_unknown)?;
685 true
686 }
687 None => false,
688 };
689 } else {
690 success = false;
694 }
695 }
696 let property = if success { event.property } else { AtomEnum::NONE.into() };
698 self.server
700 .conn
701 .send_event(
702 false,
703 event.requestor,
704 EventMask::NO_EVENT,
705 SelectionNotifyEvent {
706 response_type: SELECTION_NOTIFY_EVENT,
707 sequence: event.sequence,
708 time: event.time,
709 requestor: event.requestor,
710 selection: event.selection,
711 target: event.target,
712 property,
713 },
714 )
715 .map_err(into_unknown)?;
716
717 self.server.conn.flush().map_err(into_unknown)
718 }
719
720 fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> {
721 if self.server.win_id == 0 {
722 error!("The server's window id was 0. This is unexpected");
724 return Ok(());
725 }
726
727 let selection = LinuxClipboardKind::Clipboard;
731
732 if !self.is_owner(selection)? {
733 return Ok(());
735 }
736
737 match &*self.selection_of(selection).data.read() {
738 Some(data) => {
739 if data.iter().any(|data| data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT) {
748 if let Err(e) = self.clear(selection) {
758 warn!("failed to release sensitive data's clipboard ownership: {e}; it may end up persisted!");
759 }
761
762 return Ok(());
763 }
764 }
765 None => {
766 return Ok(());
768 }
769 }
770
771 let mut handover_state = self.handover_state.lock();
775
776 trace!("Sending the data to the clipboard manager");
777 self.server
778 .conn
779 .convert_selection(
780 self.server.win_id,
781 self.atoms.CLIPBOARD_MANAGER,
782 self.atoms.SAVE_TARGETS,
783 self.atoms.ARBOARD_CLIPBOARD,
784 Time::CURRENT_TIME,
785 )
786 .map_err(into_unknown)?;
787 self.server.conn.flush().map_err(into_unknown)?;
788
789 *handover_state = ManagerHandoverState::InProgress;
790 let max_handover_duration = Duration::from_millis(100);
791
792 let result = self.handover_cv.wait_for(&mut handover_state, max_handover_duration);
795
796 if *handover_state == ManagerHandoverState::Finished {
797 return Ok(());
798 }
799 if result.timed_out() {
800 warn!("Could not hand the clipboard contents over to the clipboard manager. The request timed out.");
801 return Ok(());
802 }
803
804 unreachable!("This is a bug! The handover was not finished and the condvar didn't time out, yet the condvar wait ended.")
805 }
806}
807
808fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>> {
809 fn handover_finished(clip: &Arc<Inner>, mut handover_state: MutexGuard<ManagerHandoverState>) {
810 log::trace!("Finishing clipboard manager handover.");
811 *handover_state = ManagerHandoverState::Finished;
812
813 drop(handover_state);
815
816 clip.handover_cv.notify_all();
817 }
818
819 trace!("Started serve requests thread.");
820
821 let _guard = ScopeGuard::new(|| {
822 context.serve_stopped.store(true, Ordering::Relaxed);
823 });
824
825 let mut written = false;
826 let mut notified = false;
827
828 loop {
829 match context.server.conn.wait_for_event().map_err(into_unknown)? {
830 Event::DestroyNotify(_) => {
831 trace!("Clipboard server window is being destroyed x_x");
833 return Ok(());
834 }
835 Event::SelectionClear(event) => {
836 trace!("Somebody else owns the clipboard now");
840
841 if let Some(selection) = context.kind_of(event.selection) {
842 let selection = context.selection_of(selection);
843 let mut data_guard = selection.data.write();
844 *data_guard = None;
845
846 let _guard = selection.mutex.lock();
852 selection.data_changed.notify_all();
853 selection.data_served.notify_all();
856 }
857 }
858 Event::SelectionRequest(event) => {
859 trace!(
860 "SelectionRequest - selection is: {}, target is {}",
861 context.atom_name_dbg(event.selection),
862 context.atom_name_dbg(event.target),
863 );
864 if let Err(e) = context.handle_selection_request(event) {
866 error!("Failed to handle selection request: {e}");
867 continue;
868 }
869
870 if let Some(selection) = context.kind_of(event.selection) {
871 let selection = context.selection_of(selection);
872
873 let _guard = selection.mutex.lock();
877 selection.data_served.notify_all();
879 }
880
881 let handover_state = context.handover_state.lock();
884 if *handover_state == ManagerHandoverState::InProgress {
885 if event.target != context.atoms.TARGETS {
888 trace!("The contents were written to the clipboard manager.");
889 written = true;
890 if notified {
892 handover_finished(&context, handover_state);
893 }
894 }
895 }
896 }
897 Event::SelectionNotify(event) => {
898 if event.selection != context.atoms.CLIPBOARD_MANAGER {
903 error!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread.");
904 continue;
905 }
906 let handover_state = context.handover_state.lock();
907 if *handover_state == ManagerHandoverState::InProgress {
908 trace!("The clipboard manager indicated that it's done requesting the contents from us.");
912 notified = true;
913
914 if written {
921 handover_finished(&context, handover_state);
922 }
923 }
924 }
925 _event => {
926 }
929 }
930 }
931}
932
933pub(crate) struct Clipboard {
934 inner: Arc<Inner>,
935}
936
937impl Clipboard {
938 pub(crate) fn new() -> Result<Self> {
939 let mut global_cb = CLIPBOARD.lock();
940 if let Some(global_cb) = &*global_cb {
941 return Ok(Self { inner: Arc::clone(&global_cb.inner) });
942 }
943 let ctx = Arc::new(Inner::new()?);
945 let join_handle;
946 {
947 let ctx = Arc::clone(&ctx);
948 join_handle = std::thread::spawn(move || {
949 if let Err(error) = serve_requests(ctx) {
950 error!("Worker thread errored with: {}", error);
951 }
952 });
953 }
954 *global_cb = Some(GlobalClipboard { inner: Arc::clone(&ctx), server_handle: join_handle });
955 Ok(Self { inner: ctx })
956 }
957
958 fn add_clipboard_exclusions(&self, exclude_from_history: bool, data: &mut Vec<ClipboardData>) {
959 if exclude_from_history {
960 data.push(ClipboardData {
961 bytes: KDE_EXCLUSION_HINT.to_vec(),
962 format: self.inner.atoms.X_KDE_PASSWORDMANAGERHINT,
963 })
964 }
965 }
966
967 pub(crate) fn clear(&self, selection: LinuxClipboardKind) -> Result<()> {
968 self.inner.clear(selection)
969 }
970
971 pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result<String> {
972 let formats = [
973 self.inner.atoms.UTF8_STRING,
974 self.inner.atoms.UTF8_MIME_0,
975 self.inner.atoms.UTF8_MIME_1,
976 self.inner.atoms.STRING,
977 self.inner.atoms.TEXT,
978 self.inner.atoms.TEXT_MIME_UNKNOWN,
979 ];
980 let result = self.inner.read(&formats, selection)?;
981 if result.format == self.inner.atoms.STRING {
982 Ok(result.bytes.into_iter().map(|c| c as char).collect())
985 } else {
986 String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
987 }
988 }
989
990 pub(crate) fn set_text(
991 &self,
992 message: Cow<'_, str>,
993 selection: LinuxClipboardKind,
994 wait: WaitConfig,
995 exclude_from_history: bool,
996 ) -> Result<()> {
997 let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
998 data.push(ClipboardData {
999 bytes: message.into_owned().into_bytes(),
1000 format: self.inner.atoms.UTF8_STRING,
1001 });
1002
1003 self.add_clipboard_exclusions(exclude_from_history, &mut data);
1004
1005 self.inner.write(data, selection, wait)
1006 }
1007
1008 pub(crate) fn get_html(&self, selection: LinuxClipboardKind) -> Result<String> {
1009 let formats = [self.inner.atoms.HTML];
1010 let result = self.inner.read(&formats, selection)?;
1011 String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
1012 }
1013
1014 pub(crate) fn set_html(
1015 &self,
1016 html: Cow<'_, str>,
1017 alt: Option<Cow<'_, str>>,
1018 selection: LinuxClipboardKind,
1019 wait: WaitConfig,
1020 exclude_from_history: bool,
1021 ) -> Result<()> {
1022 let mut data = {
1023 let cap = [true, alt.is_some(), exclude_from_history]
1024 .map(|v| usize::from(v as u8))
1025 .iter()
1026 .sum();
1027 Vec::with_capacity(cap)
1028 };
1029
1030 if let Some(alt_text) = alt {
1031 data.push(ClipboardData {
1032 bytes: alt_text.into_owned().into_bytes(),
1033 format: self.inner.atoms.UTF8_STRING,
1034 });
1035 }
1036 data.push(ClipboardData {
1037 bytes: html.into_owned().into_bytes(),
1038 format: self.inner.atoms.HTML,
1039 });
1040
1041 self.add_clipboard_exclusions(exclude_from_history, &mut data);
1042
1043 self.inner.write(data, selection, wait)
1044 }
1045
1046 #[cfg(feature = "image-data")]
1047 pub(crate) fn get_image(&self, selection: LinuxClipboardKind) -> Result<ImageData<'static>> {
1048 let formats = [self.inner.atoms.PNG_MIME];
1049 let bytes = self.inner.read(&formats, selection)?.bytes;
1050
1051 let cursor = std::io::Cursor::new(&bytes);
1052 let mut reader = image::io::Reader::new(cursor);
1053 reader.set_format(image::ImageFormat::Png);
1054 let image = match reader.decode() {
1055 Ok(img) => img.into_rgba8(),
1056 Err(_e) => return Err(Error::ConversionFailure),
1057 };
1058 let (w, h) = image.dimensions();
1059 let image_data =
1060 ImageData { width: w as usize, height: h as usize, bytes: image.into_raw().into() };
1061 Ok(image_data)
1062 }
1063
1064 #[cfg(feature = "image-data")]
1065 pub(crate) fn set_image(
1066 &self,
1067 image: ImageData,
1068 selection: LinuxClipboardKind,
1069 wait: WaitConfig,
1070 exclude_from_history: bool,
1071 ) -> Result<()> {
1072 let encoded = encode_as_png(&image)?;
1073 let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
1074
1075 data.push(ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME });
1076
1077 self.add_clipboard_exclusions(exclude_from_history, &mut data);
1078
1079 self.inner.write(data, selection, wait)
1080 }
1081
1082 pub(crate) fn get_file_list(&self, selection: LinuxClipboardKind) -> Result<Vec<PathBuf>> {
1083 let result = self.inner.read(&[self.inner.atoms.URI_LIST], selection)?;
1084
1085 Ok(paths_from_uri_list(result.bytes))
1086 }
1087
1088 pub(crate) fn set_file_list(
1089 &self,
1090 file_list: &[impl AsRef<Path>],
1091 selection: LinuxClipboardKind,
1092 wait: WaitConfig,
1093 exclude_from_history: bool,
1094 ) -> Result<()> {
1095 let files = paths_to_uri_list(file_list)?;
1096 let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
1097
1098 data.push(ClipboardData { bytes: files.into_bytes(), format: self.inner.atoms.URI_LIST });
1099 self.add_clipboard_exclusions(exclude_from_history, &mut data);
1100
1101 self.inner.write(data, selection, wait)
1102 }
1103}
1104
1105impl Drop for Clipboard {
1106 fn drop(&mut self) {
1107 const MIN_OWNERS: usize = 3;
1110
1111 let mut global_cb = CLIPBOARD.lock();
1114 if Arc::strong_count(&self.inner) == MIN_OWNERS {
1115 if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() {
1120 error!("Could not hand the clipboard data over to the clipboard manager: {}", e);
1121 }
1122 let global_cb = global_cb.take();
1123 if let Err(e) = self.inner.server.conn.destroy_window(self.inner.server.win_id) {
1124 error!("Failed to destroy the clipboard window. Error: {}", e);
1125 return;
1126 }
1127 if let Err(e) = self.inner.server.conn.flush() {
1128 error!("Failed to flush the clipboard window. Error: {}", e);
1129 return;
1130 }
1131 if let Some(global_cb) = global_cb {
1132 let GlobalClipboard { inner, server_handle } = global_cb;
1133 drop(inner);
1134
1135 if let Err(e) = server_handle.join() {
1136 let message;
1138 if let Some(msg) = e.downcast_ref::<&'static str>() {
1139 message = Some((*msg).to_string());
1140 } else if let Some(msg) = e.downcast_ref::<String>() {
1141 message = Some(msg.clone());
1142 } else {
1143 message = None;
1144 }
1145 if let Some(message) = message {
1146 error!(
1147 "The clipboard server thread panicked. Panic message: '{}'",
1148 message,
1149 );
1150 } else {
1151 error!("The clipboard server thread panicked.");
1152 }
1153 }
1154
1155 #[cfg(debug_assertions)]
1162 if let Some(inner) = Arc::get_mut(&mut self.inner) {
1163 use std::io::IsTerminal;
1164
1165 let mut change_timestamps = Vec::with_capacity(2);
1166 let mut collect_changed = |sel: &mut Mutex<Option<Instant>>| {
1167 if let Some(changed) = sel.get_mut() {
1168 change_timestamps.push(*changed);
1169 }
1170 };
1171
1172 collect_changed(&mut inner.clipboard.mutex);
1173 collect_changed(&mut inner.primary.mutex);
1174 collect_changed(&mut inner.secondary.mutex);
1175
1176 change_timestamps.sort();
1177 if let Some(last) = change_timestamps.last() {
1178 let elapsed = last.elapsed().as_millis();
1179 if elapsed > 100 {
1183 return;
1184 }
1185
1186 let msg = format!("Clipboard was dropped very quickly after writing ({elapsed}ms); clipboard managers may not have seen the contents\nConsider keeping `Clipboard` in more persistent state somewhere or keeping the contents alive longer using `SetLinuxExt` and/or threads.");
1190 if std::io::stderr().is_terminal() {
1191 eprintln!("{msg}");
1192 } else {
1193 log::warn!("{msg}");
1194 }
1195 }
1196 }
1197 }
1198 }
1199 }
1200}