clipboard_rs/platform/
x11.rs

1use crate::{
2	common::Result,
3	// #[cfg(feature = "image")]
4	ClipboardContent,
5	ClipboardHandler,
6	ContentFormat,
7};
8
9#[cfg(feature = "image")]
10use crate::{common::RustImage, RustImageData};
11
12use crate::{Clipboard, ClipboardWatcher};
13use std::sync::mpsc::{self, Receiver, Sender};
14use std::{
15	sync::{Arc, RwLock},
16	thread,
17	time::{Duration, Instant},
18};
19use x11rb::{
20	connection::Connection,
21	protocol::{
22		xfixes,
23		xproto::{
24			Atom, AtomEnum, ConnectionExt as _, CreateWindowAux, EventMask, PropMode, Property,
25			SelectionNotifyEvent, SelectionRequestEvent, WindowClass, SELECTION_NOTIFY_EVENT,
26		},
27		Event,
28	},
29	rust_connection::RustConnection,
30	wrapper::ConnectionExt as _,
31	COPY_DEPTH_FROM_PARENT, CURRENT_TIME,
32};
33
34x11rb::atom_manager! {
35	pub Atoms: AtomCookies {
36		CLIPBOARD,
37		CLIPBOARD_MANAGER,
38		PROPERTY,
39		SAVE_TARGETS,
40		TARGETS,
41		ATOM,
42		INCR,
43		TIMESTAMP,
44		MULTIPLE,
45
46		UTF8_STRING,
47		UTF8_MIME_0: b"text/plain;charset=utf-8",
48		UTF8_MIME_1: b"text/plain;charset=UTF-8",
49		// Text in ISO Latin-1 encoding
50		// See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
51		STRING,
52		// Text in unknown encoding
53		// See: https://tronche.com/gui/x/icccm/sec-2.html#s-2.6.2
54		TEXT,
55		TEXT_MIME_UNKNOWN: b"text/plain",
56		// Rich Text Format
57		RTF: b"text/rtf",
58		RTF_1: b"text/richtext",
59		HTML: b"text/html",
60		PNG_MIME: b"image/png",
61		FILE_LIST: b"text/uri-list",
62		GNOME_COPY_FILES: b"x-special/gnome-copied-files",
63		NAUTILUS_FILE_LIST: b"x-special/nautilus-clipboard",
64	}
65}
66
67pub const DEFAULT_READ_TIMEOUT: u64 = 500;
68
69// zh: 用于创建 X11 剪贴板上下文的选项
70// en: Options for creating an X11 clipboard context
71pub struct ClipboardContextX11Options {
72	// zh: 剪贴板读取操作超时
73	// en: Timeout for clipboard read operations
74	pub read_timeout: Option<Duration>,
75}
76
77const FILE_PATH_PREFIX: &str = "file://";
78pub struct ClipboardContext {
79	inner: Arc<InnerContext>,
80	read_timeout: Option<Duration>,
81}
82
83struct ClipboardData {
84	format: Atom,
85	data: Vec<u8>,
86}
87
88struct InnerContext {
89	server: XServerContext,
90	server_for_write: XServerContext,
91	ignore_formats: Vec<Atom>,
92	// 此刻待写入的剪贴板内容
93	wait_write_data: RwLock<Vec<ClipboardData>>,
94}
95
96impl InnerContext {
97	pub fn new() -> Result<Self> {
98		let server = XServerContext::new()?;
99		let server_for_write = XServerContext::new()?;
100		let wait_write_data = RwLock::new(Vec::new());
101
102		let ignore_formats = vec![
103			server.atoms.TIMESTAMP,
104			server.atoms.MULTIPLE,
105			server.atoms.TARGETS,
106			server.atoms.SAVE_TARGETS,
107		];
108
109		Ok(Self {
110			server,
111			server_for_write,
112			ignore_formats,
113			wait_write_data,
114		})
115	}
116
117	pub fn handle_selection_request(&self, event: SelectionRequestEvent) -> Result<()> {
118		let success;
119		let ctx = &self.server_for_write;
120		let atoms = ctx.atoms;
121		// we are asked for a list of supported conversion targets
122		if event.target == atoms.TARGETS {
123			let reader = self.wait_write_data.read();
124			match reader {
125				Ok(data_list) => {
126					let mut targets = Vec::with_capacity(10);
127					targets.push(atoms.TARGETS);
128					targets.push(atoms.SAVE_TARGETS);
129					if !data_list.is_empty() {
130						data_list.iter().for_each(|data| {
131							targets.push(data.format);
132						});
133					}
134					ctx.conn.change_property32(
135						PropMode::REPLACE,
136						event.requestor,
137						event.property,
138						AtomEnum::ATOM,
139						&targets,
140					)?;
141					success = true;
142				}
143				Err(_) => return Err("Failed to read clipboard data".into()),
144			}
145		} else {
146			let reader = self.wait_write_data.read();
147			match reader {
148				Ok(data_list) => {
149					success = match data_list.iter().find(|d| d.format == event.target) {
150						Some(data) => {
151							ctx.conn.change_property8(
152								PropMode::REPLACE,
153								event.requestor,
154								event.property,
155								event.target,
156								&data.data,
157							)?;
158							true
159						}
160						None => false,
161					};
162				}
163				Err(_) => return Err("Failed to read clipboard data".into()),
164			}
165		}
166		// on failure, we notify the requester of it
167		let property = if success {
168			event.property
169		} else {
170			AtomEnum::NONE.into()
171		};
172		// tell the requester that we finished sending data
173		ctx.conn.send_event(
174			false,
175			event.requestor,
176			EventMask::NO_EVENT,
177			SelectionNotifyEvent {
178				response_type: SELECTION_NOTIFY_EVENT,
179				sequence: event.sequence,
180				time: event.time,
181				requestor: event.requestor,
182				selection: event.selection,
183				target: event.target,
184				property,
185			},
186		)?;
187		ctx.conn.flush()?;
188		Ok(())
189	}
190
191	pub fn process_event(
192		&self,
193		buff: &mut Vec<u8>,
194		selection: Atom,
195		target: Atom,
196		property: Atom,
197		timeout: Option<Duration>,
198		sequence_number: u64,
199	) -> Result<()> {
200		let mut is_incr = false;
201		let start_time = if timeout.is_some() {
202			Some(Instant::now())
203		} else {
204			None
205		};
206		let ctx = &self.server;
207		let atoms = ctx.atoms;
208		loop {
209			if timeout
210				.into_iter()
211				.zip(start_time)
212				.next()
213				.map(|(timeout, time)| (Instant::now() - time) >= timeout)
214				.unwrap_or(false)
215			{
216				return Err("Timeout while waiting for clipboard data".into());
217			}
218
219			let (event, seq) = match ctx.conn.poll_for_event_with_sequence()? {
220				Some(event) => event,
221				None => {
222					thread::park_timeout(Duration::from_millis(50));
223					continue;
224				}
225			};
226
227			if seq < sequence_number {
228				continue;
229			}
230
231			match event {
232				Event::SelectionNotify(event) => {
233					if event.selection != selection {
234						continue;
235					};
236
237					let target_type = {
238						if target == atoms.TARGETS {
239							atoms.ATOM
240						} else {
241							target
242						}
243					};
244
245					let reply = ctx
246						.conn
247						.get_property(
248							false,
249							event.requestor,
250							event.property,
251							target_type,
252							buff.len() as u32,
253							u32::MAX,
254						)?
255						.reply()?;
256
257					if reply.type_ == atoms.INCR {
258						if let Some(mut value) = reply.value32() {
259							if let Some(size) = value.next() {
260								buff.reserve(size as usize);
261							}
262						}
263						ctx.conn.delete_property(ctx.win_id, property)?.check()?;
264						is_incr = true;
265						continue;
266					} else if reply.type_ != target && reply.type_ != atoms.ATOM {
267						return Err("Clipboard data type mismatch".into());
268					}
269					buff.extend_from_slice(&reply.value);
270					break;
271				}
272
273				Event::PropertyNotify(event) if is_incr => {
274					if event.state != Property::NEW_VALUE {
275						continue;
276					};
277
278					let cookie =
279						ctx.conn
280							.get_property(false, ctx.win_id, property, AtomEnum::ATOM, 0, 0)?;
281
282					let length = cookie.reply()?.bytes_after;
283
284					let cookie = ctx.conn.get_property(
285						true,
286						ctx.win_id,
287						property,
288						AtomEnum::NONE,
289						0,
290						length,
291					)?;
292					let reply = cookie.reply()?;
293					if reply.type_ != target {
294						continue;
295					};
296
297					let value = reply.value;
298
299					if !value.is_empty() {
300						buff.extend_from_slice(&value);
301					} else {
302						break;
303					}
304				}
305				_ => (),
306			}
307		}
308		Ok(())
309	}
310}
311
312impl ClipboardContext {
313	pub fn new() -> Result<Self> {
314		Self::new_with_options(ClipboardContextX11Options {
315			read_timeout: Some(Duration::from_millis(DEFAULT_READ_TIMEOUT)),
316		})
317	}
318
319	pub fn new_with_options(options: ClipboardContextX11Options) -> Result<Self> {
320		// build connection to X server
321		let ctx = InnerContext::new()?;
322		let ctx_arc = Arc::new(ctx);
323		let ctx_clone = ctx_arc.clone();
324
325		thread::spawn(move || {
326			let res = process_server_req(&ctx_clone);
327			if let Err(e) = res {
328				println!("process_server_req error: {e:?}");
329			}
330		});
331
332		Ok(Self {
333			inner: ctx_arc,
334			read_timeout: options.read_timeout,
335		})
336	}
337
338	fn read(&self, format: &Atom) -> Result<Vec<u8>> {
339		let ctx = &self.inner.server;
340		let atoms = ctx.atoms;
341		let clipboard = atoms.CLIPBOARD;
342		let win_id = ctx.win_id;
343		let cookie =
344			ctx.conn
345				.convert_selection(win_id, clipboard, *format, atoms.PROPERTY, CURRENT_TIME)?;
346		let sequence_num = cookie.sequence_number();
347		cookie.check()?;
348		let mut buff = Vec::new();
349
350		self.inner.process_event(
351			&mut buff,
352			clipboard,
353			*format,
354			atoms.PROPERTY,
355			self.read_timeout,
356			sequence_num,
357		)?;
358
359		ctx.conn.delete_property(win_id, atoms.PROPERTY)?.check()?;
360
361		Ok(buff)
362	}
363
364	fn write(&self, data: Vec<ClipboardData>) -> Result<()> {
365		let writer = self.inner.wait_write_data.write();
366		match writer {
367			Ok(mut writer) => {
368				writer.clear();
369				writer.extend(data);
370			}
371			Err(_) => return Err("Failed to write clipboard data".into()),
372		}
373		let ctx = &self.inner.server_for_write;
374		let atoms = ctx.atoms;
375
376		let win_id = ctx.win_id;
377		let clipboard = atoms.CLIPBOARD;
378		ctx.conn
379			.set_selection_owner(win_id, clipboard, CURRENT_TIME)?
380			.check()?;
381
382		if ctx
383			.conn
384			.get_selection_owner(clipboard)?
385			.reply()
386			.map(|reply| reply.owner == win_id)
387			.unwrap_or(false)
388		{
389			Ok(())
390		} else {
391			Err("Failed to take ownership of the clipboard".into())
392		}
393	}
394}
395
396fn process_server_req(context: &InnerContext) -> Result<()> {
397	let atoms = context.server_for_write.atoms;
398	loop {
399		match context
400			.server_for_write
401			.conn
402			.wait_for_event()
403			.map_err(|e| format!("wait_for_event error: {e:?}"))?
404		{
405			Event::DestroyNotify(_) => {
406				// This window is being destroyed.
407				println!("Clipboard server window is being destroyed x_x");
408				break;
409			}
410			Event::SelectionClear(event) => {
411				// Someone else has new content in the clipboard, so it is
412				// notifying us that we should delete our data now.
413				println!("Somebody else owns the clipboard now");
414				if event.selection == atoms.CLIPBOARD {
415					// Clear the clipboard contents
416					context
417						.wait_write_data
418						.write()
419						.map(|mut writer| writer.clear())
420						.map_err(|e| format!("write clipboard data error: {e:?}"))?;
421				}
422			}
423			Event::SelectionRequest(event) => {
424				// Someone is requesting the clipboard content from us.
425				context
426					.handle_selection_request(event)
427					.map_err(|e| format!("handle_selection_request error: {e:?}"))?;
428			}
429			Event::SelectionNotify(event) => {
430				// We've requested the clipboard content and this is the answer.
431				// Considering that this thread is not responsible for reading
432				// clipboard contents, this must come from the clipboard manager
433				// signaling that the data was handed over successfully.
434				if event.selection != atoms.CLIPBOARD_MANAGER {
435					println!("Received a `SelectionNotify` from a selection other than the CLIPBOARD_MANAGER. This is unexpected in this thread.");
436					continue;
437				}
438			}
439			_event => {
440				// May be useful for debugging but nothing else really.
441				// trace!("Received unwanted event: {:?}", event);
442			}
443		}
444	}
445	Ok(())
446}
447
448impl Clipboard for ClipboardContext {
449	//https://source.chromium.org/chromium/chromium/src/+/main:ui/base/x/x11_clipboard_helper.cc;l=224;drc=4cc063ac39c4a0d1f6011421b259a9715bb16de1;bpv=0;bpt=1
450	fn available_formats(&self) -> Result<Vec<String>> {
451		let ctx = &self.inner.server;
452		let atoms = ctx.atoms;
453		self.read(&atoms.TARGETS).map(|data| {
454			let mut formats = Vec::new();
455			// 解析原子标识符列表
456			let atom_list: Vec<Atom> = parse_atom_list(&data);
457			for atom in atom_list {
458				if self.inner.ignore_formats.contains(&atom) {
459					continue;
460				}
461				let atom_name = ctx.get_atom_name(atom).unwrap_or("Unknown".to_string());
462				formats.push(atom_name);
463			}
464			formats
465		})
466	}
467
468	fn has(&self, format: crate::ContentFormat) -> bool {
469		let ctx = &self.inner.server;
470		let atoms = ctx.atoms;
471		let atom_list = self.read(&atoms.TARGETS).map(|data| parse_atom_list(&data));
472		match atom_list {
473			Ok(formats) => match format {
474				ContentFormat::Text => formats.contains(&atoms.UTF8_STRING),
475				ContentFormat::Rtf => formats.contains(&atoms.RTF),
476				ContentFormat::Html => formats.contains(&atoms.HTML),
477				#[cfg(feature = "image")]
478				ContentFormat::Image => formats.contains(&atoms.PNG_MIME),
479				ContentFormat::Files => formats.contains(&atoms.FILE_LIST),
480				ContentFormat::Other(format_name) => {
481					let atom = ctx.get_atom(format_name.as_str());
482					match atom {
483						Ok(atom) => formats.contains(&atom),
484						Err(_) => false,
485					}
486				}
487			},
488			Err(_) => false,
489		}
490	}
491
492	fn clear(&self) -> Result<()> {
493		self.write(vec![])
494	}
495
496	fn get_buffer(&self, format: &str) -> Result<Vec<u8>> {
497		let atom = self.inner.server.get_atom(format);
498		match atom {
499			Ok(atom) => self.read(&atom),
500			Err(_) => Err("Invalid format".into()),
501		}
502	}
503
504	fn get_text(&self) -> Result<String> {
505		let atoms = self.inner.server.atoms;
506		let text_data = self.read(&atoms.UTF8_STRING);
507		text_data.map_or_else(
508			|_| Ok("".to_string()),
509			|data| Ok(String::from_utf8_lossy(&data).to_string()),
510		)
511	}
512
513	fn get_rich_text(&self) -> Result<String> {
514		let atoms = self.inner.server.atoms;
515		let rtf_data = self.read(&atoms.RTF);
516		rtf_data.map_or_else(
517			|_| Ok("".to_string()),
518			|data| Ok(String::from_utf8_lossy(&data).to_string()),
519		)
520	}
521
522	fn get_html(&self) -> Result<String> {
523		let atoms = self.inner.server.atoms;
524		let html_data = self.read(&atoms.HTML);
525		html_data.map_or_else(
526			|_| Ok("".to_string()),
527			|data| Ok(String::from_utf8_lossy(&data).to_string()),
528		)
529	}
530
531	#[cfg(feature = "image")]
532	fn get_image(&self) -> Result<crate::RustImageData> {
533		let atoms = self.inner.server.atoms;
534		let image_bytes = self.read(&atoms.PNG_MIME);
535		match image_bytes {
536			Ok(bytes) => {
537				let image = RustImageData::from_bytes(&bytes);
538				match image {
539					Ok(image) => Ok(image),
540					Err(_) => Err("Invalid image data".into()),
541				}
542			}
543			Err(_) => Err("No image data found".into()),
544		}
545	}
546
547	fn get_files(&self) -> Result<Vec<String>> {
548		let atoms = self.inner.server.atoms;
549		let file_list_data = self.read(&atoms.FILE_LIST);
550		file_list_data.map_or_else(
551			|_| Ok(vec![]),
552			|data| {
553				let file_list_str = String::from_utf8_lossy(&data).to_string();
554				let mut list = Vec::new();
555				for line in file_list_str.lines() {
556					if !line.starts_with(FILE_PATH_PREFIX) {
557						continue;
558					}
559					list.push(line.to_string())
560				}
561				Ok(list)
562			},
563		)
564	}
565
566	fn get(&self, formats: &[ContentFormat]) -> Result<Vec<ClipboardContent>> {
567		let mut contents = Vec::new();
568		for format in formats {
569			match format {
570				ContentFormat::Text => match self.get_text() {
571					Ok(text) => contents.push(ClipboardContent::Text(text)),
572					Err(_) => continue,
573				},
574				ContentFormat::Rtf => match self.get_rich_text() {
575					Ok(rtf) => contents.push(ClipboardContent::Rtf(rtf)),
576					Err(_) => continue,
577				},
578				ContentFormat::Html => match self.get_html() {
579					Ok(html) => contents.push(ClipboardContent::Html(html)),
580					Err(_) => continue,
581				},
582				#[cfg(feature = "image")]
583				ContentFormat::Image => match self.get_image() {
584					Ok(image) => contents.push(ClipboardContent::Image(image)),
585					Err(_) => continue,
586				},
587				ContentFormat::Files => match self.get_files() {
588					Ok(files) => contents.push(ClipboardContent::Files(files)),
589					Err(_) => continue,
590				},
591				ContentFormat::Other(format_name) => match self.get_buffer(format_name) {
592					Ok(buffer) => {
593						contents.push(ClipboardContent::Other(format_name.clone(), buffer))
594					}
595					Err(_) => continue,
596				},
597			}
598		}
599		Ok(contents)
600	}
601
602	fn set_buffer(&self, format: &str, buffer: Vec<u8>) -> Result<()> {
603		let atom = self.inner.server_for_write.get_atom(format)?;
604		let data = ClipboardData {
605			format: atom,
606			data: buffer,
607		};
608		self.write(vec![data])
609	}
610
611	fn set_text(&self, text: String) -> Result<()> {
612		let atoms = self.inner.server_for_write.atoms;
613		let text_bytes = text.as_bytes().to_vec();
614
615		let data = ClipboardData {
616			format: atoms.UTF8_STRING,
617			data: text_bytes,
618		};
619		self.write(vec![data])
620	}
621
622	fn set_rich_text(&self, text: String) -> Result<()> {
623		let atoms = self.inner.server_for_write.atoms;
624		let text_bytes = text.as_bytes().to_vec();
625
626		let data = ClipboardData {
627			format: atoms.RTF,
628			data: text_bytes,
629		};
630		self.write(vec![data])
631	}
632
633	fn set_html(&self, html: String) -> Result<()> {
634		let atoms = self.inner.server_for_write.atoms;
635		let html_bytes = html.as_bytes().to_vec();
636
637		let data = ClipboardData {
638			format: atoms.HTML,
639			data: html_bytes,
640		};
641		self.write(vec![data])
642	}
643
644	#[cfg(feature = "image")]
645	fn set_image(&self, image: RustImageData) -> Result<()> {
646		let atoms = self.inner.server_for_write.atoms;
647		let image_png = image.to_png()?;
648		let data = ClipboardData {
649			format: atoms.PNG_MIME,
650			data: image_png.get_bytes().to_vec(),
651		};
652		self.write(vec![data])
653	}
654
655	fn set_files(&self, files: Vec<String>) -> Result<()> {
656		let atoms = self.inner.server_for_write.atoms;
657		let data = file_uri_list_to_clipboard_data(files, atoms);
658		self.write(data)
659	}
660
661	fn set(&self, contents: Vec<ClipboardContent>) -> Result<()> {
662		let mut data = Vec::new();
663		let atoms = self.inner.server_for_write.atoms;
664		for content in contents {
665			match content {
666				ClipboardContent::Text(text) => {
667					data.push(ClipboardData {
668						format: atoms.UTF8_STRING,
669						data: text.as_bytes().to_vec(),
670					});
671				}
672				ClipboardContent::Rtf(rtf) => {
673					data.push(ClipboardData {
674						format: atoms.RTF,
675						data: rtf.as_bytes().to_vec(),
676					});
677				}
678				ClipboardContent::Html(html) => {
679					data.push(ClipboardData {
680						format: atoms.HTML,
681						data: html.as_bytes().to_vec(),
682					});
683				}
684				#[cfg(feature = "image")]
685				ClipboardContent::Image(image) => {
686					let image_png = image.to_png()?;
687					data.push(ClipboardData {
688						format: atoms.PNG_MIME,
689						data: image_png.get_bytes().to_vec(),
690					});
691				}
692				ClipboardContent::Files(files) => {
693					let data_arr = file_uri_list_to_clipboard_data(files, atoms);
694					data.extend(data_arr);
695				}
696				ClipboardContent::Other(format_name, buffer) => {
697					let atom = self.inner.server_for_write.get_atom(&format_name)?;
698					data.push(ClipboardData {
699						format: atom,
700						data: buffer,
701					});
702				}
703			}
704		}
705		self.write(data)
706	}
707}
708
709pub struct ClipboardWatcherContext<T: ClipboardHandler> {
710	handlers: Vec<T>,
711	stop_signal: Sender<()>,
712	stop_receiver: Receiver<()>,
713}
714
715unsafe impl<T: ClipboardHandler> Send for ClipboardWatcherContext<T> {}
716
717impl<T: ClipboardHandler> ClipboardWatcherContext<T> {
718	pub fn new() -> Result<Self> {
719		let (tx, rx) = mpsc::channel();
720		Ok(Self {
721			handlers: Vec::new(),
722			stop_signal: tx,
723			stop_receiver: rx,
724		})
725	}
726}
727
728impl<T: ClipboardHandler> ClipboardWatcher<T> for ClipboardWatcherContext<T> {
729	fn add_handler(&mut self, f: T) -> &mut Self {
730		self.handlers.push(f);
731		self
732	}
733
734	fn start_watch(&mut self) {
735		let watch_server = XServerContext::new().expect("Failed to create X server context");
736		let screen = watch_server
737			.conn
738			.setup()
739			.roots
740			.get(watch_server._screen)
741			.expect("Failed to get screen");
742
743		xfixes::query_version(&watch_server.conn, 5, 0)
744			.expect("Failed to query version xfixes is not available");
745		let cookie = xfixes::select_selection_input(
746			&watch_server.conn,
747			screen.root,
748			watch_server.atoms.CLIPBOARD,
749			xfixes::SelectionEventMask::SET_SELECTION_OWNER
750				| xfixes::SelectionEventMask::SELECTION_CLIENT_CLOSE
751				| xfixes::SelectionEventMask::SELECTION_WINDOW_DESTROY,
752		)
753		.expect("Failed to select selection input");
754
755		cookie.check().unwrap();
756
757		loop {
758			if self
759				.stop_receiver
760				.recv_timeout(Duration::from_millis(500))
761				.is_ok()
762			{
763				break;
764			}
765			let event = match watch_server
766				.conn
767				.poll_for_event()
768				.expect("Failed to poll for event")
769			{
770				Some(event) => event,
771				None => {
772					continue;
773				}
774			};
775			if let Event::XfixesSelectionNotify(_) = event {
776				self.handlers
777					.iter_mut()
778					.for_each(|handler| handler.on_clipboard_change());
779			}
780		}
781	}
782
783	fn get_shutdown_channel(&self) -> WatcherShutdown {
784		WatcherShutdown {
785			sender: self.stop_signal.clone(),
786		}
787	}
788}
789
790pub struct WatcherShutdown {
791	sender: Sender<()>,
792}
793
794impl Drop for WatcherShutdown {
795	fn drop(&mut self) {
796		let _ = self.sender.send(());
797	}
798}
799
800struct XServerContext {
801	conn: RustConnection,
802	win_id: u32,
803	_screen: usize,
804	atoms: Atoms,
805}
806
807impl XServerContext {
808	fn new() -> Result<Self> {
809		let (conn, screen) = x11rb::connect(None)?;
810		let win_id = conn.generate_id()?;
811		{
812			let screen = conn.setup().roots.get(screen).unwrap();
813			conn.create_window(
814				COPY_DEPTH_FROM_PARENT,
815				win_id,
816				screen.root,
817				0,
818				0,
819				1,
820				1,
821				0,
822				WindowClass::INPUT_OUTPUT,
823				screen.root_visual,
824				&CreateWindowAux::new()
825					.event_mask(EventMask::STRUCTURE_NOTIFY | EventMask::PROPERTY_CHANGE),
826			)?
827			.check()?;
828		}
829		let atoms = Atoms::new(&conn)?.reply()?;
830		Ok(Self {
831			conn,
832			win_id,
833			_screen: screen,
834			atoms,
835		})
836	}
837
838	fn get_atom(&self, format: &str) -> Result<Atom> {
839		let cookie = self.conn.intern_atom(false, format.as_bytes())?;
840		Ok(cookie.reply()?.atom)
841	}
842
843	fn get_atom_name(&self, atom: Atom) -> Result<String> {
844		let cookie = self.conn.get_atom_name(atom)?;
845		Ok(String::from_utf8_lossy(&cookie.reply()?.name).to_string())
846	}
847}
848
849// 解析原子标识符列表
850fn parse_atom_list(data: &[u8]) -> Vec<Atom> {
851	data.chunks(4)
852		.map(|chunk| {
853			let mut bytes = [0u8; 4];
854			bytes.copy_from_slice(chunk);
855			u32::from_ne_bytes(bytes)
856		})
857		.collect()
858}
859
860fn file_uri_list_to_clipboard_data(file_list: Vec<String>, atoms: Atoms) -> Vec<ClipboardData> {
861	let uri_list: Vec<String> = file_list
862		.iter()
863		.map(|f| {
864			if f.starts_with(FILE_PATH_PREFIX) {
865				f.to_owned()
866			} else {
867				format!("{FILE_PATH_PREFIX}{f}")
868			}
869		})
870		.collect();
871	// 再构造一个 /home/xxx/xxx 这样的路径
872	let uri_str_list: Vec<String> = file_list
873		.iter()
874		.map(|f| {
875			if let Some(stripped) = f.strip_prefix(FILE_PATH_PREFIX) {
876				stripped.to_owned()
877			} else {
878				f.to_owned()
879			}
880		})
881		.collect();
882
883	let data_text_plain = uri_str_list.join("\r\n");
884	let data_text_utf8 = uri_str_list.join("\n");
885	let data_text_uri_list = uri_list.join("\r\n");
886	let data_gnome_copied_files = ["copy\n", uri_list.join("\n").as_str()].concat();
887
888	vec![
889		ClipboardData {
890			format: atoms.TEXT_MIME_UNKNOWN,
891			data: data_text_plain.as_bytes().to_vec(),
892		},
893		ClipboardData {
894			format: atoms.UTF8_MIME_0,
895			data: data_text_plain.as_bytes().to_vec(),
896		},
897		ClipboardData {
898			format: atoms.STRING,
899			data: data_text_utf8.as_bytes().to_vec(),
900		},
901		ClipboardData {
902			format: atoms.TEXT,
903			data: data_text_utf8.as_bytes().to_vec(),
904		},
905		ClipboardData {
906			format: atoms.UTF8_STRING,
907			data: data_text_utf8.as_bytes().to_vec(),
908		},
909		ClipboardData {
910			format: atoms.FILE_LIST,
911			data: data_text_uri_list.as_bytes().to_vec(),
912		},
913		ClipboardData {
914			format: atoms.GNOME_COPY_FILES,
915			data: data_gnome_copied_files.as_bytes().to_vec(),
916		},
917		ClipboardData {
918			format: atoms.NAUTILUS_FILE_LIST,
919			data: data_gnome_copied_files.as_bytes().to_vec(),
920		},
921	]
922}