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::{
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		// Text in ISO Latin-1 encoding
76		// See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
77		STRING,
78		// Text in unknown encoding
79		// See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
80		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		// This is just some random name for the property on our window, into which
90		// the clipboard owner writes the data we requested.
91		ARBOARD_CLIPBOARD,
92	}
93}
94
95thread_local! {
96	static ATOM_NAME_CACHE: RefCell<HashMap<Atom, &'static str>> = Default::default();
97}
98
99// Some clipboard items, like images, may take a very long time to produce a
100// `SelectionNotify`. Multiple seconds long.
101const 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	/// Join handle to the thread which serves selection requests.
115	server_handle: JoinHandle<()>,
116}
117
118struct XContext {
119	conn: RustConnection,
120	win_id: u32,
121}
122
123struct Inner {
124	/// The context for the thread which serves clipboard read
125	/// requests coming to us.
126	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		// create a new connection to an X11 server
142		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            // Just in case that some program reports SelectionNotify events
151            // with XCB_EVENT_MASK_PROPERTY_CHANGE mask.
152            EventMask::PROPERTY_CHANGE |
153            // To receive DestroyNotify event and stop the message loop.
154            EventMask::STRUCTURE_NOTIFY;
155		// create the window
156		conn.create_window(
157			// copy as much as possible from the parent, because no other specific input is needed
158			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			// don't subscribe to any special events because we are requesting everything we need ourselves
169			&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 around when this selection was last changed by us
182	/// for both use with the below condvar and logging.
183	mutex: Mutex<Option<Instant>>,
184	/// A condvar that is notified when the contents of this clipboard are changed.
185	///
186	/// This is associated with `Self::mutex`.
187	data_changed: Condvar,
188}
189
190#[derive(Debug, Clone)]
191struct ClipboardData {
192	bytes: Vec<u8>,
193
194	/// The atom representing the format in which the data is encoded.
195	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	/// Performs a "clear" operation on the clipboard, which is implemented by
223	/// relinquishing the selection to revert its owner to `None`. This gracefully
224	/// and comformly informs the X server and any clipboard managers that the
225	/// data was no longer valid and won't be offered from our window anymore.
226	///
227	/// See `ask_clipboard_manager_to_request_our_data` for more details on why
228	/// this is important and specification references.
229	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		// Just setting the data, and the `serve_requests` will take care of the rest.
253		let selection = self.selection_of(clipboard_selection);
254		let mut data_guard = selection.data.write();
255		*data_guard = Some(data);
256
257		// ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data
258		// changes.
259		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		// Lock the mutex to both ensure that no wakers of `data_changed` can wake us between
267		// dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other
268		// threads in that position.
269		let mut guard = selection.mutex.lock();
270		// Record the time we modify the selection.
271		*guard = Some(Instant::now());
272
273		// Notify any existing waiting threads that we have changed the data in the selection.
274		// It is important that the mutex is locked to prevent this notification getting lost.
275		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	/// `formats` must be a slice of atoms, where each atom represents a target format.
294	/// The first format from `formats`, which the clipboard owner supports will be the
295	/// format of the return value.
296	fn read(&self, formats: &[Atom], selection: LinuxClipboardKind) -> Result<ClipboardData> {
297		// if we are the current owner, we can get the current clipboard ourselves
298		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		// if let Some(data) = self.data.read().clone() {
312		//     return Ok(data)
313		// }
314		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		// Delete the property so that we can detect (using property notify)
338		// when the selection owner receives our request.
339		reader
340			.conn
341			.delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD)
342			.map_err(into_unknown)?;
343
344		// request to convert the clipboard selection to our data type(s)
345		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				// The first response after requesting a selection.
375				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							// This means we received an indication that an the
388							// data is going to be sent INCRementally. Let's
389							// reset our timeout.
390							timeout_end += SHORT_TIMEOUT_DUR;
391						}
392						ReadSelNotifyResult::EventNotRecognized => (),
393					}
394				}
395				// If the previous SelectionNotify event specified that the data
396				// will be sent in INCR segments, each segment is transferred in
397				// a PropertyNotify event.
398				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		// The property being set to NONE means that the `convert_selection`
494		// failed.
495
496		// According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4
497		// the target must be set to the same as what we requested.
498		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		// request the selection
510		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		// trace!("Property.type: {:?}", self.atom_name(reply.type_));
518
519		// we found something
520		if reply.type_ == target_format {
521			Ok(ReadSelNotifyResult::GotData(reply.value))
522		} else if reply.type_ == self.atoms.INCR {
523			// Note that we call the get_property again because we are
524			// indicating that we are ready to receive the data by deleting the
525			// property, however deleting only works if the type matches the
526			// property type. But the type didn't match in the previous call.
527			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			// this should never happen, we have sent a request only for supported types
549			Err(Error::unknown("incorrect type received from clipboard"))
550		}
551	}
552
553	/// Returns Ok(true) when the incr_data is ready
554	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			// This must mean the selection owner received our request, and is
568			// now preparing the data
569			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		// log::trace!("Received segment. value_len {}", reply.value_len,);
579		if reply.value_len == 0 {
580			// This indicates that all the data has been sent.
581			return Ok(true);
582		}
583		incr_data.extend(reply.value);
584
585		// Let's reset our timeout, since we received a valid chunk.
586		*timeout_end = Instant::now() + SHORT_TIMEOUT_DUR;
587
588		// Not yet complete
589		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		// we are asked for a list of supported conversion targets
603		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				// Estimation based on current data types, plus the other UTF-8 ones, plus `SAVE_TARGETS`.
609				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						// When we are storing a UTF8 string,
616						// add all equivalent formats to the supported targets
617						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				// If there's no data, we advertise an empty list of targets.
628				(Vec::with_capacity(2), false)
629			};
630
631			let mut targets = data_targets;
632			targets.push(self.atoms.TARGETS);
633
634			// NB: `SAVE_TARGETS` in this context is a marker atom which infomrs the clipboard manager
635			// we support this operation and _may_ use it in the future. To try and keep the manager's
636			// expectations/assumptions (if any) about when we will invoke this handoff, we go ahead and
637			// skip advertising support for the save operation entirely when the data was marked as
638			// sensitive.
639			//
640			// Note that even if we don't advertise it, some managers may respond to it anyways so this is
641			// only half of exclusion handling. See `ask_clipboard_manager_to_request_our_data` for more.
642			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					// TODO: change to `AtomEnum::ATOM`
653					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				// This must mean that we lost ownership of the data
682				// since the other side requested the selection.
683				// Let's respond with the property set to none.
684				success = false;
685			}
686		}
687		// on failure we notify the requester of it
688		let property = if success { event.property } else { AtomEnum::NONE.into() };
689		// tell the requestor that we finished sending data
690		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			// This shouldn't really ever happen but let's just check.
714			error!("The server's window id was 0. This is unexpected");
715			return Ok(());
716		}
717
718		// Per the `ClipboardManager` specification, only the `CLIPBOARD` target is
719		// to be saved from other X clients, so if the caller set the `Primary` (or `Secondary`) clipboard,
720		// we wouldn't expect any clipboard manager to save that anyway.
721		let selection = LinuxClipboardKind::Clipboard;
722
723		if !self.is_owner(selection)? {
724			// We are not owning the clipboard, nothing to do.
725			return Ok(());
726		}
727
728		match &*self.selection_of(selection).data.read() {
729			Some(data) => {
730				// If the data we are serving intended to be excluded, then don't bother asking the clipboard
731				// manager to save it. This is for several reasons:
732				// 1. Its counter-intuitive because the caller asked for this data to be minimally retained.
733				// 2. Regardless of if `SAVE_TARGETS` was advertised, we have to assume the manager may be saving history
734				// in a more proactive way and that would also be entirely dependent on it seeing the exclusion MIME before this.
735				// 3. Due to varying behavior in clipboard managers (some save prior to `SAVE_TARGETS`), it may just
736				// generate unnessecary warning logs in our handoff path even when we know a well-behaving manager isn't
737				// trying to save our sensitive data and that is misleading to users.
738				if data.iter().any(|data| data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT) {
739					// This step is the most important. Without it, some clipboard managers may think that our process
740					// crashed since the X window is destroyed without changing the selection owner first and try to save data.
741					//
742					// While this shouldn't need to happen based only on ICCCM 2.3.1 ("Voluntarily Giving Up Selection Ownership"),
743					// its documentation that destorying the owner window or terminating also reverts the owner to `None` doesn't
744					// reflect how desktop environment's X servers work in reality.
745					//
746					// By removing the owner, the manager doesn't think it needs to pick up our window's data serving once
747					// its destroyed and cleanly lets the data disappear based off the previously advertised exclusion hint.
748					if let Err(e) = self.clear(selection) {
749						warn!("failed to release sensitive data's clipboard ownership: {e}; it may end up persisted!");
750						// This is still not an error because we werent going to handoff anything to the manager.
751					}
752
753					return Ok(());
754				}
755			}
756			None => {
757				// If we don't have any data, there's nothing to do.
758				return Ok(());
759			}
760		}
761
762		// It's important that we lock the state before sending the request
763		// because we don't want the request server thread to lock the state
764		// after the request but before we can lock it here.
765		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		// Note that we are using a parking_lot condvar here, which doesn't wake up
784		// spuriously
785		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		// Not sure if unlocking the mutex is necessary here but better safe than sorry.
805		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				// This window is being destroyed.
823				trace!("Clipboard server window is being destroyed x_x");
824				return Ok(());
825			}
826			Event::SelectionClear(event) => {
827				// TODO: check if this works
828				// Someone else has new content in the clipboard, so it is
829				// notifying us that we should delete our data now.
830				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					// It is important that this mutex is locked at the time of calling
838					// `notify_all` to prevent notifications getting lost in case the sleeping
839					// thread has unlocked its `data_guard` and is just about to sleep.
840					// It is also important that the RwLock is kept write-locked for the same
841					// reason.
842					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				// Someone is requesting the clipboard content from us.
853				if let Err(e) = context.handle_selection_request(event) {
854					error!("Failed to handle selection request: {e}");
855					continue;
856				}
857
858				// if we are in the progress of saving to the clipboard manager
859				// make sure we save that we have finished writing
860				let handover_state = context.handover_state.lock();
861				if *handover_state == ManagerHandoverState::InProgress {
862					// Only set written, when the actual contents were written,
863					// not just a response to what TARGETS we have.
864					if event.target != context.atoms.TARGETS {
865						trace!("The contents were written to the clipboard manager.");
866						written = true;
867						// if we have written and notified, make sure to notify that we are done
868						if notified {
869							handover_finished(&context, handover_state);
870						}
871					}
872				}
873			}
874			Event::SelectionNotify(event) => {
875				// We've requested the clipboard content and this is the answer.
876				// Considering that this thread is not responsible for reading
877				// clipboard contents, this must come from the clipboard manager
878				// signaling that the data was handed over successfully.
879				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					// Note that some clipboard managers send a selection notify
886					// before even sending a request for the actual contents.
887					// (That's why we use the "notified" & "written" flags)
888					trace!("The clipboard manager indicated that it's done requesting the contents from us.");
889					notified = true;
890
891					// One would think that we could also finish if the property
892					// here is set 0, because that indicates failure. However
893					// this is not the case; for example on KDE plasma 5.18, we
894					// immediately get a SelectionNotify with property set to 0,
895					// but following that, we also get a valid SelectionRequest
896					// from the clipboard manager.
897					if written {
898						handover_finished(&context, handover_state);
899					}
900				}
901			}
902			_event => {
903				// May be useful for debugging but nothing else really.
904				// trace!("Received unwanted event: {:?}", event);
905			}
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		// At this point we know that the clipboard does not exist.
921		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			// ISO Latin-1
960			// See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8
961			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		// There are always at least 3 owners:
1071		// the global, the server thread, and one `Clipboard::inner`
1072		const MIN_OWNERS: usize = 3;
1073
1074		// We start with locking the global guard to prevent race
1075		// conditions below.
1076		let mut global_cb = CLIPBOARD.lock();
1077		if Arc::strong_count(&self.inner) == MIN_OWNERS {
1078			// If the are the only owners of the clipboard are ourselves and
1079			// the global object, then we should destroy the global object,
1080			// and send the data to the clipboard manager
1081
1082			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's try extracting the error message
1100					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				// By this point we've dropped the Global's reference to `Inner` and the background
1119				// thread has exited which means it also dropped its reference. Therefore `self.inner` should
1120				// be the last strong count.
1121				//
1122				// Note: The following is all best effort and is only for logging. Nothing is guaranteed to execute
1123				// or log.
1124				#[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						// This number has no meaning, its just a guess for how long
1143						// might be reasonable to give a clipboard manager a chance to
1144						// save contents based ~roughly on the handoff timeout.
1145						if elapsed > 100 {
1146							return;
1147						}
1148
1149						// If the app isn't running in a terminal don't print, use log instead.
1150						// Printing has a higher chance of being seen though, so its our default.
1151						// Its also close enough to a `debug_assert!` that it shouldn't come across strange.
1152						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}