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}
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		Ok(paths_from_uri_list(result.bytes))
1063	}
1064
1065	pub(crate) fn set_file_list(
1066		&self,
1067		file_list: &[impl AsRef<Path>],
1068		selection: LinuxClipboardKind,
1069		wait: WaitConfig,
1070		exclude_from_history: bool,
1071	) -> Result<()> {
1072		let files = paths_to_uri_list(file_list)?;
1073		let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
1074
1075		data.push(ClipboardData { bytes: files.into_bytes(), format: self.inner.atoms.URI_LIST });
1076		self.add_clipboard_exclusions(exclude_from_history, &mut data);
1077
1078		self.inner.write(data, selection, wait)
1079	}
1080}
1081
1082impl Drop for Clipboard {
1083	fn drop(&mut self) {
1084		const MIN_OWNERS: usize = 3;
1087
1088		let mut global_cb = CLIPBOARD.lock();
1091		if Arc::strong_count(&self.inner) == MIN_OWNERS {
1092			if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() {
1097				error!("Could not hand the clipboard data over to the clipboard manager: {}", e);
1098			}
1099			let global_cb = global_cb.take();
1100			if let Err(e) = self.inner.server.conn.destroy_window(self.inner.server.win_id) {
1101				error!("Failed to destroy the clipboard window. Error: {}", e);
1102				return;
1103			}
1104			if let Err(e) = self.inner.server.conn.flush() {
1105				error!("Failed to flush the clipboard window. Error: {}", e);
1106				return;
1107			}
1108			if let Some(global_cb) = global_cb {
1109				let GlobalClipboard { inner, server_handle } = global_cb;
1110				drop(inner);
1111
1112				if let Err(e) = server_handle.join() {
1113					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						error!(
1124							"The clipboard server thread panicked. Panic message: '{}'",
1125							message,
1126						);
1127					} else {
1128						error!("The clipboard server thread panicked.");
1129					}
1130				}
1131
1132				#[cfg(debug_assertions)]
1139				if let Some(inner) = Arc::get_mut(&mut self.inner) {
1140					use std::io::IsTerminal;
1141
1142					let mut change_timestamps = Vec::with_capacity(2);
1143					let mut collect_changed = |sel: &mut Mutex<Option<Instant>>| {
1144						if let Some(changed) = sel.get_mut() {
1145							change_timestamps.push(*changed);
1146						}
1147					};
1148
1149					collect_changed(&mut inner.clipboard.mutex);
1150					collect_changed(&mut inner.primary.mutex);
1151					collect_changed(&mut inner.secondary.mutex);
1152
1153					change_timestamps.sort();
1154					if let Some(last) = change_timestamps.last() {
1155						let elapsed = last.elapsed().as_millis();
1156						if elapsed > 100 {
1160							return;
1161						}
1162
1163						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.");
1167						if std::io::stderr().is_terminal() {
1168							eprintln!("{msg}");
1169						} else {
1170							log::warn!("{msg}");
1171						}
1172					}
1173				}
1174			}
1175		}
1176	}
1177}