clipboard_rs/platform/
x11.rs

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