1use std::{
16 borrow::Cow,
17 cell::RefCell,
18 collections::{hash_map::Entry, HashMap},
19 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, LinuxClipboardKind, WaitConfig, KDE_EXCLUSION_HINT,
50 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}
189
190#[derive(Debug, Clone)]
191struct ClipboardData {
192 bytes: Vec<u8>,
193
194 format: Atom,
196}
197
198enum ReadSelNotifyResult {
199 GotData(Vec<u8>),
200 IncrStarted,
201 EventNotRecognized,
202}
203
204impl Inner {
205 fn new() -> Result<Self> {
206 let server = XContext::new()?;
207 let atoms =
208 Atoms::new(&server.conn).map_err(into_unknown)?.reply().map_err(into_unknown)?;
209
210 Ok(Self {
211 server,
212 atoms,
213 clipboard: Selection::default(),
214 primary: Selection::default(),
215 secondary: Selection::default(),
216 handover_state: Mutex::new(ManagerHandoverState::Idle),
217 handover_cv: Condvar::new(),
218 serve_stopped: AtomicBool::new(false),
219 })
220 }
221
222 fn clear(&self, selection: LinuxClipboardKind) -> Result<()> {
230 let selection = self.atom_of(selection);
231
232 self.server
233 .conn
234 .set_selection_owner(NONE, selection, Time::CURRENT_TIME)
235 .map_err(into_unknown)?;
236
237 self.server.conn.flush().map_err(into_unknown)
238 }
239
240 fn write(
241 &self,
242 data: Vec<ClipboardData>,
243 clipboard_selection: LinuxClipboardKind,
244 wait: WaitConfig,
245 ) -> Result<()> {
246 if self.serve_stopped.load(Ordering::Relaxed) {
247 return Err(Error::unknown("The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)"));
248 }
249
250 let server_win = self.server.win_id;
251
252 let selection = self.selection_of(clipboard_selection);
254 let mut data_guard = selection.data.write();
255 *data_guard = Some(data);
256
257 self.server
260 .conn
261 .set_selection_owner(server_win, self.atom_of(clipboard_selection), Time::CURRENT_TIME)
262 .map_err(|_| Error::ClipboardOccupied)?;
263
264 self.server.conn.flush().map_err(into_unknown)?;
265
266 let mut guard = selection.mutex.lock();
270 *guard = Some(Instant::now());
272
273 selection.data_changed.notify_all();
276
277 match wait {
278 WaitConfig::None => {}
279 WaitConfig::Forever => {
280 drop(data_guard);
281 selection.data_changed.wait(&mut guard);
282 }
283
284 WaitConfig::Until(deadline) => {
285 drop(data_guard);
286 selection.data_changed.wait_until(&mut guard, deadline);
287 }
288 }
289
290 Ok(())
291 }
292
293 fn read(&self, formats: &[Atom], selection: LinuxClipboardKind) -> Result<ClipboardData> {
297 if self.is_owner(selection)? {
299 let data = self.selection_of(selection).data.read();
300 if let Some(data_list) = &*data {
301 for data in data_list {
302 for format in formats {
303 if *format == data.format {
304 return Ok(data.clone());
305 }
306 }
307 }
308 }
309 return Err(Error::ContentNotAvailable);
310 }
311 let reader = XContext::new()?;
315
316 trace!("Trying to get the clipboard data.");
317 for format in formats {
318 match self.read_single(&reader, selection, *format) {
319 Ok(bytes) => {
320 return Ok(ClipboardData { bytes, format: *format });
321 }
322 Err(Error::ContentNotAvailable) => {
323 continue;
324 }
325 Err(e) => return Err(e),
326 }
327 }
328 Err(Error::ContentNotAvailable)
329 }
330
331 fn read_single(
332 &self,
333 reader: &XContext,
334 selection: LinuxClipboardKind,
335 target_format: Atom,
336 ) -> Result<Vec<u8>> {
337 reader
340 .conn
341 .delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD)
342 .map_err(into_unknown)?;
343
344 reader
346 .conn
347 .convert_selection(
348 reader.win_id,
349 self.atom_of(selection),
350 target_format,
351 self.atoms.ARBOARD_CLIPBOARD,
352 Time::CURRENT_TIME,
353 )
354 .map_err(into_unknown)?;
355 reader.conn.sync().map_err(into_unknown)?;
356
357 trace!("Finished `convert_selection`");
358
359 let mut incr_data: Vec<u8> = Vec::new();
360 let mut using_incr = false;
361
362 let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR;
363
364 while Instant::now() < timeout_end {
365 let event = reader.conn.poll_for_event().map_err(into_unknown)?;
366 let event = match event {
367 Some(e) => e,
368 None => {
369 std::thread::sleep(Duration::from_millis(1));
370 continue;
371 }
372 };
373 match event {
374 Event::SelectionNotify(event) => {
376 trace!("Read SelectionNotify");
377 let result = self.handle_read_selection_notify(
378 reader,
379 target_format,
380 &mut using_incr,
381 &mut incr_data,
382 event,
383 )?;
384 match result {
385 ReadSelNotifyResult::GotData(data) => return Ok(data),
386 ReadSelNotifyResult::IncrStarted => {
387 timeout_end += SHORT_TIMEOUT_DUR;
391 }
392 ReadSelNotifyResult::EventNotRecognized => (),
393 }
394 }
395 Event::PropertyNotify(event) => {
399 let result = self.handle_read_property_notify(
400 reader,
401 target_format,
402 using_incr,
403 &mut incr_data,
404 &mut timeout_end,
405 event,
406 )?;
407 if result {
408 return Ok(incr_data);
409 }
410 }
411 _ => log::trace!("An unexpected event arrived while reading the clipboard."),
412 }
413 }
414 log::info!("Time-out hit while reading the clipboard.");
415 Err(Error::ContentNotAvailable)
416 }
417
418 fn atom_of(&self, selection: LinuxClipboardKind) -> Atom {
419 match selection {
420 LinuxClipboardKind::Clipboard => self.atoms.CLIPBOARD,
421 LinuxClipboardKind::Primary => self.atoms.PRIMARY,
422 LinuxClipboardKind::Secondary => self.atoms.SECONDARY,
423 }
424 }
425
426 fn selection_of(&self, selection: LinuxClipboardKind) -> &Selection {
427 match selection {
428 LinuxClipboardKind::Clipboard => &self.clipboard,
429 LinuxClipboardKind::Primary => &self.primary,
430 LinuxClipboardKind::Secondary => &self.secondary,
431 }
432 }
433
434 fn kind_of(&self, atom: Atom) -> Option<LinuxClipboardKind> {
435 match atom {
436 a if a == self.atoms.CLIPBOARD => Some(LinuxClipboardKind::Clipboard),
437 a if a == self.atoms.PRIMARY => Some(LinuxClipboardKind::Primary),
438 a if a == self.atoms.SECONDARY => Some(LinuxClipboardKind::Secondary),
439 _ => None,
440 }
441 }
442
443 fn is_owner(&self, selection: LinuxClipboardKind) -> Result<bool> {
444 let current = self
445 .server
446 .conn
447 .get_selection_owner(self.atom_of(selection))
448 .map_err(into_unknown)?
449 .reply()
450 .map_err(into_unknown)?
451 .owner;
452
453 Ok(current == self.server.win_id)
454 }
455
456 fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
457 String::from_utf8(
458 self.server
459 .conn
460 .get_atom_name(atom)
461 .map_err(into_unknown)?
462 .reply()
463 .map_err(into_unknown)?
464 .name,
465 )
466 .map_err(into_unknown)
467 }
468 fn atom_name_dbg(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
469 ATOM_NAME_CACHE.with(|cache| {
470 let mut cache = cache.borrow_mut();
471 match cache.entry(atom) {
472 Entry::Occupied(entry) => *entry.get(),
473 Entry::Vacant(entry) => {
474 let s = self
475 .atom_name(atom)
476 .map(|s| Box::leak(s.into_boxed_str()) as &str)
477 .unwrap_or("FAILED-TO-GET-THE-ATOM-NAME");
478 entry.insert(s);
479 s
480 }
481 }
482 })
483 }
484
485 fn handle_read_selection_notify(
486 &self,
487 reader: &XContext,
488 target_format: u32,
489 using_incr: &mut bool,
490 incr_data: &mut Vec<u8>,
491 event: SelectionNotifyEvent,
492 ) -> Result<ReadSelNotifyResult> {
493 if event.property == NONE || event.target != target_format {
499 return Err(Error::ContentNotAvailable);
500 }
501 if self.kind_of(event.selection).is_none() {
502 log::info!("Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
503 return Ok(ReadSelNotifyResult::EventNotRecognized);
504 }
505 if *using_incr {
506 log::warn!("Received a SelectionNotify while already expecting INCR segments.");
507 return Ok(ReadSelNotifyResult::EventNotRecognized);
508 }
509 let mut reply = reader
511 .conn
512 .get_property(true, event.requestor, event.property, event.target, 0, u32::MAX / 4)
513 .map_err(into_unknown)?
514 .reply()
515 .map_err(into_unknown)?;
516
517 if reply.type_ == target_format {
521 Ok(ReadSelNotifyResult::GotData(reply.value))
522 } else if reply.type_ == self.atoms.INCR {
523 reply = reader
528 .conn
529 .get_property(
530 true,
531 event.requestor,
532 event.property,
533 self.atoms.INCR,
534 0,
535 u32::MAX / 4,
536 )
537 .map_err(into_unknown)?
538 .reply()
539 .map_err(into_unknown)?;
540 log::trace!("Receiving INCR segments");
541 *using_incr = true;
542 if reply.value_len == 4 {
543 let min_data_len = reply.value32().and_then(|mut vals| vals.next()).unwrap_or(0);
544 incr_data.reserve(min_data_len as usize);
545 }
546 Ok(ReadSelNotifyResult::IncrStarted)
547 } else {
548 Err(Error::unknown("incorrect type received from clipboard"))
550 }
551 }
552
553 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 return Ok(false);
570 }
571 let reply = reader
572 .conn
573 .get_property(true, event.window, event.atom, target_format, 0, u32::MAX / 4)
574 .map_err(into_unknown)?
575 .reply()
576 .map_err(into_unknown)?;
577
578 if reply.value_len == 0 {
580 return Ok(true);
582 }
583 incr_data.extend(reply.value);
584
585 *timeout_end = Instant::now() + SHORT_TIMEOUT_DUR;
587
588 Ok(false)
590 }
591
592 fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
593 let selection = match self.kind_of(event.selection) {
594 Some(kind) => kind,
595 None => {
596 warn!("Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
597 return Ok(());
598 }
599 };
600
601 let success;
602 if event.target == self.atoms.TARGETS {
604 trace!("Handling TARGETS, dst property is {}", self.atom_name_dbg(event.property));
605
606 let data = self.selection_of(selection).data.read();
607 let (data_targets, excluded) = if let Some(data_list) = &*data {
608 let mut targets = Vec::with_capacity(data_list.len() + 3);
610 let mut excluded = false;
611
612 for data in data_list {
613 targets.push(data.format);
614 if data.format == self.atoms.UTF8_STRING {
615 targets.push(self.atoms.UTF8_MIME_0);
618 targets.push(self.atoms.UTF8_MIME_1);
619 }
620
621 if data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT {
622 excluded = true;
623 }
624 }
625 (targets, excluded)
626 } else {
627 (Vec::with_capacity(2), false)
629 };
630
631 let mut targets = data_targets;
632 targets.push(self.atoms.TARGETS);
633
634 if !excluded {
643 targets.push(self.atoms.SAVE_TARGETS);
644 }
645
646 self.server
647 .conn
648 .change_property32(
649 PropMode::REPLACE,
650 event.requestor,
651 event.property,
652 self.atoms.ATOM,
654 &targets,
655 )
656 .map_err(into_unknown)?;
657 self.server.conn.flush().map_err(into_unknown)?;
658 success = true;
659 } else {
660 trace!("Handling request for (probably) the clipboard contents.");
661 let data = self.selection_of(selection).data.read();
662 if let Some(data_list) = &*data {
663 success = match data_list.iter().find(|d| d.format == event.target) {
664 Some(data) => {
665 self.server
666 .conn
667 .change_property8(
668 PropMode::REPLACE,
669 event.requestor,
670 event.property,
671 event.target,
672 &data.bytes,
673 )
674 .map_err(into_unknown)?;
675 self.server.conn.flush().map_err(into_unknown)?;
676 true
677 }
678 None => false,
679 };
680 } else {
681 success = false;
685 }
686 }
687 let property = if success { event.property } else { AtomEnum::NONE.into() };
689 self.server
691 .conn
692 .send_event(
693 false,
694 event.requestor,
695 EventMask::NO_EVENT,
696 SelectionNotifyEvent {
697 response_type: SELECTION_NOTIFY_EVENT,
698 sequence: event.sequence,
699 time: event.time,
700 requestor: event.requestor,
701 selection: event.selection,
702 target: event.target,
703 property,
704 },
705 )
706 .map_err(into_unknown)?;
707
708 self.server.conn.flush().map_err(into_unknown)
709 }
710
711 fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> {
712 if self.server.win_id == 0 {
713 error!("The server's window id was 0. This is unexpected");
715 return Ok(());
716 }
717
718 let selection = LinuxClipboardKind::Clipboard;
722
723 if !self.is_owner(selection)? {
724 return Ok(());
726 }
727
728 match &*self.selection_of(selection).data.read() {
729 Some(data) => {
730 if data.iter().any(|data| data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT) {
739 if let Err(e) = self.clear(selection) {
749 warn!("failed to release sensitive data's clipboard ownership: {e}; it may end up persisted!");
750 }
752
753 return Ok(());
754 }
755 }
756 None => {
757 return Ok(());
759 }
760 }
761
762 let mut handover_state = self.handover_state.lock();
766
767 trace!("Sending the data to the clipboard manager");
768 self.server
769 .conn
770 .convert_selection(
771 self.server.win_id,
772 self.atoms.CLIPBOARD_MANAGER,
773 self.atoms.SAVE_TARGETS,
774 self.atoms.ARBOARD_CLIPBOARD,
775 Time::CURRENT_TIME,
776 )
777 .map_err(into_unknown)?;
778 self.server.conn.flush().map_err(into_unknown)?;
779
780 *handover_state = ManagerHandoverState::InProgress;
781 let max_handover_duration = Duration::from_millis(100);
782
783 let result = self.handover_cv.wait_for(&mut handover_state, max_handover_duration);
786
787 if *handover_state == ManagerHandoverState::Finished {
788 return Ok(());
789 }
790 if result.timed_out() {
791 warn!("Could not hand the clipboard contents over to the clipboard manager. The request timed out.");
792 return Ok(());
793 }
794
795 unreachable!("This is a bug! The handover was not finished and the condvar didn't time out, yet the condvar wait ended.")
796 }
797}
798
799fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>> {
800 fn handover_finished(clip: &Arc<Inner>, mut handover_state: MutexGuard<ManagerHandoverState>) {
801 log::trace!("Finishing clipboard manager handover.");
802 *handover_state = ManagerHandoverState::Finished;
803
804 drop(handover_state);
806
807 clip.handover_cv.notify_all();
808 }
809
810 trace!("Started serve requests thread.");
811
812 let _guard = ScopeGuard::new(|| {
813 context.serve_stopped.store(true, Ordering::Relaxed);
814 });
815
816 let mut written = false;
817 let mut notified = false;
818
819 loop {
820 match context.server.conn.wait_for_event().map_err(into_unknown)? {
821 Event::DestroyNotify(_) => {
822 trace!("Clipboard server window is being destroyed x_x");
824 return Ok(());
825 }
826 Event::SelectionClear(event) => {
827 trace!("Somebody else owns the clipboard now");
831
832 if let Some(selection) = context.kind_of(event.selection) {
833 let selection = context.selection_of(selection);
834 let mut data_guard = selection.data.write();
835 *data_guard = None;
836
837 let _guard = selection.mutex.lock();
843 selection.data_changed.notify_all();
844 }
845 }
846 Event::SelectionRequest(event) => {
847 trace!(
848 "SelectionRequest - selection is: {}, target is {}",
849 context.atom_name_dbg(event.selection),
850 context.atom_name_dbg(event.target),
851 );
852 if let Err(e) = context.handle_selection_request(event) {
854 error!("Failed to handle selection request: {e}");
855 continue;
856 }
857
858 let handover_state = context.handover_state.lock();
861 if *handover_state == ManagerHandoverState::InProgress {
862 if event.target != context.atoms.TARGETS {
865 trace!("The contents were written to the clipboard manager.");
866 written = true;
867 if notified {
869 handover_finished(&context, handover_state);
870 }
871 }
872 }
873 }
874 Event::SelectionNotify(event) => {
875 if event.selection != context.atoms.CLIPBOARD_MANAGER {
880 error!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread.");
881 continue;
882 }
883 let handover_state = context.handover_state.lock();
884 if *handover_state == ManagerHandoverState::InProgress {
885 trace!("The clipboard manager indicated that it's done requesting the contents from us.");
889 notified = true;
890
891 if written {
898 handover_finished(&context, handover_state);
899 }
900 }
901 }
902 _event => {
903 }
906 }
907 }
908}
909
910pub(crate) struct Clipboard {
911 inner: Arc<Inner>,
912}
913
914impl Clipboard {
915 pub(crate) fn new() -> Result<Self> {
916 let mut global_cb = CLIPBOARD.lock();
917 if let Some(global_cb) = &*global_cb {
918 return Ok(Self { inner: Arc::clone(&global_cb.inner) });
919 }
920 let ctx = Arc::new(Inner::new()?);
922 let join_handle;
923 {
924 let ctx = Arc::clone(&ctx);
925 join_handle = std::thread::spawn(move || {
926 if let Err(error) = serve_requests(ctx) {
927 error!("Worker thread errored with: {}", error);
928 }
929 });
930 }
931 *global_cb = Some(GlobalClipboard { inner: Arc::clone(&ctx), server_handle: join_handle });
932 Ok(Self { inner: ctx })
933 }
934
935 fn add_clipboard_exclusions(&self, exclude_from_history: bool, data: &mut Vec<ClipboardData>) {
936 if exclude_from_history {
937 data.push(ClipboardData {
938 bytes: KDE_EXCLUSION_HINT.to_vec(),
939 format: self.inner.atoms.X_KDE_PASSWORDMANAGERHINT,
940 })
941 }
942 }
943
944 pub(crate) fn clear(&self, selection: LinuxClipboardKind) -> Result<()> {
945 self.inner.clear(selection)
946 }
947
948 pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result<String> {
949 let formats = [
950 self.inner.atoms.UTF8_STRING,
951 self.inner.atoms.UTF8_MIME_0,
952 self.inner.atoms.UTF8_MIME_1,
953 self.inner.atoms.STRING,
954 self.inner.atoms.TEXT,
955 self.inner.atoms.TEXT_MIME_UNKNOWN,
956 ];
957 let result = self.inner.read(&formats, selection)?;
958 if result.format == self.inner.atoms.STRING {
959 Ok(result.bytes.into_iter().map(|c| c as char).collect())
962 } else {
963 String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
964 }
965 }
966
967 pub(crate) fn set_text(
968 &self,
969 message: Cow<'_, str>,
970 selection: LinuxClipboardKind,
971 wait: WaitConfig,
972 exclude_from_history: bool,
973 ) -> Result<()> {
974 let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
975 data.push(ClipboardData {
976 bytes: message.into_owned().into_bytes(),
977 format: self.inner.atoms.UTF8_STRING,
978 });
979
980 self.add_clipboard_exclusions(exclude_from_history, &mut data);
981
982 self.inner.write(data, selection, wait)
983 }
984
985 pub(crate) fn get_html(&self, selection: LinuxClipboardKind) -> Result<String> {
986 let formats = [self.inner.atoms.HTML];
987 let result = self.inner.read(&formats, selection)?;
988 String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
989 }
990
991 pub(crate) fn set_html(
992 &self,
993 html: Cow<'_, str>,
994 alt: Option<Cow<'_, str>>,
995 selection: LinuxClipboardKind,
996 wait: WaitConfig,
997 exclude_from_history: bool,
998 ) -> Result<()> {
999 let mut data = {
1000 let cap = [true, alt.is_some(), exclude_from_history]
1001 .map(|v| usize::from(v as u8))
1002 .iter()
1003 .sum();
1004 Vec::with_capacity(cap)
1005 };
1006
1007 if let Some(alt_text) = alt {
1008 data.push(ClipboardData {
1009 bytes: alt_text.into_owned().into_bytes(),
1010 format: self.inner.atoms.UTF8_STRING,
1011 });
1012 }
1013 data.push(ClipboardData {
1014 bytes: html.into_owned().into_bytes(),
1015 format: self.inner.atoms.HTML,
1016 });
1017
1018 self.add_clipboard_exclusions(exclude_from_history, &mut data);
1019
1020 self.inner.write(data, selection, wait)
1021 }
1022
1023 #[cfg(feature = "image-data")]
1024 pub(crate) fn get_image(&self, selection: LinuxClipboardKind) -> Result<ImageData<'static>> {
1025 let formats = [self.inner.atoms.PNG_MIME];
1026 let bytes = self.inner.read(&formats, selection)?.bytes;
1027
1028 let cursor = std::io::Cursor::new(&bytes);
1029 let mut reader = image::io::Reader::new(cursor);
1030 reader.set_format(image::ImageFormat::Png);
1031 let image = match reader.decode() {
1032 Ok(img) => img.into_rgba8(),
1033 Err(_e) => return Err(Error::ConversionFailure),
1034 };
1035 let (w, h) = image.dimensions();
1036 let image_data =
1037 ImageData { width: w as usize, height: h as usize, bytes: image.into_raw().into() };
1038 Ok(image_data)
1039 }
1040
1041 #[cfg(feature = "image-data")]
1042 pub(crate) fn set_image(
1043 &self,
1044 image: ImageData,
1045 selection: LinuxClipboardKind,
1046 wait: WaitConfig,
1047 exclude_from_history: bool,
1048 ) -> Result<()> {
1049 let encoded = encode_as_png(&image)?;
1050 let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
1051
1052 data.push(ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME });
1053
1054 self.add_clipboard_exclusions(exclude_from_history, &mut data);
1055
1056 self.inner.write(data, selection, wait)
1057 }
1058
1059 pub(crate) fn get_file_list(&self, selection: LinuxClipboardKind) -> Result<Vec<PathBuf>> {
1060 let result = self.inner.read(&[self.inner.atoms.URI_LIST], selection)?;
1061
1062 String::from_utf8(result.bytes)
1063 .map_err(|_| Error::ConversionFailure)
1064 .map(paths_from_uri_list)
1065 }
1066}
1067
1068impl Drop for Clipboard {
1069 fn drop(&mut self) {
1070 const MIN_OWNERS: usize = 3;
1073
1074 let mut global_cb = CLIPBOARD.lock();
1077 if Arc::strong_count(&self.inner) == MIN_OWNERS {
1078 if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() {
1083 error!("Could not hand the clipboard data over to the clipboard manager: {}", e);
1084 }
1085 let global_cb = global_cb.take();
1086 if let Err(e) = self.inner.server.conn.destroy_window(self.inner.server.win_id) {
1087 error!("Failed to destroy the clipboard window. Error: {}", e);
1088 return;
1089 }
1090 if let Err(e) = self.inner.server.conn.flush() {
1091 error!("Failed to flush the clipboard window. Error: {}", e);
1092 return;
1093 }
1094 if let Some(global_cb) = global_cb {
1095 let GlobalClipboard { inner, server_handle } = global_cb;
1096 drop(inner);
1097
1098 if let Err(e) = server_handle.join() {
1099 let message;
1101 if let Some(msg) = e.downcast_ref::<&'static str>() {
1102 message = Some((*msg).to_string());
1103 } else if let Some(msg) = e.downcast_ref::<String>() {
1104 message = Some(msg.clone());
1105 } else {
1106 message = None;
1107 }
1108 if let Some(message) = message {
1109 error!(
1110 "The clipboard server thread panicked. Panic message: '{}'",
1111 message,
1112 );
1113 } else {
1114 error!("The clipboard server thread panicked.");
1115 }
1116 }
1117
1118 #[cfg(debug_assertions)]
1125 if let Some(inner) = Arc::get_mut(&mut self.inner) {
1126 use std::io::IsTerminal;
1127
1128 let mut change_timestamps = Vec::with_capacity(2);
1129 let mut collect_changed = |sel: &mut Mutex<Option<Instant>>| {
1130 if let Some(changed) = sel.get_mut() {
1131 change_timestamps.push(*changed);
1132 }
1133 };
1134
1135 collect_changed(&mut inner.clipboard.mutex);
1136 collect_changed(&mut inner.primary.mutex);
1137 collect_changed(&mut inner.secondary.mutex);
1138
1139 change_timestamps.sort();
1140 if let Some(last) = change_timestamps.last() {
1141 let elapsed = last.elapsed().as_millis();
1142 if elapsed > 100 {
1146 return;
1147 }
1148
1149 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.");
1153 if std::io::stderr().is_terminal() {
1154 eprintln!("{msg}");
1155 } else {
1156 log::warn!("{msg}");
1157 }
1158 }
1159 }
1160 }
1161 }
1162 }
1163}