arboard/platform/linux/
x11.rs

1/*
2SPDX-License-Identifier: Apache-2.0 OR MIT
3
4Copyright 2022 The Arboard contributors
5
6The project to which this file belongs is licensed under either of
7the Apache 2.0 or the MIT license at the licensee's choice. The terms
8and conditions of the chosen license apply to this file.
9*/
10
11// More info about using the clipboard on X11:
12// https://tronche.com/gui/x/icccm/sec-2.html#s-2.6
13// https://freedesktop.org/wiki/ClipboardManager/
14
15use 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::{into_unknown, paths_from_uri_list, LinuxClipboardKind, WaitConfig};
49#[cfg(feature = "image-data")]
50use crate::ImageData;
51use crate::{common::ScopeGuard, Error};
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: b"image/png",
84
85		// This is just some random name for the property on our window, into which
86		// the clipboard owner writes the data we requested.
87		ARBOARD_CLIPBOARD,
88	}
89}
90
91thread_local! {
92	static ATOM_NAME_CACHE: RefCell<HashMap<Atom, &'static str>> = Default::default();
93}
94
95// Some clipboard items, like images, may take a very long time to produce a
96// `SelectionNotify`. Multiple seconds long.
97const LONG_TIMEOUT_DUR: Duration = Duration::from_millis(4000);
98const SHORT_TIMEOUT_DUR: Duration = Duration::from_millis(10);
99
100#[derive(Debug, PartialEq, Eq)]
101enum ManagerHandoverState {
102	Idle,
103	InProgress,
104	Finished,
105}
106
107struct GlobalClipboard {
108	inner: Arc<Inner>,
109
110	/// Join handle to the thread which serves selection requests.
111	server_handle: JoinHandle<()>,
112}
113
114struct XContext {
115	conn: RustConnection,
116	win_id: u32,
117}
118
119struct Inner {
120	/// The context for the thread which serves clipboard read
121	/// requests coming to us.
122	server: XContext,
123	atoms: Atoms,
124
125	clipboard: Selection,
126	primary: Selection,
127	secondary: Selection,
128
129	handover_state: Mutex<ManagerHandoverState>,
130	handover_cv: Condvar,
131
132	serve_stopped: AtomicBool,
133}
134
135impl XContext {
136	fn new() -> Result<Self> {
137		// create a new connection to an X11 server
138		let (conn, screen_num): (RustConnection, _) =
139			RustConnection::connect(None).map_err(|_| {
140				Error::unknown("X11 server connection timed out because it was unreachable")
141			})?;
142		let screen = conn.setup().roots.get(screen_num).ok_or(Error::unknown("no screen found"))?;
143		let win_id = conn.generate_id().map_err(into_unknown)?;
144
145		let event_mask =
146            // Just in case that some program reports SelectionNotify events
147            // with XCB_EVENT_MASK_PROPERTY_CHANGE mask.
148            EventMask::PROPERTY_CHANGE |
149            // To receive DestroyNotify event and stop the message loop.
150            EventMask::STRUCTURE_NOTIFY;
151		// create the window
152		conn.create_window(
153			// copy as much as possible from the parent, because no other specific input is needed
154			COPY_DEPTH_FROM_PARENT,
155			win_id,
156			screen.root,
157			0,
158			0,
159			1,
160			1,
161			0,
162			WindowClass::COPY_FROM_PARENT,
163			COPY_FROM_PARENT,
164			// don't subscribe to any special events because we are requesting everything we need ourselves
165			&CreateWindowAux::new().event_mask(event_mask),
166		)
167		.map_err(into_unknown)?;
168		conn.flush().map_err(into_unknown)?;
169
170		Ok(Self { conn, win_id })
171	}
172}
173
174#[derive(Default)]
175struct Selection {
176	data: RwLock<Option<Vec<ClipboardData>>>,
177	/// Mutex around nothing to use with the below condvar.
178	mutex: Mutex<()>,
179	/// A condvar that is notified when the contents of this clipboard are changed.
180	///
181	/// This is associated with `Self::mutex`.
182	data_changed: Condvar,
183}
184
185#[derive(Debug, Clone)]
186struct ClipboardData {
187	bytes: Vec<u8>,
188
189	/// The atom representing the format in which the data is encoded.
190	format: Atom,
191}
192
193enum ReadSelNotifyResult {
194	GotData(Vec<u8>),
195	IncrStarted,
196	EventNotRecognized,
197}
198
199impl Inner {
200	fn new() -> Result<Self> {
201		let server = XContext::new()?;
202		let atoms =
203			Atoms::new(&server.conn).map_err(into_unknown)?.reply().map_err(into_unknown)?;
204
205		Ok(Self {
206			server,
207			atoms,
208			clipboard: Selection::default(),
209			primary: Selection::default(),
210			secondary: Selection::default(),
211			handover_state: Mutex::new(ManagerHandoverState::Idle),
212			handover_cv: Condvar::new(),
213			serve_stopped: AtomicBool::new(false),
214		})
215	}
216
217	fn write(
218		&self,
219		data: Vec<ClipboardData>,
220		selection: LinuxClipboardKind,
221		wait: WaitConfig,
222	) -> Result<()> {
223		if self.serve_stopped.load(Ordering::Relaxed) {
224			return Err(Error::unknown("The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)"));
225		}
226
227		let server_win = self.server.win_id;
228
229		// ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data
230		// changes.
231		self.server
232			.conn
233			.set_selection_owner(server_win, self.atom_of(selection), Time::CURRENT_TIME)
234			.map_err(|_| Error::ClipboardOccupied)?;
235
236		self.server.conn.flush().map_err(into_unknown)?;
237
238		// Just setting the data, and the `serve_requests` will take care of the rest.
239		let selection = self.selection_of(selection);
240		let mut data_guard = selection.data.write();
241		*data_guard = Some(data);
242
243		// Lock the mutex to both ensure that no wakers of `data_changed` can wake us between
244		// dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other
245		// threads in that position.
246		let mut guard = selection.mutex.lock();
247
248		// Notify any existing waiting threads that we have changed the data in the selection.
249		// It is important that the mutex is locked to prevent this notification getting lost.
250		selection.data_changed.notify_all();
251
252		match wait {
253			WaitConfig::None => {}
254			WaitConfig::Forever => {
255				drop(data_guard);
256				selection.data_changed.wait(&mut guard);
257			}
258
259			WaitConfig::Until(deadline) => {
260				drop(data_guard);
261				selection.data_changed.wait_until(&mut guard, deadline);
262			}
263		}
264
265		Ok(())
266	}
267
268	/// `formats` must be a slice of atoms, where each atom represents a target format.
269	/// The first format from `formats`, which the clipboard owner supports will be the
270	/// format of the return value.
271	fn read(&self, formats: &[Atom], selection: LinuxClipboardKind) -> Result<ClipboardData> {
272		// if we are the current owner, we can get the current clipboard ourselves
273		if self.is_owner(selection)? {
274			let data = self.selection_of(selection).data.read();
275			if let Some(data_list) = &*data {
276				for data in data_list {
277					for format in formats {
278						if *format == data.format {
279							return Ok(data.clone());
280						}
281					}
282				}
283			}
284			return Err(Error::ContentNotAvailable);
285		}
286		// if let Some(data) = self.data.read().clone() {
287		//     return Ok(data)
288		// }
289		let reader = XContext::new()?;
290
291		trace!("Trying to get the clipboard data.");
292		for format in formats {
293			match self.read_single(&reader, selection, *format) {
294				Ok(bytes) => {
295					return Ok(ClipboardData { bytes, format: *format });
296				}
297				Err(Error::ContentNotAvailable) => {
298					continue;
299				}
300				Err(e) => return Err(e),
301			}
302		}
303		Err(Error::ContentNotAvailable)
304	}
305
306	fn read_single(
307		&self,
308		reader: &XContext,
309		selection: LinuxClipboardKind,
310		target_format: Atom,
311	) -> Result<Vec<u8>> {
312		// Delete the property so that we can detect (using property notify)
313		// when the selection owner receives our request.
314		reader
315			.conn
316			.delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD)
317			.map_err(into_unknown)?;
318
319		// request to convert the clipboard selection to our data type(s)
320		reader
321			.conn
322			.convert_selection(
323				reader.win_id,
324				self.atom_of(selection),
325				target_format,
326				self.atoms.ARBOARD_CLIPBOARD,
327				Time::CURRENT_TIME,
328			)
329			.map_err(into_unknown)?;
330		reader.conn.sync().map_err(into_unknown)?;
331
332		trace!("Finished `convert_selection`");
333
334		let mut incr_data: Vec<u8> = Vec::new();
335		let mut using_incr = false;
336
337		let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR;
338
339		while Instant::now() < timeout_end {
340			let event = reader.conn.poll_for_event().map_err(into_unknown)?;
341			let event = match event {
342				Some(e) => e,
343				None => {
344					std::thread::sleep(Duration::from_millis(1));
345					continue;
346				}
347			};
348			match event {
349				// The first response after requesting a selection.
350				Event::SelectionNotify(event) => {
351					trace!("Read SelectionNotify");
352					let result = self.handle_read_selection_notify(
353						reader,
354						target_format,
355						&mut using_incr,
356						&mut incr_data,
357						event,
358					)?;
359					match result {
360						ReadSelNotifyResult::GotData(data) => return Ok(data),
361						ReadSelNotifyResult::IncrStarted => {
362							// This means we received an indication that an the
363							// data is going to be sent INCRementally. Let's
364							// reset our timeout.
365							timeout_end += SHORT_TIMEOUT_DUR;
366						}
367						ReadSelNotifyResult::EventNotRecognized => (),
368					}
369				}
370				// If the previous SelectionNotify event specified that the data
371				// will be sent in INCR segments, each segment is transferred in
372				// a PropertyNotify event.
373				Event::PropertyNotify(event) => {
374					let result = self.handle_read_property_notify(
375						reader,
376						target_format,
377						using_incr,
378						&mut incr_data,
379						&mut timeout_end,
380						event,
381					)?;
382					if result {
383						return Ok(incr_data);
384					}
385				}
386				_ => log::trace!("An unexpected event arrived while reading the clipboard."),
387			}
388		}
389		log::info!("Time-out hit while reading the clipboard.");
390		Err(Error::ContentNotAvailable)
391	}
392
393	fn atom_of(&self, selection: LinuxClipboardKind) -> Atom {
394		match selection {
395			LinuxClipboardKind::Clipboard => self.atoms.CLIPBOARD,
396			LinuxClipboardKind::Primary => self.atoms.PRIMARY,
397			LinuxClipboardKind::Secondary => self.atoms.SECONDARY,
398		}
399	}
400
401	fn selection_of(&self, selection: LinuxClipboardKind) -> &Selection {
402		match selection {
403			LinuxClipboardKind::Clipboard => &self.clipboard,
404			LinuxClipboardKind::Primary => &self.primary,
405			LinuxClipboardKind::Secondary => &self.secondary,
406		}
407	}
408
409	fn kind_of(&self, atom: Atom) -> Option<LinuxClipboardKind> {
410		match atom {
411			a if a == self.atoms.CLIPBOARD => Some(LinuxClipboardKind::Clipboard),
412			a if a == self.atoms.PRIMARY => Some(LinuxClipboardKind::Primary),
413			a if a == self.atoms.SECONDARY => Some(LinuxClipboardKind::Secondary),
414			_ => None,
415		}
416	}
417
418	fn is_owner(&self, selection: LinuxClipboardKind) -> Result<bool> {
419		let current = self
420			.server
421			.conn
422			.get_selection_owner(self.atom_of(selection))
423			.map_err(into_unknown)?
424			.reply()
425			.map_err(into_unknown)?
426			.owner;
427
428		Ok(current == self.server.win_id)
429	}
430
431	fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
432		String::from_utf8(
433			self.server
434				.conn
435				.get_atom_name(atom)
436				.map_err(into_unknown)?
437				.reply()
438				.map_err(into_unknown)?
439				.name,
440		)
441		.map_err(into_unknown)
442	}
443	fn atom_name_dbg(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
444		ATOM_NAME_CACHE.with(|cache| {
445			let mut cache = cache.borrow_mut();
446			match cache.entry(atom) {
447				Entry::Occupied(entry) => *entry.get(),
448				Entry::Vacant(entry) => {
449					let s = self
450						.atom_name(atom)
451						.map(|s| Box::leak(s.into_boxed_str()) as &str)
452						.unwrap_or("FAILED-TO-GET-THE-ATOM-NAME");
453					entry.insert(s);
454					s
455				}
456			}
457		})
458	}
459
460	fn handle_read_selection_notify(
461		&self,
462		reader: &XContext,
463		target_format: u32,
464		using_incr: &mut bool,
465		incr_data: &mut Vec<u8>,
466		event: SelectionNotifyEvent,
467	) -> Result<ReadSelNotifyResult> {
468		// The property being set to NONE means that the `convert_selection`
469		// failed.
470
471		// According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4
472		// the target must be set to the same as what we requested.
473		if event.property == NONE || event.target != target_format {
474			return Err(Error::ContentNotAvailable);
475		}
476		if self.kind_of(event.selection).is_none() {
477			log::info!("Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
478			return Ok(ReadSelNotifyResult::EventNotRecognized);
479		}
480		if *using_incr {
481			log::warn!("Received a SelectionNotify while already expecting INCR segments.");
482			return Ok(ReadSelNotifyResult::EventNotRecognized);
483		}
484		// request the selection
485		let mut reply = reader
486			.conn
487			.get_property(true, event.requestor, event.property, event.target, 0, u32::MAX / 4)
488			.map_err(into_unknown)?
489			.reply()
490			.map_err(into_unknown)?;
491
492		// trace!("Property.type: {:?}", self.atom_name(reply.type_));
493
494		// we found something
495		if reply.type_ == target_format {
496			Ok(ReadSelNotifyResult::GotData(reply.value))
497		} else if reply.type_ == self.atoms.INCR {
498			// Note that we call the get_property again because we are
499			// indicating that we are ready to receive the data by deleting the
500			// property, however deleting only works if the type matches the
501			// property type. But the type didn't match in the previous call.
502			reply = reader
503				.conn
504				.get_property(
505					true,
506					event.requestor,
507					event.property,
508					self.atoms.INCR,
509					0,
510					u32::MAX / 4,
511				)
512				.map_err(into_unknown)?
513				.reply()
514				.map_err(into_unknown)?;
515			log::trace!("Receiving INCR segments");
516			*using_incr = true;
517			if reply.value_len == 4 {
518				let min_data_len = reply.value32().and_then(|mut vals| vals.next()).unwrap_or(0);
519				incr_data.reserve(min_data_len as usize);
520			}
521			Ok(ReadSelNotifyResult::IncrStarted)
522		} else {
523			// this should never happen, we have sent a request only for supported types
524			Err(Error::unknown("incorrect type received from clipboard"))
525		}
526	}
527
528	/// Returns Ok(true) when the incr_data is ready
529	fn handle_read_property_notify(
530		&self,
531		reader: &XContext,
532		target_format: u32,
533		using_incr: bool,
534		incr_data: &mut Vec<u8>,
535		timeout_end: &mut Instant,
536		event: PropertyNotifyEvent,
537	) -> Result<bool> {
538		if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE {
539			return Ok(false);
540		}
541		if !using_incr {
542			// This must mean the selection owner received our request, and is
543			// now preparing the data
544			return Ok(false);
545		}
546		let reply = reader
547			.conn
548			.get_property(true, event.window, event.atom, target_format, 0, u32::MAX / 4)
549			.map_err(into_unknown)?
550			.reply()
551			.map_err(into_unknown)?;
552
553		// log::trace!("Received segment. value_len {}", reply.value_len,);
554		if reply.value_len == 0 {
555			// This indicates that all the data has been sent.
556			return Ok(true);
557		}
558		incr_data.extend(reply.value);
559
560		// Let's reset our timeout, since we received a valid chunk.
561		*timeout_end = Instant::now() + SHORT_TIMEOUT_DUR;
562
563		// Not yet complete
564		Ok(false)
565	}
566
567	fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
568		let selection = match self.kind_of(event.selection) {
569			Some(kind) => kind,
570			None => {
571				warn!("Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
572				return Ok(());
573			}
574		};
575
576		let success;
577		// we are asked for a list of supported conversion targets
578		if event.target == self.atoms.TARGETS {
579			trace!("Handling TARGETS, dst property is {}", self.atom_name_dbg(event.property));
580			let mut targets = Vec::with_capacity(10);
581			targets.push(self.atoms.TARGETS);
582			targets.push(self.atoms.SAVE_TARGETS);
583			let data = self.selection_of(selection).data.read();
584			if let Some(data_list) = &*data {
585				for data in data_list {
586					targets.push(data.format);
587					if data.format == self.atoms.UTF8_STRING {
588						// When we are storing a UTF8 string,
589						// add all equivalent formats to the supported targets
590						targets.push(self.atoms.UTF8_MIME_0);
591						targets.push(self.atoms.UTF8_MIME_1);
592					}
593				}
594			}
595			self.server
596				.conn
597				.change_property32(
598					PropMode::REPLACE,
599					event.requestor,
600					event.property,
601					// TODO: change to `AtomEnum::ATOM`
602					self.atoms.ATOM,
603					&targets,
604				)
605				.map_err(into_unknown)?;
606			self.server.conn.flush().map_err(into_unknown)?;
607			success = true;
608		} else {
609			trace!("Handling request for (probably) the clipboard contents.");
610			let data = self.selection_of(selection).data.read();
611			if let Some(data_list) = &*data {
612				success = match data_list.iter().find(|d| d.format == event.target) {
613					Some(data) => {
614						self.server
615							.conn
616							.change_property8(
617								PropMode::REPLACE,
618								event.requestor,
619								event.property,
620								event.target,
621								&data.bytes,
622							)
623							.map_err(into_unknown)?;
624						self.server.conn.flush().map_err(into_unknown)?;
625						true
626					}
627					None => false,
628				};
629			} else {
630				// This must mean that we lost ownership of the data
631				// since the other side requested the selection.
632				// Let's respond with the property set to none.
633				success = false;
634			}
635		}
636		// on failure we notify the requester of it
637		let property = if success { event.property } else { AtomEnum::NONE.into() };
638		// tell the requestor that we finished sending data
639		self.server
640			.conn
641			.send_event(
642				false,
643				event.requestor,
644				EventMask::NO_EVENT,
645				SelectionNotifyEvent {
646					response_type: SELECTION_NOTIFY_EVENT,
647					sequence: event.sequence,
648					time: event.time,
649					requestor: event.requestor,
650					selection: event.selection,
651					target: event.target,
652					property,
653				},
654			)
655			.map_err(into_unknown)?;
656
657		self.server.conn.flush().map_err(into_unknown)
658	}
659
660	fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> {
661		if self.server.win_id == 0 {
662			// This shouldn't really ever happen but let's just check.
663			error!("The server's window id was 0. This is unexpected");
664			return Ok(());
665		}
666
667		if !self.is_owner(LinuxClipboardKind::Clipboard)? {
668			// We are not owning the clipboard, nothing to do.
669			return Ok(());
670		}
671		if self.selection_of(LinuxClipboardKind::Clipboard).data.read().is_none() {
672			// If we don't have any data, there's nothing to do.
673			return Ok(());
674		}
675
676		// It's important that we lock the state before sending the request
677		// because we don't want the request server thread to lock the state
678		// after the request but before we can lock it here.
679		let mut handover_state = self.handover_state.lock();
680
681		trace!("Sending the data to the clipboard manager");
682		self.server
683			.conn
684			.convert_selection(
685				self.server.win_id,
686				self.atoms.CLIPBOARD_MANAGER,
687				self.atoms.SAVE_TARGETS,
688				self.atoms.ARBOARD_CLIPBOARD,
689				Time::CURRENT_TIME,
690			)
691			.map_err(into_unknown)?;
692		self.server.conn.flush().map_err(into_unknown)?;
693
694		*handover_state = ManagerHandoverState::InProgress;
695		let max_handover_duration = Duration::from_millis(100);
696
697		// Note that we are using a parking_lot condvar here, which doesn't wake up
698		// spuriously
699		let result = self.handover_cv.wait_for(&mut handover_state, max_handover_duration);
700
701		if *handover_state == ManagerHandoverState::Finished {
702			return Ok(());
703		}
704		if result.timed_out() {
705			warn!("Could not hand the clipboard contents over to the clipboard manager. The request timed out.");
706			return Ok(());
707		}
708
709		Err(Error::unknown("The handover was not finished and the condvar didn't time out, yet the condvar wait ended. This should be unreachable."))
710	}
711}
712
713fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>> {
714	fn handover_finished(clip: &Arc<Inner>, mut handover_state: MutexGuard<ManagerHandoverState>) {
715		log::trace!("Finishing clipboard manager handover.");
716		*handover_state = ManagerHandoverState::Finished;
717
718		// Not sure if unlocking the mutex is necessary here but better safe than sorry.
719		drop(handover_state);
720
721		clip.handover_cv.notify_all();
722	}
723
724	trace!("Started serve requests thread.");
725
726	let _guard = ScopeGuard::new(|| {
727		context.serve_stopped.store(true, Ordering::Relaxed);
728	});
729
730	let mut written = false;
731	let mut notified = false;
732
733	loop {
734		match context.server.conn.wait_for_event().map_err(into_unknown)? {
735			Event::DestroyNotify(_) => {
736				// This window is being destroyed.
737				trace!("Clipboard server window is being destroyed x_x");
738				return Ok(());
739			}
740			Event::SelectionClear(event) => {
741				// TODO: check if this works
742				// Someone else has new content in the clipboard, so it is
743				// notifying us that we should delete our data now.
744				trace!("Somebody else owns the clipboard now");
745
746				if let Some(selection) = context.kind_of(event.selection) {
747					let selection = context.selection_of(selection);
748					let mut data_guard = selection.data.write();
749					*data_guard = None;
750
751					// It is important that this mutex is locked at the time of calling
752					// `notify_all` to prevent notifications getting lost in case the sleeping
753					// thread has unlocked its `data_guard` and is just about to sleep.
754					// It is also important that the RwLock is kept write-locked for the same
755					// reason.
756					let _guard = selection.mutex.lock();
757					selection.data_changed.notify_all();
758				}
759			}
760			Event::SelectionRequest(event) => {
761				trace!(
762					"SelectionRequest - selection is: {}, target is {}",
763					context.atom_name_dbg(event.selection),
764					context.atom_name_dbg(event.target),
765				);
766				// Someone is requesting the clipboard content from us.
767				context.handle_selection_request(event).map_err(into_unknown)?;
768
769				// if we are in the progress of saving to the clipboard manager
770				// make sure we save that we have finished writing
771				let handover_state = context.handover_state.lock();
772				if *handover_state == ManagerHandoverState::InProgress {
773					// Only set written, when the actual contents were written,
774					// not just a response to what TARGETS we have.
775					if event.target != context.atoms.TARGETS {
776						trace!("The contents were written to the clipboard manager.");
777						written = true;
778						// if we have written and notified, make sure to notify that we are done
779						if notified {
780							handover_finished(&context, handover_state);
781						}
782					}
783				}
784			}
785			Event::SelectionNotify(event) => {
786				// We've requested the clipboard content and this is the answer.
787				// Considering that this thread is not responsible for reading
788				// clipboard contents, this must come from the clipboard manager
789				// signaling that the data was handed over successfully.
790				if event.selection != context.atoms.CLIPBOARD_MANAGER {
791					error!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread.");
792					continue;
793				}
794				let handover_state = context.handover_state.lock();
795				if *handover_state == ManagerHandoverState::InProgress {
796					// Note that some clipboard managers send a selection notify
797					// before even sending a request for the actual contents.
798					// (That's why we use the "notified" & "written" flags)
799					trace!("The clipboard manager indicated that it's done requesting the contents from us.");
800					notified = true;
801
802					// One would think that we could also finish if the property
803					// here is set 0, because that indicates failure. However
804					// this is not the case; for example on KDE plasma 5.18, we
805					// immediately get a SelectionNotify with property set to 0,
806					// but following that, we also get a valid SelectionRequest
807					// from the clipboard manager.
808					if written {
809						handover_finished(&context, handover_state);
810					}
811				}
812			}
813			_event => {
814				// May be useful for debugging but nothing else really.
815				// trace!("Received unwanted event: {:?}", event);
816			}
817		}
818	}
819}
820
821pub(crate) struct Clipboard {
822	inner: Arc<Inner>,
823}
824
825impl Clipboard {
826	pub(crate) fn new() -> Result<Self> {
827		let mut global_cb = CLIPBOARD.lock();
828		if let Some(global_cb) = &*global_cb {
829			return Ok(Self { inner: Arc::clone(&global_cb.inner) });
830		}
831		// At this point we know that the clipboard does not exist.
832		let ctx = Arc::new(Inner::new()?);
833		let join_handle;
834		{
835			let ctx = Arc::clone(&ctx);
836			join_handle = std::thread::spawn(move || {
837				if let Err(error) = serve_requests(ctx) {
838					error!("Worker thread errored with: {}", error);
839				}
840			});
841		}
842		*global_cb = Some(GlobalClipboard { inner: Arc::clone(&ctx), server_handle: join_handle });
843		Ok(Self { inner: ctx })
844	}
845
846	pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result<String> {
847		let formats = [
848			self.inner.atoms.UTF8_STRING,
849			self.inner.atoms.UTF8_MIME_0,
850			self.inner.atoms.UTF8_MIME_1,
851			self.inner.atoms.STRING,
852			self.inner.atoms.TEXT,
853			self.inner.atoms.TEXT_MIME_UNKNOWN,
854		];
855		let result = self.inner.read(&formats, selection)?;
856		if result.format == self.inner.atoms.STRING {
857			// ISO Latin-1
858			// See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8
859			Ok(result.bytes.into_iter().map(|c| c as char).collect())
860		} else {
861			String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
862		}
863	}
864
865	pub(crate) fn set_text(
866		&self,
867		message: Cow<'_, str>,
868		selection: LinuxClipboardKind,
869		wait: WaitConfig,
870	) -> Result<()> {
871		let data = vec![ClipboardData {
872			bytes: message.into_owned().into_bytes(),
873			format: self.inner.atoms.UTF8_STRING,
874		}];
875		self.inner.write(data, selection, wait)
876	}
877
878	pub(crate) fn get_html(&self, selection: LinuxClipboardKind) -> Result<String> {
879		let formats = [self.inner.atoms.HTML];
880		let result = self.inner.read(&formats, selection)?;
881		String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
882	}
883
884	pub(crate) fn set_html(
885		&self,
886		html: Cow<'_, str>,
887		alt: Option<Cow<'_, str>>,
888		selection: LinuxClipboardKind,
889		wait: WaitConfig,
890	) -> Result<()> {
891		let mut data = vec![];
892		if let Some(alt_text) = alt {
893			data.push(ClipboardData {
894				bytes: alt_text.into_owned().into_bytes(),
895				format: self.inner.atoms.UTF8_STRING,
896			});
897		}
898		data.push(ClipboardData {
899			bytes: html.into_owned().into_bytes(),
900			format: self.inner.atoms.HTML,
901		});
902		self.inner.write(data, selection, wait)
903	}
904
905	#[cfg(feature = "image-data")]
906	pub(crate) fn get_image(&self, selection: LinuxClipboardKind) -> Result<ImageData<'static>> {
907		let formats = [self.inner.atoms.PNG_MIME];
908		let bytes = self.inner.read(&formats, selection)?.bytes;
909
910		let cursor = std::io::Cursor::new(&bytes);
911		let mut reader = image::io::Reader::new(cursor);
912		reader.set_format(image::ImageFormat::Png);
913		let image = match reader.decode() {
914			Ok(img) => img.into_rgba8(),
915			Err(_e) => return Err(Error::ConversionFailure),
916		};
917		let (w, h) = image.dimensions();
918		let image_data =
919			ImageData { width: w as usize, height: h as usize, bytes: image.into_raw().into() };
920		Ok(image_data)
921	}
922
923	#[cfg(feature = "image-data")]
924	pub(crate) fn set_image(
925		&self,
926		image: ImageData,
927		selection: LinuxClipboardKind,
928		wait: WaitConfig,
929	) -> Result<()> {
930		let encoded = encode_as_png(&image)?;
931		let data = vec![ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME }];
932		self.inner.write(data, selection, wait)
933	}
934
935	pub(crate) fn get_file_list(&self, selection: LinuxClipboardKind) -> Result<Vec<PathBuf>> {
936		let result = self.inner.read(&[self.inner.atoms.URI_LIST], selection)?;
937
938		String::from_utf8(result.bytes)
939			.map_err(|_| Error::ConversionFailure)
940			.map(paths_from_uri_list)
941	}
942}
943
944impl Drop for Clipboard {
945	fn drop(&mut self) {
946		// There are always at least 3 owners:
947		// the global, the server thread, and one `Clipboard::inner`
948		const MIN_OWNERS: usize = 3;
949
950		// We start with locking the global guard to prevent race
951		// conditions below.
952		let mut global_cb = CLIPBOARD.lock();
953		if Arc::strong_count(&self.inner) == MIN_OWNERS {
954			// If the are the only owners of the clipboard are ourselves and
955			// the global object, then we should destroy the global object,
956			// and send the data to the clipboard manager
957
958			if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() {
959				error!("Could not hand the clipboard data over to the clipboard manager: {}", e);
960			}
961			let global_cb = global_cb.take();
962			if let Err(e) = self.inner.server.conn.destroy_window(self.inner.server.win_id) {
963				error!("Failed to destroy the clipboard window. Error: {}", e);
964				return;
965			}
966			if let Err(e) = self.inner.server.conn.flush() {
967				error!("Failed to flush the clipboard window. Error: {}", e);
968				return;
969			}
970			if let Some(global_cb) = global_cb {
971				if let Err(e) = global_cb.server_handle.join() {
972					// Let's try extracting the error message
973					let message;
974					if let Some(msg) = e.downcast_ref::<&'static str>() {
975						message = Some((*msg).to_string());
976					} else if let Some(msg) = e.downcast_ref::<String>() {
977						message = Some(msg.clone());
978					} else {
979						message = None;
980					}
981					if let Some(message) = message {
982						error!(
983							"The clipboard server thread panicked. Panic message: '{}'",
984							message,
985						);
986					} else {
987						error!("The clipboard server thread panicked.");
988					}
989				}
990			}
991		}
992	}
993}