Skip to main content

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::{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		// 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	/// A condvar that is notified when the contents of this clipboard are served.
189	///
190	/// This is associated with `Self::mutex`.
191	data_served: Condvar,
192}
193
194#[derive(Debug, Clone)]
195struct ClipboardData {
196	bytes: Vec<u8>,
197
198	/// The atom representing the format in which the data is encoded.
199	format: Atom,
200}
201
202enum ReadSelNotifyResult {
203	GotData(Vec<u8>),
204	IncrStarted,
205	EventNotRecognized,
206}
207
208impl Inner {
209	fn new() -> Result<Self> {
210		let server = XContext::new()?;
211		let atoms =
212			Atoms::new(&server.conn).map_err(into_unknown)?.reply().map_err(into_unknown)?;
213
214		Ok(Self {
215			server,
216			atoms,
217			clipboard: Selection::default(),
218			primary: Selection::default(),
219			secondary: Selection::default(),
220			handover_state: Mutex::new(ManagerHandoverState::Idle),
221			handover_cv: Condvar::new(),
222			serve_stopped: AtomicBool::new(false),
223		})
224	}
225
226	/// Performs a "clear" operation on the clipboard, which is implemented by
227	/// relinquishing the selection to revert its owner to `None`. This gracefully
228	/// and comformly informs the X server and any clipboard managers that the
229	/// data was no longer valid and won't be offered from our window anymore.
230	///
231	/// See `ask_clipboard_manager_to_request_our_data` for more details on why
232	/// this is important and specification references.
233	fn clear(&self, selection: LinuxClipboardKind) -> Result<()> {
234		let selection = self.atom_of(selection);
235
236		self.server
237			.conn
238			.set_selection_owner(NONE, selection, Time::CURRENT_TIME)
239			.map_err(into_unknown)?;
240
241		self.server.conn.flush().map_err(into_unknown)
242	}
243
244	fn write(
245		&self,
246		data: Vec<ClipboardData>,
247		clipboard_selection: LinuxClipboardKind,
248		wait: WaitConfig,
249	) -> Result<()> {
250		if self.serve_stopped.load(Ordering::Relaxed) {
251			return Err(Error::unknown("The clipboard handler thread seems to have stopped. Logging messages may reveal the cause. (See the `log` crate.)"));
252		}
253
254		let server_win = self.server.win_id;
255
256		// Just setting the data, and the `serve_requests` will take care of the rest.
257		let selection = self.selection_of(clipboard_selection);
258		let mut data_guard = selection.data.write();
259		*data_guard = Some(data);
260
261		// ICCCM version 2, section 2.6.1.3 states that we should re-assert ownership whenever data
262		// changes.
263		self.server
264			.conn
265			.set_selection_owner(server_win, self.atom_of(clipboard_selection), Time::CURRENT_TIME)
266			.map_err(|_| Error::ClipboardOccupied)?;
267
268		self.server.conn.flush().map_err(into_unknown)?;
269
270		// Lock the mutex to both ensure that no wakers of `data_changed` can wake us between
271		// dropping the `data_guard` and calling `wait[_for]` and that we don't we wake other
272		// threads in that position.
273		let mut guard = selection.mutex.lock();
274		// Record the time we modify the selection.
275		*guard = Some(Instant::now());
276
277		// Notify any existing waiting threads that we have changed the data in the selection.
278		// It is important that the mutex is locked to prevent this notification getting lost.
279		selection.data_changed.notify_all();
280
281		match wait {
282			WaitConfig::None => {}
283			WaitConfig::Forever => {
284				drop(data_guard);
285				selection.data_changed.wait(&mut guard);
286			}
287
288			WaitConfig::Until(deadline) => {
289				drop(data_guard);
290				selection.data_changed.wait_until(&mut guard, deadline);
291			}
292
293			WaitConfig::Once => {
294				drop(data_guard);
295				selection.data_served.wait(&mut guard);
296			}
297		}
298
299		Ok(())
300	}
301
302	/// `formats` must be a slice of atoms, where each atom represents a target format.
303	/// The first format from `formats`, which the clipboard owner supports will be the
304	/// format of the return value.
305	fn read(&self, formats: &[Atom], selection: LinuxClipboardKind) -> Result<ClipboardData> {
306		// if we are the current owner, we can get the current clipboard ourselves
307		if self.is_owner(selection)? {
308			let data = self.selection_of(selection).data.read();
309			if let Some(data_list) = &*data {
310				for data in data_list {
311					for format in formats {
312						if *format == data.format {
313							return Ok(data.clone());
314						}
315					}
316				}
317			}
318			return Err(Error::ContentNotAvailable);
319		}
320		// if let Some(data) = self.data.read().clone() {
321		//     return Ok(data)
322		// }
323		let reader = XContext::new()?;
324
325		trace!("Trying to get the clipboard data.");
326		for format in formats {
327			match self.read_single(&reader, selection, *format) {
328				Ok(bytes) => {
329					return Ok(ClipboardData { bytes, format: *format });
330				}
331				Err(Error::ContentNotAvailable) => {
332					continue;
333				}
334				Err(e) => return Err(e),
335			}
336		}
337		Err(Error::ContentNotAvailable)
338	}
339
340	fn read_single(
341		&self,
342		reader: &XContext,
343		selection: LinuxClipboardKind,
344		target_format: Atom,
345	) -> Result<Vec<u8>> {
346		// Delete the property so that we can detect (using property notify)
347		// when the selection owner receives our request.
348		reader
349			.conn
350			.delete_property(reader.win_id, self.atoms.ARBOARD_CLIPBOARD)
351			.map_err(into_unknown)?;
352
353		// request to convert the clipboard selection to our data type(s)
354		reader
355			.conn
356			.convert_selection(
357				reader.win_id,
358				self.atom_of(selection),
359				target_format,
360				self.atoms.ARBOARD_CLIPBOARD,
361				Time::CURRENT_TIME,
362			)
363			.map_err(into_unknown)?;
364		reader.conn.sync().map_err(into_unknown)?;
365
366		trace!("Finished `convert_selection`");
367
368		let mut incr_data: Vec<u8> = Vec::new();
369		let mut using_incr = false;
370
371		let mut timeout_end = Instant::now() + LONG_TIMEOUT_DUR;
372
373		while Instant::now() < timeout_end {
374			let event = reader.conn.poll_for_event().map_err(into_unknown)?;
375			let event = match event {
376				Some(e) => e,
377				None => {
378					std::thread::sleep(Duration::from_millis(1));
379					continue;
380				}
381			};
382			match event {
383				// The first response after requesting a selection.
384				Event::SelectionNotify(event) => {
385					trace!("Read SelectionNotify");
386					let result = self.handle_read_selection_notify(
387						reader,
388						target_format,
389						&mut using_incr,
390						&mut incr_data,
391						event,
392					)?;
393					match result {
394						ReadSelNotifyResult::GotData(data) => return Ok(data),
395						ReadSelNotifyResult::IncrStarted => {
396							// This means we received an indication that an the
397							// data is going to be sent INCRementally. Let's
398							// reset our timeout.
399							timeout_end += SHORT_TIMEOUT_DUR;
400						}
401						ReadSelNotifyResult::EventNotRecognized => (),
402					}
403				}
404				// If the previous SelectionNotify event specified that the data
405				// will be sent in INCR segments, each segment is transferred in
406				// a PropertyNotify event.
407				Event::PropertyNotify(event) => {
408					let result = self.handle_read_property_notify(
409						reader,
410						target_format,
411						using_incr,
412						&mut incr_data,
413						&mut timeout_end,
414						event,
415					)?;
416					if result {
417						return Ok(incr_data);
418					}
419				}
420				_ => log::trace!("An unexpected event arrived while reading the clipboard."),
421			}
422		}
423		log::info!("Time-out hit while reading the clipboard.");
424		Err(Error::ContentNotAvailable)
425	}
426
427	fn atom_of(&self, selection: LinuxClipboardKind) -> Atom {
428		match selection {
429			LinuxClipboardKind::Clipboard => self.atoms.CLIPBOARD,
430			LinuxClipboardKind::Primary => self.atoms.PRIMARY,
431			LinuxClipboardKind::Secondary => self.atoms.SECONDARY,
432		}
433	}
434
435	fn selection_of(&self, selection: LinuxClipboardKind) -> &Selection {
436		match selection {
437			LinuxClipboardKind::Clipboard => &self.clipboard,
438			LinuxClipboardKind::Primary => &self.primary,
439			LinuxClipboardKind::Secondary => &self.secondary,
440		}
441	}
442
443	fn kind_of(&self, atom: Atom) -> Option<LinuxClipboardKind> {
444		match atom {
445			a if a == self.atoms.CLIPBOARD => Some(LinuxClipboardKind::Clipboard),
446			a if a == self.atoms.PRIMARY => Some(LinuxClipboardKind::Primary),
447			a if a == self.atoms.SECONDARY => Some(LinuxClipboardKind::Secondary),
448			_ => None,
449		}
450	}
451
452	fn is_owner(&self, selection: LinuxClipboardKind) -> Result<bool> {
453		let current = self
454			.server
455			.conn
456			.get_selection_owner(self.atom_of(selection))
457			.map_err(into_unknown)?
458			.reply()
459			.map_err(into_unknown)?
460			.owner;
461
462		Ok(current == self.server.win_id)
463	}
464
465	fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
466		String::from_utf8(
467			self.server
468				.conn
469				.get_atom_name(atom)
470				.map_err(into_unknown)?
471				.reply()
472				.map_err(into_unknown)?
473				.name,
474		)
475		.map_err(into_unknown)
476	}
477	fn atom_name_dbg(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
478		ATOM_NAME_CACHE.with(|cache| {
479			let mut cache = cache.borrow_mut();
480			match cache.entry(atom) {
481				Entry::Occupied(entry) => *entry.get(),
482				Entry::Vacant(entry) => {
483					let s = self
484						.atom_name(atom)
485						.map(|s| Box::leak(s.into_boxed_str()) as &str)
486						.unwrap_or("FAILED-TO-GET-THE-ATOM-NAME");
487					entry.insert(s);
488					s
489				}
490			}
491		})
492	}
493
494	fn handle_read_selection_notify(
495		&self,
496		reader: &XContext,
497		target_format: u32,
498		using_incr: &mut bool,
499		incr_data: &mut Vec<u8>,
500		event: SelectionNotifyEvent,
501	) -> Result<ReadSelNotifyResult> {
502		// The property being set to NONE means that the `convert_selection`
503		// failed.
504
505		// According to: https://tronche.com/gui/x/icccm/sec-2.html#s-2.4
506		// the target must be set to the same as what we requested.
507		if event.property == NONE || event.target != target_format {
508			return Err(Error::ContentNotAvailable);
509		}
510		if self.kind_of(event.selection).is_none() {
511			log::info!("Received a SelectionNotify for a selection other than CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
512			return Ok(ReadSelNotifyResult::EventNotRecognized);
513		}
514		if *using_incr {
515			log::warn!("Received a SelectionNotify while already expecting INCR segments.");
516			return Ok(ReadSelNotifyResult::EventNotRecognized);
517		}
518		// request the selection
519		let mut reply = reader
520			.conn
521			.get_property(true, event.requestor, event.property, event.target, 0, u32::MAX / 4)
522			.map_err(into_unknown)?
523			.reply()
524			.map_err(into_unknown)?;
525
526		// trace!("Property.type: {:?}", self.atom_name(reply.type_));
527
528		// we found something
529		if reply.type_ == target_format {
530			Ok(ReadSelNotifyResult::GotData(reply.value))
531		} else if reply.type_ == self.atoms.INCR {
532			// Note that we call the get_property again because we are
533			// indicating that we are ready to receive the data by deleting the
534			// property, however deleting only works if the type matches the
535			// property type. But the type didn't match in the previous call.
536			reply = reader
537				.conn
538				.get_property(
539					true,
540					event.requestor,
541					event.property,
542					self.atoms.INCR,
543					0,
544					u32::MAX / 4,
545				)
546				.map_err(into_unknown)?
547				.reply()
548				.map_err(into_unknown)?;
549			log::trace!("Receiving INCR segments");
550			*using_incr = true;
551			if reply.value_len == 4 {
552				let min_data_len = reply.value32().and_then(|mut vals| vals.next()).unwrap_or(0);
553				incr_data.reserve(min_data_len as usize);
554			}
555			Ok(ReadSelNotifyResult::IncrStarted)
556		} else {
557			// this should never happen, we have sent a request only for supported types
558			Err(Error::unknown("incorrect type received from clipboard"))
559		}
560	}
561
562	/// Returns Ok(true) when the incr_data is ready
563	fn handle_read_property_notify(
564		&self,
565		reader: &XContext,
566		target_format: u32,
567		using_incr: bool,
568		incr_data: &mut Vec<u8>,
569		timeout_end: &mut Instant,
570		event: PropertyNotifyEvent,
571	) -> Result<bool> {
572		if event.atom != self.atoms.ARBOARD_CLIPBOARD || event.state != Property::NEW_VALUE {
573			return Ok(false);
574		}
575		if !using_incr {
576			// This must mean the selection owner received our request, and is
577			// now preparing the data
578			return Ok(false);
579		}
580		let reply = reader
581			.conn
582			.get_property(true, event.window, event.atom, target_format, 0, u32::MAX / 4)
583			.map_err(into_unknown)?
584			.reply()
585			.map_err(into_unknown)?;
586
587		// log::trace!("Received segment. value_len {}", reply.value_len,);
588		if reply.value_len == 0 {
589			// This indicates that all the data has been sent.
590			return Ok(true);
591		}
592		incr_data.extend(reply.value);
593
594		// Let's reset our timeout, since we received a valid chunk.
595		*timeout_end = Instant::now() + SHORT_TIMEOUT_DUR;
596
597		// Not yet complete
598		Ok(false)
599	}
600
601	fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
602		let selection = match self.kind_of(event.selection) {
603			Some(kind) => kind,
604			None => {
605				warn!("Received a selection request to a selection other than the CLIPBOARD, PRIMARY or SECONDARY. This is unexpected.");
606				return Ok(());
607			}
608		};
609
610		let success;
611		// we are asked for a list of supported conversion targets
612		if event.target == self.atoms.TARGETS {
613			trace!("Handling TARGETS, dst property is {}", self.atom_name_dbg(event.property));
614
615			let data = self.selection_of(selection).data.read();
616			let (data_targets, excluded) = if let Some(data_list) = &*data {
617				// Estimation based on current data types, plus the other UTF-8 ones, plus `SAVE_TARGETS`.
618				let mut targets = Vec::with_capacity(data_list.len() + 3);
619				let mut excluded = false;
620
621				for data in data_list {
622					targets.push(data.format);
623					if data.format == self.atoms.UTF8_STRING {
624						// When we are storing a UTF8 string,
625						// add all equivalent formats to the supported targets
626						targets.push(self.atoms.UTF8_MIME_0);
627						targets.push(self.atoms.UTF8_MIME_1);
628					}
629
630					if data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT {
631						excluded = true;
632					}
633				}
634				(targets, excluded)
635			} else {
636				// If there's no data, we advertise an empty list of targets.
637				(Vec::with_capacity(2), false)
638			};
639
640			let mut targets = data_targets;
641			targets.push(self.atoms.TARGETS);
642
643			// NB: `SAVE_TARGETS` in this context is a marker atom which infomrs the clipboard manager
644			// we support this operation and _may_ use it in the future. To try and keep the manager's
645			// expectations/assumptions (if any) about when we will invoke this handoff, we go ahead and
646			// skip advertising support for the save operation entirely when the data was marked as
647			// sensitive.
648			//
649			// Note that even if we don't advertise it, some managers may respond to it anyways so this is
650			// only half of exclusion handling. See `ask_clipboard_manager_to_request_our_data` for more.
651			if !excluded {
652				targets.push(self.atoms.SAVE_TARGETS);
653			}
654
655			self.server
656				.conn
657				.change_property32(
658					PropMode::REPLACE,
659					event.requestor,
660					event.property,
661					// TODO: change to `AtomEnum::ATOM`
662					self.atoms.ATOM,
663					&targets,
664				)
665				.map_err(into_unknown)?;
666			self.server.conn.flush().map_err(into_unknown)?;
667			success = true;
668		} else {
669			trace!("Handling request for (probably) the clipboard contents.");
670			let data = self.selection_of(selection).data.read();
671			if let Some(data_list) = &*data {
672				success = match data_list.iter().find(|d| d.format == event.target) {
673					Some(data) => {
674						self.server
675							.conn
676							.change_property8(
677								PropMode::REPLACE,
678								event.requestor,
679								event.property,
680								event.target,
681								&data.bytes,
682							)
683							.map_err(into_unknown)?;
684						self.server.conn.flush().map_err(into_unknown)?;
685						true
686					}
687					None => false,
688				};
689			} else {
690				// This must mean that we lost ownership of the data
691				// since the other side requested the selection.
692				// Let's respond with the property set to none.
693				success = false;
694			}
695		}
696		// on failure we notify the requester of it
697		let property = if success { event.property } else { AtomEnum::NONE.into() };
698		// tell the requestor that we finished sending data
699		self.server
700			.conn
701			.send_event(
702				false,
703				event.requestor,
704				EventMask::NO_EVENT,
705				SelectionNotifyEvent {
706					response_type: SELECTION_NOTIFY_EVENT,
707					sequence: event.sequence,
708					time: event.time,
709					requestor: event.requestor,
710					selection: event.selection,
711					target: event.target,
712					property,
713				},
714			)
715			.map_err(into_unknown)?;
716
717		self.server.conn.flush().map_err(into_unknown)
718	}
719
720	fn ask_clipboard_manager_to_request_our_data(&self) -> Result<()> {
721		if self.server.win_id == 0 {
722			// This shouldn't really ever happen but let's just check.
723			error!("The server's window id was 0. This is unexpected");
724			return Ok(());
725		}
726
727		// Per the `ClipboardManager` specification, only the `CLIPBOARD` target is
728		// to be saved from other X clients, so if the caller set the `Primary` (or `Secondary`) clipboard,
729		// we wouldn't expect any clipboard manager to save that anyway.
730		let selection = LinuxClipboardKind::Clipboard;
731
732		if !self.is_owner(selection)? {
733			// We are not owning the clipboard, nothing to do.
734			return Ok(());
735		}
736
737		match &*self.selection_of(selection).data.read() {
738			Some(data) => {
739				// If the data we are serving intended to be excluded, then don't bother asking the clipboard
740				// manager to save it. This is for several reasons:
741				// 1. Its counter-intuitive because the caller asked for this data to be minimally retained.
742				// 2. Regardless of if `SAVE_TARGETS` was advertised, we have to assume the manager may be saving history
743				// in a more proactive way and that would also be entirely dependent on it seeing the exclusion MIME before this.
744				// 3. Due to varying behavior in clipboard managers (some save prior to `SAVE_TARGETS`), it may just
745				// generate unnessecary warning logs in our handoff path even when we know a well-behaving manager isn't
746				// trying to save our sensitive data and that is misleading to users.
747				if data.iter().any(|data| data.format == self.atoms.X_KDE_PASSWORDMANAGERHINT) {
748					// This step is the most important. Without it, some clipboard managers may think that our process
749					// crashed since the X window is destroyed without changing the selection owner first and try to save data.
750					//
751					// While this shouldn't need to happen based only on ICCCM 2.3.1 ("Voluntarily Giving Up Selection Ownership"),
752					// its documentation that destorying the owner window or terminating also reverts the owner to `None` doesn't
753					// reflect how desktop environment's X servers work in reality.
754					//
755					// By removing the owner, the manager doesn't think it needs to pick up our window's data serving once
756					// its destroyed and cleanly lets the data disappear based off the previously advertised exclusion hint.
757					if let Err(e) = self.clear(selection) {
758						warn!("failed to release sensitive data's clipboard ownership: {e}; it may end up persisted!");
759						// This is still not an error because we werent going to handoff anything to the manager.
760					}
761
762					return Ok(());
763				}
764			}
765			None => {
766				// If we don't have any data, there's nothing to do.
767				return Ok(());
768			}
769		}
770
771		// It's important that we lock the state before sending the request
772		// because we don't want the request server thread to lock the state
773		// after the request but before we can lock it here.
774		let mut handover_state = self.handover_state.lock();
775
776		trace!("Sending the data to the clipboard manager");
777		self.server
778			.conn
779			.convert_selection(
780				self.server.win_id,
781				self.atoms.CLIPBOARD_MANAGER,
782				self.atoms.SAVE_TARGETS,
783				self.atoms.ARBOARD_CLIPBOARD,
784				Time::CURRENT_TIME,
785			)
786			.map_err(into_unknown)?;
787		self.server.conn.flush().map_err(into_unknown)?;
788
789		*handover_state = ManagerHandoverState::InProgress;
790		let max_handover_duration = Duration::from_millis(100);
791
792		// Note that we are using a parking_lot condvar here, which doesn't wake up
793		// spuriously
794		let result = self.handover_cv.wait_for(&mut handover_state, max_handover_duration);
795
796		if *handover_state == ManagerHandoverState::Finished {
797			return Ok(());
798		}
799		if result.timed_out() {
800			warn!("Could not hand the clipboard contents over to the clipboard manager. The request timed out.");
801			return Ok(());
802		}
803
804		unreachable!("This is a bug! The handover was not finished and the condvar didn't time out, yet the condvar wait ended.")
805	}
806}
807
808fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>> {
809	fn handover_finished(clip: &Arc<Inner>, mut handover_state: MutexGuard<ManagerHandoverState>) {
810		log::trace!("Finishing clipboard manager handover.");
811		*handover_state = ManagerHandoverState::Finished;
812
813		// Not sure if unlocking the mutex is necessary here but better safe than sorry.
814		drop(handover_state);
815
816		clip.handover_cv.notify_all();
817	}
818
819	trace!("Started serve requests thread.");
820
821	let _guard = ScopeGuard::new(|| {
822		context.serve_stopped.store(true, Ordering::Relaxed);
823	});
824
825	let mut written = false;
826	let mut notified = false;
827
828	loop {
829		match context.server.conn.wait_for_event().map_err(into_unknown)? {
830			Event::DestroyNotify(_) => {
831				// This window is being destroyed.
832				trace!("Clipboard server window is being destroyed x_x");
833				return Ok(());
834			}
835			Event::SelectionClear(event) => {
836				// TODO: check if this works
837				// Someone else has new content in the clipboard, so it is
838				// notifying us that we should delete our data now.
839				trace!("Somebody else owns the clipboard now");
840
841				if let Some(selection) = context.kind_of(event.selection) {
842					let selection = context.selection_of(selection);
843					let mut data_guard = selection.data.write();
844					*data_guard = None;
845
846					// It is important that this mutex is locked at the time of calling
847					// `notify_all` to prevent notifications getting lost in case the sleeping
848					// thread has unlocked its `data_guard` and is just about to sleep.
849					// It is also important that the RwLock is kept write-locked for the same
850					// reason.
851					let _guard = selection.mutex.lock();
852					selection.data_changed.notify_all();
853					// If a thread is waiting for the data to be served,
854					// there's no data to be served anymore, so wake it up.
855					selection.data_served.notify_all();
856				}
857			}
858			Event::SelectionRequest(event) => {
859				trace!(
860					"SelectionRequest - selection is: {}, target is {}",
861					context.atom_name_dbg(event.selection),
862					context.atom_name_dbg(event.target),
863				);
864				// Someone is requesting the clipboard content from us.
865				if let Err(e) = context.handle_selection_request(event) {
866					error!("Failed to handle selection request: {e}");
867					continue;
868				}
869
870				if let Some(selection) = context.kind_of(event.selection) {
871					let selection = context.selection_of(selection);
872
873					// It is important that this mutex is locked at the time of calling
874					// `notify_all` to prevent notifications getting lost in case the sleeping
875					// thread has unlocked its `data_guard` and is just about to sleep.
876					let _guard = selection.mutex.lock();
877					// Notify a thread that's waiting on the data to be served once.
878					selection.data_served.notify_all();
879				}
880
881				// if we are in the progress of saving to the clipboard manager
882				// make sure we save that we have finished writing
883				let handover_state = context.handover_state.lock();
884				if *handover_state == ManagerHandoverState::InProgress {
885					// Only set written, when the actual contents were written,
886					// not just a response to what TARGETS we have.
887					if event.target != context.atoms.TARGETS {
888						trace!("The contents were written to the clipboard manager.");
889						written = true;
890						// if we have written and notified, make sure to notify that we are done
891						if notified {
892							handover_finished(&context, handover_state);
893						}
894					}
895				}
896			}
897			Event::SelectionNotify(event) => {
898				// We've requested the clipboard content and this is the answer.
899				// Considering that this thread is not responsible for reading
900				// clipboard contents, this must come from the clipboard manager
901				// signaling that the data was handed over successfully.
902				if event.selection != context.atoms.CLIPBOARD_MANAGER {
903					error!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread.");
904					continue;
905				}
906				let handover_state = context.handover_state.lock();
907				if *handover_state == ManagerHandoverState::InProgress {
908					// Note that some clipboard managers send a selection notify
909					// before even sending a request for the actual contents.
910					// (That's why we use the "notified" & "written" flags)
911					trace!("The clipboard manager indicated that it's done requesting the contents from us.");
912					notified = true;
913
914					// One would think that we could also finish if the property
915					// here is set 0, because that indicates failure. However
916					// this is not the case; for example on KDE plasma 5.18, we
917					// immediately get a SelectionNotify with property set to 0,
918					// but following that, we also get a valid SelectionRequest
919					// from the clipboard manager.
920					if written {
921						handover_finished(&context, handover_state);
922					}
923				}
924			}
925			_event => {
926				// May be useful for debugging but nothing else really.
927				// trace!("Received unwanted event: {:?}", event);
928			}
929		}
930	}
931}
932
933pub(crate) struct Clipboard {
934	inner: Arc<Inner>,
935}
936
937impl Clipboard {
938	pub(crate) fn new() -> Result<Self> {
939		let mut global_cb = CLIPBOARD.lock();
940		if let Some(global_cb) = &*global_cb {
941			return Ok(Self { inner: Arc::clone(&global_cb.inner) });
942		}
943		// At this point we know that the clipboard does not exist.
944		let ctx = Arc::new(Inner::new()?);
945		let join_handle;
946		{
947			let ctx = Arc::clone(&ctx);
948			join_handle = std::thread::spawn(move || {
949				if let Err(error) = serve_requests(ctx) {
950					error!("Worker thread errored with: {}", error);
951				}
952			});
953		}
954		*global_cb = Some(GlobalClipboard { inner: Arc::clone(&ctx), server_handle: join_handle });
955		Ok(Self { inner: ctx })
956	}
957
958	fn add_clipboard_exclusions(&self, exclude_from_history: bool, data: &mut Vec<ClipboardData>) {
959		if exclude_from_history {
960			data.push(ClipboardData {
961				bytes: KDE_EXCLUSION_HINT.to_vec(),
962				format: self.inner.atoms.X_KDE_PASSWORDMANAGERHINT,
963			})
964		}
965	}
966
967	pub(crate) fn clear(&self, selection: LinuxClipboardKind) -> Result<()> {
968		self.inner.clear(selection)
969	}
970
971	pub(crate) fn get_text(&self, selection: LinuxClipboardKind) -> Result<String> {
972		let formats = [
973			self.inner.atoms.UTF8_STRING,
974			self.inner.atoms.UTF8_MIME_0,
975			self.inner.atoms.UTF8_MIME_1,
976			self.inner.atoms.STRING,
977			self.inner.atoms.TEXT,
978			self.inner.atoms.TEXT_MIME_UNKNOWN,
979		];
980		let result = self.inner.read(&formats, selection)?;
981		if result.format == self.inner.atoms.STRING {
982			// ISO Latin-1
983			// See: https://stackoverflow.com/questions/28169745/what-are-the-options-to-convert-iso-8859-1-latin-1-to-a-string-utf-8
984			Ok(result.bytes.into_iter().map(|c| c as char).collect())
985		} else {
986			String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
987		}
988	}
989
990	pub(crate) fn set_text(
991		&self,
992		message: Cow<'_, str>,
993		selection: LinuxClipboardKind,
994		wait: WaitConfig,
995		exclude_from_history: bool,
996	) -> Result<()> {
997		let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
998		data.push(ClipboardData {
999			bytes: message.into_owned().into_bytes(),
1000			format: self.inner.atoms.UTF8_STRING,
1001		});
1002
1003		self.add_clipboard_exclusions(exclude_from_history, &mut data);
1004
1005		self.inner.write(data, selection, wait)
1006	}
1007
1008	pub(crate) fn get_html(&self, selection: LinuxClipboardKind) -> Result<String> {
1009		let formats = [self.inner.atoms.HTML];
1010		let result = self.inner.read(&formats, selection)?;
1011		String::from_utf8(result.bytes).map_err(|_| Error::ConversionFailure)
1012	}
1013
1014	pub(crate) fn set_html(
1015		&self,
1016		html: Cow<'_, str>,
1017		alt: Option<Cow<'_, str>>,
1018		selection: LinuxClipboardKind,
1019		wait: WaitConfig,
1020		exclude_from_history: bool,
1021	) -> Result<()> {
1022		let mut data = {
1023			let cap = [true, alt.is_some(), exclude_from_history]
1024				.map(|v| usize::from(v as u8))
1025				.iter()
1026				.sum();
1027			Vec::with_capacity(cap)
1028		};
1029
1030		if let Some(alt_text) = alt {
1031			data.push(ClipboardData {
1032				bytes: alt_text.into_owned().into_bytes(),
1033				format: self.inner.atoms.UTF8_STRING,
1034			});
1035		}
1036		data.push(ClipboardData {
1037			bytes: html.into_owned().into_bytes(),
1038			format: self.inner.atoms.HTML,
1039		});
1040
1041		self.add_clipboard_exclusions(exclude_from_history, &mut data);
1042
1043		self.inner.write(data, selection, wait)
1044	}
1045
1046	#[cfg(feature = "image-data")]
1047	pub(crate) fn get_image(&self, selection: LinuxClipboardKind) -> Result<ImageData<'static>> {
1048		let formats = [self.inner.atoms.PNG_MIME];
1049		let bytes = self.inner.read(&formats, selection)?.bytes;
1050
1051		let cursor = std::io::Cursor::new(&bytes);
1052		let mut reader = image::io::Reader::new(cursor);
1053		reader.set_format(image::ImageFormat::Png);
1054		let image = match reader.decode() {
1055			Ok(img) => img.into_rgba8(),
1056			Err(_e) => return Err(Error::ConversionFailure),
1057		};
1058		let (w, h) = image.dimensions();
1059		let image_data =
1060			ImageData { width: w as usize, height: h as usize, bytes: image.into_raw().into() };
1061		Ok(image_data)
1062	}
1063
1064	#[cfg(feature = "image-data")]
1065	pub(crate) fn set_image(
1066		&self,
1067		image: ImageData,
1068		selection: LinuxClipboardKind,
1069		wait: WaitConfig,
1070		exclude_from_history: bool,
1071	) -> Result<()> {
1072		let encoded = encode_as_png(&image)?;
1073		let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
1074
1075		data.push(ClipboardData { bytes: encoded, format: self.inner.atoms.PNG_MIME });
1076
1077		self.add_clipboard_exclusions(exclude_from_history, &mut data);
1078
1079		self.inner.write(data, selection, wait)
1080	}
1081
1082	pub(crate) fn get_file_list(&self, selection: LinuxClipboardKind) -> Result<Vec<PathBuf>> {
1083		let result = self.inner.read(&[self.inner.atoms.URI_LIST], selection)?;
1084
1085		Ok(paths_from_uri_list(result.bytes))
1086	}
1087
1088	pub(crate) fn set_file_list(
1089		&self,
1090		file_list: &[impl AsRef<Path>],
1091		selection: LinuxClipboardKind,
1092		wait: WaitConfig,
1093		exclude_from_history: bool,
1094	) -> Result<()> {
1095		let files = paths_to_uri_list(file_list)?;
1096		let mut data = Vec::with_capacity(if exclude_from_history { 2 } else { 1 });
1097
1098		data.push(ClipboardData { bytes: files.into_bytes(), format: self.inner.atoms.URI_LIST });
1099		self.add_clipboard_exclusions(exclude_from_history, &mut data);
1100
1101		self.inner.write(data, selection, wait)
1102	}
1103}
1104
1105impl Drop for Clipboard {
1106	fn drop(&mut self) {
1107		// There are always at least 3 owners:
1108		// the global, the server thread, and one `Clipboard::inner`
1109		const MIN_OWNERS: usize = 3;
1110
1111		// We start with locking the global guard to prevent race
1112		// conditions below.
1113		let mut global_cb = CLIPBOARD.lock();
1114		if Arc::strong_count(&self.inner) == MIN_OWNERS {
1115			// If the are the only owners of the clipboard are ourselves and
1116			// the global object, then we should destroy the global object,
1117			// and send the data to the clipboard manager
1118
1119			if let Err(e) = self.inner.ask_clipboard_manager_to_request_our_data() {
1120				error!("Could not hand the clipboard data over to the clipboard manager: {}", e);
1121			}
1122			let global_cb = global_cb.take();
1123			if let Err(e) = self.inner.server.conn.destroy_window(self.inner.server.win_id) {
1124				error!("Failed to destroy the clipboard window. Error: {}", e);
1125				return;
1126			}
1127			if let Err(e) = self.inner.server.conn.flush() {
1128				error!("Failed to flush the clipboard window. Error: {}", e);
1129				return;
1130			}
1131			if let Some(global_cb) = global_cb {
1132				let GlobalClipboard { inner, server_handle } = global_cb;
1133				drop(inner);
1134
1135				if let Err(e) = server_handle.join() {
1136					// Let's try extracting the error message
1137					let message;
1138					if let Some(msg) = e.downcast_ref::<&'static str>() {
1139						message = Some((*msg).to_string());
1140					} else if let Some(msg) = e.downcast_ref::<String>() {
1141						message = Some(msg.clone());
1142					} else {
1143						message = None;
1144					}
1145					if let Some(message) = message {
1146						error!(
1147							"The clipboard server thread panicked. Panic message: '{}'",
1148							message,
1149						);
1150					} else {
1151						error!("The clipboard server thread panicked.");
1152					}
1153				}
1154
1155				// By this point we've dropped the Global's reference to `Inner` and the background
1156				// thread has exited which means it also dropped its reference. Therefore `self.inner` should
1157				// be the last strong count.
1158				//
1159				// Note: The following is all best effort and is only for logging. Nothing is guaranteed to execute
1160				// or log.
1161				#[cfg(debug_assertions)]
1162				if let Some(inner) = Arc::get_mut(&mut self.inner) {
1163					use std::io::IsTerminal;
1164
1165					let mut change_timestamps = Vec::with_capacity(2);
1166					let mut collect_changed = |sel: &mut Mutex<Option<Instant>>| {
1167						if let Some(changed) = sel.get_mut() {
1168							change_timestamps.push(*changed);
1169						}
1170					};
1171
1172					collect_changed(&mut inner.clipboard.mutex);
1173					collect_changed(&mut inner.primary.mutex);
1174					collect_changed(&mut inner.secondary.mutex);
1175
1176					change_timestamps.sort();
1177					if let Some(last) = change_timestamps.last() {
1178						let elapsed = last.elapsed().as_millis();
1179						// This number has no meaning, its just a guess for how long
1180						// might be reasonable to give a clipboard manager a chance to
1181						// save contents based ~roughly on the handoff timeout.
1182						if elapsed > 100 {
1183							return;
1184						}
1185
1186						// If the app isn't running in a terminal don't print, use log instead.
1187						// Printing has a higher chance of being seen though, so its our default.
1188						// Its also close enough to a `debug_assert!` that it shouldn't come across strange.
1189						let msg = format!("Clipboard was dropped very quickly after writing ({elapsed}ms); clipboard managers may not have seen the contents\nConsider keeping `Clipboard` in more persistent state somewhere or keeping the contents alive longer using `SetLinuxExt` and/or threads.");
1190						if std::io::stderr().is_terminal() {
1191							eprintln!("{msg}");
1192						} else {
1193							log::warn!("{msg}");
1194						}
1195					}
1196				}
1197			}
1198		}
1199	}
1200}